Data Classes and Sealed Types for Java

Data Classes and Sealed Types for Java

이 문서는 Java 언어의 데이터 클래스 및 봉인 유형 에 대한 가능한 방향을 탐색 하고 Java의 데이터 클래스에 대한 업데이트 입니다.
이것은 탐색 적 문서 일 뿐이며 특정 버전의 Java 언어의 특정 기능에 대한 계획을 구성하지 않습니다.

Background

“자바가 너무 장황하다”거나 “행사”가 너무 많다는 것은 일반적인 (그리고 종종 마땅한) 불평입니다.
이에 대한 중요한 기여는 클래스가 다양한 프로그래밍 패러다임을 유연하게 모델링 할 수 있지만
항상 모델링 오버 헤드가 수반되며 “일반 데이터 캐리어”에 지나지 않는 클래스의 경우 이러한
모델링 오버 헤드가 그들의 가치와 일치합니다. 책임 간단한 데이터 캐리어 클래스를 작성하기 위해,
우리는 낮은 값을 많이 쓸 필요, 반복적 인 코드 : constructors, accessors, equals(), hashCode(),toString(), 등등.
그리고 개발자들은 때때로 이러한 중요한 메서드를 생략하여 놀라운 동작이나 불량한 디버깅 가능성으로 이어 지거나
“올바른 모양”을 갖고 있지 않기 때문에 대체이지만 완전히 적절하지 않은 클래스를 서비스에 밀어 넣는 등의
모퉁이를 깎고 싶은 유혹을 받습니다. 또 다른 클래스를 정의하고 싶습니다.

IDE는 이 코드의 대부분 을 작성하는 데 도움 이되지만 코드 작성은 문제의 작은 부분에 불과합니다.
이 되지 않는다 “나는 일반 데이터에 대한 캐리어이야 의 의도 디자인에서 알기까지 IDE는 코드를 읽는데 아무 도움이되지 않는다.(x, y및 z상용구 코드 수십 라인에서)”
그리고 반복적 인 코드는 버그를 숨길 수있는 좋은 장소입니다.
가능하다면 은신처를 완전히 제거하는 것이 가장 좋습니다.

“일반 데이터 매체”에 대한 공식적인 정의는 없으며 정확한 “일반”의 의미에 따라 의견이 다를 수 있습니다.
아무도 그것이 SocketInputStream일부 데이터의 전달 자라고 생각하지 않습니다.
이는 일부 복잡하고 지정되지 않은 상태 (네이티브 리소스 포함)를 완전히 캡슐화하고 내부 표현과 같지 않은 인터페이스 계약을 노출합니다.

1
2
3
4
5
6
7
8
9
10
11
12
final class Point {
public final int x;
public final int y;

public Point(int x, int y) {
this.x = x;
this.y = y;
}

// state-based implementations of equals, hashCode, toString
// nothing else

“단지”데이터 (x, y)입니다. 그것의 표현은 (x, y)이고, 그것의 구성 프로토콜은 (x, y)쌍을 받아들이고
그것을 표현에 직접 저장하고, 그 표현에 대한 중재되지 않은 액세스를 제공하고,
그 표현에서 Object직접 핵심 방법을 파생시킵니다.
그리고 중간에는 선을 그려야하는 회색 영역이 있습니다.

다른 OO 언어의 구문 데이터 중심의 클래스 모델링 형태 확인

이들은 클래스의 일부 또는 모든 상태를 클래스 헤더에 직접 설명 할 수 있다는 공통점이 있습니다.
하지만 의미론 (예 : 필드의 가변성 또는 액세스 가능성에 대한 제약 조건, 클래스 확장 성 및 기타 제한 사항)은 다양합니다. .)
클래스 선언에서 상태와 인터페이스 사이의 관계의 적어도 일부에 커밋하면
많은 공통 멤버에 대해 적절한 기본값이 파생 될 수 있습니다.
이러한 모든 메커니즘은 목표(“데이터 클래스”라고 부름)에 더 가깝게 정의 할수 있다.

Point 를 다음과 같이 :

1
record Point(int x, int y) { }

여기에 컴팩트은 확실히 매력적이다 - A는 Point두 개의 정수 구성 요소에 대해 단지 캐리어입니다
x그리고 y, 그에서, 독자가 바로 분별이 있다는 것을 알고 하고 정확한 핵심을위한 구현 Object방법,
그리고를 통해 웨이드 필요가 없습니다
의미론에 대해 자신있게 추론 할 수있는 상용구 페이지.
대부분의 개발자는 “물론 나는 그것을 원합니다 .” 라고 말할 것 입니다.
또한, 존재하는 유일한 코드가 실제로는 분명하지 않은 작업을 수행하는 코드 인 위치에 더 가까워 지므로
두 가지 방법으로 코드를 더 쉽게 읽을 수 있습니다. 읽을 것이 적고 모든 줄이 유용한 것을 말합니다.

Meet the elephant

안타깝게도 이러한 보편적 합의는 구문 깊이 일뿐입니다.
우리가 간결함을 축하 한 직후, 그러한 구조의 자연적 의미론과 우리가 기꺼이 수용 할 제한 사항에 대한 논쟁이 시작됩니다.
확장 가능합니까? 필드를 변경할 수 있습니까? 생성 된 메서드의 동작이나 필드의 액세스 가능성을 제어 할 수 있습니까?
추가 필드와 생성자를 가질 수 있습니까?

시각 장애인과 코끼리 이야기처럼 개발자는 데이터 클래스의 “명백한”의미에 대해 매우 다른 가정을 할 가능성이 높습니다.
이러한 암시 적 가정을 공개하기 위해 다양한 위치의 이름을 지정하겠습니다.

Algebraic Annie 는 “데이터 클래스는 대수적인 제품 유형일뿐입니다.”라고 말합니다.
Scala의 케이스 클래스와 마찬가지로 패턴 매칭과 함께 제공되며 변경 불가능한 서비스가 가장 좋습니다.
(디저트의 경우 Annie는 밀폐형 인터페이스를 주문합니다.)

