Java New Features

Java 9 부터 코딩 스타일 위주로 변경된 내용을 기술한다. Preview 는 제외했고 정식 기능으로 들어간 것 위주라 실질적으로 몇몇 버전이 빠져있다.

코딩 스타일 외에 가장 큰 변화는 Java 21 의 Virtual Thread 를 들 수 있다. 기존의 Java thread 가 OS 의 thread 와 1:1 매핑이었다면, Virtual thread 는 go 의 coroutine 처럼 software thread 라고 할 수 있다. 많은 경우 그렇지만 virtual thread 도 만능은 아니다. 즉, 기존 thread 에서 세분화해야 하는 경우만 도입이 고려되지 그렇지 않은 경우는 software 로 OS thread 매핑이 한 번 더 되는 꼴이라 딱히 성능 이득을 볼 수 없을 수 있다.

JDK 10

로컬 변수 타입 유추

아래와 같이 var 로 선언하고 변수에 타입을 유추하는 형태

var list = new ArrayList<String>(); // infers ArrayList<String>  
var stream = list.stream();         // infers Stream<String>

JDK 11

로컬 변수 Lamba

암묵적인 타입 선언으로 람다를 구현하는 경우

(var x, var y) -> x.process(y) 

동일 형태

(x, y) -> x.process(y)

섞어서는 못 쓴다.

(var x, y) -> x.process(y)         // Cannot mix 'var' and 'no var' in implicitly typed lambda expression  
(var x, int y) -> x.process(y)     // Cannot mix 'var' and manifest types in explicitly typed lambda expression

JDK 14

Switch 구문

축약해서 사용 가능

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

변수 대입도 가능

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

JDK 15

Text Block

""" 로 시작해서 """ 로 끝나는 블록 구문이다.

기존 구문이 이렇다면

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

이렇게 가능하다.

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

JDK 16

Pattern Matching for instanceof

기존에 이렇게 쓰던 것을

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

이렇게 사용할 수 있다.

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

근데 이게 Scope 가 묘한게 if 블럭 안에서만 유효하다. 예를 들어

class Example1 {  
    String s;  

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

심지어 else 구문에서도 이게 유효하지 않다.

class Example2 {  
    Point p;  

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

Records

데이터 구조체를 간단하게 구현한 것이다. 예를 들어 전통적인 방식의 데이터 구조체를 class 로 구현한 것이 다음과 같다면

class Point {  
    private final int x;  
    private final int y;  

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

    int x() { return x; }  
    int y() { return y; }  

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

    public int hashCode() {  
        return Objects.hash(x, y);  
    }  

    public String toString() {  
        return String.format("Point[x=%d, y=%d]", x, y);  
    }  
}

이 기능을 다 하는 record 는 다음과 같이 정의한다.

record Point(int x, int y) { }

단, 이렇게 보면 좋아보이지만 이 x, y 는 초기에 값을 대입받으면 더 이상 수정할 수 없다.

Constructor

Constructor 에서 추가 작업을 한다면 다음과 같이 사용할 수 있다.

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

동일 코드를 명시적으로 작성하면 다음과 같다.

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

Exception 처리 Case

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

Copy 는 다음과 같이 사용한다. 아래와 같은 record 가 있다고 하면

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

r1 인스턴스가 먼저 있고 이게 null 이 아니면

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

r2 가 r1의 복제이다. 이 경우 r1.equals(r2) 는 true 이다.

Rules for record classes

