JavaのStreamで二つの配列から重複する要素を削除したり、独自クラスを色々する方法。

JavaのStreamで二つの配列から重複する要素を削除したり、独自クラスを色々する方法。

Java8で実装された何かと便利なStream API。
仕事で二つの配列から重複する要素を削除するStream処理を作った。
その他Streamで独自クラスをあれこれする際に役に立った処理を紹介する。

Stream APIとは

そもそもStreamとは何か、こちらの記事が詳しくまとめている。
Java Stream APIをいまさら入門
Stream APIとはJava8から追加されたAPIで、
ざっくり言うといままでforとかで回す処理を書いていたものが、
かなり簡単に書けるようになったというもの。
後並列処理が簡単に書けるようになるらしいです。

記事にもありますが、今まで以下の様に書いていたのが、

List list = Arrays.asList(1,2,3);
for (int i : list) {
  //中間処理
}

以下の様に書けるようになります。

List list = Arrays.asList(1,2,3);
list.stream().//中間処理

ちなみに、こちらの記事によると単純処理のパフォーマンスは良くならないというか、むしろ遅くなるようです。
Java それぞれ書き方でどれほどパフォーマンスが違うのか?計測比較してみた。Streamとループとか

普段使いでは気にするほどの速度ではないです。
とはいえ最近はWebの高速化もSEOに影響してくるのでもし単純処理のループが多くなってくるようなら気にした方がいいかもです。
ただ、複雑な並列処理を行う場合は当然並列で操作した方が速いと思うので、
そこらへんは上手く使い分けないといけないかなと思います。
(ただやっぱ並列処理は難しくてかえってパフォーマンス落ちることもあるので結局工数と相談になりそう・・・。)

二つの配列から重複するものを削除する。

今回行いたいのは、二つの配列から重複する要素を削除する処理。
例えば、正解となるリストと、正誤入り混じったレスポンスが返ってくるような場合に使いたい。
これが、従来の処理だと以下の様になる。

		List list = Arrays.asList(1, 2, 3, 4, 5);
		List list2 = Arrays.asList(3, 4, 5, 6, 7);
		List mList = new ArrayList();
		for (int i : list) {
			for (int k : list2) {
				if (i == k) {
					mList.add(i);
				}
			}
		}
// => [3,4,5]

これをStreamでやると以下のようになる。

 

		List list = Arrays.asList(1, 2, 3, 4, 5);
		List list2 = Arrays.asList(3, 4, 5, 6, 7);
		List mList = list.stream().filter(e -> list2.contains(e)).collect(Collectors.toList());
// => [3,4,5]

かなり見やすくなった。
単純なのだが、forの動きに引っ張られてlist2をループしないといけないと思い詰まってしまったのでメモ。
ちなみに、forの処理イメージに寄せて下の様に書くことも出来る。
そして独自クラスを扱う場合には以下の方法を使うのがよさそう。

		List list = Arrays.asList(1, 2, 3, 4, 5);
		List list2 = Arrays.asList(3, 4, 5, 6, 7);
		List mList = list.stream()
				.filter(e -> list2.stream().anyMatch(e2 -> e == e2))
				.collect(Collectors.toList());
// => [3,4,5]

独自クラスを使っている場合に二重ループが必要だったりするケース

独自クラスの中にループが二つあるとかの場合Streamを使うのがちょっと面倒。
例えば以下のような独自クラスがあるとします。

public class Human {
	String age;
	String name;
	String gender;
	List works;
}

public class Work {
	String company;
	String companyId;
}

独自クラスHumanがあり、Listの中にさらにWorksという独自クラスが入ってる感じです。
実際にこんな独自クラスがあったら怒られそうですがテストだし良い例が思いつかなかったから許して

例として以下の様にデータを突っ込みます。

		Human human1 = new Human();

		Work work1 = new Work();
		work1.company = "hoge";

		Work work2 = new Work();
		work2.company = "fuga";

		List tmp1 = new ArrayList();

		tmp1.add(work1);
		tmp1.add(work2);

		human1.name = "hogeo";
		human1.works = tmp1;

		Human human2 = new Human();

		Work work3 = new Work();
		work3.company = "fuga";

		List tmp2 = new ArrayList();
		tmp2.add(work3);

		human2.name = "fugao";
		human2.works = tmp2;

		List humans = new ArrayList();
		humans.add(human1);
		humans.add(human2);

ここでcompanyに対して比較処理を入れたいとする。
例えばhoge会社に勤める人を見つけたい場合、従来のforを使った方法は以下の様になる。

		for (Human human : humans) {
			for (Work work : human.works) {
				if ("hoge".equals(work.company)) {
					System.out.print(human.name);
				}
			}
		}
// => hogeo

で、Streamを使うと以下の様になる。

		humans.stream()
					.filter(human -> human.works.stream()
							.anyMatch(work -> "hoge".equals(work.company))
					)
					.forEach(human -> System.out.println(human.name));
// => hogeo

ぶっちゃけ可読性が悪くなってきている。
anyMatchはcontainsのようなもので、「一個でも含まれればtrueを返す」というもの。

業務ではこれを使って、「hoge会社に勤める人だけを先頭に追加し、後ろにそれ以外の人を追加」のようなことを行った。
実際に使ったのは以下のような方法。

		List sortedHumans = new ArrayList();
		humans.stream()
					.filter(human -> human.works.stream()
							.anyMatch(work -> "hoge".equals(work.company))
					)
					.forEach(human -> sortedHumans.add(human));

		humans.stream()
					.filter(human -> human.works.stream()
							.allMatch(work -> !"hoge".equals(work.company))
					)
					.forEach(human -> sortedHumans.add(human));

allMatchは「全てに含まれればtrueを返す」というもの。containsAllとかそんな感じのメソッドと同じだったと思う。多分。
そのため、hogeと比較して論理演算子!(否定)を入れて、「全てにhogeが含まれなければtrue」という条件にマッチしたら追加してます。

今回の例だと特に並びは変わらないが、色々文字をいじると実際に並び順が変わることが分かってもらえると思う。
(hogeは実際にはマジックナンバーなのでどっかに定数化するか変数にぶちこんでやったと思いますが今回書いてるときはべた書きしてしまいました許して。)

まとめ

業務で沢山Streamを使う機会があって勉強になったので誰かの役に立てば&メモとして。
ただStreamって新人が勉強するのは結構難しいという問題があると思います。
forって直感的で分かりやすいんだなぁと今更ながら思いました。
慣れてない人は最初は無理にStreamやるよりforでやってなれたらStreamとか触れるのがいいかもですね。