파란하늘의 지식창고
Published 2024. 3. 21. 17:51
JDK 22 New Features Study/Java
반응형

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

Spec

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

Final Release Specification Feature Summary

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

JEP Component Feature
JEP 423 hotspot / gc Region Pinning for G1
JEP 447 specification / language Statements before super(...) (Preview)
JEP 454   Foreign Function & Memory API
JEP 456   Unnamed Variables & Patterns
JEP 457   Class-File API (Preview)
JEP 458   Launch Multi-File Source Programs
JEP 459   String Templates (Second Preview)
JEP 460   Vector API (Seventh Incubator)
JEP 461   Stream Gatherers (Preview)
JEP 462   Structured Concurrency (Second Preview)
JEP 463   Implicity Declared Classes and Instance Main Methods (Second Preview)
JEP 464   Scoped Values (Second Preview)

JEP 447: Statements before super(...) (Preview)

Summary

Java Programming language의 constructor에서 생성 중인 instance를 참조하지 않는 statement가 명시적인 constructor 호출 전에 표시되도록 허용한다.

Goals

  • 개발자에게 constructor의 동작을 더 자유롭게 표현할 수 있도록 하여 현재 보조 static method, 보조 중간 constructor 또는 constructor argument에 고려해야 하는 로직을 보다 자연스럽게 배치할 수 있다.
  • class instance화 중에 constructor가 top-down 순서로 실행되는 기존의 보장을 유지하여 subclass constructor의 코드가 superclass instance화를 방해할 수 없도록 한다.
  • java virtual machine을 변경할 필요가 없다.
    이 java language feature는 constructor 내에서 명시적 constructor 호출 전에 나타나는 코드를 검증하고 실행하는 JVM의 현재 기능에만 의존한다.

Motivation

한 class가 다른 class를 확장하면 subclass는 superclass의 기능을 상속하고 자체 field와 method를 선언하여 기능을 추가할 수 있다.
subclass에 선언된 field의 초기 값은 superclass에 선언된 field의 초기 값에 따라 달라질 수 있으므로 subclass의 field보다 먼저 superclass의 field를 초기화하는 것이 중요하다.
예를 들어, class B가 class A를 extends 하는 경우 표시되지 않는 class Object의 field를 먼저 초기화한 다음 class A의 field, class B의 field를 차례로 초기화해야 한다.

이 순서대로 field를 초기화한다는 것은 constructor가 top down으로 실행되어야 함을 의미한다.
superclass의 constructor는 subclass의 constructor가 실행되기 전에 해당 class에서 선언된 field 초기화를 완료해야 한다.

class의 field가 초기화되기 전에 access 하지 않도록 하는 것도 중요하다.
초기화되지 않은 field에 대한 access를 방지하는 것은 constructor가 제한되어야 함을 의미한다.
즉 constructor의 body는 superclass의 constructor가 완료될 때까지 자체 class나 superclass에 선언된 field에 access 해서는 안된다.

constructor가 top down으로 실행되도록 보장하기 위해 java language는 constructor body에서 다른 constructor에 대한 명시적 호출이 첫 번째 statement로 표시되어야 하며, 명시적 constructor 호출이 제공되지 않으면 compiler가 constructor를 주입하도록 요구한다.

constructor가 초기화되지 않은 field에 access 하지 않도록 보장하기 위해 java language에서는 명시적인 constructor 호출이 제공되면 해당 argument 중 어떤 것도 현재 object인 this에 어떤 방식으로든 access 할 수 없도록 요구한다.

이러한 요구사항은 top down 동작과 초기화 전 access 금지를 보장하지만 일반 method에서 사용되는 여러 관용어를 constructor에서 사용하기 어렵거나 불가능하게 만들기 때문에 가혹하다.
다음 예에서 문제를 보여준다.

Example: Validating superclass constructor arguments

때때로 우리는 superclass constructor에 전달된 argument의 유효성을 검사해야 한다.
이후에 검증할 수 있지만 이는 잠재적으로 불필요한 작업을 수행한다는 의미이다.

public class PositiveBigInteger extends BigInteger {

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

}

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

public class PositiveBigInteger extends BigInteger {

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

    private static long verifyPositive(long value) {
        if (value <= 0)
            throw new IllegalArgumentException("non-positive value");
        return value;
    }

}

이 코드는 constructor의 유효성 검사 논리를 직접 포함할 수 있다면 더 읽기 쉽다.
작성하길 원하는 코드는 다음과 같다.

