ジェネリクス

Effective Java 第 3 版の個人的メモ

  1. 項目 26 原型を使わない
  2. 項目 27 無検査警告を取り除く
  3. 項目 28 配列よりリストを選ぶ
  4. 項目 29 ジェネリック型を使う
  5. 項目 30 ジェネリックメソッドを使う
  6. 項目 31 API の柔軟性向上のために境界ワイルドカードを使う
  7. 項目 32 ジェネリックと可変長引数を注意して組み合わせる
  8. 項目 33 型安全な異種コンテナを検討する
  9. 参考 URL

1. 項目 26 原型を使わない

  • Java 1.4 までジェネリクスがなかった。
  • Java 5 でジェネリクスが登場した。

1.1. 結論

ジェネリクスの型パラメータが指定できるときは必ず指定しましょう。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
public class Test {

    public static void main(String[] args) {
        List list = new ArrayList();
        // どんな型でも入れれてしまう。
        list.add("hello");
        list.add(111);
        list.add(new DTO("user01", 11));

        System.out.println(list);
        // 実行結果
        // [hello, 111, Test.DTO(name=user01, age=11)]
    }

    @Data
    @AllArgsConstructor
    private static class DTO {
        String name;
        int age;
    }
}

List<String> list = new ArrayList(); とすれば Integer や DTO をいれようとすると コンパイルエラーになるので、早期検出できる。

  • Java 1.4 までだと、誤って Integer を add しても、実行時にしかわからなかった。
  • 結合テストなどまですり抜けていきやすい。
  • 事象が観測された時点であちこちに散財している状態になっている。
  • ジェネリクス変数には色々な書き方がある。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
// raw
List list1 = new ArrayList();

// Java7まではこうかかないとダメ
List<String> list2 = new ArrayList<String>();

// Java8で右辺のパラメータを省略できるようになった
List<String> list3 = new ArrayList<>();

// Java11でvarが使える
var list4 = new ArrayList<String>();

// これはダメな書き方
List list5 = new ArrayList<String>();
list5.add(111);

2. 項目 27 無検査警告を取り除く

2.1. 結論

@SuppressWarnings(“unchecked”) は気をつけて使いましょう

  • @SuppressWarnings("unchecked")は、クラス、メソッド、変数宣言に付与できる。
    • 付与すると、その範囲の「型チェック実施していない」警告を抑止する。
    • 大きな範囲を抑止すると、抑止しようと思った対象以外も、抑止してしまうので、最小範囲で抑止すること。

3. 項目 28 配列よりリストを選ぶ

3.1. 結論

配列とリストは次の 2 点で大きく異なる。 リストの方は、コンパイル時に型安全性を提供するから、バグが発生しにくい。(配列は実行時に型安全性を提供する。)

  • 配列は共変で、リストは不変。

    • 共変:SubSuperのサブタイプであれば、配列型Sub[]Super[]のサブタイプということ。
    • 不変:2 つの型Type1, Type2があったとき、List<Type1>List<Type2>には互換性がない。
  • 配列は具象化されていて、リストは erasure で実装。

    • 具象化:実行時に、その要素型を強制し、問題があればArrayStoreExceptionが発生する。(コンパイル時は確認しない。)
    • erasure:コンパイル時に型制約を強制し、実行時には要素の型情報を廃棄する。

    erasureの意味は「廃棄」

3.1.1. 配列の場合

1
2
3
  Super[] superArray = new Sub[1];
  superArray[0] = new Super();
  // 実行時にArrayStoreException

3.1.2. リストの場合

1
2
  // コンパイルエラー
  List<Type1> type1List = new ArrayList<Type2>();  // 互換性のない型

3.2. リストと配列を混在させた場合

リストと配列を混在させてしまった場合は、配列の代わりにリストを使うようにしましょう。

単純なジェネリックメソッドで確認する。

3.2.1. ステップ 1(リスト → 配列)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
static <E> E reduce(List<E> list, Function<E> f, E initVal) {
  E[] snapshot = list.toArray(); // リストを内部的にロック
  E result = initVal;
  for (E e : snapshot)
    result = f.apply(result, e);
  return result;
}
/** コンパイルエラー
Reduction.java:12: 互換性のない型
検出値 : java.lang.Object[]
期待値 : E[]
      E[] snapshot = list.toArray(); // リストを内部的にロック
                                 ^
**/

3.2.2. ステップ 2(リスト[キャスト]→ 配列)

