파란하늘의 지식창고
Published 2024. 10. 7. 07:25
JDK 23 New Features Study/Java
반응형

JDK의 버전별 변경 사항은 여기를 참고하세요

Spec

Java SE 23 Platform JSR 398 에 정의된 바와 같이 JSR 398 구현이 목표
실제 Spec은 Final Release Specification 문서를 참고

Final Release Specification Feature Summary

전체 JEP Feature 목록은 OpenJDK의 JDK 23 문서 에서 확인할 수 있다.

JEP Component Feature
JEP 455 specification / language Primitive Types in Patterns, instanceof, and switch (Preview)
JEP 466 core-libs / java.lang.classfile Class-File API (Second Preview)
JEP 467 tools / javadoc(tool) Markdown Documentation Comments
JEP 469 core-libs Vector API (Eighth Incubator)
JEP 473 core-libs / java.util.stream Stream Gatherers (Second Preview)
JEP 471 core-libs Deprecate the Memory-Access Methods in sun.misc.Unsafe for Removal
JEP 474 hotspot / gc ZGC: Generational Mode by Default
JEP 476 specification / language Module Import Declarations (Preview)
JEP 477 specification / language Implicitly Declared Classes and Instance Main Methods (Third Preview)
JEP 480 core-libs Structured Concurrency (Third Preview)
JEP 481 core-libs Scoped Values (Third Preview)
JEP 482 specification / language Flexible Constructor Bodies (Second Preview)

JEP 455: Primitive Types in Patterns, instanceof, and switch (Preview)

Summary

모든 pattern contexts에서 primitive type을 허용하여 pattern matching을 강화하고, 모든 primitive type과 동작하도록 instanceof 및 switch를 확장한다.
preview 기능이다.

Goals

  • primitive type이든 reference type 이든 모든 type에 type pattern을 허용하여 균일한 data 탐색을 가능하게 한다.
  • type pattern을 instanceof 와 결합하고 instanceof 를 safe casting과 결합한다.
  • nested context와 top-level context 모두에서 primitive type pattern을 사용하도록 pattern matching을 허용한다.
  • unsafe cast로 인해 정보가 손실될 위험을 제거하는 easy-to-use construct를 제공한다.
  • java 5 (enum switch ) 및 java 7 (string switch )의 switch 를 개선한 후 모든 primitive type의 값을 처리할 수 있도록 switch 를 허용한다.

Non-Goals

  • Java 언어에 새로운 종류의 conversion을 추가하는 것이 목표는 아니다.Motivation

primitive type과 관련된 여러 제한 사항은 pattern matching, instanceofswitch 를 사용할 때 불편함을 유발한다.
이러한 제한 사항을 제거하면 Java 언어가 더 균일하고 표현력이 향상된다.

Pattern matching for switch does not support primitive type patterns

첫 번째 제한 사항은 switch (JEP 441)에 대한 pattern matching이 primitive type pattern, 즉 primitive type을 지정하는 type pattern을 지원하지 않는다는 것이다.
case Integer i 또는 case String s 와 같이 reference type을 지정하는 type pattern만 지원된다. (Java 21부터 swtich 에 record pattern (JEP 440)도 지원된다.)

switch 의 primitive type pattern 지원을 통해 switch expression을 개선할 수 있다:

switch (x.getStatus()) {
    case 0 -> "okay";
    case 1 -> "warning";
    case 2 -> "error";
    default -> "unknown status: " + x.getStatus();
}

일치하는 값을 노출하는 primitive type pattern을 사용하여 default clause를 case clause로 전환한다:

switch (x.getStatus()) {
    case 0 -> "okay";
    case 1 -> "warning";
    case 2 -> "error";
    case int i -> "unknown status: " + i;
}

primitive type pattern을 지원하면 일치하는 값을 검사하는 것을 보호할 수 있다:

switch (x.getYearlyFlights()) {
    case 0 -> ...;
    case 1 -> ...;
    case 2 -> issueDiscount();
    case int i when i >= 100 -> issueGoldCard();
    case int i -> ... appropriate action when i > 2 && i < 100 ...
}

Record patterns have limited support for primitive types

또 다른 제한 사항은 record pattern이 primitive type에 대한 지원을 제한한다는 것이다.
record pattern은 record를 개별 component로 decompose(분해)하여 data 처리를 간소화한다.
component가 primitive value인 경우 record pattern은 value type에 대해 정확해야 한다.
이는 개발자에게 불편하며 나머지 Java 언어에 유용한 자동 변환 기능이 있다는 것과도 일치하지 않는다.

예를 들어, 다음 record class를 통해 표현된 JSON data를 처리한다고 가정해보자:

sealed interface JsonValue {
    record JsonString(String s) implements JsonValue { }
    record JsonNumber(double d) implements JsonValue { }
    record JsonObject(Map<String, JsonValue> map) implements JsonValue { }
}

JSON은 integer와 non-integer를 구분하지 않으므로 JsonNumber 는 유연성을 극대화하기 위해 double component가 있는 number를 나타낸다.
그러나 JsonNumber record를 생성할 때 double 을 전달할 필요는 없다.
30과 같은 int 를 전달할 수 있으며 Java compiler는 자동으로 intdouble 로 확장한다:

var json = new JsonObject(Map.of("name", new JsonString("John"),
                                 "age",  new JsonNumber(30)));

불행하게도 JsonNumber 를 record pattern으로 decompose 하려는 경우 java compiler는 그다지 의무적이지 않다.
JsonNumberdouble component로 선언되므로 double 에 대해 JsonNumber 를 decompose하고 수동으로 int 로 변환해야 한다.

if (json instanceof JsonObject(var map)
    && map.get("name") instanceof JsonString(String n)
    && map.get("age")  instanceof JsonNumber(double a)) {
    int age = (int)a;  // unavoidable (and potentially lossy!) cast
}

즉, primitive type pattern은 record pattern 내에 중첩될 수 있지만 변하지 않는다.
pattern의 primitive type은 record component의 primitive type과 동일해야 한다.
instanceof JsonNumber(int age) 를 통해 JsonNumber 를 decompose 하고 compiler가 자동으로 double component를 int 로 좁히도록 하는 것은 불가능하다.

이러한 제한이 적용되는 이유는 축소 시 손실이 발생할 수 있기 때문이다.
runtime 시 double component의 값은 int variable에 비해 너무 크거나 정밀도가 너무 높을 수 있다.
그러나 pattern matching의 주요 이점은 단순히 일치하지 않음으로써 잘못된 값을 자동으로 거부한다는 것이다.
JsonNumberdouble component가 너무 크거나 너무 정확하여 int 로 안전하게 범위를 좁힐 수 없는 경우, instanceof JsonNumber(int age) 는 단순히 false 를 반환하여 프로그램이 다른 분기에서 큰 double component를 처리하도록 남겨둘 수 있다.

이는 reference type pattern에 대해 pattern matching이 이미 작동하는 방식이다.
예를 들어:

record Box(Object o) {}
var b = new Box(...);

if (b instanceof Box(RedBall rb)) ...
else if (b instanceof Box(BlueBall bb)) ...
else ....

여기에서 Box component는 Object type으로 선언되었지만 instanceofRedBall component 또는 BlueBall component와 Box 를 일치시키는 데 사용할 수 있다.
record pattern Box(RedBall rb)b 가 runtime 시 Box 이고 o component가 RedBall 로 범위가 좁아질 수 있는 경우에만 일치한다.
마찬가지로 Box(BlueBall bb) 는 해당 o component 를 BlueBall 로 좁힐 수 있는 경우에만 일치한다.

record pattern에서 primitive type pattern은 reference type pattern만큼 원활하게 작동해야 하며, 해당 record component가 int 가 아닌 numeric primitive type인 경우에도 JsonNumber(int age) 를 허용해야 한다.
이렇게 하면 pattern을 matching한 후 장황하고 잠재적으로 손실이 많은 cast가 필요하지 않다.

Pattern matching for instanceof does not support primitive types

또 다른 제한 사항은 instanceof (JEP 394) 에 대한 pattern matching이 primitive type pattern을 지원하지 않는다는 것이다.
reference type을 지정하는 type pattern만 지원된다. (Java 21 부터는 instanceof 에도 record pattern이 지원됨)