Boilerplate Billy 는 “데이터 클래스는 더 나은 구문을 가진 평범한 클래스 일뿐”이라고 말하고,
변경 가능성, 확장 또는 캡슐화에 대한 제약 조건에 시달릴 것입니다.
(Billy의 동생 인 JavaBean Jerry는 “물론 JavaBeans를 대체 할 것입니다.
따라서 변경 가능성과 getter 및 setter가 필요합니다.”라고 말할 것입니다.
그의 여동생 인 POJO Patty는 그녀가 엔터프라이즈 POJO에 빠져들고 있다고 말했습니다.
Hibernate와 같은 프레임 워크에서 프록시 가능합니다.)

Tuple Tommy 는 “데이터 클래스는 명목상의 튜플 일뿐”이라고 말할 것입니다.
핵심 Object메서드가 아닌 다른 메서드가있을 것으로 기대하지 않을 수도 있습니다.
이는 가장 단순한 집계 일뿐입니다.
(그는 이름이 지워질 것으로 예상하여 동일한 “모양”의 두 데이터 클래스를 자유롭게 변환 할 수 있습니다.)

Values Victor 는 “데이터 클래스는 실제로 더 투명한 값 유형입니다.”라고 말합니다.

이러한 모든 페르소나는 “데이터 클래스”를 위해 통합되지만 데이터 클래스가 무엇인지에 대한
아이디어가 다르며 모두를 만족시키는 하나의 솔루션이 없을 수 있습니다.

캡슐화 및 경계(Encapsulation and boundaries)

우리가 매일 처리하는 상태 관련 상용구를 고통스럽게 알고 있지만 상용구는 더 깊은 문제의 증상 일뿐입니다.
즉, Java는 모든 클래스가 캡슐화 비용을 똑같이 지불하도록 요청합니다. 모든 클래스가 동등하게 혜택을받습니다.

확실히 캡슐화는 필수적입니다.
상태에 대한 액세스를 중재하고 (감독없이 조작 할 수 없도록)
표현을 캡슐화하면 (따라서 API 계약에 영향을주지 않고 진화 할 수 있음)
다양한 경계 에서 안전하고 견고하게 작동 할 수있는 코드를 작성할 수 있습니다.

  • 유지 관리 경계-고객이 다른 소스 기반 (또는 조직에있는 경우)
  • 보안 및 신뢰 경계-고객이 고의적으로 수정하거나 악의적 인 방식으로 사용하지 않도록 완전히 신뢰하지 않기 때문에 고객에게 상태를 노출하고 싶지 않습니다.
  • 무결성 경계-고객의 의도를 신뢰하고 데이터를 기꺼이 공유 할 수 있지만 우리 자신의 표현 불변성을 유지하는 작업에 부담을주지 않기 때문에 고객에게 우리 상태를 노출하고 싶지 않은 경우
  • 버전 관리 경계-라이브러리의 한 버전에 대해 컴파일 된 클라이언트가 후속 버전에 대해 실행될 때 계속 작동하는지 확인하려는 경우.

그러나 모든 클래스가 경계를 동등하게 평가하는 것은 아닙니다.
이러한 경계를 방어하는 것은 같은 클래스의 필수 KeyStore 또는 SocketInputStream 하지만,
같은 클래스에 대해 훨씬 덜 가치가 Point 또는 일반 도메인 클래스.
많은 클래스는 패키지 또는 모듈에 대해 비공개이고 클라이언트와 공동 컴파일되고
클라이언트를 신뢰하며 보호가 필요한 복잡한 불변성이없는 것과 같이 경계를 방어하는 데 전혀 관심이 없습니다.
이러한 경계를 설정하고 방어하는 데 드는 비용 (생성자 인수가 상태에 매핑되는 방법, 상태에서 평등 계약을 도출하는 방법 등)은
클래스간에 일정하지만 이점은 그렇지 않기 때문에 비용은 때때로 이점과 일치하지 않을 수 있습니다.
이것이 자바 개발자가 “너무 많은 의식”을 의미하는 것입니다. 의식이 가치가 없다는 것이 아니라 충분한 가치를 제공하지 않는 경우에도
호출해야합니다 .

Java가 제공하는 캡슐화 모델 (표현이 구성, 상태 액세스 및 동등성에서 완전히 분리 된 경우)은
많은 클래스가 필요로하는 것 이상입니다. 경계와 더 간단한 관계를 가진 클래스는 클래스를 상태를 둘러싼
얇은 래퍼로 정의하고 그로부터 상태, 구성, 동등성 및 상태 액세스 간의 관계를 도출 할 수있는
더 단순한 모델의 이점을 얻을 수 있습니다.

또한 API에서 표현을 분리하는 비용은 상용구 멤버를 선언하는 오버 헤드를 초과합니다.
캡슐화는 본질적으로 정보를 파괴합니다. 인수를 사용하는 생성자를 가진 클래스가 표시되는 경우 x, 및 라는 접근을 x(),
우리는 종종 그들은 아마도 같은 일을 참조라고 우리에게 이야기하는 유일한 규칙이있다.
이것에 의존하는 것은 꽤 안전한 추측 일 수 있지만 추측 일뿐입니다.
도구와 라이브러리 코드가이 대응에 기계적으로 의존 할 수 있다면 더 좋을 것입니다.
사람이 기대치를 확인하기 위해 사양을 읽을 필요가 없습니다 (만약 있다면!).

Digression – enums

문제가 우리가 지나치게 일반적인 것으로 단순한 것을 모델링하는 것이라면,
단순화는 제약에서 비롯 될 것입니다. 어느 정도의 자유를 놓음으로써
우리는 모든 것을 명시 적으로 지정할 의무가 없어지기를 바랍니다.

enum Java 5에 추가 된 이 기능은 그러한 절충의 훌륭한 예입니다.
유형이 안전한 열거 형 패턴은 Java 5 이전에 잘 이해되고 표현하기 쉬웠습니다
(장황 하긴하지만)( Effective Java, 1st Edition , 항목 21 참조).

1
2
3
4
5
6
7
8
9
10
public class Suit {
private final String name;
public Suit(String name) { this.name = name; }
public String toString() { return name; }
public static final Suit CLUBS = new Suit("clubs");
public static final Suit DIAMONDS = new Suit("diamonds");
public static final Suit HEARTS = new Suit("hearts");
public static final Suit SPADES = new Suit("spades");
}

언어에 열거 형을 추가하려는 초기 동기는 이 관용구에는 상용구가 필요하지만 실제 이점은 의미론적 입니다.