リストを配列(E[])にキャストした。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
static <E> E reduce(List<E> list, Function<E> f, E initVal) {
  E[] snapshot = (E[]) list.toArray(); // リストを内部的にロック
  E result = initVal;
  for (E e : snapshot)
    result = f.apply(result, e);
  return result;
}
/** 警告
Reduction.java:12: 警告:[unchecked]無検査キャストです
検出値 : java.lang.Object[]
期待値 : E[]
      E[] snapshot = (E[])list.toArray(); // リストを内部的にロック
                                      ^
**/

実行時のEの型が不明で、実行時にリスト(ジェネリクス)の型情報は erasure(廃棄)されているから、キャストの安全性が検査できない。)

3.2.3. ステップ 3(リスト → リスト)

配列(E[])を使わず、リストにした。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
static <E> E reduce(List<E> list, Function<E> f, E initVal) {
  List<E> snapshot;
  synchronized(list) {
    snapshot = new ArrayList<E>(list);
  }
  E result = initVal;
  for (E e : snapshot)
    result = f.apply(result, e);
  return result;
}

4. 項目 29 ジェネリック型を使う

4.1. 結論

以下の場合は、ジェネリクスを利用する良い機会なので使ってみましょう。

  • 利用者が、メソッドからの戻り値をキャストしないといけない
  • 利用者が、実行時にキャストが失敗する可能性がある

スタックの実装をサンプルを作成します。 作成時のポイントは 2 点です。

  • 無検査キャストが安全であると明確に示せる場合、警告を抑制しましょう。
  • 利用者はキャストしなくて済むので、楽になる。

Stack.java

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
public class Stack<E> {
  private E[] elements;
  private int size = 0;
  private static final int DEFAULT_INITIAL_CAPACITY = 16;

  /*
  elements配列はpush(E)からのEインスタンスだけを含む。
  そのことは、型安全性を保証するためには十分であるが、
  配列の実行時の型はE[]ではない。常にObject[]である。
  */
  @SuppressWarnings("unchecked")
  public Stack() {
    elements = (E[]) new Object[DEFAULT_INITIAL_CAPACITY];
  }

  public void push(E e) {
    ensureCapacity();
    elements[size++] = e;
  }

  public E pop() {
    if (size == 0)
      throw new EmptyStackException();
    E result = elements[--size];
    elements[size] = null; // 廃れた参照を取り除く
    return result;
  }

  public boolean isEmpty() {
    return size == 0;
  }

  private void ensureCapacity() {
    if (elements.length == size)
      elements = Arrays.copyOf(elements, 2 * size + 1);
  }
}

Main.java

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// ユーザ側のプログラム例
public static void main(String[] args){
  var stack = new Stack<String>();
  for (var arg : args){
    stack.push(arg);
  }
  while (!stack.isEmpty()){
    System.out.println(stack.pop().toUpperCase());
  }
}

4.2. 補足 1(配列を使う理由)

Java はリストを直接はサポートしていない。

  • ArrayListなどのジェネリック型は配列を使用している。

リストよりも配列の方が、パフォーマンスが良い。

  • HashMapなどの他のジェネリック型は配列を使用している。

4.3. 補足 2(ジェネリクスの型の制限)

ジェネリクスの型が制限できる場合は、制限しましょう。

例:java.util.concurrent.DelayQueueでは、DelayQueue<E extends Delayed>としている。

5. 項目 30 ジェネリックメソッドを使う

5.1. 結論

  • ジェネリックメソッドの作り方、使い方を理解しましょう
  • 再帰型境界を使うことで、型パラメータの機能を制限できる

5.2. ジェネリックメソッド

  • ジェネリッククラスと同じように、メソッド単位で型安全性を確保できる

  • ジェネリッククラスとの文法上の違いは、メソッドの戻り値の型の手前にのように型パラメータを書くこと
    ジェネリックメソッド

    1
    2
    3
    4
    5
    
    public static <E> Set<E> union(Set<E> s1, Set<E> s2) {
        Set<E> result = new HashSet<>(s1);
        result.addAll(s2);
        return result;
    }
    

    raw タイプのメソッド

    1
    2
    3
    4
    5
    
    public static Set union(Set s1, Set s2) {
        Set result = new HashSet(s1);
        result.addAll(s2);
        return result;
    }
    

