2025/12/25

レコードクラスの中身

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

本エントリーはJava Advent Calendar 2025の最終日です! 昨日は@zoosm3さんのDBFluteの複数DBをSpring Bootで実装する 開発のヒント でした。

 

24日に会社ブログでレコードクラスについて書きました。けっこうまじめに書いたので、ぜひ読んでみてください!

[Java] 今から始めるレコードクラス
https://bsblog.casareal.co.jp/archives/13817

レコードクラスの書き方や使い方については書きましたけど、じゃあレコードクラスって実際にはどういうバイトコードになっているの??というのが本エントリーです。

レコードクラスは簡単な記述で済んでいるのですが、コンパイルするとアクセッサーメソッドやequalsメソッドが自動生成されますとよく書かれていますが(会社ブログでもそう書きました)、実際のところどうなっているのというのを紹介していきます。

 

とりあえず、逆コンパイル

では、レコードクラスのバイトコードがどうなっているのか、さっそく調べてみましょう。題材にするのは、よく出てくる座標を表すPointレコードクラスです。

 

public record Point(double x, double y) {}

 

このクラスをコンパイルした後に、javapで逆コンパイルします。

 

> javac -g Point.java
> javap -p -v Point
Classfile /C:/temp/Point.class
  Last modified 2025/12/21; size 1348 bytes
  SHA-256 checksum 4bcbbc4b22a7b6c6fb72067dba973f9f08f80def796ecca1173c7c3b6596bda4
    Compiled from "Point.java"
public final class Point extends java.lang.Record
     ... 以下、略

 

javapのオプションの-pはprivateも逆コンパイルするというオプションで、-vは詳細情報を出力するオプションです。

この後にコンスタントプールが続くのですが、とりあえず重要なところで

 

public final class Point extends java.lang.Record

 

レコードクラスは継承ができないのですが、この行が理由です。

まず、java.lang.Recordクラスのサブクラスになるということ。そして、finalクラスだということです。

Recordクラスは以下の3種類の抽象メソッドを定義しています。

  • equals
  • hashCode
  • toString

もちろん、この3つのメソッドはObjectクラスで具象メソッドとして定義されているのですが、Recordクラスではそれらを抽象クラスとしてオーバーライドしています。

つまり、Ojbectクラスの実装は使わないということですね。

しかし、recordキーワードで定義されるレコードクラスは、これらのメソッドを定義する必要がありません。これらのメソッドがコンパイル時に自動生成されるというのが、このことからも分かります。

 

レコードコンポーネント

javapでは、クラス定義の後にコンスタントプールを出力します。コンスタントプールは必要に応じて参照すればよいので、その後に続く部分を見てみましょう。

通常のクラスであればフィールドの定義が続きます。レコードクラスではどうでしょう?

 

  private final double x;
    descriptor: D
    flags: (0x0012) ACC_PRIVATE, ACC_FINAL

  private final double y;
    descriptor: D
    flags: (0x0012) ACC_PRIVATE, ACC_FINAL

 

名前だけはレコードコンポーネントになりましたが、実質的にはインスタンスフィールドと同じでした。まぁ、想像通りですね。

イミュータブルなので、finalで宣言されているところが注目すべき点です。

 

コンストラクター

レコードクラスではカノニカルコンストラクターが自動的に生成されます。と言われても、カノニカルって何?という感じですよね。

カノニカル(Canonical)は「正規の」とか「標準的な」などの意味の単語です。このため、カノニカルコンストラクターを標準コンストラクターと記述しているドキュメントもあります。でも、標準と書かれるとStandardの方をイメージしてしまうんですよね。

ネイティブの人たちはCanonicalとStandardのニュアンスの違いを分かっているのでしょうが、私には理解できないのです...

 

それはそれとして、カノニカルコンストラクターはすべてのレコードコンポーネントを初期化するためのコンストラクターです。

では、Pointレコードクラスのカノニカルコンストラクターを見てみましょう。

 

  public Point(double, double);
    descriptor: (DD)V
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=3, locals=5, args_size=3
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Record."<init>":()V
         4: aload_0
         5: dload_1
         6: putfield      #7                  // Field x:D
         9: aload_0
        10: dload_3
        11: putfield      #13                 // Field y:D
        14: return
      LineNumberTable:
        line 1: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      15     0  this   LPoint;
            0      15     1     x   D
            0      15     3     y   D
    MethodParameters:
      Name                           Flags
      x
      y

 

カノニカルコンストラクターの引数は、レコードクラスの定義におけるレコードコンポーネントの並びに対応しています。

Pointレコードクラスの場合、両方ともdoubleなので区別しにくいですが、第1引数がx、第2引数がyです。

 

一般的にコンストラクターのバイトコードでは、まずスーパークラスのデフォルトコンストラクターをコールします。それが0行目と1行目のinvokespecialです(行と書いていますが、実際にバイト数のことです。あしからず)。

4行目のaloadからがレコードコンポーネントの初期化になります。6行目のputfieldで引数のxの値をフィールドにセットしています。

同様に、11行目のputfieldで引数のyの値をフィールドにセットしています。

単純に引数の値をフィールド(レコードコンポーネント)に代入しているだけですね。

 

カノニカルコンストラクターは独自に定義することもできるのですが、それについては後述します。

 

アクセッサーメソッド

レコードクラスではレコードコンポーネントと同名のメソッドが生成され、レコードコンポーネントの値を取得できます。

まぁ、名前は違うもののgetterメソッドと同じですね。Pointレコードクラスのx()メソッドのバイトコードは以下のようになっていました。

 

  public double x();
    descriptor: ()D
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=2, locals=1, args_size=1
         0: aload_0
         1: getfield      #7                  // Field x:D
         4: dreturn
      LineNumberTable:
        line 1: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   LPoint;

 

getfieldでフィールド(レコードコンポーネント)の値をスタックに積んで、dreturn (doubleのretuen)で返り値にしています。

y()メソッドも同様です。

 

他の自動生成されたメソッド

ここまでは、まぁ予想通りのバイトコードでした。

レコードクラスではカノニカルコンストラクターとアクセッサーメソッド以外に、次のメソッドを自動生成します。

  • equals
  • hashCode
  • toString

ここではequals()メソッドのバイトコードを見てみましょう。

equals()メソッドの書き方としては、Effective Javaが詳しいですね。Effective Javaの書き方と比べてどうなっているでしょう?

 

  public final boolean equals(java.lang.Object);
    descriptor: (Ljava/lang/Object;)Z
    flags: (0x0011) ACC_PUBLIC, ACC_FINAL
    Code:
      stack=2, locals=2, args_size=2
         0: aload_0
         1: aload_1
         2: invokedynamic #24,  0             // InvokeDynamic #0:equals:(LPoint;Ljava/lang/Object;)Z
         7: ireturn
      LineNumberTable:
        line 1: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       8     0  this   LPoint;
            0       8     1     o   Ljava/lang/Object;

 

なんか全然違う!!

おもしろいのは、invokedynamic (indy)を使っているところです。つまり、equals()メソッドは初回実行時に動的に作られるということですね。

このindyの初回実行時にコールされるのが、ブートストラップと呼ばれるメソッドです。ブートストラップメソッドはクラスファイルの最後の方にあるBootstrapMethods:の箇所に記述されています。

 

BootstrapMethods:
  0: #49 REF_invokeStatic java/lang/runtime/ObjectMethods.bootstrap:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/TypeDescriptor;Ljava/lang/Class;Ljava/lang/String;[Ljava/lang/invoke/MethodHandle;)Ljava/lang/Object;
    Method arguments:
      #8 Point
      #45 x;y
      #47 REF_getField Point.x:D
      #48 REF_getField Point.y:D

 

これを見ると、ブートストラップとして使われるのは、java.lang.runtime.ObjectMethodsクラスのbootstrap()メソッドだということです。実をいうと、equals()メソッドだけでなく、hashCode()メソッドもtoString()メソッドも同じブートストラップメソッドが使われています。

このObjectMethodsクラスはpublicなクラスなので、APIドキュメントが公開されています。Java 24であれば、以下のリンクから参照できます。

ObjectMethodsクラス
https://docs.oracle.com/javase/jp/24/docs/api/java.base/java/lang/runtime/ObjectMethods.html

ちょっとおもしろいのが、bootstrap()メソッドの最後の引数にアクセッサーメソッドのMethodHandleが使われているところですね。

 

では、ObjectMethodsクラスのbootstrap()メソッドを調べてみましょう。ソースは以下のリンクにあります。

ObjectMethods
https://github.com/openjdk/jdk/blob/master/src/java.base/share/classes/java/lang/runtime/ObjectMethods.java

以下にObjectMethodsクラスのbootstrap()メソッドのソースを示します。

 

    public static Object bootstrap(MethodHandles.Lookup lookup, String methodName, TypeDescriptor type,
                                   Class<?> recordClass,
                                   String names,
                                   MethodHandle... getters) throws Throwable {

            <<省略>>
    
        List<MethodHandle> getterList = List.of(getters);
    
        MethodHandle handle = switch (methodName) {
            case "equals"   -> {
                if (methodType != null && !methodType.equals(MethodType.methodType(boolean.class, recordClass, Object.class)))
                    throw new IllegalArgumentException("Bad method type: " + methodType);

                yield makeEquals(lookup, recordClass, getterList);
            }

              <<省略>>
        };
 
        return methodType != null ? new ConstantCallSite(handle) : handle;
    }

 

メソッド名が"equals"であれば、makeEquals()メソッドをコールしています。

makeEquals()メソッドはClassfile APIでバイトコードを操作しています。ASMの頃に比べると、Classfile APIになって格段に読みやすくなりましたね。

ちょっと長いのですが、makeEquals()メソッドを以下に示しておきます。

 

    private static MethodHandle makeEquals(MethodHandles.Lookup lookup, Class<?> receiverClass,
                                           List<MethodHandle> getters) throws Throwable {
        MethodType rr = MethodType.methodType(boolean.class, receiverClass, receiverClass);
        MethodType ro = MethodType.methodType(boolean.class, receiverClass, Object.class);
        MethodHandle instanceFalse = MethodHandles.dropArguments(FALSE, 0, receiverClass, Object.class); // (RO)Z
        MethodHandle instanceTrue = MethodHandles.dropArguments(TRUE, 0, receiverClass, Object.class); // (RO)Z
        MethodHandle isSameObject = OBJECT_EQ.asType(ro); // (RO)Z
        MethodHandle isInstance = MethodHandles.dropArguments(CLASS_IS_INSTANCE.bindTo(receiverClass), 0, receiverClass); // (RO)Z
        MethodHandle accumulator = MethodHandles.dropArguments(TRUE, 0, receiverClass, receiverClass); // (RR)Z

        int size = getters.size();
        MethodHandle[] equalators = new MethodHandle[size];
        boolean hasPolymorphism = false;
        for (int i = 0; i < size; i++) {
            var getter = getters.get(i);
            var type = getter.type().returnType();
            if (isMonomorphic(type)) {
                equalators[i] = equalator(lookup, type);
            } else {
                hasPolymorphism = true;
            }
        }

        // Currently, hotspot does not support polymorphic inlining.
        // As a result, if we have a MethodHandle to Object.equals,
        // it does not enjoy separate profiles like individual invokevirtuals,
        // and we must spin bytecode to accomplish separate profiling.
        if (hasPolymorphism) {
            String[] names = new String[size];

            var classFileContext = ClassFile.of(ClassFile.ClassHierarchyResolverOption.of(ClassHierarchyResolver.ofClassLoading(lookup)));
            var bytes = classFileContext.build(ClassDesc.of(specializerClassName(lookup.lookupClass(), "Equalator")), clb -> {
                for (int i = 0; i < size; i++) {
                    if (equalators[i] == null) {
                        var name = "equalator".concat(Integer.toString(i));
                        names[i] = name;
                        var type = getters.get(i).type().returnType();
                        boolean isInterface = type.isInterface();
                        var typeDesc = type.describeConstable().orElseThrow();
                        clb.withMethodBody(name, MethodTypeDesc.of(CD_boolean, typeDesc, typeDesc), ACC_STATIC, cob -> {
                            var nonNullPath = cob.newLabel();
                            var fail = cob.newLabel();
                            cob.aload(0)
                               .ifnonnull(nonNullPath)
                               .aload(1)
                               .ifnonnull(fail)
                               .iconst_1() // arg0 null, arg1 null
                               .ireturn()
                               .labelBinding(fail)
                               .iconst_0() // arg0 null, arg1 non-null
                               .ireturn()
                               .labelBinding(nonNullPath)
                               .aload(0) // arg0.equals(arg1) - bytecode subject to customized profiling
                               .aload(1)
                               .invoke(isInterface ? Opcode.INVOKEINTERFACE : Opcode.INVOKEVIRTUAL, typeDesc, "equals", MTD_OBJECT_BOOLEAN, isInterface)
                               .ireturn();
                        });
                    }
                }
            });

            var specializerLookup = lookup.defineHiddenClass(bytes, true, MethodHandles.Lookup.ClassOption.STRONG);

            for (int i = 0; i < size; i++) {
                if (equalators[i] == null) {
                    var type = getters.get(i).type().returnType();
                    equalators[i] = specializerLookup.findStatic(specializerLookup.lookupClass(), names[i], MethodType.methodType(boolean.class, type, type));
                }
            }
        }

        for (int i = 0; i < size; i++) {
            var getter = getters.get(i);
            MethodHandle equalator = equalators[i]; // (TT)Z
            MethodHandle thisFieldEqual = MethodHandles.filterArguments(equalator, 0, getter, getter); // (RR)Z
            accumulator = MethodHandles.guardWithTest(thisFieldEqual, accumulator, instanceFalse.asType(rr));
        }

        return MethodHandles.guardWithTest(isSameObject,
                                           instanceTrue,
                                           MethodHandles.guardWithTest(isInstance, accumulator.asType(ro), instanceFalse));
    }

 

