아이템17. 변경 가능성을 최소화 하라.
핵심 정리 - 불변 클래스
- 불변 클래스는 가변 클래스보다 설계하고 구현하고 사용하기 쉬우며, 오류가 생길 여지도 적고 훨씬 안전하다.
- 불변 클래스를 만드는 다섯 가지 규칙
ㆍ객체의 상태를 변경하는 메서드를 제공하지 않는다.
▷ setter를 제공하지 않는다.
ㆍ클래스를 확장할 수 없도록 한다
▷ 상속을 할 수 없게 만든다. ( 1. final class 2. private contructor )
ㆍ모든 필드에 final로 선언한다.
ㆍ모든 필드를 private으로 선언한다.
▷ public이면 필드 참조를 할 수도 있기에 우리는 필드 참조를 원치않는다.
ㆍ자신 외에는 내부의 가변 컴포넌트에 접근할 수 없도록 한다.
▷ getter를 만들어 주지 않거나 방어적인 복사를 해서 사용
public final class Person {
private final Address address; // address가 가진 정보들 까지 final이 아니다.
public Person(Address address) {
this.address = address;
}
public Address getAddress() {
// return address;
Address copyOfAddress = new Address();
copyOfAddress.setCity(address.getCity());
copyOfAddress.setZipCode(address.getZipCode());
copyOfAddress.setStreet(address.getStreet());
return copyOfAddress; // Person의 정보가 바뀌지 않게 된다.
}
}
핵심 정리 - 불변 클래스의 장점과 단점
- 함수형 프로그래밍에 적합하다. ( 피연산자에 함수를 적용한 결과를 반환하지만 피연산자가 바뀌지는 않는다. )
- 불변 객체는 단순하다.
- 불변 객체는 근본적으로 스레드 안전하여 따로 동기화할 필요 없다.
- 불변 객체는 안심하고 공유할 수 있다. ( 상수, public static final )
- 불변 객체 끼리는 내부 데이터를 공유할 수 있다.
▷ ex) BigInteger의 negate
- 객체를 만들 때 불변 객체로 구성하면 이점이 많다.
▷ ex) Set - 컬렉션들은 구성요소들 까지 같아야 같다.
// 불변이라면
Set<Integer> numbers = Set.of(1,2,3);
// 불변이 아니라면
final Set<Point> points = new HashSet<>();
Point firstPoint = new Point(1, 2);
points.add(firstPoint);
firstPoint.x = 10;
- 실패 원자성을 제공한다. ( 아이템 76, p407 )
▷ 예외가 발생하더라도 파라미터나 필드가 바뀌지 않는다.
- 단점) 값이 다르다면 반드시 별도의 객체로 만들어야 한다.
ㆍ"다단계 연산"을 제공하거나, "가변 동반 클래스"를 제공하여 대처할 수 있다.
▷ 가변동반클래스 ex) String
public static void main(String[] args) {
String name = "whiteship";
StringBuilder nameBuilder = new StringBuilder(name);
nameBuilder.append("phm");
}
핵심 정리 - 불변 클래스 만들 때 고려할 것
- 상속을 막을 수 있는 또 다른 방법
ㆍprivate 또는 package-private 생성자 + 정적 팩토리
ㆍ확장이 가능하다. 다수의 package-private 구현 클래스를 만들 수 있다.
ㆍ정적 팩터리를 통해 여러 구현 클래스중 하나를 활용할 수 있는 유연성을 제공하고 객체 캐싱 기능으로 성능을 향상 시킬 수도 있다.
- 재정의가 가능한 클래스는 방어적인 복사를 사용해야 한다.
// BigInteger은 상속을 막아두지 않았기에 방어적인 복사를 사용
public static BigInteger safeInstance(BigInteger val) {
return val.getClass() == BigInteger.class ? val : new BigInteger(val.toByteArray());
}
- 모든 "외부에 공개하는" 필드가 final이어야 한다.
ㆍ계산 비용이 큰 값은 해당 값이 필요로 할 때 (나중에) 계산하여 final이 아닌 필드에 캐시해서 쓸 수도 있다.
완벽 공략 요약
- p105, 새로 생성된 불변 인스턴스를 동기화 없이 다른 스레드로 건네도 문제없이 동작 (JLS 17.5)
- p106, readObject 메서드 (아이템 88)에서 방어적 복사를 수행하라.
- p112, 불변 클래스의 내부에 가변 객체를 참조하는 필드가 있다면... (아이템 88)
- p113, java.util.concurrent 패키지의 CountDownLatch 클래스
완벽 공략 - final 과 자바 메모리 모델(JMM)
final을 사용하면 안전하게 초기화할 수 있다.
- JMM과 final을 완벽히 이해하려면 JLS 17.4와 JLS 17.5를 참고하라
- JMM
ㆍ자바 메모리 모델은 JVM의 메모리 구조가 아니다.
ㆍ적법한(legal) 프로그램을 실행 규칙
ㆍ메모리 모델이 허용하는 범위내에서 프로그램을 어떻게 실행하든 구현체(JMM)의 자유이다.
( 이 과정에서 실행 순서가 바뀔 수도 있다. )
- 어떤 인스턴스의 final 변수를 초기화 하기 전까지 해당 인스턴스를 참조하는 모든 스레드는 기다려야 한다.(freeze)
class FinalFieldExample {
final int x;
int y;
static FinalFieldExample f;
public FinalFieldExample() {
x = 3;
y = 4;
}
static void writer() {
f = new FinalFieldExample();
}
static void reader() {
if (f != null) {
int i = f.x; // guaranteed to see 3
int j = f.y; // could see 0
}
}
}
▷ 반드시 필드의 값이 설정되고 사용되길 바란다면 final 키워드를 추가한다.
완벽 공략 - CountDownLatch
java.util.concurrent 패키기
병행(concurrency) 프로그래밍에 유용하게 사용할 수 있는 유틸리티 묶음
- 병행(Concurrency)과 병렬(Parallelism)의 차이
- 병행은 여러 작업을 번갈아 가며 실행해 마치 동시에 여러 작업을 동시에 처리하듯 보이지만, 실제로는 한번에 오직 한 작업만 실행한다. CPU가 한개여도 가능하다.
- 병렬은 여러 작업을 동시에 처리한다. CPU가 여러개 있어야 가능하다.
- 자바의 concurrent 패키지는 병행 애플리케이션에 유용한 다양한 툴을 제공한다.
ㆍBlockingQueue, Callable, ConcurrentMap, Executor, ExecutorService, Future ...
CountDownLatch
다른 여러 스레드로 실행하는 여러 오퍼레이션이 마칠 때까지 기다릴 때 사용할 수 있는 유틸리티
- 초기회 할 때 숫자를 입력하고, await() 메서드를 사용해서 숫자가 0이 될 때까지 기다린다.
- 숫자를 셀 때는 countDown() 메서드를 사용한다.
- 재사용할 수 있는 인스턴스가 아니다. 숫자를 리셋해서 재사용하려면 CyclicBarrier를 사용해야 한다.
- 시작 또는 종료 신호로 사용할 수 있다.