열거 형의 주요 단순화는 인스턴스의 수명주기를 제한하는 것이 었습니다.
열거 형 상수는 싱글 톤이고 인스턴스화는 런타임에 의해 관리됩니다.
싱글 톤 인식을 언어 모델에 적용함으로써 컴파일러는 유형이 안전한 열거 형 패턴에 필요한
상용구를 안전하고 정확하게 생성 할 수 있습니다. 그리고 열거 형은 구문 적 목표가 아닌 의미론적 목표로
시작 되었기 때문에 열거 형이 열거 형에 대한 기능과 같은 다른 기능과 긍정적으로 상호 작용 switch하거나 무료로 비교 및
안전한 직렬화를 얻을 수있었습니다.

놀랍게도 열거 형은 클래스가 즐기는 대부분의 다른 자유도를 포기할 필요없이 구문 및 의미 론적 이점을 제공했습니다.
Java의 열거 형은 다른 많은 언어에 있기 때문에 단순한 정수 열거 형이 아니라 일부 제한이있는 완전한 클래스입니다.

데이터 클래스를 사용하여이 접근 방식의 성공을 복제하려는 경우 첫 번째 질문은 다음과 같습니다.
어떤 제약이 우리가 원하는 의미와 구문상의 이점을 제공 할 것이며, 이러한 제약을 수용 할 의향이 있습니까?

우선 순위 및 목표(Priorities and goals)

데이터 클래스를 주로 상용구 축소에 관한 것으로 취급하는 것이 표면적으로 유혹적이지만, 의미론적 목표 인 데이터를
데이터로 모델링 하는 것을 선호합니다. 목표를 올바르게 선택하면 보일러 플레이트가
스스로를 처리하고 간결함 외에도 추가적인 이점을 얻을 수 있습니다.

그렇다면 “데이터를 데이터로 모델링”이란 무엇을 의미하고 무엇을 포기해야할까요?
클래스가 누리는 자유도는 이러한 “일반”데이터 집계가 필요하지 않아 제거하여 모델을 단순화 할 수 있습니까?
Java의 객체 모델은 객체의 표현이 API에서 완전히 분리되기를 원한다는 가정을 바탕으로 구축되었습니다.
생성자, 접근자 메서드 및 Object메서드 의 API 및 동작은 객체의 상태와 직접 정렬되거나 서로 정렬 될 필요가 없습니다.
그러나 실제로는 훨씬 더 밀접하게 연결되는 경우가 많습니다. Point 오브젝트 필드를 포함 x하고 y,
사용하는 생성자 x및 y그 필드를 초기화 액세스 용 x및 y 및 Objectx및 y값 으로 만 포인트를 특성화하는 방법.
우리는 클래스가 “데이터에 대한 단순한 캐리어”라고 주장하며,이 커플 링은 신뢰할 수있는 것 입니다.
즉, API에서 (공개적으로 선언 된) 상태를 분리하는 기능을 포기하는 것입니다.
데이터 클래스에 대한 API 는 상태, 전체 상태, 상태 만 모델링 합니다.
이것의 한 가지 결과는 데이터 클래스가 투명 하다는 것입니다. 모든 요청자에게 데이터를 자유롭게 제공합니다.
(그렇지 않으면 API가 전체 상태를 모델링하지 않습니다.)

이 커플 링을 믿을 수 있다는 것은 많은 이점을 가져옵니다.
표준 클래스 멤버에 대해 합리적이고 올바른 구현을 도출 할 수 있습니다.
클라이언트는 숨겨진 데이터를 버리거나 숨겨진 가정을 훼손 할 것이라는
두려움없이 집계를 자유롭게 해체 및 재구성하거나보다 편리한 형태로 재구성 할 수 있습니다.
프레임 워크는 복잡한 매핑 메커니즘을 제공 할 필요없이 프레임 워크를 안전하고 기계적으로 직렬화하거나 마샬링 할 수 있습니다.
API에서 클래스 상태를 분리하는 유연성을 포기함으로써 이러한 모든 이점을 얻을 수 있습니다.

Records and sealed types

우리는 레코드 형태로 데이터 클래스를 표면화 할 것을 제안 합니다. enum 처럼, a는 record클래스의 제한된 형태이다.
표현을 선언하고 해당 표현과 일치하는 API를 커밋합니다.
우리는 이것을 다른 추상화, 봉인 된 유형 과 짝을 이룹니다.
이것은 어떤 다른 유형이 그 하위 클래스가 될 수 있는지에 대한 제어를 주장 할 수 있습니다.
(final클래스는 봉인 된 클래스의 궁극적 인 형태이며 하위 유형을 전혀 허용하지 않습니다.)

레코드와 봉인 된 유형으로 간단한 산술 표현식을 모델링하려면 다음과 같이 보일 것입니다.

1
2
3
4
5
6
sealed interface Expr { }

record ConstantExpr(int i) implements Expr { }
record PlusExpr(Expr a, Expr b) implements Expr { }
record TimesExpr(Expr a, Expr b) implements Expr { }
record NegExpr(Expr e) implements Expr { }

이것은 네 가지 구체적인 유형 ConstantExpr(단일 정수 보유) 및
PlusExpr(두 개의 하위 표현식 NegExpr보유 )및 TimesExpr(하나의 하위 표현식 보유) 선언합니다.
또한 표현식에 대한 공통 상위 유형 Expr 을 선언하고 이들이 유일한 하위 유형 이라는 제약 조건을 포착합니다.

(final) fields, constructors, accessors, equals(), hashCode(), and toString()
(기본 구현이 적합하지 않은 경우 명시 적으로 지정할 수 있습니다.)

레코드는 상용구 대 정보 비율을 정렬하기 위해 열거 형과 동일한 전술을 사용합니다.
표준 멤버를 파생 할 수있는보다 일반적인 기능의 제한된 버전을 제공합니다.
열거 형은 사용자에게 인스턴스 제어를 런타임에 양도하도록 요청합니다. 뿐만 아니라 같은 핵심 동작의 구현을 제공하기 때문에
교환, 언어는, 인스턴스의 간소화 선언을 제공 할 수 있습니다 Object::equals, Enum::values및 직렬화.
기록을 위해 우리는 유사한 거래를합니다. 우리 는 매우 간소화 된 선언 (그리고 그 이상)을
얻는 대가로 클래스 API를 상태 설명에서 분리 하는 유연성을 포기합니다 .

