파란하늘의 지식창고
Published 2020. 10. 13. 14:13
JDK 15 New Features Study/Java
반응형

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

Spec

Java SE 15 Platform JSR 390에 정의된 바와 같이 JSR 390 구현이 목표

실제 Spec은 Final Release Specification 문서를 참고해야 함

Final Release Specification Feature Summary

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

Component Feature
security-libs / javax.crypto Edwards-Curve Digital Signature Algorithm (EdDSA)
specification / language Sealed Classes (Preview)
core-libs / java.lang.invoke Hidden Classes
core-libs / jdk.nashorn Remove the Nashorn JavaScript Engine
specification / language Pattern Matching for instanceof (Second Preview)
hotspot / gc ZGC: A Scalable Low-Latency Garbage Collector (Production)
specification / languagespecification / language Text Blocks
hotspot / gc Shenandoah: A Low-Pause-Time Garbage Collector (Production)
specification / language Records (Second Preview)

JEP 339: Edwards-Curve Digital Signature Algorithm (EdDSA)

RFC 8032에 설명된 바와 같이 Edwards-Curve 디지털 서명 알고리즘(EdDsa)을 사용하여 암호화 서명을 구현하였다.

기존 암호화 알고리즘의 취약점과 성능을 개선한 새로운 알고리즘이 추가되었다고 보면 된다.

JEP 360: Sealed Classes (Preview)

sealed class에 대한 분량이 많아 요약을 하기보단 되도록 원문을 그대로 번역해놓았다.

Motivation

sealed class와 interface가 제공된다.
확장하거나 구현할 수 있는 class나 interface를 제한한다.
java에서 class 계층 구조는 상속을 통해 코드 재사용을 가능하게 한다.
superclass와 method는 많은 subclass에 상속되고 재사용된다.
그러나 class 계층 구조의 목적이 항상 코드를 재사용하기 위한 것은 아니다.
때때로 그래픽 라이브러리에서 지원하는 shape의 종류, 또는 금융 프로그램에서 지원하는 대출 종류와 같이 domain에 존재하는 다양한 가능성을 모델링하는 것이 목적이 될 수 있다.
이러한 방식으로 class 계층 구조를 사용하는 경우 subclass 집합을 제한하면 모델링이 간소화될 수 있다.

예를 들어, 그래픽 라이브러리에서 class 작성자는 shape의 특정 class만 확장할 수 있도록 의도할 수 있다.
라이브러리 작업의 대부분은 각 shape의 확장을 적절한 방식으로 처리하는 것과 관련 있기 때문이다.
작성자는 알려진 Shape의 subclass를 처리하는 코드의 명확성에 관심이 있고 알 수 없는 Shape의 subclass에 대해서는 방어하기 위한 코드 작성에는 관심이 없다.
이 경우 임의의 class를 확장하여 재사용을 위해 코드를 상속하는 것은 목표가 아니다.
불행하게도 Java는 코드 재사용이 항상 목표라고 가정한다.
Shape가 확장 가능하면 그다음에는 또 다른 다수의 class로도 확장이 가능하다.
sealed class는 작성자가 임의의 class에 의해 확장되지 않는 class 계층 구조 선언을 할 수 있도록 도와준다.
코드 재사용은 이런 닫힌 class 계층 내에서는 여전히 가능하지만 그 이상은 불가능하다.

Java 개발자는 API 디자인에서 자주 발생하기 때문에 subclass 집합을 제한하는 아이디어에 익숙하다.
하지만 Java는 이 영역에 제한된 방법을 제공했다.
constructor를 private으로 만들거나 class를 final로 만드는 을 통해 subclass를 만들지 못하도록 하였다.
package 전용 super class의 예는 JDK에서도 볼 수 있다.

package java.lang;

abstract class AbstractStringBuilder {...}
public final class StringBuffer  extends AbstractStringBuilder {...}
public final class StringBuilder extends AbstractStringBuilder {...}