簡単にいうと、まずレコードコンポーネントごとにその型に応じたequals()メソッドを探します。次に、レコードコンポーネントごとにequals()メソッドをコールするメソッドを持つクラスを動的に作成して、そのメソッドのMethodHandleを作っています。

なかなかおもしろいですね。

 

Classfile APIのビルド系のメソッドはバイトコードと対応したメソッド名になっているので、バイトコードを読めればだいたい分かるはずです。

 

というわけで、equals()メソッド、hashCode()メソッド、toString()メソッドは中身が動的に作成されるのでした。

 

カノニカルコンストラクター再び

最後に、もう一度カノニカルコンストラクターについて。

前述したカノニカルコンストラクターは自動生成されたものでしたが、カノニカルコンストラクターは自分で書くこともできます。

たとえば、範囲を示すRangeレコードクラスで下限が上限を超える場合IllegalArgumentException例外をスローするようなカノニカルコンストラクターを記述してみます。

 

public record Range(double min, double max) {
    public Range {
        if (min > max) {
            throw new IllegalArgumentException();
        }
    }
}

 

カノニカルコンストラクターは引数がレコードコンポーネントの宣言部分と同じなので省略した書き方になります。

そして、コンパイルした後のバイトコードが以下になります。

 

  public Range(double, double);
    descriptor: (DD)V
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=4, locals=5, args_size=3
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Record."<init>":()V
         4: dload_1
         5: dload_3
         6: dcmpl
         7: ifle          18
        10: new           #7                  // class java/lang/IllegalArgumentException
        13: dup
        14: invokespecial #9                  // Method java/lang/IllegalArgumentException."<init>":()V
        17: athrow
        18: aload_0
        19: dload_1
        20: putfield      #10                 // Field min:D
        23: aload_0
        24: dload_3
        25: putfield      #16                 // Field max:D
        28: return
      LineNumberTable:
        line 2: 0
        line 3: 4
        line 4: 10
        line 2: 18
        line 6: 28
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      29     0  this   LRange;
            0      29     1   min   D
            0      29     3   max   D
      StackMapTable: number_of_entries = 1
        frame_type = 255 /* full_frame */
          offset_delta = 18
          locals = [ class Range, double, double ]
          stack = []
    MethodParameters:
      Name                           Flags
      min                            mandated
      max                            mandated

 

前半にminとmaxの比較を行って、minが大きければ例外をスローする処理が記述されています。18行目からコンストラクターの引数をフィールド(レコードコンポーネント)に代入する処理です。

フィールドに代入する処理から行われると思っていたら、それは最後なんですね。ちょっと意外でした。

 

まとめ

レコードクラスはデータを扱うのに便利なクラスですが、その中身がどうなっているのかを紹介しました。

 

まとめてみると

  • レコードクラスはRecordクラスのサブクラスでfinalクラス
  • レコードコンポーネントはfinalなインスタンスフィールド
  • カノニカルコンストラクターが生成される
  • アクセッサーメソッドはgetter相当
  • equals(), hashCode(), toString()は実行時に動的に作成
  • カノニカルコンストラクターでフィールドへの代入は最後

 

レコードクラス自体は単純ですが、その裏側はなかなかおもしろかったですね。

OpenJDKのソースを読むのはたいへんですが、このぐらいの小さいところから読み始めるというのはいいかもしれません。

2025/12/06

Lazy Constant 実装編

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

本エントリーはJava Advent Calendar 2025の6日目です。

 

一昨日はLazy Constantの使い方を紹介したので、今日はLazy Constantがどうやって実装されているかを紹介していきます。

 

Lazy Constant
https://www.javainthebox.com/2025/12/lazy-constant.html

 

シングルトン

いきなりシングルトンと言われても... という感じだとは思いますが、Lazy Constantの実装を紐解く前に、シングルトンについて考えてみます。

シングルトンはデザインパターンの1つで、インスタンスを1つに制限するためのパターンです。

シングルトンは一種の大域変数になってしまうので、使いすぎるのはよくないですし、最近はほとんど使われなくなったように思います。でも、その実装はLazy Constantにつながるのです。

そもそもシングルトンの実装ってどうなっていたか覚えていらっしゃいますでしょうか?

 

シンプルなシングルトン

まずはシンプルなシングルトンの実装です。

シングルトンは自身のインスタンスをstaticフィールドで1つ保持します。そして、インスタンスを取得するメソッドが初めてコールされた時に、インスタンスを生成します。インスタンスが存在すれば、それを返します。つまり、遅延初期化なのです。

 

public class Singleton {
    private static Singleton instance;

    private Singleton() {}

    public static Singleton get() {
        if (instance == null) {
            instance = new Singleton();
        }

        return instance;
    }
}

 

インスタンスを勝手に作られないように、コンストラクターはprivateで宣言します。

getメソッド(getInstanceメソッドのことも多いですが、ここではgetメソッドにします)では、staticフィールドのinstanceがnullだったら、つまり初期化されていなければSingletonインスタンスを生成します。そして、そのinstanceを返します。

シングルトンにデータを持たせるのであれば、それなりにフィールドなどを定義しますが、インスタンスを1つに限定させるための実装としてはこれでOKです。

しかも、実際にシングルトンのインスタンスを実際に使う時まで、その初期化を遅らせることができます。

しかし、問題もあります。この実装はスレッドセーフではないという点です。シングルスレッドであればいいのですが、マルチスレッドでは使えません。

 

スレッドセーフなシングルトン

スレッドセーフにするにはどうすればよいでしょう?

もっとも単純なのは、getメソッドをsynchronizedにするという方法です。

 

public class Singleton {
    private static Singleton instance;

    private Singleton() {}

    public synchronized static Singleton get() {
        if (instance == null) {
            instance = new Singleton();
        }

        return instance;
    }
}

 

これでマルチスレッドでも動作します。

問題はスケールしないということです。スレッド数が2, 3であればよいのですが、スレッド数が増えるととたんにロック待ちで渋滞してしまいます。

instanceフィールドは初期化した後は参照されるだけなので、本来であれば同期化は必要ありません。問題は未初期化の状態時に複数スレッドからgetメソッドをコールされる場合です。

 

そこで、getメソッドのインスタンス生成のところだけ同期化することを考えてみます。

 

    public static Singleton get() {
        if (instance == null) {
            // NG これはダメな実装
            synchronized(instance) {
                instance = new Singleton();
            }
        }

        return instance;
    }
}

 

しかし、これではダメなのです。

CPUの効率化のために命令を入れ替えたり、キャッシュにinstanceの値が残っていることもあるので、instanceを初期化しているときに複数のスレッドからアクセスされてしまうことがあります。

そこで、出てくるのがダブルチェックロックという方法です。でも、単にダブルチェックロックだけでは、キャッシュ上のinstanceとメモリのinstanceが同期化されているか保証されません。

そこで、instanceフィールドをアトミックに処理されるようにします。

厳密にやるのであればjava.util.concurrent.atomic.AtomicReferenceクラスを使用しますが、volatileでも大丈夫です。

最終的には次のようになります。

 

public class Singleton {
    // volatileで宣言することによりアトミック性を保証する
    private static volatile Singleton instance;

    private Singleton() {}

    public static Singleton get() {
        if (instance == null) {
            synchronized (Singleton.class) {
                // ダブルチェック
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }

        return instance;
    }
}

 

これでマルチスレッドでスケールするスレッドセーフなシングルトンになります。

 

Lazy Constant

シングルトンが理解できたとして、Lazy Constantの実装です。

java.lang.LazyConstantはインタフェースなので、実際の遅延初期化の部分は実装されていません。どこで実装されているかというと、LazyConstantインタフェースのstaticメソッドのofメソッドを見れば分かります。

 

    static <T> LazyConstant<T> of(Supplier<? extends T> computingFunction) {
        Objects.requireNonNull(computingFunction);
        if (computingFunction instanceof LazyConstant<? extends T> lc) {
            return (LazyConstant<T>) lc;
        }
        return LazyConstantImpl.ofLazy(computingFunction);
    }

 

return文のところを見ると、LazyConstantImplクラスというのがあることがわかります。これがLazyConstantインタフェースを実装したコンクリートクラスです。パッケージはjdk.internal.langパッケージで、公開されていないクラスだということが分かります。

 

では、遅延初期化を行うgetメソッドを見る前に、クラスの宣言と関連するフィールド、そしてofLazyメソッドを見ておきましょう。この他にもフィールドありますが、関連するところだけ。

 

@AOTSafeClassInitializer
public final class LazyConstantImpl<T> implements LazyConstant<T> {

    @Stable
    private T constant;

    @Stable
    private volatile Supplier<? extends T> computingFunction;

    private LazyConstantImpl(Supplier<? extends T> computingFunction) {
        this.computingFunction = computingFunction;
    }

    public static <T> LazyConstantImpl<T> ofLazy(Supplier<? extends T> computingFunction) {
        return new LazyConstantImpl<>(computingFunction);
    }

 

見慣れないアノテーションが使われていますが、Leyden関連や最適化のヒントになるアノテーションです。

ここで注目しておいていただきたいのが、値を保持するconstantフィールドがvolatileではないということです。そして、ofメソッドの引数のラムダ式はcomputingFunctionフィールドで保持しています。

 

では、保持している値を返すgetメソッドを見ていきましょう。

 

    @ForceInline
    @Override
    public T get() {
        final T t = getAcquire();
        return (t != null) ? t : getSlowPath();
    }

    private T getSlowPath() {
        preventReentry();
        synchronized (this) {
            T t = getAcquire();
            if (t == null) {
                t = computingFunction.get();
                Objects.requireNonNull(t);
                setRelease(t);
                // Allow the underlying supplier to be collected after successful use
                computingFunction = null;
            }
            return t;
        }
    }

 

getメソッドの最初に出てくるgetAcquireメソッドはconstantフィールドを取得するメソッドです。このメソッドについては後でもう一度触れます。

getAcquireメソッドでconstantフィールドをローカル変数のtに代入しています。続いて、tがnullでなければ、tをそのまま返しています。逆に、tがnullならばgetSlowPathメソッドをコールしています。

getSlowPathメソッドの先頭でpreventReentryメソッドをコールしていますが、これはロックを取得している状態で再びロックを取得する(これを再入すると呼びます)ことを防ぐメソッドです。Javaのsynchronizedは再入が可能なロックなのですが、ここではそれを防いでいるということです。

そして、synchronizedでロックを取得し、再びtを取得してnullかどうかをチェックしています。つまりダブルチェックになっているということです。

ダブルチェックをしてtがnullの場合、computingFunctionフィールドに対してgetメソッドをコールしています。これがSupplierのラムダ式の実行を意味しています

つまり、ここで値の初期化を行っています。

初期化した値がnullの場合はrequireNonNullメソッドでチェックしてNullPointerException例外をスローします。これが、前回のエントリーで例外を扱う場合の1の選択肢(ラムダ式でnullを返す)時の挙動になります。

次のsetReleaseメソッドはgetAcquireメソッドの逆です。

そして、computingFunctionにnullを代入しています。つまり、一度Supplierのラムダ式が実行されたら、その後はラムダ式を実行することができないということです。

このようにして、ダブルチェックロックを使って値の初期化を行っています。

しかし、気になるのは、シングルトンでは遅延初期化するフィールドがvolatileだったのにLazyConstantImplクラスではvolatileではないという点です。

この問題はgetAcquireメソッドを見てみれば、理由が分かります。

 

    @SuppressWarnings("unchecked")
    @ForceInline
    private T getAcquire() {
        return (T) UNSAFE.getReferenceAcquire(this, CONSTANT_OFFSET);
    }

 

ここで使われているUNSAFE変数は、一般には使わないようにと言われている危険なjdk.internal.misc.Unsafeクラスです。標準ライブラリだからこそ、使えるということですね。

そして、UnsafeクラスのgetReferenceAcquireメソッドを見てみると...

 

    @IntrinsicCandidate
    public final Object getReferenceAcquire(Object o, long offset) {
        return getReferenceVolatile(o, offset);
    }

 

なんと参照をvolatileで取得するメソッドをコールしていました。

つまり、LazyConstantImplクラスのconstantフィールドはvolatileで定義されてはいないものの、アクセスする時はvolatile相当で行われるということです。

これがconstantフィールドがvolatileで定義されていない理由になります。まぁ、普通にはできない技ですね(そもそもUnsafeクラスは使えないですし)。

 

もう1つ標準ライブラリだからこその技が最適化のヒントとなるアノテーションです。

たとえば、@ForceInlineアノテーションはメソッドのインライン化を行わせるアノテーションです。また、@Stableアノテーションは値が変更されないことを保証して、値を埋め込むなどの最適化を可能にしています。

このような最適化に対するアノテーションを使うことで、実行時最適化をやりやすくしているわけです。

 

まとめ

finalフィールドの遅延初期化を行うLazy Constantの実装を見てきました。

遅延初期化で使われている手法は、シングルトンで使われていたvolatileとダブルチェックロックです。この手法を使う場面というのはなかなかないとは思いますが、知識として知っておくのはよいですね。

そもそも、Lazy Constant自体がそれほど頻繁に使われるAPIではありません。しかし、もしイミュータブルなクラスで遅延初期化をしなければならないような場合はぜひ思い出してやってください。

2025/12/04

Lazy Constant

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

本エントリーはJava Advent Calendar 2025の4日目です。昨日はAsanoさん (@mackey0225) の読書感想文 : 『Javaの10年』でした。

 

11月15日に開催されたJJUG CCC 2025 FallでLazy Constantについてプレゼンしてきました。資料はこちら。

 

Lazy Constantはfinalフィールドを遅延初期化させるためのAPIで、Java 26ではJEP 526で提案されています。

 

JEP 526: Lazy Constants (Second Preview)

https://openjdk.org/jeps/526

 

LazyConstantインタフェース自体は単機能で使い方も簡単なのですが、その導入背景は理解しておいた方がよいと思います。

 

なぜLazy Constant?

Javaでも宣言的なプログラミングスタイルが増えてきたり、並列処理が当たり前に使われるようになってきて、イミュータブル性の重要度が増しています。

また、アーキテクチャー的にもDDDの導入で値クラスを使うことが多くなっています。もちろん、値クラスはイミュータブルです。

 

そこで、イミュータブルなクラスを作ることを考えるわけですが、イミュータブルなクラスの条件の1つにフィールドはすべてfinalにするということがあります。

ここで困るのが、フィールドによっては初期化に時間がかかるものがあることです。たとえば、通信やファイル読み込みなどの外部リソースにアクセスする場合などがこれに相当します。

時間のかかるフィールドの初期化がアプリケーションの起動時にまとまって発生してしまうと、ただでさえいろいろやらなければならない起動時ですが、このようなフィールの初期化のためにさらに起動時間がかかるということになってしまいます。

通常のフィールドであれば、実際にフィールドを使用する時まで初期化を遅らせることができます。しかし、finalフィールドはオブジェクトの生成時にしか初期化することができません。

そこで登場するのがfinalフィールドの遅延初期化をサポートするLazy Constantです。

 

Lazy Constantとは

Lazy Constantは値を1つだけ保持するコンテナのようなものです。

そして、保持する値の初期化は実際に使用する時まで遅延させます。もちろん、スレッドセーフなので並列処理でも使えます。

値の初期化を行うにはSupplierインタフェースを使用します。ようするに、引数なし、戻り値ありのラムダ式ですね。

重要なことが、JVMの最適化を享受しやすい実装になっていることです。LazyConstantオブジェクトから値を取得するときにはgetメソッドを使用する必要があり、しかもgetメソッド内では値が初期化されたかどうかをチェックする必要があります。これらのオーバーヘッドがあるにも関わらず、インライン化などの最適化を行いやすくなっており、最適化後は直接変数にアクセスするのとパフォーマンスが変わらなくなります。

では、このような特徴を持つLazyConstantを使ってみましょう。

 

LazyConstantの使い方

LazyConstantはインタフェースで、パッケージはjava.langです。ですから、import文は必要ありません。

メソッドは4つだけ。しかも、ほぼofメソッドとgetメソッドしか使いません。

