파란하늘의 지식창고
Published 2023. 10. 4. 18:38
JDK 21 New Features Study/Java
반응형

JDK의 버전별 변경 사항은 이곳을 참고하세요.


Spec

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

Final Release Specification Feature Summary

전체 JEP Feature 목록은 OpenJDK의 JDK21 문서로 확인할 수 있다.

JEP Component Feature
JEP 430 specification / language String Templates (Preview)
JEP 431 core-libs / java.util:collections Sequenced Collections
JEP 439 hotspot / gc Generational ZGC
JEP 440 specification / language Record Patterns
JEP 441 specification / language Pattern Matching for switch
JEP 442 core-libs Foreign Function & Memory API (Third Preview)
JEP 443 specification / language Unnamed Patterns and Variables (Preview)
JEP 444 core-libs Virtual Threads
JEP 445 specification / language Unnamed Classes and Instance Main MEthods (Preview)
JEP 446 core-libs Scoped Values (Preview)
JEP 448 core-libs Vector API (Sixth Incubator)
JEP 452 security-libs / javax.crypto Key Encapsulation Mechanism API
JEP 453 core-libs Structured Concurrency (Preview)

JEP 430: String Templates (Preview)

java에서 계산된 값을 포함하는 문자열을 만드는 방법을 좀 더 쉽게 제공하기 위해 String Template을 제공한다.
JDK 21에서 Preview 기능이다.

기존 방식은 다음과 같다.

  • + operater 를 사용하는 경우 (코드가 읽기 어렵다.)
String s = x + " plus " + y + " equals " + (x + y);
  • StringBuilder 를 사용하는 경우 (장황하다)
String s = new StringBuilder()
                 .append(x)
                 .append(" plus ")
                 .append(y)
                 .append(" equals ")
                 .append(x + y)
                 .toString();
  • String::foramt 이나 String::formatted 를 사용하는 경우 (format string을 parameter에서 분리하면 type 불일치가 발생할 수 있다.)
String s = String.format("%2$d plus %1$d equals %3$d", x, y, x + y);
String t = "%2$d plus %1$d equals %3$d".formatted(x, y, x + y);
  • MessageFormat 을 사용하는 경우 (너무 많은 수식어가 필요하고 format string에 익숙하지 않은 구문을 사용한다.)
MessageFormat mf = new MessageFormat("{0} plus {1} equals {2}");
String s = mf.format(x, y, x + y);

많은 프로그래밍 언어가 문자열 연결(string concatenation) 대신 문자열 보간 (string interpolation)을 제공한다.

C#             $"{x} plus {y} equals {x + y}"
Visual Basic   $"{x} plus {y} equals {x + y}"
Python         f"{x} plus {y} equals {x + y}"
Scala          s"$x plus $y equals ${x + y}"
Groovy         "$x plus $y equals ${x + y}"
Kotlin         "$x plus $y equals ${x + y}"
JavaScript     `${x} plus ${y} equals ${x + y}`
Ruby           "#{x} plus #{y} equals #{x + y}"
Swift          "\(x) plus \(y) equals \(x + y)"

이러한 문자열 보간은 편리하지만 잘못된 문자열을 구성하기 쉽다.
SQL 구문, HTML/XML 문서, json 조각, shell script 등 모두 각 도메인 별 규칙에 따라 검증되고 정리되어야 한다.
Java 프로그래밍 언어가 이러한 모든 규칙을 검증할 수 없기 때문에 유효성 검사는 개발자의 몫이 된다.

만약 Sql을 다음처럼 작성하고 문자열 보간을 통해 name을 계산한다고 가정해 보면

String query = "SELECT * FROM Person p WHERE p.last_name = '${name}'";
ResultSet rs = connection.createStatement().executeQuery(query);

name이 다음과 같은 값을 가지게 되면

Smith' OR p.last_name <> 'Smith

query 문자열은 다음과 같이 계산된 결과가 되어 sql injection 문제가 발생하게 된다.

String query = "SELECT * FROM Person p WHERE p.last_name = '" + name + "'";

또한 java에서 예약된 문자가 포함된 경우 escape 처리를 해야 한다.

SELECT * FROM Person p WHERE p.last_name = '\'Smith\' OR p.last_name <> \'Smith\''

Java의 Template Expression은 새로운 종류의 표현식이다.
다음과 같이 사용한다.

String name = "Joan";
String info = STR."My name is \{name}";
assert info.equals("My name is Joan");   // true

STR. 으로 String Template 임을 표현하고
\{name} 은 name 변수로 치환된다.

좀 더 다양한 경우의 사용은 다음과 같다.

// Embedded expressions can be strings
String firstName = "Bill";
String lastName  = "Duck";
String fullName  = STR."\{firstName} \{lastName}";
| "Bill Duck"
String sortName  = STR."\{lastName}, \{firstName}";
| "Duck, Bill"
// Embedded expressions can perform arithmetic
int x = 10, y = 20;
String s = STR."\{x} + \{y} = \{x + y}";
| "10 + 20 = 30"
// Embedded expressions can invoke methods and access fields
String s = STR."You have a \{getOfferType()} waiting for you!";
| "You have a gift waiting for you!"
String t = STR."Access at \{req.date} \{req.time} from \{req.ipAddress}";
| "Access at 2022-03-25 15:34 from 8.8.8.8"

위의 다양한 경우를 통해 짐작할 수 있지만 포함된 표현식은 java의 문법을 그대로 사용할 수 있다.

예를 들어 다음과 같이 삼항 연산자도 사용할 수 있다.

String filePath = "tmp.dat";
File   file     = new File(filePath);
String old = "The file " + filePath + " " + (file.exists() ? "does" : "does not") + " exist";
String msg = STR."The file \{filePath} \{file.exists() ? "does" : "does not"} exist";
| "The file tmp.dat does exist" or "The file tmp.dat does not exist"

또한 줄 바꿈을 할 수 있다.

String time = STR."The time is \{
    // The java.time.format package is very useful
    DateTimeFormatter
      .ofPattern("HH:mm:ss")
      .format(LocalTime.now())
} right now";
| "The time is 12:34:56 right now"

심지어 String Template 안에서 String Template을 사용할 수 있다.

// Embedded expression is a (nested) template expression
String[] fruit = { "apples", "oranges", "peaches" };
String s = STR."\{fruit[0]}, \{STR."\{fruit[1]}, \{fruit[2]}"}";
| "apples, oranges, peaches"

좀 더 보기 좋게 줄 바꾼다면 다음과 같이 된다.

String s = STR."\{fruit[0]}, \{
    STR."\{fruit[1]}, \{fruit[2]}"
}";

또한 별도의 String Template으로 변수화 할 수 있다.

String tmp = STR."\{fruit[1]}, \{fruit[2]}";
String s = STR."\{fruit[0]}, \{tmp}";

또한 TextBlock 으로 사용도 가능하다.

String title = "My Web Page";
String text  = "Hello, world";
String html = STR."""
        <html>
          <head>
            <title>\{title}</title>
          </head>
          <body>
            <p>\{text}</p>
          </body>
        </html>
        """;

지금까지 STR. 으로 사용하는 StringTemplate.Processor를 사용하는 부분을 설명하였다.
이외에도 FMT. , RAW. 를 제공하고 있으며 사용자가 정의하여 사용도 가능하다.

record Rectangle(String name, double width, double height) {
    double area() {
        return width * height;
    }
}
Rectangle[] zone = new Rectangle[] {
    new Rectangle("Alfa", 17.8, 31.4),
    new Rectangle("Bravo", 9.6, 12.4),
    new Rectangle("Charlie", 7.1, 11.23),
};
String table = FMT."""
    Description     Width    Height     Area
    %-12s\{zone[0].name}  %7.2f\{zone[0].width}  %7.2f\{zone[0].height}     %7.2f\{zone[0].area()}
    %-12s\{zone[1].name}  %7.2f\{zone[1].width}  %7.2f\{zone[1].height}     %7.2f\{zone[1].area()}
    %-12s\{zone[2].name}  %7.2f\{zone[2].width}  %7.2f\{zone[2].height}     %7.2f\{zone[2].area()}
    \{" ".repeat(28)} Total %7.2f\{zone[0].area() + zone[1].area() + zone[2].area()}
    """;
| """
| Description     Width    Height     Area
| Alfa            17.80    31.40      558.92
| Bravo            9.60    12.40      119.04
| Charlie          7.10    11.23       79.73
|                              Total  757.69
| """

이로 인해 앞서 언급한 문자열 보간의 유효성 검사 문제가 해결되게 된다.

JEP 431: Sequenced Collections

Java의 Collections framework에는 정의된 encounter order를 가진 element의 sequence를 나타내는 collection type이 없다.
또한 이러한 collection에 적용되는 일관된 operation set도 없다.

예를 들어 ListDeque 는 둘 다 encounter order를 정의하지만 이들의 공통 상위 유형인 Collection은 그렇지 않다.
마찬가지로 Set 은 encounter order를 정의하지 않으며 HashSet 과 같은 subtype은 encounter order를 정의하지 않지만 SortedSetLinkedHashSet 과 같은 subtype은 정의한다.
따라서 encounter order에 대한 지원이 type hierarchy 전체에 분산되어 있어 API에서 특정 유용한 개념을 표현하기 어렵다.
Collection이나 List는 encounter order가 있는 parameter나 return value를 설명할 수 없다.

element의 처음과 마지막 element를 호출하는 방식은 다음과 같이 제각각이다.

  First element Last element
List list.get(0) list.get(list.size() - 1)
Deque deque.getFirst() deque.getLast()
SortedSet sortedSet.first() sortedSet.last()
LinkedHashSet linkedHashSet.iterator().next() // missing

이러한 부분들을 해결하기 위해 Sequenced Collection 이 제공된다.

interface SequencedCollection<E> extends Collection<E> {
    // new method
    SequencedCollection<E> reversed();
    // methods promoted from Deque
    void addFirst(E);
    void addLast(E);
    E getFirst();
    E getLast();
    E removeFirst();
    E removeLast();
}

