파란하늘의 지식창고
반응형

이 글은 JDK 12 ~ 17 사이에 추가된 language specification feature에 대해 정리한 내용입니다.

만약 각 JDK별 변경 사항을  확인하고 싶은 경우 이전 글을 참고하면 됩니다.

2019.07.18 - [Study/Java] - JDK 12 New Features

2019.09.25 - [Study/Java] - JDK 13 New Features

2020.03.30 - [Study/Java] - JDK 14 New Features

2020.10.13 - [Study/Java] - JDK 15 New Features

2021.03.18 - [Study/Java] - JDK 16 New Features

2021.09.15 - [Study/Java] - JDK 17 New Features


JDK의 버전 정책

JDK는 6개월 주기로 새 버전이 release 되고 3년 주기로 LTS (Long Term Support) 버전이 release가 된다.

https://www.youtube.com/watch?v=mFyzyVnYcoY 

https://www.slideshare.net/SimonRitter/jdk-9-10-11-and-beyond

 

JDK 9, 10, 11 and Beyond

A presentation describing the recent changes to Java in JDK 9, 10 and 11. It also covers longer-term projects like Loom and Valhalla in the OpenJDK. JDK devel…

www.slideshare.net

대부분의 경우 현업에서는 JDK 8 -> JDK 11 -> JDK 17의 순서로 LTS 버전의 JDK를 사용할 것이다.

JDK의 라이선스 정책

oracle이 Sun Microsystems를 2010년 인수하면서 JDK를 oracle이 제공하였었다.

oracle이 자사의 JDK를 구독형 라이선스로 유료화하면서 무료로 제공되는 OpenJDK와 유료화 대상인 Oracle JDK를 분리하여 제공하게 되었다.

이로 인해 기업들은 기존에 사용하던 Oracle JDK를 무료로 버그나 보안 관련 패치 지원을 해줄 수 있는 Zulu, AdoptOpenJDK, Oracle JDK 같은 vendor 지원의 JDK로 변경하게 되었다.

https://engineering.linecorp.com/ko/blog/line-open-jdk/

JDK 12 ~ JDK 17 language specification feature

JDK 11과 JDK 17 사이에는 많은 변화가 있지만 프로그래머 입장에서는 language specification을 가장 관심 있게 지켜볼 것 같다.

JDK 12 ~ JDK 17 사이 추가된 language specification 관련 JEP는 다음과 같다.

language specification 관련 JEP 적용 JDK
Switch Expressions JEP 325: Switch Expressions (Preview) JDK 12
JEP 354: Switch Expressions (Second Preview) JDK 13
JEP 361: Switch Expressions JDK 14
Text Blocks JEP 355: Text Blocks (Preview) JDK 13
JEP 368: Text Blocks (Second Preview) JDK 14
JEP 378: Text Blocks JDK 15
Pattern Matching for instanceof JEP 305: Pattern Matching for instanceof (Preview) JDK 14
JEP 375: Pattern Matching for instanceof (Second Preview) JDK 15
JEP 394: Pattern Matching for instanceof JDK 16
Records JEP 359: Records (Preview) JDK 14
JEP 384: Records (Second Preview) JDK 15
JEP 395: Records JDK 16
Sealed Classes JEP 360: Sealed Classes (Preview) JDK 15
JEP 397: Sealed Classes (Second Preview) JDK 16
JEP 409: Sealed Classes JDK 17
Restore Always-Strict Floating-Point Semantics JEP 306: Restore Always-Strict Floating-Point Semantics JDK 17
Pattern Matching for switch JEP 406: Pattern Matching for switch (Preview) JDK 17

새로 추가되는 기능은 보통 Preview -> Second Preview -> 정식 기능의 단계로 JDK에 반영되어 피드백을 받아 개선하는 절차를 거친다.

preview 기능은 JDK 12부터 도입된 기능으로 JEP 12: Preview Features에 명세되어 있다.

해당 JDK에 preview로 추가된 feature를 사용하고 싶은 경우 '--enable-preview' 옵션을 vm 실행 시 추가해야 한다.

javac Foo.java                   // Do not enable any preview features
javac --enable-preview Foo.java  // Enable all preview features

JDK 12 ~ JDK 17까지 추가된 language specifiaction feature는 총 6가지인데 'Restore Always-Strict Floating-Point Semantics'는 조금 특수한 경우이기 때문에 아래 5가지에 대한 문법적인 변경 사항을 파악하면 된다.

  • Switch Expressions
  • Text Blocks
  • Pattern Matching for instanceof
  • Records
  • Sealed Classes

Switch Expressions

이전 switch 구문의 경우 불필요하게 장황하고 누락된 break 문으로 우발적인 오류가 발생하기 쉬웠다.

switch (day) {
    case MONDAY:
    case FRIDAY:
    case SUNDAY:
        System.out.println(6);
        break;
    case TUESDAY:
        System.out.println(7);
        break;
    case THURSDAY:
    case SATURDAY:
        System.out.println(8);
        break;
    case WEDNESDAY:
        System.out.println(9);
        break;
}

이에 대해 label이 일치하는 경우 label 오른쪽에 있는 코드만 실행된다는 것을 의미하기 위해 "case L ->" switch label의 새로운 형식이 도입되었다.
또한 쉼표로 구분하여 여러 상수를 사용할 수 있게 된다.

switch (day) {
    case MONDAY, FRIDAY, SUNDAY -> System.out.println(6);
    case TUESDAY                -> System.out.println(7);
    case THURSDAY, SATURDAY     -> System.out.println(8);
    case WEDNESDAY              -> System.out.println(9);
}

"case L -> " switch label의 오른쪽에 있는 코드는 expression, block 또는 throw문을 사용할 수 있다. (기존 형태는 "case L : " 임)
이를 통해 local variable의 범위가 전체 block인 기존 switch block의 또 다른 문제를 해결할 수 있다.

switch (day) {
    case MONDAY:
    case TUESDAY:
        int temp = ...     // The scope of 'temp' continues to the }
        break;
    case WEDNESDAY:
    case THURSDAY:
        int temp2 = ...    // Can't call this variable 'temp'
        break;
    default:
        int temp3 = ...    // Can't call this variable 'temp'
}

위 예시처럼 switch block 내 locale variable를 동일한 이름을 각 case 내에서 개별적으로 사용할 수 없어서 아래처럼 사용했었다.

int numLetters;
switch (day) {
    case MONDAY:
    case FRIDAY:
    case SUNDAY:
        numLetters = 6;
        break;
    case TUESDAY:
        numLetters = 7;
        break;
    case THURSDAY:
    case SATURDAY:
        numLetters = 8;
        break;
    case WEDNESDAY:
        numLetters = 9;
        break;
    default:
        throw new IllegalStateException("Wat: " + day);
}

