Stream.allMatch()
Java 8에 포함된 Stream 클래스에는 모든 요소들이 주어진 조건에 만족하는지 여부를 검사하는 allMatch()
메서드를 제공한다.
예를 들어, 모든 회원의 나이가 5살 이상인지 검사하는 테스트 코드를 다음과 같이 작성할 수 있다.
@Test
public void allMatchTest1()) {
List<Member> members = new ArrayList<>();
members.add(Member.withAge(10));
members.add(Member.withAge(20));
members.add(Member.withAge(30));
boolean result = members.stream().allMatch(member -> member.getAge() > 5);
assertThat(result).isTrue();
}
모든 회원의 나이가 5살 이상이므로 allMatch()는 true를 리턴하고 테스트는 통과하게 된다.
조건을 바꿔서 모든 회원의 나이가 20살 이상인지 확인해보자.
@Test
public void allMatchTest2() {
List<Member> members = new ArrayList<>();
members.add(Member.withAge(10));
members.add(Member.withAge(20));
members.add(Member.withAge(30));
boolean result = members.stream().allMatch(member -> member.getAge() > 20);
assertThat(result).isFalse();
}
회원 중 10살인 회원이 존재하기 때문에 해당 조건을 모두 만족하지 않으므로 false를 리턴하고 위 테스트 코드도 통과하게 된다.
만약 회원 리스트가 빈 리스트라면 어떨까?
@Test
public void allMatchTestWhenEmpty() {
List<Member> members = new ArrayList<>();
boolean result = members.stream().allMatch(member -> member.getAge() > 20);
assertThat(result).isFalse();
}
회원 리스트가 비어있고, 그러므로 20살 이상의 회원이 한 명도 없기 때문에 조건에 맞지 않아 false를 리턴 할 것이라 생각했다.그러나 allMatch()를 true를 리턴하고 아래처럼 테스트는 실패한다.
org.junit.ComparisonFailure:
Expected :false
Actual :true
당연히 false를 리턴할 것 이라고 생각했는데 true를 리턴하는 것이 쉽게 이해가 되질 않았다. 궁금해서 구글링을 좀 해보았더니 StackOverflow에서 같은 내용의 질문을 찾을 수 있었다.
StackOverflow의 답변을 보고 이는Vacuous Truth라는 논리학 개념에서 비롯되었다는 것을 알게되었다.
Vacuous Truth
(갑자기 수학이라니 당황스럽지만 이왕 알게 된 것이니 정리는 해놔야겠다.)
참과 거짓을 판별할 수 있는 문장을 우리는 명제라고 부른다. 예를 들어, 비가 오면 재택근무를 한다
라는 문장도 하나의 명제이다. 그렇다면 이 명제의 참, 거짓은 어떻게 판별할 수 있을까?
먼저 ‘비가 온다’라는 가정이 참인 경우에는 재택근무를 하면 참, 재택근무를 하지 않으면 거짓이 된다. 그렇다면 가정이 거짓인 경우, 즉 비가 오지 않는 경우를 생각해보자. 비가 오지 않는다고 해서 문장 전체가 거짓인 문장이라고 할 수 있을까?
다른 예를 들어서내 방에 있는 모든 책은 자바책이다
명제가 참인지 판별할 때 만약 내 방에 책이 한 권도 없다면? 책이 한권도 없으므로 모든 책이 자바책인이 아닌지 따지는 것 자체가 무의미하게 느껴진다.
논리학에서는 이처럼 가정이 거짓인 명제를 참으로 간주한다. "p이면 q이다"
라는 명제에서, p가 거짓이면 q가 참이든 거짓이든 상관없이 전체 명제는 참이되며 이런 경우를 Vacuous Truth
라고 부른다. 우리말로 하면 공허한 참, 의미없는 참
이라는 뜻이다. 이러한 수학적 배경에 근거하여 빈 스트림에 allMatch() 메서드를 호출하면 true가 리턴되었던 것이다.
결론
빈 스트림에 allMatch() 메서드를 호출하면 true가 리턴되는 것은 버그가 아니고 의도된 것이 맞다. 만약 스트림이 비어있는 경우도 false로 처리하고 싶다면 아래 코드처럼 리스트가 비었는지를 먼저 검사해서 별도로 처리하는게 좋을 것 같다.
public boolean areAllMembersOver20(List<Member> members) {
if (members.isEmpty()) {
return false;
}
return members.stream().allMatch(member -> member.getAge() > 20);
}