SHOEISHA iD

※旧SEメンバーシップ会員の方は、同じ登録情報(メールアドレス&パスワード)でログインいただけます

CodeZine編集部では、現場で活躍するデベロッパーをスターにするためのカンファレンス「Developers Summit」や、エンジニアの生きざまをブーストするためのイベント「Developers Boost」など、さまざまなカンファレンスを企画・運営しています。

【最新Javaアップデート解説】変更点と過去バージョンからのおさらい

「Java23」で何ができる? Java22以降の変更点と新機能を紹介

第1回:Javaのバージョンアップの大きな流れと主なツールの変更点


  • X ポスト
  • このエントリーをはてなブックマークに追加

コア・ライブラリとツールの主な変更点

 変更の多くは、Project Amber/Panama/Loomに関わるものですが、ここでは、それら以外の主にJava APIのコアライブラリやツールにおける変更点について紹介します。

JEP 458: マルチファイル・ソースコード・プログラムの起動

 Javaでは多くの場合、ビルド(コンパイル)処理をしてから実行するというのが通常でした。しかし、Java11からは単一のソースコードに限り、コンパイルしなくてもそのまま実行する事が可能です。これは、簡単なサンプルコードを実行する場合やちょっとした機能を確認する場合には有用でした。

 しかし、Javaプロジェクトでよくあるクラス毎のファイル構成にしようとすると、結局、ビルドが必要になってしまいます。そこでJava22からは、これらの制限が撤廃され、単一ソースコードに限らず、複数のファイルで構成されているプログラムもコンパイル無しに実行できるようになりました。

 例えば、リスト1のような同等のパッケージ名とクラス名をもつファイルがあるとします。

[リスト1]ファイル構造(src/java/jep458以下のファイル構造)
├── Exec.java
├── Jep458.java
├── main
│   └── Main.java
└── sub
    └── SubTask.java

 そして、パッケージ名を持たないクラスであるリスト2を作成します。

[リスト2]パッケージを持たないクラス例(src/java/jep458/Jep458.java)
class Jep458{
    public static void main(String[] args){
        Exec.run();
    }
}

 同様に、パッケージ名をもつクラスであるリスト3を作ります。

[リスト3]パッケージをもつクラス例(src/java/jep458/main/Main.java)
package main;
class Main{
    public static void main(String[] args) {
        sub.SubTask.exec();
    }
}

 このどちらのファイルを実行する場合でもリスト4のように実行が可能です。

[リスト4]javaの実行例
$java Jep458.java
$java main/Main.java

 もちろん、--class-pathなどのオプションも使えますので、既存のライブラリと合わせても使えます。従って、gradleやmavenのようなビルドツールを導入するほどではないレベルでは、これまでよりも格段にJavaコードの実行が簡単になります。

JEP 467: Markdown形式でのJavaコメント

 Javadocを用いて出力するJavaのコメント記述においてMarkdown形式での記述ができるようになりました。これまでは、リスト5のように/**〜*/でコメントを記述していました。

[リスト5]今までのコメント記述例(src/java/jep467/Main.javaの抜粋)
/**
 *  このプログラムに関するコメントです。
 *  <ul>
 *      <li>リスト1</li>
 *      <li>リスト2</li>
 *  </ul>
 *  @see <a href="https://xxxxxx/yyyy.html">リンク</a>
 */

 これがリスト6のように記述できるようになりました。Markdown形式の場合には///(スラッシュが3つ)で始まるようにしました。

[リスト6]Markdown形式でのコメント記述例(src/java/jep467/MarkdownMain.javaの抜粋)
///
/// このプログラムのMarkdown形式でのコメントです。
///
/// - リスト1
/// - リスト2
///
/// @see <https://xxxxxx/yyyy.html>
/// @see <a href="https://xxxxxx/yyyy.html">リンク</a>
///

 最近では開発者が使うさまざまなツールにおいてMarkdown形式がサポートされていることが多いため、Markdown形式の記述を好む開発者も増えてきたと思います。そのような開発者にとっては良い選択肢ができたと言えます。

 ただし、実際には多くの開発者がIDEなどを使ってプログラムを記述していると思いますので、そちらの対応待ちという事になるのではないかと思います。また、現時点では必ずしも一般的なMarkdown形式で使える記述方法がすべて使えるわけではない点も注意が必要です。

JEP 461,473:Stream処理における中間操作での集約API

 JavaのStream処理において、各要素に対する集約を行うための中間操作が出来るようになりました。Streamは、リスト7のように3つの行程で処理を記述します。

[リスト7]Stream処理の流れ(src/java/jep473/Jep473.javaの抜粋)
var result = Stream.iterate(0, i -> i + 1) // (1) Streamの作成
    .filter(e -> e % 2 == 0).limit(5)   // (2) 中間操作
    .toList(); // (3) 終端操作

 (1)でStreamを作成し、そして、(2)での中間処理。そして、最後に(3)の終端処理です。中間処理では、複数の中間処理を連結して各要素に対して変換や条件などにより抽出などができました。

 そして、今回導入のAPIによって、この中間処理で要素に対して集約の処理が出来るようになりました。例えば、リスト8は指定した要素数毎の集約を行う処理の実装例です。

