LoginSignup
2
3

More than 5 years have passed since last update.

インフラ屋が改めてJavaをお勉強する(第2回:ジェネリクスとコレクション)

Last updated at Posted at 2018-02-11

ジェネリクス

Generic Classとは

1つ以上のタイプ変数(type valiables)つきで宣言されたクラスのことをGeneric Classという。。。
type valiablesはクラスのタイプパラメータ(type parameters)として知られ(←しらんがな)このように記載する。

Type Parameter
<TypeParameterList>
TypeParameterList
TypeParameter {, TypeParameter}

TypeParameter: Annotation Identifier TypeBound[extends TypeVariable extends ClassOrInterfaceType [+ AdditionalInterfaceType]*]
はっきりいって、よくわからん。さらにややこしい解説が公式仕様書に記載されている。。。

In a class's type parameter section, a type variable T directly depends on a type variable S if S is the bound of T, while T depends on S if either T directly depends on S or T directly depends on a type variable U that depends on S (using this definition recursively). It is a compile-time error if a type variable in a class's type parameter section depends on itself.

意訳
クラスのタイプパタメータ内では、タイプ変数"S"がタイプ変数"T"に限られている場合、タイプ変数"T"は直接タイプ変数"S"に依存する。一方で、"T"が"S"に直接依存しているか、"S"に依存する"U"に依存している場合(再帰的にいえることで"U"に依存する"V"に"T"が依存する場合も同様)、"T"は"S"に依存する。

精一杯意訳しても全くわからん。。。
とある型"T"が、別の型"S"を扱う際、その型"S"を明示的にコンパイラに伝えるための仕組みらしい。これにより、扱う型に汎用性を持たせると共に、実行時の型変換の安全性が高まると。Genericsを使えるように宣言された型を、「総称型(Generic Type)」という。その反対は、「Raw Type(生型?個別型?)」。
もう少し意訳すれば、コンパイル時(=コーディング時)に型が決まっていなくても、タイプ変数を使っておけば、呼出し時に動的に決定できるということ。なかなか際どい感じがするものの確かにそういうことなら汎用性は高まりそう(Object型として扱っておけばいいとも思ったものの、呼出せるメソッドが少ない(toString()とかgetClass()とか)ことを考えると汎用性という点では劣るか)。

class GenericHoge<T, R, HOGE> { }

一般的に、T="type", R="return", E="element", K="key", V="value"の意で指定するものの、厳格な決まりではない。HOGEとか入れてもコンパイルエラーにはならない。
で、こいつらを実際に使う際には、その時使用する型を割り当てるという手間がいる。割り当てる型を「型引数(Type Arguments)」で指定する。型引数が割り当てられた総称型のことを「パラメータ化された型(Parameterized Types)」という。

class GenericHoge<T, R, HOGE> {}
class ParameterizedHoge {
    GenericHoge<String, Integer, Double> ph = new GenericHoge<String, Integer, Double>();
    GenericHoge<String, String, String> ph2 = new GenericHoge<Integer, Integer, Integer>(); //型の宣言とnewするパラメータが不一致なのでコンパイルエラー
    GenericHoge<String, String, String> ph3 = new GenericHoge<Object, Object, Object>(); //スーパークラス指定しておけばいけるやろなんて思っていてもダメ、不一致でコンパイルエラー
    GenericHoge<Object, Object, Object> ph4 = new GenericHoge<String, String, String>(); //逆もまた然りでコンパイルエラー
    GenericHoge gh = new GenericHoge(); //型パラメタ無視してもOK(ただし下位互換性を保つためであり、コンパイル時に警告あり)
    GenericHoge<String, Integer, Double> ph5 = new GenericHoge<>(); //OK。型引数は宣言したものと一致している前提(そうでないとコンパイルできない)なのでインスタンス生成時には省略可(宣言部分はもちろん省略不可)
}

Genericクラスの注意点

  • staticな型変数は使用不可:コンパイル時に型が決まっていないので自明
  • 型変数によるインスタンス生成は不可:これもコンパイル時に型が決まっていないから自明
  • 型変数を使った配列の生成は不可:同じく
  • instanceof演算子による動的な型の判定は不可
  • 型変数に対する.classによる参照は不可
class GenericHogehoge<T, R, HOGE> {
    static T st;               //staticなのでコンパイルエラー
    T t;                       //OK
    T[] tarray = new T[5];     //T型の配列は作れないのでコンパイルエラー
    T obj = new T();           //Tを使ってインスタンスは作成不可なのでコンパイルエラー
    Class<?> tclass = T.class; //Tを使ってクラス参照不可なのでコンパイルエラー
    Class<?> objclass = Object.class; //OK

