インタフェースのキャスト
インタフェースのキャストは特殊です。
クラスの場合、「8.5 キャストできる? できない?」で説明したように、「継承関係が明確な場合」のみキャストが可能です。コンパイラは参照型変数の型を見て、「継承関係にあるはずがない型」へキャストしようとした場合にコンパイルエラーを出して「多分ダメだよ」と教えてくれていました。
ところが、インタフェースの場合は別です。
普通のクラスの参照型変数からインタフェースの参照型変数へのキャストは、コンパイルエラーになりません。
実際に試してみましょう。
/**
* とあるインタフェース。
*/
interface AnInterface
{
void printMyName();
}
/**
* とあるクラス。
*/
class AClass
{
void print()
{
System.out.println( "AClass" );
}
}
/**
* とあるクラスのサブクラスで、
* とあるインタフェースの実装クラス。
*/
class AClassSub extends AClass implements AnInterface
{
public void printMyName()
{
System.out.println( "AClassSub" );
}
}
/**
* 実行用クラス。このクラスを実行してください。
*/
class AClassRunner
{
public static void main( String[] args )
{
// AClassクラスのインスタンスを1つ作ります。
AClass refAClass = new AClass();
try
{
// 参照をAnInterfaceインタフェースの
// 参照型変数へコピーします。
AnInterface refAnInterface = (AnInterface)refAClass;
refAnInterface.printMyName();
// コンパイルは通りますが、例外は投げられます。
}
catch( ClassCastException e )
{
System.out.println( "例外が投げられました。" );
}
// 実行結果:
// 例外が投げられました。
// AClassSubクラスのインスタンスを1つ作ります。
// 参照型変数の型はAClassクラスです。
AClass refAClass2 = new AClassSub();
// 参照をAnInterfaceインタフェースの
// 参照型変数へコピーします。
AnInterface refAnInterface = (AnInterface)refAClass2;
refAnInterface.printMyName();
}
}
// AClassRunner.java /** * とあるインタフェース。 */ interface AnInterface { void printMyName(); } /** * とあるクラス。 */ class AClass { void print() { System.out.println( "AClass" ); } } /** * とあるクラスのサブクラスで、 * とあるインタフェースの実装クラス。 */ class AClassSub extends AClass implements AnInterface { public void printMyName() { System.out.println( "AClassSub" ); } } /** * 実行用クラス。このクラスを実行してください。 */ class AClassRunner { public static void main( String[] args ) { // AClassクラスのインスタンスを1つ作ります。 AClass refAClass = new AClass(); try { // 参照をAnInterfaceインタフェースの // 参照型変数へコピーします。 AnInterface refAnInterface = (AnInterface)refAClass; refAnInterface.printMyName(); // コンパイルは通りますが、例外は投げられます。 } catch( ClassCastException e ) { System.out.println( "例外が投げられました。" ); } // 実行結果: // 例外が投げられました。 // AClassSubクラスのインスタンスを1つ作ります。 // 参照型変数の型はAClassクラスです。 AClass refAClass2 = new AClassSub(); // 参照をAnInterfaceインタフェースの // 参照型変数へコピーします。 AnInterface refAnInterface = (AnInterface)refAClass2; refAnInterface.printMyName(); } }
AnInterfaceインタフェースとAClassクラスがあります。
* とあるインタフェース。
*/
interface AnInterface
{
void printMyName();
}
/**
* とあるクラス。
*/
class AClass
{
void print()
{
System.out.println( "AClass" );
}
}
/** * とあるインタフェース。 */ interface AnInterface { void printMyName(); } /** * とあるクラス。 */ class AClass { void print() { System.out.println( "AClass" ); } }
この2つのクラスは全然関係ないクラスです。
さて、まずAClassクラスのインスタンスを作り、AClassクラスの参照型変数に参照を入れます。
AClass refAClass = new AClass();
// AClassクラスのインスタンスを1つ作ります。 AClass refAClass = new AClass();
次が問題です。
refAClass変数に入っている参照を、AnInterfaceインタフェースの参照型変数にキャストしてコピーします。
これが「クラスの参照型変数にキャストしてコピー」だったらコンパイルエラーになるところですが、AnInterfaceインタフェースは「インタフェース」なので、できちゃいます。
// 参照型変数へコピーします。
AnInterface refAnInterface = (AnInterface)refAClass;
// 参照をAnInterfaceインタフェースの // 参照型変数へコピーします。 AnInterface refAnInterface = (AnInterface)refAClass;
このように、何気なくキャストできちゃいます。
ただし、これは「コンパイルが通る」というだけで、例外は投げられます。実行時にClassCastException例外が投げられるので実際はできませんが、少なくともコンパイルは通っちゃうわけです。
なぜこのようなことができるのでしょう。
「8.5 キャストできる? できない?」の理屈で考えれば、AClassクラスとAnInterfaceインタフェースには何の関係もないので、コンパイラが「キャストできない」とエラーにするはずです。でもなりません。なぜでしょう。
その理由は、前ページで紹介した「インタフェースから実装しつつ、クラスから継承することができる」というインタフェースの特殊な機能があるからです。
コンパイラは「(AnInterface)refAClass」というキャストを見たとき、「refAClass変数が指しているインスタンスがAnInterfaceインタフェースを実装している可能性」を考えます。
たとえば、AClassクラスにサブクラスがあり、そのクラスがAnInterfaceインタフェースを実装している……そんな、コンパイラにとって未知のクラスがあり、そのインスタンスを参照しているとしたら。
もしそうだとしたら、AnInterfaceインタフェースへのキャストを許さなければいけません。
だって、実際にインスタンスはAnInterfaceインタフェースを実装しているのですから。
これを実際に試してみましょう。
AClassクラスのサブクラスで、AnInterfaceインタフェースの実装クラスである、AClassSubクラスを用意します。
* とあるクラスのサブクラスで、
* とあるインタフェースの実装クラス。
*/
class AClassSub extends AClass implements AnInterface
{
public void printMyName()
{
System.out.println( "AClassSub" );
}
}
/** * とあるクラスのサブクラスで、 * とあるインタフェースの実装クラス。 */ class AClassSub extends AClass implements AnInterface { public void printMyName() { System.out.println( "AClassSub" ); } }
このクラスのインスタンスを作り、スーパークラスであるAClassクラスの参照型変数に参照を入れます。
// 参照型変数の型はAClassクラスです。
AClass refAClass2 = new AClassSub();
// AClassSubクラスのインスタンスを1つ作ります。 // 参照型変数の型はAClassクラスです。 AClass refAClass2 = new AClassSub();
この状態で、AnInterfaceインタフェースへキャストします。
// 参照型変数へコピーします。
AnInterface refAnInterface = (AnInterface)refAClass2;
// 参照をAnInterfaceインタフェースの // 参照型変数へコピーします。 AnInterface refAnInterface = (AnInterface)refAClass2;
これはコンパイルが通りますし、実行時にも例外が投げられません。
refAClass2変数のAClassクラスとキャスト先のAnInterfaceインタフェースを比較した場合、どちらも関係ない型ですが、refAClass2変数が指しているインスタンスがAnInterfaceインタフェースを実装している可能性があるため、コンパイラはこのキャストを許します。
また、refAClass2変数が参照しているインスタンスはAnInterfaceインタフェースを実装していますから、実行時にも可能なわけです。
まとめましょう。
キャスト元の参照型変数の型から、インスタンスの型は「そのクラスかサブクラス」ということが分かります。
でも、その情報から「キャスト先のインタフェースを実装している」ということは分かりません。
なので、コンパイルは可能で、実行時にキャストしてみて、実装していればOK、実装していなければ例外を投げる、というわけです。
これを、クラスの継承関係で考えてみましょう。
コンパイルするときには、refAClass2変数が参照しているインスタンスの型はわかりません。
とりあえず「AClassクラスか、そのサブクラス」ということは分かるのですが、それ以上のことは分かりません。
また、コンパイラは「AClassクラスのすべてのサブクラス」を知りません。
クラスは実行中に自由に読み込むことができるため、すべてのクラスをコンパイル時に知ることは不可能だからです。
AClassクラスのサブクラスは、自由にインタフェースを実装できますから、インスタンスがAnInterfaceインタフェースを実装しているかどうかは、実際にインスタンスを見てみないとわからないわけです。
なので、コンパイルするときにはキャストを許可して、実行時にインスタンスがAnInterfaceインタフェースを実装しているかチェックし、実装していたらキャストを許可して、実装していなかったら例外を投げるわけです。
つまり、無数に存在するかもしれないサブクラスが「インタフェースを実装している」可能性がある以上、すべてのクラスからすべてのインタフェースへのキャストを許可しなければいけないわけです。
これが、クラスへのキャストとの違いです。
クラスの場合、インタフェースのように「サブクラスが他のクラスから継承している」なんてことは絶対にありません。なので、継承関係になければキャストしても意味ないことが確実に分かるので、コンパイラはエラーにします。
でもインタフェースは「サブクラスでインタフェースを実装している」可能性があるため、コンパイラはキャストを許可します。
2つのクラスから継承することはできないけど、1つのクラスから継承するときに1つのインタフェースから実装することはできる――その違いが、コンパイラにインタフェースへのキャストを許すわけです。