StreamAPIとは
Stream APIは名前の通り、ストリーム(Stream)という流れてくるデータやイベントを処理するためのAPI群です。
Java SE8で追加された
StreamはListやMapなどのデータ集合をもとに生成し、0回以上の中間操作と、1回の終端操作を実行することで結果を得る。
StreamAPIの使い方
ざっくりと概要を掴んだ上で実際にソースを見てstreamを使用した処理と使用していない処理を比べて見ます。
サンプルとしてListを加工して一件づつ処理を行うコードを記載します。
※ラムダ式については詳しく記載しません。詳しくはこちらの記事を参照してください
Javaラムダ式についてまとめてみた
List<Integer> integerList = Arrays.asList(1, 2, 3, 4, 5);
for (Integer i : integerList) {
if (i % 2 == 0) {
System.out.println(i);
}
List<Integer> integerList = Arrays.asList(1,2,3,4);
integerList.stream()
.filter(i -> i % 2 ==0)
.forEach(i -> System.out.println(i));
2
4
簡易的なListをfor文で回し、if文で判定させ出力しているコードになります。
for文で回して処理を行う箇所はstreamで置き換えることができ、if文はfilterで内部の要素に直接絞り込みをかけ、forEachで残りの要素分出力を行っています。
Java8以前の環境に慣れている方には前者の方が見やすいかもしれませんが、streamの記述に慣れてくると後者の方が使いやすくわかりやすいものになってきます。
streamでは何をするかわかる単位でメソッドが分かれており、各メソッドの使用方法を抑えると直感的に処理内容をつかめる記述方法となっています。
Stream APIは、データ構造に対してStreamを「生成する」「操作する」「まとめる」の処理を連続して行うことにより使用されます。
生成操作
streamの生成にはざっくりと以下のようなメソッドがあります。
java.util.stream.Stream#of(T...) : Stream
java.util.stream.Stream.Builder#build() : Stream
java.util.Collection#stream() : Stream
java.util.Arrays#stream(T[]) : Stream
java.io.BufferedReader#lines() : Stream
java.nio.Files#lines(Path) : Stream
java.nio.Files#lines(Path, Charset) : Stream
java.util.regex.Pattern#splitAsStream(CharSequence) : Stream
java.lang.CharSequence#chars() : IntStream
java.lang.CharSequence#charPoints() : IntStream
今回はよく使用するListやSetなどのCollectionインタフェースから作る場合と、配列から作る2つを紹介する。
ListやSetからStreamを作る場合には、stream()メソッドを使います。
配列からStreamを作る場合には、Arrays.stream()メソッドを使います。
// Listのstream処理
List<Integer> numbers = List.of(3, 1, -4, 1, -5, 9, -2, 6, 5, 3, 5);
Stream<Integer> stream = numbers.stream();
// 配列のsteram処理
int[] array = {3, 1, -4, 1, -5, 9, -2, 6, 5, 3, 5};
IntStream stream = Arrays.stream(array);
MapからStreamを作りたい場合は、Mapから直接Streamを作るAPIが用意されていないため、MapのentrySetメソッドを用いて得られるSetからStreamを生成します。
Map<String, Integer> map = Map.of("key1", 3, "ke2", 1, "key3", -4, "key4", 1);
Stream<Entry<String, Integer>> stream = map.entrySet().stream();
テキストファイルを1行ずつ読んで文字列のStreamとする場合に、Files.lineメソッドが利用できます。
Stream<String> lines = Files.lines(Path.of("/tmp/test.txt"));
中間操作
Streamに対して行う処理を中間操作と呼んでいます。中間操作には以下のメソッドがあります。
peek(Consumer super T> action)
skip(long maxSize)
limit(long maxSize)
filter(Predicate super T> predicate)
distinct()
sorted() / sorted(Comparator super T> comparator)
map(Function super T,? extends R> mapper)
flatMap(Function super T,? extends Stream extends R>> mapper)
parallel()
sequencial()
unordered()
今回はこの中のよく使うメソッド
sorted,map,filterに絞り紹介していきます。
※他のメソッドについては別記事でまとめます
1.sorted()
sorted() は、Stream の要素をソートした Stream を返す。
sorted() には引数あり・なしの2つのオーバーロードが用意されている。
- sorted()
- sorted(Comparator super T> comparator)
引数なしの sorted() は要素を単に自然順序昇順でソートする。(ただし要素クラスは Comaprable でなければならない。
”不自然な”順序や降順でソートしたい場合には、引数ありの soreted() に、カスタムの Comparator を関数として渡す。(ラムダで与えるなら Comparator#comparing() を使ったほうがシンプルになる。)
List<String> array = Arrays.asList("a","1","-2","あ","A","123");
System.out.println("--引数なし--");
// 引数なし
array.stream()
.sorted() // 自然順序昇順
.forEach(System.out::println);
System.out.println("--Comparator--");
// Comparator
array.stream()
.sorted(Comparator.reverseOrder()) // 自然順序降順
.forEach(System.out::println);
System.out.println("--ラムダ式--");
// ラムダ式
array.stream()
.sorted((l, r) -> l.length() - r.length()) // 文字列長降順
.forEach(System.out::println);
System.out.println("--メソッド参照--");
// メソッド参照(Comparable)
array.stream()
.sorted(String::compareToIgnoreCase) // 大小文字無視
.forEach(System.out::println);
System.out.println("--関数オブジェクト--");
// 関数オブジェクト
array.stream()
.sorted(String.CASE_INSENSITIVE_ORDER) // 大小文字無視
.forEach(System.out::println);
// java.util.List` にも 1.8 で `sort()` がデフォルトメソッドで追加されている。
// List 自体の要素順が変更するのが目的ならこちらの方が経済的だ。
System.out.println("--Listによるソート--");
// Comparator を指定する
array.sort(Comparator.reverseOrder());
array.stream()
.forEach(System.out::println);
--引数なし--
-2
1
123
A
a
あ
--Comparator--
あ
a
A
123
1
-2
--ラムダ式--
a
1
あ
A
-2
123
--メソッド参照--
-2
1
123
a
A
あ
--関数オブジェクト--
-2
1
123
a
A
あ
--Listによるソート--
あ
a
A
123
1
-2
2.filter()/distinct()
filter() と distinct() は、Stream の要素をその内容で間引く。
filter() は要素を条件によって絞り込む中間操作で、引数には判定のための述語関数(Predicate)を与える。
述語が要素の値だけを見ている限り、filter() はステートレス中間操作。
distinct() は要素の重複を排除する中間操作。
System.out.println("--filter--");
List array = Arrays.asList("a.txt","b.com","c.new");
array.stream()
.filter(s -> ((String) s).endsWith(".txt")) // ラムダ式
.forEach(System.out::println);
System.out.println("--distinct--");
String data = "aaa aaa bbb aaa ccc bbb ccc ccc";
String uniq = Stream.of(data.split("\\s"))
.peek(s -> System.out.print("\n" + s))
.distinct()
.peek(s -> System.out.print("\t" + s))
.collect(Collectors.joining(","));
System.out.println();
System.out.println(uniq);
--filter--
a.txt
--distinct--
aaa aaa
aaa
bbb bbb
aaa
ccc ccc
bbb
ccc
ccc
aaa,bbb,ccc
3.map()
map() は、与えられた関数で要素を変換した Stream を返す。
関数の戻り値の型で、 Stream の要素の型を変更してもよい。
//関数の用意(先頭文字を大文字にする)
Function<String, String> capitalize = (String s) -> {
return String.format("%c%s",
Character.toTitleCase(s.charAt(0)),
s.substring(1));
};
List<String> words = Arrays.asList("aasDfag");
words = words.stream()
.peek(System.out::println)
.map(s -> s.trim()) // ラムダ式
.filter(s -> !s.isEmpty())
.map(String::toLowerCase) // メソッド参照
.peek(System.out::println)
.map(capitalize) // 関数オブジェクト
.collect(Collectors.toList());
System.out.println(words);
aasDfag
aasdfag
[Aasdfag]
終端操作
終端操作では大きく分けて4つの処理が可能となっている。
- 検索
- 集約
- 変換
- 出力
今回はこの四つに分けてメソッドを紹介していきます。
1.検索
1.1. findFirst()/findAny()
findFirst()は始めの要素をOptionalで返す。
findAny()は初めの要素をOptionalで返す。
Optionalは空かもしれない。
String[] words = {"aaaaaa", "bbbbbb", "cccccc"};
List<String> list = Arrays.asList(words);
Optional<String> first = list.stream().findFirst();
first.ifPresent(s -> {
System.out.println(s); // "aaaaaa"
});
Set<String> set = new HashSet<>(list);
Optional<String> any = set.stream().findAny();
any.ifPresent(s -> {
System.out.println(s); // "cccccc"
});
1.2. allMatch() / anyMatch() / noneMatch()
allMatch()/anyMatch()/noneMatch()は、与えられた述語関数(Predicate) を条件に Stream 結果の要素を検索し、マッチする要素の存在状態を判定する。
List<String> list = Arrays.asList("a","asdf","");
boolean ok;
// ラムダ式
ok = list.stream()
.allMatch(s -> s != null && !s.isEmpty()); // nullと空文字列を含まない
System.out.println(ok);
// メソッド参照
ok = list.stream()
.allMatch(Objects::nonNull); // null を含まない
System.out.println(ok);
// 述語関数
ok = list.stream()
.noneMatch(Predicate.isEqual("")); // null可で空文字列を含まない
System.out.println(ok);
false
true
false
2. 集約
2.1. count() / min() / max()
count()は文字通り Stream の要素数を数える。
本当に数えるので、それなりの処理時間がかかる。
// テキストファイルの行数
int lc = (int) Files.lines(Paths.get("text.txt")).count();
// テキストの単語数
int wc = (int) Pattern.compile("\\W+").splitAsStream(text).count();
// 文字の異なりを数える
int vc = (int) text.codePoints().distinct().count();
min()/max()には比較関数(Comparator)を渡して要素の最大値・最小値を得る。
戻り値はOptionalで、要素がなかった場合に empty となる。
Stream は必ず最後まで読み込まれる。
List<String> list = ... ;
Optional<String> min;
// Comparator
min = list.stream()
.min(Comparator.naturalOrder()); // 辞書順で最小の文字列
// メソッド参照
min = list.stream()
.min(String::compareToIgnoreCase); // 大小文字区別しない
// ラムダ式
min = list.stream()
.min((l, r) -> l.length() - r.length()); // 最短文字列
// Comparable
min = list.stream()
.min(Comparator.comparing(s -> s.toUpperCase())); // 大小文字区別しない
DoubleStream などプリミティブ系 Stream には、count()/min()/max()だけでなくsum()やaverage()といった終端操作も用意されている。
ループなしで集計値を得られるのは便利だが、さらにその集計値を使った計算をしようとすると、何回も Stream を走らせるはめになって逆に効率が悪い。そのためか各集計値を一発でとれるsummaryStatistics()という終端操作もある。
2.2. reduce()
カスタムの集約関数を適用したい場合はreduce()を使う。たとえば join では単純に実現できない連結処理するのに使える。
// 最後の要素を取り出す
Optional<String> last = stream.reduce((l, r) -> r);
// ドメイン名をパッケージ名に変換する
String domain = "hoge.example.co.jp";
// 要素順を逆順にする
String pkg = Stream.of(domain.split("\\."))
.reduce((l, r) -> r + "." + l).get();
// jp.co.example.hoge
正直なところ、いまいち使い方が理解できなかった。。
後日調べて記事に追加します。
3. 変換する
3.1. toArray()
toArray()は Stream をその要素の配列に変換する。
Stream<String> stream = Stream.of("a", "b", ...);
// 引数なし
Object[] arr = stream.toArray();
// 配列コンストラクタ参照
String[] arr = stream.toArray(String[]::new);
// ラムダ式
String[] arr = stream.toArray((size) -> new String[size]);
3.2. collect()
streamからListに変換する。
引数にはjava.util.stream.Collectors
に、 static なファクトリメソッドとのコレクションとしてまとめられているメソッドを呼び出し、定義する。
Java Collectorメモ(Hishidama's Java8 Collector Memo)
// Collectors を static インポートすることでクラス名を省略できる。
// IDEの設定によってはワイルドカード(*)を使わせてくれないかもしれない。
import static java.util.stream.Collectors.*;
Stream<String> stream = ... ;
// 文字列に変換(連結)
String text = stream.collect(joining());
// 文字列に変換(区切り文字指定)
String csv = src.stream().collect(joining(", "));
// List に変換
List<String> list = stream.collect(toList()); // ArrayList
// 任意の List クラスに変換
List<String> list = stream
.collect(toCollection(LinkedList::new)); // LinkedList
// Set に変換
Set<String> set = stream.collect(toSet()); // HashSet
// 任意の Set クラスに変換
SortedSet<String> set = stream
.collect(toCollection(TreeSet::new)); // TreeSet ソート済み
LinkedHashSet<String> set = stream
.collect(toCollection(LinkedHashSet::new)); // LinkedHashSet 要素順を維持
// Map に変換
// id -> object
Map<Integer, User> map = users.stream()
.collect(toMap(
e -> e.getId(), // key 重複すると例外になる
e -> e // value
)); // HashMap
// id -> name
Map<Integer, User> map = users.stream()
.collect(toMap(User::getId, User::getName));
// 任意の Map クラスに変換する
SortedMap<Integer, User> map = users.stream()
.collect(toMap(
e -> e.getId(),
e -> e,
(l, r) -> r, // キーが重複したら上書き
TreeMap::new
)); // TreeMap
4. 出力する
4.1. forEach() / forEachOrdered()
forEach()は送られてきたstreamデータに出力や変換を行える終端操作。
List<String> list = Arrays.asList("a", "b", "c");
// Stream の forEach() が副作用であることを
// ブロックを使って明示したい
list.stream()
.forEach(s -> {
System.out.println(s);
});
// ブロックを使わないとパッと見いかにも値を返してそう
list.stream().forEach(s -> System.out.println(s));
// forEach() がメソッド参照を使うとはおこがましい
list.stream().forEach(System.out::println);
// Iterable の forEach()
list.forEach(s -> {
System.out.println(s);
});
// 同等の拡張 for 構文
for (String s : list) {
System.out.println(s);
}
Map<String, String> map = new HashMap<>();
// 実は Map にもある
map.forEach((key, val) -> {
System.out.format("%s=%s\n", key, val);
});
// Map でも Stream を使いたい
map.entrySet().stream()
.forEach(e -> {
System.out.format("%s=%s\n", e.getKey(), e.getValue());
});