Records and pattern matching

단순히 상용구 축소 클래스가 아닌 공개적으로 지정된 상태 설명에
API를 결합하는 측면에서 데이터 클래스를 정의 할 때의 큰 이점 중 하나는 집계간에
데이터 클래스 인스턴스를 자유롭게 변환 할 수 있다는 것입니다. 형태와 분해 상태.
이것은 패턴 매칭 과 자연스럽게 연결됩니다 . API를 상태 설명에 결합함으로써 우리는 또한 명백한 해체 패턴을 도출 할 수 있습니다.
이 패턴은 생성자의 이중 서명입니다.

Expr계층 구조가 주어지면 표현식을 평가하는 코드는 다음과 같습니다.

1
2
3
4
5
6
7
8
9
int eval(Expr e) {
return switch (e) {
case ConstantExpr(var i) -> i;
case PlusExpr(var a, var b) -> eval(a) + eval(b);
case TimesExpr(var a, var b) -> eval(a) * eval(b);
case NegExpr(var e) -> -eval(e);
// no default needed, Expr is sealed
}
}

기록이 자동으로 수집되는 기계적으로 생성 된 패턴 추출기를 사용합니다.
레코드와 봉인 된 유형 모두 패턴 일치와 시너지 효과가 있습니다.
레코드는 구성 요소로의 쉬운 분해를 허용하고 봉인 된 유형은 컴파일러에
완전한 정보를 제공하므로 모든 하위 유형을 다루는 스위치가 default절을 제공 할 필요가 없습니다 .

Records and externalization

데이터 클래스는 또한 안전하고 기계적 외부화 (직렬화, JSON 또는 XML에 대한 마샬링, 데이터베이스 행에 매핑 등)에 적합합니다.
클래스가 상태 벡터에 대한 투명한 캐리어이고 해당 상태 벡터의 구성 요소가 원하는 인코딩으로 외부화 될 수 있는 경우
캐리어는 보안 및 무결성 위험없이 보장 된 충실도로 안전하고 기계적으로 마샬링 및 언 마샬링 될 수 있습니다.
(내장 직렬화가 수행하는 것처럼) 생성자를 우회하는 것. 사실 투명 캐리어는 외부화를 지원하기 위해 특별한 조치를 취할 필요가 없습니다.
외부화 프레임 워크는 해체 패턴을 사용하여 객체를 해체하고 이미 공개 된 생성자를 사용하여 재구성 할 수 있습니다.

Why not “just” do tuples?

어떤 독자들은 분명히이 시점에서 생각할 것입니다. 만약 우리가 튜플을 “그냥”가지고 있다면 데이터 클래스가 필요하지 않을 것입니다.
튜플은 일부 집계를 표현하는 더 가벼운 수단을 제공 할 수 있지만 결과는 종종 열등한 집계입니다.

클래스와 클래스 멤버에는 의미있는 이름이 있습니다. 튜플과 튜플 구성 요소는 그렇지 않습니다.
Java 철학의 핵심은 이름이 중요 하다는 것입니다 . Person 속성 String firstName과 String lastName 은 명확하고의 튜플보다 안전 하다.
클래스는 생성자를 통해 상태 유효성 검사를 지원합니다. 튜플은 그렇지 않습니다.
일부 데이터 집계 (예 : 숫자 범위)에는 생성자가 적용하는 경우 이후에 신뢰할 수있는 불변성이 있습니다. 튜플은이 기능을 제공하지 않습니다.
클래스는 상태에서 파생 된 동작을 가질 수 있습니다. 상태와 파생 된 동작을 함께 배치하면 더 쉽게 검색하고 액세스 할 수 있습니다.

이러한 모든 이유로 우리는 데이터 모델링을 위해 클래스를 버리고 싶지 않습니다.
클래스를 사용하여 데이터 모델링을 더 간단하게 만들고 싶습니다. 집계에 대해 명명 된 클래스를 사용할 때의 가장 큰 고통은 선언의 오버 헤드입니다.
이것을 충분히 줄일 수 있다면 더 약한 유형의 메커니즘에 도달하려는 유혹이 크게 줄어 듭니다.
(레코드를 생각할 때 좋은 출발점은 그들이 명목상 튜플이라는 것 입니다.)

Are records the same as value types?

하여 값 유형 을 통해도 내려 가고 프로젝트 발할라를 , (불변), 데이터 종류 및 값 유형 간의 데이터 다움과 가치 네스의 교차점이
서식하는에 유용한 공간할지 여부 등의 오버랩에 대해 문의하는 것이 합리적이다.

레코드와 값 유형에는 몇 가지 명백한 유사점이 있습니다.
둘 다 불변의 집합체이며 확장에 제한이 있습니다. 이것은 disguise에서 정말 동일한 기능입니까?

의미 론적 목표를 살펴보면 서로 다르다는 것을 알 수 있습니다. 값 유형은 주로 메모리에서 개체의 평면 적이고 조밀 한 레이아웃을 활성화하는 것 입니다.
개체 정체성 을 포기하는 대가로(이는 가변성과 레이아웃 다형성을 포기하는 것을 수반 함) 런타임은 힙 레이아웃을
최적화하고 값에 대한 규칙을 호출하는 기능을 얻습니다. 레코드를 사용하면 클래스 API를 표현에서 분리하는
기능을 포기하는 대가로 여러 가지 표기법 및 의미 론적 이점을 얻습니다. 그러나 우리가 포기하는 것 중 일부는 동일하지만 (변경 가능성, 확장)
일부 값은 여전히 상태 캡슐화의 혜택을받을 수 있으며 일부 레코드는 여전히 신원의 혜택을받을 수 있으므로 정확히 동일한 거래가 아닙니다.
그러나 두 가지 이점을 모두 얻기 위해 두 가지 제한을 모두 허용 할 수있는 클래스가 있습니다 value records. 이를 .
따라서 우리는 반드시 하나의 메커니즘 만 갖고 싶지는 않지만 메커니즘이 함께 작동하기를 원합니다.

Digression: algebraic data types

데이터 클래스와 봉인 된 유형 의 조합은 제품 유형 과 합계 유형 의 조합을 나타내는 대수 데이터 유형 의 한 형태입니다 .

