Ch3. コレクションとジェネリクス

コレクションAPIの概要

コレクションAPIとして提供されているList・Set・Queueインターフェースを実装したクラスは全てCollectionインターフェースを実装している。 主なメソッドは次の通り、

jdk/Collection.java at master · openjdk/jdk · GitHub

java.util.Collenction
clear()
contains(Object obj)
iterator()
toArray()
size()

一方Collectionインターフェースを実装しないコレクションAPIも存在する。 こちらはStortedMapインターフェースを実装したクラスで、このクラスはMapインターフェースを実装している。

jdk/Map.java at master · openjdk/jdk · GitHub

Collectionインターフェースと似たような抽象メソッドが宣言されているが、Mapとして利用するためのメソッドも併せて宣言されている。

java.util.Map
containsKey(Object obj)
containsValue(Object obj)
get(Object key)
put(K Key, V value)

Collection > List 実装クラス

要素の重複が許可され、更に順序づけが行われる。

ArrayList・LinkedList・Vectorが存在する。 Vector以外マルチスレッドプログラミングに対応していない。

名前から想定されるようにArrayListよりもLinkedListの方が(次の要素に対するポインタを持つため、)削除と検索が早い。 一方ランダムアクセス(検索)はArrayListの方が早い。

Collection > Set 実装クラス

重複が許可されない。また(基本的に)順不同での管理がされる。 等価の要素を2回追加した場合は、2回目以降の追加が無視される。

HashSet・TreeSet・LinkedHashSetの3クラスが存在する。

等価の判定はequalsメソッドによって行われる。 等価判定を適切に行うSetインターフェース実装クラスを作るためにはequalsメソッドがhashCodeメソッドを利用して実装されているため、hashCode()のメソッドもオーバーライドする必要がある。

TreeSetのソートの仕組みに関してはこの後後述する。

Collection > Queue/Deque 実装クラス

先入れ先出し、および後入れ先出しをサポートするコレクション。

Queueインターフェースでは「先入れ先出し」のみサポートする抽象メソッドが宣言されている。

package Chapter3;

import java.util.ArrayDeque;
import java.util.Queue;

public class QueueSample {

    public static void main(String[] args) {
        Queue<String> queue = new ArrayDeque();
        queue.add("Java");
        queue.add("Ruby");
        queue.add("Kotlin");
        System.out.println(queue.offer("Go")); //'true'

        System.out.println(queue.poll());      //'Java'
        System.out.println(queue.remove());    //'Ruby'
        System.out.println(queue.peek());      //'Kotlin'
        System.out.println(queue.poll());      //'Kotlin'
        System.out.println(queue.remove());    //'Go'

        System.out.println(queue.peek());      //'null'
        System.out.println(queue.element());   //'Exception in thread "main" java.util.NoSuchElementException'
    }

}

キューなので、中身が空っぽだったり・キューの大きさが制限されていたりする場合操作が行えない場合がある。 その際に特殊な値(nullとかfalse)を返す場合・例外を発生させる場合で異なるメソッドが提供されている。

操作 例外発生 null・false返却
挿入 add(Type element) offer(Type element)
削除 remove() poll()
検査 element() peek()
package Chapter3;

import java.util.ArrayDeque;
import java.util.Deque;

public class QueueSample3 {
    public static void main(String[] args) {
        Deque<String> queue = new ArrayDeque();
        queue.addLast("Java");
        queue.addLast("Ruby");
        queue.addLast("Kotlin");
        System.out.println(queue.offerLast("Go")); //'true'

        System.out.println(queue);//'[Java, Ruby, Kotlin, Go]'

        System.out.println(queue.pollLast());      //'Go'
        System.out.println(queue.removeLast());    //'Kotlin'
        System.out.println(queue.peek());          //'Java'
        System.out.println(queue.pollLast());      //'Ruby'
        System.out.println(queue.removeLast());    //'Java'

        System.out.println(queue.peekLast());      //'null'
        System.out.println(queue.getLast());       //'Exception in thread "main" java.util.NoSuchElementException'
    }

}

Deque(Double ended queueの略)インターフェースでは「先入れ先出し」も「後入れ先出し」もサポートできるよう、キューの両端に対する操作が出来るメソッドが用意されている。

f:id:Pyons:20210228211508j:plain
DequeインターフェースのFirst-in-Last-Out

操作 例外発生 null・false返却
挿入 addLast(Type element) offerLast(Type element)
削除 removeLast() pollLast()
検査 getLast() peekLast()

