Java

Java 21 새로운 기능(2) - preview

AlwaysPr 2023. 8. 28. 08:00

Java

Java 21 새로운 기능(1) - main에 이어 Preview 인 7개의 feature를 알아보자.

 

String Templates

String을 간단하게 사용하기 위해 이미 많은 언어에서 String Templates을 사용하고 있다. Stirng Templates은 String을 쉽게 표현해서 Java 코드 작성을 간단하게 한다. 이로 인해 text와 expressions의 가독성이 향상된다.

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

예제

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

\{변수명}으로 사용할 수 있다.

 

// 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"

또한 method 호출과 추가적인 연산도 가능하다.

 

Unnamed Patterns and Variables

사용하지 않는 변수를 이름 없는 변수로 지정하여 Java 언어를 향상시킨다. 불필요한 Nested patterns을 제거하여 Record patterns의 가독성을 개선한다. 또한 선언해야 하지만 사용되지 않는 변수를 식별하여, 코드의 유지보수성을 개선한다.

예제

// type X
... instanceof Point(int x, _)
case Point(int x, _)


// type 명시
... instanceof Point(int x, int _)
case Point(int x, int _)

// Pattern matching 이외에도 자유롭게 사용
int _ = q.remove();

... } catch (NumberFormatException _) { ...

(int x, int _) -> x + x

 

Unnamed Classes and Instance Main Methods

자바를 동작시키기 위해서는 클래스와 함께 `public static void main(String[] args)`이라는 긴 구절을 외워서 사용해야 한다. 이것을 이름 없는 클래스와 간단한 main method로 동작가능하게 한다. 학생들이 대규모 프로그램을 위해 설계된 언어 기능을 이해하지 않고도 첫 번째 프로그램을 작성할 수 있도록 한다 

예제

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

//to-be
class HelloWorld { 
    void main() { 
        System.out.println("Hello, World!");
    }
}

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

 

Scoped Values

Scoped Values을 도입하여 Thread 내 또는 Thread 간에 불변 데이터를 공유할 수 있다. 특히 많은 수의 Virtual Thread를 사용할 때 Thread 지역 변수보다 선호된다.

 

Thread 지역 변수는 몇 가지 설계적 결함이 있다.

  1. 제약 없는 변경 가능성 - 가변객체의 단점
  2. 무한한 수명 - remove 메소드를 호출하지 않으면, 계속 살아있고 메모리 누출로 이어질 수 있음
  3. 값 비싼 상속비용 - 많은 수의 Thread를 사용할 때 값비싼 비용이 발생

이러한 단점을 Scoped Values가 해소해 주며, 안전하고 효율적인 API이다.

예제

final static ScopedValue<...> V = ScopedValue.newInstance();

// In some method
ScopedValue.where(V, <value>)
           .run(() -> { ... V.get() ... call methods ... });

// In a method called directly or indirectly from the lambda expression
... V.get() ...

코드의 구조는 Thread가 ScopedValue의 값을 읽을 수 있는 기간을 나타낸다. 불변성과 결합된 이 제한된 수명은 Thread 동작에 대한 추론을 크게 단순화한다.

 

class Server {
    final static ScopedValue<Principal> PRINCIPAL
        = ScopedValue.newInstance();                                  // (1)

    void serve(Request request, Response response) {
        var level     = (request.isAdmin() ? ADMIN : GUEST);
        var principal = new Principal(level);
        ScopedValue.where(PRINCIPAL, principal)                       // (2)
                   .run(() -> Application.handle(request, response));
    }
}

class DBAccess {
    DBConnection open() {
        var principal = Server.PRINCIPAL.get();                       // (3)
        if (!principal.canOpen()) throw new  InvalidPrincipalException();
        return newConnection(...);
    }
}

웹 프레임워크를 예로 보면, 

  1. 인스턴스화한다.
  2. Value를 ScopedValue에 담는다.
  3. 사용한다.

 

Structured Concurrency

동시성은 서로 다른 Thread에서 실행 중인 관련 작업 그룹을 하나의 작업 단위로 처리하여 오류 처리 및 취소를 간소화하고, 안정성을 개선하며, 통합 가시성을 향상한다. Thread 누수 및 취소 지연과 같이 취소/종료로 인해 발생하는 일반적인 위험을 제거할 수 있는 동시 프로그래밍 스타일을 제공한다.

예제

//as-is
Response handle() throws ExecutionException, InterruptedException {
    Future<String>  user  = esvc.submit(() -> findUser());
    Future<Integer> order = esvc.submit(() -> fetchOrder());
    String theUser  = user.get();   // Join findUser
    int    theOrder = order.get();  // Join fetchOrder
    return new Response(theUser, theOrder);
}

//to-be
Response handle() throws ExecutionException, InterruptedException {
    try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
        Supplier<String>  user  = scope.fork(() -> findUser());
        Supplier<Integer> order = scope.fork(() -> fetchOrder());

        scope.join()            // Join both subtasks
             .throwIfFailed();  // ... and propagate errors

        // Here, both subtasks have succeeded, so compose their results
        return new Response(user.get(), order.get());
    }
}

 