  • static LazyConstant<T> of(Supplier<T> computingFunction) : LazyConstantオブジェクトのファクトリーメソッド
  • T get() : 値を取得するためのメソッド

 

ofメソッドでLazyConstantオブジェクトを生成して、getメソッドで値を取得するというだけです。

たとえば、Consumerクラスで遅延初期化させたいHeavyクラスのオブジェクがあるとしましょう。このオブジェクトをLazyConstantインタフェースを使用して遅延初期化させてみます。

LazyConstantインタフェースのフィールドをheavyConstantとしてfinalで定義します。

そして、実際にheavyConstantフィールドが保持する値を使用するのはconsumeメソッドだとします。

public class Consumer {
    private final LazyConstant<Heavy> heavyConstant
            = LazyConstant.of(() -> new Heavy());

    public void consume() {
        // 初回のgetメソッドコール時に
        // ofメソッドの引数で指定したラムダ式を実行し値を初期化
        // 次からは初期化後の値を取得
        var h = heavy.get();
        IO.println(h);

        // heavyを使用した処理...
    }
}

 

ofメソッドの引数のラムダ式では、Heavyオブジェクトの生成を行っています。オブジェクトの生成だけなのであれば、コンストラクター参照を使用してHeavy::newでも大丈夫です。

そして、heavyフィールドが保持している値を取得するためにgetメソッドを使用します。getメソッドの初回コール時にofメソッドの引数で指定されたラムダ式が実行されます。2回目以降は初期化された値が返ります。

 

では、このConsumerクラスを使ってみましょう。

void main() {
    var consumer = new Consumer();

    consumer.consume();
    consumer.consume();
    consumer.consume();
}

 

Heavyクラスのコンストラクターでは初期化しているというメッセージを標準出力に表示しています。

LazyConstantインタフェースはJava 26ではPreview APIなので、コンパイルや実行には--enable-previewが必要です。

>  java --enable-preview .\Main.java
ノート: C:\test\SomeData.javaはJava SE 26のプレビュー機能を使用します。
ノート: 詳細は、-Xlint:previewオプションを指定して再コンパイルしてください。
Heavy Initializing
Heavy@2002fc1d
Heavy@2002fc1d
Heavy@2002fc1d
> 

 

コンストラクターがコールされているのは1度だけで、それ以降はgetメソッドで返る値は同じオブジェクトになっていることがわかります。

 

初期化にデータが必要な場合

遅延させたい値の初期化に何らかのデータが必要になる場合はどうでしょう。

初期化にSupplierインタフェースを使うので、引数で渡すことはできません。しかし、final変数であればラムダ式の外側の変数も参照できるので次のように書くこともできます。

    public Consumer(final String filename) {
        heavyConstant = LazyConstant.of(() ->  new Heavy(filename));
    }

 

Heavyクラスのコンストラクターでは引数の文字列を表示させるようにしてみました。また、mainメソッドでも文字列を渡しています。

void main() {
    var consumer = new Consumer("Main.java");

    consumer.consume();
    consumer.consume();
    consumer.consume();
}

 

では、実行してみましょう。

>  java --enable-preview .\Main.java
ノート: C:\test\SomeData.javaはJava SE 26のプレビュー機能を使用します。
ノート: 詳細は、-Xlint:previewオプションを指定して再コンパイルしてください。
Heavy Initializing: Main.java
Heavy@2002fc1d
Heavy@2002fc1d
Heavy@2002fc1d
> 

 

このようにラムダ式を書いた時点でのfinal変数であるfilename変数をキャプチャーし、ラムダ式の実行時に使用していることが分かります。

 

初期化時の例外

外部リソースにアクセスするから遅延初期化をしたいということはけっこうあると思います。この時に困るのが例外です。

外部リソースへのアクセスはどうしても例外が発生しがちです。こんな時にどうすればよいのか?

考えられるのは以下の3つの選択肢ではないでしょうか。これについては特にJEPで触れられていないので、さくらばだったらこうするという方法です。

 

  • ラムダ式でnullを返す
  • 例外をRuntimeException例外にくるんで、スローする
  • ラムダ式の返り値をEitherもしくはResultなどにする

 

1つ目の方法は初期化のためのラムダ式で例外が発生した時に、nullを返すというものです。ラムダ式でnullを返すと、getメソッドの内部でNullPointerException例外がスローされます。

このNullPointerException例外をキャッチするという方法です。

この方法は簡単でよいのですが、残念ながら原因となる例外が隠されてしまい、スタックトレースなども引き継ぐことができません。このため、NullPointerException例外が発生したとしても、原因を調べるのが難しくなってしまいます。

 

2つ目のRuntimeException例外を使う方法は、初期化のためのラムダ式のSupplierインタフェースが検査例外をスローできないからです。これはjava.util.functionパッケージの他のインタフェースも同じですね。

そこで、何らかの例外が発生したらRuntimeException例外のcauseにセットして、RuntimeException例外をスローするという方法です。

この方法はスローするRuntimeException例外をドキュメント化しておかないと例外処理を忘れがちという問題があります。

少なくとも、マルチスレッドで使用している場合のUncaughtExceptionにならないようにしなくてはいけません。

この点に注意すれば、まぁ使える方法ではないかなと思います。

 

最後のEitherもしくはResultを使う方法は、例外を値として保存しておいて、ラムダ式の返り値として返すという方法です。

たとえば、Eitherであればleftとrightの2つの値を保持できるようになっており、正常に初期化できた時はleftに保持させ、例外が発生した時にはrightに保持させます。そして、このEitherオブジェクトをラムダ式の返り値として返します。

このため、LazyConstantインタフェースのgetメソッドの戻り値はEitherになり、leftとrightのそれぞれの場合の処理を記述します。

しかし、これだとLazyConstantというコンテナにEitherというコンテナを保持させ、実際に使うにはgetしてgetしなければならないというちょっとめんどくさいことになってしまいます。

こう考えると、finalフィールドのためにEitherを使うのはちょっとやりすぎではないかなと、さくらばは思うわけです。

 

ということで、さくらば的には2つ目のRuntimeException例外を使うかなぁ...

 

コレクションの遅延初期化

LazyConstantインタフェースではコレクションを初期化するのはちょっと難しいのですが、ListインタフェースとMapインタフェースに遅延初期化するためのメソッドが追加されています。

  • static List<E> List.ofLazy(int size, IntFunction<? extends E> computingFunction)
  • static Map<K, V> Map.ofLazy(Set<? extends K> keys, Function<? super K, ? extends V> computingFunction)

 

たとえば、0から9までを保持するリストを遅延初期化させるのであれば、次のように記述します。

private final List<Integer> nums = List.ofLazy(10, i -> i);

 

ofLazyメソッドを使用して生成したコレクションは、実際にそのコレクションにアクセスした時にofLazyメソッドの引数で指定されたラムダ式を実行して初期化を行います。

 

その他のメソッド

使うことはほぼないとは思いますが、LazyConstantインタフェースの残りの2つのメソッドについても簡単に触れておきましょう。

 

boolean isInitialized()

メソッド名で分かると思いますが、保持している値が初期化が行われたかどうか、つまりgetメソッドが1度でもコールされたかどうかを判定するためのメソッドです。初期化されて入ればtrue、初期化されていなければfalseが返ります。

 

T orElse(T other)

保持している値が初期化されていればその値を返し、初期化されていなければ引数のotherを返します。

注意しなければいけないのは、orElseメソッドの内部ではgetメソッドはコールされません。つまり、値が初期化されていなくても、初期化することはありません。

くり返しますが、初期化されていない場合はotherを返します。

 

まとめ

LazyConstantインタフェースはfinalフィールドを遅延初期化させたい場合に使用するインタフェースです。

それほど使うインタフェースではないとは思いますが、ロガーをfinalフィールドで定義したい場合などに使えます。

遅延初期化の機能だけに注目すれば、finalでないフィールドに対しても使うことが可能です。

とはいうものの、遅延初期化はそれなりにオーバーヘッドがあるので、遅延初期化を必要としないフィールドに対して使うのはやりすぎです。

遅延処理を必要とする場合に使うようにしましょう。

 

使い方だけで、意外に長くなってしまったので、LazyConstantインタフェースでの遅延処理の実装について次回説明することにします。

2025/10/11

JEPで語るJava 25 その2

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

前回は、Java 25のJEPのうち初心者向け機能と小さいオブジェクトを扱う機能を紹介しました。今回はその続きです。

まずはProject Leyden関連のJEPです。

 

Project Leyden

Project Leydenは、Javaの起動時間を短縮することを目標にしたプロジェクトです。

さくらばは、てっきりGraalのネイティブイメージを取り込むだけなのかと思っていたのですが、そうではなく、様々な手段を用いて起動時間とピークパフォーマンスに達するまでの時間を短縮するための機能を提供するようです。

Java 24ではJEP 483が導入されましたが、Java 25はこの続きになるJEPが2つ導入されました。

 

JEP 514: Ahead-of-Time Command-Line Ergonomics

JEP 483はクラスロード、クラスの解析、クラス間のリンク、static初期化を行った後のイメージをキャッシュとして保存して、起動時にそれを読み込むことによって起動時間を短縮させます。

このキャッシュ(AOTキャッシュと呼ばれます)を作るには、トレーニング実行を行って、その後キャッシュ作成するという2段階が必要でした。

しかし、起動時オプションを変えて、2回実行するというのはちょっとめんどう...

ということで、JEP 514は1度の実行でトレーニング実行とAOTキャッシュ作成を行ってしまいましょうというものです。

AOTキャッシュを作成するには以下のように実行します。

 

  • AOTキャッシュ作成
    $ java -XX:AOTCacheOutput=app.aot -cp app.jar com.example.App ...

AOTキャッシュを使って実行するのは、JEP 483と同じです。