  • 상속 불가. record class 의 부모는 모두 java.lang.Record 이다. sealed class 를 implements 는 가능하다.
package com.example.expression;  

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

public record ConstantExpr(int i)       implements Expr {...}  
public record PlusExpr(Expr a, Expr b)  implements Expr {...}  
public record TimesExpr(Expr a, Expr b) implements Expr {...}  
public record NegExpr(Expr e)           implements Expr {...}
  • record class 는 암묵적으로 final 이고 abstract 가 되지 못한다.
  • 필드들도 모두 final 이다.
  • 인스턴스 필드를 명시적으로 정의할 수 없고 필드를 초기화할 수 없다.
record Invalid(int x, inty) {  
  int z;    // 인스턴스 필드 명시화 안됨.  
  int a = 1; // 명시화도 안되고 초기화도 안됨.  
}

JDK 17

Sealed Classes

Sealed class 나 interface 는 상속 받을 대상을 명시한다. 패키지 내부인 경우

package com.example.geometry;  

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

다른 패키지인 경우

package com.example.geometry;  

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

패턴 매칭에 사용 가능하다.

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

JDK 18

Code Snippets in Java API Documentation

@snippet 태그 사용

/**  
 * The following code shows how to use {@code Optional.isPresent}:  
 * {@snippet :  
 * if (v.isPresent()) {  
 *     System.out.println("v: " + v.get());  
 * }  
 * }  
 */

실제 코드를 당겨오는 것도 가능

/**  
 * The following code shows how to use {@code Optional.isPresent}:  
 * {@snippet file="ShowOptional.java" region="example"}  
 */

ShowOptional.java 의 내용에 region 을 정의한다.

public class ShowOptional {  
    void show(Optional<String> v) {  
        // @start region="example"  
        if (v.isPresent()) {  
            System.out.println("v: " + v.get());  
        }  
        // @end  
    }  
}

그 밖에 강조를 하거나 문자열 대치, 링크 등을 사용할 수 있다.

Deprecate Finalization for Removal

다음의 코드 대신에...

FileInputStream  input  = null;  
FileOutputStream output = null;  

try {  
    input  = new FileInputStream(file1);  
    output = new FileOutputStream(file2);  
    ... copy bytes from input to output   
    output.close();  output = null;  
    input.close();   input  = null;  
} finally {  
    if (output != null) output.close();  
    if (input  != null) input.close();  
}

try-resource-statement 와 cleaners 를 사용할 것.

try-resource-statement 는 Java 7 에 도입됨.

try (FileInputStream input = new FileInputStream(file1);  
     FileOutputStream output = new FileOutputStream(file2)) {  
    ... copy bytes from input to output   
}

cleaner 는 Java 9 에 도입됨. (interface 구하는 방식)

JDK 21

Sequenced Collections

first 와 last 에 대한 명시적인 method 를 제공한다.

Sequenced collections

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();  
}

Sequenced sets

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

Sequenced maps

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();  
}

Retrofitting

그래서 다음과 같이 재조정된다.

  • List now has SequencedCollection as its immediate superinterface,
  • Deque now has SequencedCollection as its immediate superinterface,
  • LinkedHashSet additionally implements SequencedSet,
  • SortedSet now has SequencedSet as its immediate superinterface,
  • LinkedHashMap additionally implements SequencedMap, and
  • SortedMap now has SequencedMap as its immediate superinterface.

Record Patterns

JDK 16 의 Pattern Matching for instanceof 에 Record 확장판

JDK 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);  
    }  
}

이걸 JDK 21에서는 다음과 같이 사용 가능하다.

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

이 때 Point(int x, inty) 를 record pattern 이라고 한다.

Nested record patterns

다단계인 경우 다음과 같이 사용 가능하다.

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

1단계

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

2단계

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

var 사용 예제

Rectangle r = new Rectangle(new ColoredPoint(new Point(x1, y1), c1),  
                            new ColoredPoint(new Point(x2, y2), c2));  

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);  
    }  
}

Pattern Matching for switch

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;  
}

이랬던 것을 아래와 같이 사용 가능하다.

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();  
    };  
}

switch 에 null

before

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");  
    }  
}

after

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

좀 더 나가서 (아래도 JDK 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?");  
        }  
    }  
}

여러 가지로 가능하다.

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?");  
        }  
    }  
}

좀 더 늘려서...

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?");  
        }  
    }  
}

enum 쓰는 경우

before

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");  
    }  
}

after (sealed 도 같이 사용)

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");  
        }  
    }  
}

이렇게도 가능하다. (이게 좀 더 명시적인 듯...)

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");  
        }  
    }  
} 

JDK 22

Foreign Function & Memory API

https://openjdk.org/jeps/454

이건 Coding Style 보다는 기능의 대체이다. JNI 를 대체하기 위해 나왔고 조금 복잡해 보이긴 한데, JNI 도 복잡했으니 비슷하지 않을까 싶다.

목적

Java runtime 바깥의 코드와 데이터에 대한 접근을 위한 것인데 JNI 의 단점을 보완하려고 한다.

  • JNI의 native 메소드를 pure-java API 로 대체한다.
  • JNI 보다는 오버헤드가 있을 듯 함.
  • JVM 이 실행되는 모든 플랫폼의 native library 의 탐색 및 수행
  • 다양한 (native, persistent, managed heap) 메모리 접근에 대한 방법을 제공함
  • thread 간의 메모리 할당과 해지 시에도 쓰지 않는 메모리에 대한 해지 버그에 대해 보장
  • native code 와 data 에 대한 안전하지 않은 (unsafe) 동작에 대해 허용하되 이에 대해 사용자에게 경고하는 기능

Foreign memory (before)

객체들은 heap memory 에 만들어지고 사용이 끝나면 GC 로 사라진다. heap memory 말고 다른 곳에 만들어지는 off-heap memory 가 필요한 경우를 지원. 몰랐는데 이전에도 off-heap 메모리를 사용했다고...

  • ByteBuffer API 에서 시스템의 native I/O 에서 직접 access 할 수 있도록 하기 위해서 allocateDirect 로 할당 시 off-heap memory 로 관리. 주로 Non-block 이나 Async I/O 구현 하는 netty 같은 곳에서 사용.
  • sun.misc.Unsafe API 를 사용하는 경우 -> 잘 안 썼던 것 같다.

Foreign functions (before)

JNI 로 지원

그래서

FFM API 는 다음의 클래스와 인터페이스를 추가함.

  • foreign memory 에 대한 allocation 과 deallocation 제어
    • MemorySegment
    • Arena
    • SegmentAllocator
  • 구조화된 foreign memory 접근 및 조작
    • MemoryLayout
    • VarHandle
  • foreign functions 호출
    • Linker
    • SymbolLockup
    • FunctionDescriptor
    • MethodHandle

Unnamed Variables & Patterns

필요에 의해 정의는 하지만 실제로 쓰이지 않는 경우를 대비해서 만들어진 기능이다.

for loop 를 위해 정의는 하지만 실제로 필요하지 않은 경우

static int count(Iterable<Order> orders) {
    int total = 0;
    for (Order order : orders)    // order is unused
        total++;
    return total;
}

다음과 같이 사용할 수 있다.

static int count(Iterable<Order> orders) {
    int total = 0;
    for (Order _ : orders)    // Unnamed variable
        total++;
    return total;
}

아래의 경우에도 z 대신에 _ 를 사용할 수 있다.

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) ...
}

try catch 구문에서 catch 한 Exception 을 만나서 실제로 쓰지 않는 경우에도 Exception 변수로 _ 사용 가능.

패턴의 경우에도 유사.

sealed abstract class Ball permits RedBall, BlueBall, GreenBall { }
final  class RedBall   extends Ball { }
final  class BlueBall  extends Ball { }
final  class GreenBall extends Ball { }

Ball ball = ...
switch (ball) {
    case RedBall   red   -> process(ball);
    case BlueBall  blue  -> process(ball);
    case GreenBall green -> stopProcessing();
}

위와 같이 사용하는 경우 red, blue, greean 은 쓰이지 않으므로 다음과 같이 변경할 수 있다.

switch (ball) {
    case RedBall _   -> process(ball);          // Unnamed pattern variable
    case BlueBall _  -> process(ball);          // Unnamed pattern variable
    case GreenBall _ -> stopProcessing();       // Unnamed pattern variable
}
Tip

[!Tip] Java 파일 수행 JDK 22 에 Launch Multi-File Source-Code Programs 라는 항목이 있다. 그리고 JDK 11 에서 java Prog.java 형태로 소스코드 파일을 바로 실행할 수 있었다고... 이제는 멀티가 가능하다고 한다. 자세한 내용은 https://openjdk.org/jeps/458 참고.

JDK 23

Markdown Documentation Comments

기존에 html 태그를 넣어서 API 문서 작성하던 것을 Markdown 도 쓸 수 있게 함.

/**
 * Returns a hash code value for the object. This method is
 * supported for the benefit of hash tables such as those provided by
 * {@link java.util.HashMap}.
 * <p>
 * The general contract of {@code hashCode} is:
 * <ul>
 * <li>Whenever it is invoked on the same object more than once during
 *     an execution of a Java application, the {@code hashCode} method
 *     must consistently return the same integer, provided no information
 *     used in {@code equals} comparisons on the object is modified.
 *     This integer need not remain consistent from one execution of an
 *     application to another execution of the same application.
 * <li>If two objects are equal according to the {@link
 *     #equals(Object) equals} method, then calling the {@code
 *     hashCode} method on each of the two objects must produce the
 *     same integer result.
 * <li>It is <em>not</em> required that if two objects are unequal
 *     according to the {@link #equals(Object) equals} method, then
 *     calling the {@code hashCode} method on each of the two objects
 *     must produce distinct integer results.  However, the programmer
 *     should be aware that producing distinct integer results for
 *     unequal objects may improve the performance of hash tables.
 * </ul>
 *
 * @implSpec
 * As far as is reasonably practical, the {@code hashCode} method defined
 * by class {@code Object} returns distinct integers for distinct objects.
 *
 * @return  a hash code value for this object.
 * @see     java.lang.Object#equals(java.lang.Object)
 * @see     java.lang.System#identityHashCode
 */