    void doSomething(T t) {
        if(t instanceof String) System.out.println("t is String");  //OK
        if(t instanceof T) System.out.println("t is T");            //NG
        Class c = t.getClass();
        System.out.println("Class name of T: " + c.getName());
    }
    /* RとHOGEは使っていないが特にコンパイルエラーにはならない */
}

要するに、コンパイル時に型が決まっていることを前提とするようなものには型パラメータを使えないということ(staticだったり配列だったりinstanceof演算子だったり)。

Generic Methods

こちらも型パラメータつきのメソッドのこと。仕様書から引用。

A generic method declaration defines a set of methods, one for each possible invocation of the type parameter section by type arguments. Type arguments may not need to be provided explicitly when a generic method is invoked, as they can often be inferred

意訳
総称メソッドの宣言は、引数で指定された型パタメータの適切な呼出しごとに、一つのメソッドセットを定義する。型引数はたいてい推測可能なので、明示的に指定しなくてもよい。

やっぱりわからん。とりあえず、引数や戻り値の型を"Generic"にしておいて、呼出し時に実際の型を指定できるように使うらしい。

とりあえず動く例
class GenericMethod {
    public <T> void doGenericMethod(T t) {
        System.out.println(t.toString());
    }
    <T1, T2> T1 getGenericMethod(T2 t2) {
        T1 t1 = (T1) t2;
        return t1;
    }
}

public class GenericsTest {
    public static void main(String[] args) {
        GenericMethod gm = new GenericMethod();
        gm.<String> doGenericMethod("hoge");        //TypeがStringであることを明示して呼出し
        gm.doGenericMethod("hogehoge");             //メソッドの引数からStringであることは明らかなのでType引数を省略
        gm.doGenericMethod(new Double("11.11111")); //こちらもDoubleが明らかなので省略

        String s = gm.<String, String>getGenericMethod("hogehogehogehoge"); //戻り値の型がGeneric
        System.out.println(s);
    }
}

使いこなして設計するには慣れがいりそうな。。。なお、当然ながら型変数のスコープは当該メソッド内のみ。

型境界(Type Bound)

いままでの型変数では、総称型クラスのインスタンス生成時や総称型メソッドの実行時に渡す型引数について、特段制限もなくなんでもあり(StringでもDoubleでもAでもHogeでも)だったが、これに制約をつけられるというお話。
<型変数 extends 型>で「型」のサブクラスしか型引数として受け付けなくなる。
また、「型」には宣言済みの「型変数」を使用可能。
<HOGE, SUBHOGE extends HOGE>もOK。
なお、型変数が表す実体がインタフェースであったとしても、キーワードはextendsでありimplementsではない。

interface HogeInterface {}
class HogeClass<T extends HogeInterface> {} //OK
class HogeClass<T implements HogeInterface> {} //NG

型引数へのワイルドカード指定

変数宣言時点で型が決まっていない場合にワイルドカードを使うことで、さらに柔軟なGenericsの使い方が可能(今までEclipseのおかげでなんとなく使っていたが)。
ワイルドカードに型境界を設定する場合は、<? extends 型>または<? super 型>を使用可能。なお、型パラメータとして"?"の文字は使用不可。

class OmaenanishitendayoHoge<A?A> { } //"?"を含む文字列は型パラメータとしt使えない

ジェネリック型の継承および実装

総称型を継承または実装する場合、型引数を指定して総称型をパラメータ化する必要がある。

class Hoge<T> {}
class SubHoge1 extends Hoge<T> {}          //Hoge<T>をパラメータ化していないのでコンパイルエラー
class SubHoge2<T> extends Hoge<T> {}       //OK Hoge<T>の"T"にSubHoge2<T>の<T>が渡る()
class SubHoge3 extends Hoge<String> {}     //OK Hoge<String>でパラメータ化されている
class SubHoge4<T> extends Hoge<Double> {}  //OK Hoge<Double>でパラメータ化されている サブクラスで型変数を使ってはいけないとは言っていない

Collection

Collection API(Collection Framework)とは

複数のデータを一つにまとめて扱うためのデータ構造およびユーティリティ、そしてフレームワーク。

java.util.List<E> implements java.util.Collection<E>

