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の終端操作の中には先ほどみていたmax
やmin
の他にもfindAny
という変わったメソッドもある。
また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であIntStream
・DoubleStream
・LongStream
を利用する場合はそれぞれMapToInt
・MapToDouble
・MapToStream
を利用する。
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()));