primitive type pattern은 switch 와 마찬가지로 instanceof 에서도 유용하다.
instanceof 의 목적은 광범위하게 말하면 값을 주어진 type으로 안전하게 cast할 수 있는지 테스트하는 것이다.
그렇기 때문에 instanceof 와 cast operation을 항상 밀접하게 볼 수 있다.
이 테스트는 primitive value를 한 type에서 다른 type으로 변환할 때 발생할 수 있는 정보 손실 가능성 때문에 primitive type이 매우 중요하다.

예를 들어 int value를 float 로 변환하는 작업은 잠재적으로 손실이 있을지라도 할당 문에 의해 자동으로 수행되며 개발자는 이에 대한 경고를 받지 않는다.

int getPopulation() {...}
float pop = getPopulation();  // silent potential loss of information

한편 int value를 byte 로 변환하는 것은 명시적 cast를 통해 수행되지만 cast는 손실 가능성이 있으므로 번거로운 범위 검사가 선행되어야 한다.

if (i >= -128 && i <= 127) {
    byte b = (byte)i;
    ... b ...
}

instanceof 의 primitive type pattern은 Java 언어의 내장된 손실 변환을 대체하고 개발자가 거의 30년 동안 수작업으로 코딩해 온 번거로운 범위 검사를 피할 수 있다.
즉, instanceof 는 type 뿐만 아니라 value도 확인할 수 있다.
위의 두 예는 다음과 같이 다시 작성할 수 있다:

if (getPopulation() instanceof float pop) {
    ... pop ...
}

if (i instanceof byte b) {
    ... b ...
}

instanceof operator는 할당문의 편리함과 pattern matching의 안정성을 결합한다.
input (getPopulation() 또는 i ) 이 primitive type pattern의 type으로 안전하게 변환될 수 있는 경우 pattern이 일치하고 변환 결과를 즉시 사용할 수 있다. (pop 또는 b )
그러나 변환 시 정보가 손실되면 pattern이 일치하지 않으며 프로그램은 다른 분기에서 잘못된 입력을 처리해야 한다.

Primitive types in instanceof and switch

primitive type pattern 에 대한 제한을 해제하려는 경우 관련 제한을 해제하는 것이 도움이 될 것이다.
instanceof 가 pattern이 아닌 type을 취하는 경우 primitive type이 아닌 reference type만 취한다.
primitive type을 사용할 때 instanceof 는 변환이 안전한지 확인하지만 실제로 수행하지는 않는다:

if (i instanceof byte) {  // value of i fits in a byte
    ... (byte)i ...       // traditional cast required
}

instanceof 에 대한 이러한 개선은 instanceof Tinstanceof T t 의 의미 체계 간의 정렬을 복원하며, 이는 한 context에서는 primitive type을 허용하지만 다른 context에서는 허용하지 않는 경우 손실될 수 있다.

마지막으로, switchbyte , short , charint value를 사용할 수 있지만 boolean , float 또는 long value는 사용할 수 없다는 제한을 해제하는 것이 도움이 될 것이다.

boolean 값을 switching 하는 것은 삼항 조건 연산자 ( ?: ) 에 대한 유용한 대안이 될 수 있다.
boolean switch에는 표현식 뿐만 아니라 명령문도 포함될 수 있기 때문이다.
예를 들어 다음 코드는 false 인 경우 boolean switch 를 사용하여 일부 로깅을 수행한다:

startProcessing(OrderStatus.NEW, switch (user.isLoggedIn()) {
    case true  -> user.id();
    case false -> { log("Unrecognized user"); yield -1; }
});

long value 를 switching 하면 case label 이 long constant 가 될 수 있으므로 별도의 if 문을 사용하여 매우 큰 constant를 처리할 필요가 없다:

long v = ...;
switch (v) {
    case 1L              -> ...;
    case 2L              -> ...;
    case 10_000_000_000L -> ...;
    case 20_000_000_000L -> ...;
    case long x          -> ... x ...;
}

Description

Java 21에서 primitive type pattern은 다음과 같이 record pattern의 중첩 패턴으로만 허용된다.

v instanceof JsonNumber(double a)

pattern matching을 통해 일치 후보 v 의 보다 균일한 데이터 탐색을 지원하기 위해 다음을 수행한다:

  1. Extend pattern matching so that primitive type patterns are applicable to a wider range of match candidate types. This will allow expressions such as v instanceof JsonNumber(int age).
  2. Enhance the instanceof and switch constructs to support primitive type patterns as top level patterns.
  3. Further enhance the instanceof construct so that, when used for type testing rather than pattern matching, it can test against all types, not just reference types. This will extend instanceof's current role, as the precondition for safe casting on reference types, to apply to all types.
    More broadly, this means that instanceof can safeguard all conversions, whether the match candidate is having its type tested (e.g., x instanceof int, or y instanceof String) or having its value matched (e.g., x instanceof int i, or y instanceof String s).
  4. Further enhance the switch construct so that it works with all primitive types, not just a subset of the integral primitive types.
  5. primitive type pattern이 더 넓은 범위의 일치 후보 type에 적용 가능하도록 pattern matching을 확장한다.
    그러면 v instanceof JsonNumber(int age) 와 같은 표현식이 허용된다.
  6. instanceofswitch 구문이 top level pattern으로 primitive type pattern을 지원하도록 개선되었다.
  7. pattern matching이 아닌 type test에 사용될 때 reference type 뿐 아니라 모든 type에 대해 테스트할 수 있도록 instanceof 구문을 더욱 개선한다.
    이는 reference type 에 대한 안전한 casting을 위한 전재 조건으로서 모든 type에 적용되도록 instanceof 의 현재 역할을 확장한다.
  8. 더 넓게 보면, 이는 instanceof 가 match 후보의 type을 테스트하던(예: x instanceof int 또는 y instanceof String ), value를 match 하던 (예: x instanceof int i 또는 y instanceof String s ) 모든 변환을 보호할 수 있다는 의미이다.
  9. integral primitive type의 하위 집합 뿐만 아니라 모든 primitive type에서 동작하도록 switch construct를 더욱 개선한다.

이러한 변경은 primitive type 사용을 관리하는 Java 언어의 몇 가지 규칙을 변경하는 방식으로 이루어진다.

  • instanceofswitch 구성에서 primitive type 및 primitive type pattern에 대한 제한을 없앤다.
  • 모든 primitive type의 literal에 대한 constant case를 처리하도록 switch 를 확장한다.
  • 한 type에서 다른 type으로 cast가 안전한지 특성화하려면 변환할 값과 변환의 소스 및 대상 type에 대한 지식이 포함된다.Safety of conversions

정보 손실이 발생하지 않으면 변환은 exact (정확) 한 것이다.
변환이 정확한지 여부는 관련된 type 쌍과 input value에 따라 달라진다:

  • 일부 쌍의 경우 compile 시 첫 번째 type에서 두 번째 type으로 변환이 어떤 값에 대해서도 정보를 잃지 않도록 보장된다는 것이 알려져 있다.
    이러한 변환은 unconditionally exact (무조건 정확)하다고 한다.
    무조건 정확한 변환을 위해 runtime에 아무런 작업이 필요하지 않다.
    byte 에서 int 로, int 에서 long 으로, String 에서 Object 로 변환하는 것이 그 예이다.
  • 다른 쌍의 경우 run-time test를 통해 정보 손실 없이 값이 첫 번째 type에서 두 번째 type으로 변환될 수 있는지 또는 casting이 수행되는 경우 예외를 발생시키지 않고 변환할 수 있는지 확인해야 한다.
    정보 손실이나 예외가 발생하지 않으면 변환이 정확한 것이고, 그렇지 않으면 변환이 정확하지 않다.
    정확할 수 있는 변환의 예로는 long 에서 int 로, int 에서 float 로 변환할 때 각각 숫자 동일성 ( ' == ' ) 또는 표현 동등성을 사용하여 run-time에도 정밀도 손실을 감지하는 것이 있다.
    객체에서 문자열로 변환하는 경우에도 run-time test가 필요하며 input value가 동적으로 String인지 여부에 따라 변환이 정확하거나 정확하지 않을 수 있다.

간단히 말해, primitive type 간의 변환은 하나의 integral type에서 다른 integral type으로, 또는 하나의 floating-point type에서 다른 floating-point type으로, byte , short 또는 char 에서 floating-point type으로 , 또는 int 에서 double 로 넓혀지는 경우 무조건 정확하다.
또한 boxing 변환과 넓어지는 reference 변환은 무조건 정확하다.