reserved() method는 원본 collection의 역순 보기를 제공한다.

중복된 element를 포함하지 않는 SequencedSet 도 제공된다.

interface SequencedSet<E> extends Set<E>, SequencedCollection<E> {
    SequencedSet<E> reversed();    // covariant override
}

SequencedMap 도 제공된다.

interface SequencedMap<K,V> extends Map<K,V> {
    // new methods
    SequencedMap<K,V> reversed();
    SequencedSet<K> sequencedKeySet();
    SequencedCollection<V> sequencedValues();
    SequencedSet<Entry<K,V>> sequencedEntrySet();
    V putFirst(K, V);
    V putLast(K, V);
    // methods promoted from NavigableMap
    Entry<K, V> firstEntry();
    Entry<K, V> lastEntry();
    Entry<K, V> pollFirstEntry();
    Entry<K, V> pollLastEntry();
}

JEP 439: Generational ZGC

ZGC를 확장하여 young object와 old object에 대해 별도의 generation을 유지함으로써 application 성능을 향상한다.
이를 통해 ZGC는 일찍 죽는 경향이 있는 young object를 더 자주 수집할 수 있다.

Generation ZGC는 다음과 같은 이점을 누릴 수 있다.

  • 할당 지연 risk 감소
  • 필요한 heap memory overhead 감소
  • garbage collection CPU overhead 감소

JEP 440: Record Patterns

JEP 405: Record Patterns (Preview)는 JEP 427: Pattern Matching for switch (Thrid Preview) 와 연관되어 있다.
JDK 16에 추가된 JEP 395: Records 를 통해 record에 대한 instanceof pattern matching으로 다음과 같은 처리가 가능해졌었다.

// Prior to Java 16
if (obj instanceof String) {
    String s = (String)obj;
    ... use s ...
}
// As of Java 16
if (obj instanceof String s) {
    ... use s ...
}

Pattern matching and records

record에 대해서도 pattern matching을 처리할 수 있었다.

// As of Java 16
record Point(int x, int y) {}
static void printSum(Object obj) {
    if (obj instanceof Point p) {
        int x = p.x();
        int y = p.y();
        System.out.println(x+y);
    }
}

record의 경우 생성자의 매개변수가 record의 멤버 변수가 되기 때문에 생성자를 바로 사용할 수 있도록 다음과 같은 처리가 가능해지게 된다.

// As of Java 21
static void printSum(Object obj) {
    if (obj instanceof Point(int x, int y)) {
        System.out.println(x+y);
    }
}

Nested record patterns

생성자에 대한 pattern matching은 중첩 레코드를 사용할 때 우발적 복잡성을 제거하여 해당 Object가 표현하는 데이터에 집중할 수 있도록 한다.

// As of Java 16
record Point(int x, int y) {}
enum Color { RED, GREEN, BLUE }
record ColoredPoint(Point p, Color c) {}
record Rectangle(ColoredPoint upperLeft, ColoredPoint lowerRight) {}

앞서 소개했던 record pattern을 적용한 경우 다음과 같다.

// As of Java 21
static void printUpperLeftColoredPoint(Rectangle r) {
    if (r instanceof Rectangle(ColoredPoint ul, ColoredPoint lr)) {
         System.out.println(ul.c());
    }
}

ColoredPoint 값 ul은 그 자체로 record 값이므로 더 분해하고 싶을 수 있다.
따라서 record pattern은 중첩을 지원하므로 record component를 중첩된 pattern에 대해 추가로 일치시키고 분해할 수 있다.
record pattern안에 다른 pattern을 중첩하여 외부 record와 내부 record를 한 번에 분해할 수 있다.

// As of Java 21
static void printColorOfUpperLeftPoint(Rectangle r) {
    if (r instanceof Rectangle(ColoredPoint(Point p, Color c), ColoredPoint lr)) {
        System.out.println(c);
    }
}

nested pattern을 사용하면 집합을 구성하는 코드만큼이나 명확하고 간결한 코드로 집합을 분해할 수 있다.
예를 들어 rectangle을 생성하는 경우 constructor를 하나의 표현식에 중첩할 수 있다.

// As of Java 16
Rectangle r = new Rectangle(new ColoredPoint(new Point(x1, y1), c1), 
                            new ColoredPoint(new Point(x2, y2), c2));

Nested pattern을 사용하면 nested constructor의 structure를 반영하는 코드로 이러한 rectangle을 분해할 수 있다.

// As of Java 21
static void printXCoordOfUpperLeftPointWithPatterns(Rectangle r) {
    if (r instanceof Rectangle(ColoredPoint(Point(var x, var y), var c),
                               var lr)) {
        System.out.println("Upper-left corner: " + x);
    }
}

물론 nested pattern은 일치하지 않을 수 있다.

// As of Java 21
record Pair(Object x, Object y) {}

Pair p = new Pair(42, 42);

if (p instanceof Pair(String s, String t)) {
    System.out.println(s + ", " + t);
} else {
    System.out.println("Not a pair of strings");
}

여기서 record pattern Pair(String s, String t) 에는 두 개의 nested type pattern, 즉 String sString t 가 포함된다.
값이 Pair 인 경우 Pair(String s, String t) pattern과 일치하고, 재귀적으로 그 구성 요소 값이 type pattern String sString t 와 일치한다.
위의 예제 코드에서 이러한 재귀 패턴 일치는 record 구성 요소 값 중 어느 것도 문자열이 아니므로 실패하므로 else block이 실행된다.

요약하자면, nested pattern은 객체를 탐색할 때 발생하는 우발적인 복잡성을 제거하여 해당 객체가 표현하는 데이터에 집중할 수 있도록 해준다.
또한 하위 pattern 중 하나 또는 둘 다 일치하지 않으면 값이 nested pattern P(Q)와 일치하지 않으므로 오류 처리를 중앙 집중화 할 수 있는 기능을 제공한다.
전체 패턴이 일치하든 일치하지 않든 각각의 하위 패턴 일치 실패를 일일이 확인하고 처리할 필요가 없다.

Record patterns

record pattern은 record class type과 해당 record 구성 요소 값과 일치하는 데 사용되는 (비어있을 수 있는) pattern list로 구성된다.

예를 들어 다음과 같은 선언이 있다고 가정한다.

record Point(int i, int j) {}

값 v가 record type Point 의 instance인 경우 record pattern Point(int i, intj) 와 일치하며, 그렇다면 pattern variable i는 값 v에 대해 해당하는 accessor method를 호출한 결과로 초기화되고, pattern variable j는 값 v에 대해 j에 대항하는 accessor method를 호출한 결과로 초기화된다.
(pattern variable의 이름은 record component의 이름과 같을 필요는 없다.
record pattern Point(int x, int y) 는 pattern variable x와 y가 초기화된다는 점을 제외하면 동일하게 동작한다.)

null 값은 어떤 record pattern과도 일치하지 않는다.

record pattern은 구성 요소의 type을 명시하지 않고 var를 사용하여 record 구성 요소와 일치시킬 수 있다.
이 경우 compiler는 var 패턴에 의해 도입된 pattern variable의 type을 유추한다.
예를 들어, Point(var a, var b) pattern은 Point(int a, int b)의 축약형이다.

record pattern으로 선언된 pattern variable 집합에는 pattern list에 선언된 모든 pattern variable이 포함된다.

표현식이 체크되지 않은 변환 없이 pattern의 record type으로 형 변환될 수 있는 경우 record pattern과 호환된다.

record pattern이 일반 record class의 이름을 지정하지만 argument를 제공하지 않는 경우 (즉, record pattern이 원시 유형을 사용하는 경우) argument는 항상 추론된다.
예를 들어

// As of Java 21
record MyPair<S,T>(S fst, T snd){};

static void recordInference(MyPair<String, Integer> pair){
    switch (pair) {
        case MyPair(var f, var s) -> 
            ... // Inferred record pattern MyPair<String,Integer>(var f, var s)
        ...
    }
}

record pattern에 대한 argument의 추론은 record pattern을 지원하는 모든 구문, 즉 instanceof expression, switch 문 및 expression에서 지원된다.

예를 들어 추론은 nested record pattern에서 동작한다.

// As of Java 21
record Box<T>(T t) {}

static void test1(Box<Box<String>> bbs) {
    if (bbs instanceof Box<Box<String>>(Box(var s))) {
        System.out.println("String " + s);
    }
}

여기서 nested pattern Box(var s) 의 argument는 문자열로 추론되므로 pattern 자체는 Box<String>(var s) 로 추론된다.

사실 외부 record pattern에서도 argument를 삭제할 수 있으므로 간결한 코드를 만들 수 있다.

// As of Java 21
static void test2(Box<Box<String>> bbs) {
    if (bbs instanceof Box(Box(var s))) {
        System.out.println("String " + s);
    }
}

여기서 compiler는 pattern의 전체 instance가 Box<Box<String>>(Box<String>(var s)) 라고 추론한다.

호환성을 위해 type pattern은 type argument의 암시적 추론을 지원하지 않는다.
(예: type pattern List l 은 항상 raw type pattern으로 취급됨)

Record patterns and exhaustive switch

JEP 441은 pattern label을 지원하기 위해 switch 표현식과 switch 문을 모두 개선했다.
switch 표현식과 pattern switch문은 모두 완전해야 한다.
switch block에는 선택자 표현식의 가능한 모든 값을 처리하는 절이 있어야 한다.
pattern lable의 경우 pattern의 유형을 분석하여 결정된다.
예를 들어 case label Bar bBar type 및 Bar 의 가능한 모든 subtype의 값과 일치한다.

record pattern을 포함하는 pattern label의 경우 component pattern의 type을 고려하고 sealed 계층 구조를 허용해야 하므로 분석이 더 복잡해진다.
예를 들어 다음과 같은 선언이 있다고 가정하면

class A {}
class B extends A {}
sealed interface I permits C, D {}
final class C implements I {}
final class D implements I {}
record Pair<T>(T x, T y) {}

