1つの参照、多くの型
ポリモーフィズムは継承とインターフェースの成果です。親(またはインターフェース)として型付けされた1つの変数が、任意のサブタイプのオブジェクトを保持でき、その上でメソッドを呼ぶと、Javaは変数の宣言された型が示すバージョンではなく、オブジェクトの実際のクラスに属するバージョンを実行することを意味します。
継承のページですでに下地は見たはずです。サブクラスが親のメソッドをオーバーライドする、というものです。ポリモーフィズムこそが、そのオーバーライドを価値あるものにします。一般的な型に対してコードを書き、それぞれの具体的なオブジェクトに独自の振る舞いをさせることができるのです。
a も b も Animal として宣言されていますが、それぞれが自分の音を出力します。この選択は実際のオブジェクトに基づいて実行時に行われます。
動的メソッドディスパッチ
この背後にある仕組みが 動的メソッドディスパッチ(dynamic method dispatch)です。オーバーライドされたインスタンスメソッドについて、JVMはどの実装を呼び出すかを決めるためにオブジェクトの実行時クラスを見ます。コンパイラはメソッドが宣言された型に存在することだけを確認し、実際の選択はプログラムが実行されるまで先送りされます。
これこそが、1つのループがそれぞれが何であるかを一切尋ねることなく、あらゆる型の混在を扱えるようにするものです。
ループは Shape しか知りません。後で Triangle extends Shape を追加しても、このコードは変更なしで動き続けます。それがまさに狙いです。コードは具体的な型の一覧ではなく、抽象に依存しているのです。
アップキャストとダウンキャスト
Dog を Animal 変数に格納するのは アップキャスト です。階層を上って、より一般的な型へ移動します。これは常に安全で、すべての Dog は Animal である(is a)ため、Javaは暗黙的に行います。
逆方向へ進むのが ダウンキャスト です。親参照を取り、それを特定のサブタイプとして扱います。これはオブジェクトが本当にそのサブタイプである場合にのみ有効なので、キャストを明示的に書く必要があり、間違えると ClassCastException のリスクを負います。
最後のキャストは問題なくコンパイルされます。コンパイラはそれが誤りだと証明できないからです。しかし Cat は Dog ではないため、実行すると破綻します。当てずっぽうでダウンキャストしてはいけません。
instanceofでダウンキャストをガードする
ダウンキャストの前に、instanceof で実際の型を確認しましょう。現代のJavaでは結果を同じ式の中でバインドでき(instanceof のパターンマッチング)、別途のキャストを省けます。
instanceof は null に対して false を返すため、このチェックは NullPointerException からも守ってくれます。とはいえ、長い instanceof の連鎖を書いていることに気づいたら、それはその振る舞いがオーバーライドされたメソッドとしてクラスの内側に属すべきサインであることが多いです。分岐はポリモーフィズムに任せましょう。
overriding と overloading
この2つは似て聞こえますが無関係であり、混同するのは典型的な混乱の原因です。
オーバーライド(overriding) は、サブクラスが親のメソッドを同一のシグネチャで置き換えることです。オブジェクトの型によって実行時に解決されます。これが私たちが使ってきたポリモーフィズムです。
オーバーロード(overloading) は、1つのクラスが同じ名前でパラメータリストの異なる複数のメソッドを持つことです。引数の型によってコンパイル時に解決され、実行時のディスパッチは一切関与しません。
コンパイラは引数の静的型だけから、一致する describe を選びます。親子のオブジェクトは一切関与しないため、これは実行時ポリモーフィズムではありません。単にメソッド名を再利用しているだけです。
よくある落とし穴:フィールドはポリモーフィックではない
実行時にディスパッチされるのは インスタンスメソッド だけです。フィールド と 静的メソッド は宣言された型によって解決され、これが多くの人をつまずかせます。
p.name() は Child のバージョンを実行しますが(ポリモーフィズム)、p.label は Parent のフィールドを読みます。フィールドはオーバーライドされるのではなく隠蔽されるからです。解決策は単純です。フィールドは private に保ち、メソッド経由でのみアクセスすれば、ポリモーフィックな呼び出しが常に勝ちます。
次へ:アクセス修飾子
ポリモーフィズムがきれいに機能するのは、サブクラスが正しいメンバーを見てオーバーライドでき、その一方でコードの他の部分が手を伸ばして不変条件を壊せない場合だけです。そのバランスは public、protected、private、そしてパッケージプライベートのアクセス、つまりアクセス修飾子によって制御されます。次はそれを見ていきます。
よくある質問
Javaのポリモーフィズムとは何ですか?
ポリモーフィズムとは、1つの参照型が多くの異なるクラスのオブジェクトを指すことができ、実際に実行されるメソッドが変数の宣言された型ではなく、オブジェクトの実行時の本当の型によって選ばれることを意味します。つまり Shape shape 変数は Circle でも Square でも保持でき、shape.area() を呼ぶと自動的に正しいバージョンが実行されます。
Javaのoverridingとoverloadingの違いは何ですか?
オーバーライド(overriding) は、サブクラスがスーパークラスのメソッドを同じシグネチャで置き換えることで、実行時ポリモーフィズムを支える仕組みです。オーバーロード(overloading) は、1つのクラスが同じ名前でパラメータリストの異なる複数のメソッドを持つことで、コンパイラが引数に基づいてコンパイル時に1つを選びます。オーバーライドはオブジェクトの型によって実行時に解決され、オーバーロードは引数の型によってコンパイル時に解決されます。
Javaのupcastingとdowncastingの違いは何ですか?
アップキャストは子オブジェクトを親型として扱うもので(Animal a = new Dog();)、常に安全で通常は暗黙的です。ダウンキャストは逆方向で(Dog d = (Dog) a;)、オブジェクトが本当にそのサブタイプである場合にのみ安全です。そうでなければ ClassCastException をスローします。すべてのダウンキャストはまず instanceof でガードしてください。