다음 표는 primitive type 간 허용되는 변환을 나타낸다.
무조건 정확한 변환은 기호 ɛ 로 표시된다.
기호 는 동일성 변환을 ω 는 넓어지는 primitive 변환을, η 는 좁아지는 primitive 변환을, ωη 는 넓어지고 좁아지는 primitive 변환을 의미한다.
기호 는 변환이 허용되지 않음을 의미한다.

To → byte short char int long float double boolean
From ↓                
byte ɛ ωη ɛ ɛ ɛ ɛ
short η η ɛ ɛ ɛ ɛ
char η η ɛ ɛ ɛ ɛ
int η η η ɛ ω ɛ
long η η η η ω ω
float η η η η η ɛ
double η η η η η η
boolean

이 표와 JLS §5.5 의 해당 표를 비교하면, JLS §5.5에서 ω 가 허용하는 많은 변환이 위의 무조건 정확한 ɛ 로 "업그레이드된" 것을 알 수 있다.

instanceof as the precondition for safe casting

instanceof 를 사용한 type test는 전통적으로 reference type으로 제한된다.
instanceof 의 고전적인 의미는 다음과 같은 질문을 하는 전제 조건 검사이다:
이 value를 이 type으로 cast 하는 것이 안전하고 유용한가?
이 질문은 reference type 보다 primitive type에 훨씬 더 관련이 있다.
reference type의 경우 실수로 검사가 생략된 경우 unsafe cast를 수행해도 아무런 문제가 발생하지 않는다.
ClassCastException 이 발생하고 부적절하게 casting 된 값은 사용할 수 없게 된다.
반대로 안전성을 확인할 수 있는 편리한 방법이 없는 primitive type의 경우 unsafe cast를 수행하면 미묘한 버그가 발생할 가능성이 높다.
exception을 throw 하는 대신 크기, 부호 또는 정밀도와 같은 정보를 자동으로 손실하여 잘못 캐스팅 된 값이 프로그램의 나머지 부분으로 유입될 수 있다.

instanceof type test 연산자에서 primitive type을 사용하려면 왼쪽 피연산자의 type이 reference type이어야 하고, 오른쪽 피연산자가 reference type을 지정해야 한다는 제한 (JLS §15.20.2) 을 제거한다.
type test 연산자는 다음과 같이 된다.

InstanceofExpression:
    RelationalExpression instanceof Type
    ...

run time에는 정확한 변환에 호소하여 instanceof 를 primitive type으로 확장한다:
왼쪽의 값을 정확한 변환을 통해 오른쪽의 type으로 변환할 수 있다면 해당 type을 값을 casting하는 것이 안전하며 instanceoftrue 를 보고한다.

Here are some examples of how the extended instanceof can safeguard casting. Unconditionally exact conversions return true regardless of the input value; all other conversions require a run-time test whose result is shown.

다음은 확장된 instanceof 가 어떻게 casting을 보호하는지에 대한 몇 가지 예시이다.
무조건 정확한 변환은 입력 값에 관계없이 true 를 반환하고, 다른 모든 변환은 run-time test가 필요하며 그 결과가 표시된다.

byte b = 42;
b instanceof int;         // true (unconditionally exact)

int i = 42;
i instanceof byte;        // true (exact)

int i = 1000;
i instanceof byte;        // false (not exact)

int i = 16_777_217;       // 2^24 + 1
i instanceof float;       // false (not exact)
i instanceof double;      // true (unconditionally exact)
i instanceof Integer;     // true (unconditionally exact)
i instanceof Number;      // true (unconditionally exact)

float f = 1000.0f;
f instanceof byte;        // false
f instanceof int;         // true (exact)
f instanceof double;      // true (unconditionally exact)

double d = 1000.0d;
d instanceof byte;        // false
d instanceof int;         // true (exact)
d instanceof float;       // true (exact)

Integer ii = 1000;
ii instanceof int;        // true (exact)
ii instanceof float;      // true (exact)
ii instanceof double;     // true (exact)

Integer ii = 16_777_217;
ii instanceof float;      // false (not exact)
ii instanceof double;     // true (exact)

당사는 Java 언어에 새로운 변환을 추가하거나 기존 변환을 변경하지 않으며, 할당과 같은 기존 context에서 어떤 변환이 허용되는지 변경하지 않는다.
instanceof 가 주어진 value와 type에 적용 가능한지 여부는 casting context에서 변환이 허용되는지 여부와 변환이 정확한지 여부에 따라 결정된다.
예를 들어 bboolean 변수인 경우 boolean 에서 char 로 casting 변환이 허용되지 않으므로 b instanceof char 는 허용되지 않는다.

Primitive type patterns in instanceof and switch

type pattern은 type test와 조건부 변환를 병합한다.
이렇게 하면 type test가 성공하면 명시적으로 type을 cast 할 필요가 없고, type test가 실패하면 cast 되지 않은 값을 다른 branch에서 처리할 수 있다.
instanceof type test 연산자가 reference type만 지원했을 때는 instanceofswitch 에서 reference type pattern만 허용하는 것이 당연했다.
이제 instanceof type test 연산자가 primitive type을 지원하므로 instanceofswitch 도 당연히 primitive type을 허용한다.

To achieve this, we drop the restriction that primitive types cannot be used in a top level type pattern. As a result, the laborious and error-prone code
이를 위해 primitive type은 top level type pattern에서 사용할 수 없다는 제한을 없앴다.
그 결과, 힘들고 오류가 발생하기 쉬운 코드인

int i = 1000;
if (i instanceof byte) {    // false -- i cannot be converted exactly to byte
    byte b = (byte)i;       // potentially lossy
    ... b ...
}

는 다음과 같이 작성할 수 있다.

if (i instanceof byte b) {
    ... b ...               // no loss of information
}

i instanceof byte b 는 "i instanceof byte 인지 테스트하고 그렇다면 ibyte 로 cast하고 해당 값을 b 에 바인딩" 을 의미한다.

type pattern의 의미는 applicability, unconditionality 및 matching의 세 가지 술어로 정의된다.
primitive type pattern의 처리에 대한 제한을 다음과 같이 해제한다.

  • Applicability 는 compile 시점에 pattern이 적합한지 여부이다.
    이전에는 primitive type pattern을 적용하려면 input 표현식이 pattern의 type과 정확히 동일한 type을 가져야 했다.
    예를 들어 switch (... an int ...) { case double d: ... }double pattern이 int 에 적용되지 않기 때문에 허용되지 않았다.
  • 이제 unchecked warning 없이 UT 로 casting 할 수 있는 경우 type pattern T t 는 type U 의 일치 후보에 적용할 수 있다.
    이제 intdouble 로 cast 할 수 있으므로 해당 전환이 적합하다.
  • Unconditionality 는 compile 시점에 해당 pattern이 match 후보의 가능한 모든 run-time value와 일치한다는 것을 알 수 있는지 여부이다.
    unconditional pattern은 run-time check가 필요하지 않다.
  • primitive type pattern은 더 많은 type에 적용할 수 있도록 확장할 때 무조건적으로 적용되는지 지정해야 한다.
    type T 에 대한 primitive type pattern은 U 에서 T 로의 변환이 무조건적으로 정확할 경우 type U 의 일치 후보에 대해 무조건적이다.
    이는 input value와 관계없이 무조건 정확한 변환이 safe 하기 때문이다.
  • 이전에는 null 참조가 아닌 값 vClassCastException 을 던지지 않고 vT 로 cast 할 수 있는 경우 T type의 type pattern과 일치했다.
    이러한 일치의 정의는 primitivie type pattern의 역할이 제한적이었을 때 충분했다.
    이제 primitive type pattern이 광범위하게 사용될 수 있게 되면서 matching은 value를 T 로 정확히 casting 할 수 있다는 의미로 일반화되었으며, 이는 ClassCastexception 발생과 잠재적인 정보 손실도 포함하게 되었다.

Exhaustiveness

