몇 가지 흥미로운 변화와 개선을 가져오는 또 다른 LTS Java 릴리스가 이미 출시되었습니다. 가장 중요한 Java 21 기능을 분석하고, 실제로 어떻게 작동하는지 확인하고, 이 기술의 미래에 대한 중요성을 예측해 보겠습니다.
Java 플랫폼이 6개월 릴리스 주기를 채택한 이후로 "올해 Java가 사라질까요?"와 같은 영원한 질문을 넘어섰습니다. 또는 "새 버전으로 마이그레이션할 가치가 있나요?" 첫 번째 출시 이후 28년이 지났음에도 불구하고 Java는 계속해서 성장하고 있으며 많은 새로운 프로젝트의 기본 프로그래밍 언어로 인기를 끌고 있습니다.
Java 17은 중요한 이정표였지만 이제 Java 21이 차세대 장기 지원 릴리스(LTS)로 17의 자리를 차지했습니다. Java 개발자는 이 버전이 제공하는 변경 사항과 새로운 기능에 대한 최신 정보를 얻는 것이 중요합니다. 기사에서 Java 17 기능을 자세히 설명한 동료 Darek에게서 영감을 받아 JDK 21에 대해서도 비슷한 방식으로 논의하기로 결정했습니다.
JDK 21은 총 15개의 JEP(JDK Enhancement Proposals)로 구성됩니다. 공식 Java 사이트에서 전체 목록을 검토할 수 있습니다. 이 기사에서는 특히 주목할 만한 몇 가지 Java 21 JEP를 강조하겠습니다. 즉:
더 이상 지체하지 말고 코드를 자세히 살펴보고 업데이트를 살펴보겠습니다.
Spring 템플릿 기능은 아직 미리보기 모드입니다. 이를 사용하려면 컴파일러 인수에 –enable-preview 플래그를 추가해야 합니다. 그러나 미리 보기 상태임에도 불구하고 언급하기로 결정했습니다. 왜? 많은 인수가 포함된 로그 메시지나 SQL 문을 작성해야 하거나 어떤 자리 표시자가 주어진 인수로 대체될지 해독해야 할 때마다 매우 짜증이 나기 때문입니다. 그리고 Spring 템플릿은 저(그리고 여러분)에게 도움이 될 것을 약속합니다.
JEP 문서에 따르면 Spring 템플릿의 목적은 "런타임에 계산된 값을 포함하는 문자열을 쉽게 표현할 수 있도록 하여 Java 프로그램 작성을 단순화"하는 것입니다.
정말 더 간단한지 확인해 보겠습니다.
"기존 방식"은 String 객체에 formatd() 메서드를 사용하는 것입니다.
var msg = "Log message param1: %s, pram2: %s".formatted(p1, p2);
이제 StringTemplate.Processor(STR)를 사용하면 다음과 같습니다.
var interpolated = STR."Log message param1: \{p1}, param2: \{p2}";
위와 같은 짧은 텍스트를 사용하면 수익이 그다지 눈에 띄지 않을 수 있습니다. 하지만 큰 텍스트 블록(json, sql 문)의 경우 이름이 지정된 매개변수가 많은 도움이 될 것입니다.
Java 21에서는 새로운 Java 컬렉션 계층 구조를 도입했습니다. 아래 다이어그램을 보고 프로그래밍 수업 중에 배운 내용과 비교해 보세요. 세 개의 새로운 구조물이 추가된 것을 확인할 수 있습니다(녹색으로 강조 표시됨).
출처: JEP 431
순차화된 컬렉션에는 새로운 내장 Java API가 도입되어 정렬된 데이터세트에 대한 작업이 향상됩니다. 이 API를 사용하면 컬렉션의 첫 번째 요소와 마지막 요소에 편리하게 액세스할 수 있을 뿐만 아니라 효율적인 순회, 특정 위치에 삽입, 하위 시퀀스 검색도 가능합니다. 이러한 향상된 기능을 통해 요소 순서에 따른 작업이 더 간단하고 직관적으로 이루어지며 목록 및 유사한 데이터 구조로 작업할 때 성능과 코드 가독성이 모두 향상됩니다.
SequencedCollection 인터페이스의 전체 목록은 다음과 같습니다.
public interface SequencedCollection<E> extends Collection<E> { SequencedCollection<E> reversed(); default void addFirst(E e) { throw new UnsupportedOperationException(); } default void addLast(E e) { throw new UnsupportedOperationException(); } default E getFirst() { return this.iterator().next(); } default E getLast() { return this.reversed().iterator().next(); } default E removeFirst() { var it = this.iterator(); E e = it.next(); it.remove(); return e; } default E removeLast() { var it = this.reversed().iterator(); E e = it.next(); it.remove(); return e; } }
이제 다음 대신
var first = myList.stream().findFirst().get(); var anotherFirst = myList.get(0); var last = myList.get(myList.size() - 1);
다음과 같이 작성하면 됩니다.
var first = sequencedCollection.getFirst(); var last = sequencedCollection.getLast(); var reversed = sequencedCollection.reversed();
작은 변화지만 참 편리하고 유용한 기능이네요.
스위치의 패턴 매칭과 레코드 패턴의 유사성으로 인해 함께 설명하겠습니다. 레코드 패턴은 새로운 기능입니다. Java 19(미리보기로)에 도입되었습니다. 반면 switch에 대한 패턴 일치는 확장된 인스턴스 표현식의 연속입니다. 복잡한 데이터 지향 쿼리를 더 쉽게 표현할 수 있는 새로운 스위치 문 구문을 제공합니다.
이 예제에서는 OOP의 기본을 잊어버리고 직원 개체를 수동으로 분해해 보겠습니다(employee는 POJO 클래스입니다).
Java 21 이전에는 다음과 같았습니다.
if (employee instanceof Manager e) { System.out.printf("I’m dealing with manager of %s department%n", e.department); } else if (employee instanceof Engineer e) { System.out.printf("I’m dealing with %s engineer.%n", e.speciality); } else { throw new IllegalStateException("Unexpected value: " + employee); }
보기 흉한 인스턴스를 제거할 수 있다면 어떨까요? 이제 Java 21의 강력한 패턴 일치 덕분에 가능합니다.
switch (employee) { case Manager m -> printf("Manager of %s department%n", m.department); case Engineer e -> printf("I%s engineer.%n", e.speciality); default -> throw new IllegalStateException("Unexpected value: " + employee); }
While talking about the switch statement, we can also discuss the Record Patterns feature. When dealing with a Java Record, it allows us to do much more than with a standard Java class:
switch (shape) { // shape is a record case Rectangle(int a, int b) -> System.out.printf("Area of rectangle [%d, %d] is: %d.%n", a, b, shape.calculateArea()); case Square(int a) -> System.out.printf("Area of square [%d] is: %d.%n", a, shape.calculateArea()); default -> throw new IllegalStateException("Unexpected value: " + shape); }
As the code shows, with that syntax, record fields are easily accessible. Moreover, we can put some additional logic to our case statements:
switch (shape) { case Rectangle(int a, int b) when a < 0 || b < 0 -> System.out.printf("Incorrect values for rectangle [%d, %d].%n", a, b); case Square(int a) when a < 0 -> System.out.printf("Incorrect values for square [%d].%n", a); default -> System.out.println("Created shape is correct.%n"); }
We can use similar syntax for the if statements. Also, in the example below, we can see that Record Patterns also work for nested records:
if (r instanceof Rectangle(ColoredPoint(Point p, Color c), ColoredPoint lr)) { //sth }
The Virtual Threads feature is probably the hottest one among all Java 21 – or at least one the Java developers have waited the most for. As JEP documentation (linked in the previous sentence) says, one of the goals of the virtual threads was to “enable server applications written in the simple thread-per-request style to scale with near-optimal hardware utilization”. However, does this mean we should migrate our entire code that uses java.lang.Thread?
First, let’s examine the problem with the approach that existed before Java 21 (in fact, pretty much since Java’s first release). We can approximate that one java.lang.Thread consumes (depending on OS and configuration) about 2 to 8 MB of memory. However, the important thing here is that one Java Thread is mapped 1:1 to a kernel thread. For simple web apps which use a “one thread per request” approach, we can easily calculate that either our machine will be “killed” when traffic increases (it won’t be able to handle the load) or we’ll be forced to purchase a device with more RAM, and our AWS bills will increase as a result.
Of course, virtual threads are not the only way to handle this problem. We have asynchronous programming (frameworks like WebFlux or native Java API like CompletableFuture). However, for some reason – maybe because of the “unfriendly API” or high entry threshold – these solutions aren’t that popular.
Virtual Threads aren’t overseen or scheduled by the operating system. Rather, their scheduling is handled by the JVM. While real tasks must be executed in a platform thread, the JVM employs so-called carrier threads — essentially platform threads — to “carry” any virtual thread when it is due for execution. Virtual Threads are designed to be lightweight and use much less memory than standard platform threads.
The diagram below shows how Virtual Threads are connected to platform and OS threads:
So, to see how Virtual Threads are used by Platform Threads, let’s run code that starts (1 + number of CPUs the machine has, in my case 8 cores) virtual threads.
var numberOfCores = 8; // final ThreadFactory factory = Thread.ofVirtual().name("vt-", 0).factory(); try (var executor = Executors.newThreadPerTaskExecutor(factory)) { IntStream.range(0, numberOfCores + 1) .forEach(i -> executor.submit(() -> { var thread = Thread.currentThread(); System.out.println(STR."[\{thread}] VT number: \{i}"); try { sleep(Duration.ofSeconds(1L)); } catch (InterruptedException e) { throw new RuntimeException(e); } })); }
Output looks like this:
[VirtualThread[#29,vt-6]/runnable@ForkJoinPool-1-worker-7] VT number: 6 [VirtualThread[#26,vt-4]/runnable@ForkJoinPool-1-worker-5] VT number: 4 [VirtualThread[#30,vt-7]/runnable@ForkJoinPool-1-worker-8] VT number: 7 [VirtualThread[#24,vt-2]/runnable@ForkJoinPool-1-worker-3] VT number: 2 [VirtualThread[#23,vt-1]/runnable@ForkJoinPool-1-worker-2] VT number: 1 [VirtualThread[#27,vt-5]/runnable@ForkJoinPool-1-worker-6] VT number: 5 [VirtualThread[#31,vt-8]/runnable@ForkJoinPool-1-worker-6] VT number: 8 [VirtualThread[#25,vt-3]/runnable@ForkJoinPool-1-worker-4] VT number: 3 [VirtualThread[#21,vt-0]/runnable@ForkJoinPool-1-worker-1] VT number: 0
So, ForkJonPool-1-worker-X Platform Threads are our carrier threads that manage our virtual threads. We observe that Virtual Threads number 5 and 8 are using the same carrier thread number 6.
The last thing about Virtual Threads I want to show you is how they can help you with the blocking I/O operations.
Whenever a Virtual Thread encounters a blocking operation, such as I/O tasks, the JVM efficiently detaches it from the underlying physical thread (the carrier thread). This detachment is critical because it frees up the carrier thread to run other Virtual Threads instead of being idle, waiting for the blocking operation to complete. As a result, a single carrier thread can multiplex many Virtual Threads, which could number in the thousands or even millions, depending on the available memory and the nature of tasks performed.
Let’s try to simulate this behavior. To do this, we will force our code to use only one CPU core, with only 2 virtual threads – for better clarity.
System.setProperty("jdk.virtualThreadScheduler.parallelism", "1"); System.setProperty("jdk.virtualThreadScheduler.maxPoolSize", "1"); System.setProperty("jdk.virtualThreadScheduler.minRunnable", "1");
Thread 1:
Thread v1 = Thread.ofVirtual().name("long-running-thread").start( () -> { var thread = Thread.currentThread(); while (true) { try { Thread.sleep(250L); System.out.println(STR."[\{thread}] - Handling http request ...."); } catch (InterruptedException e) { throw new RuntimeException(e); } } } );
Thread 2:
Thread v2 = Thread.ofVirtual().name("entertainment-thread").start( () -> { try { Thread.sleep(1000L); } catch (InterruptedException e) { throw new RuntimeException(e); } var thread = Thread.currentThread(); System.out.println(STR."[\{thread}] - Executing when 'http-thread' hit 'sleep' function"); } );
Execution:
v1.join(); v2.join();
Result:
[VirtualThread[#21,long-running-thread]/runnable@ForkJoinPool-1-worker-1] - Handling http request .... [VirtualThread[#21,long-running-thread]/runnable@ForkJoinPool-1-worker-1] - Handling http request .... [VirtualThread[#21,long-running-thread]/runnable@ForkJoinPool-1-worker-1] - Handling http request .... [VirtualThread[#23,entertainment-thread]/runnable@ForkJoinPool-1-worker-1] - Executing when 'http-thread' hit 'sleep' function [VirtualThread[#21,long-running-thread]/runnable@ForkJoinPool-1-worker-1] - Handling http request .... [VirtualThread[#21,long-running-thread]/runnable@ForkJoinPool-1-worker-1] - Handling http request .... [VirtualThread[#21,long-running-thread]/runnable@ForkJoinPool-1-worker-1] - Handling http request .... [VirtualThread[#21,long-running-thread]/runnable@ForkJoinPool-1-worker-1] - Handling http request .... [VirtualThread[#21,long-running-thread]/runnable@ForkJoinPool-1-worker-1] - Handling http request .... [VirtualThread[#21,long-running-thread]/runnable@ForkJoinPool-1-worker-1] - Handling http request ....
We observe that both Virtual Threads (long-running-thread and entertainment-thread) are being carried by only one Platform Thread which is ForkJoinPool-1-worker-1.
To summarize, this model enables Java applications to achieve high levels of concurrency and scalability with much lower overhead than traditional thread models, where each thread maps directly to a single operating system thread. It’s worth noting that virtual threads are a vast topic, and what I’ve described is only a small fraction. I strongly encourage you to learn more about the scheduling, pinned threads and the internals of VirtualThreads.
위에 설명된 기능은 제가 Java 21에서 가장 중요하다고 생각하는 기능입니다. 대부분은 JDK 17에 도입된 일부 기능만큼 혁신적이지는 않지만 여전히 매우 유용하고 사용하기에 좋습니다. QOL(삶의 질) 변화가 있습니다.
그러나 JDK 21의 다른 개선 사항도 무시해서는 안 됩니다. 전체 목록을 분석하고 모든 기능을 자세히 살펴보시기 바랍니다. 예를 들어, 제가 특히 주목한다고 생각하는 것 중 하나는 지원되는 일부 CPU 아키텍처에서 벡터 계산을 허용하는 Vector API입니다. 이전에는 불가능했습니다. 현재는 아직 인큐베이터 상태/실험 단계에 있지만(그래서 여기서 더 자세히 강조하지 않았습니다) Java의 미래에 대한 큰 가능성을 갖고 있습니다.
전반적으로 다양한 영역에서 이루어진 Java의 발전은 수요가 많은 애플리케이션의 효율성과 성능을 개선하려는 팀의 지속적인 노력을 나타냅니다.
Java에 관심이 있다면 다음 기사를 확인해 보세요.
다음은 JDK 21과 Java 기본 인터페이스 및 기능에 관한 몇 가지 일반적인 질문에 대한 답변입니다.
Java SE(Java Platform, Standard Edition)는 데스크톱과 서버에서 Java 애플리케이션을 개발하고 배포하기 위한 기본 플랫폼입니다.
Java 프로그램이 Java 런타임 외부의 데이터 및 코드와 상호 운용될 수 있게 해주는 미리보기 기능입니다. API를 사용하면 Java 프로그램이 JNI의 경우보다 더 안전하게 네이티브 라이브러리를 호출하고 네이티브 데이터를 처리할 수 있습니다. API는 외부 메모리와 코드에 안전하게 접근하고, 외부 함수를 효율적으로 호출하기 위한 도구입니다.
핵심 측면 중 하나는 코드 검토입니다. AI 코드 검토 도구를 사용하면 이 프로세스에 소요되는 시간을 좀 더 줄일 수 있습니다.
Java의 동적 로딩은 초기 프로그램 시작이 아닌 런타임에 클래스나 리소스를 로드하는 것을 의미합니다.
Java의 구조적 동시성은 멀티스레드 코드의 유지 관리 가능성, 신뢰성 및 관찰 가능성 향상을 목표로 동시 프로세스를 제어된 방식으로 구성하는 접근 방식입니다.
위 내용은 Java 기능: 새로운 LTS 릴리스의 가장 중요한 변경 사항을 자세히 살펴봅니다.의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!