5.3. 再帰型境界

  • 再帰型境界は、T extends HogeInterfaceのように型パラメータを書くことで、T は HogeInterface が実装されている型であることを制限すること。
  • よくある利用法は Comparable インターフェース
    • 下の最大値を返す max メソッドは T を Comparable インターフェースが実装されている、つまり比較できること(compareTo メソッドが使えること)を保証しているので、最大値を求めることができる
       1
       2
       3
       4
       5
       6
       7
       8
       9
      10
      11
      
      public static <E extends Comparable<E>> E max(Collection<E> c) {
          if (c.isEmpty()){
              throw new IllegalArgumentException("Empty collection");
              E result = null;
          }
          for (E e : c){
              if (result == null || e.compareTo(result) > 0)
                  result = Objects.requireNonNull(e);
          }
          return result;
      }
      
      1
      2
      3
      
      public interface Comparable<T> {
          int compareTo(T o);
      }
      

5.4. 余談

  • 型パラメータの文字としてよく使われるTEKVは何の略なのか
表記 意味 説明
T Type 汎用的
E Element 配列の要素など
K Key 連想配列やマップのキーなど
V Value 配列の値や結果
N Number 数値を表す
A Array A[]という形式で使われる
Args Arguments C++で使われることが多い
F, Fn, Func Function コールバック関数、クロージャー、ラムダ等

6. 項目 31 API の柔軟性向上のために境界ワイルドカードを使う

6.1. 結論

  • 最大限の柔軟性のために、プロデューサーかコンシューマーを表す入力パラメータに対してワイルドカード型を使用するべき
    • PECS(producer extends, consumer super)と覚えよう!