switch 표현식 또는 case label이 pattern인 switch 문은 완전(exhaustive) 해야한다.
selector 표현식의 가능한 모든 value를 switch block에서 처리해야 한다.
switch 는 무조건적인 type pattern을 포함하는 경우 포괄적이며, sealed class의 허용가능한 모든 subtype을 포함하는 것과 같은 다른 이유로도 포괄적일 수 있다.
어떤 상황에서는 어떤 경우에도 일치하지 않는 runtime value가 있을 수 있는데도 switch 가 완전한 것으로 간주될 수 있으며, 이러한 경우 Javacompiler는 이러한 예기치 않은 input을 처리하기 위해 synthetic default 절을 삽입한다.
완전성(Exhaustiveness) 은 Patterns: Exhaustiveness, Unconditionality, and Remainder 에서 좀 더 자세히 설명한다.

primitive type pattern이 도입됨에 따라 소진 여부를 결정하는데 새로운 규칙이 하나 추거되었다:
primitive type P 에 대한 wrapper type W 가 일치 후보인 switch 가 주어졌을 때 type pattern TP 에서 무조건 정확하면 TW 를 소진한다.
이 경우 null 은 나머지 부분의 일부가 된다.
다음 예제에서 일치 후보가 primitive type byte 의 wrapper type 이며 byte 에서 int 로의 변환은 무조건 정확하다.
결과적으로 다음 switch 는 완전하다:

Byte b = ...
switch (b) {             // exhaustive switch
    case int p -> 0;
}

이 동작은 record pattern의 완전성 처리와 유사하다.

switch 에서 pattern 완전성을 사용하여 case가 모든 input value를 포함하는지 확인하는 것처럼, switch 에서는 지배력(dominance)를 사용하여 input value와 일치하지 않는 경우가 있는지 확인한다.

한 pattern이 다른 pattern과 일치하는 모든 value와 일치하면 다른 pattern이 지배적( dominates )이다.
예를 들어 type pattern Object oString s 와 일치하는 모든 것이 Object o 와 일치하기 때문에 type pattern String s 를 지배한다.
switch 에서 보호되지 않는 type pattern P 를 가진 case label이 type pattern Q 를 가진 case label 보다 앞서는 것은 올바르지 않다. (PQ 를 지배하는 경우)
우위의 의미는 변하지 않는다:
type pattern T t 가 type U 의 일치 후보에 대해 무조건적일 경우 type pattern T t 가 type pattern U u 를 지배한다.

Expanded primitive support in switch

switch construct를 개선하여 long, float, double, 및 boolean type의 selector 표현식과 해당 boxed type을 사용할 수 있도록 했다.

selector 표현식의 type이 long, float, double, 또는 boolean 인 경우 case label에 사용되는 constant는 selector 표현식과 동일한 type이거나 해당 boxed type이어야 한다.
예를 들어 selector 표현식의 type이 fload 또는 Float 인 경우 모든 case constant는 float type의 부동 소수점(floating-point) literal이어야 한다. (JLS §3.10.2)
case constant와 selector 표현식이 일치하지 않으면 손실 변환이 발생하여 프로그래머의 의도가 훼손될 수 있으므로 이 제한이 필요하다.
The following switch is legal, but it would be illegal if the 0f constant were accidentally written as 0.
다음 switch 는 적합하지만 0f constant가 실수로 0 으로 기록되면 올바르지 않다.

float v = ...
switch (v) {
    case 0f -> 5f;
    case float x when x == 1f -> 6f + x;
    case float x -> 7f + x;
}

case label에서 floating-pint literal의 의미는 compile time과 run time의 표현 동등성(representation equivalence ) 측면에서 정의된다.
표현이 동등한 두 floating-point literal을 사용하는 것은 compile time error이다.
For example, the following switch is illegal because the literal 0.999999999f is rounded up to 1.0f, creating a duplicate case label.
예를 들어 다음 switch 는 literal 0.999999999f1.0f 로 반올림되어 case label이 중복되므로 올바르지 않다.

float v = ...
switch (v) {
    case 1.0f -> ...
    case 0.999999999f -> ...    // error: duplicate label
    default -> ...
}

Since the boolean type has only two distinct values, a switch that lists both the true and false cases is considered exhaustive. The following switch is legal, but it would be illegal if there were a default clause.
boolean type에는 두 개의 고유한 값만 있으므로 truefalse 의 경우를 모두 나열하는 switch 는 완전한 것으로 간주된다.
다음 switch 는 적합하지만 default case가 있으면 적합하지 않다.

boolean v = ...
switch (v) {
    case true -> ...
    case false -> ...
    // Alternatively: case true, false -> ...
}

JEP 467: Markdown Documentation Comments

JavaDoc 문서 주석 작성 시 기존 HTML 과 JavaDoc @ -tag 뿐만 아니라 Markdown 도 지원하게 된다.

자세한 설명은 생락하며 해당 JEP 467 문서에서 관련 내용을 확인하면 된다.

JEP 473: Stream Gatherers (Second Preview)

기존에 제안되었던 Stream Gatherers preview ( JEP 461 ) 에 대해 추가 경험과 피드백을 얻기 위해 내용 변경 없이 다시 Second Preview를 제안되었다.
이전 JDK 22 New Features의 관련 내용 참조

JEP 474: ZGC: Generational Mode by Default

Z Garbage Coollector (ZGC)의 default mode를 generational mode로 전환한다.
향후 release에서 non-generational mode를 제거하기 위해 non-generational mode는 더 이상 사용하지 않는다.

JEP 476: Module Import Declarations (Preview)

Summary

module 에서 export한 모든 package를 간결하게 import 할 수 있는 기능으로 Java programming 언어를 개선할 수 있다.
이렇게 하면 modular library의 재사용이 간소화되지만 importing code가 module 자체에 있을 필요는 없다.
이것은 preview 언어 기능 이다.

Goals

  • 전체 module을 한 번에 import 할 수 있도록 하여 modular library의 재사용을 간소화한다.-
  • module에서 export한 API의 다양한 부분을 사용할 때 여러 개의 type-import-on-demand 선언 (예 : import com.foo.bar.* ) 로 인한 잡음을 피한다.
  • 초보자도 package hierarchy 에서 해당 library의 위치를 학습할 필요 없이 third-party library와 기본 java class를 더 쉽게 사용할 수 있다.
  • module import 기능을 사용하는 개발자에게 자체 코드를 modularize 할 것을 요구하지 않는다.

Motivation

Object, String, 및 Comparable 같은 java.lang package의 class와 interface는 모든 java program에 필수적이다.
이러한 이유로 Java compiler는 요청 시 java.lang package의 모든 class와 interface를 다음과 같이 자동으로 import한다.

import java.lang.*;

이는 모든 source file의 시작 부분에 나타난다.

Java Platform이 발전함에 따라 List, Map, Stream, 및 Path 와 같은 class와 interface는 거의 필수적인 요소가 되었다.
그러나 이중 어느 것도 java.lang 에 포함되어 있지 않으므로 자동으로 import 되지 않으며, 개발자는 모든 source file의 시작 부분에 수많은 import 선언을 작성하여 complier를 만족시켜야 한다.
예를 들어, 다음 코드는 문자열 배열을 대문자에서 소문자로 변환하지만, import에는 코드만큼이나 많은 줄이 필요하다:

import java.util.Map;                   // or import java.util.*;
import java.util.function.Function;     // or import java.util.function.*;
import java.util.stream.Collectors;     // or import java.util.stream.*;
import java.util.stream.Stream;         // (can be removed)

String[] fruits = new String[] { "apple", "berry", "citrus" };
Map<String, String> m =
    Stream.of(fruits)
          .collect(Collectors.toMap(s -> s.toUpperCase().substring(0,1),
                                    Function.identity()));

개발자들은 single-type-import 또는 type-import-on-demand 선언을 선언할지 여부에 대해 다양한 견해를 가지고 있다.
명확성이 가장 중요한 대규모의 성숙한 codebase에서는 single-type import를 선호 )하는 경우가 많다.
그러나 편의성이 명확성보다 우선시되는 초기 단계에서는 개발자가 on-demand import를 선호하는 경우가 많다.
예를 들면

Java 9부터는 module을 통해 package set을 그룹화하여 단일 이름으로 재사용 할 수 있다.
module의 export한 package는 일관되고 일관된 API를 형성하기 위한 것이므로 개발자가 전체 module, 즉 module에서 내보낸 모든 package에서 필요에 따라 가져올 수 있다면 편리할 것이다.
마치 내보낸 모든 package를 한번에 가져오는 것과 같다.

