Java17→Java21の追加機能紹介

thumbnail

はじめに

こんにちは。DX推進部エンジニアリングGの今泉です。
サントリーウエルネスには今年の8月に入社したばかりになります。 業務では主にビジネス基盤となるプロダクトのマネジメントやフロントエンド開発のリーディングを担当しています(が、今回はJavaがテーマです)

2023/9/19に最新のLTSであるJava21がリリースされました。弊社でもバックエンド開発では主にJavaを利用しています。
今回は直近の最新LTSバージョンであったJava17からの主要な変更点について紹介していきます。

Java21の詳細は以下にまとまっています。

リリースノート

JDK 21 Release Notes

APIドキュメント
Java® Platform, Standard Edition & Java Development Kit Version 21 API Specification

Java17からの変更点

以下にJava17からJava21までに取り込まれたJEPがまとまっています。

JEPs in JDK 21 integrated since JDK 17

JEP400:UTF-8のデフォルト化(Java18)

JEP 400: UTF-8 by Default
Java17以前では、デフォルトの文字セットはJavaランタイムの起動時に決定されており、macOSではUTF-8が指定されるのに対し、 日本語WindowsではShift-JIS(より正確はMS932)で指定されていました。 このため文字セットの指定がない場合、これらの文字セットが利用されていました。
今回の変更により、標準Java APIのデフォルト文字セットがUTF-8となりました。
これにより文字セット指定がない場合のファイルがUTF-8として扱われるようになります。

  • 互換オプション
    今回の変更によって文字データやコメントを日本語で記載しShift-JISでファイルを保存している場合、Java18以降でコンパイルをすると文字化けすることになります。
    これを防ぐにはソースコードをUTF-8に変換するか、Java17以前までの処理とするため、-Dfile.encoding=COMPATをJava実行時のオプションとして追加する必要があります。

JEPより

If an application that has been running for years with windows-31j as the default charset is upgraded to a JDK release that uses UTF-8 as the default charset then it will experience problems when reading files that are encoded in windows-31j. In this case, the application code could be changed to pass the windows-31j charset when opening such files. If the code cannot be changed, then starting the Java runtime with -Dfile.encoding=COMPAT will force the default charset to be windows-31j until the application is updated or the files are converted to UTF-8.

JEP408:シンプルなWEBサーバー(Java18)

JEP 408: Simple Web Server
このJEPでは開発者が簡単にWEBサーバーを起動させるためのCLIツールを提供しています。
あくまでアドホックなコーディングやテストに役立てることを目的としており、プロダクション環境に利用することは想定されていません。  

jwebserverコマンドでWEBサーバーを起動できます。
起動したWEBサーバーは、起動時のカレントディレクトリと子ディレクトリを返却する挙動になります。

MacBook-Pro:bin aXXXXXX$ java --version
openjdk 21 2023-09-19 LTS
OpenJDK Runtime Environment Corretto-21.0.0.35.1 (build 21+35-LTS)
OpenJDK 64-Bit Server VM Corretto-21.0.0.35.1 (build 21+35-LTS, mixed mode, sharing)
MacBook-Pro:bin aXXXXXX$ jwebserver
デフォルトでループバックにバインドします。すべてのインタフェースで"-b 0.0.0.0"または"-b ::"を使用します。
/Users/aXXXXXX/.sdkman/candidates/java/21-amzn/binおよびサブディレクトリを127.0.0.1ポート8000で使用します
URL http://127.0.0.1:8000/
127.0.0.1 - - [15/10月/2023:19:03:58 +0900] "GET / HTTP/1.1" 200 -
127.0.0.1 - - [15/10月/2023:19:03:58 +0900] "GET /favicon.ico HTTP/1.1" 404 -

-hオプションで利用可能な起動オプションを確認できます。

MacBook-Pro:bin aXXXXXX$ jwebserver -h
使用方法: jwebserver [-b bind address] [-p port] [-d directory]
                  [-o none|info|verbose] [-h to show options]
                  [-version to show version information]