Pair<A> p1;
Pair<I> p2;

다음 switch는 두 값이 모두 A인 쌍에 대해 일치하는 항목이 없으므로 완전하지 않다.

// As of Java 21
switch (p1) {                 // Error!
    case Pair<A>(A a, B b) -> ...
    case Pair<A>(B b, A a) -> ...
}

이 두 switch는 Interface I 가 sealed 되어 있기 때문에 가능한 모든 경우는 type C와 type D가 된다.

// As of Java 21
switch (p2) {
    case Pair<I>(I i, C c) -> ...
    case Pair<I>(I i, D d) -> ...
}

switch (p2) {
    case Pair<I>(C c, I i) -> ...
    case Pair<I>(D d, C c) -> ...
    case Pair<I>(D d1, D d2) -> ...
}

하지만 아래 switch는 두 값이 모두 type D인 쌍에 대해 일치하는 항목이 없으므로 완전하지 않다.

// As of Java 21
switch (p2) {                        // Error!
    case Pair<I>(C fst, D snd) -> ...
    case Pair<I>(D fst, C snd) -> ...
    case Pair<I>(I fst, C snd) -> ...
}

JEP 441: Pattern Matching for switch

Java 16에 적용된 JEP 394: Pattern Matching for instanceof 를 통해 아래와 같이 instanceof and cast 사용을 간결하게 할 수 있게 되었다.

// Prior to Java 16
if (obj instanceof String) {
    String s = (String)obj;
    ... use s ...
}
// As of Java 16
if (obj instanceof String s) {
    ... use s ...
}

또한 이를 통해 아래와 같이 사용하던 부분도

// Prior to Java 21
static String formatter(Object obj) {
    String formatted = "unknown";
    if (obj instanceof Integer i) {
        formatted = String.format("int %d", i);
    } else if (obj instanceof Long l) {
        formatted = String.format("long %d", l);
    } else if (obj instanceof Double d) {
        formatted = String.format("double %f", d);
    } else if (obj instanceof String s) {
        formatted = String.format("String %s", s);
    }
    return formatted;
}

아래와 같이 switch를 통해 반환이 가능해졌다.

// As of Java 21
static String formatterPatternSwitch(Object obj) {
    return switch (obj) {
        case Integer i -> String.format("int %d", i);
        case Long l    -> String.format("long %d", l);
        case Double d  -> String.format("double %f", d);
        case String s  -> String.format("String %s", s);
        default        -> obj.toString();
    };
}

Switches and null

일반적으로 Switch 문과 표현식은 selector 표현식이 null로 평가되면 NullPointerException을 던지므로, null에 대한 테스트는 switch 외부에서 수행해야 한다.

// Prior to Java 21
static void testFooBarOld(String s) {
    if (s == null) {
        System.out.println("Oops!");
        return;
    }
    switch (s) {
        case "Foo", "Bar" -> System.out.println("Great");
        default           -> System.out.println("Ok");
    }
}

switch에서 몇 가지 reference type만 지원할 때는 이 방법이 합리적이었다.
그러나 switch에서 모든 reference type의 selector 표현식을 허용하고 case label에 type pattern이 있을 수 있는 경우 독립된 null test는 불필요한 상용구와 오류 가능성을 불러일으키는 임의적인 구문처럼 느껴진다.
새로운 null case label을 허용하여 switch에 null test를 통합하는 것이 더 나을 것이다.

// As of Java 21
static void testFooBarNew(String s) {
    switch (s) {
        case null         -> System.out.println("Oops");
        case "Foo", "Bar" -> System.out.println("Great");
        default           -> System.out.println("Ok");
    }
}

Case refinement

constant가 있는 case label과 달리 pattern case label은 여러 값에 적용될 수 있다.
이로 인해 switch 규칙의 오른쪽에 조건부 코드가 생기는 경우가 많다.
예를 들면 다음과 같다.

// As of Java 21
static void testStringOld(String response) {
    switch (response) {
        case null -> { }
        case String s -> {
            if (s.equalsIgnoreCase("YES"))
                System.out.println("You got it");
            else if (s.equalsIgnoreCase("NO"))
                System.out.println("Shame");
            else
                System.out.println("Sorry?");
        }
    }
}

여기서 문제는 단일 pattern을 사용하여 사례를 구분하는 것이 단일 조건 이상으로 확장되지 않는다는 것이다.
여러 pattern을 작성하는 것을 선호하지만 pattern에 대한 세분화를 표현할 방법이 필요하다.
따라서 switch block의 경우 절에서 case label을 pattern화 하기 위한 guard를 지정할 수 있도록 허용한다.
(예: case String s when s.equalsIgnoreCase("YES")
이러한 case label을 guard 된 case label이라고 하고 boolean expression을 guard라고 한다.)

이 접근 방식을 사용하면 guard를 사용하여 위의 코드를 다시 작성할 수 있다.

// As of Java 21
static void testStringNew(String response) {
    switch (response) {
        case null -> { }
        case String s
        when s.equalsIgnoreCase("YES") -> {
            System.out.println("You got it");
        }
        case String s
        when s.equalsIgnoreCase("NO") -> {
            System.out.println("Shame");
        }
        case String s -> {
            System.out.println("Sorry?");
        }
    }
}

이렇게 하면 테스트의 복잡성이 switch 규칙의 왼쪽에 표시되고 해당 테스트가 충족될 경우 적용되는 논리가 switch 규칙의 오른쪽에 표시되는 보다 읽기 쉬운 스타일의 switch 프로그래밍이 가능해진다.

다른 알려진 constant string에 대한 추가 규칙을 사용하여 이 예제를 더욱 개선할 수 있다.

// As of Java 21
static void testStringEnhanced(String response) {
    switch (response) {
        case null -> { }
        case "y", "Y" -> {
            System.out.println("You got it");
        }
        case "n", "N" -> {
            System.out.println("Shame");
        }
        case String s
        when s.equalsIgnoreCase("YES") -> {
            System.out.println("You got it");
        }
        case String s
        when s.equalsIgnoreCase("NO") -> {
            System.out.println("Shame");
        }
        case String s -> {
            System.out.println("Sorry?");
        }
    }
}

Switches and enum constants

현재 case label에 enum constant를 사용하는 것은 매우 제한적이다.
switch의 selector 표현식은 enum type이어야 하며 label은 enum constants의 간단한 이름이어야 한다.
예를 들면 다음과 같다.

// Prior to Java 21
public enum Suit { CLUBS, DIAMONDS, HEARTS, SPADES }

static void testforHearts(Suit s) {
    switch (s) {
        case HEARTS -> System.out.println("It's a heart!");
        default -> System.out.println("Some other suit");
    }
}

pattern label을 추가한 후에도 이 제약 조건으로 인해 불필요하게 장황한 코드가 생성된다.
예를 들면 다음과 같다.

// As of Java 21
sealed interface CardClassification permits Suit, Tarot {}
public enum Suit implements CardClassification { CLUBS, DIAMONDS, HEARTS, SPADES }
final class Tarot implements CardClassification {}

static void exhaustiveSwitchWithoutEnumSupport(CardClassification c) {
    switch (c) {
        case Suit s when s == Suit.CLUBS -> {
            System.out.println("It's clubs");
        }
        case Suit s when s == Suit.DIAMONDS -> {
            System.out.println("It's diamonds");
        }
        case Suit s when s == Suit.HEARTS -> {
            System.out.println("It's hearts");
        }
        case Suit s -> {
            System.out.println("It's spades");
        }
        case Tarot t -> {
            System.out.println("It's a tarot");
        }
    }
}

이 코드는 guard pattern이 많은 대신 각 enum constant에 대해 별도의 case를 사용 수 있다면 더 가독성이 높아질 것이다.
따라서 selector 표현식이 enum이어야 한다는 요건을 완화하고 case constant가 enum constant의 정규화된 이름을 사용할 수 있도록 허용하였다.
이를 통해 위의 코드를 다음과 같이 재작성할 수 있다.

// As of Java 21
static void exhaustiveSwitchWithBetterEnumSupport(CardClassification c) {
    switch (c) {
        case Suit.CLUBS -> {
            System.out.println("It's clubs");
        }
        case Suit.DIAMONDS -> {
            System.out.println("It's diamonds");
        }
        case Suit.HEARTS -> {
            System.out.println("It's hearts");
        }
        case Suit.SPADES -> {
            System.out.println("It's spades");
        }
        case Tarot t -> {
            System.out.println("It's a tarot");
        }
    }
}

Improved enum constant case labels

기존 Java코드와의 호환성을 유지하기 위해 enum type을 switching 할 때 case constant는 switch 되는 enum type의 constant의 간단한 이름을 그대로 사용할 수 있다.

새로운 코드의 경우 enum 처리를 확장한다.
먼저 enum constant의 정규화된 이름이 case constant로 표시되도록 허용한다.
이러한 정규화된 이름은 enum type을 전화할 때 사용할 수 있다.

enum constant 중 하나의 이름이 case constant로 사용되는 경우 select 표현식이 enum type이어야 한다는 요건을 삭제한다.
이러한 상황에서는 이름이 정규화되어야 하고 그 값이 select 표현식의 유형과 할당 호환 가능해야 한다.
(이렇게 하면 enum case constant의 처리 방식이 숫자 case constant와 일치하게 된다.)

예를 들어 다음 두 가지 방법이 허용된다.

// As of Java 21
sealed interface Currency permits Coin {}
enum Coin implements Currency { HEADS, TAILS } 

static void goodEnumSwitch1(Currency c) {
    switch (c) {
        case Coin.HEADS -> {    // Qualified name of enum constant as a label
            System.out.println("Heads");
        }
        case Coin.TAILS -> {
            System.out.println("Tails");
        }
    }
}

static void goodEnumSwitch2(Coin c) {
    switch (c) {
        case HEADS -> {
            System.out.println("Heads");
        }
        case Coin.TAILS -> {    // Unnecessary qualification but allowed
            System.out.println("Tails");
        }
    }
}