예를 들어, java.base module을 on-demand로 import 하면 java.util 을 on-demand로, java.util.stream을 on-demand로 수동으로 import 하지 않아도 List, Map, Stream, 및 Path 에 즉시 접근할 수 있다.

module level에서 import 기능은 한 module의 API가 다른 module의 API와 밀접한 관계가 있는 경우 특히 유용하다.
이는 JDK와 같은 대규모 multi-module library에서 흔히 볼 수 있다.
예를 들어, java.sql module은 java.sqljavax.sql package를 통해 database access를 제공하지만, 그 interface 중 하나인 java.sql.SQLXMLjava.xml module의 javax.xml.transform package의 insterface를 사용하는 signature를 가진 public method를 선언한다.
java.sql.SQLXML 에서 이러한 method를 호출하는 개발자는 일반적으로 java.sql.SQLXML package와 javax.xml.transform package를 모두 가져온다.
이 extra import를 용이하게 하기 위해 java.sql module은 java.xml module에 일시적( transitively )으로 종속되므로 java.sql module에 종속된 program은 on-demand 방식으로 import하면 java.xml module도 on-demand방식으로 자동으로 import 하면 편리할 것이다.
이 시나리오에서는 java.sql module을 on-demand로 import하면 java.xml module도 on-demand로 자동으로 가져오면 편리할 것이다.
transitive dependencies에서 필요에 따라 자동으로 import하면 prototype을 만들고 탐색할 때 더욱 편리할 것이다.

Description

module import 선언 의 형태는 다음과 같다.

import module M;

필요에 따라 다음과 같은 모든 public top-level class와 interface를 가져온다.

  • module M 에서 현재 module로 export 한 package와
  • module M 을 읽음으로써 현재 module에서 읽은 module이 export한 package

두 번째 절은 프로그램이 다른 모듈의 class 및 interface를 참조할 수 있는 module의 API를 다른 모듈을 가져오지 않고도, 사용할 수 있게 해준다.

예를 들면:

  • import module java.basejava.base module에서 export 한 각 packages 에 대해 하나씩 54개의 on-demand package import와 동일한 효과를 갖는다.
    이는 마치 source file에 import java.io.* , import java.util.* 등이 포함된 것과 같다.
  • import module java.sqlimport java.sql.*import javax.sql.* 와 동일한 효과에 java.sql module의 간접 export 를 위한 on-demand package import를 더한다.

This is a preview language feature, disabled by default

JDK 23에서 아래 예제를 사용해 보려면 preview 기능을 활성화해야 한다.

  • javac --release 23 --enable-preview Main.java 로 프로그램을 compile 하고 java --enable-preview Main 로 실행하거나
  • source code launcher 를 사용하는 경우 java --enable-preview Main.java 또는
  • jshell 을 사용하는 경우 jshell --enable-preview 로 시작한다.

Syntax and semantics

We extend the grammar of import declarations (JLS §7.5) to include import module clauses:
import module 절을 포함하도록 import 선언 문법(JLS §7.5) 을 확장한다:

ImportDeclaration:
  SingleTypeImportDeclaration
  TypeImportOnDemandDeclaration
  SingleStaticImportDeclaration
  StaticImportOnDemandDeclaration
  ModuleImportDeclaration

ModuleImportDeclaration:
  import module ModuleName;

import module 은 module name을 사용하므로 name이 지정되지 않은 module, 즉 class path에서 package를 import 할 수 없다.
이는 module name을 사용하고 이름 없는 module에 대한 종속성을 표현할 수 없는 module 선언, 즉 module-info.java 파일의 require 절과 일치한다.

import module 은 모든 source file에서 사용할 수 있다.
source file이 명시적인 module과 연결되어 있지 않아도 된다.
예를 들어 java.basejava.sql 은 표준 Java runtime의 일부이며 module로 개발되지 않은 프로그램에서 import 할 수 있다.
(기술적 배경은 JEP 261 참조)

package를 export 하지 않는 module은 package를 export하는 다른 module을 일시적으로 필요로 하기 때문에 module을 import 하는 것이 유용할 때가 있다.
예를 들어 java.se module은 package를 export 하지는 않지만 19개의 다른 module을 전이적으로(transitively) 필요로 하므로 import module java.se 효과는 해당 module에서 export하는 package, 즉 java.se module의 간접 export 로 나열된 123개의 package를 재귀적으로 가져오는 것이다.
import module java.se 는 이미 requires java.se 가 명명된 module의 compile 단위에서만 가능하다는 점에 유의하라.
class를 암시적으로 선언하는 것 과 같은 명명되지 않은 module의 complie 단위에서는 import module java.se 를 사용할 수 없다.

Ambiguous imports

module을 import하면 여러 package를 import 하는 효과가 있으므로 다른 package에서 동일한 simple name을 가진 class를 import할 수 있다.
simple name은 모호하므로 이를 사용하면 compile-time error가 발생한다.

예를 들어 이 source file에서 simple name Element 는 모호하다.

import module java.desktop;   // exports javax.swing.text,
                              // which has a public Element interface,
                              // and also exports javax.swing.text.html.parser,
                              // which has a public Element class

...
Element e = ...               // Error - Ambiguous name!
...

또 다른 예로, 이 source file에서 simple name List 는 모호하다.

import module java.base;      // exports java.util, which has a public List interface
import module java.desktop;   // exports java.awt, which a public List class

...
List l = ...                  // Error - Ambiguous name!
...

마지막 예로, 이 source file에서 simple name Date 는 모호하다.

import module java.base;      // exports java.util, which has a public Date class
import module java.sql;       // exports java.sql, which has a public Date class

...
Date d = ...                  // Error - Ambiguous name!
...

모호한 문제를 해결하는 방법은 간단하다.
single-type-import 선언을 사용하면 된다.
예를 들어 이전 예제의 모호한 Date 를 해결하려면 다음과 같이 한다:

import module java.base;      // exports java.util, which has a public Date class
import module java.sql;       // exports java.sql, which has a public Date class

import java.sql.Date;         // resolve the ambiguity of the simple name Date!

...
Date d = ...                  // Ok!  Date is resolved to java.sql.Date
...

A worked example

다음은 import module 이 어떻게 동작하는지 보여주는 예이다.
C.java 가 module M0 과 연결된 source file이라고 가정한다:

// C.java
package q;
import module M1;             // What does this import?
class C { ... }

여기서 module M0 에는 다음과 같은 선언이 있다:

module M0 { requires M1; }

import module M1 의 의미는 M1 의 export 및 M1 이 전이적으로(transitively) 필요로 하는 module에 따라 달라진다.

module M1 {
    exports p1;
    exports p2 to M0;
    exports p3 to M3;
    requires transitive M4;
    requires M5;
}

module M3 { ... }

module M4 { exports p10; }

module M5 { exports p11; }

import module M1 의 효과는 다음과 같다.

  • M1p1 을 모든 사람에게 export 하므로, package p1 에서 public top level class 및 interface를 가져온다.
  • M1p2C.java 가 연결된 module인 M0 으로 export 하므로 package p2 에서 public top level class 및 interface를 가져온다.
  • Import the public top level classes and interfaces from package p10, since M1 requires transitively M4, which exports p10.
  • M1p10 을 export 하는 M4 를 전이적으로 필요로 하므로 package p10 에서 public top level class 및 interface를 가져온다.

p3 또는 p11 package에서 C.java 로 import 한 것은 없다.

Implicitly declared classes

이 JEP는 JEP 477: Implicitly Declared Classes and Instance main Methods 와 함께 개발되었으며 java.base module 에서 export 한 모든 package의 모든 public top level class 및 interface가 암시적으로 선언된 class에서 on-demand로 자동 import 하도록 지정한다.
즉, 모든 일반 class의 시작 부분에 import module java.base 가 나타나는 반면, 모든 class의 시작 부분에 import java.lang.* 이 나타나는 것과 마찬가지이다.

JShell tool 은 필요에 따라 10개의 package를 자동으로 import 한다.
package 목록은 ad-hoc (임시적) 이다.
따라서 JShell을 자동으로 import module java.base로 변경할 것을 제안한다.