public class PositiveBigInteger extends BigInteger {

    public PositiveBigInteger(long value) {
        if (value <= 0)
            throw new IllegalArgumentException("non-positive value");
        super(value);
    }

}

Example: Preparing superclass constructor arguments

때때로 superclass constructor에 대한 argument를 준비하기 위해 보조 method에 의존하여 중요한 계산을 수행해야 한다.

public class Sub extends Super {

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

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

}

superclass constructor는 byte array argument를 사용하지만 subclass constructor는 Certificate argument를 사용한다.
superclass constructor 호출이 subclass constructor의 첫 번째 statement여야 한다는 제한사항을 충족하기 위해 해당 호출에 대한 argument를 준비하는 보조 method prepareByteArray를 선언하였다.

argument 준비 코드를 constructor에 직접 포함할 수 있다면 이 코드를 더 쉽게 읽을 수 있다.
작성하길 원하는 코드는 다음과 같다.

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

Example: Sharing superclass constructor arguments

때로는 값을 계산하고 이를 superclass constructor 호출의 argument 간에 공유해야 하는 경우도 있다.
constructor 호출이 먼저 나타나야 한다는 요구 사항은 이러한 공유를 달성하는 유일한 방법이 중간 보조 constructor를 통하는 것임을 의미한다.

public class Super {

    public Super(F f1, F f2) {
        ...
    }

}

public class Sub extends Super {

    // Auxiliary constructor
    private Sub(int i, F f) { 
        super(f, f);                // f is shared here
        ... i ...
    }

    public Sub(int i) {
        this(i, new F());
    }

}

public Sub constructor에서 class F의 새 instance를 만들고 해당 instance에 대한 2개의 참조를 superclass constructor에 전달하려고 한다.
보조 전용 constructor를 선언하여 이를 수행한다.

작성하길 원하는 코드에서는 constructor에서 직접 copy를 수행하므로 보조 constructor가 필요 없다.

public Sub(int i) {
        var f = new F();
        super(f, f);
        ... i ...
    }

Summary

이 모든 예제에서 작성하길 원하는 constructor 코드에는 명시적인 constructor 호출 이전에 statement 가 포함되어 있지만 superclass constructor가 완료되기 전에는 이를 통해 어떤 field도 access 하지 않는다.
현재 이러한 constructor는 모두 안전함에도 불구하고 compiler에서 거부된다.
constructor를 top down으로 실행하는데 협력하고 초기화되지 않는 field에 access 하지 않는다.

Java language가 보다 유연한 규칙을 통해 top down 구성과 초기화 전 access 금지를 보장할 수 있다면 코드 작성과 유지 관리가 더 쉬워질 것이다.
constructor는 서투른 보조 method나 constructor를 통해 작업을 수행하지 않고도 argument validation, argument preparation 및 argument sharing을 보다 자연스럽게 수행할 수 있다.
Java 1.0부터 시행된 단순한 구문 요구 사항, 즉 "super(...) 또는 this(...)가 첫 번째 statement여야 한다", "this를 사용하지 않음" 등을 넘어서야 한다.

JEP 456: Unnamed Variables & Patterns

unnamed variables & unnamed patterns는 JEP 443을 통해 JDK 21에서 preview로 소개되었다.
기존 preview에 기능 변경 없이 JEP 456으로 마무리되었다.

따라서 기존 JEP 443의 내용을 참고하면 된다.

2023.10.04 - [Study/Java] - JDK 21 New Features

JEP 459: String Templates (Second Preview)

String Template은 JDK 21의 JEP 430에서 preview feature로 제안되었었다.
추가적인 경험과 피드백을 얻기 위해 두 번째 preview가 제안되었으며 변경된 부분은 없다.

따라서 기존 JEP 430의 내용을 참고하면 된다.

2023.10.04 - [Study/Java] - JDK 21 New Features

JEP 461: Stream Gatherers (Preview)

Summary

custom 중간 작업을 지원하도록 Stream API 를 향상한다.
이를 통해 Stream pipeline은 기존 내장 중간 작업으로는 쉽게 달성할 수 없는 방식으로 데이터를 변환할 수 있다.
preview API 이다.

Goals

  • Stream pipeline을 더욱 유연하고 표현력 있게 만든다.
  • 가능한 한 사용자 지정 중간 작업을 통해 무한 크기의 stream을 조작할 수 있다.