다음 예제는 허용되지 않는다.

// As of Java 21
static void badEnumSwitch(Currency c) {
    switch (c) {
        case Coin.HEADS -> {
            System.out.println("Heads");
        }
        case TAILS -> {         // Error - TAILS must be qualified
            System.out.println("Tails");   
        }
        default -> {
            System.out.println("Some currency");
        }
    }
}

Patterns in switch labels

switch block에서 switch label을 읽을 수 있도록 문법이 수정되었다.

SwitchLabel:
  case CaseConstant { , CaseConstant }
  case null [, default]
  case Pattern [ Guard ]
  default

주요 개선 사항은 새로운 case label인 case p를 도입하는 것이다. (여기서 p는 패턴)
select 표현식의 값을 switch label과 비교하여 label 중 하나를 선택하고 해당 label과 연관된 코드를 실행하거나 평가하는 switch의 본질은 변하지 않는다.
이제 pattern이 있는 case label의 경우 선택되는 label이 동일성 테스트가 아닌 pattern matching 결과에 따라 결정된다는 점이 달라졌다.
예를 들어 다음 코드에서 객체의 값은 pattern Long l 과 일치하며, label case Long l 과 연결된 표현식이 평가된다.

// As of Java 21
static void patternSwitchTest(Object obj) {
    String formatted = switch (obj) {
        case Integer i -> String.format("int %d", i);
        case Long l    -> String.format("long %d", l);
        case Double d  -> String.format("double %f", d);
        case String s  -> String.format("String %s", s);
        default        -> obj.toString();
    };
}

pattern matching에 성공한 후에는 matching 결과를 추가로 테스트하는 경우가 많다.
이로 인해 다음과 같은 번거로운 코드가 발생할 수 있다.

// As of Java 21
static void testOld(Object obj) {
    switch (obj) {
        case String s:
            if (s.length() == 1) { ... }
            else { ... }
            break;
        ...
    }
}

원하는 테스트 (객체가 길이 1의 문자열이라는 것)는 안타깝게도 pattern case label과 다음 if문으로 나뉘어있다.

이 문제를 해결하기 위해 boolean 표현식인 optional guard 가 pattern label 뒤에 올 수 있도록 하여 guarded pattern case label 을 도입했다.
이렇게 하면 위의 코드를 재작성하여 모든 조건 로직이 switch label로 옮겨지도록 할 수 있다.

// As of Java 21
static void testNew(Object obj) {
    switch (obj) {
        case String s when s.length() == 1 -> ...
        case String s                      -> ...
        ...
    }
}

첫 번째 절은 객체가 문자열이면서 길이가 1이면 일치한다.
두 번째 경우는 객체가 길이에 상관없이 문자열이면 일치한다.

pattern label에만 guard가 있을 수 있다.
예를 들어 case constant와 guard가 있는 label을 작성하는 것은 유효하지 않다.
(예: case "Hello" when callRandomBooleanExpression() )

switch에서 pattern을 지원할 때 고려해야 할 5가지 주요 언어 디자인 영역이 있다.

  • enhanced type checking
  • switch 표현식 및 명령문의 완전성
  • pattern variable 선언의 범위
  • null 처리
  • 에러

Enhanced type checking

Selector expression typing

switch에서 pattern을 지원한다는 것은 selector 표현식의 type에 대한 제한을 완화할 수 있다는 것을 의미한다.
현재 일반 switch에서 selector 표현식의 type은 정수형 primitive type (long 제외), 해당 boxed form(예: Character, Byte, Short 또는 Integer), String 또는 enum type 중 하나여야 한다.
이를 확장하여 selector 표현식의 type이 정수형 primitive type (long 제외) 또는 referenced type 이어야 한다.

예를 들어 다음 pattern switch에서 selector 표현식 객체는 class type, enum, record type 및 array type과 함께 case label 및 default가 포함된 type pattern과 일치한다.

// As of Java 21
record Point(int i, int j) {}
enum Color { RED, GREEN, BLUE; }

static void typeTester(Object obj) {
    switch (obj) {
        case null     -> System.out.println("null");
        case String s -> System.out.println("String");
        case Color c  -> System.out.println("Color: " + c.toString());
        case Point p  -> System.out.println("Record class: " + p.toString());
        case int[] ia -> System.out.println("Array of ints of length" + ia.length);
        default       -> System.out.println("Something else");
    }
}

switch block의 모든 case label은 selector 표현식과 호환되어야 한다.
pattern label이라고 하는 pattern이 있는 case label의 경우 표현식과 pattern의 호환성이라는 기존 개념을 사용한다.

Dominance of case labels

pattern case label을 지원한다는 것은 selector 표현식의 주어진 값에 대해 이전에는 최대 하나의 case label만 적용될 수 있었지만 이제 2개 이상의 case label이 적용될 수 있다는 것을 의미한다.
예를 들어 selector 표현식이 문자열로 평가되는 경우 case label인 case lables case String scase CharSequence cs 가 모두 적용된다.

가장 먼저 해결해야 할 문제는 이 상황에서 어떤 label을 적용해야 하는지 정확히 결정하는 것이다.
복잡한 최적 접근 방식을 시도하는 대신 더 간단한 의미론을 채택하였다.
switch block에서 값이 적용되는 첫 번째 case label이 선택된다.

// As of Java 21
static void first(Object obj) {
    switch (obj) {
        case String s ->
            System.out.println("A string: " + s);
        case CharSequence cs ->
            System.out.println("A sequence of length " + cs.length());
        default -> {
            break;
        }
    }
}

이 예제에서는 객체 값이 String type이면 첫 번째 case label이 적용되고 String type이 아닌 CharSequence type이면 두 번째 pattern label이 적용된다.

하지만 이 두 case label의 순서를 바꾸면 어떻게 될까?

// As of Java 21
static void error(Object obj) {
    switch (obj) {
        case CharSequence cs ->
            System.out.println("A sequence of length " + cs.length());
        case String s ->    // Error - pattern is dominated by previous pattern
            System.out.println("A string: " + s);
        default -> {
            break;
        }
    }
}

이제 객체 값이 String type인 경우 switch block에서 가장 먼저 나타나므로 CharSequence case label이 적용된다.
String case label은 selector 표현식의 값을 선택하게 하는 값이 없다는 점에서 도달할 수 없다.
연결할 수 없는 코드와 유사하게이는 프로그래머 오류로 간주되어 compile-time error가 발생한다.

좀 더 정확하게 말하면, 첫 번째 case label case 구분 기호인 CharSequence cs 가 두 번째 case label 구분 기호인 String s 를 지배한다고 말할 수 있는데, 이는 String s pattern과 일치하는 모든 값이 CharSequence cs pattern과도 일치하지만 그 반대의 경우는 아니기 때문이다.
이는 두 번째 pattern의 type인 String 이 첫 번째 pattern의 type인 CharSequence 의 subtype이기 때문이다.

unguarded pattern case label은 동일한 pattern을 가진 guarded pattern case label 보다 우선한다.
예를 들어 (unguarded) pattern case label case String s 는 guarded pattern case label case String when s.length() > 0 에 일치하는 모든 값이 case String s when s.length() > 0 에 일치해야 하기 때문에 guarded pattern case label case String s 보다 우위에 있다.

guarded pattern case label은 전자의 pattern이 후자의 pattern을 지배하고 guard가 참이라는 값의 constant 표현인 경우에만 다른 pattern case label (guard 또는 unguard)를 지배한다.
예를 들어 guarded pattern case label String s 가 pattern case label String s 를 지배하는 경우 일반적으로 결정할 수 없는 문제인 pattern label과 일치하는 값을 보다 정확하게 결정하기 위해 guard 표현식을 더 이상 분석하지 않는다.

pattern case label은 constant case label을 지배할 수 있다.
예를 들어 A가 enum class type E의 멤버인 경우 pattern case label Integer i 가 constant case label case 42 를 지배하고 pattern case label case E 가 constant case label case A 를 지배한다.
guard가 없는 동일한 pattern case label이 있는 경우 guarded pattern case label이 constant case label을 지배한다.
즉 일반적으로 결정할 수 없으므로 guard를 확인하지 않는다.
예를 들어 pattern case label case String s when s.length() > 1 는 예상대로 constant case label case "hello" 를 지배하지만 case Integer i when i != 0 은 case label case 0 을 지배한다.

이 모든 것은 constant case label이 guarded pattern case label앞에 표시되고 guarded pattern case label이 unguarded pattern case label 앞에 표시되어야 하는 간단하고 예측 가능하며 읽기 쉬운 case label 순서를 제안한다.

// As of Java 21
Integer i = ...
switch (i) {
    case -1, 1 -> ...                   // Special cases
    case Integer j when j > 0 -> ...    // Positive integer cases
    case Integer j -> ...               // All the remaining integers
}

compiler는 모든 case label을 확인한다.
switch block의 case label이 해당 switch block의 선행 case label에 의해 지배되는 것은 compil 시 오류이다.
이 우세 요건은 switch block에 type pattern case label만 포함된 경우 subtype 순서로 표시되도록 한다.

(우세 개념은 type문의 catch 절에 대한 조건과 유사하며, exception class E를 잡는 catch 절 앞에 E 또는 E의 상위 class를 잡을 수 있는 catch 절이 오면 오류이다.
논리적으로 앞의 catch 절이 뒤의 catch 절보다 우선한다.)

또한 switch 식 또는 switch 문의 switch block에 match-all switch label이 두 개 이상 있는 것도 compile-time error이다.
match-all label은 pattern이 selector 표현식과 무조건 일치하는 기본 및 pattern case label이다.
예를 들어 type pattern String s 는 String type의 selector 표현식과 무조건 일치하고 type pattern Object o 는 모든 참조 유형의 selector 표현식과 무조건 일치한다.

// As of Java 21
static void matchAll(String s) {
    switch(s) {
        case String t:
            System.out.println(t);
            break;
        default:
            System.out.println("Something else");  // Error - dominated!
    }
}

