불변 객체란 말 그대로 변경이 불가능한 객체이다.
객체를 생성 후 외부에 의해 그 상태를 바꿀 수 없다.
여기서 바꿀 수 없다는 것은 heap 영역에서 그 객체가 가리키고 있는
데이터 자체의 변화가 불가능 함을 의미하며 stack에 있는 주소값을 다른 주소값을
가리키도록 변경하는 것은 문제 없다.
간단하게 말해 원시타입(int, boolean 등)은 그대로 stack 영역에 올라가지만,
참조변수를 가지고 있는 타입(Object, Array 등)은 그 실제 데이터들은 heap 영역에 저장하고 이 주소값을 Stack 영역에 가지고 있다.
예시로 String name = "amazzi";
에서 name = "newwisdom";
으로
name이 가리키는 주소의 변경은 가능하다.
여기서 String은 불변 객체로 name의 값을 바꿔준 것처럼 보이지만 실제로는
String 객체에 "newwisdom"
을 인스턴스 변수로 가지고 있는 새로운 객체를 참조하고 있는 것이다.
즉 name이 처음에 참조하는 값이 변경되는게 아닌 아예 새로운 객체를 만들고
이를 name이 참조하고 있는 것이다.
근데 수업시간에서 제이슨이 말하기를 Oracle 공식 문서에는
"불변 객체는 여러분이 메모리를 걱정하는 것보다 훨씬 큰 이익을 가져옵니다."
이런 뉘앙스의 문장이 있다고 한다.
원시 타입에서의 불변은 쉽다.
원시 타입은 참조 값이 없기 때문에 값을 그대로 외부에 내보내도 내부 객체는 불변이다.
class Person {
private final int age;
private final int name;
public Person(int age, int name) {
this.age = age;
this.name = name;
}
}
인스턴스 변수들이 final
로 선언되었기 때문에 인스턴스 값의 변경이 불가능하다.
따라서 setter 메소드도 사용이 불가능하다.
이 객체의 인스턴스 변수들의 값을 변경하려면 새로운 인스턴스를 만드는 방법 뿐이다.
"원시 타입만 있는 경우 처럼 단순히 final
을 붙이고 setter 메소드만 안사용하면 되지 않아?"라는
질문에 대한 대답은 아래와 같은 예시를 보면 된다.
public class Car {
private final Position position;
public Car(final Position position) {
this.position = position;
}
public Position getPosition() {
return position;
}
}
public class Position {
private int value;
public Position(final int value) {
this.value = value;
}
public void setValue(final int value) {
this.value = value;
}
public int getValue() {
return value;
}
}
여기서 Car는 불변 객체인가?
아니다.
Car는 final을 사용하고, setter가 없지만 우리는 Car의 position을 변경할 수 있다.
public static void main(String[] args) {
Position position = new Position(1);
Car car = new Car(position);
System.out.println(car.getPosition().getValue());
// 1
car.getPosition().setValue(10);
System.out.println(car.getPosition().getValue());
// 10
}
불변 객체라고 생각했던 Car 내부의 참조변수 Position은 불변 객체가 아니었기 때문에
Car 또한 불변 객체가 될 수 없었다.
즉, 불변 객체의 참조 변수 또한 불변, 불변 객체여야 한다.
위 예제에서 참조 변수인 Position도 불변 객체로 만든다.
public class Car {
private final Position position;
public Car(final Position position) {
this.position = position;
}
public Position getPosition() {
return position;
}
}
public class Position {
private final int value;
public Position(final int value) {
this.value = value;
}
public void setValue(final int value) {
this.value = value;
}
public int getValue() {
return value;
}
}
참조변수가 List일 경우는 List에 담고 있는 객체가 불변 객체여도 주의해야한다.
이 경우는 다음과 같은 조건을 만족 시켜야 한다.
생성될 때 인자로 넘어온 List를 외부에서 변경하면 List를 가진 객체의 내부 인스턴스 또한 변한다.
때문에 생성자를 통해 값을 전달 받을 때 new ArrayList<>()
를 통해
새로운 값을 참조하도록 방어적 복사를 도입해야 한다.
이러면 외부에서 넘겨주는 List와 객체 내부의 인스턴스 변수가 참조하는 값이
다르기 때문에 외부에서 제어가 불가능하다.
getter를 통해 List의 참조변수를 그대로 내보내게 되면
Collections
의 API를 통해 이 값을 추가/삭제할 수 있다.
때문에 getter 메소드 구현 시 이를 통해 add/remove가 불가능하도록
List의 값 변경을 막는 Collectiions.unmodifiableList()
메소드를 사용해야 한다.
public class Positions {
private final List<Position> positions;
public Positions(final List<Position> positions) {
this.positions = new ArrayList<>(positions);
}
public Position getPosition() {
return Collections.unmodifiableList(position);
}
}