Javaの型パラメーターに対してstaticメソッドを呼び出した場合の挙動
以前にJavaの配列関連で調べたことがあったのですが、Javaの総称型は型消去によって直感的でない挙動をする場合があります。
Java言語のClassクラスが持つちょっと不思議な性質について - 達人プログラマーを目指して
Java5の型システムを理解するにはリフレクションAPIを使ってみるのが最短の近道になる - 達人プログラマーを目指して
特に、総称型の型パラメーターTについては以下はコンパイルできないという制約があります。
- new T()
- new T[配列サイズ]
- catch (T ...
- extends T
- T.class
- instanceof T
また、staticメソッドやstatic初期化ブロック内でクラスの型パラメータを使えないという制約もあります。
AngelikaLanger.com - Java Generics FAQs - Type Parameters - Angelika Langer Training/Consulting
このような制約は型Tが実際にはコンパイル時に消去されるということを考えれば納得のできるものです。これらの制約は初心者だけでなく、C++、C#、Haskellといった言語を使いこなす上級者であっても勘違いしやすいところですね。
それで、最近ちょっと話題になって気づいたのですが、実は型パラメーターTに対してstaticメンバのアクセスはどうなるのかという点が実は盲点で、自分もちょっと勘違いしていました。当然new Tが認められていないのだから、T.staticメンバという形式の呼び出しはコンパイルエラーなのだと考えていたのですが、実際に以下のコードを実行してみるとわかるように、Tは単に消去型として処理されるのですね。だから、型の上限が設定されていた場合はその上限の型のstaticメンバを呼び出すことができてしまうようです。
class Parent { public static String field = "Parent"; public static void method() { System.out.println("Parent"); } } class Child extends Parent { public static String field = "Child"; public static void method() { System.out.println("Child"); } } public class Tester<T extends Parent> { public void test() { T.method(); System.out.println(T.field); } public static void main(String[] args) { Tester<Child> child = new Tester<>(); child.test(); // Parent // Parent } }
実際にはTの消去型は上限のParentなのでTesterクラスは型消去されると以下と等価になります。
public class Tester { public void test() { Parent.method(); System.out.println(Parent.field); } public static void main(String[] args) { Tester child = new Tester(); child.test(); // Parent // Parent } }
そのように考えれば、このように実際にバインドされたChildでなく「Parent」が表示されてしまうのも納得がいきますが、かなり直感に反する気もします。Tester
どうして、Javaではこのように型パラメーターに対するstaticメンバーの呼び出しを認めているのでしょうか。newの場合と同じ理屈ならコンパイルエラーにもできたはずなのですけれどね。
(追記)
ちなみに、staticでない場合を試してみました。
class Parent { public String field = "Parent"; public void method() { System.out.println("Parent"); } } class Child extends Parent { public String field = "Child"; public void method() { System.out.println("Child"); } } public class Tester<T extends Parent> { public void test(T instance) { instance.method(); System.out.println(instance.field); } public static void main(String[] args) { Tester<Child> child = new Tester<>(); child.test(new Child()); // Child // Parent } }
new T()はコンパイルできないため、外部でChildをインスタンス化して渡すようにしています。普通のインスタンスメソッドの場合は、
いまさらですが、職業Javaプログラマーなら理解しておいてほしい「継承」の意味について - 達人プログラマーを目指して
で説明したとおり、ポリモーフィズムがあるので、意図したとおりChildの方のメソッドが呼び出されていることが確認できます。しかし、publicフィールドの場合は、staticメソッドと同様にポリモーフィズムがないためParentの側が表示されました。
ブクマのコメントにもありましたが、教訓としては、インスタンスメソッド以外サブクラスで「オーバーライド」するのは避けた方が良いということですね。