Ch5. Stream API

Streamオブジェクトの初期化と終端操作

まずは、Streamオブジェクトの生成方法と終端操作を見ていく。

import java.util.stream.Stream;

public class StreamTest {
    public static void main(String[] args){
        String[] stringArray = {"Hello", "World", "from", "Stream"};
        Stream<String> stringStream = Arrays.stream(stringArray);
        System.out.println(stringStream.reduce("", (x,y) -> x + ' ' + y));//' Hello World from Stream'

        IntStream intStream = Arrays.stream(new int[]{1,2,3,4,5});
        intStream.forEach(x -> System.out.print(x));//'12345'

        Stream<String> stringArray2 = Stream.of("This", "is", "Stream");
        System.out.println(stringArray2.toArray());//'[Ljava.lang.Object;@5315b42e'

        IntStream intStreamRange = IntStream.range(100,200);
        System.out.println(intStreamRange.count());//'100'

        IntStream intStreamRangeClose = IntStream.rangeClosed(100,200);
        System.out.println(intStreamRangeClose.min());//'OptionalInt[100]'

        IntStream intStreamLength0    = IntStream.range(0,0);
        System.out.println(intStreamLength0.min());//'OptionalInt.empty'

        //System.out.println(intStreamRangeClose.max());//'stream has already been operated upon or closed' NOTE:終端操作は1回のみ
    }
}

Optional

StreamAPIにはいくつか、Optionalというオブジェクトを返却する終端操作が存在する。 これはnullになっている可能性のある変数をラップするオブジェクトで、値の中身がnullであった場合に他の値を返却するよう指定したり、別の操作を実行するように出来る。

import java.util.Optional;

public class OptionalTest {
    public static void main(String[] args){
        Optional<String> obj = Optional.of("Hello World");
        Optional<String> obj2 = Optional.empty();

        obj.ifPresent(System.out::println);//'Hello World'
        System.out.println(obj2.orElse("This is Empty String"));//'This is Empty String

        String testString = null;
        //Optional<String> obj3 = Optional.of(testString);//'Exception in thread "main" java.lang.NullPointerException'
        Optional<String> obj3 = Optional.ofNullable(testString);
        obj3.get();//'Exception in thread "main" java.util.NoSuchElementException'
    }
}

Optional.ofでOptionalなオブジェクトを作成できるが、ofの時点でラップする対象のオブジェクトがnullだった場合、nullPointerExceptionが発生してしまう。 この問題は(なぜか教科書では触れられていないが)ofNullableを利用してOptionalオブジェクトのラップ対象がnullでない場合も例外を発生しないように出来る。

Optionalを返却するStreamAPIの終端操作の中には先ほどみていたmaxminの他にもfindAnyという変わったメソッドもある。

Stream (Java SE 10 & JDK 10 )

またOptionalにはorElseの他にも、Nullが返却された場合の動作についていろいろ指定できる。

        System.out.println(obj3.orElseGet(() -> "This is Empty String"));//'This is Empty String'
        System.out.println(obj3.orElseThrow(NoSuchMethodError::new));//'Exception in thread "main" java.lang.NoSuchMethodError'

中間操作

中間操作の代表的なものを見ていく。

        Stream<String> obj1 = Stream.of("Fuijmura", "Ureshino", "Oizumi", "Suzui");
        //obj1.map((x) -> x + " san");//'.IllegalStateException: stream has already been operated upon or closed'
        obj1.map(x -> x + " san ").skip(1).filter(x -> x.contains("o")).forEach(System.out::println);//'Ureshino san '

        Stream<String> obj2 = Stream.of("Kagawa", " Tokushima", " Kochi", " Ehime", " Tokushima");
        obj2.distinct().map(x -> x + "-ken").forEach(System.out::print);//'Kagawa-ken Tokushima-ken Kochi-ken Ehime-ken'

中間操作で不思議な挙動をするのはflatmapで、配列などで入れ子になっているStreamの要素を子要素ごとに分解する。

        List<Integer> first  = Arrays.asList(100,200);
        List<Integer> second = Arrays.asList(200,300,400);
        List<Integer> third  = Arrays.asList(500,600,700,800);
        List<List<Integer>> nested = Arrays.asList(first, second, third);

        nested.stream()
                .peek(System.out::println)//'[100, 200]'
                .map(data -> data.stream())
                .peek(System.out::println)//'java.util.stream.ReferencePipeline$Head@73f792cf'
                .forEach((l -> {l.forEach(System.out::println);}));//'100''200'

        nested.stream()
                .peek(System.out::println)//'[100, 200]'
                .flatMap(data -> data.stream())
                .peek(System.out::println)//'100'
                .forEach(System.out::println);//'100'