  • 本番実行
    $ java -XX:AOTCache=app.aot -cp app.jar com.example.App ...

 

もちろん、JEP 483の2段階でAOTキャッシュを作成することも可能です。

 

JEP 515: Ahead-of-Time Method Profiling

HotSpot VMはアプリケーションを実行しながら、その片手間にプロファイリングもやっています。プロファイルイングの結果から、実行中にメソッドをネイティブコードにコンパイルするなどの最適化を行っています。

逆にいうと、ピークパフォーマンスに達するまでに時間がかかるということです。

Leydenで起動時間を短縮するのが必要なユースケースでは、なおさらピークパフォーマンスに達する時間が問題になりそうです。

そこで、JEP 515では、AOTキャッシュを作成するトレーニング実行中に作成したプロファイリングの解析結果をAOTキャッシュに含めるようになりました。

AOTキャッシュを使って実行すると、プロファイリングを行わずにすぐにネイティブコンパイルなどの最適化を行うことができるので、ピークパフォーマンスに到達する時間を大幅に短縮することができます。

 

Project Leydenでは、この後、JEP 516: Ahead-of-Time Object Caching with Any GCやまだドラフトですがJEP draft: Ahead-of-Time Code Compilationなどが控えています。

 

これらのProject Leydenの機能に関して、9月のJJUGナイトセミナーでOracleのじゅくちょー阪田さんが解説してくれたのですが、その資料がまだ公開されていない...

もし、LeydenのAOTキャッシュに興味があるのであれば、資料が公開されるのを待ちましょう!

 

安全性、セキュリティ

JEP 510の鍵導出関数はとりあえずおいておいて、サポートできなくなったポーティングは削除しましょうというのがJEP 503です。

 

JEP 503: Remove the 32-bit x86 Port

32bit版のWindowsのサポートが今月終了するということで、Java 24で32bit版のWindowsのポーティングが削除されました。また、x86のポーティングも削除予定になっていました。

そして、Java 25でx86のポーティングが削除されました。

サポートされないOSに対しては安全性の問題もあるので、削除はしかたないですね。

 

モニタリング

OpenJDKでのモニタリングとプロファイリングといえば、JDK Flight Recorder (JFR)ですね。

Java 25では、このJFR関連のJEPが3つあります。1つはExperimentalなので後述することにして、残りの2つについて簡単に紹介します。

とはいうものの、1つは内部実装の話なので、機能という感じではないですけど。

 

JEP 518: JFR Cooperative Sampling

非同期にスレッドのスタックトレースをサンプリングする部分の実装を見直して、安定性を高めたというJEP。

使い方の変更はないので、Java 25のJFRであれば安定してスタックトレースを取得できるようになるはずです。

 

JEP 520: JFR Method Timing & Tracing

メソッドがコールされたタイミングや回数などをサンプリングではなく、実測で調べるためのJFRのイベントが追加されました。

追加されたイベントはjdk.MethodTimeingとjdk.MethodTraceの2種類です。

これらのイベントを使用することで、メソッドがコールされたタイミングとメソッドの処理時間、そしてスタックトレースを取得することができます。

 

その他

Standard JEPの残った2つは、Project Loom関連のScoped Valueと、GCのShenandoah GCに関連したJEPです。

 

JEP 506: Scoped Values

ThreadLocalクラスはミュータブルでいろいろと問題のあるクラスですが、それをある程度置き換えることができるのがScopedValueクラスです。

すべてではないのはScopedValueクラスがイミュータブルなクラスなので、ThreadLocalクラスでsetメソッドを使っているようなケースは置き換えられないからです。

 

使い方は簡単で、ScopedValueインスタンスをnewInstance()メソッドで作成しておいて、そこにstaticメソッドのwhere()メソッドで保持する値をバインドさせます。

where()メソッドの返り値の型はScopedValue.Carrierクラスです。このScopedValue.Carrierクラスのrun()メソッド、もしくはcall()メソッドで実行するタスク内だけでScopedValueオブジェクトにバインドしたデータを使用できます。

つまり、run()メソッドとcall()メソッドで指定する関数だけがスコープになるわけです。

 

たとえば、時間を共有したい場合を考えてみます。

// スコープで共有するScopedValueオブジェクト
final ScopedValue<LocalDateTime> TIME = ScopedValue.newInstance();

void task1() {
  // get()メソッドで共有したデータを取得
  System.out.println("Task1: " + TIME.get());
}

void task2() {
  // get()メソッドで共有したデータを取得
  System.out.println("Task2: " + TIME.get());
}

void main() {
  Runnable r = () -> {
    // whereメソッドでTIMEに現在時刻をバインド
    // runメソッドで指定するラムダ式の中だけでバインドしたデータを共有
    ScopedValue.where(TIME, LocalDateTime.now())
               .run(() -> {
                  task1();
                  task2();
                });

  };

  try (var executor = Executors.newCachedThreadPool()) {
    executor.submit(r);
  }
}

 

とはいうものの、ThreadLocalクラスやScopedValueクラスを使ったデータの共有はできるだけ避ける方がよいです。共有したデータのせいでスケールしないことも多いですし、バグも発生しやすいです。

ScopedValueクラスを安易に使うよりは、もう一度設計を見直すことの方が賢明だと思います。

たとえば、上のコードでもtask1()メソッドとtask2()メソッドの引数として時間を渡した方が分かりやすいですよね。共有せずに済むのであれば、共有しない設計にすべきです。

 

JEP 521: Generational Shenandoah

Red Hatが進めていたShenandoah GCは、以前のZGCのように世代別GCではなかったのですが、ZGCの後を追うように世代別GCになりました。

Shenandoahで世代別モードにするには、まず実行時オプションの-XX:+UseShenandoahGCでGCにShenandoahを指定します。そして、同じく実行時オプションで-XX:ShenandoahGCMode=generationalを指定します。

 

Preview

ここからはお試し機能。まずは、Preview JEPからです。

Preview JEPは4種類。JEP 507はJava 25でStandardになると予想していたんですけど、Previewでしたね。残念。

 

JEP 470: PEM Encodings of Cryptographic Objects (Preview)

1つ目のPreview JEPは鍵導出関数に関するAPIです。

 

JEP 502: Stable Values (Preview)

StableValueクラスはfinalなフィールドに対して遅延初期化を行うためのクラスです。

それにしても、java.langパッケージにScopedValueクラスとStableValueクラスという似たようなクラスができると混乱するよなぁ... と思っていたら、Java 26がターゲットのJEP 526ではLazyConstantクラスに名前が変わりました!

LazyConstantクラスに関してはJJUG CCC 2025 Fallでプレゼンすることになったので、その時に詳しく解説します。

 

JEP 505: Structured Concurrency (Fifth Preview)

Structured ConcurrencyはなかなかStandard JEPになりませんが、複数の非同期実行のタスクの結果をまとめることなどに使用するAPIです。

Java 25で5回目のPreviewですが、APIに変更があったためJava 26でもう一度Previewということになっています(JEP 525)。

Java 24まではStructuredTaskScopeクラスのインスタンシエーションにはnewを使用していたのですが、Java 25でファクトリーメソッドのopen()メソッドが導入されています。

また、全部成功したらとか、1つでも失敗したらなどの条件を表すために、Java 24まではStructuredTaskScopeクラスのサブクラスで表していましたが、JEP 505ではStructuredTaskScope.Joinerインタフェースに委譲するようになりました。この方が分かりやすいですし、今後条件の拡張しやすくていいですね。

 

JEP 507:Primitive Types in Patterns, instanceof, and switch (Third Preview)

パターンマッチングにプリミティブ型を使用できるようにするのがJEP 507です。

JEP 507は変更なしだったのですが、まだドラフトなのですが次のJEPで変更が入ったので、Standardになるのは当分先になってしまいました。残念。

 

Experimental

Experimental JEPは1つだけで、JFRの機能追加に関してです。

 

JEP 509: JFR CPU-Time Profiling (Experimental)

CPUの計測を行うJFRのイベントが追加されました。ただし、Linuxだけです。

イベントはjdk.CPUTimeSampleです。

今のところ、新しいJEPは提案されていないようなので、Java 26でStandard JEPになるかもしれません。

 

Incubator

最後のIncubator JEPは、Value Classが導入するまでずっとIncubatorのままのあれです。

 

JEP 508: Vector API (Tenth Incubator)

ベクトル計算を行うためのVector APIですが、JEP 508ではいくつかの変更がありました。ただ、Value Classが導入されるまでIncubatorのままなので、今それを調べてもなぁ... と調べるモチベーションが上がりません😰

 

まとめ

Java 25はLTSということもあり、Standard JEPが多いリリースでした。

特にJEP 512のメインクラスの省略は意外に便利です。また、JEP 506のScoped Valueも正式化したので、今後ThreadLocalの置き換えが進むかもしれません。

 

ところで、Java 25とは関係ないのですが、ずっとドラフトのままだったValue Classがサブミットされて、通常のJEPにやっとなりました。JEP 401: Value Classes and Objectsです。

まだ、ターゲットバージョンが決まっていないのですが、早ければJava 26からPreviewが始まるかもしれません。

いつ導入されるのか全然わからなかったValue Classですが、やっと一歩踏み出した感じですね。

2025/10/04

JEPで語るJava 25 その1

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

さて、Java 25のJEP (JDK Enhancement Proposal)です。

Java 25のJEPは18個。そのうち、Standard JEPが12で、Preview JEPが4、Experimental JEPとIncubator JEPがそれぞれ1つという内訳です。

ちなみに、gihyo.jpでJEPの概要について記事を書きましたので、ざっと分かればよいという方はこちらをご参照ください。

 

Java 25リリース 初心者向け機能や起動時間短縮など

https://gihyo.jp/article/2025/09/java25

 

Java 25のJEPの一覧を次に示します。

  • 470: PEM Encodings of Cryptographic Objects (Preview)
  • 502: Stable Values (Preview)
  • 503: Remove the 32-bit x86 Port
  • 505: Structured Concurrency (Fifth Preview)
  • 506: Scoped Values
  • 507: Primitive Types in Patterns, instanceof, and switch (Third Preview)
  • 508: Vector API (Tenth Incubator)
  • 509: JFR CPU-Time Profiling (Experimental)
  • 510: Key Derivation Function API
  • 511: Module Import Declarations
  • 512: Compact Source Files and Instance Main Methods
  • 513: Flexible Constructor Bodies
  • 514: Ahead-of-Time Command-Line Ergonomics
  • 515: Ahead-of-Time Method Profiling
  • 518: JFR Cooperative Sampling
  • 519: Compact Object Headers
  • 520: JFR Method Timing & Tracing
  • 521: Generational Shenandoah

gihyo.jpの記事でも、関連したJEPをまとめて扱いましたが、ここでもそうしていきます。

  • 初心者向け: 511, 512
  • 小さいオブジェクト: 513, 519
  • Project Leyden: 514, 515
  • 安全性、セキュリティ: 503, 510
  • モニタリング: 518, 520
  • その他: 506, 521
  • Preview: 470, 502, 505, 507
  • Experimental: 509
  • Incubator: 508

なお、セキュリティに関するJEP 470とJEP 510は、いつものごとく説明を省略します。

 

初心者向け

今までの初心者向け機能というと、Javaのソースファイルをコンパイルしなくても実行することができるJEP 330とJEP 458がありました。

これらは、コンパイルして実行しなくてはいけないという手間を減らすための機能です。

しかし、Javaが初心者向けではないといわれるもう1つの要因が残っていました。たとえば、"Hello, World!"を出力するだけのプログラムでもメインクラスを書かなくてはいけないなど、どうしても記述量が多くなってしまうという点です。

それに対し、Java 25では2つのJEPでこれに対応しています。

 

JEP 511: Module Import Declarations

IDEを使ってプログラムを書いていると、import文はIDEが勝手に整理してくれるのでほとんど気にしないですけど、初心者にとってはハードルが高いのも確か。

あっという間にimport文が何十行にもなっていたりするんですよね。しかも、なぜかimport文でアスタリスクを使うのは嫌われているし。あれは何で嫌われているんでしょうね?

それに対し、JEP 511はimport文をモジュール単位で指定できるようにしましょうというJEPです。

書き方は簡単です。Mモジュールで定義されたクラスやインタフェースなどを使用する場合、次のように記述します。

import module M;

 

moduleキーワードが入るだけですね。

たとえば、GUIでAWTやSwingを使いたいのであれば、次のようになります。

import module java.desktop;

 

しかし、AWTを使うと、Listが問題になります。ようするに、java.awt.Listなのか、java.util.Listなのかという問題です。

import module java.desktop;
import module java.base;

  ...

  List list = ... // これは java.awt.List か java.util.List か?

 

こういう時は、モジュールインポートより通常のインポート文の方が優先されるので、次のように書きます。

import module java.desktop;
import java.util.List;

  ...

  List list = ... // これは java.util.List

 

あくまでも説明のためなので、RAW型を使うなとか言わないでくださいね。

また、アスタリスクで指定でも大丈夫です。

 

import module java.desktop;
import java.util.*;

  ...

  List list = ... // これは java.util.List

 

単一のインポート > アスタリスクでインポート > モジュールインポート の順に優先度が高くなります。

JEP 511ではあいまいなインポートを解決するためにインポート文を記述するのをシャドーイングという言葉で説明しているます。そういわれれば分かるものの、さくらばには、そのニュアンスはなかなか理解できないのでした。

 

JEP 512 Compact Source Files and Instance Main Methods

もう1つの初心者向け機能がJEP 512です。

JEP 512ではプログラム実行の起点であるmainメソッドを簡略化するための仕様を取り決めています。

簡略化できるのは次の4項目です。