Alternatives

  • import module ... 의 대안은 java.lang 보다 더 많은 package를 자동으로 가져오는 것이다.
    이렇게 하면 더 많은 class를 simple name으로 사용할 수 있게 되고 초보자가 import에 대해 배울 필요도 줄어든다.
    그렇다면 어떤 추가 package를 자동으로 import 해야 할까?
  • 모든 독자는 어디에나 존재하는 java.base module에서 어떤 package를 auto-import 할 것 인지에 대한 제안을 받게 될 것이다.
    java.iojava.util 은 거의 보편적인 제안이 될 것이고, java.util.streamjava.util.function 은 일반적이며, java.math, java.net, 및 java.time 은 각각 supporter가 있을 것이다.
    JShell tool의 경우 일회성 java code를 실험할 때 대체로 유용한 10개의 java.* package를 찾을 수 있었지만 모든 Java program에 영구적으로 자동으로 import 할 수 있는 java.* package의 subset을 파악하기는 어려웠다.
    또한 이 목록은 Java platform이 발전함에 따라 변경될 수 있다. (예: java.util.streamjava.util.function 은 Java 8에서만 도입됨)
    개발자는 어떤 automatic import가 적용되고 있는지 상기시키기 위해 IDE에 의존하게 될 가능성이 높으며 이는 바람직하지 않은 결과이다.
  • 이 기능의 중요한 사용 사례는 암시적으로 선언된 class의 java.base module에서 on-demand로 자동 import 하는 것이다.
    또는 java.base 에서 내보낸 54개의 package를 자동으로 import할 수도 있다.
    그러나 암시적 class가 예상 수명 주기인 일반 명시적 class로 migration 되는 경우 개발자는 54개의 on-demand package import를 작성하거나 필요한 import를 알아내야 한다.
  • Risks and Assumptions

하나 이상의 module import 선언을 사용하면 여러 package가 동일한 simple name을 가진 member를 선언하기 때문에 이름이 모호해질 위험이 있다.
이러한 모호성은 program에서 모호한 simple name이 사용될 때까지 감지되지 않으며 compile-time error가 발생한다.
simgle-type-import 선언을 추가하여 모호성을 해결할 수 있지만 이러한 이름 모호성을 관리하고 해결하는 것은 번거롭고 읽기 및 유지 관리가 어려운 코드로 이어질 수 있다.

JEP 477: Implicitly Declared Classes and Instance Main Methods (Third Preview)

기존 JEP 363 second preview에 다음 추가 기능이 third preview에 추가되었다.

  • 암시적으로 선언된 class는 console에서 간단한 text I/O를 위한 세 가지 static method를 자동으로 가져온다.
    이러한 method는 새로운 top-level class인 java.io.IO 에 선언된다.
  • 암시적으로 선언된 class는 필요에 따라 java.base module에서 export한 package의 모든 public top-level class와 interface를 자동으로 import 한다.

변경된 부분에 대한 설명이 추가되고 기존 설명이 변경되긴 하였다.
관련 내용은 JEP 477 문서에서 확인할 수 있다.

JEP 480: Structured Concurrency (Third Preview)

서로 다른 thread에서 실행되는 관련 작업 그룹을 단일 작업 단위로 처리하여 오류 처리 및 취소를 간소화 하여 동시성 프로그래밍을 좀 더 간소화 하는 방법을 제안한다.
첫 번째 preview 이후 변경 점은 없으며 더 많은 피드백을 얻기 위해 두번째, 세번째 preview가 제안되고 있다.

관련 내용은 JEP 480 문서에서 확인할 수 있다.

JEP 481: Scoped Values (Third Preview)

Thread 내의 호출자(callee)와 자식 thread 모두에서 변경 불가능한 데이터를 공유하는 scoped value가 제안되었다.
앞서 소개한 JEP 480과 같이 사용하면 기존에 비해 좀 더 간결하게 작성을 할 수 있다.

두차례 preview를 통해 얻은 피드백을 통해 다음과 같은 변경 사항이 추가되었다.

  • 이제 ScopedValue.callWhere method 의 operation parameter type은 java compiler가 검사된 exception의 발생 여부를 유추할 수 있는 새로운 기능 interface이다.
    이 변경으로 ScopedValue.getWhere method는 더이상 필요하지 않으며 제거되었다.

관련 내용은 JEP 481 문서에서 확인할 수 있다.

JEP 482: Flexible Constructor Bodies (Second Preview)

Summary

Java 프로그래밍 언어의 constructor에서는 명시적 constructor 호출 앞에 super(..) 또는 this(..) 같은 문이 표시되도록 허용한다.
이 문은 생성 중인 instance를 참조할 수는 없지만 해당 field를 초기화할 수 있다.
다른 constructor를 호출하기 전에 field를 초기화하면 method가 재정의될 때 class의 안정성을 높일 수 있다.
이는 preview language feature이다.

History

이 기능은 JEP 447 에서 다른 제목으로 제안되었으며 JDK 22에서 preview 기능으로 제공되었다.
여기서 한 가지 중요한 변경 사항을 적용하여 두 번째 preivew를 제안한다:

  • constructor를 명시적으로 호출하기 전에 constructor body이 같은 클래스의 field를 초기화할 수 있도록 허용한다.
    이렇게 하면 subclass의 constructor가 superclass의 constructor가 subclass의 field의 default value (예: 0, false, or null )을 확인하는 코드를 실행하지 않도록 할 수 있다.
    이는 재정의(overriding)로 인해 superclass의 constructor가 field를 사용하는 subclass의 method를 호출할 때 발생할 수 있다.

Goals

  • 개발자가 constructor의 동작을 더 자유롭게 표현할 수 있도록 하여 현재 보조 static method, 보조 중간 constructor 또는 constructor argument에 고려해야 하는 로직을 보다 자연스럽게 배치할 수 있다.
  • class instance화 중에 constructor가 하향식 순서로 실행되는 기존의 보장을 유지하여 subclass의 constructor의 코드가 superclass의 instance화를 방해할 수 없도록 한다.Motivation

class의 constructor는 해당 class의 유효한 instance를 생성할 책임이 있다.
예를 들어 Person class의 instance 값이 항상 130보다 작아야 하는 age field가 있다고 가정해보자.
나이 관련 매개 변수 (예: 생년월일)를 받는 constructor는 이를 유효성 검사하고 age field에 기록하여 유효한 instance가 생성되도록 하거나 그렇지 않으면 exception을 던져야 한다.

또한 class의 constructor는 subclass가 있을 때 유효성을 보장할 책임이 있다.
예를 들어 EmployeePerson 의 subclass라고 가정해보자.
모든 Employee constructor는 암시적으로 또는 명시적으로 Person constructor를 호출한다.
constructor는 함께 동작하면서 유효한 instance를 보장해야 한다.
Employee constructor는 Employee class에 선언된 field를 담당하고, Person constructor는 Person class에서 선언된 field를 담당한다.
Employee constructor의 코드는 Person constructor에 의해 초기화된 field를 참조할 수 있으므로 후자가 먼저 실행되어야 한다.

일반적으로 constructor는 top down으로 실행되어야 한다.
superclass의 constructor가 먼저 실행되어 해당 class에서 선언된 field의 유효성을 확인한 다음 subclass의 constructor가 실행되어야 한다.

constructor가 top down으로 실행되는 것을 보장하기 위해 Java 언어에서는 constructor body에서 첫 번째 문이 다른 constructor의 명시적 호출, 즉 super(..) 또는 this(..) 여야 한다.
constructorl 본문에 명시적인 constructor 호출이 나타나지 않으면 compiler는 constructor body의 첫 번째 문으로 super() 를 삽입한다

이 언어는 명시적 constructor 호출의 경우 argument가 어떤 방식으로든 생성 중인 instance를 사용할 수 없도록 요구한다.

이 두 가지 요구 사항은 새 instance를 구성할 때 어느 정도 예측 가능성(predictability)과 위생(hygiene)을 보장하지만, 익숙한 특정 programming pattern을 금지하기 때문에 엄격하다.
다음 예시는 이러한 문제를 설명한다.

Example: Validating superclass constructor arguments

때로는 superclass의 constructor에 전달되는 argument의 유효성을 검사해야 할 때가 있다.
superclass의 constructor를 호출한 후 argument의 유효성을 검사할 수 있지만, 이는 불필요한 작업을 수행할 수 있다는 것을 의미한다:

public class PositiveBigInteger extends BigInteger {

    public PositiveBigInteger(long value) {
        super(value);                 // Potentially unnecessary work
        if (value <= 0) throw new IllegalArgumentException(..);
    }

}