package private 접근 방식은 AbstractStringBuilder 코드를 공유하는 subclass와 같이 코드 재사용이 목표인 경우 유용하다.
하지만 사용자의 코드가 superclass의 key 추상화를 접근할 수 없기 때문에 대안을 모델링하는 것이 목표일 때는 쓸모가 없다.
확장을 허용하지 않으면 사용자가 superclass에 접근하도록 허용할 수 없다.
요약하면 superclass가 광범위하게 접근할 수 있지만 광범위하게 확장할 수 없어야 한다. (sugclass가 작성자가 알고 있는 것으로 제한되어야 하므로)

그러한 superclass는 독자에 대한 의도를 문서화하고 java compiler에 의한 시행을 허용하기 위해 주어진 subclass set과 함께 공통 개발되었음을 표현할 수 있어야 한다.
또한 superclass는 강제로 final이나 상태를 정의하지 못하게 하는 것과 같이 subclass를 부당하게 제한해서는 안된다.

Description

sealed class나 interface는 해당 class와 interface만 extend나 implement를 허용할 수 있다.

sealed class는 sealed modifier를 적용해 선언된다.
그런 다음 extends나 implements 절 뒤에 permits 절은 sealed class를 확장할 수 있는 class를 지정한다.

예를 들어 다음 선언은 Shape 가 3개의 subclass를 허용한다.

package com.example.geometry;

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

permits로 명시된 class는 superclass 근처에 위치해야 한다.
(superclass가 명명된 module에 있는 경우 동일 module에, module로 명명되지 않은 경우 동일 package에 위치)
예를 들어 아래의 Shape 선언은 같은 module의 다른 package에 위치한 subclass를 허용한다.

package com.example.geometry;

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

permitted subclass가 규모가 작고 개수가 적을 경우 sealed class와 동일한 source file에서 선언하는 것이 편리할 수 있다.
이러한 방식으로 선언되면 sealed class는 permits 절을 생략할 수 있으며 java compiler는 source file의 선언에서 허용된 subclass를 유추한다.
예를 들어 다음과 같은 Shape class는 3개의 permitted subclass가 있다는 것을 알 수 있다.

package com.example.geometry;

abstract sealed class Shape {...}
... class Circle    extends Shape {...}
... class Rectangle extends Shape {...}
... class Square    extends Shape {...}

class sealing의 목적은 client code가 허용된 모든 subclass에 대해 명확하게 알 수 있도록 하는 것이다.

subclass에 대해 추론하는 전통적인 방법은 if-else 구문으로 instanceof를 연달아 사용하는 방법이었다. 하지만 이 구문으로 분석하는 것은 compiler에게 어렵기 때문에 이 테스트가 허용된 모든 subclass가 포함된다고 판단할 수는 없다.

예를 들어 다음 method는 Shape의 모든 subclass가 테스트되고 return으로 이어진다는 개발자의 확신을 compiler가 공유하지 않기 때문에 compile time error를 발생시킨다.

int getCenter(Shape shape) {
    if (shape instanceof Circle) {
        return ... ((Circle)shape).center() ...
    } else if (shape instanceof Rectangle) {
        return ... ((Rectangle)shape).length() ...
    } else if (shape instanceof Square) {
        return ... ((Square)shape).side() ...
    }
}

포괄적인 else 절을 추가하는 것은 테스트가 철저하다는 개발자의 확신에 위배된다.
또한 compiler는 확신이 잘못된 것으로 판명되면 개발자를 구할 수 없다.
위의 코드가 실수로 instanceof Rectangle 테스트를 생략하도록 편집되었다고 가정하자.
compile 타임에 오류가 발생하지 않는다. (이 누락은 세 개의 permitted subclass에선 쉽게 발견할 수 있지만 10개 또는 20개가 된다면 발견하기 어려울 것이다.)
단 3개로도 코드는 작성하기가 답답하고 읽기가 지루해진다.
permitted subclass에 대해 명확하고 결정적으로 추론하는 기능은 패턴 일치를 지원하는 향후 release에서 실현될 것이다.
if-else 구문으로 sealed class의 instance를 검사하는 대신 switch 구문을 통해 type test pattern(JEP 375)을 이용할 수 있다.
이를 통해 compiler는 pattern이 완전한지 확인할 수 있다.
예를 들어 다음 코드 가주어지면 compiler는 Shape의 모든 허용된 subclass가 Shape에 포함되어 있다고 추론하므로 default 절이 필요하지 않다.
또한 compiler는 세 가지 경우 중 하나라도 누락된 경우 오류를 제공한다.

