도메인: Java 규칙
어제의 입력에 최적화된 경로는 오늘의 입력이 도착하면 비최적화된다 [R:a-speculated-path-deoptimises-when-the-input-changes]
Aleksey Shipilev의 "JVM Anatomy Quarks", 프로파일 오염, 인라인 및 비최적화, HotSpot의 uncommon-trap 메커니즘에서 인용.
JIT은 코드를 컴파일하지 않습니다. 코드에 대한 추측을 컴파일하며, 그 추측이 더 이상 참이 아닌 순간에 그 추측은 결산됩니다.
HotSpot은 프로파일을 기반으로 컴파일합니다: 이 호출 사이트는 단 한 가지 수신자 타입만 보았으므로 인라인하고 보호합니다; 이 분기는 한 번도 실행되지 않았으므로 방출하지 않습니다; 이 필드는 한 번도 null이 아니었으므로 검사를 건너뜁니다; 이 클래스는 한 번도 로드되지 않았으므로 하위 클래스가 없다고 가정합니다. 각 가정에는 보호 장치가 있으며, 컴파일된 경로는 빠릅니다. 정확히 그 보호 장치가 저렴하고 본문이 없기 때문입니다. 보호 장치가 실패하면 프레임은 비최적화됩니다: uncommon trap이 발생하고, 인터프리터 프레임이 컴파일된 프레임에서 재구성되며, 실행은 인터프리터에서 계속되고, 새로운 프로파일로 재컴파일될 때까지 기다립니다.
타이밍이 이 규칙을 처리량 규칙이 아닌 지연 시간 규칙으로 만드는 요소입니다. 가정은 입력이 변경될 때 깨집니다 — 특이한 순서 타입, 오늘의 첫 예외, 새로운 장소의 메시지 형식, 여섯 번째 시간에 로드된 인터페이스의 두 번째 구현. 따라서 경로가 생성할 최악의 지연 시간은 가장 드문 이벤트에서 발생하며, 이는 일반적으로 가장 중요한 이벤트입니다. 평균적인 트래픽을 대표하는 부하 테스트는 이를 재현하지 않습니다.
또한 기본적으로 조용합니다. 아무것도 로깅되지 않고, 메트릭이 움직이지 않으며, 유일한 추적은 수집 일시 중지나 스케줄링 지연과 구별할 수 없는 지연 시간 이상값입니다. -XX:+PrintCompilation 및 -XX:+TraceDeoptimization은 무슨 일이 일어났는지 말해줍니다; JFR은 더 적은 오버헤드로 이벤트를 전달하므로 켜두는 것이 좋습니다.
따라서 일반적인 경우뿐만 아니라 특이한 경우로도 경로를 워밍업하세요 — 이것이 [R:a-measurement-matches-the-regime-it-reports]와 만나는 지점입니다: 일반 트래픽만으로 구축된 워밍업은 일반 트래픽이 확인하고 비정형 트래픽이 파괴하는 프로파일을 생성합니다. 핫 호출 사이트를 의도적으로 모노모픽으로 유지하세요; 핫 경로에서의 메가모픽 인터페이스 호출은 일회성 트랩이 아니라 영구적인 인라인 장벽입니다. 경로가 활성화되기 전에 경로에 필요한 클래스를 로드하세요. 그리고 핫 경로에 추가된 람다 또는 동적 호출 사이트를 다시 획득해야 하는 새로운 프로파일로 취급하세요.
실패 모드 확인: JIT이 이 경로에 대해 무엇을 가정했는가, 그리고 그 가정 중 하나가 처음으로 거짓이 되면 어떻게 되는가? 정직한 대답이 아무도 무엇이 가정되었는지 모른다면, 그것이 바로 발견입니다 — 가정은 읽을 수 있으며, 그것을 읽는 것이 작업입니다.
컬렉션 뷰는 다른 사람의 데이터를 보는 창이다 [R:a-view-is-not-a-copy]
데이터를 보유하고 있는지 아니면 데이터를 보는 창을 보유하고 있는지 알아야 합니다. 정적 타입은 두 경우 모두 List<T>이며 절대 알려주지 않습니다.
라이브러리는 대부분의 코드가 가정하는 것보다 훨씬 더 자주 뷰를 반환하며, 각각은 고유한 방식으로 놀라움을 줍니다:
- **
Arrays.asList(a)**는 배열에 대한 고정 크기 래퍼입니다.set은 배열에 기록됩니다;add와remove는UnsupportedOperationException을 던집니다.List.of(...)는 불변이고 독립적이며, 일반적으로 의도된 것입니다. - **
list.subList(from, to)**는 라이브 뷰입니다. 쓰기는 통과되고, 부모에 대한 구조적 변경은 서브리스트를 다음 사용 시ConcurrentModificationException을 던지게 만듭니다 — 서브리스트를 가져온 다음 부모를 지우는 일반적인 관용구를 포함합니다. - **
map.keySet(),values(),entrySet()**는 뷰입니다.keySet().remove(k)는 맵에서 항목을 제거합니다. 그것은 때때로 원하는 것이며 항상 의도적으로 행동할 가치가 있습니다. - **
Collections.unmodifiableList(l)**는 복사 없이 래핑합니다: 래퍼는 쓰기를 거부하고 원본은 여전히 작동하므로, 호출자로부터 방어할 뿐 당신으로부터 방어하지는 않습니다. 그것은 방어적 복사본이 아니며, 이것이[R:no-reference-to-internals-escapes]가 대신List.copyOf를 사용하는 이유입니다. - **
Stream.toList()**는 수정 불가능합니다; **collect(Collectors.toList())**는 그러한 보장을 제공하지 않으며 그럴 수도 있고 아닐 수도 있습니다. 둘 중 하나가 가변적이라고 의존하지 마십시오.
따라서 생성 시점에 어떤 것을 원하는지 명시하세요. 수신자가 독립적이어야 할 때는 List.copyOf, 공유가 의도이고 수명이 짧을 때는 뷰를 사용하세요. 필드에 저장된 뷰는 가장 주의 깊게 봐야 할 형태입니다: 그것을 만든 표현식보다 오래 지속되며, 백업 컬렉션은 이후 언제든지 그 아래에서 변경될 수 있습니다.
동일한 구분이 반복을 지배합니다. 컬렉션을 반복하는 동안 수정하면 단일 스레드에서도 ConcurrentModificationException이 발생합니다. 반복자도 뷰이기 때문입니다 — removeIf를 사용하거나, 복사본을 반복하거나, 제거 항목을 수집하여 나중에 적용하세요.
실패 모드 확인, 수신되거나 반환된 모든 컬렉션에 대해: 누군가 여기에 쓰면, 다른 무엇이 변경되는가? 팩토리 메서드의 Javadoc을 열지 않고 대답할 수 없다면, 복사하세요.
래퍼 타입은 escape analysis가 제거할 때까지 객체 비용이 든다 [R:a-wrapper-type-is-not-free-here]
여기서도 도메인 개념에 고유한 타입을 부여하세요 — AccountId가 있어야 할 곳에 전달될 수 없는 OrderId는 Java에서도 다른 곳에서만큼 가치가 있습니다. 하지만 비용 주장을 설계 주장과 함께 가져오지 마세요. 그 중 하나만 전달되기 때문입니다.
Rust에서 newtype은 컴파일 타임 구조입니다: struct OrderId(u64)는 런타임에 u64이며 컴파일러가 이를 증명합니다. Java에서 record OrderId(long value)는 객체입니다 — 헤더, 필드, 따라가야 할 참조, 그리고 그것을 가리키는 것과는 별개의 캐시 라인. JIT이 이를 제거할 수 있습니다: escape analysis와 scalar replacement는 증명 가능하게 컴파일 단위를 이스케이프하지 않는 래퍼를 평탄화하며, 이는 일반적인 코드의 많은 부분을 포함합니다. 그러나 그것이 다루지 못하는 것은 우리가 신경 쓰는 경우입니다 — 필드에 저장되고, 컬렉션에 넣어지고, JIT이 인라인을 거부한 메서드에 전달되거나, 배열에 있는 래퍼. 여기서 OrderId[]는 포인터 배열이고 long[]은 숫자 배열입니다.
따라서 규율은 값이 사는 위치에 따라 나뉩니다:
- API 경계, 그리고 정확성이 우려되는 곳에서는 래퍼를 사용하세요. 그것이 방지하는 인수 교환 버그는 JIT이 일반적으로 어차피 제거할 할당 가치가 있습니다.
- 핫 루프 내부, 큰 배열 내부, 또는 메시지당 구조 내부에서는 래퍼가 사라졌다고 가정하기 전에 측정하세요. 이것은
[R:verify-the-abstraction-compiled-away]가 최적화 프로그램의 결정이 동적인 언어에 도착하는 것입니다:-XX:+PrintInlining, JFR 할당 프로파일, 또는 escape analysis 덤프가 실제로 무슨 일이 일어났는지 말해주며, 답은 동일한 바이너리의 실행 간에 다를 수 있습니다. - 사라지지 않고 예산이 실제로 있는 곳에서는 프리미티브를 유지하고 비용이 들지 않는 곳에 안전성을 두세요 — 명명된 매개변수, 빌더, 시그니처가 잘못 호출될 수 없는 정적 팩토리.
곧 도래할 이 규칙의 버전은 기다리기보다는 알아둘 가치가 있습니다: Project Valhalla의 값 클래스는 최적화 프로그램의 재량 대신 명세에 의해 정확히 이 래퍼가 평탄화되도록 설계되었습니다. 코드가 그것이 사실인 JVM에서 실행될 때까지 비용은 동적이며 측정되어야 하고 가정되어서는 안 됩니다.
실패 모드 확인, 예산이 있는 경로에서 프리미티브를 래핑하기 전: 이 객체는 JIT이 끝난 후에도 여전히 존재하는가, 그리고 무엇이 나에게 말해주었는가? "그냥 래퍼일 뿐이다"는 Rust의 대답이며, 전이되지 않는 대답입니다.
리소스는 그것을 연 구조에 의해, 모든 경로에서 닫힌다 [R:close-what-you-open]
리소스는 try-with-resources 헤더에서 열고 다른 곳에서는 열지 마세요. 이 구조는 모든 종료 지점에서 역순으로 닫으며, 수동 finally가 할 수 없는 부분인 close도 실패할 때 원래 예외를 유지합니다.
try (var conn = pool.take(); var stmt = conn.prepare(sql)) {
return stmt.execute();
}
finally 블록이 도입하는 실패는 올바르게 보이기 때문에 명확히 언급할 가치가 있습니다. 본문이 예외를 던지고 close()도 예외를 던지면, finally의 예외가 본문의 예외를 대체합니다: 프로그램은 연결 닫기 실패를 보고하고 먼저 실패한 쿼리에 대해 아무 말도 하지 않습니다. 대신 try-with-resources는 close 실패를 억제하고 원래 예외에 첨부하므로 둘 다 살아남고 원인이 당신이 원했던 것입니다. 누출은 핸들을 잃게 합니다; 원인 손실은 조사를 잃게 합니다.
구조가 적용되지 않지만 규율은 여전히 적용되는 세 가지 경우:
- 수명이 블록이 아닌 필드인 리소스 — 풀, 클라이언트, 실행자. 소유 객체는
AutoCloseable이 되고 규율은 한 단계 위로 이동합니다; 거부해야 할 것은 열렸지만 특별히 누군가에 의해 닫히지 않는 필드입니다. - **
ExecutorService**는shutdown()만으로 닫히지 않습니다.shutdown()다음awaitTermination()다음shutdownNow()가 순서이며, 이것을 건너뛰면 데몬이 아닌 스레드가 JVM을 붙잡고 있게 됩니다. - 파일에 대한
Stream(Files.lines,Files.walk)은 핸들을 보유하며 반드시 닫혀야 합니다. 누출되는 유일한 스트림이며, 누출되지 않는 스트림과 정확히 똑같이 보입니다.
종료자나 Cleaner를 주요 메커니즘으로 사용하지 마세요. 그것들은 지정되지 않은 시간에 실행되거나 전혀 실행되지 않으며, 규율이 이미 실패한 경우를 포착하기 위해 존재합니다.
실패 모드 확인, 모든 리소스에 대해: 다음 줄이 예외를 던지면 어떤 구조가 이것을 닫는가? 대답이 메서드 아래쪽의 close() 호출이라면, 아무것도 닫지 않습니다.
클래스는 final이거나, 상속이 설계되고 문서화되어야 한다 [R:design-for-inheritance-or-forbid-it]
하위 클래스를 위해 설계하지 않았다면 클래스를 final로 만들고, 설계했다면 하위 클래스가 무엇을 재정의할 수 있고 무엇을 해서는 안 되는지 정확히 명시하세요.
상속은 보이는 것과 같은 재사용 메커니즘이 아닙니다. 하위 클래스는 상위 클래스의 메서드 중 어떤 것이 다른 것을 호출하는지에 의존합니다 — addAll이 add를 호출하는 것이 전형적인 예 — 그리고 이것은 상위 클래스가 패치 릴리스에서 변경할 권리가 있는 구현 세부 사항입니다. 따라서 작동하는 하위 클래스는 부모가 리팩토링될 때 두 작성자 모두 잘못한 것 없이 깨집니다. 컴포지션에는 이 속성이 없습니다: 래퍼는 공개 계약에만 의존하고 다른 것에는 의존하지 않습니다.
생성자 경우는 취약한 것보다 더 나쁩니다, 처음부터 고장 나 있습니다. 재정의 가능한 메서드를 호출하는 생성자는 하위 클래스 자신의 필드가 할당되기 전에 하위 클래스의 재정의를 실행하므로, 재정의는 final로 선언되고 초기화된 필드에서 null과 0을 봅니다. 아무것도 경고하지 않습니다. 이것은 [R:no-reference-to-internals-escapes]가 this 이스케이프라고 명명한 것과 동일한 공개 위험이며, 일반 메서드 디스패치처럼 보이는 경로를 통해 도달합니다 — 그리고 [R:publish-safely-or-not-at-all]이 스레드가 하나만 있을 때도 그것이 중요한 이유입니다.
따라서:
- 기본적으로
final클래스와 하위 클래스가 대체할 이유가 없는 모든 메서드에 대해. 봉인 ([R:seal-the-alternatives])은 하위 타입 집합이 닫혀 있고 알려진 더 강력한 형태입니다. - 생성자, 초기화 프로그램,
clone()또는readObject()에서 재정의 가능한 메서드를 절대 호출하지 마세요. 이것들은 다른 이름을 가진 생성자입니다. - 클래스가 상속을 위해 설계된 경우, 자체 사용을 문서화하세요: 어떤 메서드가 어떤 것을 호출하는지, 어떤 것이 재정의해도 안전한지, 하위 클래스가 무엇을 호출해야 하는지. 그 문서는 계약의 일부이며, 그 비용이 대부분의 클래스가 대신
final이어야 하는 정직한 이유입니다. - 컴포지션을 선호하세요. 인스턴스를 보유하고 전달하는 래퍼는 작성하는 데 더 오래 걸리지만, 부모의 내부로부터 면역이며, 동기가 대체 가능성보다 재사용이었을 때 사용해야 할 것입니다.
실패 모드 확인, 모든 non-final 클래스에 대해: 내 메서드 중 어떤 것이 서로를 호출하는가, 그리고 그중 하나를 재정의하는 하위 클래스가 내가 그것들을 재정렬한 후에도 여전히 올바른가? 그것에 답할 수 없다면, 클래스는 우연히 확장 가능한 것입니다.
equals와 hashCode는 하나의 결정이며, 가변 키는 둘 다 깨뜨린다 [R:equality-is-one-contract]
동등성을 한 번 결정하고, 동일한 변경에서 두 반쪽을 모두 구현하며, 객체가 컬렉션에서 도달 가능한 동안 변경될 수 없는 필드에서 파생시키세요.
계약은 기계적입니다: 동등한 객체는 동등한 해시 코드를 가져야 합니다. 이를 깨면 HashMap이 잘못된 버킷을 찾게 되어, get은 포함된 키에 대해 null을 반환하고 remove는 제거할 항목을 찾을 수 없습니다 — 크기가 트래픽에 따라 증가하는 누출. 아무것도 이를 감지하지 않습니다. 컬렉션은 정확히 명시된 대로 동작하고 있습니다; 명세가 거짓말하는 객체를 받은 것입니다.
가변 키 부분은 리뷰를 살아남는 부분입니다. 클래스가 작성될 때는 올바르지만 누군가 setter를 추가할 때 잘못되기 때문입니다. hashCode가 나중에 변경되는 필드에 의존하는 객체는 맵에 알리지 않고 버킷을 이동했으며, 이제 그것이 제출된 바로 그 키를 통해 도달할 수 없습니다. 따라서 키로 사용되는 타입은 불변이거나, 키로 사용되지 않습니다. record는 컴포넌트와 final 필드로부터 생성된 두 반쪽을 모두 제공하므로, 값 타입의 기본 형태이며 이 두 메서드를 수동으로 작성하는 것이 결정처럼 느껴져야 하고 상용구처럼 느껴져서는 안 되는 이유입니다.
동일한 계약에서 따르며 각각 고유한 방식으로 잘못될 수 있기 때문에 언급할 가치가 있는 두 가지 더:
compareTo는equals와 일치해야 합니다 타입이TreeMap또는 정렬된 집합에 도달하는 모든 곳에서. 이들은 순서를 사용하고equals를 전혀 호출하지 않습니다. 비교기가 두 객체가 동등하다고 말하는 반면equals는 다르다고 말하면, 집합은 조용히 그중 하나만 보유합니다.- 상속과 동등성은 구성되지 않습니다.
instanceof로 작성된equals는 하위 클래스를 받아들이고 비대칭이 됩니다;getClass()로 작성된 것은 대칭이고 모든 하위 클래스를 거부합니다. 세 번째 옵션은 없으며, 이것이[R:design-for-inheritance-or-forbid-it]가final을 선호하는 이유 중 하나입니다.
실패 모드 확인, 타입이 키로 사용되거나 집합에 넣어지기 전: 그 동등성이 의존하는 필드는 무엇이며, 그 필드 중 어느 것이 객체 수명 동안 변경될 수 있는가? 하나가 변경될 수 있다면, 그 타입은 키가 아닙니다.
예외는 실패한 것을 말로 표현한다, 던진 곳을 말로 표현하지 않는다 [R:exceptions-name-what-failed]
메시지는 무엇이 예상되었는지와 무엇이 실제로 발견되었는지를 서술합니다. "오류 발생"은 재시도 논리를 작성하는 사람에게 아무 말도 하지 않습니다; "accountId가 필요하지만 null이 전달됨"은 말합니다. 스택 추적이 말해주지 않는 것, 즉 실패한 추상적 개념과 그것을 어떻게 해결할지에 대한 첫 번째 단서를 제공하는 것이 메시지의 임무입니다.
throw new IllegalArgumentException(
"orderId가 필요하지만 null이 전달됨: " + request);
세 가지 실용적인 지침:
- 타입을 사용하여 의미를 전달하세요.
IllegalArgumentException은 null 또는 범위를 벗어난 값에 대한 것입니다;IllegalStateException은 메서드가 호출되어서는 안 되는 시점에 호출된 것입니다;IOException은 실패한 I/O 작업입니다. 예외 타입은 오류 코드의 한 형태이며, 그에 따라 사용하세요. - 메시지에 값을 포함하세요 — 어떤 파라미터, 어떤 상태, 어떤 식별자. 민감할 수 있는 것(PII, 자격 증명)과 그렇지 않은 것의 균형을 유지하되, 포함하지 않으면 문제는 조사할 수 있게 되지 않고 단지 발생한 것으로만 남습니다.
- 체인 예외. 포착하여 다시 던질 때, 원래 예외를 원인으로 첨부하세요. 던지고 다시 던지는 계층은 원인이 깨지면 진단적 가치를 잃습니다.
실패 모드 확인, 모든 catch 블록에 대해: 누군가 이 메시지를 기반으로 버그를 어떻게 디버깅할 것인가, 아니면 디버깅할 수 없는가? 재시도해도 소용없는 경우에 관한 것이라면, catch 블록은 조용히 삼키지 않고 최소한 로깅해야 합니다.
박스의 경우, 동일성은 동등성이 아니다 [R:identity-is-not-equality-for-boxes]
==는 프리미티브에 대한 값을, 참조에 대한 객체 동일성을 비교합니다. 박스된 프리미티브는 둘 중 하나이며, 당신이 생각하는 것이 항상 명확하지는 않습니다.
컴파일러가 캐시된 값을 재사용하는 작은 값(일반적으로 -128에서 127까지의 Integer, 모든 Boolean 및 Byte)의 경우 ==는 예상대로 작동합니다. 큰 값은 캐시되지 않으므로 new Integer(200) == new Integer(200)은 객체 비교이며 거짓입니다. 따라서 127 이하에서는 작동하는 코드가 128에서 깨지며, 아무것도 변경되지 않았습니다.
이것이 Integer, Long, Short, Byte, Character, Boolean에 대해 == 대신 .equals()를 사용해야 하는 이유입니다. int와 long을 원시 값에 사용할 수 있다면, 그것이 더 낫습니다.
또한 switch에 대해 피상적인 함정이 있습니다: 이전 Java 버전에서는 Integer에 대한 switch가 널을 NullPointerException으로 언박싱합니다. 최신 Java는 널에 대한 일치를 추가했지만(Java 17+), 그렇지 않은 경우를 주의하세요.
실패 모드 확인: 이 ==가 참조인가 값 비교인가? 타입이 박스된 프리미티브라면, 값 비교를 의미했다면 .equals()로 변경하세요.
객체의 내부에 대한 참조는 유출되지 않는다 [R:no-reference-to-internals-escapes]
내부 컬렉션을 노출하지 말고, 보유한 객체에 대한 참조를 저장하지 말며, 생성자가 this를 이스케이프하도록 허용하지 마세요.
처음 두 가지는 방어적 복사에 관한 것입니다: 필드가 가변적이고 호출자 또는 수신자가 그 참조를 통해 변경할 수 있다면, 그 필드는 캡슐화되어 있지 않습니다. this 이스케이프는 생성자가 this를 다른 스레드나 리스너에 전달할 때, 즉 객체가 초기화되기 전에 발생합니다. 이것은 [R:publish-safely-or-not-at-all]에서 더 자세히 다루는 동시성 문제입니다.
// 좋지 않음: 방어적 복사 없이 내부 배열 유출
public Date[] getDates() { return dates; }
// 좋음: 방어적 복사
public Date[] getDates() { return dates.clone(); }
실패 모드 확인: 이 객체를 가변 상태를 노출하는 방법으로 사용할 수 있는가? 그렇다면, 캡슐화가 깨졌습니다.
null은 값이 아닙니다 [R:null-is-not-a-value]
null을 일반적인 반환 값으로 사용하지 말고, null 확인을 생략하지 말며, Optional을 null을 피하기 위해 사용하되 과도하게 사용하지 마세요.
null은 부재를 나타내지만 타입 시스템이 그것을 강제하지 않습니다. 모든 참조는 null일 수 있으며, 컴파일러는 당신이 확인했다고 가정합니다. Optional은 컴파일 타임에 부재를 명시적으로 만듭니다: Optional<T>는 null일 수도 있고 아닐 수도 있는 T가 아니라, 값이 있거나 없음을 나타냅니다. 반환 타입에 사용하되 필드나 메서드 파라미터에는 사용하지 마세요 (대신 오버로딩을 사용하거나 별도의 메서드를 만드세요).
실패 모드 확인: 이 메서드가 null을 반환할 수 있는가? 그렇다면, 호출자는 그것을 확인해야 하며, 호출자가 확인할 가능성이 낮으면 Optional을 반환하는 것이 더 안전합니다.
안전하게 공개하거나 전혀 공개하지 마세요 [R:publish-safely-or-not-at-all]
객체를 다른 스레드에 공개할 때, 객체가 완전히 생성될 때까지 다른 스레드가 그것을 볼 수 없도록 하세요. 불변 객체는 항상 안전하게 공개됩니다 (올바르게 생성되었다면); 가변 객체는 동기화 또는 volatile 또는 AtomicReference 또는 final 필드가 있는 올바르게 잠긴 액세스가 필요합니다.
핵심 원칙: 데이터 경합이 없어야 합니다. 스레드가 동기화 없이 변수를 읽고 쓰는 경우, 읽기는 절대 값이 아닌 오래된 값을 볼 수 있습니다.
실패 모드 확인: 이 객체가 여러 스레드에서 액세스되는가? 그렇다면, 공개가 안전합니까 (잠금, volatile, 불변 객체)? 그렇지 않다면, 데이터 경합이 있습니다.
대안을 봉인하세요 [R:seal-the-alternatives]
가능한 하위 타입의 완전한 집합을 알 때 sealed 클래스 또는 인터페이스를 사용하세요. 이것은 [R:design-for-inheritance-or-forbid-it]의 더 강력한 버전으로, 하위 클래스 집합이 닫혀 있고 알려져 있습니다.
public sealed interface PaymentMethod permits CreditCard, PayPal, BankTransfer { }
이것은 컴파일러가 모든 대안이 처리되었는지 확인할 수 있게 하여, switch에서 포괄적인 처리를 강제합니다. 또한 라이브러리 설계자에게 의도하지 않은 확장을 방지하는 방법을 제공합니다.
실패 모드 확인: 이 타입의 가능한 모든 하위 타입을 알고 있는가? 그렇다면, 봉인하는 것을 고려하세요.
Serializable은 두 번째 생성자입니다 [R:serializable-is-a-second-constructor]
Serializable을 구현하는 클래스는 역직렬화가 새로운 인스턴스를 생성하기 위한 또 다른 경로임을 인식해야 합니다. 기본 역직렬화는 생성자를 우회하여, 불변성과 캡슐화를 잠재적으로 깨뜨립니다. 따라서:
serialVersionUID를 명시적으로 선언하세요.readObject를 재정의하여 불변성과 캡슐화를 복원하세요. *final필드가 올바르게 복원되도록 보장하세요. *transient필드는 역직렬화되지 않으며, 기본값으로 초기화됩니다.
실패 모드 확인: 이 클래스가 직렬화 가능한가? 그렇다면, 역직렬화가 생성자를 우회한다는 사실을 설명했는가? 그렇지 않다면, 설계에 구멍이 있습니다.
모든 규칙은 "안전하게 공개하거나 전혀 공개하지 마세요"로 귀결됩니다 [R:all-rules-lead-to-publish-safely]
이 문서의 모든 규칙은 하나의 공통 주제를 가지고 있습니다: 객체가 다른 코드 (다른 스레드 또는 동일한 스레드에서 나중에)에 의해 올바르게 관찰될 수 있도록 하는 것. JIT 가정, 컬렉션 뷰, 래퍼 타입, 리소스 관리, 상속, 동등성, 예외, 박스, 내부 유출, null 처리, 그리고 봉인된 타입 — 이 모든 것은 어떤 형태로든 객체가 안전하게 공개되고 사용되도록 하는 것에 관한 것입니다.
실패 모드 확인, 모든 규칙에 대해: 이 코드가 언젠가 다른 코드에 의해 잘못 관찰될 수 있는가? 그렇다면, 이 문서의 관련 규칙을 적용했는가? 아니라면, 아마도 그래야 합니다.