superclass의 constructor를 호출하기 전에 argument의 유효성을 검사하여 빠르게 실패하는 constructor를 선언하는 것이 좋다.
현재로서는 super(..) 호출의 일부로 보조 method를 inline으로 호출해야만 이를 수행할 수 있다:

public class PositiveBigInteger extends BigInteger {

    private static long verifyPositive(long value) {
        if (value <= 0) throw new IllegalArgumentException(..);
        return value;
    }

    public PositiveBigInteger(long value) {
        super(verifyPositive(value));
    }

}

constructor body에 유효성 검사 로직을 배치하면 코드가 더 읽기 쉬워진다:

public class PositiveBigInteger extends BigInteger {

    public PositiveBigInteger(long value) {
        if (value <= 0) throw new IllegalArgumentException(..);
        super(value);
    }

}

Example: Preparing superclass constructor arguments

때로는 superclass constructor에 대한 argument를 준비하기 위해 사소하지 않은 계산을 수행해야 할 때가 있다.
이 경우에도 super(..) 호출의 일부로 보조 method를 inline으로 호출해야 한다.
예를 들어 constructor가 Certificate argument를 받지만 superclass constructor를 위해 byte array로 변환해야 한다고 가정해보자:

public class Sub extends Super {

    private static byte[] prepareByteArray(Certificate certificate) {
        var publicKey = certificate.getPublicKey();
        if (publicKey == null) throw new IllegalArgumentException(..);
        return switch (publicKey) {
            case RSAKey rsaKey -> ...
            case DSAPublicKey dsaKey -> ...
            default -> ...
        };
    }

    public Sub(Certificate certificate) {
        super(prepareByteArray(certificate));
    }

}

constructor body에서 argument를 직접 준비할 수 있다면 코드가 더 가독성이 높아질 것이다:

public Sub(Certificate certificate) {
        var publicKey = certificate.getPublicKey();
        if (publicKey == null) throw ...
        byte[] certBytes = switch (publicKey) {
            case RSAKey rsaKey -> ...
            case DSAPublicKey dsaKey -> ...
            default -> ...
        };
        super(certBytes );
    }

Example: Sharing superclass constructor arguments

때로는 superclass constructor에 동일한 값을 다른 argument로 두 번 이상 전달해야 할 때가 있다.
이를 수행하는 유일한 방법은 보조 constructor를 사용하는 것이다:

public class Super {
    public Super(C x, C y) { ... }
}

public class Sub extends Super {
    private Sub(C x)   { super(x, x); }     // Pass the argument twice to Super's constructor
    public  Sub(int i) { this(new C(i)); }  // Prepare the argument for Super's constructor
}

constructor body에 공유를 배치하여 보조 constructor가 필요하지 않도록 하면 코드의 유지 관리가 더 쉬워질 것이다:

public class Sub extends Super {
    public Sub(int i) {
        var x = new C(i);
        super(x, x);
    }
}

Summary

이 모든 예제에서 작성하고자 하는 constructor body에는 명시적 constructor 호출 전에 생성 중인 instance를 사용하지 않는 문이 포함되어 있다.
블행히도 constructor body는 모두 안전함에도 불구하고 compiler에 의해 거부된다:

Java 언어가 보다 유연한 규칙으로 top down 구성을 보장할 수 있다면 constructor body는 작성하기 쉽고 유지 관리가 쉬워질 것이다.
constructor body는 서투른 보조 method나 constructor를 호출하지 않고도 argument 유효성 검사, argument 준비 및 argument 고유를 보다 자연스럽게 수행할 수 있다.
Java 1.0 부터 시행된 super(..) 또는 this(..) 가 constructor body의 첫 번째 문이어야 한다는 단순한 구문 요구사항을 넘어서야 할 때이다:

Description

constructor body 의 문법을 수정하여 명시적인 constructor 호출 전, 즉 다음 부터 구문을 허용한다:

ConstructorBody:
    { [ExplicitConstructorInvocation] [BlockStatements] }

to:

ConstructorBody:
    { [BlockStatements] ExplicitConstructorInvocation [BlockStatements] }
    { [BlockStatements] }

일부 세부 사항을 제외하고 명시적 constructor 호출은 super(..) 또는 this(..) 이다.

명시적 constructor 호출 앞에 나타나는 문은 constructor body의 prologue 를 구성한다.

명시적 constructor 호출 뒤에 나타나는 문은 constructor body의 epilogue 를 구성한다.

constructor body에서 명시적인 constructor 호출을 생략할 수 있다.
이 경우 prologue는 비어있고 constructor body의 모든 문이 epilogue를 구성한다.

constructor body의 epilogue에는 표현식을 포함하지 않는 경우 return 문이 허용된다.
즉, return; 은 허용되지만 return e; 는 허용되지 않는다.
constructor body의 prologue에 return 문이 나타나는 것은 compile-time error이다.

constructor b ody의 prologue 또는 epilogue에서 exception을 던지는 것은 허용된다.
prologue에서 exception을 던지는 것은 일반적으로 빠른 실패 시나리오에서 사용된다.

This is a preview language feature, disabled by default

JDK 23에서 아래 예제를 시도하려면 preview 기능을 활성화해야 한다:

  • javac --release 23 --enable-preview Main.java 로 program을 compile하고 java --enable-preview Main 로 실행하거나
  • source code launcher 를 사용하는 경우 java --enable-preview Main.java 또는
  • jshell 을 사용하는 경우 jshell --enable-preview 로 시작한다.

Early construction contexts

Java 언어에서 명시적 constructor 호출의 argument 목록에 나타나는 코드는 static context 에 나타난다고 한다.
즉, 명시적 constructor 호출의 argument는 static method의 코드처럼, 다시 말해 instance가 없는 것처럼 취급된다.
그러나 static context의 기술적 제한은 필요 이상으로 강력하여 유용하고 안전한 코드가 constructor argument로 표시되는 것을 방지한다.

static context의 개념을 수정하는 대신 명시적 constructor 호출의 argument 목록과 constructor body, 즉 prologue에서 그 앞에 나타나는 모든 문을 포함하는 early construction context라는 개념을 도입한다.
early construction context 의 코드는 자체 initializer가 없는 field를 초기화하는 경우를 제외하고는 구성 중인 instance를 사용해서는 안된다.

즉, 현재 instance를 참조하거나 현재 instance의 field에 access 하거나 method를 호출하기 위해 this 를 명시적 또는 암시적으로 사용하는 것은 early construction context에서 허용되지 않는다:

class A {

    int i;

    A() {

        System.out.print(this);  // Error - refers to the current instance

        var x = this.i;          // Error - explicitly refers to field of the current instance
        this.hashCode();         // Error - explicitly refers to method of the current instance

        var x = i;               // Error - implicitly refers to field of the current instance
        hashCode();              // Error - implicitly refers to method of the current instance

        super();

    }

}

마찬가지로, 모든 field access, method 호출 또는 super 로 한정된 method 참조는 early construction context에서 허용되지 않는다:

class B {
    int i;
    void m() { ... }
}

class C extends B {

    C() {
        var x = super.i;         // Error
        super.m();               // Error
        super();
    }

}

Using enclosing instances in early construction contexts

class 선언이 중첩된 경우 inner class의 코드는 enclosing class의 instance를 참조할 수 있다.
이는 enclosing class의 instance가 inner class의 instance보다 먼저 생성되기 때문이다.
constructor body를 포함한 inner class의 code는 simple name이나 qualified this 표현식 을 사용하여 enclosing instance의 field에 access 하고 method를 호출할 수 있다.
따라서 enclosing instance에 대한 연산은 early construction context에서 허용된다.

아래 코드에서 Inner 선언은 Outer 선언에 중첩되어 있으므로 Inner 의 모든 isntance에는 Outer 의 enclosing instance가 있다.
Inner 의 constructor에서 early construction context의 코드는 simple name이나 Outer.this 를 통해 enclosing instance와 그 멤버를 참조할 수 있다.

class Outer {

    int i;

    void hello() { System.out.println("Hello"); }

    class Inner {

        int j;

        Inner() {
            var x = i;             // OK - implicitly refers to field of enclosing instance
            var y = Outer.this.i;  // OK - explicitly refers to field of enclosing instance
            hello();               // OK - implicitly refers to method of enclosing instance
            Outer.this.hello();    // OK - explicitly refers to method of enclosing instance
            super();
        }

    }

}