Streamオブジェクトのキャスト

Stream<Integer>からStream<String>のような通常のStreamオブジェクト同士の変換は以下のように行う。

import java.util.Arrays;
import java.util.List;
import java.util.stream.Stream;

public class StreamCast {
    public static void main(String[] args){
        List<Integer> numbers = Arrays.asList(1,2,3,4,5);
        Stream<String> strings = numbers.stream().map(x->"'" + x.toString() + "'");
        strings.forEach(System.out::print);//'1''2''3''4''5'

        System.out.println();

        List<String> strings2 = Arrays.asList("Shinagawa", "Kamata", "Kawasaki", "Yokohama");
        Stream<Integer> numbers2 = strings2.stream().map(x->x.length());
        numbers2.forEach(System.out::print);//'9688'
    }
}

一方、プリミティブ型のStreamであIntStreamDoubleStreamLongStreamを利用する場合はそれぞれMapToIntMapToDoubleMapToStreamを利用する。

        IntStream streamPrimitiveInt = Arrays.asList(Integer.valueOf(10), Integer.valueOf(20), Integer.valueOf(30)).stream().mapToInt(x -> x);
        streamPrimitiveInt.forEach(System.out::print);//'102030'

        System.out.println();

        IntStream streamPrimitiveInt2 = Arrays.asList("Shinjyuku", "Yoyogi-Uehara", "Sagami-Ono").stream().mapToInt(x -> x.length());
        streamPrimitiveInt2.forEach(System.out::print);//'91310'

プリミティブ型のStreamから通常の型パラメータを指定したStreamに変換するにはmapToObjあるいはBoxedを利用する。

        IntStream streamPrimitiveInt3 = Arrays.asList(1,2,3,4,5).stream().mapToInt(x ->x);
        streamPrimitiveInt3.mapToObj(x -> "'" + x + "'").forEach(System.out::print);//'1''2''3''4''5'

        System.out.println();

        IntStream streamPrimitiveInt4 = Arrays.asList(1,2,3,4,5).stream().mapToInt(x ->x);
        streamPrimitiveInt4.boxed().forEach(System.out::print);//'12345'

また、IntStreamとLongStreamには暗黙の型変換を行ってくれるメソッドも存在する。

        LongStream streamPrimitiveInt5 = Arrays.asList(1,2,3,4,5).stream().mapToInt(x -> x).asLongStream();
        DoubleStream streamPrimitiveInt6 = Arrays.asList(1,2,3,4,5,6).stream().mapToInt(x ->x).asDoubleStream();

更に終端操作であaverageを利用するとOptionalDoubleというプリミティブ型のOptionalラッパークラスが得られる。 これに対してgetAsDoubleすることでプリミティブ型として利用できる。

        LongStream streamPrimitiveInt5 = Arrays.asList(1,2,3,4,5).stream().mapToInt(x -> x).asLongStream();
        OptionalDouble result = streamPrimitiveInt5.average();
        System.out.println(result);//'OptionalDouble[3.0]'

        DoubleStream streamPrimitiveInt6 = Arrays.asList(1,2,3,4,5,6).stream().mapToInt(x ->x).asDoubleStream();
        double result2 =  streamPrimitiveInt6.average().getAsDouble();
        System.out.println(result2);//'3.5'

collectによる終端操作

collectによる終端操作は、引数にCollecterクラスのクラスメソッドの引数を取る。 CollecterクラスにはStreamの各要素を集計する様々なクラスメソッドが用意されている。