Non-Goals

  • stream 처리를 더 용이하게 하기 위해 Java Programming language를 변경하는 것이 목표는 아니다.
  • Stream API를 사용하는 code compile을 특수한 경우로 만드는 것은 목표가 아니다.

Motivation

Java 8에서는 lambda expression을 위해 특별히 설계된 최초의 API인 Stram API인 java.util.stream 을 도입하였다.
stream은 lazy 하게 연산하고 잠재적으로 제한되지 않는 value sequence이다.
API는 stream을 순차적으로 또는 병렬로 처리하는 기능을 지원한다.

stream pipeline은 element의 source, 중간 작업(intermediate operation), 끝단 작업(terminal operation) 세 부분으로 구성된다.
예를 들면:

long numberOfWords =
    Stream.of("the", "", "fox", "jumps", "over", "the", "", "dog")  // (1)
          .filter(Predicate.not(String::isEmpty))                   // (2)
          .collect(Collectors.counting());                          // (3)

이 programming style은 표현력이 풍부하고 효율적이다.
builder style API를 사용하면 각 중간 작업이 새 stream을 반환한다.
연산은 terminal operation이 호출될 때만 시작된다.
이 예제에서 line (1)은 stream을 생성하지만 연산하지 않고, line (2)는 중간 filter 작업을 설정하지만 여전히 stream을 연산하지 않으며, 마지막으로 line (3)의 terminal collection operation은 전체 stream pipeline을 연산한다.

Stream API는 mapping, filtering, reduction, sorting 등 고정적이기는 하지만 상당히 풍부한 중간 및 끝단 작업 set을 제공한다.
또한 확장가능한 terminal 연산인 Stream::collect도 포함되어 있어 pipeline의 출력을 다양한 방식으로 요약할 수 있다.

Java ecosystem에서 stream의 사용은 널리 퍼져있으며 많은 작업에 이상적이다.
그러나 고정된 중간 작업(intermediate operation) set은 일부 복잡한 작업을 stream pipeline으로 쉽게 표현할 수 없음을 의미한다.
필요한 중간 작업이 없거나 존재하지만 작업을 직접 지원하지 않는다.

예를 들어, 작업이 문자열 stream을 가져와 distinct 하지만 내용보다는 string length에 기반으로 distinct 한다고 가정해 보자.
즉, 길이가 1인 문자열은 최대 1개, 길이가 2인 문자열은 최대 1개, 길이가 3인 문자열은 최대 1개 등으로 내보내야 한다.
이상적으로 코드는 다음과 같다.

var result = Stream.of("foo", "bar", "baz", "quux")
                   .distinctBy(String::length)      // Hypothetical
                   .toList();

// result ==> [foo, quux]

블행하게도, distinctBy는 내장된 중간 작업이 아니다.
가장 가까운 내장 연산인 distinct는 객체 동등성을 사용하여 비교함으로써 이미 본 element를 추적한다.
즉, distinct는 stateful이지만 이 경우 잘못된 state를 사용한다.
문자열 내용이 아닌 문자열 길이의 동일성을 기준으로 element를 추적하기를 원한다.
문자열 길이 측면에서 객체 동등성을 정의하는 class를 선언하고, 각 문자열을 해당 class의 instance로 wrapping 하고 해당 instance에 구별을 적용하여 이 제한을 해결할 수 있다.
그러나 이러한 작업 표현은 직관적이지 않으며 유지 관리가 어려운 코드를 만든다.

record DistinctByLength(String str) {

    @Override public boolean equals(Object obj) {
        return obj instanceof DistinctByLength(String other)
               && str.length() == other.length();
    }

    @Override public int hashCode() {
        return str == null ? 0 : Integer.hashCode(str.length());
    }

}

var result = Stream.of("foo", "bar", "baz", "quux")
                   .map(DistinctByLength::new)
                   .distinct()
                   .map(DistinctByLength::str)
                   .toList();

// result ==> [foo, quux]

또 다른 예로, 작업이 element를 3개의 고정 크기 그룹으로 그룹화하고 처음 두 그룹만 유지하는 것이라고 가정한다.
[0, 1, 2, 3, 4, 5, 6, ...][[0, 1, 2], [3, 4, 5]] 을 생성해야 한다.
이상적으로 코드는 다음과 같다.

var result = Stream.iterate(0, i -> i + 1)
                   .windowFixed(3)                  // Hypothetical
                   .limit(2)
                   .toList();