이와 대조적으로 아래 표시된 Outer 의 constructor 에서 early construction context의 코드는 new Inner() 를 사용하여 Inner class를 instance화 할 수 없다.
이 표현식은 실제로는 this.new Inner() 이며, 이는 Inner object를 enclosing instance로 Outer 의 현재 instance를 사용한다는 의미이다.
앞의 규칙에 따라 현재 instance를 참조하기 위해 명시적이든 암시적이든 this 를 사용하는 것은 early construction context에서 허용되지 않는다.

class Outer {

    class Inner {}

    Outer() {
        var x = new Inner();       // Error - implicitly refers to the current instance of Outer
        var y = this.new Inner();  // Error - explicitly refers to the current instance of Outer
        super();
    }

}

Early assignment to fields

early construction context에서 현재 instance의 field에 access 하는 것이 허용되지 않지만, 아직 구성 중일 때 현재 instance의 field에 할당하는 것은 어떻게 될까?

이러한 할당을 허용하면 subclass의 constructor가 superclass의 constructor가 subclass에서 초기화되지 않은 field를 보는 것을 방어하는데 유용할 수 있다.
이는 superclass의 constructor가 subclass의 method에 의해 override되는 superclass의 method를 호출할 때 발생할 수 있다.
Java 언어에서 constructor가 override가 가능한 method를 호출하는 것을 허용 하지만 이는 잘못된 관행으로 간주된다.
Effective Java (Third Edition) 의 19번 항목에서는 "constructor는 override 가능한 method를 호출해서는 안된다"고 조언한다.
이것이 왜 나쁜 관행으로 간주되는지 알아보려면 다음 class hierarchy를 고려해보자:

class Super {

    Super() { overriddenMethod(); }

    void overriddenMethod() { System.out.println("hello"); }

}

class Sub extends Super {

    final int x;

    Sub(int x) {
        /* super(); */    // Implicit invocation
        this.x = x;
    }

    @Override
    void overriddenMethod() { System.out.println(x); }

}

new Sub(42) 는 무엇을 출력할까?
42 가 출력될 것으로 예상할 수 있지만 실제로는 0 이 출력된다.
이는 Super constructor가 Sub constructor body에서 field 할당 전에 암시적으로 호출되기 때문이다.
그런 다음 Super constructor가 overridenMethod 를 호출하여 Sub constructor body가 field에 42 를 할당하기 전에 Sub 의 해당 method를 실행되도록 한다.
결과적으로 Sub 의 method는 field의 기본값인 0 을 보게 된다.

이 pattern은 많은 bug와 error의 원인이다.
나쁜 프로그래밍 관행으로 간주되지만 드물지 않으며, 특히 superclass를 수정할 수 없는 경우 subclass에 곤란을 초래한다.

Sub constructor가 명시적으로 Super constructor를 호출하기 전에 Sub 의 field를 초기화할 수 있도록 하여 이 난제를 해결한다.
이 예제는 다음과 같이 다시 작성할 수 있으며, 여기서 Sub class만 변경된다:

class Super {

    Super() { overriddenMethod(); }

    void overriddenMethod() { System.out.println("hello"); }

}

class Sub extends Super {

    final int x;

    Sub(int x) {
        this.x = x;    // Initialize the field
        super();       // Then invoke the Super constructor explicitly
    }

    @Override
    void overriddenMethod() { System.out.println(x); }

}

Now, new Sub(42) will print 42, because the field in Sub is assigned to 42 before overriddenMethod is invoked.
이제 new Sub(42)42 를 출력한다.
overriddenMethod 가 호출되기 전에 Sub 의 field가 42 에 할당되었기 때문이다.

constructor body에서 field 선언에 initializer가 없는 경우, 같은 class에서 선언된 field에 대한 간단한 할당 은 early construction context에서 허용된다.
즉, constructor body는 early consturction context에서 class 자체 field를 초기화할 수 있지만 superclass의 field는 초기화할 수 없다.

앞서 설명한 것처럼 constructor body는 명시적 constructor 호출 이후, 즉 epilogue가 끝날 때까지 (constructor와 같은 class에서 선언되었든 superclass에서 선언되었든) 현재 instance의 field를 읽을 수 없다.

Records

record class의 constructors 는 이미 일반 class의 constructor보다 더 많은 제한을 받고 있다. 특히

  • 표준 record constructor에는 명시적인 constructor 호출이 포함되어서는 안된다. 그리고
  • 비표준 record constructor에는 superclass constructor 호출 (super(..) )이 아닌 대체 constructor 호출 (this(..) ) 이 포함되어야 한다.

이러한 제한은 그대로 유지된다.
그렇지 않으면 record constructor는 위에서 설명한 변경 사항의 이점을 얻을 수 있다.
주로 비표준 record constructor가 대체 constructor 호출 전에 명령문을 포함할 수 있기 때문이다.

Enums

enum class의 constructors 에는 대체 constructor 호출이 포함될 수 있지만 superclass의 constructor 호출은 포함되지 않는다.
enum class는 주로 constructor가 대체 constructor 호출 이전의 문을 포함할 수 있기 때문에 위에서 설명한 변경 사항의 이점을 누릴 수 있다.

Testing

  • 변경된 동작을 검증하는 테스트를 제외하고는 변경되지 않은 기존 단위 테스트와 적절하게 새로운 positive및 negative 테스트 케이스로 compiler 변경 사항을 테스트한다.
  • 이전 버전과 새 버전의 compiler를 사용하여 모든 JDK class를 compile하고 결과 bytecode가 동일한지 확인한다.
  • platform 별 테스트가 필요하지 않아야 한다.

Risks and Assumptions

위에서 제안한 변경 사항은 source 및 동작과 호환된다.
기존의 모든 java 프로그램의 의미를 유지하면서 합법적인 Java 프로그램 set을 엄격하게 확장한다.

이러한 변경 사항은 그 자체로는 사소한 것이지만, constructor 호출이 있는 경우 항상 constructor body의 첫 번재 문으로 표시되어야 한다는 오랜 요구 사항에서 중요한 변화를 나타낸다.
이 요구 사항은 code analyzer, stype checker, syntex highlighter, development environment 및 java ecosystem의 기타 tool에 깊숙이 내장되어 있다.
모든 언어 변경과 마찬가지로 tool이 업데이트 될 때 일정 기간의 진통이 있을 수 있다.

Dependencies

Java 언어의 유연한 constructor body는 constructor에서 constructor 호출 전에 나타나는 임의의 코드가 생성 중인 instance를 참조하지 않는 한, 해당 코드를 검증하고 실행하는 JVM의 기능에 따라 달라진다.
다행히도 JVM은 이미 constructor body에 대한 보다 유연한 처리를 지원한다:

  • code path에 정확히 하나의 호출이 있는 경우 constructor body에는 여러 개의 constructor 호 출이 나타날 수 있다.
  • constructor 호출 전에 임의의 코드가 표시될 수 있지만, 해당 코드가 field 할당 외에는 생성 중인 instance를 참조하지 않는 한 해당 코드가 표시될 수 있다.
  • 명시적 constructor 호출은 try block 내에서, 즉 bytecode exception 범위 내에서 나타나지 않을 수 있다.

JVM의 규칙은 여전히 top-down 초기화를 보장한다.

  • sueprclass의 초기화는 항상 superclass의 constructor 호출을 통해 직접 또는 대체 constructor 호출을 통해 간접적으로 정확히 한 번 발생한다.
  • 초기화되지 않은 instance는 superclass의 최기화가 완료될 때까지 결과에 영향을 주지 않는 field 할당을 제외하고는 사용할 수 없다.

따라서 Java Machine 사양은 변경할 필요가 없으며, Java 언어 사양만 변경하면 된다.

유연한 constructor body를 허용하는 JVM과 보다 제한적인 기존 Java 언어 간의 불일치는 역사적일 유물이다.
원래는 JVM이 더 제한적이었지만 이로 인해 innerclass 및 captured free variable과 같은 새로운 언어 기능에 대한 compiler 생성 field의 초기화 문제가 발생했다.
compiler 생성 코드를 수용하기 위해 수년 전에 JVM 사양을 완화했지만, 이 새로운 유연성을 활용하기 위해 Java 언어 사양을 수정한 적은 없다.

반응형
profile

파란하늘의 지식창고

@Bluesky_

도움이 되었다면 광고를 클릭해주세요