- 인스턴스의 내부 값을 수정할 수 없는 클래스
- 불변 인스턴스에 간직된 정보는 고정되어 객체가 파괴되는 순간까지 절대 달라지지 않음
- 객체의 상태를 변경하는 메서드를 제공하지 않는다.
- 클래스를 확장할 수 없도록 한다.
- final class 혹은 정적 팩터리 사용
- 모든 필드를 final로 선언한다.
- 모든 필드를 private으로 선언한다.
- 자신 외에는 내부의 가변 컴포넌트에 접근할 수 없도록 한다.
- 클래스에 가변 객체를 참조하는 필드가 하나라도 있다면 클라이언트에서 참조할 수 없도록 한다.
- 접근자 메서드가 해당 필드를 그대로 반환해서도 안 된다.
- 생성자, 접근자,
readObject
메서드에서 방어적 복사를 수행해야 한다.
private final class Complex {
private final double re; // real part
private final double im; // imaginary part
// ...
public Complex plus(Complex c) {
return new Complex(re + c.re, im + c.im);
}
- 인스턴스 자신을 수정하는 것이 아닌, 새로운 인스턴스를 만들어 반환하고 있다(함수형 프로그래밍).
- 함수형 프로그래밍: 피연산자에 함수를 적용해 그 결과를 반환하지만, 피연산자 자체는 그대로인 프로그래밍 패턴
- 절차형(명령형) 프로그래밍에서는 메서드에서 피연산자인 자신을 수정해 자신의 상태가 변한다.
add
라는 이름을 사용하지 않고plus
라는 이름을 사용했다. 이처럼 메서드가 객체의 값을 변경하지 않는다는 사실을 메서드명으로 강조할 수 있다.
- 생성된 시점의 상태를 파괴될 때까지 그대로 간직한다.
- 근본적으로 스레드 안전하여 따로 동기화할 필요가 없고, 따라서 안심하고 공유할 수 있다.
- 방어적 복사도 필요가 없다. 아무리 복사해도 원본과 똑같다.
- 따라서
clone
과 같은 메서드를 제공하지 않는 것이 좋다.
- 불변 객체끼리는 내부 데이터를 공유할 수 있다.
- 불변 객체는 그 자체로 실패 원자성(failure atomicity)을 제공한다.
- 실패 원자성: 호출된 메서드가 실패하더라도 해당 객체는 메서드 호출 전 상태를 유지해야 한다.
- 상태가 변하지를 않으니 잠깐이라도 불일치에 빠질 가능성이 없다.
- 값이 다르면 반드시 독립된 객체로 만들어야 하고, 이는 큰 비용으로 이어질 수 있다.
- 원하는 객체를 완성하기까지의 단계가 많고, 그 중간 단계에서 만들어진 객체들이 모두 버려진다면 성능 문제가 더 불거진다.
- 이를 개선하기 위해 가변 동반 클래스가 사용될 수 있다. ex)
String
과StringBuilder
-
자바의
BigInteger
와BigDecimal
은 재정의할 수 있도록 잘못 설계되었다. -
불변 인스턴스인 경우에만 보안이 유지된다면, 진짜로 해당 클래스의 인스턴스인지 확인해야 한다.
public static BigInteger safeInstance(BigInteger val) { return val.getClass() == BigInteger.class ? val : new BigInteger(val.toByteArray()); }
- 불변 클래스의 규칙: “모든 필드가 final이고 어떤 메서드도 그 객체를 수정할 수 없다.”
- 성능을 위해 다음과 같이 완화할 수 있다: “어떤 메서드도 객체의 상태 중 외부에 비치는 값을 변경할 수 없다.”
- 이처럼 완화된 규칙을 적용하여, 계산 비용이 큰 값을 필드에 캐시해놓는 전략이 가능하다.
- 클래스는 꼭 필요한 경우가 아니라면 불변이어야 한다.
- 단순한 값 객체는 항상 불변으로 만들고, 성능이 우려되는 복잡한 객체는 가변 동반 클래스를 고려하자.
- 불변으로 만들 수 없는 클래스라도 변경할 수 있는 부분을 최소화하자.
- 합당한 이유가 없다면, 모든 필드는
private final
이어야 한다. - 생성자는 불변식 설정이 모두 완료된, 초기화가 완벽히 끝난 상태의 객체를 생성해야 한다.