제품 유형은 값 세트가 유형 벡터의 값 세트에 대한 데카르트 곱이기 때문에 데카르트 곱 에서 이름을 가져옵니다 .
records많은 ad-hoc 도메인 클래스 (예 : PersonString 필드 firstName및 lastName.) 와
마찬가지로 튜플은 일종의 제품 유형 입니다. sum 유형은 고정 된 유형 집합의 구별 된 결합입니다.
열거 형은 일종의 공용체입니다 (조합 구성원이 상수 인 경우).

일부 언어는 대수 데이터 유형 선언을 직접 지원합니다.
예를 들어, Expr Haskell 의 계층 구조와 동일한 data구조를 다음 과 같이 선언합니다 .

1
2
3
4
5
6
7

data Expr = ConstantExpr Int
| PlusExpr Expr Expr
| TimesExpr Expr Expr
| NegExpr Expr
deriving (Show, Eq);

( deriving절은 이러한 유형이 Object::equals및 의 명백한 등가물을 자동으로 획득한다고 말합니다 Object::toString.)

Use cases

기록 및 기록의 봉인 된 계층에 대한 사용 사례가 많습니다. 몇 가지 일반적인 예는 다음과 같습니다.

  • 트리 노드. Expr앞 의 예제는 레코드가 문서, 쿼리 또는 표현식을 나타내는 것과 같은
    트리 노드의 짧은 작업을 수행하는 방법을 보여 주며 봉인을 통해 개발자와 컴파일러는 모든 사례가 언제 처리되었는지 추론 할 수 있습니다.
    트리 노드에 대한 패턴 일치는 방문자 패턴보다 순회에 대한보다 직접적이고 유연한 대안을 제공합니다.

  • 여러 반환 값. 효율성 (단일 패스에서 여러 수량을 추출하는 것이 두 패스를 만드는 것보다 더 효율적일 수 있음) 또는
    일관성 (변경 가능한 데이터 구조에서 작동하는 경우 두 번째 패스)을 이유로 메서드가 둘 이상의 것을 반환하는 것이 바람직합니다.
    다른 상태에서 작동 할 수 있습니다.

예를 들어, 배열의 최소값과 최대 값을 모두 추출한다고 가정 해보십시오.
두 개의 정수를 포함하도록 클래스를 선언하는 것은 과도하게 보일 수 있지만 선언 오버 헤드를 충분히 줄일 수 있다면
사용자 지정 제품 유형을 사용하여 이러한 관련 수량을 표현하여보다 효율적이고 읽기 쉬운 계산을 가능하게하는 것이 매력적이됩니다.

1
2
3
4
5

record MinMax(int min, int max);

public MinMax minmax(int[] elements) { ... }

앞서 언급했듯이 일부 사용자는 명목상의 메커니즘보다는 구조적 튜플을 통해이 기능을 노출하는 것을 선호 할 것입니다.
그러나 MinMax 유형을 합리적 으로 선언하는 비용을 줄인 후, 이점은 비용과 일치하기 시작합니다.
min및 같은 명목 구성 요소 max는 단순한 구조 튜플보다 더 읽기 쉽고 오류 발생 가능성이 적습니다.
A Pair<int,int>는 그것이 무엇을 나타내는 지 독자에게 말하지 않습니다. MinMax
(그리고 컴파일러는 둘 다 int 쌍으로 모델링 될 수 있지만 실수로 a MinMax를에 할당하는 것을 방지합니다 Range.)

  • 데이터 전송 개체. 데이터 전송 객체는 누구의 유일한 목적은 그들이 한 작업에서 다른 작업에 전달 될 수 있도록 관련 값을 패키징하는 것입니다 집계입니다.
    데이터 전송 개체는 일반적으로 상태 저장, 검색 및 마샬링 이외의 동작이 없습니다.

  • 스트림 작업에 참여합니다. 파생 수량이 있고 파생 수량에 대해 작동하는 스트림 작업 (필터링, 매핑, 정렬)을 수행하려고한다고 가정합니다.
    예를 들어, Person이름 (대문자로 정규화 됨)이 가장 큰 객체 를 선택한다고 가정 합니다 hashCode(). a record를 사용하여
    파생 수량 (또는 수량)을 임시로 첨부하고 작업 한 다음 다음과 같이 원하는 결과로 다시 투영 할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
List<Person> topThreePeople(List<Person> list) {
// local records are OK too!
record PersonX(Person p, int hash) {
PersonX(Person p) {
this(p, p.name().toUpperCase().hashCode());
}
}

return list.stream()
.map(PersonX::new)
.sorted(Comparator.comparingInt(PersonX::hash))
.limit(3)
.map(PersonX::person)
.collect(toList());
}

여기서는 Person파생 된 수량과 결합하여 시작하고 , 조합에 대해 일반적인 스트림 작업을 수행 할 수 있으며, 완료되면 래퍼를 버리고 Person.

추가 객체를 구체화하지 않고도이 작업을 수행 할 수 있었지만 잠재적으로 각 요소에 대해 해시 (및 대문자 문자열)를 여러 번 계산해야합니다.

  • 복합 맵 키. 때때로 우리 Map는 두 개의 별개 도메인 값의 결합에 키가 지정된 a 를 원합니다 .
    예를 들어, 주어진 사람이 주어진 장소에서 마지막으로 본 시간을 나타내고 싶다고 가정 해 봅시다.
    우리는 HashMap키가 Person와를 결합 Place하고 그 값이 LocalDateTime. 그러나 시스템에 PersonAndPlace유형 이없는 경우 생성, 같음,
    hashCode 등의 상용구 구현으로 하나를 작성해야합니다. 레코드가 원하는 생성자 equals(), 및 hashCode()메서드를
    자동으로 획득하므로 복합 맵 키로 사용할 준비가되었습니다.