[リスト8]固定要素数の集約処理(src/java/jep473/Jep473.javaの抜粋)
var result = Stream.of(1,2,3,4,5,6,7,8,9,10)
    .gather(Gatherers.windowFixed(2))
    .toList();
    // [[1, 2], [3, 4], [5, 6], [7, 8], [9, 10]]

 また、指定した要素数毎に1つずつ値をスライドさせていくような集約の実装もあります。(リスト9)

[リスト9]固定要素毎の集約処理(src/java/jep473/Jep473.javaの抜粋)
var result = Stream.of(1,2,3,4,5,6)
    .gather(Gatherers.windowSliding(3))
    .toList();
    // [[1, 2, 3], [2, 3, 4], [3, 4, 5], [4, 5, 6]]

 また、独自に集約処理を実装する場合には、Gathererインターフェースを実装します。リスト10は、Gatherers.windowFixedと同様の処理をする実装コード例です。

[リスト10]集約処理の実装例(src/java/jep473/Jep473.javaの抜粋)
record WindowFixed<TR>(int windowSize)
                implements Gatherer<TR, ArrayList<TR>, List<TR>> {

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

    @Override
    public Supplier<ArrayList<TR>> initializer() {
        // (1) 初期化処理
        return () -> new ArrayList<>(windowSize);
    }

    @Override
    public Integrator<ArrayList<TR>, TR, List<TR>> integrator() {
        // (2) 各要素の処理
        return Gatherer.Integrator.ofGreedy((window, element, downstream) -> {
            //  (3) windowは初期処理(1)のオブジェクト,elementは各要素,downstreamはストリーム
            window.add(element);

            if (window.size() < windowSize) {
                //  サイズが十分でないので、後の処理はしない
                return true;
            }
            // 処理の単位が満たされたので、ストリームに書き出す
            var result = new ArrayList<TR>(window);
            window.clear();
            return downstream.push(result);
        });
    }

    @Override
    public BiConsumer<ArrayList<TR>, Downstream<? super List<TR>>> finisher() {
        //  (4) ストリームの終端処理
        return (window, downstream) -> {
            //  (5) 残ったデータがあれば、それをストリームに書き出す
            if (!downstream.isRejecting() && !window.isEmpty()) {
                downstream.push(new ArrayList<TR>(window));
                window.clear();
            }
        };
    }
}

 まず、大きな流れとして(1)で初期化処理を実装し、(2)で各要素での処理、(4)でストリームの終端となる処理を実装します。

 そして、指定された要素毎のグループに変換するので、(3)処理に必要な各オブジェクトが引数から取得できるので、それらを使って指定したまとまりへと変換します。

 また、最後に残ったデータがある場合には、それを(5)のようにストリームに書き出します。

 この定義を使う場合には、リスト11のようになります。

[リスト11]集約処理の利用例(src/java/jep473/Jep473.javaの抜粋)
var result = Stream.of(1, 2, 3, 4, 5, 6,7)
    .gather(new WindowFixed<>(3))
    .toList();

 また、Gatherer.ofSequential()メソッドを使えば、Gathererインターフェースの定義を別に行わず、先ほどの処理をリスト12のように書き換える事が出来ます。

[リスト12]集約処理のインライン実装例(src/java/jep473/Jep473.javaの抜粋)
var result = Stream.of(1, 2, 3, 4, 5, 6)
    .gather(Gatherer.ofSequential(
        () -> new ArrayList<>(),
        Gatherer.Integrator.ofGreedy((window, element, downstream) -> {
            // (省略)
        }),
        (window, downstream) -> {
            // (省略)
        }))
        .toList();

JEP 457,466:クラスファイル API

 Javaではクラスファイルのバイトコードを解析し、それに応じて任意の処理を挿入、もしくは変更してしまうケースがあります。例えば、特定のメソッドを実行する場合、自動的に何らかの事前処理を強制的に差し込んだり、または実装を他の内容に書き換えたりという事です。

 このAPIはClassなどのリフレクションを用いたものではなく、バイトコード(.classファイルの中身)から構造を解析、変更する為のAPIです。このような事をする場合には、ASMJavaassistなどがありますが、本APIで同様な事ができるようになりました。

 ただし、これらのライブラリを置き換える為のAPIというよりは、これらのライブラリにかかわる開発者に向けたAPIと言えると思います。

 例えば、リスト13のような簡単なクラスを例に説明します。

[リスト13]バイトコードにする前のソースコード(src/java/jep466/Sub.javaの抜粋)
package sub;
public class Sub {
    private static int DEF_VALUE = 10;

    //  常に10を返す
    public int constantValue(){
        return DEF_VALUE;
    }
}

 このクラスから生成したバイトコードを読み込み、フィールドの情報やメソッドの情報を読み取る為のサンプルコードがリスト14です。

[リスト14]バイトコードからの情報の読み取り処理の記述例(src/java/jep466/Main.javaの抜粋)
ClassFile cf = ClassFile.of();

File file = new File("java/jep466/sub/Sub.class");
byte[] fileContent = Files.readAllBytes(file.toPath());

