世の中は古い地味だと思われるJavaですが、今の現場の金融機関ではまだ比較的に新しい。そのJavaの比較的に新しい特性について少し書くことが今度のネタです。
Javaのバージョン事情
Javaの誕生は1995年、最新の長期サポート(LTS)バージョンは2023年9月にリリースされたJava SE 21です。長い時を渡り、時代と共に進化し続いてきたJavaですが、2024年に迎える現在、一番人気のあるバージョンは、なんと2014年3月がリリースされたJava SE 8です。
JetBrainsのアンケート結果は上記の通りですし、この記事執筆した最初に、Qiitaのタグ候補もこの感じです。
Streamとは
では、この古くてもまだまだいけるJava8から追加されたイテレーションの拡張APIは、今回テーマのStreamです。
Streamは、日本語で「流れ」という意味になり、Javaで言うとデータの流れになります。要は工場のラインのようなものをイメージすると分かりやすいです。
入力データは、流れのように各段階で規定された処理だけが実行され、最終的に出力データは出来上がります。要約すると、Streamを使うには、3つのステップが必要です。
- Streamの生成(Collection)
- 中間操作(Intermediate)
- 終端操作(Terminal)
Streamを使えば、今まで手続き型言語の感じでデータを処理する作業は、あまりループを意識せずに、流れのように処理することができます。
//20歳を超える利用者を抽出する
List<User> result=new ArrayList<>();
for(User user:users){
if(user.getAge()>20){
result.add(user);
}
}
return result;
//Streamを使った実装
return users.stream().filter(s->s.getAge()>18).collect(Collectors.toList());
Streamの生成
- 大体の Collection は .stream() があります
Collection.stream()
Arrays.asList(1,2,3).stream()
Arrays.stream(new int[]{1,2,3})
- Steamの静的メソッドも使えます
IntStream.range(int, int)
Stream.iterate(0, n -> n * 2)
Random.ints()
- ファイル操作(I/O)からもStreamが作れます
java.nio.file.Files.walk()
java.io.BufferedReader.lines()
Streamの中間操作
filter
偶数だけを取得する
List<Integer> l = IntStream.range(1,10)
.filter( i -> i % 2 == 0)
.boxed()
.collect(Collectors.toList());
System.out.println(l); //[2, 4, 6, 8]
map
文字要素をそれぞれハスキーコードに変換する
List<Integer> l = Stream.of('a','b','c')
.map( c -> c.hashCode())
.collect(Collectors.toList());
System.out.println(l); //[97, 98, 99]
limit
先頭要素だけ取得する
List<Integer> l = IntStream.range(1,100).limit(5)
.boxed()
.collect(Collectors.toList());
System.out.println(l);//[1, 2, 3, 4, 5]
peak
要素に対してConsumerを適用した後、次へ流します
String[] arr = new String[]{"a","b","c","d"};
Arrays.stream(arr)
.peek(System.out::println) //a,b,c,d
.count();
Streamの終端操作
中間操作と似ているメソッドが結構ありますが、違いはそのまま次へ流さず、そのまま戻り値を返却するところです。要はStreamの流れを終わらせるメソッドです。
Match
filterの終端バージョン、判断結果をTrue/Falseで返却する
System.out.println(Stream.of(1,2,3,4,5).allMatch( i -> i > 0)); //true
System.out.println(Stream.of(1,2,3,4,5).anyMatch( i -> i > 0)); //true
System.out.println(Stream.of(1,2,3,4,5).noneMatch( i -> i > 0)); //false
System.out.println(Stream.<Integer>empty().allMatch( i -> i > 0)); //true
System.out.println(Stream.<Integer>empty().anyMatch( i -> i > 0)); //false
System.out.println(Stream.<Integer>empty().noneMatch( i -> i > 0)); //true
forEach
peakの終端バージョン
Stream.of(1,2,3,4,5).forEach(System.out::println);
reduce
要素を集約する、iterateの特性がある
Optional<Integer> total = Stream.of(1,2,3,4,5).reduce( (x, y) -> x +y);
Integer total2 = Stream.of(1,2,3,4,5).reduce(0, (x, y) -> x +y);
count
処理後の要素数を返却する
System.out.println(IntStream.range(1,100).limit(5).count()); //5
collect
Streamの結果をCollectorに格納するメソッドだが、2種類がある
<R,A> R collect(Collector<? super T,A,R> collector)
<R> R collect(Supplier<R> supplier, BiConsumer<R,? super T> accumulator, BiConsumer<R,R> combiner)
引数が1つしかないやつはCollectorの特性が分かればそのまま使える
//年齢でgroupby
Map<Integer,List<User>> map= users.stream().
collect(Collectors.groupingBy(user::getAge));
引数が2つのやつは、概ねスレッド実行の時に、最後にそれぞれの結果を合併する必要がある場合が使われる
(容器生成するメソッド, 容器に格納するメソッド, 容器を合併するメソッド)
List<String> asList = stringStream.collect(ArrayList::new, ArrayList::add,
ArrayList::addAll);
String concat = stringStream.collect(StringBuilder::new, StringBuilder::append,
StringBuilder::append)
.toString();
遅延評価
遅延評価 (lazy evaluation) は、よくHaskellの特徴として挙げられています。
-- 先頭の10個だけが生成され
Prelude> take 10 [1..]
[1,2,3,4,5,6,7,8,9,10]
-- 先頭の4個だけが生成され
Prelude> zip [1..] ["a","b","c","d"]
[(1,"a"),(2,"b"),(3,"c"),(4,"d")]
上記のように、無限長リストの構文であっても、別に無限ループが実行されるわけではなく、終端操作(take zip)が必要な数だけは評価され、実行されました。
実はこの遅延評価はStreamの特徴でもあります。Streamは一連の操作の中、中間操作が呼び出されただけで、式として評価(実行)されません。終端操作が呼び出されてから、初めて式として評価され、実行されます。
//先頭の10個だけが生成され
Stream.iterate(0, i -> ++ i).limit(10).forEach(System.out::print);
//終端操作がなければ何も実行されない
new Random().ints().limit(10).peek(System.out::println);
最後に
遅延評価ができる純粋関数型言語のHaskellはつい延々と長いコードになりがちですが、StreamはあくまでJava8の特性の一つです。ソースコードの可読性を注意しましょう。
//良い例
users.stream().limit(3).
sorted(Comparator.comparingInt(user::getAge)).
peek(System.out::println).
collect(Collectors.toList());
//悪い例
users.stream().limit(3).sorted(Comparator.comparingInt(user::getAge)).peek(System.out::println).collect(Collectors.toList());
参考