1
2
3
4
5
6
record PersonPlace(Person person, Place place) { }
Map<PersonPlace, LocalDateTime> lastSeen = ...
...
LocalDateTime date = lastSeen.get(new PersonPlace(person, place));
...

  • 메시지. 레코드와 레코드의 합계는 일반적으로 행위자 기반 시스템 및 기타 메시지 지향 시스템 (예 : Kafka)에서 메시지를 나타내는 데 유용합니다.
    행위자가 교환하는 메시지는 제품별로 이상적으로 설명됩니다. 행위자가 일련의 메시지에 응답하는 경우 이는 제품 합계로 이상적으로 설명됩니다.
    또한 합계 유형 아래에 전체 메시지 세트를 정의 할 수 있으므로 메시징 API에 대한보다 효과적인 유형 검사가 가능합니다.

  • 값 래퍼. Optional클래스 변장 대수 데이터 유형; 대수적 데이터 유형 및 패턴 일치를 사용하는 언어에서는
    Optional일반적으로 a Some(T value)와 None유형 (구성 요소가없는 퇴화 제품) 의 합계로 정의됩니다.
    마찬가지로 Either<T,U>유형은 a Left와 Right유형 의 합으로 설명 할 수 있습니다.
    (Optional Java에 추가 되었을 당시에 는 대수 데이터 유형도 패턴 일치도 없었기 때문에보다 전통적인 API 관용구를 사용하여 노출하는 것이 합리적이었습니다.)

  • 차별적 개체. 보다 정교한 예는 식별 된 엔티티를 반환해야하는 API입니다.
    예를 들어, JEP 348에서 Java 컴파일러는 JDK API 호출이
    컴파일 시간에보다 효율적인 표현으로 변환 될 수있는 메커니즘으로 확장됩니다.
    여기에는 컴파일러와 “내장 프로세서”간의 대화가 포함됩니다. 컴파일러는 호출 사이트에 대한 정보를 프로세서에
    전달하여 호출을 변환하는 방법에 대한 설명을 반환합니다. 옵션은 다음과 같습니다.으로 변환 invokedynamic, 상수로 변환 또는 아무것도하지 않음.
    봉인 된 유형 및 레코드를 사용하는 API는 다음과 같습니다.

1
2
3
4
5
6
7
8
9
10
11
interface IntrinsicProcessor {

sealed interface Result {
record None() implements Result;
record Ldc(ConstantDesc constant) implements Result;
record Indy(DynamicCallSiteDesc site, Object[] args)
implements Result;
}

public Result tryIntrinsify(...);
}

이 모델에서 내장 프로세서는 호출 사이트에 대한 정보를 수신하고 None(변환 안 함),
Indy(호출을 지정된으로 대체 invokedynamic) 또는 Ldc(호출을 지정된 상수로 대체 )를 반환합니다 .
이 제품 합계는 다음과 같습니다. 선언하기 쉽고, 읽기 쉬우 며, 패턴 매칭으로 분해하기 쉽습니다.
이러한 메커니즘이 없으면 API를 읽기 어렵거나 오류가 발생하기 쉬운 방식으로 구성하려는 유혹을받을 수 있습니다.

Records

레코드는 클래스가 일반적으로 즐기는 주요 자유도, 즉 표현에서 클래스 API를 분리하는 기능을 포기합니다.
레코드에는 이름, 상태 설명 및 본문이 있습니다.

1
record Point(int x, int y) { }

레코드는 “상태, 전체 상태 및 상태”이기 때문에 대부분의 구성원을 기계적으로 추출 할 수 있습니다.

  • final상태 설명의 각 구성 요소에 대해 동일한 이름과 유형을 가진 개인 필드
  • 상태 설명의 각 구성 요소에 대해 동일한 이름과 유형을 가진 공용 읽기 접근 자 메서드
  • 해당 인수에서 각 필드를 초기화하는 상태 설명과 서명이 동일한 공용 생성자
  • 해당하는 바인딩 슬롯으로 각 필드를 추출하는 상태 설명과 시그니처가 동일한 공개 해체 패턴
  • 구현 equals및 hashCode동일한 유형의 그들과는 동일한 상태를 포함하는 경우라고 개의 레코드 같다
  • 구현 toString에는 이름과 함께 모든 구성 요소가 포함됩니다.

구성, 해체 (해체 자 패턴 또는 접근 자 또는 둘 다), 동등성 및 표시를위한 표현 및 프로토콜은 모두 동일한 상태 설명에서 파생됩니다.

Customizing records

열거 형과 같은 레코드는 클래스입니다. 레코드 선언에는 액세스 가능성 수정 자, Javadoc, 어노테이션, implements절 및 유형 변수
(레코드 자체가 내재적으로 최종적 임에도 불구하고 클래스 선언이 할 수있는 대부분의 것 )가있을 수 있습니다.
컴포넌트 선언에는 어노테이션 및 액세스 가능성 수정자가있을 수 있습니다 (컴포넌트 자체는 암시 적으로 private및 final).
본문에는 정적 필드, 정적 메서드, 정적 이니셜 라이저, 생성자, 인스턴스 메서드, 인스턴스 이니셜 라이저 및 중첩 유형이 포함될 수 있습니다.

컴파일러에서 암시 적으로 제공하는 모든 멤버의 경우 명시 적으로 선언 할 수도 있습니다.
(단, 함부로 접근을 무시하거나 equals/ hashCode기록의 의미 불변 훼손 될 위험.)

또한 기본 생성자 (시그니처가 레코드의 상태 설명과 일치하는 서명)를 명시 적으로 선언하기 위해 몇 가지 특별한 고려 사항이 제공됩니다.
인수 목록은 생략됩니다 (상태 설명과 동일하기 때문). 또한 모든 정상적인 완료 경로에서 확실히 할당되지 않은 레코드 필드는 종료시
해당 인수 ( this.x = x) 에서 암시 적으로 초기화됩니다 . 이렇게 하면 생성자 본문이 인수 유효성 검사 및 정규화 만 지정하고
명백한 필드 초기화를 생략 할 수 있습니다.

예를 들면 :

1
2
3
4
5
6
record Range(int lo, int hi) {
public Range {
if (lo > hi)
throw new IllegalArgumentException(String.format("(%d,%d)", lo, hi));
}
}

암시 적 분해 패턴은 필드에 대한 접근 자에서 파생되므로 이러한 접근자가 재정의되면 분해 의미론에도 반영됩니다.

중첩 된 컨텍스트에서 선언 된 레코드는 암시 적으로 정적입니다.

Odds and ends

