raw type とワイルドカードの使い分け
型引数を省くと型安全が失われます。List<Object>とList<?>の違いも整理します。
エラーメッセージの読み方
Main.java:8: error: incompatible types: Object cannot be converted to String
Main.java- ファイル名
8- 行番号 — javacが異常に気づいた位置。原因の行とは限りません
error- 種別 — コンパイルエラー。実行まで到達しません
incompatible types: Object cannot be converted to String- 内容 — javacが期待していたもの、または見つけられなかったもの
このエラーが出る典型パターン
パターン1
1 import java.util.ArrayList; 2 import java.util.List; 3 4 public class Main { 5 public static void main(String[] args) { 6 List list = new ArrayList<>(); ^ 7 list.add("a"); 8 String s = list.get(0); 9 System.out.println(s); 10 } 11 }
型引数を省くと生の型になり、要素はObjectとして扱われます。取り出すたびにキャストが必要になります。
直し方: (空) を <String> にします。
パターン2
1 import java.util.List; 2 3 public class Main { 4 static void show(List<Object> list) { ^ 5 for (Object o : list) System.out.println(o); 6 } 7 public static void main(String[] args) { 8 List<Integer> nums = List.of(1, 2, 3); 9 show(nums); 10 } 11 }
List<Integer>はList<Object>の部分型ではありません。何でも受けるにはワイルドカード ? を使います。
直し方: Object を ? にします。
パターン3
1 public class Main { 2 static <T> T first(java.util.ArrayList <T> list) { ^ 3 return list.get(0); 4 } 5 public static void main(String[] args) { 6 System.out.println(first(java.util.List.of("a"))); 7 } 8 }
引数は最も広い型で受けるのが原則です。ArrayListで受けるとListしか持たない呼び出し側が渡せません。
直し方: java.util.ArrayList を java.util.List にします。
パターン4
1 import java.util.ArrayList; 2 import java.util.List; 3 public class Main { 4 public static void main(String[] args) { 5 List list = new ArrayList(); 6 list.add("a"); 7 list.add(1 ); ^ 8 for (Object o : list) { 9 String s = (String) o; 10 System.out.println(s); 11 } 12 } 13 }
raw型のListは要素の型をコンパイル時にチェックしません。型の違う値が紛れ込んでも、実際にキャストするまで気づけません。
直し方: 1 を "b" にします。
パターン5
1 import java.util.ArrayList; 2 import java.util.List; 3 public class Main { 4 static List getRaw() { 5 List list = new ArrayList(); 6 list.add(42 ); ^ 7 return list; 8 } 9 public static void main(String[] args) { 10 List<String> list = getRaw(); 11 String s = list.get(0); 12 System.out.println(s); 13 } 14 }
raw型を返すメソッドは呼び出し側の型チェックをすり抜けます。警告だけでコンパイルが通り、実際に取り出すまで型の不一致が発覚しません。
直し方: 42 を "ok" にします。
よくある誤解
警告だけでコンパイルは通ります。通ってしまうからこそ、本番で ClassCastException として現れます。
実務での勘所
raw typeが今も使える(禁止されていない)のは、Java 5でジェネリクスを導入したときに、それ以前に書かれた大量のコードを壊さないための後方互換性を優先したためです。ジェネリクスの型情報はコンパイル時にしか存在せず、コンパイル後のバイトコードでは型引数の情報が消える(型消去、type erasure)という実装方式が採られました。この方式のおかげでジェネリクス登場前と登場後のコードが同じJVM上で混在して動きますが、その代償としてraw typeを使えばコンパイラの型検査をすり抜けられてしまいます。