static void matchAll2(String s) {
    switch(s) {
        case Object o:
            System.out.println("An Object");
            break;
        default:
            System.out.println("Something else");  // Error - dominated!
    }
}

Exhaustiveness of switch expressions and statements

Type coverage

switch 표현식은 selector 표현식의 가능한 모든 값을 switch block에서 처리해야 한다.
즉, 완전해야 한다.
이렇게 하면 switch 표현식을 성공적으로 평가하면 항상 값이 산출된다는 속성을 유지할 수 있다.

일반 switch 표현식의 경우, 이 속성은 switch block의 간단한 추가 조건 집합에 의해 적용된다.

pattern switch 표현식 및 문의 경우 switch block에서 switch label의 type coverage 개념을 정의하여 이를 구현한다.
그런 다음 switch block에 있는 모든 switch label의 type 적용 범위를 결합하여 switch block이 selector 표현식의 모든 가능성을 소진하는지 여부를 결정한다.

이 (잘못된) pattern switch 표현식을 가정해 보자.

// As of Java 21
static int coverage(Object obj) {
    return switch (obj) {           // Error - not exhaustive
        case String s -> s.length();
    };
}

switch block에는 단 하나의 switch label, case String s만 있다.
이는 type이 String의 하위 유형인 객체의 모든 값과 일치한다.
따라서 이 switch label type coverage는 String의 모든 subtype이라고 할 수 있다.
이 pattern switch 표현식은 switch block의 type 범위(String의 모든 하위 유형)에 selector 표현식(OBject)의 유형이 포함되지 않으므로 완전하지 않다.

이 (여전히 잘못된) 예를 생각해 보자.

// As of Java 21
static int coverage(Object obj) {
    return switch (obj) {           // Error - still not exhaustive
        case String s  -> s.length();
        case Integer i -> i;
    };
}

이 switch block의 type coverage는 두 switch label의 coverage를 합한 것이다.
즉, type coverage는 String의 모든 subtype 집합과 Integer의 모든 subtype 집합이다.
그러나 type coverage에 여전히 selector 표현식의 유형이 포함되지 않으므로 이 pattern switch 표현식도 완전하지 않으며 compile-time error가 발생한다.

default label의 type coverage는 모든 type이므로 이 예제는 (마침내!) 유효하다.

// As of Java 21
static int coverage(Object obj) {
    return switch (obj) {
        case String s  -> s.length();
        case Integer i -> i;
        default -> 0;
    };
}

Exhaustiveness in practice

type coverage라는 개념은 non-pattern switch 표현식에도 이미 존재한다.
예를 들면 다음과 같다.

// As of Java 20
enum Color { RED, YELLOW, GREEN }

int numLetters = switch (color) {   // Error - not exhaustive!
    case RED -> 3;
    case GREEN -> 5;
}

enum class에 대한 이 switch 표현식은 예상되는 입력 YELLOW가 포함되지 않기 때문에 완전하지 않다.
예상대로 YELLOW enum constant를 처리하기 위해 case label을 추가하면 switch를 완전하게 만드는데 충분하다.

// As of Java 20
int numLetters = switch (color) {   // Exhaustive!
    case RED -> 3;
    case GREEN -> 5;
    case YELLOW -> 6;
}

이러한 방식으로 작성된 switch는 두 가지 중요한 이점이 있다.

첫째, 이미 모든 경우를 처리했기 때문에 exception을 던지는 기본 적을 작성해야 하는 것은 번거로울 수 있다.

int numLetters = switch (color) {
    case RED -> 3;
    case GREEN -> 5;
    case YELLOW -> 6;
    default -> throw new ArghThisIsIrritatingException(color.toString());
}

이 상황에서 기본 절을 수동으로 작성하는 것은 짜증스러울 뿐만 아니라 compiler가 기본 절 없이도 완정성을 더 잘 검사할 수 있기 때문에 실제로는 해롭다.
(case default, case null, default 또는 unconditional type pattern과 같은 다른 match-all 절의 경우에도 마찬가지이다.)
기본 절을 생략하면 case label을 잊어버렸는지 runtime에 발견하지 않고 complie 시점에 발견하게 되며, 심지어는 발견하지 못할 수도 있다.

더 중요한 것은 나중에 누군가 Color enum에 다른 constasnt를 추가하면 어떻게 될까?
명시적으로 일치하는 모든 절을 사용하면 runtime에 새 constant 값이 표시되는 경우에만 발견할 수 있다.
하지만 compile 시점에 알려진 모든 constant가 포함하도록 switch를 코딩하고 match-all 절을 생략하면 다음에 switch가 포함된 class를 다시 compile 할 때 이 변경 사항을 알 수 있다.
match-all 절은 완전성 오류를 덮어버릴 위험이 있다.

결론적으로 가능하면 match-all 절이 없는 전체 switch가 match-all 절이 있는 switch 보다 낫다.

runtime을 살펴볼 때 새로운 Color constant가 추가되었는데 switch가 포함된 class가 다시 compile 되지 않으면 어떻게 될까?
새 constant가 switch에 노출될 위험이 있다.
이러한 위험은 enum에 항상 존재하므로 enum switch에 일치 절이 없는 경우 compiler는 예외를 던지는 기본 절을 합성한다.
이렇게 하면 절 중 하나를 선택하지 않으면 switch가 정상적으로 완료되지 않는다.

완전성의 개념은 모든 합리적인 경우를 포괄하는 동시에 실제 가치가 거의 없는 드문 corner case를 많이 작성하도록 강요하지 않으면서도 코드를 오염시키거나 심지어 지배하지 않도록 균형을 맞추기 위해 고안된 개념이다.
다시 말해 완전성은 실제 runtime 완전성에 대한 compile-time 근사치이다.

Exhaustiveness and sealed classes

selector 표현식의 type이 sealed class 인 경우 type coverage check에서 sealed class의 허용 절을 고려하여 switch block이 포괄적인지 여부를 결정할 수 있다.
이렇게 하면 때때로 default 절의 필요성을 제거할 수 있으며, 이는 위에서 설명한 대로 좋은 관행이다.
세 개의 허용된 subclass A, B, C가 있는 sealed interface S의 다음 예를 보자.

// As of Java 21
sealed interface S permits A, B, C {}
final class A implements S {}
final class B implements S {}
record C(int i) implements S {}    // Implicitly final

static int testSealedExhaustive(S s) {
    return switch (s) {
        case A a -> 1;
        case B b -> 2;
        case C c -> 3;
    };
}

compiler는 switch block의 type coverage가 A, B, C type임을 확인할 수 있다.
selector 표현식 S의 type은 허용되는 subclass가 정확히 A, B, C인 sealed interface이므로 이 switch block은 포괄적이다.
따라서 default label이 필요하지 않다.

허용된 direct subclass가 (generic) sealed superclass의 특정 매개변수화만 구현하는 경우 약간의 주의가 필요하다.
예를 들면 다음과 같다.

// As of Java 21
sealed interface I<T> permits A, B {}
final class A<X> implements I<String> {}
final class B<Y> implements I<Y> {}

static int testGenericSealedExhaustive(I<Integer> i) {
    return switch (i) {
        // Exhaustive as no A case possible!
        case B<Integer> bi -> 42;
    };
}

허용되는 I의 subclass는 A와 B 뿐이지만 compiler는 selector 표현식이 I type이고 A의 매개변수화가 I의 subtype이 아니므로 switch block이 B class만 포괄하면 충분하다는 것을 감지할 수 있다.

다시 말하지만, 완전성의 개념은 근사치이다.
별도의 compile로 인해 interface I의 새로운 구현이 runtime에 표시될 수 있으므로 compiler는 이 경우 throw 하는 합성 기본 절을 삽입한다.

record pattern은 중첩될 수 있기 때문에 완전성 개념은 record pattern(JEP 440)에 의해 더 복잡해진다.
따라서 완전성 개념은 이러한 잠재적 재귀 구조를 반영해야 한다.

Exhaustiveness and compatibility

완전성 요건은 pattern switch 표현식과 pattern switdch 문 모두에 적용된다.
이전 버전과의 호환성을 보장하기 위해 기존의 모든 switch 문은 변경되지 않고 compile 된다.
그러나 switch 문이 이 JEP에 설명된 switch 개선 사항 중 하나를 사용하는 경우 compiler가 완전성 여부를 확인한다.
(향후 Java 언어의 compiler는 완전하지 않은 legacy switch 문에 대해 경고를 표시할 수 있다.)

보다 정확하게는 pattern 또는 null label을 사용하거나 selector 표현식이 legacy type (char, byte, short, int, Character, Byte, Short, Integer 또는 enum type) 중 하나가 아닌 모든 switch 문에 대해 완전성이 요구된다.
예를 들면 다음과 같다.

// As of Java 21
sealed interface S permits A, B, C {}
final class A implements S {}
final class B implements S {}
record C(int i) implements S {}    // Implicitly final

static void switchStatementExhaustive(S s) {
    switch (s) {                   // Error - not exhaustive;
                                   // missing clause for permitted class B!
        case A a :
            System.out.println("A");
            break;
        case C c :
            System.out.println("C");
            break;
    };
}

Scope of pattern variable declarations

pattern variable (JEP 394)는 pattern으로 선언되는 local variable이다.
pattern variable 선언은 그 범위가 흐름에 민감하다는 점에서 특이하다.
다시 한번 정리하자면 다음 예제에서 type pattern String s가 pattern variable s를 선언하는 경우를 생각해 보자.

// As of Java 21
static void testFlowScoping(Object obj) {
    if ((obj instanceof String s) && s.length() > 3) {
        System.out.println(s);
    } else {
        System.out.println("Not a string");
    }
}

s의 선언은 pattern variable s가 초기화될 코드 부분의 범위 내에 있다.
이 예에서는 && 표현식의 오른쪽 피연산자와 "then" block에 있다.
그러나 s는 "else" block의 범위 내에 있지 않다.
"else" block으로 제어권이 넘어가려면 pattern match가 실패해야 하며, 이 경우 pattern variable이 초기화되지 않을 것이다.