이제 다음처럼 사용할 수 있다.

int numLetters = switch (day) {
    case MONDAY, FRIDAY, SUNDAY -> 6;
    case TUESDAY                -> 7;
    case THURSDAY, SATURDAY     -> 8;
    case WEDNESDAY              -> 9;
};

"switch L ->" 오른쪽에 있는 코드의 결과가 반환되어 numLetters variable에 할당되게 된다.

Yielding a value

단일 표현식으로 처리되지 않는 경우 block 구문에서 반환 값 처리를 위해 yield 구문이 추가되었다.

int j = switch (day) {
    case MONDAY  -> 0;
    case TUESDAY -> 1;
    default      -> {
        int k = day.toString().length();
        int result = f(k);
        yield result;
    }
};

이는 새로 추가된 "switch L ->" 뿐만 아니라 기존 "switch L :" 에서도 사용할 수 있고 또한 반환 값을 담을 수 있다.

int result = switch (s) {
    case "Foo": 
        yield 1;
    case "Bar":
        yield 2;
    default:
        System.out.println("Neither Foo nor Bar, hmmm...");
        yield 0;
};

yield는 새로운 keyword가 아닌 (var와 같은) 제한된 식별자(restricted identifier)이다. 

switch가 case에 대한 실행이 아닌 결괏값 반환이 되면서 가능한 모든 값에 대해 일치하는 label이 존재하거나 default를 사용해야 한다.

따라서 컴파일러는 모든 switch label에 대해 일치하는 경우 값을 산출할 수 있는지 확인한다.

int i = switch (day) {
    case MONDAY -> {
        System.out.println("Monday"); 
        // ERROR! Block doesn't contain a yield statement
    }
    default -> 1;
};
i = switch (day) {
    case MONDAY, TUESDAY, WEDNESDAY: 
        yield 0;
    default: 
        System.out.println("Second half of the week");
        // ERROR! Group doesn't contain a yield statement
};

Text Blocks

여러 줄에 걸친 text를 좀 더 편하게 사용할 수 있게 되었다.

HTML 예시

기존의 경우

String html = "<html>\n" +
              "    <body>\n" +
              "        <p>Hello, world</p>\n" +
              "    </body>\n" +
              "</html>\n";

text block을 사용하는 경우

String html = """
              <html>
                  <body>
                      <p>Hello, world</p>
                  </body>
              </html>
              """;

SQL 예시

기존의 경우

String query = "SELECT \"EMP_ID\", \"LAST_NAME\" FROM \"EMPLOYEE_TB\"\n" +
               "WHERE \"CITY\" = 'INDIANAPOLIS'\n" +
               "ORDER BY \"EMP_ID\", \"LAST_NAME\";\n";

text block을 사용하는 경우

String query = """
               SELECT "EMP_ID", "LAST_NAME" FROM "EMPLOYEE_TB"
               WHERE "CITY" = 'INDIANAPOLIS'
               ORDER BY "EMP_ID", "LAST_NAME";
               """;

polyglot language 예시

기존의 경우

ScriptEngine engine = new ScriptEngineManager().getEngineByName("js");
Object obj = engine.eval("function hello() {\n" +
                         "    print('\"Hello, world\"');\n" +
                         "}\n" +
                         "\n" +
                         "hello();\n");

text block을 사용하는 경우

ScriptEngine engine = new ScriptEngineManager().getEngineByName("js");
Object obj = engine.eval("""
                         function hello() {
                             print('"Hello, world"');
                         }
                         
                         hello();
                         """);

text block은 여는 구분 기호와 닫는 구분 기호로 묶인 0개 이상의 contents character로 구성된다.

내용에는 string 문자열과 다르게 큰따옴표 문자가 직접 포함될 수 있다.
\"를 사용할 수 있지만 필요하거나 권장하지는 않는다.

내용에 line terminator를 직접 포함할 수 있다.
\n을 사용할 수 있지만 필요하거나 권장하지는 않는다.

"""
line 1
line 2
line 3
"""

위 내용은 아래와 같다.

"line 1\nline 2\nline 3\n"

이는 또한 아래와 같다.

"line 1\n" +
"line 2\n" +
"line 3\n"

text block으로 빈 문자열을 표현하려면 다음과 같이 두줄로 표현해야 한다.
한 줄에 여는 구분 기호와 닫는 구분 기호를 같이 사용할 수 없다.

String empty = """
""";

아래는 잘못된 형식의 text block 사용을 보여준다.

String a = """""";   // no line terminator after opening delimiter
String b = """ """;  // no line terminator after opening delimiter
String c = """
           ";        // no closing delimiter (text block continues to EOF)
String d = """
           abc \ def
           """;      // unescaped backslash (see below for escape processing)

Compile-time processing

text block은 String literal과 마찬가지로 String type의 constant expression이다.
그러나 String literal과 달리 text block의 내용은 Java 컴파일러에서 다음 세 단계로 처리된다.

  1. content의 line terminator는 LF(\u000A)로 변환된다.
  2. Java 소스 코드의 들여 쓰기와 일치하도록 도입된 content 주변의 부수적인 공백이 제거된다
  3. content의 escape sequence가 ​​해석된다.
    마지막 단계로 해석을 수행한다는 것은 개발자가 이전 단계에서 수정하거나 삭제하지 않고도 \n과 같은 escape sequence를 작성할 수 있음을 의미한다.

Incidental white space (2번째 단계)

text block의 닫는 기호 앞의 공백만큼 모든 라인의 공백이 제거된 상태로 처리된다.

String html = """
..............<html>
..............    <body>
..............        <p>Hello, world</p>
..............    </body>
..............</html>
..............""";

위에 점으로 표현한 공백은 제거되게 된다.

또한 문자의 뒤 공백도 제거가 된다.

String html = """
..............<html>...
..............    <body>
..............        <p>Hello, world</p>....
..............    </body>.
..............</html>...
..............""";

제거된 공백을 |로 시각화하여 표현하면 다음과 같다.

|<html>|
|    <body>|
|        <p>Hello, world</p>|
|    </body>|
|</html>|

만약 닫는 기호 앞의 공백이 다음과 같이 없다면

String html = """
              <html>
                  <body>
                      <p>Hello, world</p>
                  </body>
              </html>
