5.32:ジェネリクスと可変長引数を注意して組み合わせる
要点
ジェネリクス + varargs は heap pollution を引き起こしやすい。@SafeVarargs を付けられる条件を満たし、かつ「varargs 配列を外部に露出しない」実装にするか、そもそも varargs を使わない設計にするのが正解。
なぜ問題になるか
- T... elems は実行時には T[] elems(配列)となる。
- ジェネリクスは実行時に型情報が消える(型消去)ため、T[] が実行時にはただの Object[] 的に扱われうる。
- その配列が他の参照経路から不適切な型で書き換えられると、コンパイル時の型期待とヒープ上の実体が食い違い、取り出したときに ClassCastException を招く(=heap pollution)。
悪い例
1. ジェネリック varargs をそのまま返す / 配列を露出させる
// NG: elems 配列を backing にする Arrays.asList を返している → 配列が外に出る
public static <T> List<T> bad1(T... elems) {
return Arrays.asList(elems);
}
2. パラメタライズド型の可変長引数
// NG: List<String>... はコンパイラが unchecked 警告を出す典型例
public static void dangerous(List<String>... stringLists) {
Object[] array = stringLists; // varargs配列は実際にObject[]
array[0] = Arrays.asList(42); // List<Integer> を突っ込めてしまう
String s = stringLists[0].get(0); // ClassCastException が発生
}
➡ これが典型的な heap pollution。stringLists の要素が List< String > のはずが List< Integer > になってしまう。
良い例
1. 可能なら varargs を使わない
- 代わりに Collection< ? extends T > や List< ? extends T> を受ける。
public <T> void addAll(Collection<? extends T> c) { ... } // 柔軟で安全
2. どうしても varargs を提供するなら 配列を外に出さない実装 & @SafeVarargs
- メソッドを static または final にし、かつ配列をそのまま返したり、配列をバックにしたビューを返さない。
@SafeVarargs
public static final <T> List<T> safeListOf(T... elems) {
// elems 配列の要素をコピーして新しいコレクションを返す
List<T> list = new ArrayList<>(elems.length);
Collections.addAll(list, elems);
return list; // 配列は外に出ない -> 安全
}
- @SafeVarargs は「このメソッドは型安全に varargs を扱っている」と宣言するもの。付ける前に実装が本当に安全か自分で保証する必要があります。
3. パラメタライズド型の varargs(例:List< String >...)は避ける
- List< String >... のような宣言はコンパイル時に unchecked 警告が出やすく、非常に危険。代替は List< ? extends T > の集合を受け取る API にする。
4. どうしても配列を使う必要がある場合(最小化して使う)
- @SuppressWarnings("unchecked") を 局所化して使う(コメントでなぜ安全かを明記)。
@SuppressWarnings("unchecked")
public static <T> void legacyVariant(Object o) {
List<T>[] arr = (List<T>[]) new List<?>[10]; // unsafe but documented and local
// ... 小さく限定して使う
}
→ ただし可能な限り避ける。
まとめ
- 基本方針:ジェネリックな varargs は便利だが危険。公開 API では避けるか、@SafeVarargs を使える条件を満たして「配列を露出しない」実装に限定する。
- 代替:Collection< ? extends T >/List< ? extends T > を受ける API にして、必要なら利便性のために安全な varargs オーバーロードを薄く置く。
- 警告:配列をバックにするビュー(Arrays.asList(elems))をそのまま返す実装は避ける。