記事の概要
Stream APIの使い方をメインとした記事です。
初めてStream APIに触れる方でも理解できるようにソースコードの例や解説を多く設けています。
また、Stream APIを使ったStrategyパターンなど応用した使い方も紹介しています。
Stream APIを覚えることのメリット
- 処理を言葉のようにかけるため、可読性が上がる。
- inputとoutputが明確なためテストコードが書きやすくなる
- 状態変化を引き起こさないためバグが潜みにくい
- 関数同士を組み合わせても相互に影響しない
- 並列処理にも対応
Stream APIはいつ使えるのか?
一番多い使い方がfor文などのループ処理を行っている箇所に使用できます。
Stream APIの使い方
基本的な流れはStreamインスタンスを生成し、行いたい処理(中間操作)を好きなだけ実施し、最後に欲しい状態(終点操作)にします。
この処理をイメージするためにStream APIを使わなかった場合、使った場合を比較して見て見ましょう。
以下に書いたコードはListに格納された文字列を大文字に変換し、その中でも4文字の文字列のみを取り出し表示するソースコードを表しています。
// Stream APIを使わずに書いたコード
List<String> list2 = Arrays.asList("hoge", "foo", "bar","hoga");
List<String> stream2 = new ArrayList<String>();
for(String s :list2){
String sUpperCase = s.toUpperCase();
if(sUpperCase.length() == 4){
stream2.add(sUpperCase);
}
}
for(int i = 0; i < stream2.size(); i++){
System.out.println(stream2.get(i));
}
/*
結果は以下の通り
HOGE
HOGA
*/
// Stream APIを使った場合
List<String> list1 = Arrays.asList("hoge", "foo", "bar","hoga");
List<String> stream1 = list1.stream()
.map(String::toUpperCase)
.filter(s -> s.length() == 4)
.collect(Collectors.toList());
stream1.forEach(System.out::println);
/*
結果は以下の通り
HOGE
HOGA
*/
こんな感じでStream APIを使った方が、宣言的にやりたいことを書くだけでいいので可読性が良いです。
(他にも状態変化がなかったり、処理の遅延により無駄な処理が走らないなので利点はあります)
ひとまずStream APIの使い方のイメージが立ったところで、詳しい使い方について説明して行きます。
Streamの流れを以下のように切り分けて説明していきます。
1. Streamインスタンス作成
2. 中間操作
3. 終点操作
1. Streamインスタンス作成
Streamインスタンスを生成する方法は色々ありますが、ここではよく使いそうな4つを上げます。
クラス/インターフェース | メソッド | 概要 |
---|---|---|
Collection | stream() | Collectionインタフェースを継承しているクラスからStreamインスタンス生成 |
Arrays | stream (T[] array) | 配列からStreamインスタンスを生成 |
Stream | of(T… values) | 直接値を元にStreamインスタンスを生成 |
Stream | iterate(T seed, UnaryOperator f) | 順序付けされた無限Streamインスタンスを生成 |
ソースコードの書き方
// Collection#stream()
List<String> list = Arrays.asList("hoge", "foo", "bar");
Stream<String> stream = list.stream();
// Arrays#stream (T[] array)
String[] array = {"hoge", "foo", "bar"};
Stream<String> stream2 = Arrays.stream(array);
// Stream#of (T[] array)
Stream<String> stream3 = Stream.of("hoge", "foo", "bar");
// Stream#iterate (T seed, UnaryOperator f)
Stream<Integer> stream4 = Stream.iterate(2, x -> x * 2);
2. 中間操作
中間操作は大きく分けて3つの処理方法が存在します。
- 要素の絞り込み
- 要素の加工
- 要素の並び替え
1. Streamインスタンス作成で生成の仕方は理解したと思うので、その生成したStreamインスタンスに対して、中間操作の絞り込みを行って行きます。
※PredicateやFunctionなどありますが、これはあとで詳しく解説します。
とりあえずはPrediateにはtrue、falseを返す処理が書かれ、
Functionには加工する処理が書かれる程度に思っておいてください。
要素の絞り込み
絞り込みなのでinputとoutputの数が変わります。
メソッド | 概要 |
---|---|
filter(Predicate super T> predicate) | Predicateで定義したbooleanの判定がtrueの要素のみに絞ったStreamを返す |
limit(long maxSize) | 要素の最初からmaxSizeまでの要素のStreamを返す |
skip(long maxSize) | 要素の最初からmaxSizeまでの要素を飛ばしたStreamを返す |
distinct() | 要素同士をequalsメソッドで比較し重複するものを除いたStreamを返す |
ソースコードの書き方
List<String> list = Arrays.asList("hoge", "foo", "bar","hoga","hoga");
// filterを使って4文字の文字列のみ取り出した
System.out.println("****↓ filter****");
list.stream()
.filter(s -> s.length() == 4)
.forEach(System.out::println);
/*
出力結果
hoge
hoga
hoga
*/
List<String> list = Arrays.asList("hoge", "foo", "bar","hoga","hoga");
// limitを使い最初の文字から3つまでの文字列を取り出した
list.stream()
.limit(3)
.forEach(System.out::println);
/*
出力結果
hoge
foo
bar
*/
List<String> list = Arrays.asList("hoge", "foo", "bar","hoga","hoga");
// skipを使い最初の文字から要素番目の文字列を飛ばした
list.stream()
.skip(1)
.limit(2)
.forEach(System.out::println);
/*
出力結果
foo
bar
*/
List<String> list = Arrays.asList("hoge", "foo", "bar","hoga","hoga");
// distinctを使い重複している文字列を取り除いた
list.stream()
.distinct()
.forEach(System.out::println);
/*
出力結果
hoge
foo
bar
hoga
*/
要素の加工
加工していくだけなので、inputとoutputの数は変わりません。
例えば、小文字から大文字にしたり、文字列から文字列の文字数にしたりできます。
メソッド | 概要 |
---|---|
map(Function super T, ? extends R> function) | functionが引数として受け取ったTからRを返し、そのRを要素として生成されたStreamを返す |
ソースコードの書き方
List<String> list = Arrays.asList("hoge", "foo", "bar","hoga");
list.stream()
.map(String::length)
.forEach(System.out::println);
/*
出力結果
4
3
3
4
*/
要素の並び替え
メソッド | 概要 |
---|---|
sorted(Comparator super T> comparator) | Comparatorで比較し並べ替えたStreamを返す |
ソースコードの書き方
List<Person> people = Arrays.asList(
new Person("鈴木",24),
new Person("山田",53),
new Person("ジャンバルジャン",9),
new Person("横内",39));
// 年齢でソートした場合
people.stream()
.sorted(comparing(Person::getAge))
.forEach(s -> System.out.println(s.toString()));
/*
出力結果
ジャンバルジャン:9
鈴木:24
横内:39
山田:53
*/
// 名前でソートし、.reversed()を使い降順にした場合
people.stream()
.sorted(comparing(Person::getName).reversed())
.forEach(s -> System.out.println(s.toString()));
}
/*
出力結果
鈴木:24
横内:39
山田:53
ジャンバルジャン:9
*/
// 上記で使ったPersonクラス
public class Person {
private String name;
private int age;
public Person(String name, int age){
this.name = name;
this.age = age;
}
public String getName(){
return name;
}
public int getAge(){
return age;
}
public String toString(){
return name + ":" + age;
}
}
3. 終点操作
終点操作はStreamインスタンスを生成し、中間操作などを終えた最後の操作になります。
メソッド | 戻り値型 | 内容 |
---|---|---|
forEach(Consumer super T> consumer) | void | Streamの各要素をconsumerが引数として受け取り、処理を行う。並列処理で使った場合、元データがListのような集合体でも順番を保障しない |
forEachOrdered(Consumer super T> consumer) | void | Streamの各要素が順番を担保している場合、その各要素をconsumerが順番通り引数として受け取り、処理を行う |
toArray() | Object[] | Streamが持つ要素をObjectの配列にして返す |
reduce(T unit, BinaryOperator acc) | T | リダクション(畳み込み)。単位元unitに対して要素を累積関数accで結合していった結果を返す。 |
collect(Supplier factory, BiConsumer acc, BiConsumer combiner) | 結果コンテナ | 可変リダクション。factoryで生成した可変コンテナ(例えばArrayList)に対し、accで要素を追加し、combinerで各コンテナを結合する |
min(Comparator super T> comparator) | Optional | 要素の中で最小のものを返す。大小の比較には引数のcomparatorを使う。要素がなかった場合はemptyなOptionalを返す |
max(Comparator super T> comparator) | Optional | 要素の中で最大のものを返す。大小の比較には引数のcomparatorを使う。要素がなかった場合はemptyなOptionalを返す |
count() | long | Streamが持つ要素の個数を返す |
anyMatch(Predicate super T> predicate) | boolean | Streamの全ての要素がpredicateの判定でTrueを返す場合、戻り値としてTrueを返す |
allMatch(Predicate super T> predicate) | boolean | Streamの全ての要素がpredicateの判定でTrueを返す場合、戻り値としてTrueを返す |
noneMatch(Predicate super T> predicate) | boolean | Streamの要素がどれもpredicateの判定でTrueを返さない場合、戻り値としてTrueを返す |
findFirst() | Optional | 要素の中で最初の要素を返す。要素がなかった場合はemptyなOptionalを返す |
findAny() | Optional | 要素の中の1つの要素を返す。要素がなかった場合はemptyなOptionalを返す |
sum() | int / long /double | Streamが持つ要素の合計を返す。戻り値はそのStreamが表すものになる。要素がない場合は0を返す |
average() | OptionalDouble | 平均値を返す。要素がない場合はEmptyなOptionalDoubleを返す。割り切れない場合はdouble値で表現できる値に丸められる |
ソースコードの書き方
今回は量が多いので適当に3つほど抜粋して例を書きました。
戻り値にOptionalというのがありますが、これはあとで説明します。
List<Person> people = Arrays.asList(
new Person("鈴木",24),
new Person("山田",53),
new Person("ジャンバルジャン",9),
new Person("横内",39));
// 年齢でソートしてListに格納
List<Person> sortedPeople = people.stream()
.sorted(comparing(Person::getAge))
.collect(Collectors.toList());
System.out.println(sortedPeople);
/*
出力結果
[ジャンバルジャン:9, 鈴木:24, 横内:39, 山田:53]
*/
// 年齢で降順ソートし、最初のPersonを取得
Optional<Person> firstPerson = people.stream()
.sorted(comparing(Person::getAge).reversed())
.findFirst();
System.out.println(firstPerson);
/*
出力結果
Optional[山田:53]
*/
// 全手のPersonの年齢が10以上ならtrue、それ以外はfalse
boolean ss = people.stream()
.allMatch(p -> p.getAge() > 10);
System.out.println(ss);
/*
出力結果
false
*/
Optionalについて
Optionalとは?
null以外の値が含まれている場合も含まれていない場合も格納できるコンテナ・オブジェクトです。
if(xxx == null)などヌルポ処理をいちいちする必要が無くなります。
ドキュメンにわかりやすくまとめられているので一読し見てください。
https://docs.oracle.com/javase/jp/8/docs/api/java/util/Optional.html
Optionalの使い方
ifPresent(Consumer super T> consumer) メソッドを使った一例です。
苗字の文字数が3文字以上で最初に見つかった値を出力するプログラムです。
// Optionalを使わなかった場合
List<String> people = Arrays.asList("山田","田中","鈴木");
String firsPerson = null;
for(String s : people){
if(s.length() == 3){
firsPerson = s;
break;
}
}
// nullチェック
if(firsPerson != null){
System.out.println(firsPerson);
}
// Optionalを使い、値が存在するときのみ実行
Optional<String> firstPerson = Stream.of("山田","田中","鈴木")
.filter(s -> s.length() == 3)
.findFirst();
firstPerson.ifPresent(System.out::println);
値が存在する場合は指定されたコンシューマをその値で呼び出し、それ以外の場合は何も行いません。
Predicate
Predicateとは?
処理結果のbooleanを返します。
また処理を繋げることができるので、if文を大量に繋げている処理がある場合は
効果的に活用できます。
Predicateの使い方
T 型の値を引数に受け取って、 boolean の値を返す処理を実装する。
testメソッドに引数Tを与えることでbooleanの値が帰る。
Predicate<String> predicate = s -> s.length() == 5;
System.out.println(predicate.test("tarou"));
Function
Functionとは?
1つの引数を受け取って結果を生成する関数を表します。
Functionの使い方
T 型の引数を受け取って、 R 型の値を返す処理を実装する。
applyメソッドに引数Tを与えることで実行できる。
Function<String,String> toUpper = s -> s.toUpperCase();
System.out.println(toUpper.apply("tarou"));
Consumer
Optionalとは?
Optionalの使い方
T 型の値を引数に受け取って、戻り値無しの処理を実装する。
acceptメソッドに引数Tを与えることで、実行できる。
Consumer<String> consumer = string -> System.out.println("Cunsumer : " + string);
consumer.accept("tarou");
並列処理
comming soon
###遅延評価による再帰処理
comming soon