""";

다음처럼 공백을 처리하지 않는다.

|              <html>
|                  <body>
|                      <p>Hello, world</p>
|                  </body>
|              </html>

다음처럼 닫는 기호 앞의 공백이 content 보다 앞에 있다면

String html = """
........      <html>
........          <body>
........              <p>Hello, world</p>
........          </body>
........      </html>
........""";

그만큼만 공백을 제거하고 나머지는 남긴다.

|      <html>
|          <body>
|              <p>Hello, world</p>
|          </body>
|      </html>

만약 닫는 기호 앞의 공백이 content보다 많다면

String html = """
..............<html>
..............    <body>
..............        <p>Hello, world</p>
..............    </body>
..............</html>
..............    """;

앞의 공백은 위와 같이 제거되며 닫는 기호 앞의 공백은 문자 뒤 공백으로 간주되어 제거되게 된다.
따라서 다음과 같이 처리된다.

|<html>
|    <body>
|        <p>Hello, world</p>
|    </body>
|</html>

escape sequence (3번째 단계)

들여 쓰기 공백을 처리한 후 3번째 단계로 escape sequence를 해석한다.

다음과 같이 \r escape sequence(CR)를 사용한 경우

String html = """
              <html>\r
                  <body>\r
                      <p>Hello, world</p>\r
                  </body>\r
              </html>\r
              """;

다음과 같이 해석한다.

|<html>\u000D\u000A
|    <body>\u000D\u000A
|        <p>Hello, world</p>\u000D\u000A
|    </body>\u000D\u000A
|</html>\u000D\u000A

text block의 여는 기호와 닫는 기호가 큰따옴표 3개이기 때문에 그보다 적은 수의 큰따옴표의 연속 사용은 아무 문제없이 출력된다.

String story = """
    "When I use a word," Humpty Dumpty said,
    in rather a scornful tone, "it means just what I
    choose it to mean - neither more nor less."
    "The question is," said Alice, "whether you
    can make words mean so many different things."
    "The question is," said Humpty Dumpty,
    "which is to be master - that's all."
    """;    // Note the newline before the closing delimiter

String code =
    """
    String empty = "";
    """;

만약 text block 내에서 동일하게 큰따옴표 3개를 쓰려면 닫는 기호로 해석되는 것을 피하기 위해 적어도 하나는 escape 해야 한다.

String code = 
    """
    String text = \"""
        A text block inside a text block
    \""";
    """;

String tutorial1 =
    """
    A common character
    in Java programs
    is \"""";

String tutorial2 =
    """
    The empty string literal
    is formed from " characters
    as follows: \"\"""";

System.out.println("""
     1 "
     2 ""
     3 ""\"
     4 ""\""
     5 ""\"""
     6 ""\"""\"
     7 ""\"""\""
     8 ""\"""\"""
     9 ""\"""\"""\"
    10 ""\"""\"""\""
    11 ""\"""\"""\"""
    12 ""\"""\"""\"""\"
""");

New escape sequences

줄 바꿈과 공백 처리를 더 세밀하게 제어할 수 있도록 두 개의 새로운 이스케이프 시퀀스를 도입했다.

\<line-terminator> escape sequence는 줄 바꿈 문자의 삽입을 명시적으로 막는다.

아래와 같은 기존 코드는

String literal = "Lorem ipsum dolor sit amet, consectetur adipiscing " +
                 "elit, sed do eiusmod tempor incididunt ut labore " +
                 "et dolore magna aliqua.";

아래와 같이 \<line-terminator> escape sequence를 사용한 text block과 동일하다.

String text = """
                Lorem ipsum dolor sit amet, consectetur adipiscing \
                elit, sed do eiusmod tempor incididunt ut labore \
                et dolore magna aliqua.\
                """;

\s escape secuqnce는 후행 공백 제거를 명시적으로 막는다.

아래의 경우 \s를 사용하여 문자 뒤 공백이 제거되지 않고 유지되게 된다.

String colors = """
    red  \s
    green\s
    blue \s
    """;

text block 연결

text block은 기존 String literal을 사용하는 모든 곳에서 사용할 수 있다.

따라서 기존 String literal과 혼합하여 연결이 가능하다.

String code = "public void print(Object o) {" +
              """
                  System.out.println(Objects.toString(o));
              }
              """;

그런데 만약 text block 사이에 string literal을 다음과 같이 연결하고 type 뒤의 text block 여는 기호가 line terminator를 사용 후 가독성을 높이기 위해 공백을 사용하면 출력 시 긴 공백이 발생하게 된다.

String code = """
              public void print(""" + type + """
                                                 o) {
                  System.out.println(Objects.toString(o));
              }
              """;

공백을 없애기 위해 아래처럼 사용하면 가독성이 떨어진다.

String code = """
              public void print(""" + type + """
               o) {
                  System.out.println(Objects.toString(o));
              }
              """;

이 경우 대안으로 아래와 같이 String::replace 또는 String::format을 사용하는 방법이 있다.

String code = """
              public void print($type o) {
                  System.out.println(Objects.toString(o));
              }
              """.replace("$type", type);
String code = String.format("""
              public void print(%s o) {
                  System.out.println(Objects.toString(o));
              }
              """, type);

Additional Methods

text block을 지원하기 위해 아래 method가 추가된다.

  • String::stripIndent(): text block content에서 부수적인 공백을 제거하는 데 사용
  • String::translateEscapes(): escape sequences를 번역하는 데 사용
  • String::formatted(Object... args): text block에서 값 대체를 단순화

Pattern Matching for instanceof

기존의 경우 instanceof를 다음과 같이 사용하였다.

if (obj instanceof String) {
    String s = (String) obj;    // grr...
    ...
}

obj가 String instance인 것을 확인한 후 해당 block내에서 사용하기 위해 String으로 casting 하는 과정이 필요했다.

다음과 같이 사용할 수 있게 개선되었다.

if (obj instanceof String s) {
    // Let pattern matching do the work!
    ...
}

obj가 String instance인 경우 s에 String으로 부여된다.

pattern variable의 범위는 flow scoping 개념을 사용한다.
pattern variable은 컴파일러가 pattern이 확실히 일치하고 variable에 값이 할당되었다고 추론할 수 있는 범위에만 있다.

 

if (a instanceof Point p) {
    // p is in scope
    ...
}
// p not in scope here
if (b instanceof Point p) {     // Sure!
        ...
}

단순히 block 범위라고 표현하지 않은 이유는 다음과 같이 사용할 수 있기 때문이다.

if (obj instanceof String s && s.length() > 5) {
    flag = s.contains("jdk");
}

할당된 pattern variable s를 조건문에서 사용할 수 있다.

하지만 다음과 같이 사용할 수 없다.

if (obj instanceof String s || s.length() > 5) {    // Error!
    ...
}

and 조건은 앞의 조건을 만족하고, s가 할당된 그다음 조건을 체크하지만 or 조건은 앞의 조건이 만족하지 않더라도 다음 조건을 진행하기 때문이다.

pattern matching을 사용하면 명시적인 casting 수를 줄일 수 있다.

기존의 경우 아래와 같은 코드는

public boolean equals(Object o) {
    return (o instanceof CaseInsensitiveString) &&
        ((CaseInsensitiveString) o).s.equalsIgnoreCase(s);
}

