「メソッドのシグネチャ」はJava言語とJava仮想マシンで違う

プロになるJava」でシグネチャのことを次のように説明しています。

メソッドの名前と受け取る引数、戻り値の種類をあわせたものを シグネチャ といいます。

戻り値は含まないのでは?という話になり、結論から言えばJava言語では名前と引数でメソッドを区別、Java仮想マシンでは名前と引数、戻り値まで含めてメソッドを区別しているけど、ここではJava言語の話をしているので含まないほうが正しい、です。

シグネチャの定義

Java言語仕様では、「シグネチャとは」という定義はありませんが、「シグネチャが同じとは」という説明があります。ジェネリクスの型パラメータまで考慮はするけど、名前と引数が同じとみなせれば同じシグネチャだよという感じですね。

Two methods or constructors, M and N, have the same signature if they have the same name, the same type parameters (if any) (§8.4.4), and, after adapting the formal parameter types of N to the type parameters of M, the same formal parameter types.
https://docs.oracle.com/javase/specs/jls/se17/html/jls-8.html#jls-8.4.2

一方でJava 7のJava仮想マシン仕様では、戻り値やthrowsまで含めるような記述があります。そしてよく見るとメソッド名はありませんね。

A method signature, defined by the production MethodTypeSignature, encodes the (possibly parameterized) types of the method's formal arguments and of the exceptions it has declared in its throws clause, its (possibly parameterized) return type, and any formal type parameters in the method declaration.
https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-4.html#jvms-4.3.4

そしてJava 8からは、この記述があった4.3.4. Signaturesごと消えています。

f:id:nowokay:20220402232630p:plain

Java言語とJava仮想マシンでのメソッド特定方法の違い

クラスFooから、クラスBarvoid print(int)なメソッドを呼び出します。

public class Foo {
    public static void main(String[] args) {
        Bar.print(3);
    }
}

メソッド定義はこんな感じ。

public class Bar {
    static void print(int a) {
        System.out.println(a);
    }
}

javacでコンパイルして実行できます。

C:\Users\naoki\Desktop>dir *.class

ファイルが見つかりません

C:\Users\naoki\Desktop>javac Foo.java

C:\Users\naoki\Desktop>java Foo
3

このprintメソッドの戻り値をintに変更します。クラスFooはそのまま。

public class Bar {
    static int print(int a) {
        System.out.println(a);
        return 0;
    }
}

コンパイルしなおす前にclassファイルを退避しておきます。

C:\Users\naoki\Desktop>copy Bar.class Bar.class_
        1 個のファイルをコピーしました。

一旦classファイルを全部消して、コンパイル・実行するとちゃんと実行できます。

C:\Users\naoki\Desktop>del *.class

C:\Users\naoki\Desktop>javac Foo.java

C:\Users\naoki\Desktop>java Foo
3

このことから、Java言語では戻り値の型が変わってもメソッドの特定に問題がないことがわかります。

では、先ほど退避したBarクラスのclassファイルを戻して実行します。

C:\Users\naoki\Desktop>copy Bar.class_ Bar.class
Bar.class を上書きしますか? (Yes/No/All): y
        1 個のファイルをコピーしました。

C:\Users\naoki\Desktop>java Foo
Exception in thread "main" java.lang.NoSuchMethodError: 'int Bar.print(int)'
        at Foo.main(Foo.java:3)

そうすると、NoSuchMethodErrorが発生しました。そしてここで、見つからなかったメソッドとして戻り値の型まで含んでいます。 このことから、Java仮想マシンでは戻り値の型が変わるとメソッドが特定できなくなることがわかります。

ということで、「Java」の用語の話をしていても言語かVMかで扱いが変わることがあるんだなということでした。

いいわけ

ただ、「プロになるJava」で戻り値まで含める説明をしたのは、JShellでメソッドの「シグネチャ」として戻り値まで出ているので、その説明をしやすくするためというのがあります。

jshell> "test".toUpperCase(
シグネチャ:
String String.toUpperCase(Locale locale)
String String.toUpperCase()

<ドキュメントを表示するにはタブを再度押してください>

シグネチャというのはなにかを特定するための情報なので、メソッドのシグネチャという場合にJava言語では戻り値は含めないのですが、実際の「シグネチャ」の使い方はメソッドの「概形」をさすことも多いので、うまい説明を考えないといけない。