Chciałem podzielić się z Tobą krótką historią, jak trzy niepozorne metody mogą sprawić bardzo duże problemy. A chodzi o metody: anyMatch
, noneMatch
, allMatch
, które pochodzą z klasy Stream
. I nie chodzi o samo ich użycie, ale kontekst w jakim można je zastosować oraz to jakie konsekwencje będzie to miało dla Twojej aplikacji.
Metody te są do siebie podobne i sprawdzają, czy dane elementy strumienia pasują bądź nie do predykatu, który jest przekazywany jako parametr.
Stream.anyMatch
Metoda anyMatch(Predicate<? super T> predicate)
służy do sprawdzenia, czy którykolwiek z elementów strumienia pasuje do predykatu. Jeśli tak, metoda zwraca true, jeśli nie to false.
IntStream numbers = IntStream.of(1, 2, 3, 4); numbers.anyMatch(i -> i == 2); // true
Stream.noneMatch
Metoda noneMatch(Predicate<? super T> predicate)
służy do sprawdzenia, czy żaden z elementów nie pasuje do predykatu. Jeśli żaden nie pasuje, metoda zwraca true, jeśli jakiś pasuje zwraca false.
IntStream numbers = IntStream.of(1, 2, 3, 4); numbers.noneMatch(i -> i < 0); // true
Stream.allMatch
Metoda allMatch(Predicate<? super T> predicate)
służy do sprawdzenie, czy wszystkie elementy strumienia pasują do predykatu. Jeśli pasują to metoda zwraca true, jeśli nie to false.
IntStream numbers = IntStream.of(1, 2, 3, 4); numbers.allMatch(i -> i > 0); // true
Ale gdzie jest problem?
Na razie jest wszystko jasne, więc gdzie jest problem. Sprawa może się skomplikować w sytuacji, gdy wykorzystamy te metody nie do końca świadomie.
Wszystkie te metody zwracają boolean
, więc można wykorzystać je w jakimś warunku lub w metodzie, która jest używana w jakimś warunku. Np.:
IntStream numbers = IntStream.of(1, 2, 3, 4); if (numbers.allMatch(i -> i > 0)) { System.out.println("Wszystkie liczby są dodatnie"); } // wydrukuje Wszystkie liczby są dodatnie
A co w sytuacji, gdy strumień jest pusty?
IntStream numbers = IntStream.of(); if (numbers.allMatch(i -> i > 0)) { System.out.println("Wszystkie liczby są dodatnie"); } // wydrukuje Wszystkie liczby są dodatnie
Drukuje się to samo. Czy tego się spodziewałeś? Przecież nie ma żadnych liczb w strumieniu. Czy to jest poprawne zachowanie? Okazuje się, że tak.
Przyznam szczerze, że kiedyś błędnie założyłem, że w takiej sytuacji allMatch
powinno zwrócić false. Na szczęście błąd nie miał poważnych konsekwencji, ale przecież mogło być inaczej…
Dlaczego powstał błąd?
Przeanalizujmy najpierw, jak zachowuje się metoda anyMatch
w podobnej sytuacji.
Dla pustego strumienia anyMatch
zwróci false. Czyli nie ma żadnego elementu, który by spełniał predykat. Jest to taka sama sytuacja, kiedy mamy w strumieniu elementy, z których żaden nie spełnia predykatu. I to mogła być jedna z przyczyn powstania tego błędu. Skoro anyMatch
zwraca false, to można pomyśleć, że inne metody *Match
będą zachowywały się podobnie.
Natomiast metoda noneMatch
dla pustego strumienia zwróci już true. Żaden z elementów nie spełnia predykatu. Taką samą sytuację mamy, gdy w strumieniu mamy elementy, które nie spełniają predykatu.
Metoda allMatch
, sprawdza czy wszystkie elementy strumienia pasują do predykatu. I w tym miejscu możesz pomyśleć, że skoro strumień jest pusty, to żaden element nie pasuje do predykatu, więc allMatch
powinno zwracać false (i to mogło być kolejna przyczyna powstania tego błędu). Niestety allMatch
zwraca true i jest to jak najbardziej prawidłowe zachowanie. Wynika to z faktu, że zastosowano tutaj Universal quantification. Gdzie przez konwencję, wszystkie elementy pustej kolekcji spełniają dany warunek i jest to znane jako Vacuous truth (zostało to opisane w JavaDoc dla tej metody).
Nie tylko ja miałem taki problem, na Stackoverflow jest wątek, w którym zostało to wyjaśnione.
Podsumowanie
Jak i czy można uniknąć takich błędów? Oczywiście, mógłbym napisać, że trzeba czytać dokumentację, JavaDoc itd., ale i taka wiedza może szybko ulecieć z naszej głowy.
Najlepsze rozwiązanie, które trwale może nas zabezpieczyć przed takimi błędami, to dobrze napisane testy jednostkowe. Dobre testy to takie, które testują wszystkie możliwe sytuacje. W tym przypadku mimo, że testy jednostkowe były, to zabrakło tego jednego, który by testował zachowanie metody w sytuacji gdy strumień jest pusty.
Jedną z ważniejszych kwestii w testach jednostkowych jest testowanie warunków brzegowych (pustych kolekcji, strumieni, pustych parametrów metod itd.) i warto mieć to zawsze na uwadze.
Żródła:
https://en.wikipedia.org/wiki/Universal_quantification
https://en.wikipedia.org/wiki/Vacuous_truth
https://docs.oracle.com/javase/8/docs/api/java/util/stream/Stream.html
Proponuję mnemoniki:
allMatch zwraca true wtedy i tylko wtedy, gdy w strumieniu nie ma elementu, który by nie spełniał podanego warunku. Intuicyjny przykład:
boolean isStreamVirusFree = stream.allMatch(T::isVirusFree);
noneMatch zwraca true wtedy i tylko wtedy, gdy w strumieniu nie ma elementu, który by spełniał podany warunek. Intuicyjny przykład:
boolean isStreamVirusFree = stream.noneMatch(T::isInfected);