int getCenter(Shape shape) {
    return switch (shape) {
        case Circle c    -> ... c.center() ...
        case Rectangle r -> ... r.length() ...
        case Square s    -> ... s.side() ...
    };
}

sealed class는 permitted subclass (permits 절에 지정된 class)에 세 가지 제약 조건을 적용한다. 

  1. sealed class와 그의 permitted subclass는 동일한 module에 속해야 하며 이름이 지정되지 않은 module에서 선언된 경우 동일한 package에 속해야 한다.
  2. 모든 permitted subclass는 sealed class를 직접(directly) 확장해야 한다. 
  3. 모든 permitted subclass는 superclass에 의해 시작된 봉인을 계속하는 방법을 설명하는 modifier를 선택해야 한다.
  • permitted subclass는 class 계층 구조의 일부가 더 이상 확장되지 않도록 하기 위해 final로 선언될 수 있다.
  • permitted subclass는 sealed로 선언되어 계층 구조의 일부가 봉인된 superclass가 예상하는 것보다 더 확장될 수 있지만 제한된 방식으로 확장될 수 있다.
  • permitted subclass는 non-sealed로 선언되어 계층 구조의 일부가 알려지지 않은 subclass에 의해 확장을 위해 열린 상태로 되돌아 갈 수 있다. (sealed class는 permitted subclass가 이 작업을 수행하는 것을 막을 수 없다.)

세 번째 제약 조건의 예로 Rectangle 은 sealed이고 Square는 non-sealed인 경우이다.

package com.example.geometry;

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

public final class Circle extends Shape {...}

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

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

허용되는 각 subclass에서 final, sealed 및 non-sealed modifier 중 하나만 사용해야 한다.

sealed(implying subclasses)와 final(implying subclasses) 또는
non-sealed(implying subclasses)와 final(implying subclasses) 또는
sealed(implying restricted subclasses)와 non-sealed(implying unrestricted subclasses)
class에서 같이 쓰는 것은 불가능하다.

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

abstract class는 sealed 또는 non-sealed class는 abstract class일 수 있으며 abstract member를 포함한다.
sealed class는 abstract subclass를 허용할 수 있다. (final이 아닌 sealed 또는 non-sealed 인 경우)

class 접근성. extends 및 permits 절은 class 이름을 사용하므로 permitted subclass와  sealed superclass는 서로 access 할 수 있어야 한다.
그러나 허용된 subclass는 각각 sealed class와 동일한 접근성을 가질 필요는 없다.
특히 subclass는 sealed class보다 접근성이 떨어질 수 있다.
즉, switch가 pattern 일치를 지원하는 향후 release에서 일부 사용자는 기본 절 (또는 기타 전채 패턴)을 사용하지 않는 한 subclass를 완전히 전환할 수 없다.
Java compiler는 사용자의 switch가 사용자가 생각한 것만큼 완전하지 않은 경우를 감지하고 오류 메시지를 사용자 정의하여 default 절을 권장하도록 한다.

Sealed interfaces

class의 경우와 유사하게 interface에 sealed modifier를 사용하여 interface를 sealed 처리한다.
superinterface를 지정하기 위한 extends 절 뒤에 implement class와 subinterface가 permits 절로 지정된다.
예를 들면 아래와 같다.

package com.example.expression;

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

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

Sealed classes and Records

sealed class는 Java 15의 또 다른 preview 기능인 record(JEP 384)와 함께 잘 동작한다.
record는 암시적으로 final이므로 record가 있는 sealed 계층은 위의 예 보다 약간 더 간결하다.