インデックスによって順序づけされるデータ構造。主に以下の実装クラスがある。非同期処理は、add()やremove()等が並列に実行された場合に結果を保証しない。外側からなんらかの手段でsyncronizeする必要あり。iteratorのConcurrentModificationExceptionをcatchする手もあるが、ベスト・エフォートなのでデバッグ目的。

ArrayList<E> extends List<E>

  • サイズ変更可能な配列
  • nullを含むすべての要素を許容
  • 非同期
  • size、isEmpty、get、set、iterator、およびlistIteratorの処理は、一定の時間で実行
  • addは内部配列の先頭に近いほど遅い(配列の要素をずらす内部処理が必要)
  • ランダムアクセスは比例的な時間で実行(LinkedListより速い)
List<?> list = Collections.synchronizedList(new ArrayList()); //スレッド・セーフなArrayList

CopyOnWriteArrayList<E> extends List<E>

  • 基になる配列のコピーを新規に作成してadd、setなどが実装されるArrayListのスレッド・セーフ版
  • nullを含むすべての要素を許容

LinkedList<E> extends List<E>

  • 二重リンク・リスト(連結リスト)1の実装
  • nullを含むすべての要素を許容
  • 非同期
  • ランダムアクセスはArrayListより遅い(リンクを辿って目的のオブジェクトまで探索するため)
  • 要素の追加・削除はArrayListより速い(特に先頭付近や末尾付近への追加・削除)
List<?> list = Collections.synchronizedList(new LinkedList()); //スレッド・セーフなLinkedList

Vector<E> extends List<E>

  • オブジェクトの可変長配列
  • 整数インデックスを使ってアクセス
  • サイズはElastic(必要に応じて増減)
  • capacityとcapacityIncrementを維持することによって記憶領域の管理を最適化
  • ArrayListのスレッド・セーフ版(同期をとる必要がないならパフォーマンスの面でArrayListのが有利)

Queue<E> extends Collection<E>

  • FIFO
  • いわゆるQueueの実装
操作 失敗時に例外 失敗時に値を返す
挿入 add(e) offer(e)
取出し remove() poll()
検査 element() peek()

Deque<E> extends Queue<E>

  • 両端からQueueを操作できる
操作 失敗時に例外 失敗時に値を返す
挿入 addFirst(e), addLast(e) offerFirst(e), offerLast(e)
取出し removeFirst(), removeLast() pollFirst(), pollLast()
検査 getFirst(), getLast() peekFirst(), peekLast()

Stack<E> extends Vector<E>

  • 後入れ先出し(LIFO)スタック
  • スレッド・セーフ
  • スタックの先頭に要素を追加:public E push(E item)
  • スタックの先頭から要素を取り出して削除:public E pop()
  • スタックの先頭から要素を取り出してそのまま残す:public E peek()
  • スタックからオブジェクトを検索して何番目にあるかを返す(配列インデックスと違い0からではなく1から数え):public int search(Object obj)

ArrayDeque<E> implements Deque<E>

  • Dequeインタフェースの実装でCollectionフレームワークの一つ
  • サイズ変更可能
  • スレッドセーフではない(Stackはスレッド・セーフ)
  • null要素禁止
  • LIFOとしてはStackより高速で、FIFOとしてはLinkedListよりも高速

public static <T> List<T> asList(T... a) java.util.Arrays.asList()

可変長引数からListオブジェクトを生成するメソッド。ジェネリクスではプリミティブ型(intとか)をそのまま扱えないが、asList()にプリミティブ型を渡すと、返されるList<T>の型Tは自動的に適当なクラスに変換される(Auto-Boxing)。

List<Integer> iList = Arrays.asList(3, 3, 5, 7, 11, 13);
List<Boolean> bList = Arrays.asList(true, true, false, false);

ここで渡されるListは、Arraysクラス内部のprivate static ArrayListクラスインスタンス。つまり、返されたListまたは元の配列の変更は、もう一方のオブジェクトへも伝搬する。ただし、通常のArrayListのようにadd()を使用する操作は不可。

import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;