因みに英語でpeekとは『チラッと見る』の意味だそう。

Dequeで「先入れ後出し」を利用することが出来るが、これは本来Stackというクラスが担う役割だった。 Stackクラスはパフォーマンス上の問題からJava SE6よりDequeにとって代わられることになったようだ。

Deque (Java Platform SE 8)

DequeとStackと - CLOVER🍀

その代わり、DequeではStackクラスから引き継いだメソッドがエイリアスとして利用可能になっている。 内部実装はDequeクラスのメソッドをそのまま読んでいるだけ。

jdk/ArrayDeque.java at master · openjdk/jdk · GitHub

package Chapter3;

import java.util.ArrayDeque;
import java.util.Deque;

public class QueueSqmple2 {
    public static void main(String[] args) {
        Deque<String> queue = new ArrayDeque();
        queue.push("Java");//'alias of addFirst()'
        queue.push("Ruby");//'alias of addFirst()'
        queue.push("Kotlin");//'alias of addFirst()'
        System.out.println(queue.offerLast("Go")); //'true'

        System.out.println(queue); //'[Kotlin, Ruby, Java, Go]'

        System.out.println(queue.pollLast()); //'Go'
        System.out.println(queue.pop());      //'Kotlin' alias of removeFirst();
        System.out.println(queue.peekLast()); //'Java'
        System.out.println(queue.pollLast()); //'Java'   alias of removeFirst();
        System.out.println(queue.pop());      //'Ruby'

        System.out.println(queue.peekLast());    //'null'
        System.out.println(queue.getLast());     //'Exception in thread "main" java.util.NoSuchElementException'
    }
}

f:id:Pyons:20210228210326j:plain
Stackクラスを代替するDequeクラス

Map > SortedMap 実装クラス

こちらのクラスは他のコレクションAPIと異なりCollectionインターフェースを実装していない独立したコレクションAPIである。

Collectionインターフェースの実装クラスと同じく、イテレータ―も利用することが出来る。ジェネリクスEntryインターフェースになる。

Map.Entry (Java Platform SE 8)

package Chapter3;

import java.util.HashMap;
import java.util.Iterator;
import java.util.Map.Entry;

public class MapSample {

    public static void main(String[] args) {
        HashMap<String, String> map = new HashMap<String, String>();
        map.put("Destination", "Sydney");
        map.put("Aircraft Type", "A340");
        map.put("Flight Hour", "10.0 h");
        map.put("Seat", "A30");

        System.out.println(map.containsKey("Destnation")); //'false'
        System.out.println(map.containsValue("A340"));     //'true'
        System.out.println(map.get("Seat"));               //'A30'

        Iterator<Entry<String, String>> iter = map.entrySet().iterator();
        while(iter.hasNext()) {
            System.out.println(iter.next());
            //'Destination=Sydney'
            //'Seat=A30'
            //'Aircraft Type=A340'
            //'Flight Hour=10.0 h'
        }
    }
}

Map > StoredMap実装クラスにはNavigableMapというインターフェースが用意されている。普通のクラスにこのインターフェースを利用することで「指定されたキーに対して最も近い要素を返す」という利用が出来る。完全一致するキーを指定しなくてもよくなるが、テストがつらそうなので使い道はあまりない気がする。

package Chapter3;

import java.util.NavigableMap;
import java.util.TreeMap;

public class Navigable {
    public static void main(String[] args) {
        NavigableMap<Integer, String> map2 = new TreeMap<Integer, String>();
        map2.put(140, "Ota");
        map2.put(150, "Setagaya");
        map2.put(160, "Minato");
        map2.put(170, "Shinagawa");

        System.out.println(map2.higherKey(155));//'160'
        System.out.println(map2.lowerKey(161));//'160'
        System.out.println(map2.higherEntry(156));//'160=Minato'
        System.out.println(map2.subMap(140, true, 160,false));//'{140=Ota, 150=Setagaya}'

    }
}

ジェネリック

今までジェネリックスと呼んでい<T>による型指定の書き方は【型パラメータリスト】という名前がついている。 この【型パラメータリスト】には【ダイヤモンド演算子】が利用できる。ダイヤモンド演算子による型推論はメソッドの仮引数や、戻り値などにも活用できる。

package Chapter3;

import java.util.ArrayList;

public class Diamond {

    public static void staticMethod(ArrayList<String> list) {
        System.out.println(list);
    }
    