Vector API

지원되는 CPU 아키텍처에서 최적의 Vector 명령어로 런타임에 안정적으로 컴파일되어 동등한 스칼라 계산보다 뛰어난 성능을 달성하는 벡터 계산을 표현하는 API를 도입한다.

예제

//as-is
void scalarComputation(float[] a, float[] b, float[] c) {
   for (int i = 0; i < a.length; i++) {
        c[i] = (a[i] * a[i] + b[i] * b[i]) * -1.0f;
   }
}


//to-be
static final VectorSpecies<Float> SPECIES = FloatVector.SPECIES_PREFERRED;

void vectorComputation(float[] a, float[] b, float[] c) {
    int i = 0;
    int upperBound = SPECIES.loopBound(a.length);
    for (; i < upperBound; i += SPECIES.length()) {
        // FloatVector va, vb, vc;
        var va = FloatVector.fromArray(SPECIES, a, i);
        var vb = FloatVector.fromArray(SPECIES, b, i);
        var vc = va.mul(va)
                   .add(vb.mul(vb))
                   .neg();
        vc.intoArray(c, i);
    }
    for (; i < a.length; i++) {
        c[i] = (a[i] * a[i] + b[i] * b[i]) * -1.0f;
    }
}

VetorSpecies를 정적 final 필드에 저장하여 사용하면 런타임 컴파일러가 값을 상수로 취급하여 벡터 계산을 더 잘 최적화할 수 있다.

 

Foreign Function & Memory API

Java 프로그램이 Java 런타임 외부의 코드 및 데이터와 상호 운용할 수 있는 API이다. 이 API는 외부 함수(즉, JVM 외부의 코드)를 효율적으로 호출하고 외부 메모리(즉, JVM에서 관리하지 않는 메모리)에 안전하게 액세스함으로써 Java 프로그램이 JNI의 취약성과 위험 없이 네이티브 라이브러리를 호출하고 네이티브 데이터를 처리할 수 있게 해 준다. 해당 API는 java.lang.foreign 패키지에 존재한다. 

예제

// Foreign memory access
 MemorySegment segment = Arena.ofAuto().allocate(10 * 4, 1);
 for (int i = 0 ; i < 10 ; i++) {
     segment.setAtIndex(ValueLayout.JAVA_INT, i, i);
 }

// Deterministic deallocationCopy URL
try (Arena arena = Arena.ofConfined()) {
    MemorySegment segment = arena.allocate(10 * 4);
    for (int i = 0 ; i < 10 ; i++) {
        segment.setAtIndex(ValueLayout.JAVA_INT, i, i);
    }
}

// Foreign function access
Linker linker = Linker.nativeLinker();
SymbolLookup stdlib = linker.defaultLookup();
MethodHandle strlen = linker.downcallHandle(
    stdlib.find("strlen").get(),
    FunctionDescriptor.of(ValueLayout.JAVA_LONG, ValueLayout.ADDRESS)
);

try (Arena arena = Arena.ofConfined()) {
    MemorySegment cString = arena.allocateUtf8String("Hello");
    long len = (long)strlen.invoke(cString); // 5
}