class ArrayListTest {
    public void doArrayListTest() {
        String[] str = new String[] {"hoge", "hogehoge", "hogehogehoge"};
        List<String> list = Arrays.asList(str);                        //元の配列との関係を維持
        ArrayList<String> aList = new ArrayList(Arrays.asList(str));   //newしているので元の配列とは無関係
        LinkedList<String> lList = new LinkedList(Arrays.asList(str)); //newしているので元の配列とは無関係

        printArrayList(list, str);
        try {
            list.add("hogehogehogehoge");   //listに追加 → UnsupportedOperationException
        } catch(UnsupportedOperationException e) {
            //nothing to do
        }
        list.set(0, "geho");                //listの先頭要素を変更
        printArrayList(list, str);

        printArrayList(aList, str);
        aList.add("hogehogehogehoge");
        aList.set(0, "gehogeho");
        printArrayList(aList, str);

        printArrayList(lList, str);
        lList.add("hogehogehogehoge");
        lList.set(0, "gehogeho");
        printArrayList(lList, str);
    }
    <T> void printArrayList(List<T> list, Object[] array) { //Generic Methodにしてみた
        System.out.println("---list---");
        for(T s : list) {
            System.out.println(s);
        }
        System.out.println("===array===");
        for(int i = 0; i < array.length; i++) {
            System.out.println(array[i].toString());
        }
    }
}

public class CollectionTest {
    public static void main(String[] args)  {
        ArrayListTest alt = new ArrayListTest();
        alt.doArrayListTest();
    }
}
実行結果
---list---
hoge
hogehoge
hogehogehoge
===array===
hoge
hogehoge
hogehogehoge
---list---
geho
hogehoge
hogehogehoge
===array===
geho
hogehoge
hogehogehoge
---list---
hoge
hogehoge
hogehogehoge
===array===
geho
hogehoge
hogehogehoge
---list---
gehogeho
hogehoge
hogehogehoge
hogehogehogehoge
===array===
geho
hogehoge
hogehogehoge
---list---
hoge
hogehoge
hogehogehoge
===array===
geho
hogehoge
hogehogehoge
---list---
gehogeho
hogehoge
hogehogehoge
hogehogehogehoge
===array===
geho
hogehoge
hogehogehoge

java.util.Set<E>

  • 重複する要素を持たないことを保証するCollection。
  • 数学的にな集合を表す
  • 集合であるため、順番に意味はない
  • nullを許容するが重複不可のため一つだけ
  • セット内に可変オブジェクトが格納され、equals()メソッドに影響する(重複チェックに影響する)値が変更された場合の動作は保証されない

HashSet<E>

  • 非スレッド・セーフな実装
  • 順序保証なし
  • null要素を許容
  • ハッシュ関数を使って管理

TreeSet<E>

  • 非スレッド・セーフな実装
  • 順序保証あり
  • 「自然な」順序またはインスタンス生成時に指定したComparatorを使う
  • null要素を許容
  • ハッシュ関数を使って管理
スレッド・セーフにする
Set set = Collections.synchronizedSet(new HashSet());
SortedSet sortedSet = Collections.synchronizedSortedSet(new TreeSet());

主要メソッド

method 内容
boolean add(E e) 指定した"e"がセット内にない場合は追加してtrue、既存の場合false
void clear() すべての要素をセットから削除
boolean contains(Object o) 指定した要素がセットに含まれている場合true
boolean remove(Object o) 指定した要素がセット内に含まれている場合削除して成功したらtrue
int size() セットの要素数(カーディナリティ:数学用語で集合の濃度)を返す
void [add/remove]All(Collection<? extends E> c) 複数要素の追加/削除

java.util.Map<K, V>

一意なキー(K)とそれに紐づく値(V)のペアを要素として扱うデータ構造。

主要メソッド

method 内容
void clear() すべての要素をMapから削除
boolean containsKey(Object key) 指定した要素がマップのキーに含まれている場合true
boolean containsValue(Object key) 指定した要素がマップの値に含まれている場合true
Set<Map.Entry<K,V>> entrySet() マップのSetビューへの参照を返すが、add()は不可
Set<K> keySet() マップに含まれるキーのSetビューを返す
<V> remove(Object key) 指定したキー要素がマップ内に含まれている場合削除して成功したらVを返す
default boolean remove(Object key, Object value) 指定したキー要素と値要素のマッピングがマップ内に含まれている場合削除
int size() マップのキー数を返す
void putAll(Map<? extends K, extends V> m) 複数マップ要素を追加
Collection<V> values() 値のCollectionビューを返すが、add()は不可

HashTable<K, V>

  • nullオブジェクト以外であれば、どのオブジェクトでもキーや値に使用可能
  • キーとするオブジェクトはhashCode()およびequals()メソッド実装要
  • 多くのエントリを入れる場合は、十分に大きな容量で作成する方が、必要に応じてハッシュを自動的にやり直して表を大きくするよりも、エントリを効率的に挿入可能
  • スレッド・セーフ
  • 順序は非保証

