共変戻り値 (Covariant Return Type) は、Java 5 から導入された機能で、「子クラスでメソッドをオーバーライドするとき、戻り値の型を親クラスの戻り値のサブクラスに変更できる」というものです。
- 例えば、親クラスのメソッド戻り値が
Animalという型だった場合、オーバーライドする子クラスでは、その戻り値をAnimalのサブクラスであるCatやDog等に変更して定義することができます。
【復習】オーバーライドとは
オーバーライド (Override)は、親クラスで宣言・定義したメソッドと同名・同シグニチャ(引数リスト)を持つメソッドを、子クラス側で再定義することです。これにより、親クラスの振る舞いを「子クラスならではの振る舞い」に差し替えることができます。
- オーバーライドの条件
- メソッド名が親クラスと同一であること
- メソッドの引数リスト(パラメータの型や数など)が親クラスと同一であること
- アクセス修飾子は、親クラスのメソッドよりも厳しくしてはいけない
- 例:親クラスで
publicだったメソッドをprivateにするのは不可
- 例:親クラスで
- 戻り値の型が親クラスと同じか、またはそのサブクラスであること
- ここが共変戻り値のキーポイント
つまり、もともとは「戻り値の型が親と全く同じであること」が基本だったのですが、Java 5 以降は「戻り値の型が親よりもサブクラス(より限定的な型)ならOK」と認められています。
共変戻り値の実例:サンプルコード
以下のように、「動物」を表すクラス Animal を親クラス、その「猫」を表すクラス Cat を子クラスとします。
class Animal {
private String name;
public Animal(String name) {
this.name = name;
}
// 返り値は "Animal"
public Animal getAnimal() {
System.out.println("Animal: getAnimal()");
return this;
}
}
class Cat extends Animal {
public Cat(String name) {
super(name);
}
// オーバーライドで、戻り値を "Cat" (親である Animal のサブクラス) に変更
@Override
public Cat getAnimal() {
System.out.println("Cat: getAnimal()");
return this;
}
}
Animalクラスは、getAnimal()メソッドでAnimal型を返している。CatクラスはAnimalクラスを継承し、同名・同シグニチャのgetAnimal()を定義している。ただし、戻り値の型をCatに変更。- Java 5 以降の共変戻り値のおかげで、これがコンパイルエラーにならず、正しくオーバーライドとして認識される。
呼び出しの様子
public class Main {
public static void main(String[] args) {
Animal a1 = new Animal("Generic Animal");
Animal a2 = new Cat("Tama");
// 親クラスのインスタンスの場合
Animal result1 = a1.getAnimal(); // Animal: getAnimal()
// 実体は Cat だが、型が Animal として扱われる
Animal result2 = a2.getAnimal(); // Cat: getAnimal() が呼ばれるが, 変数型は Animal
// Cat で変数を受け取ればキャスト不要で Cat のメソッドが呼べる
Cat c = new Cat("Mike");
Cat result3 = c.getAnimal(); // Cat: getAnimal()
// ここで return this; は Cat 型として返るので、result3 は明確に Cat 型
}
}
// 出力
// Animal: getAnimal()
// Cat: getAnimal()
// Cat: getAnimal()
a2.getAnimal() のように、変数型は Animal であっても、実行時には Cat の getAnimal() が呼ばれます(動的ディスパッチ)。ただし、返ってくる型を実際に Cat として使いたい場合は、型を Cat にしておけばキャストが不要です。
共変戻り値がないとどうなる?
Java 5 以前では、オーバーライドの戻り値を親クラスと同じ型に限定していました。そのため、「実装としては Cat を返していても、定義上は Animal を返す」としか書けなかったのです。すると、以下のようなキャストが必須になってしまうことが多々ありました。
public Animal getAnimal() {
// 実は Cat インスタンスを返している
return new Cat("Tama");
}
public void someMethod() {
Animal a = getAnimal();
// Cat のメソッドを呼びたいときはキャストが必要
if (a instanceof Cat) {
((Cat) a).meow();
}
}
共変戻り値が使えるようになると、戻り値の宣言時点で Cat を返すことが明示できるので、呼び出し側が無駄なキャストをしなくてよくなり、より型安全・読みやすいコードになります。
- Q戻り値がサブクラスに変わるだけで何が変わるのか?
- A
戻り値が「より具体的な型」になることで、後続の処理でキャストが不要になるのが一番大きな変化です。これにより、コンパイル時に型チェックが厳密に働くので、安全性も向上します。
- Q「共変戻り値」と「ジェネリクス」との関係は?
- A
ジェネリクス(Generics)を使ったクラス継承においても、「親クラスのジェネリック引数のサブタイプ」にする といったケースで、共変戻り値に似た考え方が使われることがあります。
ただし、Java のジェネリクス自体は共変・反変といった概念をそのままはサポートしていません(ワイルドカード
? extends/? superなどでそれっぽく表現しますが、C# のようにキーワードで明示的に共変・反変を指定するわけではない)。いずれにしても、「子クラスでより限定的な型を返すことでコンパイル時に型安全を保ちやすくする」という思想は共通しています。
