2020/04/07

Java の final の仕様(2) - final クラス/メソッド

(1) - final 変数 に続いて、今回は final クラスと final メソッドの話です。こちらは final 変数ほど難しくはありません。

final クラス

サブクラスを定義することが望ましくない、あるいは必要ないクラスは、final クラスとして宣言することができます。final クラスが extends 句に出てくると、コンパイルエラーになります。したがって、一切サブクラスを定義することができなくなります。結果として、final クラスのメソッドはオーバーライドされることもありません。

final と abstract を同時に宣言することもコンパイルエラーになります。abstract はクラス定義が完了していないことを意味していますから、サブクラスが定義できないと、クラス定義が完了できません。このような宣言は意味を為さないため、コンパイルエラーとしています。

final メソッド

サブクラスでオーバーライドされたくないメソッドは final メソッドとして宣言することができます。オーバーライドしようとするとコンパイルエラーになります。

暗黙の final メソッド

private メソッドはオーバーライドできませんので、暗黙的に final メソッドです。
また、final クラスのメソッドも同様に、自動的に final メソッドとなります。

インライン化

実行時、仮想マシンによるネイティブコードへの翻訳や最適化の際に、final メソッドはインライン化されることがあります。暗黙の final メソッドも対象です。
インライン化とは、メソッドの呼び出しの代わりに、メソッドの本体のコードを直接呼び出し元の位置に埋め込むことです。インライン化の際には、メソッド呼び出しのセマンティクスは維持されます。例えば、以下のようなことが考慮されます。
  • インスタンスメソッドを呼び出すターゲットが null の場合、インライン化されていても NullPointerException が発生する
  • 実引数の評価順がメソッド呼び出しのときと変わらないように処理する

この最適化は、コンパイル時には行われませんし、実行時にも行われるとは限りません。「final メソッドの呼び出しはコンパイル時にインライン化される」という話をときどき聞きますが、それは jdk 1.4 までの話で、今の Java では行われていません。
以下の例を見てください。
add1() は public な final メソッドです。final メソッドですが、public なので別のクラスから呼び出されるため、呼び出し元と呼び出し先のクラスが別々にコンパイルされることまで考慮する必要があります。そうなると、コンパイル時のインライン化ができません。
add2() は private メソッドなので、暗黙の final メソッドです。private なので他クラスから呼び出されることを考慮する必要はあまりありません (リフレクションはありますが)。そのため、コンパイル時にインライン化してもいいように思えます。
add3() は private static メソッドで、やはりコンパイル時にインライン化しても良さそうです。

public class FinalMethod {
  public final int add1(final int x, final int y) {
    return x + y;
  }
 
  private int add2(final int x, final int y) {
    return x + y;
  }

  private static final int add3(final int x, final int y) {
    return x + y;
  }

  public static final void main(final String[] args) {
    FinalMethod f = new FinalMethod();
    for (int i = 0; i < 3; i++) {
      for (int j = 0; j < 3; j++) {
        int p = f.add1(i, j);
        int q = f.add2(i, j);
        int r = FinalMethod.add3(i, j);
        System.out.println("p=" + p + ", q=" + q + ", r=" + r);
      }
    }
  }
}

ところが、実際にコンパイル後のバイトコードを確認してみると、いずれもインライン化はされていません。以下に抜粋します。

     18  aload_1 [f]
     19  iload_2 [i]
     20  iload_3 [j]
     21  invokevirtual FinalMethod.add1(int, int) : int [24]
     24  istore 4 [p]
     26  aload_1 [f]
     27  iload_2 [i]
     28  iload_3 [j]
     29  invokevirtual FinalMethod.add2(int, int) : int [26]
     32  istore 5 [q]
     34  iload_2 [i]
     35  iload_3 [j]
     36  invokestatic FinalMethod.add3(int, int) : int [28]
     39  istore 6 [r]

このように、コンパイル時はインライン化されません。ただし、実行時には行われている可能性があります。