  • メインクラスの省略
  • mainメソッドの簡略化
  • java.baseモジュールのインポートの省略
  • 標準入出力を行うクラスの導入

 

では、おなじみのHello, World!で考えてみましょう。ここではHello.javaファイルに記述しているものとします。

public class Hello {
    public static void main(String[] args) {
        System.out.println("Hello World");
    }
}

 

メインクラスの省略

プログラムの実行という意味では起点となるmainメソッドだけあればよいのですが、Javaでメソッドだけ単体で定義するわけにはいかなかったので、メインクラスが必要でした。

これに対して、Java 25ではメインクラスが省略可能になりました。

public static void main(String[] args) {
    System.out.println("Hello World");
}

 

これだけでかなりスッキリしますね。

 

mainメソッドの簡略化

メインクラスが省略できるようになったのに、わざわざアクセス修飾子のpublicやstaticなどは書く意味がありません。しかも、初心者にpublicやstaticを教えるのもなかなか大変なので、できれば触れたくない部分でもあります。

ということで、Java 25ではmainメソッドも大幅に簡略して書くことができるようになりました。

メインクラスも省略すると、"Hello, World!"を出力するには以下のようになりました!

void main() {
    System.out.println("Hello World");
}

 

アクセス修飾子もstaticも引数も省略可能です。もちろん、書いてもだいじょうぶです。

 

さくらばの予想では、省略してもコンパイル時に補完されてpublic static void main(String... args)としてクラスファイルに記述されると思っていました。

ところが、クラスファイルをjavapで見てみるとvoid main()のままでした。

ということは、javaの起動時にmainという名前のメソッドを検索して実行するように動作が変わったようです。

 

では、mainメソッドがオーバーロードされていたらどうなるでしょう。

void main(String arg) {
    System.out.println("Hello World 3");
}

void main(String... args) {
    System.out.println("Hello World 2");
}

void main() {
    System.out.println("Hello World 1");
}

 

さて、実行すると1、2、3のどちらが出力されるでしょう。

> java Hello.java
Hello World 2
    
>

 

やはり、元々のpublic static void main(String... args)に近いシグネチャーのオーバーロードが選ばれるようです。

 

ところで、void main()はstaticがついていないので、クラスファイルにはインスタンスメソッドとして定義されています。

この場合、他のメソッドはコールできるのでしょうか?

void hello() {
    System.out.println("Hello, World!");
}

static void staticHello() {
    System.out.println("Hello, Static World!");
}

void main() {
    hello();
    staticHello();
}

 

インスタンスメソッドとstaticメソッドを定義してコールしてみました。実行してみると...

> java Hello.java
Hello, World!
Hello, Static World!
    
>

 

インスタンスメソッドもstaticメソッドもコールすることができました。

ただし、mainメソッドをstaticで定義してしまうと、インスタンスメソッドをコールする部分でコンパイルエラーになります。

void hello() {
    System.out.println("Hello, World!");
}

static void staticHello() {
    System.out.println("Hello, Static World!");
}

static void main() {
    hello();
    staticHello();
}

 

> javac Hello.java
Hello.java:10: エラー: staticでないメソッド hello()をstaticコンテキストから参照することはできません
    hello();
    ^
エラー1個
    
>

 

java.baseモジュールのインポートの省略

次はインポート文の簡略化です。

リストの要素の一覧を出力するプログラムを考えてみましょう。

Listインタフェースを使うのですから、import java.util.List;が必要になりますが、JEP 511のおかげでimport module java.base;で済みます。

ところが、このインポート文は省略することができます。java.langパッケージのインポートは省略できるのと同じように、java.baseモジュールのインポートを省略できるのです。

したがって、次のようにListインタフェースを使用していても、インポート文は書かずに済みます。

void main() {
    List.of("Alpha", "Bravo", "Charlie")
        .forEach(System.out::println);
}

 

ただし、これはメインクラスを省略した場合だけです。

class Hello {
    void main() {
        List.of("Alpha", "Bravo", "Charlie")
            .forEach(System.out::println);
    }
}

 

> javac Hello.java
Hello.java:3: エラー: シンボルを見つけられません
        List.of("Alpha", "Bravo", "Charlie")
        ^
  シンボル:   変数 List
  場所: クラス Hello
エラー1個
    
>

 

メインクラスを書くのであれば、インポート文も記述しましょう。

import module java.base;

class Hello {
    void main() {
        List.of("Alpha", "Bravo", "Charlie")
            .forEach(System.out::println);
    }
}

 

> java Hello.java
Alpha
Bravo
Charlie
    
>

 

標準入出力を行うクラスの導入

最後が標準入出力を行うクラスです。

意外にSystem.outとSystem.inは説明するのが難しいんですよね。そこで、標準出力と入力の両方を扱うためのjava.lang.IOクラスが導入されました。

IOクラスでは、標準入出力のための最低限のメソッドだけが定義されています。もちろん標準エラー出力も扱いません。

定義してあるメソッドは以下の5種類です。

  • static void print(Object obj)
  • static void println()
  • static void println(Object obj)
  • static String readln()
  • static String readln(String prompt)

 

一番使うのはprintln(Object obj)でしょうね。標準入力もあるので、次のようなプログラムも簡単に記述できます。

void main() {
    for(;;) {
        var to = IO.readln("To: ");
        IO.println("Hello, " + to + "!");
    }
}

 

> java Hello.java
To: Wolrd
Hello, Wolrd!
To: Java
Hello, Java!
To:

 

ここまで省略できたり簡略化されたりすると、かなり書くのも楽になりますね。

 

小さいオブジェクト

 

Javaは歴史的経緯もあって少数のミュータブルで巨大なオブジェクトを扱うことが多かったのですが、Java SE 8から関数型の機能が導入されてから多数のイミュータブルな小さいオブジェクトを扱うように変わってきています。

イミュータブルな小さいオブジェクトを効率的に扱うようにするのが、Project ValhallaのValue Classです。

しかし、Value Classの導入には言語仕様やVM仕様の大幅な変更が伴うため、おいそれと導入するわけにはいきません。そこで、Value Classがまとまる前に、できるものから導入していこうというのが現在のステータスです。Java 25ではJEP 513: Flexible Constructor Bodiesが、それに相当します。

もう一方のJEP 519: Compact Object HeadersはValue Classとは直接関連はないのですが、多量の小さいオブジェクトを扱うためにオブジェクトヘッダーをコンパクト化しましょうというJEPになります。

 

JEP 513: Flexible Constructor Bodies

Java 24の時にも書きましたが、コンストラクター内でスーパークラスや自分自身のコンストラクターをコールするのは、コンストラクターの先頭と決まっていました。

これに対し、自分自身のフィールドの初期化を行ってから、他のコンストラクターをコールできるようになりました。

Value Classはイミュータブルなクラスですが、今までのオブジェクトの初期化順だと、初期化前のフィールドにスーパークラスからアクセスできてしまうという問題があります。

問題になるようなことはほとんどないはずですが、イミュータブルなオブジェクトにとっては値が不定な時にアクセスできてしまうというのは困ってしまいます。

そこれで、JEP 513でValue Classが導入される前に、この問題ををつぶしておこうというわけです。

 

JEP 519: Compact Object Headers

オブジェクトヘッダーをコンパクト化するJEP 519は、Java 24のJEP 450と特に変更はないようです。なので、Java 24のJEPで語るをご覧いただければと思います。

結局、Value Classのビットはそのままになったようです。

また、JEP 519にはJava 24での実行時オプションがのったままになっているのですが、これはJava 25でもそのままなのかどうかイマイチ分かりません。

デフォルトでコンパクトヘッダーになっているかどうかは、小さいオブジェクトをいっぱい使って、JFRとかでヒープの使用量を見れば分かるかな? 時間があれば、やってみます。

 

長くなってしまったので、残りは次回!

2025/09/16

JEPでは語れないJava 25

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

毎度おなじみ半年ぶりのJavaのアップデートです。

OpenJDK的にはLTSかどうかは関係なく、LTSかどうかはOracleなどのJDKディストリビューターによります。OracleがJava 25をLTSとするので、右にならえで他のディストリビューターもLTSにしてますね。

OpenJDKはLTSとは関係ないとはいうものの、実際にはLTSに間に合わせて機能を入れ込むということはありそうです。実際、Java 25のStandard JEPは12と結構な数になっています。

Java 25のJEPの一覧はこちら。

  • 470: PEM Encodings of Cryptographic Objects (Preview)
  • 502: Stable Values (Preview)
  • 503: Remove the 32-bit x86 Port
  • 505: Structured Concurrency (Fifth Preview)
  • 506: Scoped Values
  • 507: Primitive Types in Patterns, instanceof, and switch (Third Preview)
  • 508: Vector API (Tenth Incubator)
  • 509: JFR CPU-Time Profiling (Experimental)
  • 510: Key Derivation Function API
  • 511: Module Import Declarations
  • 512: Compact Source Files and Instance Main Methods
  • 513: Flexible Constructor Bodies
  • 514: Ahead-of-Time Command-Line Ergonomics
  • 515: Ahead-of-Time Method Profiling
  • 518: JFR Cooperative Sampling
  • 519: Compact Object Headers
  • 520: JFR Method Timing & Tracing
  • 521: Generational Shenandoah

 

目だったところでいうと、Scoped Valueがやっと正式になりましたね。でも、もう1つのStructured ConcurrencyがまだPreview...

ニーズ的にはThreadLocalを置き換えるScoped Valueの方が高いから、こちらが優先されたのかもしれません。

初学者向け機能であるJEP 511とJEP 512が正式導入されたのもうれしいところです。

Project LeydenのJEP 514とJEP 515も正式になって、GraalVMのネイティブイメージではない選択肢が増えてきたのはよいことですね。

JEPについての詳しい解説は「JEPで語る」に書く予定です。

 

さて、JEPで語れない方です。Java 25はそこそこ多いです。

Java 25はjava.baseモジュール以外にも変更があるので、そちらも紹介します。しかし、セキュリティ関連はいつものごとく省略させてください。

 

廃止になったAPI

Java 24で削除されたAPIはメソッドとコンストラクタが1つずつ。いずれもSwing関連のAPIです。

メソッド

SwingのLoo&FeelのSynthに関するメソッドが削除されました。

  • javax.swing.plaf.synth.SynthLookAndFeel.load(URL url)

このメソッドはLook&Feelの設定を書いたファイルを読み込むためのものなのですが、安全性に問題があるということでJava 21でforRemoval=trueになっていました。

まぁ、Look&FeelのSynthを使ったアプリケーションはほぼないと思いますし、他の手段もあるので、困る人も少ないでしょう。

 

コンストラクタ

SwingのスライダーのUIクラスのコンストラクターが削除されました。

  • javax.swing.plaf.basic.BasicSliderUI.BasicSliderUI()

このデフォルトコンストラクターは間違って公開されてしまったらしいですよw

本来は引数にJSliderクラスをとるコンストラクターを使用するので、まぁ使っている人はいないとは思いますが、使っていた場合は引数をとる方のコンストラクターに書き換えてください。

 

廃止予定に追加されたAPI

Java 25では、Java 24でのセキュリティマネージャーの削除に伴って、パーミッション系のクラスがforRemoval=trueになっています。

クラス

  • java.io.FilePermission
  • java.io.SerializablePermission
  • java.lang.RuntimePermission
  • java.lang.management.ManagementPermission
  • java.lang.reflect.ReflectPermission
  • java.net.NetPermission
  • java.net.URLPermission
  • java.nio.file.LinkPermission
  • java.security.SecurityPermission
  • java.security.UnresolvedPermission
  • java.util.PropertyPermission
  • java.util.logging.LoggingPermission
  • javax.management.MBeanPermission
  • javax.management.MBeanServerPermission
  • javax.management.MBeanTrustPermission
  • javax.management.remote.SubjectDelegationPermission
  • javax.net.ssl.SSLPermission
  • javax.security.auth.AuthPermission
  • javax.security.auth.PrivateCredentialPermission
  • javax.security.auth.kerberos.DelegationPermission
  • javax.security.auth.kerberos.ServicePermission

 

例外

  • javax.management.modelmbean.XMLParseException

 

メソッド

  • java.net.HttpURLConnection.getPermission
  • java.net.URLConnection.getPermission
  • javax.management.modelmbean.DescriptorSupport.toXMLString

もし、パーミッション系のAPIを使っているのであれば、早めに他の手段に置き換えた方がいいですね。

 

追加/変更されたAPI

今回のJava 25は、Java 24とは異なりいろいろ追加されています。しかも、JEPに関連のないAPIの変更がいろいろあります。

とはいうものの、使うかと言われたら微妙なものが多いですね。

今回もjava.baseモジュール以外の変更は少ないので、java.baseモジュールだけです。また、セキュリティ系のAPIは省略させてください。

 

java.base/java.ioパッケージ

JEP 512で正式に導入されたIOクラスは、Java 24まではjava.ioパッケージだったのですが、Java 25からjava.langパッケージに変更されました。また、同様にConsoleクラスにPreviewでprint(Object obj)メソッドなどが追加されていましたが、結局キャンセルになったようです。

Readerクラス

なぜかJava 25にもなって、Readerクラスに一括読み込みのメソッドが追加されました。

  • String readAllAsString()
  • Line<String> readAllLines()

今までReader系のクラスでは逐次読み込みばかりだったので、便利なのは確かなのですが、なぜ今??

やっぱり、Files.readAllLinesメソッドを使うようになってから、逐次読み込みなんか書いてられないということなのでしょうか?

おもしろいのは、JavadocのThrowsにOutOfMemoryError例外が記載されていること。Stringクラスがあふれちゃうことがあるかららしいです。

 

java.base/java.langパッケージ

Java 25では、Unicodeのバージョンアップはないので、Unicodeに関するクラスや列挙型には変更ありません。

その代わりといってはなんですが、Math/StrictMathクラスにメソッドがいろいろ追加されています。

また、java.ioパッケージのところでも言及しましたが、IOクラスがjava.langパッケージに変更されてきています。

 

CharSequenceインタフェース

文字列の部分配列を取得するためのメソッドが追加されました。インタフェースなので、もちろんdefaultメソッドです。

  • default void getChars(int srcBegin, int srcEnd, char[] dst, int dstBegin)

getCharsメソッドはStringクラスにはあったので、それをCharSequenceでも使えるようにした感じですね。

引数のdstに結果が返るというのは今時ではないですけど、StringクラスのgetCharsメソッドははじめからあるメソッドなのでしかたないです。

 

IOクラス

JEP 512で、標準入出力からの入出力を行うために導入されたのがIOクラスです。Java 24の時とパッケージが変わっただけで、メソッドは変更なしです。

  • static void print(Object obj)
  • static void println()
  • static void println(Object obj)
  • static String readln()
  • static String readln(String prompt)

今までSystem.out.println("Hello, World");と書いていたのが、IO.println("Hello, World!");になります。

初心者にとって学ぶことが少しでも減れば、学ぶときのハードルが下がりますね。

 

Mathクラス/StrictMathクラス

Math/StrictMathクラスにメソッドが7つ追加されました。

歴史的な経緯でMathクラスとStrictMathクラスは別々のクラスになっていますが、実際にはStrictMathクラスは内部でMathクラスを呼び出しているだけなので、今となっては同じクラスとして扱っても構わないです。

  • static int powExact(int x, int n)
  • static long powExact(long x, int n)
  • static int unsignedMultiplyExact(int x, int y)
  • static long unsignedMultiplyExact(long x, int y)
  • static long unsignedMultiplyExact(long x, long y)
  • static int unsignedPowExact(int x, int n)
  • static long unsignedPowExact(long x, int n)

今まで、累乗演算は引数がdoubleのものだけでしたが、整数型が加わりました。また、累乗計算も含めて、符号なし計算も追加されています。

これまで、Mathクラスでは符号なし計算はunsignedMultiplyHighメソッドだけだったので、今後も増えるのかもしれません。

 

ScopedValueクラス

JEP 506でPreviewがはずれて、正式に導入されたScopedValueクラスです。

Java 24との違いはorElseメソッドの引数にnullが許されなくなったことぐらいですね。

ScopedValueクラスについてはここではなく、JEPで語る方で紹介します。

 

StableValueインタフェース

JEP 502でPreviewとして追加されたのが、StableValueインタフェースです。

同じパッケージにScopedValueとStableValueという同じような名前のクラス/インタフェースができたので、ごっちゃになりやすくて困ってますw

StableValueインタフェースもJEPで語る方で紹介します。

 

java.base/java.lang.classfileパッケージ

ClassFileクラスにバージョンを表す定数が追加されています。

 

ClassFileクラス

ClassFileクラスでは、クラスファイルのバージョンを表す定数が定義されていますが、それが追加されました。