オプション:
-b, --bind-address    - バインド先アドレス。デフォルト: 127.0.0.1 (ループバック)                        すべてのインタフェースで"-b 0.0.0.0"または"-b ::"を使用します。
-d, --directory       - 使用するディレクトリ。デフォルト: 現在のディレクトリ。
-o, --output          - 出力形式。none|info|verbose。デフォルト: info。
-p, --port            - リスニングするポート。デフォルト: 8000。
-h, -?, --help        - ヘルプ・メッセージを出力して終了します。
-version, --version   - バージョン情報を出力して終了します。
サーバーを停止するには、[Ctrl]+[C]を押します。

また、APIを利用して起動することもできます。

import java.net.http.SimpleFileServer;
import java.nio.file.Paths;

public class MyWebServer {
    private SimpleFileServer server;

    public void start(int port) throws Exception {
        server = SimpleFileServer.create()
                .bindAddress("localhost", port)
                .directory(Paths.get("./")) 
                .build();
        server.start();
    }

    public void stop() {
        server.stop();
    }
}

従前は以下のようにcom.sun.net.httpserverなどを利用するケースが多かったのではと思いますが幾分か簡単になりました。

import com.sun.net.httpserver.HttpServer;
import com.sun.net.httpserver.HttpHandler;
import com.sun.net.httpserver.HttpExchange;

import java.io.IOException;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.net.InetSocketAddress;

public class HttpServerFileServer {

    public static void main(String[] args) throws IOException {
        HttpServer server = HttpServer.create(new InetSocketAddress(8080), 0);
        server.createContext("/", new StaticFileHandler());
        server.start();

        System.out.println("Server started at http://localhost:8080/");
    }

    static class StaticFileHandler implements HttpHandler {
        @Override
        public void handle(HttpExchange exchange) throws IOException {
            String requestedFile = exchange.getRequestURI().getPath();
            if (requestedFile.equals("/")) {
                requestedFile = "/index.html"; 
            }

            Path filePath = Paths.get("." + requestedFile);
            if (Files.exists(filePath)) {
                byte[] response = Files.readAllBytes(filePath);
                exchange.sendResponseHeaders(200, response.length);
                try (OutputStream os = exchange.getResponseBody()) {
                    os.write(response);
                }
            }
        }
    }
}

次はJEP408とJunitを利用したテストコードのサンプルです。
(MyWebServerは上記SimpleFileServerを利用したコードを利用)

import org.junit.jupiter.api.*;

import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;

import static org.junit.jupiter.api.Assertions.*;

class MyWebServerTest {
    private static MyWebServer webServer;
    private static final int PORT = 8081;
    private HttpClient httpClient;

    @BeforeAll
    static void setUpServer() throws Exception {
        webServer = new MyWebServer();
        webServer.start(PORT);
    }

    @AfterAll
    static void tearDownServer() {
        webServer.stop();
    }

    @BeforeEach
    void setUp() {
        httpClient = HttpClient.newHttpClient();
    }

    @Test
    void testServerResponds() throws Exception {
        HttpRequest request = HttpRequest.newBuilder()
                .uri(new URI("http://localhost:" + PORT + "/index.html"))
                .build();
        HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());

        assertEquals(200, response.statusCode(), "Expected a 200 OK response");
        assertTrue(response.body().contains("Hello, World!"), "Expected to find 'Hello, World!' in the response");
    }
}

JEP431:順序付きのコレクション(Java21)

JEP 431: Sequenced Collections
順序付きのコレクションに新しいインターフェイスが追加されました。
従前コレクションフレームワークには順序が規定された要素のシーケンスを表すコレクション型がありませんでした。
このため、コレクション全体に適用される操作が統一されておらず、一貫性が欠落していました。このJEPはその解決をするものになります。

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

こんな感じで使えます。

import java.util.*;

class Main{
    public static void main (String[] args){
        SequencedCollection<String> set = List.of("セサミン","オメガエイト","ロコモア");
        System.out.println(set.getLast());
        System.out.println(set.reversed());
        set = new LinkedHashSet<>();
        set.addAll(List.of("セサミン","オメガエイト","ロコモア"));
        System.out.println(set.getLast());
        System.out.println(set.reversed());
        //ロコモア
        //[ロコモア, オメガエイト, セサミン]
        //ロコモア
        //[ロコモア, オメガエイト, セサミン]
    }
}