    public static ArrayList<Integer> staticMethod2() {
        //メソッド戻り値での使用例
        return new ArrayList<>();
    }
    
    //public static ArrayList<> staticMethodError(){
        //'型 ArrayList<E> の引数の数が誤っています。引数 <> でパラメーター化できません'
    // return new ArrayList<Integer>();
    //}

    public static void main(String[] args) {
        staticMethod(new ArrayList<>());//実引数での使用例
        // ArrayList<> variable = staticMethod2();//'型 ArrayList<E> の引数の数が誤っています。引数 <> でパラメーター化できません'
        ArrayList<Integer> variable = staticMethod2();
    }

}

普通の(ユーザー任意の)クラス定義でもジェネリクスを使うことが出来る。この際型を指定するのに使う変数を【型パラメータ】と呼ぶ。 実際に使い方を見てみる。

package Chapter3;

class GenericsUserClass<T>{
    
    //'非 static 型 T を static 参照できません'
    //static T staticMethod() {
    // return "Hello";
    //}
    private T variable;

    public T getVariable() {
        return variable;
    }

    public void setVariable(T param) {
        this.variable = param;
    }
}

public class Generics {
    public static void main(String[] args) {
        GenericsUserClass object = new GenericsUserClass();
        object.setVariable("Hello");
        object.setVariable(99999);
        System.out.println(object.getVariable());//'99999'

        GenericsUserClass<Integer> object2 = new GenericsUserClass();
        object2.setVariable(1000);
        //object2.setVariable("HelloWorld");//'型 GenericsUserClass<Integer> のメソッド setVariable(Integer) は引数 (String) に適用できません'
        System.out.println(object2.getVariable());//'1000'
    }
}

型パラメータによる型指定をしなくても、オブジェクトの利用は可能である。 その場合、ArrayListで型を指定しない場合と同じようにあらゆる型が変数に代入できるようになる。

package Chapter3;

class GenericMethodSampleClass{
    public <T> T genericsMethod(T param) {
        return param;
    }
}

public class GenericsMethod {
    public static void main() {
        GenericMethodSampleClass object = new GenericMethodSampleClass();
        Object variable  = object.genericsMethod("String");
        String variable2 = object.genericsMethod("String");
        //Integer variable3 = object.genericsMethod("String");//'型の不一致: String から Integer には変換できません'
        
        Integer variable3 = object.<Integer>genericsMethod(100);
    }
}

ジェネリクスはインターフェースにも利用できる。 ただし、継承先クラスのクラス宣言時に型を指定することは必須ではない。

interface GenericsInterface<T>{
    public T method();
}

class GenericsChildClass implements GenericsInterface{
    //'GenericsInterface は raw 型です。総称型 GenericsInterface<T> への参照は、パラメーター化する必要があります'
    public String method(){return "";};
}

class GenericsChildClass2 implements GenericsInterface<Integer>{
    //public String method(){return "";};//'戻りの型は GenericsInterface<Integer>.method() と互換性がありません'
    public Integer method() {return 100;};
}

型パラメータに指定するするクラスに宣言を持たせることもできる。 右辺で指定したクラスのサブクラスのみを型パラメータに指定させることが出来る。

package Chapter3;

class ChildClass<T extends Number> {

    void method1(T param) {
        System.out.println(param);
    }

    void method2(T param) {
        System.out.println(param);
    }

}

public class GenericsSubclass {
    public static void main(String[] args) {
        ChildClass<Integer> object  = new ChildClass();
        ChildClass<Short>   object2 = new ChildClass();
        //ChildClass<String>  object2 = new ChildClass();//'制約の不一致: 型 String は、型 ChildClass<T> の制約付きパラメーター <T extends Number> の代替として有効ではありません'
    }
}

ワイルドカードを利用したジェネリクス

ここがいまいち理解できていない。

Javaの道:ジェネリクス(3.ワイルドカード)

【Java・ジェネリックス】ワイルドカード型とは何か【前半】 - The King's Museum
【Java・ジェネリックス】ワイルドカード型とは何か【後半】 - The King's Museum

Comparableインターフェース

TreeSetクラス(一意なオブジェクトを格納するとともにオブジェクトの順序付けを行う)では、オブジェクトの順序付けにComparableインターフェースで宣言されているcomparaTo()メソッドを利用している。Comparableインターフェースを実装していないクラスはTreeSetで扱うことが出来ない。

注意すべきは、TreeSetにComparableインターフェースが実装されていないオブジェクトが渡されていないとき、コンパイルエラーではなく【実行時エラー(ClassCastException)】になる点