pattern variable 선언에 대한 이러한 흐름에 민감한 범위 개념을 확장하여 세 가지 새로운 규칙을 통해 case label에서 발생하는 pattern 선언을 포함하도록 한다.

  1. guarded case label에서 발생하는 pattern variable 선언의 범위에는 guard 즉 when 표현식이 포함된다.
  2. switch 규칙의 case label에서 발생하는 pattern variable 선언의 범위에는 화살표 오른쪽에 표시되는 표현식, block 또는 throw 문이 포함된다.
  3. switch label이 지정된 문 그룹의 case label에서 발생하는 pattern variable 선언의 범위에는 해당 문 그룹의 block 문이 포함된다.
    pattern variable을 선언하는 case label을 통과하는 것은 금지되어 있다.

이 예에서는 첫 번째 규칙의 적용을 보여준다.

// As of Java 21
static void testScope1(Object obj) {
    switch (obj) {
        case Character c
        when c.charValue() == 7:
            System.out.println("Ding!");
            break;
        default:
            break;
    }
}

pattern variable c의 선언 범위에는 guard, 즉 c.charValue() == 7 식이 포함된다.

다음 변형은 두 번째 규칙의 적용을 보여준다.

// As of Java 21
static void testScope2(Object obj) {
    switch (obj) {
        case Character c -> {
            if (c.charValue() == 7) {
                System.out.println("Ding!");
            }
            System.out.println("Character");
        }
        case Integer i ->
            throw new IllegalStateException("Invalid Integer argument: "
                                            + i.intValue());
        default -> {
            break;
        }
    }
}

여기서 pattern variable c의 선언 범위는 첫 번째 화살표 오른쪽에 있는 block이다.
pattern variable i의 선언 범위는 두 번째 화살표 오른쪽에 있는 throw 문이다.

세 번째 규칙은 더 복잡하다.
먼저 switch label이 지정된 문 그룹에 case label이 하나만 있는 예를 살펴보자.

// As of Java 21
static void testScope3(Object obj) {
    switch (obj) {
        case Character c:
            if (c.charValue() == 7) {
                System.out.print("Ding ");
            }
            if (c.charValue() == 9) {
                System.out.print("Tab ");
            }
            System.out.println("Character");
        default:
            System.out.println();
    }
}

pattern variable c의 선언 범위에는 문 그룹의 모든 문, 즉 두 개의 if 문과 println 문이 포함된다.
첫 번째 문 그룹의 실행이 default switch label을 통과하여 이러한 문을 실행할 수 있지만 범위에는 default 문 그룹의 문이 포함되지 않는다.

pattern variable을 선언하는 case label을 통과할 수 있는 가능성을 금지한다.
이 잘못된 예는 다음과 같다.

// As of Java 21
static void testScopeError(Object obj) {
    switch (obj) {
        case Character c:
            if (c.charValue() == 7) {
                System.out.print("Ding ");
            }
            if (c.charValue() == 9) {
                System.out.print("Tab ");
            }
            System.out.println("character");
        case Integer i:                 // Compile-time error
            System.out.println("An integer " + i);
        default:
            break;
    }
}

이를 허용하고 객체의 값이 Character인 경우 switch block의 실행은 pattern variable i가 초기화되지 않았을 경우 Integer i 다음에 두 번째 문 그룹을 통과할 수 있다.
따라서 pattern variable을 선언하는 case label을 통해 실행을 허용하면 compile-time error가 발생한다.

이것이 바로 여러 개의 pattern label로 구성된 switch label(예: case String c: case Integer i: ...)이 허용되지 않는 이유이다.
단일 case label 내에서 여러 pattern을 금지하는 것도 비슷한 논리가 적용된다.
case Character c, Integer i: ...case Character c, Integer i -> ... 모두 허용되지 않는다.
이러한 case label이 허용되면 콜론 또는 화설표 뒤의 범위에 c와 i가 모두 포함되지만 객체의 값이 문자 또는 정수인지에 따라 둘 중 하나만 초기화된다.

반면에 이 예제에서 볼 수 있듯이 pattern variable을 선언하지 않는 label을 통과하는 것은 안전하다.

// As of Java 21
void testScope4(Object obj) {
    switch (obj) {
        case String s:
            System.out.println("A string: " + s);  // s in scope here!
        default:
            System.out.println("Done");            // s not in scope here
    }
}

Dealing with null

일반적으로 switch는 selector 표현식이 null로 평가되면 NullPointerException을 던진다.
이는 잘 알려진 동작이며 기존 switch 코드에 대한 변경을 제안하지 않는다.
그러나 pattern matching과 null 값에 대한 합리적이고 Exception을 발생시키지 않는 시맨틱이 있으므로 pattern switch block에서 기존 switch 시맨틱과 호환성을 유지하면서 보다 일반적인 방식으로 null을 처리할 수 있다.

먼저 새로운 null case label을 도입한다.
그런 다음 selector 표현식의 값이 null이면 switch가 즉시 NullPointerException을 던진다는 포괄적인 규칙을 해제한다.
대신 case label을 검사하여 switch의 동작을 결정한다.

  • selector 표현식이 null로 평가되면 case label이 모두 일치하는 것으로 간주된다.
    switch block과 연관된 label이 없으면 switch는 이전과 마찬가지로 NullPointerException을 던진다.
  • selector 표현식이 null이 아닌 값으로 평가되면 정상적으로 일치하는 case label을 선택한다.
    일치하는 case label이 없으면 모든 default label이 일치하는 것으로 간주된다.

예를 들어 아래 선언이 주어졌을 때 nullMatch(null)을 평가하면 NullPointerException을 던지는 대신 null! 을 출력한다.

// As of Java 21
static void nullMatch(Object obj) {
    switch (obj) {
        case null     -> System.out.println("null!");
        case String s -> System.out.println("String");
        default       -> System.out.println("Something else");
    }
}

case null label이 없는 switch block은 본문이 NullPointerException을 던지는 case null 규칙이 있는 것처럼 취급된다.
즉 다음의 코드는

// As of Java 21
static void nullMatch2(Object obj) {
    switch (obj) {
        case String s  -> System.out.println("String: " + s);
        case Integer i -> System.out.println("Integer");
        default        -> System.out.println("default");
    }
}

다음과 동일하다.

// As of Java 21
static void nullMatch2(Object obj) {
    switch (obj) {
        case null      -> throw new NullPointerException();
        case String s  -> System.out.println("String: " + s);
        case Integer i -> System.out.println("Integer");
        default        -> System.out.println("default");
    }
}

두 예제 모두에서 nullMatch(null)을 평가하면 NullPointerException이 발생하게 된다.

기존 switch 구조의 직관성을 유지하여 null에 대해 switch를 수행하는 것은 예외적인 작업이다.
pattern switch의 차이점은 switch 내부에서 이 경우를 직접 처리할 수 있다는 점이다.
switch blcok에 null label이 표시되면 해당 label은 null 값과 일치한다.
switch block에 null label이 표시되지 않으면 이전과 마찬가지로 null 값으로 switching 하면 NullPointerException이 발생한다.
따라서 switch block에서 null 값 처리가 정규화되었다.

null case와 default를 결합하는 것은 의미 있는 일이며 드문 일은 아니다.
이를 위해 null case label에 optional default를 허용한다.

// As of Java 21
Object obj = ...
switch (obj) {
    ...
    case null, default ->
        System.out.println("The rest (including null)");
}

객체의 값이 null 참조값이거나 다른 case label이 일치하지 않는 경우 이 label과 일치한다.

switch block에 default가 함께 있는 null case label과 default label이 함께 있는 경우 compile-time error가 발생한다.

Errors

pattern matching이 갑자기 완료될 수 있다.
예를 들어 record pattern에 대해 값을 일치시킬 때 record의 accessor method가 갑자기 완료될 수 있다.
이 경우 pattern matching은 MatdchException을 던져 갑작스럽게 완료되도록 정의된다.
이러한 pattern이 switch의 label로 나타나면 switch도 MatchException을 던져 갑작스럽게 완료된다.

case pattern에 guard가 있고 guard 평가가 갑자기 완료되면 같은 이유로 switch도 갑자기 완료된다.

pattern switch의 label이 selector 표현식의 값과 일치하지 않으면 pattern switch는 완전해야 하므로 MatchException을 던저 switch가 갑작스럽게 완료된다.

예를 들면

// As of Java 21
record R(int i) {
    public int i() {    // bad (but legal) accessor method for i
        return i / 0;
    }
}

static void exampleAnR(R r) {
    switch(r) {
        case R(var i): System.out.println(i);
    }
}

exampleAnR(new R(42))MatchException 을 던진다.
(항상 예외를 던지는 record accessor method는 매우 불규칙하며, MatchException을 던지는 철저한 pattern switch는 매우 드문 경우이다.)

이와는 대조적으로

// As of Java 21
static void example(Object obj) {
    switch (obj) {
        case R r when (r.i / 0 == 1): System.out.println("It's an R!");
        default: break;
    }
}

example(new R(42))ArithmeticException 을 발생시킨다.

pattern switch 시맨틱과 일치하도록 enum class에 대한 switch 표현식은 이제 runtime에 switch label이 적용되지 않을 때 IncompatibleClassChangeError 대신 MatchException을 던진다.
이는 호환되지 않는 사소한 언어 변경이다.
(enum에 대한 완전한 switch는 switch가 컴파일된 후에 enum class가 변경된 경우에만 일치하지 않는데 이는 매우 드문 경우이다.)

JEP 442: Foreign Function & Memory API (Third Preview)