  • static final int JAVA_25_VERSION

この値はJava 25だから25というわけではなく、クラスファイルのバージョンの69になっています。

 

java.base/java.lang.classfile.constantpoolパッケージ

java.lang.classfile.constantpoolパッケージではクラスファイルのコンスタントプールに関する構造体を表すインタフェースが定義されています。

このコンスタントプールの構造体を表すインタフェースは、java.lang.constantパッケージで定義されているインタフェースをモデル化したものです。そこで、この対応関係を調べるためのメソッドが追加されました。

Class-File APIは新しいAPIですが、新しいからこそ、まだいろいろ追加されるのかもしれません。

 

ClassEntryインタフェース

コンスタントプールで参照型を表すCONSTANT_Class_info構造体をモデル化したのがClassEntryインタフェースです。ClassEntryインタフェースはjava.lang.constant.ClassDescインタフェースによってモデル化されたインタフェースになります。

このClassDescインタフェースとマッチするかどうかを調べるためのメソッドが追加されています。

  • boolean matches(ClassDesc desc)

ClassEntryオブジェクトとClassDescオブジェクトが表している参照型クラスが同じであればtrueが返ります。また、引数のdescがプリミティブ型の場合はfalseになります。

 

MethodTypeEntryインタフェース

コンスタントプールでCONSTANT_MethodType_info構造体をモデル化したのがMethodTypeEntryインタフェースです。MethodTypeEntryインタフェースはjava.lang.constant.MethodTypeDescインタフェースによってモデル化されたインタフェースです。

  • boolean matches(MethodTypeDesc desc)

同じメソッドを表している場合、trueが返ります。

 

ModuleEntryインタフェース

コンスタントプールでCONSTANT_Module_info構造体をモデル化したのがModuleEntryインタフェースです。対応するのが、java.lang.constant.ModuleDescインタフェースです。

  • boolean matches(ModuleDesc desc)

同じモジュールを表している場合、trueが返ります。

 

PackageEntryインタフェース

コンスタントプールでCONSTANT_Package_info構造体をモデル化したのがPackageEntryインタフェースです。対応するのが、java.lang.constant.PackageDescインタフェースです。

  • boolean matches(PackageDesc desc)

同じパッケージを表している場合、trueが返ります。

 

StringEntryインタフェース

コンスタントプールでCONSTANT_String_info構造体をモデル化したのがStringEntryインタフェースです。Stringクラスは参照型なので、本来であれば対応するのはClassDescインタフェースですが、文字列リテラルは特別扱いです。

  • boolean equalsString(String value)

StringEntryインタフェースはmatchesメソッドではなく、文字列と直接比較するequalsStringメソッドが追加されています。

 

Utf8Entryインタフェース

コンスタントプールでは、文字列リテラルだけでなくクラス名やメソッド名はUTF-8で保持されています。それを表すCONSTANT_UTF8_info構造体をモデル化したのがUtf8Entryインタフェースです。

Utf8EntryインタフェースはすでにequalsStringメソッドは定義してあるのですが、クラス名やメソッド名とマッチさせるメソッドが追加されました。

  • boolean isFieldType(ClassDesc desc)
  • boolean isMethodType(MethodTypeDesc desc)

 

java.base/java.lang.reflectパッケージ

java.lang.reflectパッケージはいつも通り新しいバージョンに合わせた定数が追加されているのですが、Java 25ではアクセスフラグにも追加があります。

AccessFlag列挙型

アクセスフラグのセットを作成するmaskToAccessFlagsメソッドに、クラスファイルのバージョンを指定できるオーバーロードが追加されました。

  • static Set<AccessFlag> maskToAccessFlags(int mask, AccessFlag.Location location, ClassFileFormatVersion cffv)

 

AccessFlag.Location列挙型

アクセスフラグを適用できる場所を示しているのがAccessFlag.Location列挙型です。

列挙型ですが、メソッドが追加されました。

  • int flagsMask()
  • int flagsMask(ClassFileFormatVersion cffv)
  • Set<AccessFlag> flags()
  • Set<AccessFlag> flags(ClassFileFormatVersion cffv)

たとえば、MODULEであればフラグマスクはACC_OPEN | ACC_SYNTHETIC | ACC_MANDATEDになります。

クラスファイルのバージョンによって使用できるフラグマスクが異なるので、バージョンを指定するオーバーロードがあるわけです。

 

ClassFileFormatVersion列挙型

いつものように、Java 25に対応する定数が追加されました。

  • RELEASE_25

 

java.base/java.nioパッケージ

なぜかCharBufferクラスにメソッドが追加されました。

CharBufferクラス

一括取得のメソッドが追加されたのですが、今まで使用していたgetメソッドと何が違うの?という感じです。

  • public void getChars(int srcBegin, int srcEnd, char[] dst, int dstBegin)

Javadocにも書かれているのですが、このメソッドはgetメソッドを使った次の式と同じ動作になります。

buffer.get(position() + srcBegin, dst, dstBegin, srcEnd - srcBegin);

記述が簡単になったというわけでもないですし、何のために追加されたんでしょうね。

 

java.base/java.utilパッケージ

CurrencyクラスとTimeZoneクラスにStreamインタフェースを返すユーティリティメソッドが追加されました。

Currencyクラス

使用可能な通貨を返すメソッドが追加されています。

  • static Stream<Currency> availableCurrencies()

これまではgetAvailableCurrenciesメソッドが使えましたが、このメソッドは返り値がSetインタフェースでした。availableCurrenciesメソッドは返り値の型がStreamインタフェースなので、Stream APIで扱いやすくなっています。

 

TimeZoneクラス

TimeZoneクラスでも使用可能なIDを返すメソッドが追加されています。

  • static Stream<String> availableIDs()
  • static Stream<String> availableIDs(int rawOffset)

TimeZoneもgetAvailableIDsメソッドがありましたが、戻り値の型がStringクラスの配列でした。追加された2メソッドはStreamインタフェースになっています。

 

java.base/java.util.concurrentパッケージ

ForkJoinPoolクラスに大きな変更がありました。

ForkJoinPoolクラス

ForkJoinPoolクラスはこれまでExecutorインタフェース、ExecutorServiceインタフェース、AutoClosableインタフェースを実装していましたが、新たにScheduledExecutorServiceインタフェースを実装するようになりました。

このため、ScheduledExecutorServiceインタフェースで定義されているメソッドが追加されています。

  • void cancelDelayedTasksOnShutdown()
  • long getDelayedTaskCount()
  • ScheduledFuture<?> schedule(Runnable command, long delay, TimeUnit unit)
  • <V> ScheduledFuture<V> schedule(Callable<V> callable, long delay, TimeUnit unit)
  • ScheduledFuture<?> scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit)
  • ScheduledFuture<?> scheduleWithFixedDelay(Runnable command, long initialDelay, long delay, TimeUnit unit)
  • <V> ForkJoinTask<V> submitWithTimeout(Callable<V> callable, long timeout, TimeUnit unit, Consumer<? super ForkJoinTask<V>> timeoutAction)

ScheduledExecutorServiceインタフェース由来のメソッドはscheduleで始まる4つのメソッドです。

scheduleメソッドはタスク処理の開始を指定された時間だけ遅延させるメソッドです。オーバーロードは引数がRunnableインタフェースか、Callableインタフェースの違いです。

たとえば、10秒後に現在時刻を出力するのであれば、次のようになります。

jshell> var pool = new ForkJoinPool(2)
jshell> pool.schedule(() -> IO.println(LocalDateTime.now()), 10, TimeUnit.SECONDS)
$2 ==> java.util.concurrent.DelayScheduler$ScheduledForkJoinTask@7a07c5b4[Wrapped task = $Lambda/0x0000000088048858@26a1ab54]

jshell> IO.println(LocalDateTime.now())
2025-09-12T18:42:45.527033900

jshell> 2025-09-12T18:42:53.898474800

比較のために、IO.printlnをもう1度実行していますが、手で打ち込んでいるので遅いですねw

scheduleAtFixedRateメソッドとscheudleWithFixedDelayの違いは、前者が同じ周期でタスクを実行するのに対し、後者はタスクが終わってから次のタスクまでの遅延が常に同じということです。

たとえば10秒ごとで処理に1秒かかる場合、scheduleAtFixedRateメソッドであれば10秒ごとにタスクが処理されるのに対し、scheduleWithFixedDelayメソッドだと11秒ごとに処理が行われます。

 

jshell> var pool = new ForkJoinPool(2)
jshell> pool.scheduleAtFixedRate(() -> IO.println(LocalDateTime.now()), 0, 10, TimeUnit.SECONDS)
$4 ==> java.util.concurrent.DelayScheduler$ScheduledForkJoinTask@6433a2[Wrapped task = $Lambda/0x0000000082048210@5910e440]

jshell> 2025-09-12T19:32:30.296824400
2025-09-12T19:32:40.258892800
2025-09-12T19:32:50.264887400
2025-09-12T19:33:00.258888800
2025-09-12T19:33:10.257071800

 

jshell> var pool = new ForkJoinPool(2)
jshell> pool.scheduleWithFixedDelay(() -> { IO.println(LocalDateTime.now()); try { Thread.sleep(1_000); } catch (Exception e){}}, 0, 10, TimeUnit.SECONDS)
$4 ==> java.util.concurrent.DelayScheduler$ScheduledForkJoinTask@7a07c5b4[Wrapped task = $Lambda/0x0000000033048858@26a1ab54]

jshell> 2025-09-12T19:48:54.864989800
2025-09-12T19:49:05.866353500
2025-09-12T19:49:16.869035100
2025-09-12T19:49:37.873515600

ちょっと分かりにくいかもしれないですけど、後者は11秒周期になっています。

周期的なタスクを中止するには、返り値のScheduledFutureオブジェクトに対し、cancelメソッドをコールします。

cancelDelayedTasksOnShutdownメソッドは、ForkJoinPoolオブジェクトをシャットダウンする時に遅延させた未実行のタスクをキャンセルする場合に使用します。

getDelayedTaskCountメソッドは、遅延させた未実行のタスクの数を返すメソッドです。

そして、最後のsubmitWithTimeoutメソッドだけがScheduledExecutorServiceインタフェースに関連のないメソッドで、タスク処理にタイムアウトを設定できるメソッドです。

 

ForkJoinPoolは、ForkJoinTaskと合わせることで分割統治とWork-Stealingでタスクを並行処理するために導入されたのですが、Work-Stealingだけ使いたいということが増えてきているんでしょうね。

 

java.base/java.util.zipパッケージ

DeflaterクラスとInflaterクラスがAutoClosableインタフェースを実装するようになりました。これによりcloseメソッドが追加されています。

Deflaterクラス/Inflaterクラス

AutoClosableインタフェースの実装に伴い、closeメソッドが追加されました。

  • void close()

これまでは、処理の終了時にはendメソッドを使用していましたが、AutoClosableインタフェースを実装したことによりtry-with-resources構文を使用できるようになりました。

 

その他

Javadoc

Javadocの検索候補の表示が変更されています。

たとえば、検索窓にStrまで入力した場合、Java 24だと以下のようになります。

これに対して、Java 25だと次のようになります。

Java 24では検索候補のパッケージやクラスの前にモジュールやパッケージが表示されていますが、Java 25では検索候補がまず表示されます。そのモジュールやパッケージはウィンドウの幅が十分にある場合は右側、なければ検索候補の下に表示されるようになりました。

ちなみに、Java 26でもJavadocが変更されていて、検索候補を右クリックで別タブでの表示などを選択できるメニューが表示されます。他にも、細かな変更がされています。

すでに、実装されているので、Java 26のアーリーアクセスビルドで試すことができます。

 

まとめ

APIの変更は多いものの、普通のアプリケーションでは使わなそうなものが多い感じです。

IOクラスが一番使うかもしれませんが、ほとんどの人はSystem.outを使い続けるような気が...

並行処理を書くことがあるのであれば、ScopedValueクラスは使うかもしれません。しかし、そもそもThreadLocalクラスでさえ通常は使わないので、ScopedValueクラスを使うとしても限定的でしょうね。

というか、こういう並行処理でこういう複数のオブジェクトで状態を共有というのは、スケールしなくなるし、いいことはないです。ThreadLocalクラスが使われているところをScopedValueクラスに置き換えるということはあるかもしれませんが、まずはその設計を見直した方がいいような気が...

 

さて、次のエントリーではJEPに関して簡単な説明を加えていく予定です。

2025/08/23

バイトコード入門 その6 制御構造

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

6回に渡ってバイトコードについて紹介してきましたが、今回が最終回です。

計算やオブジェクト生成、メソッドコールまで紹介したので、今回はif文やループなどの制御構造について説明します。

制御構造が理解できれば、バイトコードも一通り読みこなせるはずです。