package com.example.expression;

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

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

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

Sealed classes in the JDK

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

package java.lang.constant;

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

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

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

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

Java Grammar

NormalClassDeclaration:
  {ClassModifier} class TypeIdentifier [TypeParameters]
  [Superclass] [Superinterfaces] [PermittedSubclasses] ClassBody

ClassModifier:
  (one of)
  Annotation public protected private
  abstract static sealed final non-sealed strictfp

PermittedSubclasses:
  permits ClassTypeList

ClassTypeList:
  ClassType {, ClassType}

JVM support for sealed classes

Java Virtual Machine은 runtime 시 sealed class 및 interface를 인식하고 권한이 없는 subclass 및 subinterface에 의한 확장을 방지한다.

seal은 class modifier지만 ClassFile 구조에는 ACC_SEALED flag가 없다.
대신 sealed class의 class file에는 sealed modifier를 암시적으로 나타내고 허용된 subclass를 명시적으로 지정하는 PermittedSubclasses 속성이 있다.

PermittedSubclasses_attribute {
    u2 attribute_name_index;
    u4 attribute_length;
    u2 number_of_classes;
    u2 classes[number_of_classes];
}

permitted subclass 목록은 필수이다.
permitted subclass가 compiler에 의해 추론된 경우에도 이러한 추론된 subclass는 PermittedSubclasses 속성에 명시적으로 포함된다.

permitted subclass의 class file에는 새 attribute는 없다.

JVM이 superclass 또는 superinterface에 PermittedSubclasses 속성이 있는 class를 정의하려고 할 때 class는 attribute에 의해 이름이 지정되어야 한다.
그렇지 않으면 IncompatibleClassChangeError가 발생한다.

JEP 371: Hidden Classes

다른 class의 bytecode에서 직접 사용할 수 없는 class이다.
Hidden class는 runtime에 class를 생성하고 reflection을 통해 간접적으로 사용하는 framework에서 사용하기 위한 것이다.
hidden class는 access 제어 중첩의 member로 정의될 수 있으며 다른 class와 독립적으로 unload 될 수 있다.

JEP 372: Remove the Nashorn JavaScript Engine

Nashorn Javscript engine과 관련 API, jjs toole이 제거되었다.
Java 11에서 deprecated 처리가 되었던 항목이다.

JEP 375: Pattern Matching for instanceof (Second Preview)

JDK 14에서 JEP 305로 소개된 patten matching for instanceof의 두 번째 preview이다.

문서상 달라진 부분은 없으며 기존에 관련 내용을 소개한 글을 참고하면 된다.

2020/03/30 - [Study/Java] - JDK 14 New Features

JEP 377: ZGC: A Scalable Low-Latency Garbage Collector (Production)

JDK 11에 experimental feature로 추가되었던 Z Garbage Collector가 product feature가 되었다. (JEP 333)

기존의 default GC인 G1을 변경하는 것은 아니다.

기존엔 Linux x64 만 지원하던 것이 이제 아래 플랫폼을 모두 지원하게 되었다.

  • Linux/x86_64
  • Linux/aarch64
  • Windows
  • macOS

이제 XX:+UnlockExperimentalVMOptions 옵션을 추가하지 않아도 사용 가능하며 기존엔 -XX:+UnlockExperimentalVMOptions -XX:+UseZGC 옵션을 통해 사용할 수 있었다.

JDK 11에 도입된 이후 여러 피드백을 받고 버그를 수정했으며 여러 기능과 개선 사항을 추가하였다.

그중 중요한 몇 가지는 아래와 같다.

  • Concurrent class unloading
  • Uncommitting unused memory
  • 최대 heap size 4TB 에서 16TB로 변경
  • 최소 heap size 8MB로 감소
  • -XX:SoftMaxHeapSize
  • JFR leak profiler 지원
  • class-data sharing 지원
  • 제한적이고 불연속적인 address space
  • NVRAM heap 배치 지원
  • NUMA 인식 향상
  • multi threaded heap pre-touching

