S-JIS[2008-02-08/2009-02-25] 変更履歴

共変戻り値型(covariant return type)

共変ってあんまり聞かない言葉だけど。
メソッドをオーバーライドした際、戻り値の型が(JDK1.4までは一致している必要があったが、JDK1.5から)サブクラスに出来るようになった。

ちなみに、共変の反対は「反変(contravariant)」と言うらしい…が、それはJavaとは無関係^^;


具体例(インターフェース)

interface Interface {
	public Number getValue();
}
class Implement implements Interface {

	public Integer getValue() { //戻り値の型にサブクラスを指定
		return 123;	//自動ボクシング
	}
}

この例では、インターフェースでは戻り値をNumberで宣言しているが、その実装ではNumberのサブクラスであるIntegerで定義している。

		Interface obj = new Implement();
		Number n = obj.getValue();
		Implement obj = new Implement();
		Integer n = obj.getValue();

→インターフェースとクラス間だけでなく、複数のインターフェース間とクラスでも可 [2008-05-02]


具体例(クラス)

クローンの定義が便利になる。
つまり、今まで自分の複製を返すにも関わらずObjectで定義するしかなかったので、clone()を呼んだ側でキャストする必要があった。これからはちゃんと自分のクラスを指定できる。

Cloneableの例


具体例(配列・総称型)

配列共変なので、配列は共変戻り値に出来る(ただし、たぶん推奨されない)。[2009-02-25]
総称型List等)は共変ではないので、共変戻り値に出来ない。

class CovariantSuper {
	public Number method() {
		return Byte.valueOf((byte) 0);
	}

	public Number[] methodArray() {
		return new Number[10];
	}

	public List<Number> methodList() {
		return new ArrayList<Number>();
	}
}
class CovariantInteger extends CovariantSuper {

	@Override
	public Integer method() {
		return Integer.valueOf(123);
	}

	@Override
	public Integer[] methodArray() {
		return new Integer[10];
	}

	@Override
	public List<Integer> methodList() {	←コンパイルエラー
		return null;
	}
}

プリミティブ型は対象外

共変戻り値が指定できるのはあくまでサブクラス(派生クラス)なので、プリミティブ型は対象外。

class Super {
	public int getValue() {
		return 0;
	}
}
class Sub extends Super {

	public int getValue() {
//×	public Integer getValue() {	//intのラッパーであるIntegerもダメ
//×	public long getValue() {	//intよりサイズの大きいlongもダメ
		return 0;
	}
}

共変戻り値型の実現方法

戻り値型を変えたクラスをjavapで見てみると、(JDK1.4まで通りの)まったく同一の戻り値型を持つメソッドと、戻り値型を変えたメソッドの2つのメソッドが定義されているのが分かる。

>javap CloneSample
Compiled from "CloneSample.java"
public class jp.hishidama.sample.covariant.CloneSample extends java.lang.Object implements java.lang.Cloneable{
    public jp.hishidama.sample.covariant.CloneSample();
    public jp.hishidama.sample.covariant.CloneSample clone() throws java.lang.CloneNotSupportedException;
    public java.lang.Object clone() throws java.lang.CloneNotSupportedException;
}

より詳細(バイトコード)を見てみると、同一の戻り値型のメソッドが、戻り値型を変えた方のメソッドを呼ぶようになっていた。
たぶん、このメソッドのことを橋渡しメソッド(ブリッジメソッド)と呼ぶのだと思う。

>javap -v CloneSample
public jp.hishidama.sample.covariant.CloneSample clone() throws java.lang.CloneNotSupportedException;
  Exceptions:
   throws java.lang.CloneNotSupportedException  Code:
   Stack=1, Locals=1, Args_size=1
   0:   aload_0
   1:   invokespecial   #21; //Method java/lang/Object.clone:()Ljava/lang/Object;	←Object#clone()の呼び出し
   4:   checkcast       #1; //class jp/hishidama/sample/covariant/CloneSample
   7:   areturn
public java.lang.Object clone() throws java.lang.CloneNotSupportedException;
  Exceptions:
   throws java.lang.CloneNotSupportedException  Code:
   Stack=1, Locals=1, Args_size=1
   0:   aload_0
   1:   invokevirtual   #27; //Method clone:()Ljp/hishidama/sample/covariant/CloneSample;	CloneSampleを返すclone()の呼び出し
   4:   areturn

ついでに、このCloneSampleをさらに派生して新しいクラスを作り、clone()もオーバーライドして別クラスを返すようにしたら3つ目のメソッドが出来るかと思って試してみた。

class Clone2 extends CloneSample {

	@Override
	public Clone2 clone() throws CloneNotSupportedException {
		return (Clone2) super.clone();
	}
}
>javap Clone2
Compiled from "CloneSample.java"
class jp.hishidama.sample.covariant.Clone2 extends jp.hishidama.sample.covariant.CloneSample{
    jp.hishidama.sample.covariant.Clone2();
    public jp.hishidama.sample.covariant.Clone2 clone()       throws java.lang.CloneNotSupportedException;
    public jp.hishidama.sample.covariant.CloneSample clone()       throws java.lang.CloneNotSupportedException;
}

どうやら、すぐ上のクラス(スーパークラス)のメソッドのオーバーライド分だけが作られるようだ。


では、リフレクションはどうなるか。
(リフレクションでメソッドを取得するには メソッド名と引数の型だけを指定し、戻り値の型は指定しない為)

	Class<CloneSample> cls = CloneSample.class;

	System.out.println("●getMethod()");
	Method m = cls.getMethod("clone", new Class[] {});
	System.out.println(m.getReturnType()); //戻り値の型を表示

	System.out.println("●getDeclaredMethod()");
	Method d = cls.getDeclaredMethod("clone", new Class[] {});
	System.out.println(d.getReturnType()); //戻り値の型を表示

	System.out.println("●getMethods()");
	Method[] ms = cls.getMethods();
	for (Method i : ms) {
		if (i.getName().equals("clone")) {
			System.out.println(i);
			System.out.println("橋 …" + i.isBridge());
			System.out.println("合成…" + i.isSynthetic());
		}
	}

実行結果:

●getMethod()
class jp.hishidama.sample.covariant.CloneSample
●getDeclaredMethod()
class jp.hishidama.sample.covariant.CloneSample
●getMethods()
public jp.hishidama.sample.covariant.CloneSample jp.hishidama.sample.covariant.CloneSample.clone() throws java.lang.CloneNotSupportedException
橋 …false
合成…false
public java.lang.Object jp.hishidama.sample.covariant.CloneSample.clone() throws java.lang.CloneNotSupportedException
橋 …true
合成…true

単独のメソッドを取得するgetMethod()getDeclaredMethod()では、自分で定義した(戻り値の型が異なっている)メソッドが取得されている。
全メソッドを取得するgetMethods()ではどちらも取得できる。

ただ、橋渡しメソッド(ブリッジメソッド)かどうかを判断するisBridge()や、合成メソッドかどうかを判断するisSynthetic()は値が異なっている。
定義したメソッドを呼ぶ側(戻り値の型が親クラスと一致しているメソッド)は橋渡しメソッドになっているようだ。
(合成メソッドってのは よく分からないけど、コンパイラが生成するものらしく、ソースには書けないらしい)


Java目次へ戻る / 新機能へ戻る / 技術メモへ戻る
メールの送信先:ひしだま