2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

JavaAdvent Calendar 2024

Day 22

クラスファイルの ACC_SUPER フラグの意味

Last updated at Posted at 2024-12-22

Java クラスファイルのアクセスフラグの一つに ACC_SUPER というものがあります。
このフラグの意味は、ドキュメントに以下のように書かれています。

ACC_SUPER フラグは、このクラスまたはインタフェースに invokespecial 命令 (§invokespecial) が出現した場合に、2 つの代替セマンティクスのどちらが表現されるかを示します。
Chapter 4. The class File Format

これは何を意味しているのでしょうか。

結論としては、もはやなんの意味もありません。
以前はこのフラグの有無で少し挙動が変わっていたのですが、Java 7 Update 13 から (正式には Java 8 から)クラスファイルにこのフラグがセットされていてもいなくても同じ挙動になったためです。

Java SE 8 以降では、Java 仮想マシンは、クラス ファイル内のフラグの実際の値やクラスファイルのバージョンに関係なく、すべてのクラス ファイルで ACC_SUPER フラグが設定されているものと見なします。
Chapter 4. The class File Format

そのため、このフラグを気にする必要はありません。


以下、Java 考古学者向けの補足です。

ACC_SUPER 誕生の経緯

この StackOverflow の回答を元に、補足しながら説明します。

元々、ACC_SUPERinvokespecial の挙動を変更するために生まれました。

Java 1.0.2 の invokespecial の仕様

invokespecial はコンストラクタの呼び出しや、オーバーライドした親クラスのメソッド呼び出しなどで使われる命令です。Java 1.0.2 の仕様では、invokespecial 命令は単純に指定されたメソッドを呼び出すとなっていました。

例えば、以下のクラスは Java の最初のバージョン(1.0.2)では C クラスの super.foo() の部分が invokespecial 命令で A.foo() を呼び出すというコンパイル結果になります。実行すると C.foo()A.foo() の順番で実行されるので "CA" と表示されます。1

A.java
class A {
    public void foo() {
        System.out.print("A");
    }
}
B.java
class B extends A {
    // foo メソッドを A から継承するが、 オーバーライドしない
}
C.java
class C extends B {
    public void foo() {
        System.out.print("C");
        super.foo();   // invokespecial A.foo():V
    }
}
Main.java
public class Main {
    public static void main(String[] args) {
        new C().foo();
    }
}

Java 1.1 での invokespecial の仕様変更

ところが、「invokespecial は指定されたメソッドを呼び出す」という仕様だと、オーバーライドが追加された場合に問題が起こることが発覚しました。
例えば、上記の B クラスに以下のように foo() メソッドが追加された場合、C クラスの super.foo() では追加された B.foo() が実行されるべきですが、C クラスをコンパイルし直さない限り invokespecial A.foo():V となっているので 依然として A.foo() が実行されてしまっていました。

B.java
class B extends A {
    public void foo() {
        System.out.print("B");
        super.foo();
    }
}

そこで、Java 1.1 で invokespecial の仕様に手を加えて、invokevirtual と同様にオーバーライドを考慮して呼び出すメソッドを決めるようになりました。 今回の場合だと、invokespecial A.foo():V という命令で A.foo() ではなく、C クラスのスーパークラスから順に foo() を探すようになり、 B.foo() があるのでそれを呼び出すようになりました。

しかし、単純に仕様を変更すると些細な違いですが Java 1.0.2 と挙動が変わり、互換性がなくなってしまいます。
そのため、以下の仕様となりました。

  • クラスファイルにアクセスフラグ ACC_SUPER を追加
    • Java 1.1 以降のコンパイラでは、このフラグを必ず設定する
  • ACC_SUPER の有無で、そのクラス内の invokespecial 命令の挙動を変更する
    • ACC_SUPER フラグが設定されている(= Java 1.1 以降でコンパイルした)
      • 指定されたメソッドのオーバーライドを考慮して呼び出す
        • 上記の例では、invokespecial A.foo():VB.foo() を呼び出し、"CBA" という表示になる
    • ACC_SUPER フラグが設定されていない(= Java 1.0.2 でコンパイルした)
      • Java 1.0.2 と同じ挙動にする
      • 指定されたメソッドをオーバーライドを考慮せず呼び出す
        • 上記の例では、invokespecial A.foo():VA.foo() を呼び出し、"CA" という表示になる

ACC_SUPER の変更

このような経緯で誕生した ACC_SUPER ですが、Java 7 Update 13 から(正式な仕様としては Java 8 から)クラスファイルの ACC_SUPER が設定されていなくても、ACC_SUPER フラグが設定されているものとして扱うようになりました。

つまり、Java 1.0.2 でコンパイルした(ACC_SUPER フラグが設定されていない)クラスファイルの挙動が変わりました。具体的には、上記の C クラスの invokespecial A.foo():VA.foo() を呼び出していたのが、Java 7 Update 13 からは B.foo() を呼び出すように変わりました。

この変更理由は明らかにされていませんが、上記の StackOverflow のコメントではおそらくセキュリティ上の問題があったからではないかと言われています。
具体的な例として、以下のブログでは Java 7 以降では Thread クラスのクローンができないように Thread.clone()CloneNotSupportedException をスローするように実装されているにもかかわらず、それをスキップして Object.clone() を呼べてしまうという点が指摘されています。
JDK 7 Thread Cloning Vulnerability - IKVM.NET Weblog

まとめ

基本的に Java 仮想マシン仕様では後方互換性を重視しているので、過去のどのバージョンでコンパイルされたクラスファイルであっても、最新の Java 仮想マシンで実行した際には変わらずに動くようになっています。

それにもかかわらず、一度入れた仕様を打ち消す変更をして挙動を変えるというのはとても珍しいです。もしかしたら、唯一かもしれません。

  1. ちなみに、検証したところ Java 1.2 からは invokespecial の実行対象が B:foo() に変わっていました。

2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?