// result ==> [[0, 1, 2], [3, 4, 5]]

불행하게도 내장된 중간 작업(intermediate operation)은 이 작업을 지원하지 않는다.
가장 좋은 옵션은 custom collector로 collect를 호출하여 fixed-window grouping logic을 끝단 작업(terminal operaion)에 배치하는 것이다.
그러나 Collector 는 무한한 stream에서 영원히 발생하는 새로운 element가 나타나는 동안 collector가 완료되었음을 수집하도록 신호를 보낼 수 없기 때문에 fixed-size limit operation을 수행해야 한다.
또한 작업은 본질적으로 순서가 지정된 데이터에 관한 것이므로 collector가 병렬로 grouping을 수행하도록 하는 것은 불가능하며 combiner가 호출되면 예외를 던져 이 사실을 표시해야 한다.
result 코드는 이해하기 어렵다.

var result
    = Stream.iterate(0, i -> i + 1)
            .limit(3 * 2)
            .collect(Collector.of(
                () -> new ArrayList<ArrayList<Integer>>(),
                (groups, element) -> {
                    if (groups.isEmpty() || groups.getLast().size() == 3) {
                         var current = new ArrayList<Integer>();
                         current.add(element);
                         groups.addLast(current);
                     } else {
                         groups.getLast().add(element);
                     }
                },
                (left, right) -> {
                    throw new UnsupportedOperationException("Cannot be parallelized");
                }
            ));

// result ==> [[0, 1, 2], [3, 4, 5]]

수년에 걸쳐 Stream API에 대한 많은 새로운 중간 작업(intermediate operation)이 제안되었었다.
이들 중 대부분은 개별적으로 고려할 때 의미가 있지만, 이를 모두 추가하면 (이미 큰) Stream API 작업을 검색하기가 어렵기 때문에 배우기거 더 어려워진다.

Stream API의 설계자는 누구나 중간 stream 작업을 정의할 수 있도록 확장 지점을 갖는 것이 바람직하다는 것을 이해했다.
그러나 당시 그들은 확장 지점(extension point )이 어떤 모습이어야 하는지 몰랐다.
결국 끝단 작업 (terminal operation)을 위한 확장 지점, 즉 Stream::collect(Collector)가 효과적이라는 것이 분명해졌다.
이제 중간 작업에 대해서도 비슷한 접근 방식을 취할 수 있다.

요약하자면, 중간 작업이 많을수록 상황에 맞는 가치가 더 많이 생성되므로 stream이 더 많은 작업에 더 적합해진다.
우리는 개발자가 원하는 방식으로 유한 및 무한 stream을 변환할 수 있도록 custom 중간 작업을 위한 API를 제공해야 한다.

Description

Stream::gather(Gatherer) 는 Gatherer라는 user-defined entity를 적용하여 stream의 element를 처리하는 새로운 중간 stream 작업이다.
수집 작업을 통해 거의 모든 중간 작업을 구현하는 효율적인 병렬 지원 stream을 구축할 수 있다.
Stream::collect(Collector)가 끝단 작업(terminal operation)에서 수행하는 것이라면 Stream::gather(Gatherer)는 중간 작업(intermediate operation)에서 수행하는 것이다.

Gatherer는 stream element의 변환을 나타낸다.
이는 java.util.stream.Gatherer interface의 instance이다.
Gatherer는 1:1, 1:N, N:1 또는 N:N 방식으로 element를 변환할 수 있다.
이후 element의 변환에 영향을 주기 위해 이전에 본 element를 추적할 수 있고, 무한 stream을 유한 stream으로 변환하기 위해 중단(short-circuit) 할 수 있으며, 병렬 실행을 가능하게 할 수 있다.
예를 들어, gatherer는 일부 조건이 true가 될 때까지 하나의 input element를 하나의 output element로 변환할 수 있으며, 이 시점에서 하나의 input element를 두 개의 output element로 변환하기 시작한다.