LinkedHashSetのオブジェクトを逆順に並べ替えるには逆順にイテレートする処理を実装したり、Listに変換したりする必要がありましたが、同じように扱うことができます。 競プロなんかで便利そう。

JEP444:仮想スレッド(Java21)

JEP 444: Virtual Threads
Java19でプレビュー機能として提案された仮想スレッドが正式機能となりました。
仮想スレッドは軽量スレッドとも呼ばれます。
伝統的なスレッドすなわちプラットフォームスレッドはOSスレッドのラッパーとして実装されており、スレッドの作成や削除、スケジューリングはOSで行われます。
このため、高いオーバーヘッドが生じることがあります。
一方、仮想スレッドはJVM上で動作し、複数の仮想スレッドがOSスレッドを共有するよう設計されています。
ランタイムにおいてOSスレッドの共有することでブロッキングのコストが(ほぼ)不要になります。
このメリットにより、最低限のオーバーヘッドで動作するため、膨大な数の仮想スレッドを生成することが可能になります。
ただし、注意点として、仮想スレッドは必ずしも高速とは言えないことが挙げられます。
前述したように、ブロッキングのコストを削減することでスループットが向上するため、I/Oバウンドのタスクや高度な並行処理を行う必要がある場面で有効です。
しかしプラットフォームスレッドと比較して計算速度やMIPSが向上するわけではありません。
注目すべきアップデートですが、使用方法は通常のスレッドと大差ないため本記事では割愛します。

JEP440:レコードパターン(Java21)

JEP 440: Record Patterns
パターンマッチングを利用してRecordクラスの分解を可能とする機能です。
Java19でJEP405のプレビュー機能として提供されたものが正式に組み込まれたものになります。

以下は使用例です。

class Main {
    public record Supplement(
        String name,
        String brand,
        String type,  
        int servingsPerDay
    ) { }

    public static String getSupplementInfo(Object obj) {
        return (obj instanceof Supplement (String name,String brand,String type,int servingsPerDay)) 
            ? """
                This supplement is %s from %s. It's a type of %s with recommended %d servings per day.
                """.formatted(name,brand,type,servingsPerDay)
            : "Not a valid supplement.";
    }

    public static void main(String[] args) {
        Supplement sesaex = new Supplement("セサミンEX", "SUNTORY WELLNESS", "vitamin", 3);
        System.out.println(getSupplementInfo(sesaex));

        String testString = "Just a string";
        System.out.println(getSupplementInfo(testString)); 
    }
}

JEP441:switch文におけるパターンマッチング(Java21)

JEP 441: Pattern Matching for switch
switch文でパターンマッチが利用できるようになりました。
従前は次のようなswitch文を書かねばならず、その時間計算量はO(N)でした。

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

しかしswitch文に型と変数を書くことが可能になったため、O(1)で処理できる可能性が高まりました。
本機能はレコードパターンと併用することで特に強みを発揮しますが、詳しい日本語での解説はきしださんの記事が大変参考になります。

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

おわりに

いかがでしたでしょうか? Java8のような革新的な変更ありませんが、重要な機能追加がいくつもあったリリースだったと思います。
Java18でFinalizeが非推奨となっておりますが元々使うべきではないメソッドですし、Java21で廃止になったAPIもまず使われることがないと思われるため、
文字コードの問題がなければJava21へのアップデートを検討してみてもよいかもしれません。
私はJava8を最後にしばらくJavaから離れており、サントリーへの入社を機に再びJavaを業務で利用するようになりましたが、JEPを読む中ですこしずつJavaも改良されてきている様子が窺えて面白かったです。
個人的には次のLTSで今回プレビュー機能として追加されたJEP445:Unnamed Classes and Instance Main Methodsの正式な機能追加あたりに期待したいです。
最後までお読みいただきありがとうございました。