JDK 19에서 JEP 424로 preview 기능으로 제안되었으며 JDK 20에서 JEP 434로 두 번째 preview를 거쳐 이번 JDK 21에 세 번째 Preview로 제안되었다.
이번 주요 변경 사항은 다음과 같다.

  • Arena interface에서 native segment의 lifetime 관리를 중앙 집중화
  • address layout을 역 참조하기 위한 새로운 요소로 layout path 향상
  • 수명이 짧고 Java로 상향 호출되지 않는 함수 (예: clock_gettime)에 대한 호출을 최적화하기 위한 linker option 제공
  • porting을 용이하게 하기 위해 libffi 기반의 fallback native linker 구현 제공
  • VaList class 제거

JEP 443: Unnamed Patterns and Variables (Preview)

component의 name이나 type을 명시하지 않고 record component와 일치하는 unnamed pattern과 초기화할 수 있지만 사용할 수 없는 unnamed variable를 사용하여 Java 언어를 향상한다.
둘 다 밑줄 문자 _ 로 표시된다.

주요 목표는 다음과 같다.

  • 불필요한 nested pattern을 제거하여 record pattern의 가독성을 향상
  • 선언해야 하지만 (예: catch 절에서) 사용되지 않을 변수를 식별하여 모든 코드의 유지 관리성을 향상

Unused patterns

JEP 395의 record와 JEP 440의 record pattern과 함께 사용하여 data 처리를 간소화한다.
record class는 data item의 component를 instance로 집계하는 반면 record class의 instance를 수신하는 코드는 record pattern과 pattern matching을 사용하여 instance를 해당 component로 분해한다.

예를 들어

record Point(int x, int y) { }
enum Color { RED, GREEN, BLUE }
record ColoredPoint(Point p, Color c) { }
... new ColoredPoint(new Point(3,4), Color.GREEN) ...
if (r instanceof ColoredPoint(Point p, Color c)) {
    ... p.x() ... p.y() ...
}

이 코드에서 program의 한 부분은 ColoredPoint instance를 생성하고 다른 부분은 instanceof와 pattern matching을 사용하여 변수가 ColoredPoint인지 테스트하고 만약 그렇다면 두 component를 추출한다.

ColoredPoint(Point p, Color c) 와 같은 record pattern은 설명하기 좋지만 program에서 추가 처리를 위해 일부 component만 필요로 하는 경우가 일반적이다.
예를 들어 위 코드에서는 if block에 c가 아닌 p만 필요하다.
이러한 pattern matching을 수행할 때마다 record class의 모든 component를 작성하는 것은 번거롭다.
또한 Color component가 관련성이 없다는 것이 시각적으로 명확하지 않다.
게다가 이로 인해 if block의 조건도 읽기 어려워진다.
이는 다음과 같이 component 내에서 data를 추출하기 위해 record pattern을 중첩할 때 특히 두드러진다.

if (r instanceof ColoredPoint(Point(int x, int y), Color c)) {
    ... x ... y ...
}

var를 사용하여 불필요한 component Color c의 시각적 비용을 줄일 수 있지만 예를 들어 ColoredPoint(Point(int x, int y), var c) 와 같이 불필요한 component를 모두 생략하여 비용을 더 줄이는 것이 더 좋다.
이렇게 하면 record pattern을 작성하는 작업이 간소화되고 코드의 군더더기를 제거하여 가독성이 향상된다.

개발자들이 record class의 data-oriented 방법론과 그 동반 메커니즘인 sealed classes (JEP 409)에 대한 경험을 쌓아감에 따라 복잡한 data structure에 대한 pattern matching이 일반화될 것으로 예상한다.
structure의 모양이 그 안에 있는 개별 data item만큼이나 중요한 경우가 많다.
매우 간단한 예로 다음과 같은 Ball 및 Box class와 Box의 content를 검색하는 switch를 생각해 보자.

sealed abstract class Ball permits RedBall, BlueBall, GreenBall { }
final  class RedBall   extends Ball { }
final  class BlueBall  extends Ball { }
final  class GreenBall extends Ball { }
record Box<T extends Ball>(T content) { }
Box<? extends Ball> b = ...
switch (b) {
    case Box(RedBall   red)   -> processBox(b);
    case Box(BlueBall  blue)  -> processBox(b);
    case Box(GreenBall green) -> stopProcessing();
}

각 사례는 contents에 따라 Box를 처리하지만 빨간색, 파란색 및 녹색 변수는 사용되지 않는다.
변수가 사용되지 않으므로 이름을 생략하면 이 코드를 더 읽기 쉽게 만들 수 있다.

또한 처음 두 pattern을 하나의 case label에 그룹화하도록 switch를 리팩터링 한 경우

case Box(RedBall red), Box(BlueBall blue) -> processBox(b);

이러면 component에 이름을 지정하는 것은 잘못된 것이다.
왼쪽의 pattern 중 하나가 일치할 수 있으므로 오른쪽에는 이름을 사용할 수 없다.
이름을 사용할 수 없으므로 제거할 수 있다면 더 좋을 것이다.

Unused variables

기존의 명령형 코드를 사용하다 보면 대부분의 개발자가 의도하지 않은 변수를 선언해야 하는 상황에 직면하게 된다.
이는 일반적으로 명령문의 결과보다 부작용이 더 중요할 때 발생한다.
예를 들어 다음 코드는 loop 변수 순서를 사용하지 않고 loop의 부작용으로 합계를 계산한다.

int total = 0;
for (Order order : orders) {
    if (total < LIMIT) { 
        ... total++ ...
    }
}

order가 사용되지 않는다는 점을 감안할 때 order의 선언이 눈에 띄는 것은 불행한 일이다.
선언을 var order로 줄일 수는 있지만 이 변수에 이름을 지정하는 것을 피할 방법은 없다.
이름 자체는 예를 들어 o로 줄일 수 있지만, 이러한 구문적 트릭으로는 변수가 사용되지 않을 것이라는 의미적 의도를 전달할 수 없다.
또한 정적 분석 도구는 일반적으로 개발자가 사용하지 않으려는 경우에도 사용하지 않는 변수에 대해 불만을 표시하며 warning을 표시하지 않는 방법이 없을 수 있다.

다음은 표현식의 결과보다 부작용이 더 중요하여 사용되지 않는 변수로 이어지는 예제이다.
다음 코드는 데이터를 대기열에 넣지만 element 3개 중 2개만 필요하다.

Queue<Integer> q = ... // x1, y1, z1, x2, y2, z2 .. 
while (q.size() >= 3) {
   int x = q.remove();
   int y = q.remove();
   int z = q.remove(); // z is unused
    ... new Point(x, y) ...
}

remove() 를 세 번째로 호출하면 그 결과가 변수에 할당되는지 여부와 관계없이 element를 queue에 넣는 부작용이 발생하므로 z 선언을 생략할 수 있다.
그러나 유지보수성을 위해 개발자는 현재 사용되지 않거나 정적 분석 경고가 발생하더라도 변수를 선언하여 remove() 의 결과를 일관되게 나타내고자 할 수 있다.
안타깝게도 많은 프로그램에서 변수 이름 선택이 위 코드의 z처럼 쉽지 않다.

사용되지 않는 변수는 부작용에 초점을 맞춘 두 가지 다른 종류의 문에서 자주 발생한다.

  • try-with-resources 문은 항상 그 부작용, 즉 resource 자동 종료에 사용된다.
    경우에 따라 resource는 try block의 코드가 실행되는 context를 나타내며, 코드가 context를 직접 사용하지 않으므로 resource 변수의 이름은 상관없다.
    예를 들어, AutoCloseable인 ScopedContext resource가 있다고 가정하면 다음 코드는 context를 획득하고 자동으로 해제한다.
try (var acquiredContext = ScopedContext.acquire()) {
    ... acquiredContext not used ...
}

acquiredContext라는 이름은 단지 혼란스로울 뿐이므로 생략하는 것이 좋다.

  • Exception은 궁극적인 side effect 이며, exception을 처리하면 사용되지 않는 변수가 발생하는 경우가 많다.
    예를 들어, 대부분의 java 개발자는 exceptino 매개변수의 이름과 무관한 이러한 형식의 catch block을 작성한다.
String s = ...;
try { 
    int i = Integer.parseInt(s);
    ... i ...
} catch (NumberFormatException ex) { 
    System.out.println("Bad number: " + s);
}

부작용이 없는 코드라도 사용하지 않는 변수를 선언해야 할 때가 있다.
예를 들어

...stream.collect(Collectors.toMap(String::toUpperCase,
                                   v -> "NODATA"));

이 코드는 각 키를 동일한 자리 표시자 값에 매핑하는 map을 생성한다.
lamba 매개변수 v는 사용되지 않으므로 이름은 상관없다.

변수가 사용되지 않고 이름이 관련이 없는 이러한 모든 시나리오에서는 이름 없이 변수를 선언할 수 있다면 더 좋을 것이다.
이렇게 하면 유지 관리자가 관련 없는 이름을 이해하지 않아도 되고, 정적 분석 도구에서 미사용에 대한 오탐을 피할 수 있다.

이름 없이 선언할 수 있는 변수의 종류는 위와 같이 method 외부에 표시되지 않는 변수, 즉 지역변수, 예외 매개변수, lambda 매개변수 등이다.
이러한 종류의 변수는 외부 영향 없이 이름을 바꾸거나 이름을 지정하지 않을 수 있다.
반면 필드는 비공개라고 하더라도 메서드 간에 객체의 상태를 전달하며, 이름이 없는 상태는 유용하지도 않고 유지 관리가 불가능하다.

Description