이렇게 변경된다.

public boolean equals(Object o) {
    return (o instanceof CaseInsensitiveString cis) &&
        cis.s.equalsIgnoreCase(s);
}

다른 경우를 예로 들면 아래와 같은 코드는

public boolean equals(Object o) {
    if (!(o instanceof Point))
        return false;
    Point other = (Point) o;
    return x == other.x
        && y == other.y;
}

이렇게 변경된다.

public boolean equals(Object o) {
    return (o instanceof Point other)
        && x == other.x
        && y == other.y;
}

또한 부정문을 사용한 경우에도 pattern variable의 할당이 이루어질 수 있다.

다음의 경우를 보면

public void onlyForStrings(Object o) throws MyException {
    if (!(o instanceof String s))
        throw new MyException();
    // s is in scope
    System.out.println(s);
    ...
}

String 이 아닌 경우 throw 처리를 하며 String인 경우 실행되는 if 조건 block을 벗어난 위치에서도 pattern variable s를 사용할 수 있다.

pattern variable은 범위 정의가 이렇게 조금 특별하지만 그 외의 모든 부분에서 local variable로 취급된다.
이는 (1) 할당될 수 있고 (2) field 선언을 shadowing 할 수 있다는 것을 의미한다.

class Example1 {
    String s;

    void test1(Object o) {
        if (o instanceof String s) {
            System.out.println(s);      // Field s is shadowed
            s = s + "\n";               // Assignment to pattern variable
            ...
        }
        System.out.println(s);          // Refers to field s
        ...
    }
}

variable shadowing을 이해하기 쉬운 예제는 다음과 같다.

static int x;

@Test
void testShadow() {
    x = 5;
    System.out.println("x = " + x);	// x = 5
    int x;
    x = 10;
    System.out.println("x = " + x);	// x = 10
    System.out.println("SimpleTest.x = " + SimpleTest.x);	// SimpleTest.x = 5
}

이런 pattern variable의 flow scoping 특성은 field 선언을 shadowing 하는 pattern variable을 참조하는지 또는 field 선언 자체를 참조하는지에 대해 주의를 기울여야 하기도 한다.

class Example2 {
    Point p;

    void test2(Object o) {
        if (o instanceof Point p) {
            // p refers to the pattern variable
            ...
        } else {
            // p refers to the field
            ...
        }
    }
}

Records

record는 다음 사항을 목표로 하고 있다.

  • value의 단순한 집계를 표현하는 객체 지향 구조
  • programmer가 확장 가능한 동작이 아닌 불변 데이터를 모델링하는데 집중할 수 있도록 도움
  • equals와 accessor 같은 data 기반 method를 자동으로 구현
  • 기존 방식과 migration 호환성 유지

기존에 Domain을 만들면 lombok의 @Data나 @Getter, @Setter를 통해 간략하게 사용하던 부분을 JDK 자체가 record란 개념으로 제공한다고 보면 된다.

record는 data를 data로 modeling 하는 것을 목표로 한다.
java language에 새로운 종류의 class이다.
record class는 class가 일반적으로 누리는 자유도를 포기하지만 대신 간결하게 쓸 수 있다.

record 선언은 name, optional type parameters, header 및 body로 구성된다.
header에는 상태(state)를 구성하는 variable인 record class의 component list가 나열된다. (component list를  state description이라고도 함)
예를 들면 아래와 같다.

record Point(int x, int y) { }

record는 data에 대한 투명한 전달을 하기 때문에 많은 표준 구성 요소를 자동으로 획득한다.

  • header의 각 component는 두 개의 member를 가짐 : component와 동일한 name과 return type을 가지는 public accessor method, component와 동일한 type을 가지는 private final field
  • signature가 header와 동일하고 record를 instance화 하는 새 expression의 해당 argument에 각 private field를 할당하는 canonical constructor
  • 두 record가 동일한 type이고 동일한 component value를 포함하는 경우 두 record가 동일한지 확인하는 equals 및 hashCode method
  • 모든 record component의 name과 함께 문자열 표현을 반환하는 toString method

즉 record의 header는 상태(component의 type과 name)를 설명하고 API는 해당 상태 설명에 대해 기계적으로 완전하게 파생된다.
API에는 construction, member access, equality 및 display를 위한 protocol이 포함되어 있다. (향후 버전에서는 강력한 pattern matching이 가능하도록 deconstruction pattern을 지원할 예정)

Constructors for record classes

record class의 constructor 규칙은 일반 class와 다르다.
constructor 선언이 없는 일반 class에는 자동으로 기본 constructor가 제공된다.
대조적으로, constructor 선언이 없는 record class에는 record를 instance화한 새 expression의 해당 argument에 모든 private field를 할당하는 canonical constructor가 자동으로 제공된다.
예를 들어, 이전에 선언된 레코드인 record Point(int x, int y) { }는 다음과 같이 컴파일된다.

record Point(int x, int y) {
    // Implicitly declared fields
    private final int x;
    private final int y;

    // Other implicit declarations elided ...

    // Implicitly declared canonical constructor
    Point(int x, int y) {
        this.x = x;
        this.y = y;
    }
}

canonical constructor는 위에 표시된 것처럼 record header와 일치하는 formal parameter 목록을 사용하여 명시적으로 선언될 수 있다.
formal parameter 목록을 생략하여 보다 간결하게 선언할 수도 있다.
이러한 간결한 canonical constructor에서 parameter는 암시적으로 선언되며 record component에 해당하는 private field는 body에 할당할 수 없지만 constructor 끝에 있는 해당 formal parameter (this.x = x;)에 자동으로 할당된다.
이는 개발자가 field에 parameter를 할당하는 지루한 작업 없이 pararmeter의 유효성을 검사하고 정규화하는 데 집중할 수 있도록 도와준다.

예를 들어, 다음은 암시적 formal parameter의 유효성을 검사하는 간결한 canonical constructor이다.

record Range(int lo, int hi) {
    Range {
        if (lo > hi)  // referring here to the implicit constructor parameters
            throw new IllegalArgumentException(String.format("(%d,%d)", lo, hi));
    }
}

다음은 formal parameter를 정규화하는 간결한 canonical constructor이다.

record Rational(int num, int denom) {
    Rational {
        int gcd = gcd(num, denom);
        num /= gcd;
        denom /= gcd;
    }
}

이는 다음과 같다.

record Rational(int num, int denom) {
    Rational(int num, int demon) {
        // Normalization
        int gcd = gcd(num, denom);
        num /= gcd;
        denom /= gcd;
        // Initialization
        this.num = num;
        this.denom = denom;
    }
}

암시적으로 선언된 constructor와 method가 있는 record class는 중요하고 직관적인 의미론적 속성을 충족한다.
예를 들어, 다음과 같이 선언된 record class R을 보면