  1. 準備編
  2. スタックマシン
  3. バイトコード処理の構成
  4. バイトコード処理の基礎
  5. オブジェクト生成、メソッドコール
  6. 制御構造 (今回)

 

if文

制御構造のはじめはif文です。

if文は条件付きジャンプ命令とジャンプ命令で構成されます。条件付きジャンプ命令は複数の種類があるのですが、全部覚える必要はありません。基本的な構成さえ分かっていれば、命令が変わってもすぐに分かるはずです。

たとえば、次のIfSampleクラスのような単純な例から始めてみましょう。

 

public class IfSample {
    public static void main(String args[]) {
        if (args.length == 0) {
            System.out.println("No Args");
        }
    }
}

 

mainの引数の配列の長さが0だったら、"No Args"と標準出力に出力するだけのクラスです。

このクラスを-gオプションを使ってコンパイルし、javap -v -pした時のmainメソッドのバイトコードは以下のようになります。

 

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: (0x0009) ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=1, args_size=1
         0: aload_0
         1: arraylength
         2: ifne          13
         5: getstatic     #7  // Field java/lang/System.out:Ljava/io/PrintStream;
         8: ldc           #13 // String No Args
        10: invokevirtual #15 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        13: return
      LineNumberTable:
        line 3: 0
        line 4: 5
        line 6: 13
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      14     0  args   [Ljava/lang/String;
      StackMapTable: number_of_entries = 1
        frame_type = 13 /* same */

 

配列はオブジェクトなので、その参照をオペランドスタックにロードするにはaloadを使います。そして、1行目のarraylengthで配列のサイズを取得します。arraylengthが終わると配列のサイズがスタックに残ります。

2行目のifneが条件付きジャンプ命令の1つです。ifの後のneはNot Equalです。何と比較してNot Equalなのかというと、0です。0やnullなど、条件でよく使われる値は専用の命令が用意されているわけです。

ここで、「えっ、if文は配列のサイズが0かどうかが条件だったはず」と思うのは当然です。

そうなのです、条件付きジャンプ命令はif文の条件の否定になっています。そして、条件に合致している場合、条件付きジャンプ命令の後の数値の行にジャンプするのです。ここでは、13行(行と書いていますが、本当はバイトなので飛び飛びの値になっています)にジャンプします。

つまり、配列の長さが0以外なら13行にジャンプし、0だったら次の行に制御が移るというわけです。このために条件がif文とは反転しているのでした。

5, 8, 10行は標準出力への文字列定数を出力しています。

そして、ジャンプ先である13行目はreturnなので、mainメソッドから抜けるだけですね。

 

もう少し複雑な例で、else節がある場合を見てみましょう。

 

public class IfSample {
    public static void main(String args[]) {
        if (args.length == 0) {
            System.out.println("No Args");
        } else {
            System.out.println("There are Args");
        }
}

 

先ほどのif文にelse節を追加しただけです。

これもコンパイルして、javapします。

 

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: (0x0009) ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=1, args_size=1
         0: aload_0
         1: arraylength
         2: ifne          16
         5: getstatic     #7  // Field java/lang/System.out:Ljava/io/PrintStream;
         8: ldc           #13 // String No Args
        10: invokevirtual #15 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        13: goto          24
        16: getstatic     #7  // Field java/lang/System.out:Ljava/io/PrintStream;
        19: ldc           #21 // String There are Args
        21: invokevirtual #15 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        24: return
      LineNumberTable:
        line 3: 0
        line 4: 5
        line 6: 16
        line 8: 24
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      25     0  args   [Ljava/lang/String;
      StackMapTable: number_of_entries = 2
        frame_type = 16 /* same */
        frame_type = 7 /* same */

 

2行目のifneは同じですね。0でなければ、16行に飛びます。0の場合は5, 8, 10行が処理されるのも先ほどと同じですが、次が違います。

13行でgotoが出てきました。goto命令は無条件にジャンプするための命令です。

Java言語ではgotoはキーワードになっているのですが、実際にはgoto文はありません。しかし、バイトコードにはgotoがあるのです。

さて、13行のgotoの飛び先は24行、つまりreturnのところです。

if節を抜けて、mainメソッドが終わるところですね。

16行からelse節になります。出力する文字列は違うものの、やっていることは同じです。

 

ここでは、条件付きジャンプ命令としてifneが使われていましたが、この部分がif文の条件によって異なります。とはいうものの、命令が違うだけで、処理の方法は同じです。

たとえば、上記コードの args.length == 0 の部分が > 0 であれば、ifle命令になります。また、== 1であれば、条件付きジャンプ命令の前にiconst命令で1をスタックに置き、その後にif_icmpne命令が使われます。icmpneの最初のiはintのiで、最後のneがNot Equalです。つまり、スタックにある2つのint数が等しくないかどうかをチェックします。

 

for文

では、次にfor文です。実をいうと、for文もやっていることは、if文とたいして変わりません。

つまり、条件付きジャンプとジャンプ命令の組み合わせです。

そこに、必要に応じてインデックスの増減などの処理が組み合わさったのがfor文です。

for文も簡単なコードでどのようなバイトコードになるか、見てみましょう。

 

public class ForSample {
    public static void main(String args[]) {
        int sum = 0;
        for (int i = 0; i < 10; i++) {
            sum += i;
        }

        System.out.println(sum);
    }
}

 

0から10までの数値を合計するというfor文です。

これをコンパイルして、javapしたのが以下のバイトコードです。

 

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: (0x0009) ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=3, args_size=1
         0: iconst_0 スタックに0を積む
         1: istore_1 スタックの値をsumに代入
         2: iconst_0 スタックに0を積む
         3: istore_2 スタックの値をiに代入  
         4: iload_2  iをスタックに積む
         5: bipush        10 10をスタックに積む
         7: if_icmpge     20 iが10以上なら20へジャンプ
        10: iload_1 sumをスタックに積む
        11: iload_2 iをスタックに積む
        12: iadd     スタックの2つの数を加算
        13: istore_1 スタックの値をsumに代入
        14: iinc          2, 1  iをインクリメント
        17: goto          4 4にジャンプ
        20: getstatic     #7   // Field java/lang/System.out:Ljava/io/PrintStream;
        23: iload_1
        24: invokevirtual #13  // Method java/io/PrintStream.println:(I)V
        27: return
      LineNumberTable:
        line 3: 0
        line 4: 2
        line 5: 10
        line 4: 14
        line 8: 20
        line 9: 27
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            4      16     2     i   I
            0      28     0  args   [Ljava/lang/String;
            2      26     1   sum   I

 

赤字でバイトコードが何を行っているかコメントしたので、分かりやすいと思います。

for文の中心になっているのが、7行目の条件付きジャンプ命令if_icmpeと、17行目のジャンプ命令gotoです。

条件付きジャンプでループを脱出するか決め、ループを行う場合はgoto命令で条件付きジャンプの前にジャンプします。

 

whileやdo-whileも、どこに条件付きジャンプ命令が使われるかが違うだけで、条件付きジャンプ命令とジャンプ命令で構成されるのは同じです。

 

for-each文はコンパイル時に、for文に展開されるのですが、Iteratorインタフェースとかが出てきて複雑になってしまうので、割愛します。

 

switch式

最後はswitch式です。

switchを行うバイトコードは2種類で、lookupswitch命令とtableswitch命令です。

通常のswitchにはlookupswitch命令が使われて、enumやSealed Classを使う場合tableswitch命令が使われます。

どちらも処理の流れは同じようになるので、ここではlookupswitch命令だけ紹介します。

 

ここでも、switch式の簡単な例でバイトコードがどうなるか見てみましょう。

 

public class SwitchSample {
    public static int sw(int x) {
        var y = switch (x) {
            case 0 -> 0;
            case 1 -> 2;
            case 2 -> 4;
            default -> -1;
        };

        return y;
    }

    public static void main(String... args) {
	System.out.println(sw(1));
    }
}

 

switch式を使っているswメソッドは、intの値で分岐してintの値を返すというだけのメソッドです。

これをコンパイルして、javapしたのがこちら。

 

  public static int sw(int);
    descriptor: (I)I
    flags: (0x0009) ACC_PUBLIC, ACC_STATIC
    Code:
      stack=1, locals=2, args_size=1
         0: iload_0
         1: tableswitch   { // 0 to 2
                       0: 28
                       1: 32
                       2: 36
                 default: 40
            }
        28: iconst_0
        29: goto          41
        32: iconst_2
        33: goto          41
        36: iconst_4
        37: goto          41
        40: iconst_m1
        41: istore_1
        42: iload_1
        43: ireturn
      LineNumberTable:
        line 3: 0
        line 4: 28
        line 5: 32
        line 6: 36
        line 7: 40
        line 8: 41
        line 10: 42
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      44     0     x   I
           42       2     1     y   I

 

まず、0行のiload_0で引数xの値をスタックに積みます。

その後がtableswitch命令です。だいたい分かると思いますが、値が0の時は28行にジャンプ、1の時は32にジャンプというようになっています。

値が0の時にジャンプする28行にはiconst_0でスタックに0を積み、その後のgoto命令で41行に飛んでいます。

他の値の場合も同じようになっています。

ここではintでしたが、他の場合でも同じような感じです。ただし、文字列の場合は、文字列のハッシュコードを用いてswitchのcaseに対応させています。

また、パターンマッチングの場合、indyが絡んできてめんどくさいので、ここでは省略します。興味のある方は、switch式で扱うために、その直前で行っているindyのブートストラップメソッドのjava.lang.runtime.SwitchBootstraps.typeSwitchメソッドを見てみるとおもしろいかもしれません。

 

というわけで、今回は制御構造に関するバイトコードを見ていきました。ここまでくれば、オペランドスタックを図示しなくてもだいたい分かるはずです。

途中、ずいぶん間があいてしまいましたが、6回に渡ったバイトコード入門もこれでおしまいです。

紹介していないバイトコードもありますが、ここまで理解できれば自力で読みこなせるでしょう。JVMSを手元に置いておけば大丈夫です。

バイトコードが読めるようになったら、次はClassfile APIですかね。これもおもしろいのですが、それはまた別の機会に。

2025/04/10

バイトコード入門 その5 オブジェクト生成、メソッドコール

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

前回、スタックマシーンとしてのJVMの基本的な動作を紹介しました。

プリミティブ型の加算という簡単な処理ですが、これが分かればより複雑なバイトコードでも理解できるはずです。

そこで、今回はもう少し複雑な例としてオブジェクトの生成やメソッドコールについて紹介します。

  1. 準備編
  2. スタックマシン
  3. バイトコード処理の構成
  4. バイトコード処理の基礎
  5. オブジェクト生成、メソッドコール (今回)
  6. 制御構造

 

命令セット

今までバイトコードのオペコード(命令、もしくはインストラクション)について簡単な説明しかしていなかったので、ここで紹介しましょう。

とはいってもすべてを紹介するのは大変ですし、おぼえても意味はないので、主に使われるオペコードについて見ていきます。

また、if文やfor文、switch式などの制御構造に関するバイトコードは次回紹介します。

 

オペコードの詳細な定義はJVMSのChapter 6にあります。

リンクはJava 24のJVMSです。JVMSも毎バージョンごとにアップデートされているので、なるべく最新を参照するようにしてください。

 

オペコードの構成

現在定義されているオペコードは200種類ほどあります。しかし、それらがすべて違う動作を表しているわけではありません。動作は同じだけど型が違う、動作は同じだけどローカル変数配列のインデックスが違うなどがあります。

型について接頭辞、インデックスなどは接尾辞で表します。接尾辞はインデックス以外にも繰り返し回数などがあり、オペコードによって意味が異なります。一方の接頭辞は常に型を表します。接頭辞の一覧を次に示します。

接頭辞
i int
l long
s short
b byte
c char
f float
d double
z boolean
a 参照

オペランドスタックやローカル変数配列にはプリミティブ型の値か参照しか入れることができません。このため、オブジェクトを扱う場合はヒープに存在するオブジェクトへの参照として扱われます。

もしかしたら、今後導入が予定されているValue Classであれば、直接オペランドスタックに積めるようになるかもしれません。

とはいうものの、バイトコードが増えるわけではなく、あくまでも最適化された場合に限るはずです。

 

たとえば、前回使用したオペランドのiload_1は、接頭辞がi、接尾辞が_1です。つまり、int型のloadで、ローカル変数配列のインデックス1からのロードということを表しています。

 

主なオペコード

オペコードが本体と接頭辞、接尾辞から構成されることが分かったところで、主なオペコードについて見ていきましょう。

 

スタック操作

オペランドスタックに対して何らかの操作を行うオペコードです。オペランドスタックと書くと長くなるので、以下では単にスタックと表記します。

  • ldc: コンスタントプールの定数をスタックに積む
  • const: インデックスで指定された値をスタックに積む (型、接尾辞あり)
  • bipush/sipush: byte値、short値をスタックに積む
  • pop: スタックから値を取り除く
  • dup: スタック末尾の要素を複製
  • swap: スタック末尾の2データを入れ替え

ldcはロードコンスタントのことですね。コンスタントプールは定数で書き換えることはないため、ストアはないです。

constは接尾辞で値を指定します。int型の1であれば iconst_1 となります。接尾辞は-1(m1)から5まであり、これを超える範囲の場合、次に紹介するbipush/sipushを使用します。

bipush/sipushもconstに似ていますが、byte値もしくはshort値を直接スタックに積みます。他の型はありません。

整数型の変数で、byteもしくはshortの範囲に収まるものはbipush/sipushが使われます。shortを超える範囲の場合、コンスタントプールにある定数を使用してldcが使われます。

 

ローカル変数

オペランドスタックとローカル変数配列とのやり取りに関するオペコードです。

  • load: ローカル変数配列の値をスタックに積む (型、接尾辞あり)
  • store: スタックから値を取り出し、ローカル変数配列に置く (型、接尾辞あり)

接尾辞のインデックスは0, 1, 2しかないため、それ以上の配列インデックスは iload 5 のようにオペコードの後に指定します。

 

算術演算

四則演算などの算術演算を行うオペコードです。

  • add: 加算 (型あり)
  • sub: 減算 (型あり)
  • mul: 乗算 (型あり)
  • div: 除算 (型あり)
  • rem: 除算の余り (型あり)
  • neg: 符号反転 (型あり)
  • iinc: インクリメント

インクリメントを行うiincはint型だけです。long型の場合はladdが使われます。

 

論理演算

AND/OR/XORとシフト演算を行うオペコードです。

  • iand/land: 論理積
  • ior/lor: 論理和
  • ixor/lxor: 排他論理和
  • ishl/lshl: 左シフト
  • ishr/lshr: 右シフト (符号は維持)
  • iushr/lushr: 右シフト

論理演算はint型とlong型に対してのみ存在します。byte/short/charに対してint型のオペコードが使われます。

 

オブジェクト関連

オブジェクトの生成などで使用するオペコードです。

  • new: インスタンス生成
  • getfield: インスタンス変数の値をスタックに積む
  • putfield: スタックから値を取り出し、インスタンス変数に代入
  • getstatic: クラス変数の値をスタックに積む
  • putstatic: スタックから値を取り出し、クラス変数に代入

newはインスタンスを生成するだけで、コンストラクターのコールは含まれていません。

コンストラクターのコールは、メソッドコールのバイトコードで行います。具体的な例は後述します。

getfield/putfieldは接頭辞による型の指定はありませんが、オペコードの後に指定します。getstatic/putstaticでも同じです。

 

配列関連

配列の生成や配列からの値の取り出し、代入などで使用するオペコードです。

  • newarray: プリミティブ型を要素にとる配列の生成
  • anewarray: 参照型を要素にとる配列の生成
  • multianewarray: 多次元配列の生成
  • aload: 配列のインデックスの要素をスタックに積む (型あり)
  • astore: スタックから値を取り出し、配列のインデックスの要素に代入 (型あり)
  • arraylength: 配列の長さをスタックに積む

Javaでは配列はオブジェクトの一種、つまり参照型として表されます。しかし、配列を扱うためのバイトコードも提供されています。

loadやstoreに接頭辞のaがついているのは、配列が参照型だからです。さらに要素の型をその前に接頭辞として付加します。たとえば、int型の配列から値を取り出すときはialoadになります。

 

メソッドコール

メソッドコールを行うバイトコードはメソッドの種類により5種類提供されています。

  • invokevirtual: インスタンスメソッド
  • invokeinterface: インタフェースメソッド
  • invokestatic: クラスメソッド
  • invokespecial: コンストラクター、プライベートメソッド、スーパークラスのインスタンスメソッド
  • invokedynamic: コールするメソッドを実行時に決定させるメソッドコール
  • return: メソッドから返る。(型あり)

メソッドの種類によってオペコードを変えますが、メソッド種類ごとにスタックに積んでおく値も異なります。たとえば、インスタンスメソッドであればコールするメソッドのオブジェクトが必要ですが、クラスメソッドであれば必要ありません。

特殊なのがinvokedynamicです。invokedynamicでは初回コール時にbootstrapという指定されたメソッドをコールし、ターゲットとなるメソッドを同定させてから、メソッドコールを行います。

2回目以降は同定したメソッドコールを直接行います。

invokedynamicに関しては、YujiSoftwareさんとJJUG CCCでセッションを行ったので、その資料をご参照ください。

 

メソッドから返るのがreturnです。int型の値を返すのであればがireturnになりますが、返り値がない場合はreturnのまま使用します。

 

残りは次回に回しましょう。

前回は算術演算を行うメソッドだけだったので、今回はその残りの部分の動作を見ていきます。

 

オブジェクト生成、メソッドコール

前章でオペコードにどのようなものがあるか紹介したので、実際にこれらのオペコードを使っていく様子を見てみましょう。

題材は前回と同じ足し算を行うだけのAdderクラスです。

public class Adder {
    public int add(int x, int y) {
        int z = x + y;
        return z;
    }

    public void static main(String... args) { 
        Adder adder = new Adder();
        int result = adder.add(2, 3);
        System.out.println(result);
    }
}

 

前回はaddメソッドの実行を見ましたが、今回はmainメソッドです。

Adderクラスをコンパイルオプションの-gを付加してコンパイルし、javap -vでmainメソッドを逆コンパイルしたのが以下です。

  public static void main(java.lang.String...);
    descriptor: ([Ljava/lang/String;)V
    flags: (0x0089) ACC_PUBLIC, ACC_STATIC, ACC_VARARGS
    Code:
      stack=3, locals=3, args_size=1
         0: new           #7    // class Adder
         3: dup
         4: invokespecial #9    // Method "<init>":()V
         7: astore_1
         8: aload_1
         9: iconst_2
        10: iconst_3
        11: invokevirtual #10   // Method add:(II)I
        14: istore_2
        15: getstatic     #14   // Field java/lang/System.out:Ljava/io/PrintStream;
        18: iload_2
        19: invokevirtual #20   // Method java/io/PrintStream.println:(I)V
        22: return
      LineNumberTable:
        line 8: 0
        line 9: 8
        line 10: 15
        line 11: 22
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      23     0  args   [Ljava/lang/String;
            8      15     1 adder   LAdder;
           15       8     2 result   I
}

 

mainメソッドがコールされた時のオペランドスタックとローカル変数配列の初期状態は次のようになります。

 

addメソッドはインスタンスメソッドだったので、ローカル変数配列のインデックス0にはthisが格納されていました。しかし、mainメソッドはクラスメソッド(staticメソッド)なので、thisはありません。そのため、インデックス0にはmainメソッドの引数が格納されます。

argsは文字列の配列ですが、それがそのままローカル変数配列に格納されるわけではありません。文字列配列argsの実態はヒープに作成され、ローカル変数配列にはその参照が格納されます。

 

コンスタントプール

mainメソッドでは、最初にAdderオブジェクトを生成し、adder変数に代入しています。

まずはAdderオブジェクトの生成です。

0行のnew #7がオブジェクトを生成している部分です。この#7はコンスタントプールへの参照になります。

addメソッドではコンスタントプールを使用することがなかったので、ここで触れておきましょう。

javap -vの出力にはコンスタントプールが含まれています。その一部を以下に示します。

Constant pool:
   #1 = Methodref          #2.#3          // java/lang/Object."<init>":()V
   #2 = Class              #4             // java/lang/Object
   #3 = NameAndType        #5:#6          // "<init>":()V
   #4 = Utf8               java/lang/Object
   #5 = Utf8               <init>
   #6 = Utf8               ()V
   #7 = Class              #8             // Adder
   #8 = Utf8               Adder
   #9 = Methodref          #7.#3          // Adder."<init>":()V
  #10 = Methodref          #7.#11         // Adder.add:(II)I
  #11 = NameAndType        #12:#13        // add:(II)I
  #12 = Utf8               add
  #13 = Utf8               (II)I

 

コンスタントプールは番号付きでこのように並べられています。

さて、先ほどのnew #7の#7を見てみるとClass #8とあります。Classは文字通りクラスを示しています。そのクラス名が#8になります。

#8は、Utf8 Adderです。Utf8は文字列定数を示しています。実際にクラスファイルでは、文字列をUTF-8で表記しています。そして、その後の"Adder"がその文字列定数の実態です。

つまり、#7でクラスを示し、そのクラス名は#8で"Adder"であるということを示しています。

 

オブジェクト生成

では、mainメソッドに戻りましょう。

new #7では、Adderクラスのオブジェクト生成を行い、その結果、ヒープに生成したオブジェクトの参照をオペランドスタックに積みます。

 

ここまではオブジェクトの生成をしただけの状態で、コンストラクターはコールしていません。

mainメソッドの3行、4行がコンストラクターをコールする部分です。

まず、3行でdupを行います。dupはduplicateのコートで、スタックの値を複製します。

ここではAdderオブジェクトへの参照を複製します。

 

そして、その後のinvokesupecialでコンストラクターをコールします。

invokespecialはコンストラクター、プライベートメソッド、スーパークラスのインスタンスメソッドをコールするためのオペコードです。

コールするメソッドが、コンスタントプールの#9です。#9の部分を見てみると...

   #3 = NameAndType        #5:#6          // "<init>":()V
   #4 = Utf8               java/lang/Object
   #5 = Utf8               <init>
   #6 = Utf8               ()V
   #7 = Class              #8             // Adder
   #8 = Utf8               Adder
   #9 = Methodref          #7.#3          // Adder."<init>":()V

 

#9はMethodrefで、メソッドへの参照を示しています。その値の#7は先ほど見たようにAdderクラスを指しています。

#3を見てみると、NameAndTypeとなっています。これはメソッド名とメソッドのシグネチャー(引数と戻り値の型)を示しています。

NameAndType #5:#6の#5がメソッド名、#6がシグネチャーです。

#5を見てみると、文字列定数で<init>です。

コンストラクター名は、Javaのコードではクラス名と同じですが、バイトコードではどのクラスでも<init>になります。

シグネチャーを表すのが、#6の文字列定数の()Vです。

()の中に引数の型が示されますが、Adderクラスのコンストラクターはデフォルトコンストラクターで引数なしなので、単に()となっています。

()の後に示されるのが、戻り値の型です。Vはvoid、つまり戻り値なしです。

コールするメソッドが分かったので、実際にコールするわけですが、その時にオペランドスタックの値が使われます。

ここでは、Adderオブジェクトへの参照が2つスタックに格納されていますが、1つがコールするメソッドのオブジェクト、そしてもう1つがそのメソッドへの暗黙の引数と考えることができます。

オブジェクト生成はnew, dup, invokespecialが常にセットになっているので、そういうものだと考えていただいて大丈夫です。

invokespecialを実行すると、mainメソッドはそのままにして、Adderクラスのコンストラクターを実行するフレームがJVMスタックに積まれます。

 

そして、コンストラクターの実行が完了すると、コンストラクターのフレームが取り除かれて、mainメソッドが再開します。

オペランドスタックには、コンストラクターをコールした後のAdderオブジェクトが積まれます。

 

さて、その後の7行のastore_1で、Adderオブジェクトの参照をローカル変数配列のインデックス1に格納します。

 

これで、Adder adder = new Adder();の処理が完了しました。

 

インスタンスメソッドコール

Adderオブジェクトが生成出来たので、次はaddメソッドをコールする部分です。

         8: aload_1
         9: iconst_2
        10: iconst_3
        11: invokevirtual #10   // Method add:(II)I
        14: istore_2

インスタンスメソッドをコールするには、コールするメソッドのオブジェクトとメソッドの引数をオペランドスタックに積んでおきます。

まず、aload_1でローカル変数のインデックス1にあるAdderオブジェクトを積みます。

 

9行と10行のiconstは、定数をスタックに積むオペコードです。接尾語の_2と_3がスタックに積む値を示しています。

 

constの接尾語は-1から5までなので、これを超える数にするとどうなるでしょう。

byte型の範囲であればbipush、short型の範囲であればsipush、それを超える場合はldcが使われます。

たとえば、adder(128, 1_000_000);とすると、バイトコードは次のように変化します。

         8: aload_1
         9: sipush        128
        12: ldc           #10                 // int 1000000
        14: invokevirtual #11                 // Method add:(II)I
        17: istore_2

 

128にはsipush、1,000,000にはldcでコンスタントプールの値が使われています。

 

メソッドのターゲットとなるオブジェクトと引数をオペランドスタックに積んだ後に、インスタンスメソッドをコールするinvokevirtualを実行します。

どのメソッドをコールするのかは、コンスタントプールの#10に記述されています。

Adderクラスのデフォルトコンストラクターと同じようにコンスタントプールをたどっていくと、Adder.add(II)Iになることが分かります。

(II)が引数が2つで両方ともint、最後のIで戻り値の型もintだということを示しています。

invokevirtualを実行すると、mainメソッドのフレームは一時停止し、新たにaddメソッドのフレームがJVMスタックに積まれ、addメソッドの実行が始まります。

 

addメソッドの実行は前回紹介したので、ここでは省略します。

addメソッドの実行が完了すると、addメソッドのフレームは取り除かれ、addメソッドの戻り値がmainメソッドのフレームのオペランドスタックに積まれます。

 

後は、その値をistore_2を使用してローカル変数配列のインデックス2に格納します。

 

ローカル変数配列のインデックス2はresult変数を表しているので、addメソッドをコールしてresultに代入するJavaのコードが完了したことになります。

 

最後のSystem.out.printlnメソッドもインスタンスメソッドなので、メソッドコールの方法はaddメソッドの場合と同じです。

少しだけ違うのが、printlnメソッドのターゲットとなるオブジェクトのSystem.outです。

System.outはSystemクラスのクラス変数(static変数)であるoutです。

このため、System.outを取得するために、オペコードのgetstaticを使用します。

15行のgetstatic #14がその部分です。

コンスタントプールの#14を見てみると、次のようになっています。

  #14 = Fieldref           #15.#16        // java/lang/System.out:Ljava/io/PrintStream;
  #15 = Class              #17            // java/lang/System
  #16 = NameAndType        #18:#19        // out:Ljava/io/PrintStream;
  #17 = Utf8               java/lang/System
  #18 = Utf8               out
  #19 = Utf8               Ljava/io/PrintStream;

 

#14がFieldrefでフィールドの参照を示していることが分かります。その値が#15.#16です。

#15の方がフィールドのクラス、#16がフィールド名と型を表します。

さらにたどっていくと、#15がjava.lang.Systemを指していることが分かります。

#16はNameAndTypeで、#18がフィールド名、#19が型を表しています。#18には文字列定数でout、#19も文字列定数でLjava/io/PrintStreamとなっています。

今まで、Iがint、voidがVだということは出てきました。プリミティブ型ではなく参照型の場合、接頭語としてLが使用され、その後にパッケージを含めたクラス名が続きます。

Aではなくて、Lになることにご注意ください。

これで、getstaticでSystem.outの参照がオペランドスタックに積まれることが分かりました。

後は、addメソッドと同じように引数もオペランドスタックに積んで、invokevirtualを実行します。

 

mainメソッドにはreturn文の記述はありませんが、実際には省略されているだけでreturn文は必ずあります。

25行のreturnがそれに相当します。mainメソッドに戻り値はないので、接頭辞はないreturnを使用します。

returnを実行すると、mainメソッドのフレームが破棄され、Adderクラスの実行が完了します。

 

まとめ

Javaだと高々数行のコードですが、バイトコードで見てみるとJavaのコードを細かく分解して実行されます。

ここで示したように、メソッド単位でフレームが作られ、その内部でオペランドスタックとローカル変数配列で処理を進めていくのです。

さて、次回はif文やforループなどの制御構造について紹介する予定です。