package Chapter3;

import java.util.TreeSet;

class Aircraft implements Comparable<Aircraft>{
    private Integer speed;
    public int compareTo(Aircraft aircraft) {
        System.out.println(aircraft);
        return (this.getSpeed() > aircraft.getSpeed() ? 1: (this.speed == aircraft.getSpeed()) ? 0 : -1);
    }

    public int getSpeed() {
        return this.speed;
    }

    public void setSpeed(int param) {
        this.speed = param;
    }

}

class Train{
}

public class WildCard {

    public static void main(String[] args) {
        //TreeSet<Train> trainTreeSet = new TreeSet<>();
        //trainTreeSet.add(new Train()); //'Exception in thread "main" java.lang.ClassCastException: Chapter3.Train cannot be cast to java.lang.Comparable'


        TreeSet<Aircraft> aircraftTreeSet = new TreeSet<>();

        Aircraft object1 = new Aircraft();
        object1.setSpeed(100);
        Aircraft object2 = new Aircraft();
        object2.setSpeed(200);

        aircraftTreeSet.add(object1);
        aircraftTreeSet.add(object2);

        System.out.println(aircraftTreeSet);//'[Chapter3.Aircraft@15db9742, Chapter3.Aircraft@6d06d69c]'
    }
}

今回は比較するオブジェクト自体に比較方法に関する実装を行った。 これを独立して別クラスとして実装することもできる。

比較ルールを記述したクラスにCorporatorインターフェースを実装するという方法である。このインターフェースは【型パラメータ】を要求するので、ここに比較対象のオブジェクトのクラスを設定する。

package Chapter3;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;

class Airplane{
    private Integer speed;

    public String toString() {
        return this.speed.toString();
    }

    public int getSpeed() {
        return this.speed;
    }

    public void setSpeed(int param) {
        this.speed = param;
    }
}

class HowToCompare implements Comparator<Airplane>{
    public int compare(Airplane obj1, Airplane obj2) {
        return (obj1.getSpeed() > obj2.getSpeed() ? 1: (obj1.getSpeed() == obj2.getSpeed()) ? 0 : -1);
    }
}


public class ComparatorSample {
    public static void main(String[] ags) {

        Airplane b747 = new Airplane();
        b747.setSpeed(100);
        Airplane dhc8 = new Airplane();
        dhc8.setSpeed(65);
        Airplane superJet = new Airplane();
        superJet.setSpeed(85);
        Airplane a320 = new Airplane();
        a320.setSpeed(95);

        ArrayList<Airplane> list = new ArrayList<Airplane>();
        list.add(dhc8);
        list.add(b747);
        list.add(superJet);
        list.add(a320);

        System.out.println(list); //'[65, 100, 85, 95]'
        Collections.sort(list, new HowToCompare());
        System.out.println(list); //'[65, 85, 95, 100]'
    }
}

配列のソートと検索

上記の例ではCollection.sortメソッドを用いてCorporatorインターフェースの実装クラス(≒ソートのルール)を引数として渡して実施している。 この他にも、Collection.sortには【自然順序付け】(≒Comparableインターフェースの実装クラスである要素に対してcompareToメソッドを使って)ソートする利用法もある。

CollectionsクラスはコレクションAPIに対するソート処理用のユーティリティクラスであり、実装されているメソッドは全てクラスメソッドになっている。

Collections (Java Platform SE 8)

一方、通常の配列のソート用ユーティリティクラスはArraysクラスが存在する。 ArraysクラスもCollectionsクラスと同様にクラスメソッドのみが実装されている。

Arrays (Java Platform SE 8) 配列のソートでもCorporatorインターフェースの実装クラス(≒ソートのルール)を引数として渡してソートさせるメソッドがオーバーライドされている。 一方通常の【自然順序付け】ソートの場合、要素間のソートが出来ないとClassCastExceptionとなる。

package Chapter3;

import java.util.Arrays;

public class SortSample {
    public static void main(String[] args) {
        Object[] array = {"Hello", 12, 'a', 0x14};
        Arrays.sort(array);
        //'Exception in thread "main" java.lang.ClassCastException: java.lang.String cannot be cast to java.lang.Integer'
    }
}

ArraysユーティリティはasListというメソッドを備えているため、ArrayListのオブジェクトに変換可能。 ただし、配列の中身をArrayListのコンストラクタに渡しているだけなので生成されたArrayListオブジェクトの要素は変更不可となる。