불변 클래스
불변 클래스란 간단히 말해 그 인스턴스의 내부 값을 수정할 수 없는 클래스다. 불변 클래스는 가변 클래스보다 설계하고 구현하고 사용하기 쉬우며, 오류가 생길 여지도 적고 훨씬 안전하다.
불변 클래스를 만드는 규칙
- 객체의 상태를 변경하는 메서드를 제공하지 않는다.
- 클래스를 확장할 수 없도록 한다.
=> 상속을 막는 대표적인 방법은 클래스를 final로 선언하는 것이다.
=> 생성자를 모두 private으로 만들고 정적 팩터리를 제공하는 방법도 있다.
=> 다음 코드에서는 PhoneNumber를 상속하고 name필드를 바꿀 수 있는 변경 메서드를 정의함으로써 불변이 아닌 경우의 수를 제공하게 된다.
// 불변 클래스를 의도하고 만들었다고 가정하자.
public class PhoneNumber {
private short areaCode, prefix, lineNum;
public PhoneNumber(short areaCode, short prefix, short lineNum) {
this.areaCode = areaCode;
this.prefix = prefix;
this.lineNum = lineNum;
}
}
public class MyPhoneNumber extends PhoneNumber {
private String name;
public void setName(String name) {}
}
public class Client {
PhoneNumber myPhoneNumber = new MyPhoneNumber();
myPhoneNumber.setName("이름 바꾸면 불변이 깨진다.");
}
- 모든 필드를 final로 선언한다.
=> 쓸 수 있으면 최대한 사용하는 것이 좋다. (성능과 설계상의 이점이 있다.) - 모든 필드를 private으로 선언한다.
- 자신 외에는 내부의 가변 컴포넌트에 접근할 수 없도록 한다.
=> 클래스에 가변 객체를 참조하는 필드가 하나라도 있다면 클라이언트에서 그 객체의 참조를 얻을 수 없도록 해야 한다.
=> 접근자 메서드가 그 필드를 그대로 반환해서는 안 된다. 즉, 생성자, 접근자, readObject 메서드 모두에서 방어적 복사를 수행해야 한다.
public final class Person {
private final Address address;
public Person(Address address) {
this.address = address;
}
}
public class Address {
private String name;
private String code;
public String getName() { return name; }
public String getCode() { return code; }
public void setName(String name) { this.name = name; }
public void setCode(String code) { this.code = code; }
}
public class Client {
public static void main(String[] args) {
Address address = new Address();
address.setName("서산");
Person person = new Person(address);
Address address2 = person.getAddress();
address2.setName("서울"); // 값이 변경됨.
}
}
// 위의 문제를 해결하기 위해 방어적 복사를 사용해야 한다.
public final class Person {
public Address getAddress() {
Address copyAddress = new Address();
copyAddress.setName(address.getName());
return copyAddress;
}
}
불변 클래스를 만드는 방법
- 클래스를 final로 선언한다.
- 생성자를 모두 private으로 만들고 정적 팩터리를 제공한다.
=> 이 방법은 내부에서 하위 클래스를 만들어 리턴할 수 있는 유연함을 제공한다.
public class Complex {
private Complex() {}
// private 클래스 안에서는 내부 클래스 정의가 가능하다.
private static class MyComplex extends Complex {
...
}
public static Complex valueOf(double re, double im) {
return new Complex(re, im);
// return new MyComplex(); -> 장점
}
}
불변 클래스의 장점과 단점
장점
- 함수형 프로그래밍에 적합하다.(피연산자에 함수를 적용한 결과를 반환하지만 피연산자가 바뀌지는 않는다.)
public final class Complex {
...
public Complex plus(Complex c) {
return new Complex(re + c.re, im + c.im);
}
public Complex minus(Complex c) {
return new Complex(re - c.re, im - c.im);
}
}
- 불변 객체는 단순하며, 스레드 안전하다.
- 불변 객체 끼리는 내부 데이터를 공유할 수 있다.
BigInteger ten = BigInteger.TEN;
BigInteger minusTen = ten.negate(); // negate안에서 배열을 넘기지만 값이 바뀌지 않는다.
- 실패 원자성을 제공한다.
=> 원래의 데이터가 바뀌지 않는다.
단점
- 값이 다르다면 별도의 객체로 만들어야 한다.
=> 다단계 연산을 제공한다.
public final class Complex {
...
public Complex plusMinus(Complex c) {
//plus minus를 모두 연산 후 리턴.
return new Complex();
}
}
=> 가변 동반 클래스를 제공하여 대치할 수 있다.
String s = "zxcvsdsf";
StringBuilder builder = new StringBuilder(s); // String 가변 동반 클래스
참고
만약 불변 클래스를 의도했지만 상속을 막지 않은 경우 (잘못되었지만)에는 다음과 같이 사용할 수 있다.
val.getClass() == BigInteger.class ? val : new BigInteger(val.toByteArray());
만약에 lazy loading이 하고 싶은 값이라면 넣어두고 재사용하고 싶다면 final이 아닌 필드를 선언하고 다음과 같이 사용한다.
외부에 공개되는 것은 final을 하는 것이 좋고, 외부에 공개되지 않는 경우에만 사용하는 것이 좋다.
private volatile int hashCode;
@Override public int hashCode() {
if(this.hashCode != 0) {
return hashCode;
}
synchronized(this) {
... hashcode생성
}
return hashcode;
}
[final과 자바 메모리 모델]
JMM이론에 따르면 final 키워드를 붙이지 않으면 다음과 같은 코드를 실행했을 때 x와 y의 값이 할당됨을 보장 해주지 못할 수 있다.
public class Point {
private int x;
private int y;
public Point() {
this.x = 5;
this.y = 6;
}
}
하지만 final 키워드를 붙이면 값을 할당하고 사용할 수 있도록 강제하기 때문에 초기화가 보장이 된다.