6.2. 境界ワイルドカードを使用しない場合の問題点

  • 下記のような Stack クラスがある。

    1
    2
    3
    4
    5
    6
    
    public class Stack<E> {
        public Stack() {}
        public void push(E element) { ... }
        public E pop() { ... }
        public boolean isEmpty() { ... }
    }
    
  • 一連の要素を受け取りそれら要素を全てスタックにプッシュするメソッド、pushAll()を下記のように追加したとする。

    1
    2
    3
    4
    5
    
    public void pushAll(List<E> src) {
        for (E e: src) {
            this.push(e);
        }
    }
    
  • このとき、Number 型の Stack を定義して、一つの Interger 変数を push することはできるが、Listを pushAll することはできない。なぜなら、Listは Listのサブクラスではないから。

    1
    2
    3
    4
    5
    6
    
    Stack<Number> numberStack = new Stack<>();
    
    numberStack.push(new Integer(1));//これはOK
    
    List<Integer> integers = ...;
    numbers.pushAll(integers);//これはコンパイルエラー
    
  • ひとつだと追加できるのに、複数だと追加できないのは非常に不便。

  • 次に、渡したリストにすべての要素を追加してくれるメソッド、popAll()を下記のように追加したとする。

    1
    2
    3
    4
    5
    
    public void popAll(List<E> dst) {
        while (!isEmpty()) {
            dst.add(pop());
        }
    }
    
  • このとき、Number 型の Stack を定義して、pop して1つの Object 変数に代入することはできるが、popAll して List代入することできない。なぜなら、Listは Listのスーパークラスではないから。

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    
    Stack<Number> numberStack = new Stack<>();
    
    numberStack.push(1);
    numberStack.push(2);
    numberStack.push(3);
    
    Object obj = numbers.pop();//これはOK
    
    List<Object> objects = ...;
    numbers.popAll(objects);//これはコンパイルエラー
    
  • これも不便。

  • 6.3. 境界ワイルドカードでの解決

    • 前述の問題は境界ワイルドカードを使うことで解決できる

    • pushAll()popAll()を下記のように境界ワイルドカードを使って定義することで、List<? extends E>は「E のサブタイプをパラメータとするリスト」、List<? super E>「E のスーパータイプをパラメータとするリスト」とすることができ、解決できる。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      
      public void pushAll(List<? extends E> src) {
          // 同じ
          ...
      }
      
      public void popAll(List<? super E> src) {
          // 同じ
          ...
      }
      

    6.4. GET & PUT 原則(PECS)

    • <? extends E><? super E>のどちらを使うか決めるための原則
      • 関数内において、ジェネリックス型の引数の役割が「プロデューサー(Producer)」であれば extends
      • 「コンシューマー(Consumer)」であれば super
        • プロデューサーは、関数内で何らかの値を生成(提供)する引数
          • pushAll()の引数 src は値を提供するのでプロデューサー
        • コンシューマーは、関数内で何らかの値を消費(利用)する引数
          • popAll()の引数 dst は値を消費するのでコンシューマー
      • 作用の方向で考えるとよい

    6.5. GET & PUT 原則の例(問題)

    • 問 1. 前項目で登場した下記の union メソッドはどのように境界ワイルドカードを付けるべきか?

      1
      2
      3
      4
      5
      
      public static <E> Set<E> union(Set<E> s1, Set<E> s2) {
          Set<E> result = new HashSet<>(s1);
          result.addAll(s2);
          return result;
      }
      
    • 問 1 の解答 s1、s2 は値を提供しているから。

      1
      2
      3
      4
      5
      
      public static <E> Set<E> union(Set<? extends E> s1, Set<? extends E> s2) {
          Set<E> result = new HashSet<>(s1);
          result.addAll(s2);
          return result;
      }
      
    • 問 2. 前項目で登場した下記の max メソッドはどのように境界ワイルドカードを付けるべきか?(ヒント:2 つ付ける。)

       1
       2
       3
       4
       5
       6
       7
       8
       9
      10
      11
      
      public static <E extends Comparable<E>> E max(Collection<E> c) {
          if (c.isEmpty()){
              throw new IllegalArgumentException("Empty collection");
              E result = null;
          }
          for (E e : c){}
              if (result == null || e.compareTo(result) > 0)
                  result = Objects.requireNonNull(e);
          }
          return result;
      }
      
    • 問 2 の解答

      1
      2
      3
      4
      5
      6
      7
      8
      9
      
      public static <E extends Comparable<? super E>> E max(Collection<? extends E> c) {
          if (c.isEmpty())
              throw new IllegalArgumentException("Empty collection");
          E result = null;
          for (E e : c)
              if (result == null || e.compareTo(result) > 0)
                  result = Objects.requireNonNull(e);
          return result;
      }
      

      E extends Comparable<? super E>の理由は以下。 [PECSの原則に従うと] Comparableは常に消費者(consumer)なので、Comparable<T>ではなくComparable<? super T>を常に使用するべき。

    • 上記の実際の例としては、ScheduledFuture インターフェースがある。

    下記のリストをComparable<E>のままの max メソッドで使おうとすると、コンパイルエラーになる。
    なぜなら、ScheduledFutureComparable<ScheduledFuture>で拡張されていないからである。
    しかし、ScheduledFutureDelayedのサブインターフェースであり、DelayedComparable<Delayed>を拡張しているので、ScheduledFutureComparable<Delayed>で比較可能である。
    したがって、Comparable<? super E>とすれば、下記のリストに max メソッドを使えるようになる。

    1
    
    List<ScheduledFuture<?>> scheduledFutures=...;
    

    7. 項目 32 ジェネリックと可変長引数を注意して組み合わせる

    7.1. 結論

    • ジェネリクスと可変長引数を同時に使用する場合は、以下を守った上で @SafeVarargs を付与する
      1. 配列を変更していない(要素に値を格納していない)
      2. 引数を呼び出し側に参照させていない(return していない)
    • コレクションをネストさせる等で対処することも検討する

    7.2. ジェネリクスと可変長引数を同時に使用すると、タイプアンセーフになる。

    以下のように、ジェネリック型の可変長引数に値を格納するのは安全でないケースが存在する。

    1
    2
    3
    4
    5
    6
    7
    
    // Mixing generics and varargs can violate type safety!
    static void dangerous(List<String>... stringLists) {
        List<Integer> intList = List.of(42);
        Object[] objects = stringLists;
        objects[0] = intList;             // Heap pollution
        String s = stringLists[0].get(0); // ClassCastException
    }
    

    なお、このような可能性があるため、コンパイル時にジェネリクスと可変長引数を組み合わせたメソッドを呼び出す部分で unchecked generic array creation for varargs parameter と警告がでる。

    とはいえ、以下の標準 API ではジェネリック型の可変長引数が定義されており、必ず問題となるわけではない。これらはタイプセーフ。

    • Arrays.asList(T... a)
    • Collections.addAll(Collection<? super T> c, T... elements)

    また、本当に安全ならば、メソッドに @SafeVarargs を付与することで、コンパイル時の警告は抑止できる。

    1
    2
    3
    4
    5
    6
    
    // java.util.Arrays#asList
    @SafeVarargs
    @SuppressWarnings("varargs")
    public static <T> List<T> asList(T... a) {
        return new ArrayList<>(a);
    }
    

    7.3. 本当にタイプセーフなのかを知るには

    可変長配列の引数が以下の両方を満たす状態ならばタイプセーフと言える

    1. 配列を変更していない(要素に値を格納していない)
    2. 引数を呼び出し側に参照させていない(return していない)

    以下は 2.に違反している NG ケース

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    
    public class Test {
        public static void main(String[] args) {
            String[] attributes = pickTwo("Good", "Fast", "Cheap"); // String型であるため、'T'に渡しても型推論ができる。
        }
    
        static <T> T[] toArray(T... args) {
            return args;
        }
        static <T> T[] pickTwo(T a, T b, T c) {
            switch(ThreadLocalRandom.current().nextInt(3)) {
            case 0: return toArray(a, b); // 型が推測できないため、Object[]型でreturnする
            case 1: return toArray(a, c); // 引数が'T'であり、そこから'T'のメソッドの渡すため、型推論ができない。
            case 2: return toArray(b, c);
            }
            throw new AssertionError(); // Can't get here
        }
    }
    

    そもそも、以下のようにすればよいという話はある。

    1
    2
    3
    4
    5
    6
    7
    
    // List as a typesafe alternative to a generic varargs parameter
    static <T> List<T> flatten(List<List<? extends T>> lists) {
        List<T> result = new ArrayList<>();
        for (List<? extends T> list : lists)
            result.addAll(list);
        return result;
    }
    

    コレクションがネストした時点で、利用側にミスを誘発しやすい。
    ネストの何段目を操作しているのか混乱しやすく、ミスをコンパイルで発見しにくくなる。
    その場合は、クラスでくるむことで、意図を明確にしやすくなる。

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    
    public class Test {
        public static void main(String[] args) {
            System.out.println(flatten(List.of(new Record<>("aaa"),new Record<>("bbb","ccc"))));
        }
    
        static <T> List<T> flatten(List<Record<T>> lists) {
            List<T> result = new ArrayList<>();
            for (Record<T> record : lists)
                result.addAll(record.getElements());
            return result;
        }
    
        @Value
        public static class Record<T> {
            List<T> elements;
    
            public Record(T... elements) {
                this.elements = List.of(elements);
            }
        }
    }
    

    8. 項目 33 型安全な異種コンテナを検討する

    8.1. 結論

    8.1.1. 前提

    • 型安全とは、String 型のインスタンスを要求したときに、その型のインスタンスを返すこと。
    • 異種コンテナとは、クラスをキー(Key)、そのクラスのインスタンスを値(Value)とするもの

    8.1.2. ポイント

    • Map は キーとバリューの対応を、型では保証していない。
    • put は何も考えずに格納する
    • get 時に辻褄が合うように型変換する

    8.2. 型パラメータの個数を可変にする方法

    Set<E> は 1 個、 Map<K,V> は 2 個とパラメータ数が固定だが、これを固定したくない(できない)ケースがある。
    代表的なものは、コンテナを実装するとき。 このケースでは、キーをパラメータ化することで様々な型を受け取れるようにする。

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    
    public class Test {
        public static void main(String[] args) {
            Favorites f = new Favorites();
            f.putFavorite(String.class, "Java");
            f.putFavorite(Integer.class, 0xcafebabe);
            f.putFavorite(Class.class, Favorites.class);
    
            String favoriteString = f.getFavorite(String.class);
            int favoriteInteger = f.getFavorite(Integer.class);
            Class<?> favoriteClass = f.getFavorite(Class.class);
    
            System.out.printf("%s %x %s%n", favoriteString, favoriteInteger, favoriteClass.getName());
        }
    
        public static class Favorites {
            private Map<Class<?>, Object> favorites = new HashMap<>();
    
            public <T> void putFavorite(Class<T> type, T instance) {
                favorites.put(Objects.requireNonNull(type), instance);
            }
    
            public <T> T getFavorite(Class<T> type) {
                return type.cast(favorites.get(type));
            }
        }
    }
    

    ポイントは以下。

    • Map は キーとバリューの対応を、型では保証していない。
    • put は何も考えずに格納する
    • get 時に辻褄が合うように型変換する

    注意点は以下。

    • raw タイプな Class オブジェクトを使って Favorites クラスのタイプセーフを破ってくる
      • putFavorite メソッドにおいて、 cast メソッドを使って確かめればよい。
    • non-reifiable(具象化可能= new できる)な型を利用できない。つまり、 String や、 String[] を格納することはできるが、 List<String> を格納することはできない。 ジェネリクスはイレイジャで実行時に消されてしまうので。

    前述の型パラメータは extends を用いて境界を設けることも可能。

    9. 参考 URL