Markdown 은 /// 로 구분한다.

/// Returns a hash code value for the object. This method is
/// supported for the benefit of hash tables such as those provided by
/// [java.util.HashMap].
///
/// The general contract of `hashCode` is:
///
///   - Whenever it is invoked on the same object more than once during
///     an execution of a Java application, the `hashCode` method
///     must consistently return the same integer, provided no information
///     used in `equals` comparisons on the object is modified.
///     This integer need not remain consistent from one execution of an
///     application to another execution of the same application.
///   - If two objects are equal according to the
///     [equals][#equals(Object)] method, then calling the
///     `hashCode` method on each of the two objects must produce the
///     same integer result.
///   - It is _not_ required that if two objects are unequal
///     according to the [equals][#equals(Object)] method, then
///     calling the `hashCode` method on each of the two objects
///     must produce distinct integer results.  However, the programmer
///     should be aware that producing distinct integer results for
///     unequal objects may improve the performance of hash tables.
///
/// @implSpec
/// As far as is reasonably practical, the `hashCode` method defined
/// by class `Object` returns distinct integers for distinct objects.
///
/// @return  a hash code value for this object.
/// @see     java.lang.Object#equals(java.lang.Object)
/// @see     java.lang.System#identityHashCode

table 가능하고 JavaDoc 태그는 그냥 써야 하는 듯 하다. 코드 블럭도 가능하다.

JDK 24

대부분 성능 관련 개선이나 API 개선에 대한 내용들이다.

JDK 25

Scoped Values

Virtual Thread 를 지원하기 위해 Thread 내에서 값을 공유할 수 있다.

public class Framework {

    private final Application application;

    public Framework(Application app) { this.application = app; }

    private static final ThreadLocal<FrameworkContext> CONTEXT 
                       = new ThreadLocal<>();    // (1)

    void serve(Request request, Response response) {
        var context = createContext(request);
        CONTEXT.set(context);                    // (2)
        Application.handle(request, response);
    }

    public PersistedObject readKey(String key) {
        var context = CONTEXT.get();              // (3)
        var db = getDBConnection(context);
        db.readKey(key);
    }

}

Thread 간의 value 공유도 가능한 듯 한데, 이 부분은 structured concurrency(현재는 preview) 에서 가능한 듯 하다.

Key Derivation Function API

HKDF(RFC 5869)나 Argon2 (RFC 9106) 같은 키 도출 함수 지원.

// Create a KDF object for the specified algorithm
KDF hkdf = KDF.getInstance("HKDF-SHA256"); 

// Create an ExtractExpand parameter specification
AlgorithmParameterSpec params =
    HKDFParameterSpec.ofExtract()
                     .addIKM(initialKeyMaterial)
                     .addSalt(salt).thenExpand(info, 32);

// Derive a 32-byte AES key
SecretKey key = hkdf.deriveKey("AES", params);

// Additional deriveKey calls can be made with the same KDF object

Module Import Declarations

import module 문으로 다양한 패키지를 모듈 단위로 import 한다. 예를 들어 아래와 같은 예는

import javax.xml.*;
import javax.xml.parsers.*;
import javax.xml.stream.*;

다음과 같이 바꿀 수 있다. 이는 계층 구조 전체를 가져올 수 있다는 뜻.

import module java.xml;

Compact Source Files and Instance Main Methods

이 소스코드를...

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

이렇게 줄일 수 있다고...

void main() {
    IO.println("Hello, World!");
}
PS C:\Users\bruckner\Workspace\T2X> cat .\HelloWorld.java
void main() {
    IO.println("Hello, World!");
}
PS C:\Users\bruckner\Workspace\T2X> java .\HelloWorld.java
Hello, World!
PS C:\Users\bruckner\Workspace\T2X> java -version
openjdk version "25.0.1" 2025-10-21 LTS
OpenJDK Runtime Environment Temurin-25.0.1+8 (build 25.0.1+8-LTS)
OpenJDK 64-Bit Server VM Temurin-25.0.1+8 (build 25.0.1+8-LTS, mixed mode, sharing)