Gahterer는 함께 작동하는 4가지 기능으로 정의된다.

  • optional initialzer function은 stream element를 처리하는 동안 private state를 유지하는 object를 제공한다.
    예를 들어, Gatherer는 현재 element를 저장하여 다음에 적용할 때 새 element를 이전 element와 비교하여 둘 중 더 큰 element만 내보낼 수 있다.
    실제로 이러한 gatherer는 두 개의 input element를 하나의 output element로 변환한다.
  • integrator function은 input stream의 새 element를 통합하여 private state object를 검사하고 element를 output stream으로 내보낼 수도 있다.
    또한 input stream의 끝에 도달하기 전에 처리를 종료할 수도 있다.
    예를 들어 integer stream 중 가장 큰 것을 검색하는 gatherer는 Integer.MAX_VALUE를 감지하면 종료될 수 있다.
  • input stream이 병렬로 표시되면 optional combiner function을 사용하여 gatherer를 병렬로 연산할 수 있다.
    gatherer가 병렬을 지원하지 않는 경우에도 여전히 병렬 stream pipeline의 일부일 수 있지만 순차적으로 평가된다.
    이는 작업이 본질적으로 순서가 지정되어 있어 병렬화 할 수 없는 경우에 유용하다.
  • 소비할 input element가 더 이상 없을 때 optional finisher function이 호출된다.
    이 function은 private state object를 검사하고 가능하면 추가 output element를 내보낼 수 있다.
    예를 들어, input element 중에서 특정 element를 검색하는 gatherer는 finisher가 호출될 때 exception을 발생시켜 실패를 report 할 수 있다.

Stream::gather가 호출되면 다음 단계와 동일한 작업을 수행한다.

  • gatherer의 output type element가 제공되면 이를 pipeline의 다음 단계로 전달하는 Downstream object를 만든다.
  • initializer 의 get() method를 호출하여 gatherer의 private state object를 가져온다.
  • integrator() method를 호출하여 gatherer의 integrator 를 가져온다.
  • 더 많은 input element가 있는 동안 integrate(...) method를 호출하여 state object, next element, 및 downstream object를 전달한다.
    해당 method가 false를 반환하면 종료된다.
  • gatherer의 finisher 를 획득하고 state 및 downstream object와 함께 호출한다.

Stream interface에 선언된 모든 기존 중간 작업은 해당 작업을 구현하는 gatherer로 gather를 호출하여 구현할 수 있다.
예를 들어 T type element의 stream이 있는 경우 Stream::map은 function을 적용하여 각 T element를 U element로 변환한 다음 U element를 downstream으로 전달한다.
이는 단순히 stateless 1:1 gatherer이다.
또 다른 예로 Stream::filter는 input element를 downstream으로 전달해야 하는지 여부를 결정하는 predicate를 사용한다.
이는 단순히 stateless 1:N gatherer이다.
실제로 모든 stream pipeline은 개념적으로 다음과 동일하다.

source.gather(...).gather(...).gather(...).collect(...)

Built-in gatherers

java.util.stream.Gatherers class에 내장된 다음과 같은 gatherer를 소개한다.

  • fold 는 집계를 점진적으로 구성하고 더 이상 input element 가 없을 때 해당 집계를 내보내는 stateful N:1 gatherer이다.
  • mapConcurrent 는 제공된 한도까지 각 input element에 대해 제공된 함수를 동시에 호출하는 stateful 1:1 gatherer이다.
  • scan 은 제공된 함수를 현재 state와 현재 element에 적용하여 downstream에 전달되는 다음 요소를 생성하는 stateful 1:1 gatherer이다.
  • windowFixed 는 input element를 제공된 크기의 list로 그룹화하고 window가 가득 차면 downstream으로 내보내는 stateful N:N gatherer이다.
  • windowSliding 은 input element를 제공된 크기의 list로 그룹화하는 stateful N:N gatherer이다.
    첫 번째 window 이후에 각 후속 window는 첫 번째 element를 삭제하고 input stream의 다음 element를 추가하여 이전 window의 복사본에서 생성된다.

Parallel evaluation

Gatherer의 parallel evaluation은 두 가지 mode로 나뉜다.
combiner가 제공되지 않는 경우에도 stream library는 parallel().forEachOrdered() operation과 유사하게 upstream 및 downstream 작업을 병렬로 실행하여 병렬성(parallelism)을 추출할 수 있다.
combiner가 제공되면 parallel evaluation은 parallel().reduce() operation과 유사하다.

Composing gatherers

Gatherer는 두 개의 gatherer를 연결하여 첫 번째 Gatherer가 소비할 수 있는 element를 제공하면 두 번째 gatherer가 이를 소비하는 andThen(Gatherer) method를 통해 composition을 지원한다.
이를 통해 function composition 과 마찬가지로 간단한 gatherer를 구성하여 정교한 gatherer를 만들 수 있다.

source.gather(a).gather(b).gather(c).collect(...)