// (1) バイトコード情報を読み取る
ClassModel classModel = cf.parse(fileContent);

// (2) フィールドやメソッドの情報などを取得する
for (FieldModel fm : classModel.fields())
    System.out.printf("Field %s%n", fm.fieldName().stringValue());
for (MethodModel mm : classModel.methods())
    System.out.printf("Method %s%n", mm.methodName().stringValue());

 (1)でバイト情報を読み込み、そして、(2)でフィールドやメソッドの情報を取得しています。また、実行した結果がリスト15です。

[リスト15]実行例
Field DEF_VALUE
Method <init>
Method constantValue
Method <clinit>

 そしてリスト16は、先ほどのSubクラスにおけるconstantValueメソッドの実装部分を変更する場合の実装例です。

[リスト16]バイトコードからメソッド内の処理の書換例(src/java/jep466/Main.javaの抜粋)
// (省略)
// (1) バイトコード情報を読み取る
ClassModel classModel = cf.parse(fileContent);

// (2) クラス構造の読み込みと書換
byte[] newBytes = cf.transform(classModel, (classBuilder, ce) -> {
    if (ce instanceof MethodModel mm) {
        //  (3) メソッドの定義部分
        if(mm.methodName().equalsString("constantValue")){
            //  (4) 変更するメソッドの書換
            classBuilder.transformMethod(mm, (methodBuilder, me)-> {
                if(me instanceof CodeModel cm){
                    // (5) コードを書換
                    methodBuilder.transformCode(cm, (codeBuilder, e) -> {
                        switch (e){
                            case FieldInstruction c -> {
                                //  (6) 定数ではなく、指定した数字を返すように変更する
                                codeBuilder.bipush(15);
                            }
                            default -> {
                                codeBuilder.with(e);
                            }
                        }
                    });
                }
                else{
                    methodBuilder.with(me);
                }
            });
        }
        else{
            classBuilder.with(mm);
        }
    }
    else{
        classBuilder.with(ce);
    }
});

 (1)でバイトコードの内容を読み込みます。そして、(2)でクラス構造の読み込みと書換を行うtransformメソッドを使います。(3)対象となるメソッドの時に、(4)のようにメソッド構造の読み込みと書換を行う処理を実行し、(5)続いてメソッド内の読み込みと書換を行います。

 そして、(6)が本来static fieldのDEF_VALUEを返していたところに15という値を返すように書き換えています。

 今回のコードはあくまでJavaのバイトコードが書き換えできるという一例を示したに過ぎません。実用的なコードではありませんので、ご注意ください。

最後に

 Java22およびJava23はLTS版ではなく、新機能のお試しリリースという位置づけが大きいためPreview版での機能が多くなっています。

 ただし、主にツールに関する部分(javaやjavadocコマンドなど)は正式リリースとなっていて、これらの影響が各関連ツール(IDEなど)に反映されTLS版がリリースされる頃には実際の開発者が触っているツールでも問題なく使えるようになっているはずです。

 次回は、Java開発者にとって最も大きな影響と思われるProject Amber関連の変更点を中心に紹介します。

参考資料

この記事は参考になりましたか?

  • X ポスト
  • このエントリーをはてなブックマークに追加
【最新Javaアップデート解説】変更点と過去バージョンからのおさらい連載記事一覧

もっと読む

この記事の著者

WINGSプロジェクト 小林 昌弘(コバヤシ マサヒロ)

WINGSプロジェクトについて>有限会社 WINGSプロジェクトが運営する、テクニカル執筆コミュニティ(代表 山田祥寛...

※プロフィールは、執筆時点、または直近の記事の寄稿時点での内容です

山田 祥寛(ヤマダ ヨシヒロ)

静岡県榛原町生まれ。一橋大学経済学部卒業後、NECにてシステム企画業務に携わるが、2003年4月に念願かなってフリーライターに転身。Microsoft MVP for Visual Studio and Development Technologies。執筆コミュニティ「WINGSプロジェクト」代表。主な著書に「独習シリーズ(Java・C#・Python・PHP・Ruby・JSP&サーブレットなど)」「速習シリーズ(ASP.NET Core・Vue.js・React・TypeScript・ECMAScript、Laravelなど)」「改訂3版JavaScript本格入門」「これからはじめるReact実践入門」「はじめてのAndroidアプリ開発 Kotlin編 」他、著書多数

※プロフィールは、執筆時点、または直近の記事の寄稿時点での内容です

この記事は参考になりましたか?

この記事をシェア

  • X ポスト
  • このエントリーをはてなブックマークに追加
CodeZine(コードジン)
https://codezine.jp/article/detail/20227 2024/10/17 16:47

おすすめ

アクセスランキング

アクセスランキング

イベント

CodeZine編集部では、現場で活躍するデベロッパーをスターにするためのカンファレンス「Developers Summit」や、エンジニアの生きざまをブーストするためのイベント「Developers Boost」など、さまざまなカンファレンスを企画・運営しています。

新規会員登録無料のご案内

  • ・全ての過去記事が閲覧できます
  • ・会員限定メルマガを受信できます

メールバックナンバー

アクセスランキング

アクセスランキング