위는 스케치 일뿐입니다. 해결해야 할 작은 세부 사항이 많이 있습니다.

  • Javadoc. 필드와 접근 자 메소드는 클래스 선언의 일부로 선언되었으므로 이를 수용하기 위해 Javadoc 규칙을 약간 조정해야합니다.
    이는 @param레코드에 태그를 허용하여 수행 할 수 있으며 , 이는 필드 및 접근 자 문서로 전파 될 수 있습니다.

  • Annotations 레코드 구성 요소는 주석을 넣을 새로운 장소를 구성합니다. @Target이를 반영하기 위해 메타 주석을 확장하고 싶을 것 입니다.

  • Reflection 기록이되는 것은 의미론적 진술이기 때문에 기록성, 그리고 상태 구성 요소의 이름과 유형은 반영 적으로 사용할 수 있어야합니다.
    enum추가 메서드 및 / 또는 사양이 존재할 수있는 레코드 ( 클래스에있는 것처럼)의 기본 유형을 고려할 수 있습니다.

  • Serialization 레코드의 장점 중 하나는 마샬링 및 마샬링 해제를위한보다 안전한 프로토콜을 기계적으로 도출 할 수 있다는 것입니다.
    이를 활용하는 현명한 방법은 악의적 인 스트림이 잘못된 데이터를 주입하는 것을 방지하기 위해 상태를 추출하고 생성자를 통해 다시 실행 Serializable하는
    readResolve 방법 을 자동으로 획득하는 레코드를 구현 하는 것입니다.

  • Extension 관련 레코드가 특정 구성원을 공유 할 수 있도록하는 추상 레코드를 확장하는 레코드를 허용 할 수 있습니다. 우리는이 가능성을 보류해야합니다.

  • Compatibility 구성 요소의 배열, 이름 및 유형이 멤버의 서명과 이름으로
    직접 전파되기 때문에 상태 설명에 대한 변경 사항은 소스 또는 이진 호환이되지 않을 수 있습니다.

  • Named invocation 구성 요소의 이름이 레코드 API의 일부를 형성하기 때문에 생성자의 명명 된 호출 에 대한 문이
    열리므로 대부분의 경우 도메인 클래스와 함께 사용 되는 동반 빌더 를 잊어 버릴 수 있습니다.
    (모든 클래스에 대해이를 지원하는 것이 좋지만 레코드에는이를 훨씬 덜 복잡하게 만드는 몇 가지 특수 속성이 있습니다.
    레코드로 시작하여 확장하는 것을 고려할 수 있습니다.)

Restrictions

주의 깊은 독자라면 몇 가지 제한 사항에 주목할 것입니다. 레코드 필드는 변경할 수 없습니다.
주 설명에있는 필드 이외의 필드는 허용되지 않습니다. 기록은 다른 유형을 확장하거나 확장 할 수 없습니다.

이러한 각각의 제한이 숨겨지는 상황을 상상하기 쉽고 적용 가능성을 확대하기 위해 구성을 더 유연하게 만들려고합니다.
그러나 우리는 단일 상태 설명에서 모든 것을 도출 할 수 있도록 처음부터 작동하게하는 조건을 희생하여 그렇게해서는 안됩니다.

  • Extension 우리의 진언이 레코드의 상태 설명이 “상태, 전체 상태, 상태 만”이라는 것이라면,이 안에 숨겨진 상태가 없다는 것을
    확신 할 수 없기 때문에 어떤 것도 확장하지 않습니다
    (추상 레코드 제외). 슈퍼 클래스. 마찬가지로 레코드를 확장 할 수있는 경우 해당 상태 설명은 다시 해당 상태에 대한 완전한 설명이 아닙니다.
    (일반 확장을 제외하는 것 외에도 동적 프록시도 제외됩니다.)

  • Mutability 이론상 목표에 어긋나지 않는 예를 상상할 수 있기 때문에 가변성에 대한 제한은 더 복잡합니다.
    그러나 변경 가능성은 상태와 API 간의 정렬에 압력을가합니다. 예를 들어,
    그것의 의미의 기준으로 일반적으로 잘못된 equals()및 hashCode()가변 온 상태로하는 단계; 그렇게하면 그러한 요소가 HashSet 또는 HashMap.
    따라서 레코드에 변경 가능성을 추가하면 상태 설명과 다른 평등 프로토콜이 필요할 수도 있습니다.
    또한 다른 구성 프로토콜을 원할 수도 있습니다 (많은 도메인 객체가 인수가없는 생성자로 생성되고 setter로 상태가 수정되거나
    “기본 키”필드 만 사용하는 생성자로 수정 됨). 주요 구별 기능 : 단일 상태 설명에서 주요 API 요소를 도출 할 수 있습니다.
    (그리고 일단 가변성을 도입하면 쓰레드 안전성에 대해 생각할 필요가 있습니다. 이것은 레코드의 목표와 조화를 이루기 어려울 것입니다.)

변경 가능한 JavaBeans의 상용구를 자동화하는 것이 좋을만큼, 그렇게하려는 시도 (Lombok, Immutables, Joda Beans 등)를 살펴보고 그들이 획득 한
“노브”수를 살펴보기 만하면됩니다. 수년에 걸쳐 임의의 코드 에 대한 상용구 감소에만 초점을 맞춘 접근 방식이 새로운 종류의
상용구를 생성하는 것으로 보장 된다는 사실을 깨달았습니다 . 이러한 클래스는 하나의 간단한 설명으로 캡처하기에는 너무 많은 자유도가 있습니다.

의미 체계가 명확하게 정의 된 명목 튜플 은 많은 사용 사례에 대해 프로그램을 더 간결 하고 더 안정적으로 만들 수있는 것입니다.
하지만 여전히 우리를 위해 할 수있는 한계를 넘어서는 사용 사례가 있습니다.
(이것은 이러한 클래스에 대해 우리가 할 수있는 일이 없다는 것을 의미하지는 않습니다. 단지 이 기능이 그들을위한 전달 수단이 아니라는 것을 의미합니다.)
따라서 명확하게 말하면, 레코드는 JavaBeans를 대체하기위한 것이 아닙니다. , 또는 기타 변경 가능한 집합체-괜찮습니다.

  • Additional fields 관련 긴장은 레코드가 주 설명의 일부가 아닌 필드를 선언 할 수 있는지 여부입니다.
    (또한 이것이 안전한 예를 쉽게 상상할 수 있습니다.) 반면에이 기능은 “상태, 전체 상태, 상태 만 위반”하려는 유혹을 다시 도입합니다.
    즉, 가장 잘 피하는 유혹입니다.