public class CollectSample {
    public static void main(String[] args){
        List<String> keikyuString = Arrays.asList("Shinagawa", "Kamata", "Kawasaki", "Yokohama");

        List<String> keikyuStringResult = keikyuString.stream().collect(Collectors.toList());
        String keikyuStringResult2      = keikyuString.stream().collect(Collectors.joining("-Station "));
        int keikyuStringResult3         = keikyuString.stream().collect(Collectors.summingInt(t -> t.length()));
        double keikyuStringResult4      = keikyuString.stream().collect(Collectors.averagingInt(t -> t.length()));

        System.out.println("Collectors.toList() :" + keikyuStringResult);//'Collectors.toList() :[Shinagawa, Kamata, Kawasaki, Yokohama]'
        System.out.println("Collectors.joining() :" + keikyuStringResult2);//'Collectors.joining() :Shinagawa-Station Kamata-Station Kawasaki-Station Yokohama'
        System.out.println("Collectors.summingInt() :" + keikyuStringResult3);//'Collectors.summingInt() :31'
        System.out.println("Collectors.averagingInt() :" + keikyuStringResult4);//'Collectors.averagingInt() :7.75'

        List<String> odakyuString = Arrays.asList("Shinjyuku", "Yoyogi-Uehara", "Sagami-Ono", "Shinjyuku", "Atsugi", "Sagami-Ono");
        Set<String>    odkyuStringResult = odakyuString.stream().collect(Collectors.toSet());
        //Map<String, String> odakyuStringResult2 = odakyuString.stream().collect(Collectors.toMap(s -> s.toLowerCase(), n -> n.toUpperCase()));//'Duplicate key shinjyuku (attempted merging values SHINJYUKU and SHINJYUKU)'
        Map<String, String> odakyuStringResult2 = odakyuString.stream().collect(Collectors.toMap(s -> s.toLowerCase(), n -> n.toUpperCase(), (dup1,dup2) -> dup1 + " & " + dup2));

        System.out.println("Collectors.toSet() :" + odkyuStringResult);//'Collectors.toSet() :[Atsugi, Sagami-Ono, Shinjyuku, Yoyogi-Uehara]'
        System.out.println("Collectors.toMap() :" + odakyuStringResult2);//'Collectors.toMap() :{sagami-ono=SAGAMI-ONO & SAGAMI-ONO, atsugi=ATSUGI, yoyogi-uehara=YOYOGI-UEHARA, shinjyuku=SHINJYUKU & SHINJYUKU}'
    }
}

toMapメソッドでは、Streamの終端操作の結果同じキーをもつペアが発生した場合に例外が発生する仕様となっている。 そこで重複するキーを持つValue同士をマージするBinaryOperatorを第三引数に指定することで、例外を回避ことが出来る。

また、Mapインターフェースを実装しているクラスであればHashMap以外のMapクラスにキャストすることも出来る。その場合一番最後の引数でその旨を指定する。

       NavigableMap<String, String> odakyuStringResult3 = odakyuString.stream().collect(Collectors.toMap(s -> s.toLowerCase(), n -> n.toUpperCase(), (dup1,dup2) -> dup1 + " & " + dup2, TreeMap::new));
       System.out.println("Collectors.toMap() (Casted to TreeMap) :" + odakyuStringResult3.getClass());//'class java.util.TreeMap'

groupingByは引数にFunction型をとる。この関数から同じ値が返却された場合、同じグループに所属するValueと認識される。 デフォルトでは同じグループに所属するValueがSetとして返却されるが、Collectors.joining()を利用して文字列に返却出来る。

        Map<Boolean, String> group3 = tokyuString.stream().collect(Collectors.groupingBy(s -> s.contains("i"), LinkedHashMap::new ,Collectors.joining(" & ")));
        System.out.println(group3.getClass());//'class java.util.LinkedHashMap'

groupingByではtoMapと同じくMapインターフェースの実装クラスのうち、どのクラスにキャストするかを選択できる。 なお、partioningByメソッドもgroupingByと似たような働きをする。

ただし、引数にPredicate型しか取れないためMapのキーがbooleanに限定される(上記の例ではgroupingByのメソッドの引数にFunction<String, Boolean>を利用してしまっているため、実質的にgroupingByと同じになっている)。

        String group4 = tokyuString.stream().collect(Collectors.mapping(s -> s.toLowerCase(), Collectors.joining(" & ")));
        System.out.println(group4);//'shibuya & hiyoshi & kikuna & yokohama'

今までmapで各要素に対する操作を実施しcollect(Collectors.method())で各要素に対する集計処理をしていた。 mappingメソッドは第一引数に各要素に対する操作を、第二引数で集計処理を指定する。

        String group4 = tokyuString.stream().collect(Collectors.mapping(s -> s.toLowerCase(), Collectors.joining(" & ")));
        System.out.println(group4);//'shibuya & hiyoshi & kikuna & yokohama'

最後にStreamの各要素の最大・最小を取得するメソッドを確認する。 最大・最小を確認するにはメソッドの引数としてComparatorクラスのオブジェクトを渡すことになっている。

        List<Airplane> airplaneList = Arrays.asList(a380, b747, dhc8, superJet);
        Optional<Airplane> result = airplaneList.stream().collect(Collectors.maxBy(new HowToCompare()));
        System.out.println(result);//'Optional[500]'

        List<Airplane> airplaneList2 = Arrays.asList(a380, b747, dhc8, superJet, f15, mig29, mirage2000);
        Map<Boolean, Optional<Airplane>> result3 = airplaneList2.stream().collect(Collectors.groupingBy(x -> x.getSpeed() > 400), Collectors.maxBy(new HowToCompare()));