record R(T1 c1, ..., Tn cn){ }

R의 인스턴스 r1이 다음과 같은 방식으로 복사되는 경우

R r2 = new R(r1.c1(), r1.c2(), ..., r1.cn());

r1이 null 참조가 아닌 경우 항상 r1.equals(r2) expression은 true가 된다.

compiler가 명시적으로 선언된 method가 이 불변성을 존중하는지 확인하는 것은 일반적으로 불가능하다.
예를 들어, record class의 다음 선언은 해당 accessor methdo가 record instance의 상태를 "자동으로" 조정하고 위의 불변량이 충족되지 않기 때문에 잘못된 스타일로 간주되어야 한다.

record SmallPoint(int x, int y) {
  public int x() { return this.x < 100 ? this.x : 100; }
  public int y() { return this.y < 100 ? this.y : 100; }
}

또한 모든 record class에 대해 암시적으로 선언된 equals method가 구현되어 반사적이며 floating point component가 있는 레코드 클래스에 대해 hashCode와 일관되게 동작한다

명시적으로 선언된 equals 및 hashCode method는 유사하게 작동해야 한다.

Rules for record classes

일반 class와 비교하여 record class의 선언에는 많은 제한 사항이 있다.

  • record class 선언에는 extends 절이 없다.
    record class의 superclass는 항상 java.lang.Record이다.
    enum class의 superclass가 항상 java.lang.Enum인 것과 유사하다.
    일반 class는 암시적 superclass Object를 명시적으로 extends 할 수 있지만 record는 암시적 superclass Record도 명시적으로 extends 할 수 없다.
  • record class는 암시적으로 final이며 abstract 일 수 없다.
    이러한 제한은 record class의 API가 상태 설명만으로 정의되며 나중에 다른 class에 의해 향상될 수 없다는 것을 강조한다.
  • record component에서 파생된 필드는 final이다.
    이 제한은 data-carrier class에 광범위하게 적용되는 기본 정책에 의해 불변하는 정책을 구현한다.
  • record class는 instance field를 명시적으로 선언할 수 없으며 instance initializer를 포함할 수 없다.
    이러한 제한은 record header만 record 값의 상태를 정의하도록 한다.
  • 그렇지 않으면 자동으로 파생되는 member의 명시적 선언은 명시적 선언에 대한 annotation을 무시하고 자동으로 파생되는 member의 type과 정확히 일치해야 한다.
    accessor 또는 equals 또는 hashCode method의 명시적 구현은 record class의 의미 불변성을 보존하기 위해 주의해야 한다.
  • record class는 native method를 선언할 수 없다.
    record class가 native method를 선언할 수 있는 경우 record class의 동작은 정의에 따라 record class의 명시적 상태가 아니라 외부 상태에 따라 달라진다.
    native method가 있는 class는 record로 migration 하기 적당하지 않다.

위의 제한 사항을 넘어서 record class는 일반 class처럼 동작한다.

  • record class의 instance는 new expression을 사용하여 생성된다.
  • record class는 top level로 선언되거나 중첩될 수 있고 generic일 수 있다.
  • record class는 static method, static field, static initializer를 선언할 수 있다.
  • record class는 instance method를 선언할 수 있다.
  • record class는 interface를 구현할 수 있다.
    record class는 header에 설명된 상태를 넘어 상속된 상태를 의미하므로 superclass를 지정할 수 없다.
    그러나 record class는 superinterface를 자유롭게 지정하고 이를 구현하기 위한 instance method를 선언할 수 있다.
    class와 마찬가지로 interface는 많은 record의 동작을 유용하게 특성화할 수 있다.
    동작은 domain-independent(예: Comparable)하거나 domain-specific 일 수 있으며, 이 경우 record는 domain을 capture 하는 sealed hierarchy 일부일 수 있다 (아래 참조).
  • record class는 중첩된 record class를 포함하여 중첩된 type을 선언할 수 있다.
    record 자체가 중첩된 경우 암시적으로 static이다.
    이렇게 하면 record class에 상태를 자동으로 추가하는 즉시 enclosing instance가 방지된다.
  • record class와 header의 component는 annotation으로 decorate 될 수 있다.
    record component의 모든 annotation은 annotation에 적용 가능한 대상 집합에 따라 자동으로 파생된 field, method 및 constructor parameter로 전파된다.
    record component의 type에 대한 type annotation은 자동으로 파생된 member의 해당 type 사용에도 전파된다.
  • record class의 instance는 serialize 및 deserialize 될 수 있다.
    그러나, writeObject, readObject, readObjectNoData, writeExternal, 또는 readExternal method를 제공하여 process를 customize 할 수 없다.
    record class의 component는 serialize를 제어하는 ​​반면 record class의 canonical constructor는 deserialize를 제어한다.

Local record classes

record class의 instance를 생성하고 소비하는 프로그램은 그 자체로 단순한 variable group인 많은 중간 값을 처리할 가능성이 있다.
이러한 중간 값을 모델링하기 위해 record class를 선언하는 것이 편리한 경우가 많다.
한 가지 옵션은 오늘날 많은 프로그램이 helper class를 선언하는 것처럼 static 및 nested "helper" record class를 선언하는 것이다.
더 편리한 옵션은 variable을 조작하는 code에 가까운 method 내부에 record를 선언하는 것이다.
따라서 기존 locale class 구성과 유사한 local record class를 정의한다.

다음 예에서 판매점 및 월별 판매 수치를 local record class인 MerchantSales로 모델링한다.
이 record class를 사용하면 다음과 같은 stream operaion의 가독성이 향상된다.

List<Merchant> findTopMerchants(List<Merchant> merchants, int month) {
    // Local record
    record MerchantSales(Merchant merchant, double sales) {}

    return merchants.stream()
        .map(merchant -> new MerchantSales(merchant, computeSales(merchant, month)))
        .sorted((m1, m2) -> Double.compare(m2.sales(), m1.sales()))
        .map(MerchantSales::merchant)
        .collect(toList());
}

local record class는 중첩 record class의 특별한 경우이다.
중첩 record class와 마찬가지로 local record class는 암시적으로 static이다.
즉, 자체 method가 감싸는 method의 variable에 access 할 수 없으므로 record class에 상태를 자동으로 추가하는 즉시 감싸는 instance를 capture 하지 않는다.
local record class가 암시적으로 static이라는 사실은 암시적으로 static이 아닌 local class와 대조적이다.
실제로 local class는 암시적 또는 명시적으로 static class가 아니며 항상 둘러싸는 method의 variable에 access 할 수 있다.

Local enum classes and local interfaces

local record class를 추가하면 다른 종류의 암시적 static local 선언을 추가할 수 있다.

중첩된 enum class와 중첩된 interface는 이미 암시적으로 static이므로 일관성을 위해 암시적으로 static인 local enum class와 local interface를 정의한다.