위는 다음과 같다.

source.gather(a.andThen(b).andThen(c)).collect(...)

Gatherers vs. collectors

Gatherer interface의 디자인은 Collector의 디자인에 크게 영향을 받았다.
주요 차이점은 다음과 같다.

  • Gatherer는 Downstream object에 대한 추가 input element가 필요하고 처리를 계속해야 하는지 여부를 나타내는 boolean을 반환해야 하기 때문에 element 별 처리에 BiConsumer 대신 Integrator 를 사용한다.
  • Gatherer는 Downstream object에 대한 추가 input parameter가 필요하고 결과를 반환할 수 없어 무효이기 때문에 Function 대신 finisher로 BiConsumer 를 사용한다.

Example: Embracing the stream

때때로 적절한 중간 작업이 없기 대문에 stream을 list로 평가하고 분석 논리를 loop에서 실행해야 하는 경우가 있다.
예를 들어, 시간 순으로 정렬된 온도 판독 값 stream이 있다고 가정헤보자.

record Reading(Instant obtainedAt, int kelvins) {

    Reading(String time, int kelvins) {
        this(Instant.parse(time), kelvins);
    }

    static Stream<Reading> loadRecentReadings() {
        // In reality these could be read from a file, a database,
        // a service, or otherwise
        return Stream.of(
                new Reading("2023-09-21T10:15:30.00Z", 310),
                new Reading("2023-09-21T10:15:31.00Z", 312),
                new Reading("2023-09-21T10:15:32.00Z", 350),
                new Reading("2023-09-21T10:15:33.00Z", 310)
        );
    }

}

이 stream에서 5초 이내에 두 번의 연속 측정값에서 30도 캘빈 이상의 온도 변화로 정의되는 의심스러운 변화를 감지하고 싶다고 가정해 보자.

boolean isSuspicious(Reading previous, Reading next) {
    return next.obtainedAt().isBefore(previous.obtainedAt().plusSeconds(5))
           && (next.kelvins() > previous.kelvins() + 30
               || next.kelvins() < previous.kelvins() - 30);
}

이를 위해서는 input stream을 순차적으로 스캔해야 하므로 선언적 stream 처리를 피하고 분석을 필수적으로 구현해야 한다.

List<List<Reading>> findSuspicious(Stream<Reading> source) {
    var suspicious = new ArrayList<List<Reading>>();
    Reading previous = null;
    boolean hasPrevious = false;
    for (Reading next : source.toList()) {
        if (!hasPrevious) {
            hasPrevious = true;
            previous = next;
        } else {
            if (isSuspicious(previous, next))
                suspicious.add(List.of(previous, next));
            previous = next;
        }
    }
    return suspicious;
}

var result = findSuspicious(Reading.loadRecentReadings());

// result ==> [[Reading[obtainedAt=2023-09-21T10:15:31Z, kelvins=312],
//              Reading[obtainedAt=2023-09-21T10:15:32Z, kelvins=350]],
//             [Reading[obtainedAt=2023-09-21T10:15:32Z, kelvins=350],
//              Reading[obtainedAt=2023-09-21T10:15:33Z, kelvins=310]]]

그러나 Gatherer를 사용하면 이를 더 간결하게 표현할 수 있다.

List<List<Reading>> findSuspicious(Stream<Reading> source) {
    return source.gather(Gatherers.windowSliding(2))
                 .filter(window -> (window.size() == 2
                                    && isSuspicious(window.get(0),
                                                    window.get(1))))
                 .toList();
}

Example: Defining a gatherer

Gatherers class에서 선언된 windowFixed Gatherer는 Gatherer interface의 직접 구현으로 작성할 수 있다.