JEP 378: Text Blocks

Text Block 기능은 JDK 13에 preview 기능으로 추가되었었다. (JEP 355)

당초 목표는 JDK 12였지만 철회되었고(관련 글 참고) JDK 13과 JDK 14에 단계적으로 추가되어 최종적으로 JDK 15에서 preview 딱지를 떼게 되었다. 

기존에 JDK 13와 JDK 14에서 소개한 글을 참고하면 된다.

2019/09/25 - [Study/Java] - JDK 13 New Features

2020/03/30 - [Study/Java] - JDK 14 New Features

JEP 379: Shenandoah: A Low-Pause-Time Garbage Collector (Production)

JDK 12에 experimental feature로 추가되었던 Shenandoah garbage collector도 product feature로 변경되었다. (JEP 189)

ZGC와 마찬가지로 default GC인 G1 GC를 대체하지는 않는다.

이제 XX:+UnlockExperimentalVMOptions 옵션을 추가하지 않아도 사용 가능하며 기존엔 -XX:+UnlockExperimentalVMOptions -XX:+UseShenandoahGC 옵션을 통해 사용할 수 있었다.

Shenandoah GC는 실행 중인 Java thread와 병렬로 gc 작업을 수행하여 GC 일시 중지 시간을 줄인다고 한다.

JEP 384: Records (Second Preview)

Record에 대한 분량이 많아 요약을 하기보단 되도록 원문을 그대로 번역해놓았다.

JDK 14에서 JEP 359로 소개된 Records의 두 번째 preview이다.

Goals

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

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

Motivation

Java가 너무 서술적이라는 건 일반으로 많이 알려진 불만 사항이다.
작은 값에 대한 불변 데이터를 가진 class의 경우에도 올바르게 사용하려면 accessor, constructor, equals, hashCode, toString 같은 코드를 작성해야 한다.
예를 들어 x, y 좌표를 전달하는 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);
    }
}

개발자는 때로 equals와 같은 method를 생략하고 이 불완전한 class를 service를 통해 사용하여 코드의 양을 줄이는 유혹을 받는다.
그게 좋은 모양으로 보이기 때문이다.

IDE는 data carrier class에서 대부분의 코드를 작성하는데 도움이 되지만 독자가 수십 줄에서 "나는 x, y 및 z를 위한 data carrier이다"라는 의도를 추측하는데 도움이 되진 않는다.

Description

작은 값을 모델링하는 Java 코드를 작성하면 쓰기, 읽기 및 올바른 확인이 더 쉬워야 한다.
record는 data를 data로 모델링하는 것을 목표로 한다.
소규모의 variable의 그룹이 새로운 종류의 entity로 간주되는 것을 선언하는 것이다.
java 언어의 새로운 종류의 class이다.
record는 일반적인 class가 가지는 자유도를 포기하지만 대신 간결하게 쓸 수 있다.

record 선언은 name, header 및 body를 지정한다.
header에는 상태를 구성하는 변수인 record의 구성 요소가 나열된다. (구성 요소 목록을 상태 설명이라고도 함)
예를 들면 아래와 같다.

record Point(int x, int y) { }

record는 data에 대한 투명한 carrier라는 의미를 가지기 때문에 많은 표준 구성 요소를 자동으로 획득한다.

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

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

Rules for Records

record의 component에서 파생된 private field를 제외하고 header에서 자동으로 파생되는 모든 member는 명시적으로 선언할 수 있다.
accessor 또는 equals/hashCode의 명시적 구현은 record의 의미적 불변성을 보존하도록 주의해야 한다.

constructor에 대한 규칙은 일반 class와 record에서 다르다.
constructor 선언이 없는 일반 class에서는 default constructor가 자동으로 제공된다.
반대로 constructor 선언이 없는 record에는 record를 instance화 한 새 표현식의 해당 argument에 모든 private field를 할당하는 표준 constructor가 자동으로 제공된다.
예를 들어 이전에 선언된 record -- Point(int x, int y) {} -- 는 다음과 같이 compile 된다.

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

    // Other implicit declarations elided ...

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

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

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

