Upgrade to Pro — share decks privately, control downloads, hide ads and more …

ジェネリクスについて/Java Generics

ジェネリクスについて/Java Generics

Java初心者向けの勉強会用資料

Toshiyuki Imaizumi

May 29, 2020
Tweet

More Decks by Toshiyuki Imaizumi

Other Decks in Programming

Transcript

  1. ジェネリクスとは • 型パラメータを使用してクラスやメソッドを作成する機能のこと • 型パラメータ:List<String>の<String>の部分 • 型パラメータを指定できるクラスを「ジェネリック型」という • List<String>のように型パラメータが指定された型を「パラメータ化された型」 という

    • 型パラメータを使うことで、同じクラスでもインスタンスごとに違う型を扱うこと ができるようになる • Java1.5から追加された機能 • それまで実行時(runtime)に発覚していたエラーを コンパイル時(compile-time)に発見できるようにすることを目的として追加さ れた 3
  2. ジェネリクスがない時代のコード • コレクションフレームワークを利用する際、 要素を取得する時にキャストが必須だった class ArrayList { private Object[] elements;

    public void add(Object elem){…} public Object get(int idx){…} } List strList = new ArrayList(); strList.add(“aaa”); strList.add(“bbb”); for(int i=0;i<strList.size();i++){ String str = (String)strList.get(i); } コレクションを利用するコード コレクションの実装 5
  3. • 実行時にClassCastExceptionが出ても、原因箇所が分からない • この例はシンプルだが、実際はListが様々なクラスを渡り歩いている 可能性もあり、気づきにくい ジェネリクスがない時代の問題点 private void exec(){ List

    strList = new List(); complexCalculate(strList); for(int i=0;i<strList.size();i++){ String str = (String)strList.get(i); //実行時エラー } } private void complexCalculate(List list){ list.add(1); // Stringを想定したListに数値を入れてしまう } 6
  4. ジェネリクスがあると • 型パラメータを指定していれば、コレクション利用時に ClassCastExceptionが出ることはない private void exec(){ List<String> strList =

    new ArrayList<String>(); complexCalculate(strList); for(int i=0;i<strList.size();i++){ String str = strList.get(i); //キャストは不要 } } private void complexCalculate(List<String> list){ list.add(1); //コンパイルエラー } 7
  5. ジェネリック型を作成する • クラスの宣言時にクラス名の後ろに型パラメータを追加する • TypeのTやElementのEが使われることが多いが、任意の名称が可能 • 型パラメータは指定されると全ての箇所でその型になる • 複数の型パラメータも指定可能 class

    MyList<T>{ private T[] elements; public boolean add(T elem){} public T get(int idx){} } class MyList{ private String[] elements; public boolean add(String elem){} public String get(int idx){} } MyList<String>と宣言すると そのインスタンスは 右になるイメージ(厳密には違う) class MyMap<K,V>{ private V put(K key,V value); } 9
  6. ジェネリックメソッドを作成する • インスタンス単位ではなく、メソッド単位でも型パラメータを使用可能 (ジェネリックメソッド) • 返り値の型の前に型パラメータを書く • 引数から型を自動的に推論するため、利用時に型を指定する必要はない • ジェネリック型と同様に、複数の型パラメータも使用可能

    public static <T> List<T> add(List<T>list ,T value) { if(list != null){list.add(value);} return list; } List<String> strList; add(strlist,"hoge"); List<Integer> intList; add(intList,1); ジェネリックメソッド 利用箇所 public static <K, V> V put(Map<K, V> map, K key, V val) {…} 10
  7. ジェネリッククラスが不変である理由 • なぜList<String>はList<Object>に代入できないか • 型安全が崩れ、コンパイル時にエラーを発見するという目的が達成できない • もし代入できると、例えば以下のようなコードが書けてしまう List<String> strList =

    new ArrayList<String>(); List<Object> objList = strList; //本来はここでコンパイルエラー objList.add(1); //Stringでない値がstrListに入ってしまう! for(int i=0;i<strList.size();i++){ String str = strList.get(i); //ClassCastExceptionが発生する } 14
  8. 任意の型パラメータで受けたい場合 • 「リストがnullか空ならばtrueを返す」というユーティリティメソッドを考える • 原型は使うべきではないので、型パラメータを指定する必要がある • この処理はどんな型でもいいので、試しにObjectにしてみる • 上記メソッドを利用しようとすると、List<Object>以外は引数に渡せない •

    これはかなり使いづらい List<String> stringList; if(isEmpty(stringList)){ … } // コンパイルエラー public boolean isEmpty(List anyList){ return anyList == null || anyList.isEmpty(); } public boolean isEmpty(List<Object> anyList){…} 16 List<String> stringList; List<Object> objectList = new ArrayList<>(stringList); if(isEmpty(objectList)){ … } //OK
  9. ワイルドカードで対応できないケース • ワイルドカードを指定した型は、以下の性質となる • 型パラメータが引数に現れるメソッドは呼べなくなる • 例:List<?> anyList に対し、anyList.add("hoge")を呼ぶとエラー •

    返り値の型が型パラメータであるメソッドの返り値の型はObjectとなる • 例:List<?> anyList に対し、anyList.get(0)を呼ぶと返り値の型はObjectとなる • このため、以下のようなコードは書けない • 実際に渡されるlist1,list2が同じ型であってもNG • 上記のようなケースではジェネリックメソッドを使う public static void add(List<?> list1, List<?> list2) { list1.add(list2.get(0)); //コンパイルエラー } 18
  10. ジェネリックメソッドよりワイルドカードがよい 場合 • 複数のワイルドカードを指定した場合、それぞれ別の型を渡せる • ジェネリックメソッドを使う場合、上記のケースはエラーになってしまう public int size(List<?> list1,

    List<?> list2){ return list1.size() + list2.size(); } List<String> strList; List<Integer> intList; int size = size(strList, intList); //OK 20 public <E> int size(List<E> list1, List<E> list2){ return list1.size() + list2.size(); } List<String> strList; List<Integer> intList; int size = size(strList, intList); //コンパイルエラー
  11. ジェネリックメソッドで上限付き境界型パラ メータを使う • ジェネリックメソッドでも境界型パラメータを利用できる • 上記だとまだ使いづらい部分が残る public static <T extends

    Storable> void add(List<T> list1, List<T> list2) { Storable elem =list2.get(0); // 境界型パラメータを使っているので返り値はStorableになる if (elem.getId() != null){ list1.add(elem); //ワイルドカードでないので引数が型パラメータのメソッドも呼べる } } List<Storable> storableList; List<User> userList; add(storableList, storableList); // OK。T=Storable add(userList, userList); // OK。T=User add(storableList, userList); //Tの型が異なるためコンパイルエラー 24
  12. ジェネリックメソッドとワイルドカードを組み合 わせる • 型が完全一致していなくてもよい場合、以下のようにワイルドカードと 組み合わせると使いやすくなる • list2の型パラメータにはlist1の型パラメータを継承した型(list1と同じ型も含む) • list1の型パラメータはStorableを継承した型 •

    →list2の型パラメータはStorableを継承した任意の型 • 上記だとさきほどのコードの「add(storableList, userList)」はOK public static <T extends Storable> void add(List<T> list1, List<? extends T> list2) { Storable elem =list2.get(0); if (elem.getId() != null){ list1.add(elem); } } 25
  13. まとめ • ジェネリック型は原型(List<E>に対するList)では使わないこと • 任意の型パラメータで受けたい場合、ワイルドカードを利用する (List<?>) • 引数や返り値の型に相関関係がある場合、ワイルドカードではなく ジェネリックメソッドを使う(List<T>) •

    型パラメータに特定のクラスを継承しているという制約を付けたい 場合は、上限付き境界型パラメータを利用する(List<? extends E>) • 型パラメータに特定のクラスの親であるという制約を付けたい場合 は下限付き境界型パラメータを利用する(List<? super E>) 28