Javaは「強く型付けされている(strong typed)」プログラミング言語なので、コンパイル時に型の互換性に問題が見つかった場合には、コンパイルエラーが発生します。実行時にも、型の互換性で問題が見つかった場合には、実行時エラーが発生します。
参照型の互換性について単純に説明すると、ある型Aの変数へ、ある型Bのオブジェクトを代入できれば、「AはBと互換性がある」ということになりますし、代入できなければ、「AはBと互換性がない」ということになります。「class A {}」と「class B extends A {}」があったときに、「A a; B b = new B(); a=b;」のような処理は問題なくできます。
しかし、「「A a = new A(); B b; b=a;」のような処理はできません。BはAをextendsしているので、Aの機能を持っていることになり、互換性があります。一方、AはBのスーパークラスですから「BにはAが持っていない機能がある」ということになります。このため、互換性がありません。
メソッドの戻り値型についても、代入互換性が必要です。例えば、次のようなメソッドでは、java.util.LinkedListがjava.util.Listの実装クラスであり、互換性があるのでコンパイルエラーにはなりません。
public java.util.List createList() { return new java.util.LinkedList(); }
しかし下記の例では、コンパイルエラーとなります。
public java.util.LinkedList createLinkedList() { java.util.List list = new java.util.LinkedList(); return list; }
java.util.LinkedList型のインスタンスはjava.util.List型と互換性があるので、その型の変数listへ代入できます。しかし、java.util.List型の値として扱われるようになってしまうと、戻り値型のjava.util.LinkedListと互換性がなくなってしまいます。java.util.List型では、java.util.LinkedList型に必要な機能のうち、足りないものがあるからです。
ここまでの話を整理してみましょう。継承関係にある型については階層構造になります。例えば、次のような4つのクラス、「A」「B extends A」「C extends A」「D extends B」があったとすると、次のような階層となります。
A +-- B | +-- D | +-- C
これを「型階層」といい、上位の型は階層の下位の型よりも、「より広い(wider)」型ですし、下位の型はスーパークラスよりも、「より狭い(narrower)」型です。
「広い型への変換(Widerning conversion)」は明示的に指定しなくても、暗黙のうちにできます。明示的に指定しても構いません。逆に、「狭い型への変換(Narrowing conversion)」については、「キャスト(cast)」により明示的に指定が必要です。
キャストをするためには、変換したい値の前に「(型名)」のように記述したコードを付けます。例えば、変数oがあったとして、「(Object)o」のように書きます。
「(Object)"Hello"」はString型をスーパークラスのObject型へ変換しています。これは「アップキャスト」といい、安全です。
逆に、サブクラスへキャストすることを「ダウンキャスト」といい、こちらは安全ではありません。キャストで互換性がない場合は、問題となります。参照型においては問題のあるキャストをすると、「java.lang.ClassCastException」という「例外」が発生します。
例外については、本連載で別途説明する予定となっているので、ここでは問題となることだけ理解しておいてください。この問題を発生させないようにするためには、ダウンキャストをする際に「instanceof演算子を使って型のチェックをする」という方法があります。
if (o instanceof Integer) { Integer x = (Integer)o; }
ここまで説明した型変換について理解しておけば、代入時のエラーについて対応ができるようになります。ここで、「null」は特別です。これを記述した場合は、nullオブジェクトを参照することになりますが、このオブジェクトは、すべての参照型と代入互換性があります。参照型には、配列も含みます。
文章だとよく分からないかもしれませんが、「次のコードではエラーにならない」といわれると理解できるはずです。A型の変数aへnullは代入できますし、B型の変数bへnullは代入できます。nullは型A、型Bと互換性があるので、代入ができます。
A a = null; B b = null;
ここまでの内容を理解するために、簡単なプログラムを作成して動作させてみましょう。instanceof演算子の使い方、ダウンキャストの方法、「null」の代入について示しています。
package sample18; public class Sample05 { static class App { void print(Object v) { //instanceof演算子で確認 if (v instanceof Integer) { // ダウンキャスト Integer i = (Integer)v; System.out.println("Object -> Integer: " + i); } else { System.out.println(v.toString()); } } // 戻り値型のjava.util.Listへ暗黙の変換 public java.util.List createList() { return new java.util.LinkedList(); } /* このメソッドはコンパイルエラーとなる public java.util.LinkedList createLinkedList() { java.util.List list = new java.util.LinkedList(); return list; } */ } public static void main(String[] args) { App app = new App(); java.util.List list = app.createList(); list.add(Integer.valueOf(1)); list.add(Boolean.valueOf(true)); //明示的なアップキャスト list.add((Object)"Hello"); list.add(app); for (Object o: list) { app.print(o); } app = null; // nullは特別 list = null; // nullは特別 } }
ダウンキャストをする場合は、instanceof演算子をよく利用しますから、セットで覚えておきましょう。
Javaでは、基本データ型の値は対応するラッパーオブジェクトに自動変換されます。これを「ボクシング変換(boxing conversion)」といいます。これにより、どんな式の値もObject型の変数に代入できます。
逆に、ラッパーオブジェクトから基本データ型の値を取り出すこともできます。これを「アンボクシング変換(unboxing conversion)」といいます。
ただし、これらはJava 5以降の話なので、Java 5より前のバージョンでは自分で変換する必要があります。
package sample18; public class Sample06 { public static void main(String[] args) { // Java5以降 { // ボクシング変換 Integer o1 = 1; Double o2 = 1.0; Boolean o3 = true; // アンボクシング変換 int x1 = o1; double x2 = o2; boolean x3 = o3; } // Java5より前 { Integer o1 = Integer.valueOf(1); Double o2 = Double.valueOf(1.0); Boolean o3 = Boolean.valueOf(true); int x1 = o1.intValue(); double x2 = o2.doubleValue(); boolean x3 = o3.booleanValue(); } } }
この機能のおかげで、Java 5以降は手動変換が不要となってプログラムのコードが随分見やすく記述できるようになりました。
ここで、オーバーロード機能を使ったときに、どうなるか気になる読者もいるのではないでしょうか。そこで次のように、int型、double型、Double型をパラメータとして持つ同名のメソッドを用意して、そのメソッドへ、int型、Integer型、double型、Double型の値を渡して実行してみましょう。
package sample18; public class Sample07 { static class PrintApp { public void print(int v) { Integer i = Integer.valueOf(v); String s = i.toString(); System.out.println(s); } public void print(double v) { Double d = Double.valueOf(v); String s = d.toString(); System.out.println(s); } public void print(Double v) { System.out.println("Double:"+v); } } public static void main(String[] args) { PrintApp app = new PrintApp(); app.print(1); app.print(Integer.valueOf(2)); app.print(2.0); app.print(Double.valueOf(2.0)); } }
実行結果は、次のようになります。
1 2 2.0 Double:2.0
Integer型の値を渡したときは、アンボクシング変換により「print(int v)」が呼び出されていることが分かります。一方、Double型ではアンボクシング変換はされずに、きちんと「print(Double v)」が呼び出されていることが分かります。
とはいえ、普通は基本データ型用のメソッドとラッパークラス用のメソッドの両方を用意する必要はないはずです。どちらか一方で対応するようにしましょう。
このように基本データ型とラッパークラス型では、ボクシング変換、アンボクシング変換という便利な変換が暗黙のうちに適用されています。次ページでは、基本データ型における型の互換性について説明します。
Copyright © ITmedia, Inc. All Rights Reserved.