record WindowFixed<TR>(int windowSize)
    implements Gatherer<TR, ArrayList<TR>, List<TR>>
{

    public WindowFixed {
        // Validate input
        if (windowSize < 1)
            throw new IllegalArgumentException("window size must be positive");
    }

    @Override
    public Supplier<ArrayList<TR>> initializer() {
        // Create an ArrayList to hold the current open window
        return () -> new ArrayList<>(windowSize);
    }

    @Override
    public Integrator<ArrayList<TR>, TR, List<TR>> integrator() {
        // The integrator is invoked for each element consumed
        return Gatherer.Integrator.ofGreedy((window, element, downstream) -> {

            // Add the element to the current open window
            window.add(element);

            // Until we reach our desired window size,
            // return true to signal that more elements are desired
            if (window.size() < windowSize)
                return true;

            // When the window is full, close it by creating a copy
            var result = new ArrayList<TR>(window);

            // Clear the window so the next can be started
            window.clear();

            // Send the closed window downstream
            return downstream.push(result);

        });
    }

    // The combiner is omitted since this operation is intrinsically sequential,
    // and thus cannot be parallelized

    @Override
    public BiConsumer<ArrayList<TR>, Downstream<? super List<TR>>> finisher() {
        // The finisher runs when there are no more elements to pass from
        // the upstream
        return (window, downstream) -> {
            // If the downstream still accepts more elements and the current
            // open window is non-empty, then send a copy of it downstream
            if(!downstream.isRejecting() && !window.isEmpty()) {
                downstream.push(new ArrayList<TR>(window));
                window.clear();
            }
        };
    }
}

사용 예시:

jshell> Stream.of(1,2,3,4,5,6,7,8,9).gather(new WindowFixed(3)).toList()
$1 ==> [[1, 2, 3], [4, 5, 6], [7, 8, 9]]

Example: An ad-hoc gatherer

또는 Gatherer.ofSequential(...) factory method를 통해 임시 방식으로 windowFixed gatherer를 작성할 수 도 있다.

/**
 * Gathers elements into fixed-size groups. The last group may contain fewer
 * elements.
 * @param windowSize the maximum size of the groups
 * @return a new gatherer which groups elements into fixed-size groups
 * @param <TR> the type of elements the returned gatherer consumes and produces
 */
static <TR> Gatherer<TR, ?, List<TR>> fixedWindow(int windowSize) {

    // Validate input
    if (windowSize < 1)
      throw new IllegalArgumentException("window size must be non-zero");

    // This gatherer is inherently order-dependent,
    // so it should not be parallelized
    return Gatherer.ofSequential(

            // The initializer creates an ArrayList which holds the current
            // open window
            () -> new ArrayList<TR>(windowSize),

            // The integrator is invoked for each element consumed
            Gatherer.Integrator.ofGreedy((window, element, downstream) -> {

                // Add the element to the current open window
                window.add(element);

                // Until we reach our desired window size,
                // return true to signal that more elements are desired
                if (window.size() < windowSize)
                    return true;

                // When window is full, close it by creating a copy
                var result = new ArrayList<TR>(window);

                // Clear the window so the next can be started
                window.clear();

                // Send the closed window downstream
                return downstream.push(result);

            }),

            // The combiner is omitted since this operation is intrinsically sequential,
            // and thus cannot be parallelized

            // The finisher runs when there are no more elements to pass from the upstream
            (window, downstream) -> {
                // If the downstream still accepts more elements and the current
                // open window is non-empty then send a copy of it downstream
                if(!downstream.isRejecting() && !window.isEmpty()) {
                    downstream.push(new ArrayList<TR>(window));
                    window.clear();
                }
            }

    );
}

사용 예시 :

jshell> Stream.of(1,2,3,4,5,6,7,8,9).gather(fixedWindow(3)).toList()
$1 ==> [[1, 2, 3], [4, 5, 6], [7, 8, 9]]

Example: A parallelizable gatherer

parallel stream에서 사용되는 경우 gatherer는 combiner function을 제공하는 경우에만 parallel로 평가된다.
예를 들어 이 병렬화 가능한 gatherer는 제공된 selector function에 따라 최대 하나의 element를 출력한다.

static <TR> Gatherer<TR, ?, TR> selectOne(BinaryOperator<TR> selector) {

    // Validate input
    Objects.requireNonNull(selector, "selector must not be null");

    // Private state to track information across elements
    class State {
        TR value;            // The current best value
        boolean hasValue;    // true when value holds a valid value
    }

    // Use the `of` factory method to construct a gatherer given a set
    // of functions for `initializer`, `integrator`, `combiner`, and `finisher`
    return Gatherer.of(

            // The initializer creates a new State instance
            State::new,

            // The integrator; in this case we use `ofGreedy` to signal
            // that this integerator will never short-circuit
            Gatherer.Integrator.ofGreedy((state, element, downstream) -> {
                if (!state.hasValue) {
                    // The first element, just save it
                    state.value = element;
                    state.hasValue = true;
                } else {
                    // Select which value of the two to save, and save it
                    state.value = selector.apply(state.value, element);
                }
                return true;
            }),

            // The combiner, used during parallel evaluation
            (leftState, rightState) -> {
                if (!leftState.hasValue) {
                    // If no value on the left, return the right
                    return rightState;
                } else if (!rightState.hasValue) {
                    // If no value on the right, return the left
                    return leftState;
                } else {
                    // If both sides have values, select one of them to keep
                    // and store it in the leftState, as that will be returned
                    leftState.value = selector.apply(leftState.value,
                                                     rightState.value);
                    return leftState;
                }
            },

            // The finisher
            (state, downstream) -> {
                // Emit the selected value, if there is one, downstream
                if (state.hasValue)
                    downstream.push(state.value);
            }

    );
}