Sealed types

밀봉 유형은 서브 클래 싱이 유형의 선언으로 지정된 지침에 따라 제한되는 것입니다. (최종성은 퇴화 된 밀봉 형태로 간주 될 수 있습니다.)

씰링은 두 가지 용도로 사용됩니다. 첫 번째는 하위 유형이 될 수있는 사람을 제한한다는 것입니다.
이는 주로 API 소유자가 API의 무결성을 보호하고자하는 선언 사이트 문제입니다.
다른 덜 분명한 이점은 밀폐 된 유형에서 유형 패턴을 전환 할 때와 같이 사용 현장에서 철저한 분석이 가능하다는 것 입니다.

sealed선택적인 permits목록 과 함께 클래스, 추상 클래스 또는 인터페이스에 수정자를 적용하여 클래스가 봉인되도록 지정 합니다.

1
2
sealed interface Node
permits A, B, C { ... }

이 명시 적 형식에서는 목록에 Node열거 된 유형에 의해서만 확장 될 수 있습니다 permits(동일한 패키지 또는 모듈의 구성원이어야 함).
많은 상황에서 이것은 지나치게 명시적일 수 있습니다. 모든 하위 유형이 동일한 컴파일 단위에서 선언 permits된 경우 절을 생략 할 수 있습니다 .
이 경우 컴파일러는 현재 컴파일 단위의 하위 유형을 열거하여 이를 추론합니다.

봉인 된 유형의 익명 하위 클래스 (및 람다)는 금지됩니다.

최종 성과 마찬가지로 봉인은 언어이자 JVM 기능입니다.
유형의 봉인 성 및 허용 된 하위 유형 목록은 런타임에 적용 할 수 있도록 클래스 파일에서 수정됩니다.

허용 된 하위 유형 목록도 어떻게 든 Javadoc에 통합되어야합니다.
이것은 Javadoc이 현재 포함하고있는 현재 “모든 구현 클래스”목록과 정확히 동일하지 않으므로
“허용 된 모든 하위 유형”과 같은 목록이 추가 될 수 있습니다 (하위 유형이 상위 유형보다 액세스 할 수없는 경우 일부 표시가있을 수 있음).
나열되지 않은 다른 항목이 있다는 주석을 포함합니다.)

Exhaustiveness

봉인의 이점 중 하나는 컴파일러가 봉인 된 형식의 허용 된 하위 형식을 열거 할 수 있다는 것입니다.
이를 통해 밀봉 된 유형과 관련된 패턴을 전환 할 때 철저한 분석을 수행 할 수 있습니다.

참고 : 겉으로는 말 permits package하거나 permits module속기처럼 말하고 싶은데,
패키지 메이트 또는 모듈 메이트가 모두 나열하지 않고 유형을 확장 할 수 있습니다.
그러나 패키지와 모듈이 항상 공동 컴파일되는 것은 아니기 때문에 컴파일러가 완전성에 대해 추론하는 능력을 약화시킬 수 있습니다.

반면에 하위 유형은 봉인 된 부모만큼 액세스 할 수있을 필요가 없습니다.
이 경우 일부 클라이언트는 완전히 전환 할 기회를 얻지 못할 수 있습니다.
default절이나 다른 전체 패턴 을 사용하여 이러한 스위치를 완전하게 만들어야합니다.
이러한 봉인 된 유형에 대한 스위치를 컴파일 할 때 컴파일러는 유용한 오류 메시지를 제공 할 수 있습니다
(“이것이 봉인 된 유형이라는 것을 알고 있지만 모든 하위 유형을 볼 수 없기 때문에 여기에서 완전한 검사를 제공 할 수 없습니다. 여전히 기본값이 필요합니다.”)

Inheritance

달리 지정하지 않는 한 봉인 된 유형의 추상 하위 유형은 암시 적으로 봉인되고 구체적인 하위 유형은 암시 적으로 최종입니다.
를 사용하여 하위 유형을 명시 적으로 수정하면 되돌릴 수 있습니다 non-sealed. (하지만 기록 용은 아닙니다 final. 항상 그렇습니다 .)

계층 구조에서 하위 유형의 봉인을 해제한다고해서 봉인의 모든 이점이 손상되지는 않습니다.
명시 적으로 허용 된 하위 유형 집합이 여전히 전체 커버링을 구성하기 때문입니다.
그러나 봉인되지 않은 하위 유형에 대해 알고있는 사용자는이 정보를 자신의 이익을 위해 사용할 수 있습니다
(오늘날 예외가있는 경우와 매우 유사합니다. 원하는 경우 FileNotFoundException 별도로 잡을 수 IOException있지만 반드시 그럴 필요는 없습니다).

명시 적 봉인 해제 (및 개인 하위 유형)가 유용한 경우의 예는 JEP-334 API에서 찾을 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
sealed interface ConstantDesc
permits String, Integer, Float, Long, Double,
ClassDesc, MethodTypeDesc, MethodHandleDesc,
DynamicConstantDesc { }

sealed interface ClassDesc extends ConstantDesc
permits PrimitiveClassDescImpl, ReferenceClassDescImpl { }

private class PrimitiveClassDescImpl implements ClassDesc { }
private class ReferenceClassDescImpl implements ClassDesc { }
sealed interface MethodTypeDesc extends ConstantDesc
permits MethodTypeDescImpl { }

sealed interface MethodHandleDesc extends ConstantDesc
permits DirectMethodHandleDesc, MethodHandleDescImpl { }
sealed interface DirectMethodHandleDesc extends MethodHandleDesc
permits DirectMethodHandleDescImpl { }

// designed for subclassing
non-sealed class DynamicConstantDesc extends ConstantDesc { ... }

Summary

레코드와 봉인 된 유형의 조합은 구조화 된 데이터의 관련 그룹을 설명하기위한 강력하고
잘 이해 된 패턴을 따르며 공통 코드의 가독성과 간결성을 향상시킬 수있는 많은 상황이 있습니다.
기록은 선언의 상용구에서 약간의 완화를 원하는 모든 코드에 적합하지 않을 수 있습니다.
우리는 이러한 사용 사례의 상황을 개선 할 수있는 기능도 계속 조사 할 계획입니다.

참조