unnamed pattern 은 밑줄 문자 _ (U_005F)로 표시된다.
이 문자를 사용하면 패턴 일치에서 레코드 구성 요소의 유형과 이름을 생략할 수 있다.

  • ... instanceof Point(int x, _)
  • case Point(int x, _)
    unnamed pattern variable 은 type pattern의 pattern variable이 밑줄로 표시될 때 선언된다.
    이 경우 type pattern의 type 또는 var 뒤에 오는 식별자를 생략할 수 있다.
  • ... instanceof Point(int x, int _)
  • case Point(int x, int _)
    local variable 선언 문의 local variable, catch 절의 예외 매개변수 또는 lamba 표현식의 lamba 매개변수가 밑줄로 표시될 때 unnamed variable이 선언된다.
    이를 통해 문이나 표현식에서 type 또는 var 뒤에 오는 식별자를 생략할 수 있다.
  • int _ = q.remove();
  • ... } catch (NumberFormatException _) { ...
  • (int x, int _) -> x + x

_ -> "NODATA" 와 같은 단일 매개변수 lambda 표현식의 경우 매개변수로 사용되는 unnamed variable을 unnamed pattern과 혼동해서는 안된다.

밑줄 하나는 이름이 없음을 나타내는 가장 가벼운 합리적인 구문이다.
밑줄은 Java 1.0에서 식별자로 유효했기 때문에 2014년에 unnamed pattern과 variable에 대한 밑줄을 되찾기 위한 장기적인 프로세스를 시작했다.
2014년 Java 8에서 밑줄이 식별자로 사용되었을 때 컴파일 타임 경고를 발행하기 시작했으며 Java 9(2017, JEP 213)에서 이러한 경고를 오류로 전환했다.
스칼라 파이썬등 다른 많은 언어에서도 밑줄을 사용하여 unnamed variable을 선언한다.

밑줄은 java letter와 Java letter-or-digit로 유지되므로 길이가 두 개 이상인 식별자에 밑줄을 사용할 수 있는 기능은 변경되지 않는다.
예를 들어 _ageMAX_AGE__ (밑줄 2개)와 같은 식별자는 계속 유효하다.

밑줄을 숫자 구분 기호로 사용할 수 있는 기능은 변경되지 않는다.
예를 들어 123_456_7890b1010_0101 과 같은 숫자 리터럴은 계속 유효하다.

The unnamed pattern

unnamed pattern은 아무것도 바인딩하지 않는 무조건 적인 pattern이다.
type pattern이나 record pattern 대신 중첩된 위치에서 사용할 수 있다.
예를 들어

  • ... instanceof Point(_, int y)
    는 유효하지만 다음은 그렇지 않다.
  • r instanceof _
  • r instanceof _(int x, int y)

따라서 앞의 예제에서는 Color component의 type pattern을 완전히 생략할 수 있다.

if (r instanceof ColoredPoint(Point(int x, _), _)) { ... x ... y ... }


깊게 중첩된 위치에서 unnamed pattern을 사용하면 복잡한 데이터 추출을 수행하는 코드의 가독성이 향상된다.

if (r instanceof ColoredPoint(_, Color c)) { ... c ... }


예를 들어 이 코드는 중첩된 포인트의 x 좌표를 추출하는 동시에 y 및 색상 component를 모두 생략한다.

if (r instanceof ColoredPoint(Point(int x, _), _)) { ... x ... }

Unnamed pattern variables

unnamed pattern variable은 type pattern이 최상위 레벨에 나타나든 record pattern이 중첩되어 있든 관계없이 모든 type pattern에 나타날 수 있다.
예를 들어 다음 두 가지는 모두 유효하다.

  • r instanceof Point _
  • r instanceof ColoredPoint(Point(int x, int _), Color _)
    이름을 생략할 수 있는 unnamed pattern variable은 특히 switch 문과 표현식에 사용될 때 type pattern에 기반한 runtime 데이터 탐색을 시각적으로 더 명확하게 해 준다.

unnamed pattern variable은 switch가 여러 case에 대해 동일한 동작을 실행할 때 특히 유용하다.
예를 들어 앞의 Box와 Ball 코드를 다음과 같이 재작성할 수 있다.

switch (b) {
    case Box(RedBall _), Box(BlueBall _) -> processBox(b);
    case Box(GreenBall _)                -> stopProcessing();
    case Box(_)                          -> pickAnotherBox();
}

처음 두 사례는 오른쪽이 Box의 component를 사용하지 않기 때문에 unnamed pattern variable을 사용한다.
세 번째의 경우는 새로운 경우로, Box를 null component와 일치시키기 위해 unnamed pattern을 사용한다.

여러 pattern이 있는 case label에는 guard가 있을 수 있다.
guard는 개별 pattern이 아닌 case 전체를 관리한다.
예를 들어 정수 variable x가 있다고 가정하면 이전 예제의 첫 번째 case는 추가로 제약될 수 있다.

case Box(RedBall _), Box(BlueBall _) when x == 42 -> processBox(b);

각 pattern에 guard를 페어링 하는 것은 허용되지 않으므로 금지된다.

case Box(RedBall _) when x == 0, Box(BlueBall _) when x == 42 -> processBox(b);

unnamed pattern은 type pattern var _ 의 약어이다.
unnamed pattern이나 var _ 는 pattern의 최상위 수준에서 사용할 수 없으므로 다음은 모두 금지된다.

  • ... instanceof _
  • ... instanceof var _
  • case _
  • case var _

Unnamed variables

다음 종류의 선언은 named variable(식별자로 표시) 또는 unnamed variable (밑줄로 표시)를 도입할 수 있다.

  • A local variable declaration statement in a block (JLS 14.4.2),
  • A resource specification of a try-with-resources statement (JLS 14.20.3),
  • The header of a basic for statement (JLS 14.14.1),
  • The header of an enhanced for loop (JLS 14.14.2),
  • An exception parameter of a catch block (JLS 14.20), and
  • A formal parameter of a lambda expression (JLS 15.27.1).

(unnamed local variable이 pattern, 즉 pattern variable (JLS 14.30.1)에 의해 선언될 가능성은 위에서 다루었다.

unnamed variable을 선언하면 scope에 이름이 지정되지 않으므로 variable이 초기화된 후에는 variable을 쓰거나 읽을 수 없다.
위의 각 선언 유형에서 unnamed variable에 대해 initializer를 제공해야 한다.

unnamed variable은 이름이 없기 때문에 다른 variable에 그림자를 드리우지 않으므로 같은 block에 여러 개의 unnamed variable을 선언할 수 있다.

다음은 unnamed variable을 사용하도록 수정한 위의 예제이다.

  • 부작용이 있는 향상된 for loop
int acc = 0;
for (Order _ : orders) {
    if (acc < LIMIT) { 
        ... acc++ ...
    }
}

기본 for loop의 초기화는 unnamed local variable을 선언할 수도 있다.

for (int i = 0, _ = sideEffect(); i < 10; i++) { ... i ... }
  • 오른쪽에 있는 표현식의 결과가 필요하지 않은 할당문
Queue<Integer> q = ... // x1, y1, z1, x2, y2, z2, ...
while (q.size() >= 3) {
   var x = q.remove();
   var y = q.remove();
   var _ = q.remove();
   ... new Point(x, y) ...
}

프로그램이 x1, x2 등의 좌표만 처리해야 하는 경우 unnamed variable을 여러 할당문에 사용할 수 있다.

while (q.size() >= 3) {
    var x = q.remove();
    var _ = q.remove();
    var _ = q.remove(); 
    ... new Point(x, 0) ...
}
  • catch blockunnamed variable은 여러 catch block에서 사용할 수 있다.
  • try { ... } catch (Exception _) { ... } catch (Throwable _) { ... }
  • String s = ... try { int i = Integer.parseInt(s); ... i ... } catch (NumberFormatException _) { System.out.println("Bad number: " + s); }
  • try-with-resources
try (var _ = ScopedContext.acquire()) {
    ... no use of acquired resource ...
}
  • 매개변수와 무관한 lambda
...stream.collect(Collectors.toMap(String::toUpperCase, _ -> "NODATA"))

JEP 444: Virtual Threads

Java platform에 Virtual Thread를 도입한다.
Virtual Thread는 처리량이 높은 동시 application을 작성, 유지 관리 및 관찰하는 노력을 크게 줄여주는 경량 Thread이다.

JEP 425에서 preview로 제안되었으며 JDK 19에 제공되었다.
JEP 436에서 두 번째 preview로 제안되었으며 JDK 20에 제공되었다.

개인적으로 Reactive 프로그래밍을 그리 좋아하는 편이 아니어서 그에 대한 대체 방식으로 제안되는 Virtual Thread에 대해 이전 JDK 19나 JDK 20에서 깊게 살펴보지는 않았다.
Runnable과 비슷하게 작성하는 것으로 보였었는데 이후 JDK 21이 어느 정도 보편화 되면 비동기 프로그래밍에 변화를 가져올지 지켜보면 좋을 것 같다.
lambda와 함께 사용하면 코드 가독성을 크게 떨어트리지 않으면서 손쉽게 도입할 수 있게 되어 많이 쓰이지 않을까 싶다.

JEP 445: Unnamed Classes and Instance Main Methods (Preview)

학생들이 대규모 프로그램을 위해 설계된 언어 기능을 이해하지 않고도 첫 번째 프로그램을 작성할 수 있도록 Java 언어를 발전시킨다.
학생들은 별도의 Java 방언을 사용하는 않고도 단일 클래스 프로그램에 대한 간소화된 선언을 작성한 다음, 실력이 성장함에 따라 고급 기능을 사용하도록 프로그램을 원활하게 확장할 수 있다.

기존의 경우 Hello, World! 를 출력하기 위해 작성해야 하는 코드는 다음과 같았다.

public class HelloWorld { 
    public static void main(String[] args) { 
        System.out.println("Hello, World!");
    }
}

이를 클래스 선언과 main method 선언을 생략하고 다음처럼 작성할 수 있게 해 준다.

String greeting = "Hello, World!";
void main() {
    System.out.println(greeting);
}

초기 학습 진입점을 낮추기 위한 용도로 제공되는 기능이다.

JEP 446: Scoped Values (Preview)

method parameter를 사용하지 않고 method에 안전하고 효율적으로 공유할 수 있는 값인 scoped value를 도입한다.
특히 많은 수의 가상 thread를 사용할 때 thread-local variable보다 선호된다.

JEP 429에서 Incubator로 제안되었으며 JDK 20에 제공되었다.

반응형
profile

파란하늘의 지식창고

@Bluesky_

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