record 선언에는 여러 제한 사항이 있다.

  • record에는 extends 절이 없다.
    record의 superclass는 항상 java.lang.Record이며, enum의 superclass가 항상 java.lang.Enum 인 것과 유사하다.
    일반 class가 암시적 superclass Object를 명시적으로 확장할 수 있지만 record는 암시적 superclass Record 조차도 명시적으로 확장할 수 없다.
  • record는 암묵적으로 final이며 abstract 일 수 없다.
    이러한 제한은 record의 API가 상태 설명에 의해서만 정의되며 나중에 다른 class나 record에 의해 향상될 수 없음을 강조한다.
  • record는 instance field를 명시적으로 선언할 수 없으며 instance initializer를 포함할 수 없다.
    이러한 제한은 record header만으로 record 값의 상태를 정의하도록 한다.
  • record class의 record component에 해당하는 암시적으로 선언된 field는 final이며 reflection을 통해 수정할 수 없다. (그렇게 하면 IllegalAccessException이 발생함)
    이러한 제한은 data carrier class에 널리 적용되는 기본적으로 변경 불가능한 정책을 구현한다.
  • 그렇지 않으면 자동으로 파생되는 member의 모든 명시적 선언은 명시적 선언에 대한 주석을 무시하고 자동으로 파생된 member의 형식과 정확히 일치해야 한다.
  • record는 native method를 선언할 수 없다.
    record가 native method를 선언할 수 있는 경우 record의 동작은 정의에 따라 record의 명시적 상태가 아닌 외부 상태에 따라 달라진다.
    native method가 있는 class는 record로 migration 하기 적당하지 않다.

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

  • record는 new keyword로 instance화 된다.
  • record는 최상위 레벨로 선언되거나 중첩될 수 있고 generic일 수 있다.
  • record는 static method, static field, static initializer를 선언하라 수 있다.
  • record는 instance method를 선언할 수 있다.
    즉 record는 component에 해당하는 public accessor method를 명시적으로 선언할 수 있으며 다른 instance method도 선언할 수 있다.
  • record는 interface를 구현할 수 있다.
    record는 superclass를 지정할 수 없지만 (header에 설명된 상태를 넘어서 상속된 상태를 의미하기 때문에) record는 superinterface를 자유롭게 지정하고 이를 구현하는데 도움이 되는 instance method를 선언할 수 있다.
    class와 마찬가지로 interface는 많은 record의 동작을 유용하게 특성화할 수 있다.
    동작은 domain 독립적 (ex: Comparable) 또는 domain 특정일 수 있으며, 이 경우 record는 domain을 capture 하는 sealed hierarchy의 일부가 될 수 있다. (아래 참조)
  • record는 중첩된 record를 포함하여 중첩된 type을 선언할 수 있다.
    record 자체가 중첩된 경우 암시적으로 static이다.
    이렇게 하면 record에 상태를 자동으로 추가하는 즉시 enclosing instance를 피할 수 있다.
  • record 및 해당 상태 설명의 component에 주석을 달 수 있다.
    주석은 자동으로 파생된 field, method 및 constructor parameter로 전파된다.
    record component type에 대한 Type annotation도 자동으로 파생된 member type으로 전파된다.

Records and Sealed Types

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

package com.example.expression;

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

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

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

Local records

record를 생성하고 소비하는 프로그램은 그 자체로 단순한 변수 그룹인 많은 중간 값(intermediate values)을 다룰 가능성이 높다.
이러한 중간 값을 모델링하기 위해 record를 선언하는 것이 편리한 경우가 많다.
한 가지 옵션은 오늘날 많은 프로그램이 helper class를 선언하는 것처럼 정적이고 중첩된 "helper" record를 선언하는 것이다.
더 편리한 옵션은 variable을 조작하는 코드에 가까운 method 내부에 record를 선언하는 것이다.
따라서 이 JEP에서는 전통적인 local class 구성과 유사한 local record를 제안한다.

