값과 참조
java의 변수는 두 가지 유형으로 나뉜다.
- 기본 자료형 (Primitive type) : 값을 직접 저장 - int, double, boolean
- 참조 자료형 (Reference type) : 객체의 메모리 주소 저장 - class , array , interface
참조 변수는 선언만 하고 초기화 하지 않으면 null 값을 가진다. null은 참조 변수가 어떤 객체도 가리키지 않음을 나타낸다
JAVA는 가비지 컬렉션 ( Garbage Collection ) 을 통해 더 이상 참조되지 않는 객체를 자동으로 제거한다.
참조의 비교
참조 변수의 비교는 두가지 방식이 존재한다.
- == 연산자 : 두 참조가 같은 객체를 가리키는지를 비교
- .equals() 메서드 : 객체의 내용이 같은 지 비교 (일부 클래스에서 오버라이드 됨)
public class ReferenceComparison {
public static void main(String[] args) {
String str1 = new String("Hello");
String str2 = new String("Hello");
// 참조 비교
System.out.println(str1 == str2); // false (다른 객체)
// 내용 비교
System.out.println(str1.equals(str2)); // true (내용 동일)
}
}
깊은 복사와 얕은 복사
참조 변수 복사는 얕은 복사(Shallow Copy)를 기본으로 하지만, 필요에 따라 깊은 복사 (Deep Copy) 를 구현할 수 있다
얕은 복사
- 객체의 참조(주소)만 복사
- 복사된 변수는 원본 객체와 같은 메모리 주소를 가리키므로 하나의 객체 공유
- 원본 객체의 값을 변경하면 복사된 객체에도 영향을 미친다.
class ShallowExample {
int[] data;
ShallowExample(int[] data) {
this.data = data;
}
}
public class ShallowCopy {
public static void main(String[] args) {
int[] original = {1, 2, 3};
ShallowExample obj1 = new ShallowExample(original);
ShallowExample obj2 = new ShallowExample(obj1.data); // 얕은 복사
obj2.data[0] = 99; // 원본 데이터도 변경됨
System.out.println(obj1.data[0]); // 99
}
}
깊은 복사
- 객체의 실제 내용을 새롭게 복사하여 메모리에 저장한다.
- 원본 객체와 복사된 객체는 독립적이므로 한쪽을 변경해도 다른 쪽에 영향을 주지 않는다.
깊은 복사가 필요한 이유
- 객체를 독립적으로 사용하고 싶을 때 ( 원본 데이터를 유지하면서 복사본을 변경해야 하는 경우)
- 객체 내부에 참조형 필드(배열 ,리스트, 객체)가 있을 때 - 얕은 복사로는 내부 객체가 공유되므로 문제 발생
- 멀티스레드 환경에서 동시 접근을 방지하고 싶을 때 (객체가 공유되면 데이터 충돌 가능)
1. 생성자를 활용한 깊은 복사
class Address {
String city;
Address(String city) {
this.city = city;
}
}
class Person {
String name;
Address address;
// 생성자를 활용한 깊은 복사
Person(String name, Address address) {
this.name = name;
this.address = new Address(address.city); // 새로운 Address 객체 생성
}
}
public class DeepCopyExample1 {
public static void main(String[] args) {
Address addr1 = new Address("Seoul");
Person p1 = new Person("Alice", addr1);
Person p2 = new Person(p1.name, p1.address); // 깊은 복사
p2.address.city = "Busan"; // p2의 주소 변경
System.out.println(p1.address.city); // "Seoul" (p1은 영향 없음)
System.out.println(p2.address.city); // "Busan"
}
}
2. clone() 메서드를 활용한 깊은 복사
class Address implements Cloneable {
String city;
Address(String city) {
this.city = city;
}
@Override
protected Object clone() throws CloneNotSupportedException {
return new Address(this.city);
}
}
class Person implements Cloneable {
String name;
Address address;
Person(String name, Address address) {
this.name = name;
this.address = address;
}
@Override
protected Object clone() throws CloneNotSupportedException {
Person cloned = (Person) super.clone();
cloned.address = (Address) this.address.clone(); // 참조 필드도 복사
return cloned;
}
}
public class DeepCopyExample2 {
public static void main(String[] args) throws CloneNotSupportedException {
Address addr1 = new Address("Seoul");
Person p1 = new Person("Alice", addr1);
Person p2 = (Person) p1.clone(); // 깊은 복사
p2.address.city = "Busan"; // p2의 주소 변경
System.out.println(p1.address.city); // "Seoul" (p1은 영향 없음)
System.out.println(p2.address.city); // "Busan"
}
}
미리 생각해야할 점
- Cloneable 인터페이스는 JAVA가 미리 정의한 인터페이스로 java.lang 패키지에 존재한다.
- 따라서 직접 interface Cloneable()을 정의할 필요 없이 클래스에서 implements Cloneable 사용 가능
- 마찬가지로 clone() 메서드는 JAVA에서 기본적으로 제공
- Object클래스도 JAVA에서 기본적으로 제공하는 기능으로 모든 JAVA 클래스의 최상위 부모 클래스이다. 즉, 모든 클ㄹ래스는 Object를 자동으로 상속받고 있다.
너무 복잡하니까 하나씩 살펴보자
class Address implements Cloneable { //address 라는 클래스 정의 및 Cloneable 인터페이스 구현
String city;
public Address(String city) { // 생성자로 city의 값을 받아 초기화
this.city = city;
}
@Override // clone() 메서드를 오버라이딩하여 이 객체를 복제할 수 있도록
protected Object clone() throws CloneNotSupportedException {
return super.clone(); // super.clone()을 호출하여 현재 객체의 복사본을 생성하여 반환
}
}
class Person implements Cloneable { //person 클래스 정의 및 Cloneable 인터페이스 구현
String name;
Address address; // 참조 타입 (Address 객체를 가리킴)
public Person(String name, Address address) { // 생성자로 name과 address를 받아서 초기화
this.name = name;
this.address = address;
}
@Override
protected Object clone() throws CloneNotSupportedException { // 공식이라 생각하면 쉬울듯
//Person 객체를 복제하는 clone 메서드 구현
Person cloned = (Person) super.clone(); // 얕은 복사한 후 (Person)으로 형변환
cloned.address = (Address) address.clone();
// address는 원본과 같은 객체를 참조하게 되므로 추가로 깊은 복사 수행 + Address로형 변환
return cloned;
}
}
public class Main {
public static void main(String[] args) throws CloneNotSupportedException {
Address address1 = new Address("Seoul"); //seoul 을 가지는 Address 객체 생성
Person person1 = new Person("Alice", address1); // Alice 와 address1을 가지는 Person 객체 생성
Person person2 = (Person) person1.clone(); // 깊은 복사 수행
person2.name = "Bob";
person2.address.city = "Busan"; // 복사본의 내부 객체 변경
System.out.println(person1.name + " - " + person1.address.city); // Alice - Seoul 유지
System.out.println(person2.name + " - " + person2.address.city); // Bob - Busan 변경
}
}
3. 직렬화를 이용한 깊은 복사
직렬화를 이용한 깊은 복사는 객체를 직렬화하여 바이트 스트림으로 변환한 후, 다시 역직렬화하여 새로운 객체를 만드는 방식이다.
- 직렬화
직렬화는 객체를 바이트 스트림으로 변환하는 과정으로 자바에서 직렬화를 하려면 객체가 Serializable 인터페이스를 구현해야 한다.
import java.io.Serializable;
class Address implements Serializable {
String city;
public Address(String city) {
this.city = city;
}
}
class Person implements Serializable {
String name;
Address address; // 참조 타입
public Person(String name, Address address) {
this.name = name;
this.address = address;
}
}
- 직렬화와 역직렬화를 이용한 깊은 복사
public Person deepCopy() throws IOException, ClassNotFoundException {
// 객체를 직렬화하여 바이트 스트림으로 변환
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
objectOutputStream.writeObject(this);
objectOutputStream.flush();
// 바이트 스트림을 다시 객체로 역직렬화하여 새로운 객체 생성
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(byteArrayOutputStream.toByteArray());
ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
return (Person) objectInputStream.readObject(); // 깊은 복사된 객체 반환
}

