Streamの落とし穴
Java8のStreamは非常に便利だがあくまでも「フロー」を提供するものであることは注意する必要がある。その中でもallMatch
は非常に危険!というか注意して使わないと副作用が起きる。なぜならallMatchは短絡評価をするからである(判定条件にそぐわないものがあった場合、以降の処理を継続せず、そこで処理を終了する)
テスト内容
文字列を保持するクラスをつくり、それを保持するListに対して、文字列長がすべて5文字以下であるかの判定をし、かつ6文字以上のデータが含まれていた場合にはその文字列の代わりにsorry
という文字列を代入するプログラム。以下の4種類の処理方法でテスト。
- A. Listに対して直接allMatchを実行
- B. mapしてからStream処理内でallMatchを実行
- C: mapをして一度Listに保存してから新たにStreamをひらいてallMatchを実行
- D: 処理結果をfilterしてその数とStreamを流している対象のリスト長とを比較する
サンプルコード。簡単なものだとおもう。
public class MyProject {
public static void main(String[] args) {
List<String> baseStrList = Arrays.asList("test", "tetetete", "tetetete", "tetetete", "tetetete");
List<TestData> listA = baseStrList.stream().map(TestData::new).collect(Collectors.toList());
List<TestData> listB = baseStrList.stream().map(TestData::new).collect(Collectors.toList());
List<TestData> listC = baseStrList.stream().map(TestData::new).collect(Collectors.toList());
List<TestData> listD = baseStrList.stream().map(TestData::new).collect(Collectors.toList());
boolean a = listA.stream().allMatch(TestData::checkLength);
boolean b = listB.stream().map(TestData::checkLength).allMatch(e -> e);
List<Boolean> list3Temp = listC.stream().map(TestData::checkLength).collect(Collectors.toList());
boolean c = list3Temp.stream().allMatch(e -> e);
boolean d = listD.size() == listD.stream().filter(TestData::checkLength).count();
System.out.println("a:" + a + ", list:" + listA.toString());
System.out.println("b:" + b + ", list:" + listB.toString());
System.out.println("c:" + c + ", list:" + listC.toString());
System.out.println("d:" + d + ", list:" + listD.toString());
}
public static class TestData {
public String content = "fefe";
@Override
public String toString() {
return content;
}
public TestData(String a) {
this.content = a;
}
public boolean checkLength() {
if (getContent().length() > 5) {
setContent("sorry");
return false;
}
return true;
}
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
}
}
結果
a:false, list:[test, sorry, tetetete, tetetete, tetetete]
b:false, list:[test, sorry, tetetete, tetetete, tetetete]
c:false, list:[test, sorry, sorry, sorry, sorry]
d:false, list:[test, sorry, sorry, sorry, sorry]
結果考察
- A: allMatchの短絡評価によって、2要素目のみsorryが代入されて、そのあとの要素は処理されないまま。
- B: StreamAPIのmap()を挟んだとしてもallMatch()の手中にいるため、残念ながらAと同じ結果になる
- C: 一度新規にListを生成しているため、効率は悪いが全要素に対して正しく処理が行われている
- D: こちらは新規にListを生成していないのでcよりも効率がいいはず。。。
以上からallMatchの正しい使い方を理解する必要があることがわかる
個人的にはallMatchを使用する場合はListに対してなんらかの確認を行いたい場合のみがいと思う。今回のようにデータに破壊をもたらす場合、または何か特別な処理を行ってその結果を集計する場合などは CあるいはDの方式でやるのがよいかと思われる
[2017/10/23 09:40追記]
また、当然空リストに対してのallMatchはtrueなので気をつけよう。Dの方式でも同様の結果になる
##ちなみに
lombok使えば楽になるので皆さん使いましょう。
import lombok.Data;
import lombok.val;
import java.util.Arrays;
import java.util.stream.Collectors;
public class MyProject {
public static void main(String[] args) {
val baseStrList = Arrays.asList("test", "tetetete", "tetetete", "tetetete", "tetetete");
val listA = baseStrList.stream().map(TestData::new).collect(Collectors.toList());
val listB = baseStrList.stream().map(TestData::new).collect(Collectors.toList());
val listC = baseStrList.stream().map(TestData::new).collect(Collectors.toList());
val listD = baseStrList.stream().map(TestData::new).collect(Collectors.toList());
val a = listA.stream().allMatch(TestData::checkLength);
val b = listB.stream().map(TestData::checkLength).allMatch(e -> e);
val list3Temp = listC.stream().map(TestData::checkLength).collect(Collectors.toList());
val c = list3Temp.stream().allMatch(e -> e);
val d = listD.size() == listD.stream().filter(TestData::checkLength).count();
System.out.println("a:" + a + ", list:" + listA.toString());
System.out.println("b:" + b + ", list:" + listB.toString());
System.out.println("c:" + c + ", list:" + listC.toString());
System.out.println("d:" + d + ", list:" + listD.toString());
}
@Data
public static class TestData {
public String content = "fefe";
@Override
public String toString() {
return content;
}
public TestData(String a) {
this.content = a;
}
public boolean checkLength() {
if (getContent().length() > 5) {
setContent("sorry");
return false;
}
return true;
}
}
}