Static members of inner classes

inner class가 constant variable이 아닌 경우 명시적으로 또는 암시적으로 static인 member를 선언하는 경우 compile-time error로 명시된다.
예를 들어 중첩된 record class는 암시적으로 static이기 때문에 inner class는 record class member를 선언할 수 없다.

inner class가 명시적으로 또는 암시적으로 static member를 선언할 수 있도록 이 제환을 완화한다.
특히 이를 통해 inner class가 record class인 static member를 선언할 수 있다.

Annotations on record components

record component는 record 선언에서 여러 역할을 한다.
record componet는 first-class 개념이지만 각 component는 동일한 name과 type의 field, 동일한 name과 return type의  accessor method, 그리고 동일한 name과 type의 canonical constructor의 formal parameter에도 해당한다.

이에 의문이 제기된다.
component에 annotation을 달 때 실제로 annotation이 달린 것은 무엇인가?
답은 "이 특정 annotation에 적용되는 모든 element"이다.
이를 통해 field, constructor parameter, 또는 accessor method에 대한 annotation을 사용하는 classs는 이러한 member를 중복 선언하지 않고도 record로 migration 될 수 있다.
예를 들면 다음 class는

public final class Card {
    private final @MyAnno Rank rank;
    private final @MyAnno Suit suit;
    @MyAnno Rank rank() { return this.rank; }
    @MyAnno Suit suit() { return this.suit; }
    ...
}

훨씬 더 읽기 쉬운 record 선언으로 migration 할 수 있다.

public record Card(@MyAnno Rank rank, @MyAnno Suit suit) { ... }

annotation의 적용 대상은 @Target meta-annotation을 사용하여 선언된다.
다음과 같은 경우

@Target(ElementType.FIELD)
    public @interface I1 {...}

이것은 field 선언에 @I1 annotation 적용이 가능하다.
annotation이 둘 이상의 선언에 적용 가능하도록 선언할 수 있다.
예를 들면

@Target({ElementType.FIELD, ElementType.METHOD})
    public @interface I2 {...}

이것은 field 선언과 method 선언에 모두 @I2 annotation 적용이 가능하다.

record component의 annotation으로 돌아가면 이러한 annotation이 해당하는 프로그램 지점에 나타난다.
즉, 전파(propagation)는 @Target meta-annotation을 사용하여 프로그래머가 제어한다.
전파 규칙은 체계적이고 직관적이며 적용되는 모든 사항은 다음과 같다.

  • record component의 annotation이 field 선언에 적용 가능한 경우 해당 private field에 annotation이 나타난다.
  • record component의 annotation이 method 선언에 적용 가능한 경우 해당 accessor method에 annotation이 나타난다.
  • record component의 annotation이 formal parameter에 적용 가능한 경우 annotation이 명시적으로 선언되지 않은 경우 canonical constructor의 해당 formal parameter에 annotation이 표시되고 명시적으로 선언된 경우 compact constructor의 해당 formal parameter에 annotation이 나타난다.
  • record component의 annotation이 type에 적용 가능한 경우 annotation은 다음 모두에 전파된다.
    • 해당 field의 type
    • 해당 accessor method의 return type
    • canonical constructor의 해당 formal parameter type
    • record component의 type (reflection을 통해 runtime에 access 할 수 있음)

public accessor method 또는 (non-compact) canonical constructor가 명시적으로 선언된 경우 직접 표시되는 annotation만 있다.
해당 record component에서 이러한 member로 전파되는 것은 없다.

annotation @Target(RECORD_COMPONENT)를 meta annotation으로 지정하지 않는 한 record component에 대한 선언 annotation은 reflection API를 통해 runtime에 record component와 연결된 annotatio에 포함되지 않는다.

Compatibility and migration

abstract class java.lang.Record는 모든 record class의 공통 superclass이다.
모든 java source file은 preview 기능을 활성화하거나 비활성화하는지에 관계없이 java.lang package의 다른 모든 type뿐만 아니라 java.lang.Record class도 암시적으로 import 한다.
그러나 application이 다른 package에서 Record라는 이름의 다른 class를 import 하는 경우 compiler error가 발생할 수 있다.

다음과 같은 Record class가 있는 경우

package com.myapp;

public class Record {
    public String greeting;
    public Record(String greeting) {
        this.greeting = greeting;
    }
}

다음 예제에서 org.example.MyappPackageExample는 wildcard를 사용하여 com.myapp.Record를 import 하지만 compile 되지 않는다.

package org.example;
import com.myapp.*;

public class MyappPackageExample {
    public static void main(String[] args) {
       Record r = new Record("Hello world!");
    }
}

compiler는 다음과 유사한 error message를 생성한다.

./org/example/MyappPackageExample.java:6: error: reference to Record is ambiguous
       Record r = new Record("Hello world!");
       ^
  both class com.myapp.Record in com.myapp and class java.lang.Record in java.lang match

./org/example/MyappPackageExample.java:6: error: reference to Record is ambiguous
       Record r = new Record("Hello world!");
                      ^
  both class com.myapp.Record in com.myapp and class java.lang.Record in java.lang match

com.myapp package와 java.lang package의 두 Record가 wildcard로 import 된다.
결과적으로 어떤 class도 우선하지 않으며 compiler는 단순 Record 이름을 사용하는 경우 error message를 생성한다.

이 예제를 compile 하려면 import를 변경하여 Record의 명확한 이름을 가져올 수 있다.

import com.myapp.Record;

Records and Sealed Types

record는 sealed type(JEP 360)에서 잘 작동한다.
예를 들어 record 제품군은 동일한 sealed interface를 구현할 수 있다.

package com.example.expression;

public sealed interface Expr
    permits ConstantExpr, PlusExpr, TimesExpr, NegExpr {...}

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

record class와 sealed type의 조합을 대수 데이터 유형(algebraic data types)이라고 한다.
record class를 통해 product type을 표현할 수 있고 sealed type을 사용하여 sum type을 표현할 수 있다.

Sealed Classes

Goals

  • class 또는 interface 작성자가 구현을 담당하는 코드를 제어할 수 있게 한다.
  • superclass 사용을 제한하기 위해 access modifier 보다 더 선언적인 방법을 제공한다.
  • patttern의 철저한 분석을 위한 기반을 제공하여 pattern matching을 지원한다.

Description

class와 interface의 상속 계층 구조는 좋은 모델링이지만 고정된 값 집합을 모델링 하고 싶을 때가 있다.
예를 들어 다음과 같이 천문 영역에서의 값의 종류를 모델링할 수 있다.

interface Celestial { ... }
final class Planet implements Celestial { ... }
final class Star   implements Celestial { ... }
final class Comet  implements Celestial { ... }