되네요.

Flexible Constructor Bodies

JDK 25 중 가장 중요한 변화. 상속 받은 Class 생성 시에 Super Class 먼저 생성하는 방식에 대한 변경이다.

이전에 이런 방식으로 밖에 짤 수 없었다.

class Object {
    Object() {
        // Object constructor body
    }
}

class A extends Object {
    A() {
        super();
        // A constructor body
    }
}

class B extends A {
    B() {
        super();
        // B constructor body
    }
}

class C extends B {
    C() {
        super();
        // C constructor body
    }
}

class D extends C {
    D() {
        super();
        // D constructor body
    }
}

호출하면 다음과 같이 된다.

D
--> C
    --> B
        --> A
            --> Object constructor body
        --> A constructor body
    --> B constructor body
--> C constructor body
D constructor body

변경된 방식은 다음과 같다.

class Object {
    Object() {
        // Object constructor body
    }
}

class A extends Object {
    A() {
        // A prologue
        super();
        // A epilogue
    }
}

class B extends A {
    B() {
        // B prologue
        super();
        // B epilogue
    }
}

class C extends B {
    C() {
        // C prologue
        super();
        // C epilogue
    }
}

class D extends C {
    D() {
        // D prologue
        super();
        // D epilogue
    }
}

호출 순서는 다음과 같다.

D prologue
--> C prologue
    --> B prologue
        --> A prologue
            --> Object constructor body
        --> A epilogue
    --> B epilogue
--> C epilogue
D epilogue

생각해보면 쉬운 일 같은데 지금까지 안해준 것도 신기하고... 물론 제약 사항도 있는데 super 를 호출하기 전 까지는 인스턴스화가 완전하게 된 것이 아니어서 다음과 같이 오류가 날 수 있다.

class X {

    int i;
    String s = "hello";

    X() {

        System.out.print(this);  // Error - explicitly 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 y = i;               // Error - implicitly refers to field of the current instance
        hashCode();              // Error - implicitly refers to method of the current instance

        i = 42;                  // OK - assignment to an uninitialized declared field

        s = "goodbye";           // Error - assignment to an initialized declared field

        super();

    }

}

Garbage Collector 중간 정리

종류 Option 동작 방식 장점 단점
Serial -XX:+UseSerialGC 싱글 스레드로 동작 CPU 자원이 적을 때 효과적 멀티 스레드에서 쥐약. GC 동작 시 얼음 현상
Parallel -XX:+UseParallelGC 멀티 스레드로 동작 가장 많은 Garbage 정리는 가능 GC 동작 시 얼음 현상
G1(Garbage First) -XX:+UseG1GC 파티션 구조 낮은 지연 시간 메모리 오버헤드
Shenandoah -XX:+UseShenandoahGC 파티션 구조, 잦은 GC 수행 Z 대비 처리 시간이 김 Z 대비 Garbage 처리량이 높음
Z +XX:+UseZGC
+XX:+UseGenerationalZGC
동적 파티션 구조 셰넌도 대비 처리 시간이 짧음 오리지널은 셰넌도 대비 처리량이 낮았으나 Z Gen 에서 극복

일단 SerialGC 가 먼저 나왔고 ParallelGC 는 JDK 8 에서 default 가 되었다. 이외에 CMS GC 라는 것도 있었는데 JDK 9 이후 deprecated 되고 JDK 14 에서 제거되었다. 이후 G1GC 가 JDK7 에 도입되고 JDK9에 default 가 된다. ZGC 는 JDK 15 에, GenerationalZGC 는 JDK 21 부터 지원한다. Shenandoah GC 는 JDK 17 부터 지원한다. (지원의 의미는 안정화.) Generational Shenandoah 는 experimental 로 JDK25 부터 도입. (안정화는 기다려야 할 듯)

G1이 JDK 9 이후 현재 Default 이며 (LTS 로 보면 11, 17, 21, 25) G1 보다 저지연 및 메모리 오버헤드를 줄인 GC 로 만들고 있는 것이 Z 하고 Shenandoah 라고 보면 되는데 전반적으로 Z 가 조금 앞서는 느낌이다.