HashMap<K, V>

  • null値およびnullキーを使用可能
  • 非スレッド・セーフ
  • 順序は非保証

TreeMap<K, V>

  • 順序を保証
  • 非スレッド・セーフ
スレッド・セーフにする
Map m = Collections.synchronizedMap(new HashMap());
SortedMap m = Collections.synchronizedSortedMap(new TreeMap());

Comparable<T>の実装

ObjectクラスはComparableインタフェースを実装していないため、これを継承したクラスもデフォルトで「比較不可(! Comparable<T>)」。
比較するためには、Comparableを実装(implements)し、int compare()を実装(@Override)する。

class ComparableHoge implements Comparable<ComparableHoge> {
    private int hoge;
    public int compareTo(ComparableHoge o) {
        if(this.hoge == o.getHoge()) return 0;      //等しいと見なされる場合は0を返す
        else if(this.hoge > o.getHoge()) return 1;  //自分のほうが大きいと見なされる場合は正の値を返す
        else return -1;                             //自分のほうが小さいと見なされる場合は負の値を返す
    }
    public int getHoge() { return hoge; }
    public ComparableHoge(int hoge) { this.hoge = hoge; }
}

public class ComparatorTest {
    public static void main(String[] args) {
        ComparableHoge hoge1 = new ComparableHoge(1);
        ComparableHoge hoge2 = new ComparableHoge(2);
        System.out.println(hoge1.compareTo(hoge2));
    }
}
実行結果
-1

Comparator<T>の実装

オブジェクトを比較するための実装クラスを作りましょうというお話。int compare(T o1, T o2)を実装(@Override)する。

import java.util.Comparator;

class ComparableHoge implements Comparable<ComparableHoge> {
    private int hoge;
    public int compareTo(ComparableHoge o) {
        if(this.hoge == o.getHoge()) return 0;      //等しいと見なされる場合は0を返す
        else if(this.hoge > o.getHoge()) return 1;  //自分のほうが大きいと見なされる場合は正の値を返す
        else return -1;                             //自分のほうが小さいと見なされる場合は負の値を返す
        /* return this.hoge - o.getHoge(); */       //1行でこう書ける
    }
    public int getHoge() { return hoge; }
    public ComparableHoge(int hoge) { this.hoge = hoge; }
}
class HogeComparator implements Comparator<ComparableHoge> {
    @Override
    public int compare(ComparableHoge o1, ComparableHoge o2) {
        return o1.compareTo(o2);
    }
}
public class ComparatorTest {
    public static void main(String[] args) {
        ComparableHoge hoge1 = new ComparableHoge(1);
        ComparableHoge hoge2 = new ComparableHoge(2);
        System.out.println(hoge1.compareTo(hoge2));
        System.out.println(new HogeComparator().compare(hoge1, hoge2));
    }
}
実行結果
-1
-1
もう少し意味のあるやつ
import java.util.Comparator;
import java.util.Set;
import java.util.TreeSet;

class StrLenComparator implements Comparator<String> {
    @Override
    public int compare(String s1, String s2) {
            return s1.length() - s2.length(); //文字列が短い順に並ぶようにする
    }
}
public class StrLenComparatorTest {
    public static void main(String[] args) {
        Set<String> strSet = new TreeSet<>(new StrLenComparator());
        strSet.add("ho");
        strSet.add("hog");
        strSet.add("h");
        strSet.add("hoge");
        System.out.println(strSet);
    }
}
実行結果
[h, ho, hog, hoge]
匿名内部クラスを使って実装
public class StrLenComparatorTest {
    public static void main(String[] args) {
        Set<String> strSet = new TreeSet<>(new Comparator<String>() {
                @Override
                public int compare(String s1, String s2) {
                    return s1.length() - s2.length();
                }
        });
        strSet.add("ho");
        strSet.add("hog");
        strSet.add("h");
        strSet.add("hoge");
        System.out.println(strSet);
    }
}

今回はここまで。。。LinkedHashMapとか出てこなかったので気力が回復したら追記するかもしれません。

仕様書の参照先

  1. Generic Classes and Type Parameters
  2. Generic Methods
  3. Generic Constructors
  4. Generic Interfaces and Type Parameters
  5. Nested Generic Classes
  6. Generic Functional Interfaces
  7. Generic Function Types
2
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
3