위의 경우 이 계층 구조는 이 모델이 세 종류의 천체만 있다는 도메인 지식을 반영할 수 없다.
이 상황에서 더 이상 확장하거나 구현하지 못하게 막을 수 있는 방법은 없다.

sealed class 또는 interface는 허용된 class 및 interface에 의해서만 확장되거나 구현될 수 있다.

다음과 같은 형태로 사용한다.

package com.example.geometry;

public abstract sealed class Shape
    permits Circle, Rectangle, Square { ... }

위 코드를 보면 permits를 통해 해당 Shape class를 확장할 수 있는 허용 class를 지정하였다.

permits에 지정된 class는 반드시 superclass 근처에 위치해야 한다.
동일한 모듈 (superclass가 지정된 모듈에 있는 경우) 또는 동일한 package (superclass가 지정되지 않은 모듈에 있는 경우)에 있어야 한다.

다음과 같은 Shape 선언에서 허용되는 subclass는 모두 동일한 지정된 모듈의 서로 다른 package에 위치한다.

package com.example.geometry;

public abstract sealed class Shape 
    permits com.example.polar.Circle,
            com.example.quad.Rectangle,
            com.example.quad.simple.Square { ... }

permits 대상 class가 작고 몇 개 없으면 sealed class와 동일한 source file에서 선언하는 것이 편리할 수 있다.
이러한 방식으로 선언되면 sealed class는 permit 절을 생략할 수 있으며 java compiler는 source file의 선언에서 허용된 subclass를 유추한다. (subclass는 보조 class 또는 중첩 class일 수 있다.)

예를 들어 다음 코드가 Rtoot.java에 있으면 sealed class Root는 3개의 permit subclass를 가진 것으로 추론된다.

abstract sealed class Root { ... 
    final class A extends Root { ... }
    final class B extends Root { ... }
    final class C extends Root { ... }
}

permits에 의해 지정된 class는 반드시 canonical name이 있어야 한다.
그렇지 않으면 compile-time에 error가 발생한다.
즉 익명 class와 local class는 permits 대상이 될 수 없다.

sealed class는 permitted subclass에 세 가지 제약 조건을 적용한다. 

  1. sealed class와 그의 permitted subclass는 동일한 module에 속해야 하며 명시되지 않은 module에서 선언된 경우 동일한 package에 속해야 한다.
  2. 모든 permit 된 subclass는 sealed class를 직접(directly) 확장해야 한다. 
  3. 모든 permit 된 subclass는 modifier를 사용하여 superclass에 의해 시작된 봉인을 전파하는 방법을 설명해야 한다.
    • permit 된 subclass는 class 계층 구조의 더 이상 확장되지 않도록 하기 위해 final로 선언할 수 있다. (record class는 암시적으로 final 선언됨)
    • permit 된 subclass는 sealed를 선언하여 superclass가 의도한 것보다 더 확장될 수 있지만 제한된 방식이다.
    • permit 된 subclass는 알 수 없는 subclass에 의한 확장에 대해 열린 상태로 되돌아가도록 non-sealed로 선언될 수 있다. (non-sealed modifier는 Java에 의해 제안된 최초의 하이픈 연결 키워드이다.)

세 번째 제약 조건의 예로 Circle과 Square는 final이고 Rectangle 은 3개의 subclass를 가지고 있고 WeirdShape는 non-sealed이다.

package com.example.geometry;

public abstract sealed class Shape
    permits Circle, Rectangle, Square, WeirdShape { ... }

public final class Circle extends Shape { ... }

public sealed class Rectangle extends Shape 
    permits TransparentRectangle, FilledRectangle { ... }
public final class TransparentRectangle extends Rectangle { ... }
public final class FilledRectangle extends Rectangle { ... }

public final class Square extends Shape { ... }

public non-sealed class WeirdShape extends Shape { ... }

WeirdShape는 알 수 없는 class에 의해 확장될 수 있지만 해당 subclass의 모든 instance는 WeirdShape의 instance이기도 하다.
따라서 Shape의 instance가 Circle, Rectangle, Square 또는 WeirdShape 중 어느 것인지 테스트하기 위해 작성된 코드는 전체적으로 유지된다.

permit 된 subclass에서 final, sealed 및 non-sealed modifier 중 하나는 반드시 사용해야 한다.
sealed(subclasses 있음 암시)와 final(subclasses 없음 암시) 또는
non-sealed(subclasses 있음 암시)와 final(subclasses 없음 암시) 또는
sealed(제한된 subclasses 암시)와 non-sealed(제한되지 않은 subclasses 암시)
class에서 같이 쓰는 것은 불가능하다.

(final modifier는 extends/implement이 완전히 금지된 강력한 밀봉 형태로 간주될 수 있다. 즉, final은 개념적으로 sealed + permits과 같다. 이러한 permits 절은 Java로 작성할 수 없다.)

sealed 또는 non-sealed 인 class는 abstract class일 수 있으며 abstract member를 가질 수 있다.
sealed class는 final이 아닌 sealed 또는 non-sealed인 경우 abstract subclass를 허용할 수 있다.

class가 sealed class를 extends 하지만 permit 대상 class가 아닌 경우 comile-time error가 발생한다.

Class accessibility

extend와 permit 절은 class name을 사용하기 때문에 permit 된 subclass와 sealed superclass는 서로 access 할 수 있어야 한다.
그러나 permit 된 subclass는 서로 sealed superclass와 동일한 접근성을 가질 필요는 없다.
특히 subclass는 sealed class보다 낮은 접근성을 가질 수 있다.
이는 switch에 의해 pattern matching이 지원되는 향후 릴리스에서 일부 코드는 default 절 (또는 다른 전체 pattern)을 사용하지 않는 한 subclass를 완전히 전환할 수 없다는 것을 의미한다.
Java compiler는 switch가 원래 작성자가 생각했던 것만큼 완전하지 않을 때를 감지하고 error message를 사용자 정의하여 default 절을 추천하도록 권장될 것이다.

Sealed interfaces

interface에서 class에 대해 sealed modifier를 사용하여 interface를 sealed 처리할 수 있다.
superinterface를 지정하기 위한 extends 절 뒤에 implement class와 subinterface가 permits 절로 지정된다.
예를 들면 아래와 같다.

package com.example.expression;

public sealed interface Expr
    permits ConstantExpr, PlusExpr, TimesExpr, NegExpr {...}

public final class ConstantExpr implements Expr {...}
public final class PlusExpr     implements Expr {...}
public final class TimesExpr    implements Expr {...}
public final class NegExpr      implements Expr {...}

다음은 알려진 subclass set이 있는 class hierarchy의 또 다른 예이다.

package com.example.expression;

public sealed interface Expr
    permits ConstantExpr, PlusExpr, TimesExpr, NegExpr { ... }

