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ですかね。これもおもしろいのですが、それはまた別の機会に。

0 件のコメント: