불변 클래스란 인스턴스의 내부 값을 수정할 수 없는 클래스를 말한다. 불변 인스턴스에 간직된 정보는 고정되어 객체가 파괴되는 순간까지 절대 달라지지 않는다.
Java 플랫폼 라이브러리에도 대표적으로 String, 기본타입 박싱클래스, BigInteger, BigDecimal 등이 불변클래스이다.
public final class complex {
private final double re;
private final double im;
public Complex(double re, double im) {
this.re = re;
this.im = im;
}
public double realPart() { return re; }
public double imaginaryPart() { return im; }
public Complex plus(Complex c) {
return new Complex(re + c.re, im + c.im);
}
public Complex minus(Complex c) {
...
}
public Complex times(Complex c) {
....
}
public Complex dividedBy(Complex c){
...
}
@Override public boolean equals(Object o){
...
}
@Override public int hashCode() {
...
}
@Override public String toString() {
...
}
}
위의 코드에서 사칙연산 메서드들이 인스턴스 자신은 수정하지 않고 새로운 Complex 인스턴스를 만들어 반환하는 모습에 주목하자.
이렇게 피연산자에 함수를 적용해 그 결과는 반환하지만, 피연산자 자체는 그대로인 프로그래밍 패턴은 보통 함수형 프로그래밍에서 많이 나타난다. 이 방식으로 프로그래밍하면 코드에서 불변이 되는 영역의 비율이 높아지는 장점을 누릴 수 있어 좋다.
불변클래스의 단점으로 한 클래스에 다양한 값들이 존재한다면 이들을 매번 모두 만들어야하고, 그만큼 비용이 소모된다는 점이다.
예를들어 백만비트짜리 BigInteger에서 비트 하나를 바꿔야 한다고 해보자.
BigInteger moby = ...;
moby = moby.flipBit(0);
flipBit
메서드는 딱 한 비트만 다른 새로운 BigInteger 인스턴스를 만들게 되는데 이는 BigInteger 크기에 비례해 O(N)만큼의 시간과 공간을 잡아먹는다.
이와 달리 BitSet은 BigInteger처럼 임의 길이의 비트 순열을 표현하지만, BigInteger와는 달리 가변이라, 원하는 비트 하나만 상수시간안에 바꿔주는 메서드를 제공한다.
BitSet moby = ,,,;
moby.flip(0);
이 말고도 원하는 객체를 완성하기까지의 단계가 많고, 중간 단계에서 만들어진 객체들이 모두 버려진다면 성능 문제가 더 불거지곤 한다. 이 문제에 대해 어떻게 대처해야할까
Java에서는 대표적으로 String <-> StringBuilder 처럼 복잡한 연산들을 상수시간내에 연산이 가능하도록 바꾸는 클래스가 있다.
final 클래스로 선언하지만, 더 유연한 방법으로는
모든 생성자를 private(package-private)으로 만들고 public 정적 팩터리를 제공하는 방법
public class Complex {
private final double re;
private final double im;
private Complex(double re, double im) {
this.re = re;
this.im = im;
}
public static Complex valueOf(double re, double im) {
return new Complex(re, im);
}
}
바깥에서 볼 수 없는 package-private 구현 클래스를 원하는 만큼 만들어 활용할 수 있으니 훨씬 유연하다..?
public 이나 protected 생성자가 없으니 다른 패키지에서는 이 클래스를 확장하는게 불가능하기 때문에 사실상 이 불변객체는 final이다.