다음 예에서 판매자와 월별 판매 수치의 집계는 local record인 MerchantSales로 모델링 된다.
이 record를 사용하면 다음과 같은 stream operation의 가독성이 향상된다.

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

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

local record는 중첩 record의 특별한 경우이다.
모든 중첩 record와 마찬가지로 local record는 암시적으로 static이다.
이는 자신의 method가 바깥쪽 method의 variable에 access 할 수 없음을 의미한다.
결과적으로 이것은 record의 상태에 자동으로 추가하는 즉시 enclosing instance를 캡처하는 것을 방지한다.
local record가 암시적으로 static이라는 사실은 암시적으로 static이 아닌 local class와 대조된다.
실제로 local class는 절대적으로 (암시적 또는 명시적으로) static이 아니며 항상 enclosing method의 변수에 access 할 수 있다.

local record의 유용성을 고려할 때 local enum과 local interface도 갖는 것이 유용할 것이다.
의미론에 대한 우려 때문에 전통적으로 Java에서는 허용되지 않았다.
특히 중첩된 enum과 중첩된 interface는 암시적으로 static이므로 local enum 및 local interface도 암시적으로 static이어야 한다.
아직 Java 언어 (local variable, local class)의 local 선언은 절대 static이 아니다.
그러나 JEP 359의 local record 도입은 이러한 의미론적 문제를 극복하여 local 선언이 정적이 되고 local enum 및 local interface에 대한 문을 열었다.

Annotations on records

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

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

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

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

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

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

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

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

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

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

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

  • record component의 annotation이 field 선언에 적용 가능한 경우 해당 private field에 annotation이 나타난다.
  • record component의 annotation이 method 선언에 적용 가능한 경우 해당 accessor method에 annotation이 나타난다.
  • record component의 annotation이 formal parameter에 적용 가능한 경우 annotation이 명시적으로 선언되지 않은 경우 표준 constructor에 해당 formal parameter에 표시되고 명시적으로 선언된 경우 compact constructor의 해당 formal parameter에 annotation이 나타난다.
  • record component의 annotation이 type에 적용 가능한 경우 전파 규칙은 선언이 아닌 해당 type 사용에 annotation이 표시된다는 점을 제외하면 선언 annotation의 경우와 동일하다.

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

annotation이 새로운 annotation 선언 @Target(RECORD_COMPONENT)를 사용하여 record component에 정의된 annotation에서 왔음을 선언할 수도 있다.
이러한 annotation은 reflection을 통해 검색할 수 있다.

Java Grammar

RecordDeclaration:
  {ClassModifier} `record` TypeIdentifier [TypeParameters]
    RecordHeader [SuperInterfaces] RecordBody

RecordHeader:
 `(` [RecordComponentList] `)`

RecordComponentList:
 RecordComponent { `,` RecordComponent}

RecordComponent:
 {Annotation} UnannType Identifier
 VariableArityRecordComponent

VariableArityRecordComponent:
 {Annotation} UnannType {Annotation} `...` Identifier

RecordBody:
  `{` {RecordBodyDeclaration} `}`

RecordBodyDeclaration:
  ClassBodyDeclaration
  CompactConstructorDeclaration

CompactConstructorDeclaration:
 {Annotation} {ConstructorModifier} SimpleTypeName ConstructorBody

Class-file representation

record의 class file은 Record attribute를 사용하여 record component에 대한 정보를 저장한다.

Record_attribute {
    u2 attribute_name_index;
    u4 attribute_length;
    u2 components_count;
    record_component_info components[components_count];
}

record_component_info {
    u2 name_index;
    u2 descriptor_index;
    u2 attributes_count;
    attribute_info attributes[attributes_count];
}

record component에 지워진 descriptor와 다른 일반 signature가 있는 경우 record_component_info 구조에 Signature 속성이 있어야 한다.

반응형
profile

파란하늘의 지식창고

@Bluesky_

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