public final class ConstantExpr implements Expr { ... }
public final class PlusExpr     implements Expr { ... }
public final class TimesExpr    implements Expr { ... }
public final class NegExpr      implements Expr { ... }

Sealing and record classes

sealed class는 record class와 잘 작동한다.
record class는 암시적으로 final이므로 record class의 sealed hierarchy는 위의 예보다 약간 더 간결하다.

package com.example.expression;

public sealed interface Expr
    permits ConstantExpr, PlusExpr, TimesExpr, NegExpr { ... }

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

sealed class와 record class의 조합을 대수 데이터 형식 (algebraic data types)이라고 한다.
record class를 사용하면 product type을 표현할 수 있고 sealed class를 사용하여 sum type을 표현할 수 있다.

Sealed classes and conversions

cast expression은 value를 type으로 변환한다.
type instanceof expression은 type에 대해 value를 test 한다.
Java는 이러한 종류의 expression에 허용되는 유형에 대해 매우 관대하다.
예를 들면

interface I {}
class C {} // does not implement I

void test (C c) {
    if (c instanceof I) 
        System.out.println("It's an I");
}

이 프로그램은 현재 C object가 I interface를 구현하는 것이 불가능하더라도 문법에 오류는 없다.
물론 프로그램이 발전함에 따라 다음과 같은 상황이 발생할 수 있다.

...
class B extends C implements I {}

test(new B()); 
// Prints "It's an I"

type conversion rule은 개방형 확정성이 개념을 가지고 있다.
Java type 시스템은 닫힌 세계를 가정하지 않는다.
class와 interface는 나중에 extends 될 수 있고 casting conversion은 rumtime test로 compile 되므로 안전하게 유연해질 수 있다.

그러나 final class인 경우는 다르다.

interface I {}
final class C {}

void test (C c) {
    if (c instanceof I)     // Compile-time error!
        System.out.println("It's an I");
}

compiler가 C의 subclass가 있을 수 없다는 것을 알기 때문에 method test는 compile하지 못하므로 C값이 I를 구현하는 것은 불가능하다.
compile-time error이다.

만약 C가 final이 아니라 sealed 였다면 직접 subclass는 명시적으로 열거되고 sealed 정의에 의해 동일한 모듈에 있기 때문에 compiler가 유사한 compile-time error를 발견할 수 있는지 확인할 수 있게 된다.

interface I {}
sealed class C permits D {}
final class D extends C {}

void test (C c) {
    if (c instanceof I)     // Compile-time error!
        System.out.println("It's an I");
}

class C는 I를 구현하지 않으며 final이 아니므로 기존 rule에 따라 변환이 가능하다고 결론 내릴 수 있다.
그러나 C는 sealed이고 C의 허용된 직접 subclass인 D가 있다.
sealed type 정의에 따라 D는 final, sealed 또는 non-sealed여야 한다.
이 예에서 C의 모든 직접 subclass는 final이며 I를 구현하지 않는다.
따라서 C의 하위 유형이 있을 수 없기 때문에 이 프로그램은 거부되어야 한다.

반대로 sealed class의 subclass 중 하나가 non-sealed인 경우

interface I {}
sealed class C permits D, E {}
non-sealed class D extends C {}
final class E extends C {}

void test (C c) {
    if (c instanceof I) 
        System.out.println("It's an I");
}

non-sealed D의 하위 유형이 I를 구현할 수 있기 때문에 문법에 오류는 없다.

즉 sealed class를 사용하면 compile 시 어떤 변환이 가능한지에 대해 판단하기 위해 탐색하는 범위를 sealed class hierarchy로 좁힐 수 있게 된다.

Sealed classes in the JDK

JDK에서 sealed class를 사용하는 방법의 예는 JVM entity의 descriptor를 모델링하는 java.lang.constant package에 있다.

package java.lang.constant;

public sealed interface ConstantDesc
    permits String, Integer, Float, Long, Double,
            ClassDesc, MethodTypeDesc, DynamicConstantDesc { ... }

// ClassDesc is designed for subclassing by JDK classes only
public sealed interface ClassDesc extends ConstantDesc
    permits PrimitiveClassDescImpl, ReferenceClassDescImpl { ... }
final class PrimitiveClassDescImpl implements ClassDesc { ... }
final class ReferenceClassDescImpl implements ClassDesc { ... } 

// MethodTypeDesc is designed for subclassing by JDK classes only
public sealed interface MethodTypeDesc extends ConstantDesc
    permits MethodTypeDescImpl { ... }
final class MethodTypeDescImpl implements MethodTypeDesc { ... }

// DynamicConstantDesc is designed for subclassing by user code
public non-sealed abstract class DynamicConstantDesc implements ConstantDesc { ... }

Sealed classes and pattern matching

JEP 406에서는 sealed class의 상당한 이점이 실현될 것이며, JEP 406은 pattern matching으로 switch를 확장할 것을 제안한다.
if-else chain으로 sealed class의 instance를 검사하는 대신 사용자 코드는 pattern으로 강화된 switch를 사용할 수 있다.
sealed class를 사용하면 Java Compiler가 pattern이 완전한지 확인할 수 있다.

예를 들어 이전에 선언된 sealed hierarchy를 사용한 경우

Shape rotate(Shape shape, double angle) {
        if (shape instanceof Circle) return shape;
        else if (shape instanceof Rectangle) return shape;
        else if (shape instanceof Square) return shape;
        else throw new IncompatibleClassChangeError();
}

Java Compiler는 test instance가 Shape의 허용된 모든 subclass를 포함하는지 확인할 수 없다.
마지막 else 절은 실제로 연결할 수 없지만 compiler에서 확인할 수 없다.
또한 Square에 대한 if절이 없어도 compie-time error가 발생하지도 않는다.

반대로 switch에 대한 pattern matching (JEP 406)과 함께 compiler는 Shape의 모든 허용된 subclass가 포함됨을 확인할 수 있으므로 default 절이나 다른 전체 pattern이 필요하지 않다.
또한 compiler는 다음과 같은 세 가지 사례 중 하나가 누락되면 error message를 발생시킨다.

Shape rotate(Shape shape, double angle) {
    return switch (shape) {   // pattern matching switch
        case Circle c    -> c; 
        case Rectangle r -> shape.rotate(angle);
        case Square s    -> shape.rotate(angle);
        // no default needed!
    }
}

덧. 다만 pattern matching for switch는 JDK 17에서는 preview 기능이며 이후 JDK 19 정도 되어야 정식 기능이 될 예정이기 때문에 이번 LTS 버전에서는 이를 위해 sealed와 함께 사용하는 걸 고려하지 않아도 된다.

반응형
profile

파란하늘의 지식창고

@Bluesky_

내용이 유익했다면 광고 배너를 클릭 해주세요