임의의 integer stream에 대한 사용 예시이다:

jshell> Stream.generate(() -> ThreadLocalRandom.current().nextInt())
              .limit(1000)                   // Take the first 1000 elements
              .gather(selectOne(Math::max))  // Select the largest value seen
              .parallel()                    // Execute in parallel
              .findFirst()                   // Extract the largest value
$1 ==> Optional[99822]

JEP 463: Implicitly Declared Classes and Instance Main Methods (Second Preview)

Summary

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

History

JEP 445에서 Unnamed Classes and Instance main Method를 제안하였고 JDK 21에서 preview로 제공되었다.
피드백에 따르면 이 기능은 다음과 같은 주요한 변경 사항과 함께 JDK 22에서 두 번째로 preview 되어야 한다는 의견이 제시되어 제목이 수정되었다.

  • class의 이름을 지정하지 않고, class 선언이 없는 소스 파일에 이름 없는 class를 암시적으로 선언할 수 있도록 허용하는 아이디어는 주로 다른 class에서 해당 class를 사용할 수 없도록 하기 위한 주요 기능이었다.
    하지만 이는 오히려 방해가 되는 것으로 입증되었다.
    따라서 더 간단한 접근 방식을 채택하였다.
    class 선언이 없는 소스 파일은 host 시스템에서 선택한 이름으로 class를 암시적으로 선언하는 것이다.
    이렇게 암시적으로 선언된 class는 일반적인 최상위 class처럼 동작하며 추가적인 tooling, library, runtime 지원이 필요 없다.
  • 호출할 main method를 선택하는 절차는 method에 argument가 있는지 여부와 static method인지, instance method인지를 모두 고려해야 하는 등 너무 복잡했다.
    이번 두 번째 preview에서는 선택 절차를 두 단계로 단순화할 것을 제안한다.
    String[] parameter가 있는 후보 main method가 있으면 해당 method를 호출하고, 그렇지 않으면 parameter가 없는 후보 main method를 호출한다.
    class는 이름과 signature가 같은 static method와 instance method를 선언할 수 없으므로 여기에는 모호함이 없다.

Goals

  • 강사가 점진적인 방식으로 개념을 소개할 수 있도록 Java 프로그래밍에 대한 원활한 진입로를 제공한다.
  • 학생들이 간결한 방식으로 기본 프로그램을 작성하고 실력이 성장함에 따라 코드를 우하하게 성장시킬 수 있도록 도와준다.
  • 스크립트 및 명령줄 유틸리티와 같은 간단한 프로그램을 작성하는 의식을 줄인다.
  • 별도의 초급자용 java language dialect를 도입하지 않는다.
  • 별도의 초급자용 toolchain을 도입하지 말고, 학생 프로 그램은 모든 java 프로그램을 컴파일하고 실행하는 것과 동일한 툴을 사용하여 컴파일하고 실행해야 한다.

Implicitly declared classes

이제 다음처럼 Hello, World!를 작성할 수 있다.

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

최상위 멤버는 암시적 class의 member로 해석되므로 다음과 같이 프로그램을 작성할 수도 있다.

String greeting() { return "Hello, World!"; }

void main() {
    System.out.println(greeting());
}

또는 다음과 같이 field를 사용한다.

String greeting = "Hello, World!";

void main() {
    System.out.println(greeting);
}

암시적 class에 static main method가 아닌 instance method가 있는 경우 이를 실행하는 것은 기존의 anonymous class declaration construct 를 사용하는 다음과 동일하다.

new Object() {
    // the implicit class's body
}.main();

암시적 class가 포함된 HelloWorld.java 라는 소스 파일은 다음과 같이 소스 코드 런처를 사용하여 시작할 수 있다.

java HelloWorld.java
반응형
profile

파란하늘의 지식창고

@Bluesky_

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