最近変数に破壊的な操作をするのがちょっと怖くなったり、なんもわからないなりに副作用を減らしてコードを書きたいという欲が出てきたと同時に、周りにstream()を活用してコードを書いている人があんまりいないな、と感じたので自分の書きたい欲を発散すると同時に布教がてらこの記事を書いています。
stream()とは
「そもそもstream()って何?」という人のためにものすごくざっくり説明しておくと、stream()はコレクション(List,Set,Mapなど)に対して様々な処理を行うためのものです。stream()のメソッドを使うと例えば「すべての要素を2倍する」であったりとか、「要素の中で〇〇を含むものだけを取り出す」といったような操作をすることができます。
勘のいい人であれば「それforとifでできるよね」と思ったと思うのですが、その通りです。ではなぜわざわざstream()を使うのでしょうか。メリットやデメリットをいくつか紹介したいと思います。
stream()のメリット・デメリット
オブジェクトの書き換えが起こらない
stream()を用いた場合、元のオブジェクトは書き換わりません。元のオブジェクトのコピーに対して操作を行うためです。次のコードを見てください。
import java.util.List;
import java.util.ArrayList;
import java.util.stream.Collectors;
public class Program {
//各要素の末尾に"Qiita"を結合したListを生成する。
public static void main(String[] args) {
List<String> arr = new ArrayList<>(List.of("hoge","fuga","piyo","foo","bar"));
for (int i = 0; i < arr.size(); i++) {
String str = arr.get(i) + "Qiita";
arr.set(i,str);
}
}
}
このコードでは文字列の結合を行っていますが、このような操作を行うとarr
を再利用したいときに最初に生成されたものとは違うものを扱うことになります。このコードは短いですし特に気にすることもないのですが、コード量が増えてきたときにオブジェクトの内容が変わっているとコードを追いづらくなってしまいます。
stream()を使うとこうなります。
import java.util.List;
import java.util.ArrayList;
import java.util.stream.Collectors;
public class Program {
//各要素の末尾に"Qiita"を結合したListを生成する。
public static void main(String[] args) {
final List<String> arr = new ArrayList<>(List.of("hoge","fuga","piyo","foo","bar"));
final List<String> operated = arr.stream().map(x -> x + "Qiita").collect(Collectors.toList());
/* for文だとこんな感じ
for (int i = 0; i < arr.size(); i++) {
operated.add(arr.get(i) + "Qiita");
}*/
}
}
このコードでは新しいオブジェクトに演算結果を格納しています。そのため、演算前のものを扱いたいときには簡単に扱うことができます。また、コメントアウトしているところでadd()
を使っていますが、このメソッドはオブジェクトの内容を変化させてしまうため、あまり使いすぎすぎると先ほど述べたような問題点が生じてきます。
ただし、もちろんデメリットもあり、オブジェクトのコピーを作成するためメモリをその分消費します。ですので、必ずしもstream()を使うことが最適だとは限りません。
コードが簡潔になることがある
次のコードを見てください。
import java.util.List;
import java.util.ArrayList;
import java.util.stream.Collectors;
public class Program {
//Listの要素の総和を取る
public static void main(String[] args) {
final List<Integer> arr = new ArrayList<>(List.of(1,2,3,4,5,6,7,8,9,10));
Integer sum = 0;
for (Integer val : arr) {
sum += val;
}
}
}
同じ操作をstream()ではこう書けます。
import java.util.List;
import java.util.ArrayList;
import java.util.stream.Collectors;
public class Program {
//Listの総和を取る。
public static void main(String[] args) {
final List<Integer> arr = new ArrayList<>(List.of(1,2,3,4,5,6,7,8,9,10));
final Integer sum = arr.stream().mapToInt(i -> i).sum();
}
}
このように、使いどころによってはforを使うよりもコードを簡潔にすることができます。
見た目がかっこいい
正直これが一番動機として大きいんですがメソッドチェーンの見た目カッコよくないですか?
具体的な使い方
①map
mapはstreamの各要素に対して指定した操作を行った結果を返すメソッドです。
具体例を見てみましょう。先ほどの、Listの要素を2倍にするコードをmapで書き換えるとこうなります。
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
public class Program {
public static void main(String[] args) {
final List<Integer> arr = new ArrayList<>(List.of(1, 2, 3, 4, 5));
final List<Integer> ret = arr.stream()
.map(x -> 2 * x)
.collect(Collectors.toList());
ret.forEach(x -> System.out.print(x + " "));
System.out.println();
}
}
どうでしょう、ものすごくすっきりしたと思いませんか?
では軽く説明をしましょう。まず stream()
で ret
をStreamに変換します。これを挟むことで map
が使えるようになります。
map
の中を見てみると、x -> 2 * x
という書き方がされています。これはラムダ式というもので、Streamの各要素xに対してxを2倍するよ、ということを書いています。このStreamの各要素はListの各要素に対応していると考えて問題ありません。
最後にcollect(Collectors.toList())
の部分ですが、ここではStreamをListに変換しています。今回はListですが、MapやSetに変換することもできます。
先ほども書きましたが、stream()で操作を行っても元のオブジェクトは書き換わりません。言い換えると上の例ではmap
を適用した後にarr
を出力しても元のデータのままです。ですのでstreamの操作を行った後は別のオブジェクトにその結果を格納するのがよいでしょう。
ではもう一つ具体例を。String型のListの各要素に"Qiita"という文字列をくっつけたいとします。これもmapで実現できるでしょうか。もちろんできます。
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.LinkedHashSet;
public class Program
{
public static void main(String[] args) {
final Set<String> arr = new LinkedHashSet<>(Set.of("hoge","fuga","piyo","foo","bar"));
final List<String> ret = arr.stream()
.map(x -> x + "Qiita")
.collect(Collectors.toList());
ret.forEach(x -> System.out.print(x + " "));
System.out.println();
}
}
実行結果はこうなります。
hogeQiita fugaQiita piyoQiita fooQiita barQiita
見事に欲しいものを生成することができました。先ほども書きましたが、上二つの例ではstream()を適用した変数の中身が書き換わることはありません。よって、後でその変数を再利用したくなったときに、そのままその変数を使うことができます。うっかりミスをしてしまいがちな添え字からも解放されたのは大きいですね。
②filter
次に紹介するメソッドはfilterです。filterを用いると、元のオブジェクトのうち条件を満たしたものだけを取り出したオブジェクトを生成することができます。
次のコードを見てみてください。
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
public class Program {
public static void main(String[] args) {
final List<String> arr = new ArrayList<>(List.of("hoge", "fuga", "piyo", "foo", "bar"));
final Set<String> ret = arr.stream()
.filter(x -> x.length() > 3)
.collect(Collectors.toSet());
ret.forEach(x -> System.out.print(x + " "));
System.out.println();
}
}
これを実行した結果が次です。
hoge fuga piyo
mapとほとんど書き方は変わっていないように見えますが、先ほどは要素に対して適用したい操作を書いていた場所に、条件式が書いてあると思います。ここで指定した条件を満たす要素だけを取り出してくれるのがfilterです。
上のコードをforを使って書くとこうなります。
import java.util.ArrayList;
import java.util.List;
import java.util.LinkedHashSet;
import java.util.Set;
public class Program {
public static void main(String[] args) {
final List<String> arr = new ArrayList<>(List.of("hoge", "fuga", "piyo", "foo", "bar"));
final Set<String> ret = new LinkedHashSet<>();
for (String val : arr) {
if (val.length() > 3)
arr.add(val);
}
ret.forEach(x -> System.out.print(x + " "));
System.out.println();
}
}
また、mapとfilterを組み合わせることもできます。
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
public class Program {
public static void main(String[] args) {
final List<String> arr = new ArrayList<>(List.of("hoge", "fuga", "piyo", "foo", "bar"));
final Set<String> ret = arr.stream()
.filter(x -> x.length() > 3 && x.charAt(0) < 'j')
.map(x -> x + "Qiita")
.collect(Collectors.toSet());
ret.forEach(x -> System.out.print(x + " "));
System.out.println();
}
}
出力結果はこんな感じです
hogeQiita fugaQiita
これをforを使って書くとこうなります。
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.LinkedHashSet;
public class Program {
public static void main(String[] args) {
final List<String> arr = new ArrayList<>(List.of("hoge", "fuga", "piyo", "foo", "bar"));
final Set<String> ret = new LinkedHashSet<>();
for (String val : arr) {
if (val.length() > 3 && val.charAt(0) < 'j')
ret.add(val + "Qiita");
}
ret.forEach(x -> System.out.print(x + " "));
System.out.println();
}
}
③sorted
これは名前の通りオブジェクトをソートしてくれるメソッドです。次のコードを例として挙げます。
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
import java.util.Comparator;
public class Program {
public static void main(String[] args) {
final List<Integer> arr = new ArrayList<>(List.of(100,2,-1,102,1000000007,2020,50,5,998244353));
//昇順ソート
final List<Integer> naturalOrder = arr.stream()
.sorted()
.collect(Collectors.toList());
//降順ソート
final List<Integer> reverseOrder = arr.stream()
.sorted(Comparator.reverseOrder())
.collect(Collectors.toList());
naturalOrder.forEach(x -> System.out.print(x + " "));
System.out.println();
reverseOrder.forEach(x -> System.out.print(x + " "));
System.out.println();
}
}
注意点として、小さい順のソートであればsorted()の引数に何も指定しなくてよいのですが、自分で決めた独自の順番や降順でソートをしたい場合はコード例のようにComparatorというものを渡す必要があります。Comparatorについてはここでは触れません。
stream()を使うことで、元のオブジェクトの順番を変えることなくソートをすることができました。
どうでしょう、少しはstream()が怖くないと思ってもらえたでしょうか。うまく使えばコードをとても簡潔にすることができる非常に便利な武器です。今回紹介した以外にもメソッドは存在しますので、よろしければ調べてみてください。
それではみなさん、よいJavaライフを!
おまけ
2次元リストに対してstream()
を使う例です。2次元リストはリストを要素に持つリストだととらえられるので、イメージとしては2次元リスト→1次元リスト→1次元リストの各要素という風に次元を下げていく必要があり、stream()のネストが発生します。まあforを書いてもネストは発生しますが……
import java.util.List;
import java.util.ArrayList;
import java.util.stream.Collectors;
public class Program {
public static void main(String[] args) {
final List<List<Integer>> arr = new ArrayList<>(Arrays.asList(Arrays.asList(1, 2, 3, 4, 5), Arrays.asList(2, 3, 4, 5, 6), Arrays.asList(3, 4, 5, 6, 7)));
final List<List<Integer>> ans = arr.stream().map(x -> x.stream()
.filter(v -> v > 3)
.collect(Collectors.toList()))
.collect(Collectors.toList());
ans.forEach(v ->
{
v.forEach(x -> System.out.print(x + " "));
System.out.println();
});
}
}