61
21

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Systemi(株式会社システムアイ)Advent Calendar 2023

Day 24

Streamとは?古いJavaでも新しく書こう~

Last updated at Posted at 2023-12-23

世の中は古い地味だと思われるJavaですが、今の現場の金融機関ではまだ比較的に新しい。そのJavaの比較的に新しい特性について少し書くことが今度のネタです。

Javaのバージョン事情

Javaの誕生は1995年、最新の長期サポート(LTS)バージョンは2023年9月にリリースされたJava SE 21です。長い時を渡り、時代と共に進化し続いてきたJavaですが、2024年に迎える現在、一番人気のあるバージョンは、なんと2014年3月がリリースされたJava SE 8です。

image.png

https://www.jetbrains.com/ja-jp/lp/devecosystem-2023/java/

JetBrainsのアンケート結果は上記の通りですし、この記事執筆した最初に、Qiitaのタグ候補もこの感じです。

Streamとは

では、この古くてもまだまだいけるJava8から追加されたイテレーションの拡張APIは、今回テーマのStreamです。

Streamは、日本語で「流れ」という意味になり、Javaで言うとデータの流れになります。要は工場のラインのようなものをイメージすると分かりやすいです。

入力データは、流れのように各段階で規定された処理だけが実行され、最終的に出力データは出来上がります。要約すると、Streamを使うには、3つのステップが必要です。

  • Streamの生成(Collection)
  • 中間操作(Intermediate)
  • 終端操作(Terminal)

image.png

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());

参考

61
21
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
61
21

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?