Wyobraźmy sobie system do przechowywania zamówień, w którym zamówienie zawierać będzie informacje o kliencie, listę zamówionych pozycji oraz swój numer.
@Data @AllArgsConstructor public class Order { private Integer orderNumber; private Customer customer; private List<OrderItem> items; }
Aby nie zaciemniać klas konstuktorami, getterami i setterami skorzstam z Project Lombok. Adnotacje @Data
oraz @AllArgsConscturor
zrobią całą robotę za nas.
Klasa Customer
zawiera nazwę oraz adres klienta.
@Data @AllArgsConstructor public class Customer { private String customerName; private Address address; }
@Data @AllArgsConstructor public class Address { private String city; private String country; }
Pozycja na zamówieniu zawiera towar oraz jego ilość.
@Data @AllArgsConstructor public class OrderItem { private Item item; private Integer quantity; public Integer getItemValue() { return item.getPrice() * quantity; } }
@Data @AllArgsConstructor public class Item { private String itemName; private Integer price; }
Stwórzmy kilka zamówień.
Customer domesticCustomer = new Customer("Frutos S.A", new Address("Kraków", "Polska")); Customer anotherDomesticCustomer = new Customer("Verduras Sp. z o.o.", new Address("Warszawa", "Polska")); Customer foreignCustomer = new Customer("Uluru Ltd.", new Address("Sidney", "Australia")); Item apple = new Item("Golden Delicious Apple", 6); Item pear = new Item("Conference Pear", 5); Item potato = new Item("Potato", 2); Item strawberry = new Item("Strawberry", 9); Item onion = new Item("Onion", 1); Order localOrder = new Order(1, domesticCustomer, Arrays.asList( new OrderItem(potato, 100), new OrderItem(onion, 20), new OrderItem(strawberry, 50), new OrderItem(apple, 200))); Order anotherLocalOrder = new Order(2, anotherDomesticCustomer, Arrays.asList( new OrderItem(potato, 200), new OrderItem(onion, 180))); Order exportOrder = new Order(3, foreignCustomer, Arrays.asList( new OrderItem(apple, 1300), new OrderItem(pear, 600) )); List<Order> orderList = new ArrayList<>(); orderList.add(localOrder); orderList.add(anotherLocalOrder); orderList.add(exportOrder);
Wyciągnijmy z listy zamówień wszystkie adresy dostaw.
List<Address> addresses = orderList.stream() .map(order -> order.getCustomer().getAddress()) .collect(Collectors.toList()); System.out.println(addresses);
Wynik:
[Address(city=Kraków, country=Polska), Address(city=Warszawa, country=Polska), Address(city=Sidney, country=Australia)]
Użyliśmy operacji map
, która mapuje obiekt Order
na Address
. Wynik operacji umieszczony został na liście addresses
za pomocą operacjicollect
i kolektora Collectors.toList()
.
A co gdybyśmy chcieli zobaczyć tylko listę zamówień krajowych?
List<Address> addresses = orderList.stream() .map(order -> order.getCustomer().getAddress()) .filter(address -> address.getCountry().equals("Polska")) .collect(Collectors.toList());
Jak widzisz pojawiła się operacja filter
, która za pomocą predykatu pozostawia w strumieniu elementy spełniające podany warunek. Predykat to w interfejs definiujący metodę test
. Poniżej fragment jego definicji z java.util.function
.
public interface Predicate<T> { /** * Evaluates this predicate on the given argument. * * @param t the input argument * @return {@code true} if the input argument matches the predicate, * otherwise {@code false} */ boolean test(T t);
Dobrą praktyką, znacznie poprawiającą czytelność kodu (zwłaszcza w przypadku bardziej skomplikowanych warunków) jest definiowanie predykatów na zewnątrz strumienia.
Predicate<Address> polishAddress = address -> address.getCountry().equals("Polska");
Filtrowanie z wykorzystaniem zdefiniowanego predykatu:
List<Address> addresses = orderList.stream() .map(order -> order.getCustomer().getAddress()) .filter(polishAddress) .collect(Collectors.toList());
Zbierzmy teraz wszystkie pozycje z listy zamówień.
List<OrderItem> items = orderList.stream() .map(Order::getItems) .flatMap(Collection::stream) .collect(Collectors.toList()); System.out.println(items);
Operacja mapowania map
dokonała konwersji zamówienia Order
do listy List<OrderItem>
. Za pomocą flatMap
dołączmy wszystkie elementy OrderItem
kolekcji do jednego strumienia wynikowego, który to następnie zbieramy do listy. Collectors::stream
zamienia List<OrderItem>
w strumień, co inaczej można by zapisać .flatMap(orderItems -> orderItems.stream())
Zsumujmy wartość zamówienia exportOrder
Integer sum = exportOrder.getItems().stream() .map(OrderItem::getItemValue) .reduce(Integer::sum) .orElse(0);
Kilka słów wyjaśnienia co się dzieje. Za pomocą map
konwertujemy obiekt OrderItem
na obiekt Integer
. Robi to funkcja getItemValue
, która oblicza wartość pozycji zamówienia. W wyniku tej operacji otrzymujemy strumień liczb całkowitych, który następnie redukujemy do jednego elementu za pomocą operacji reduce
. Jako reduktor użyta została metoda statyczna sum
z klasy Integer
. Końcowe orElse
zwraca wartość 0 gdyby strumień okazał się pusty np. zamiast exportOder
sumowalibyśmy emptyOrder
zdefiniowane następująco: Order emptyOrder = new Order(4, null, Collections.emptyList());
Poniżej bardziej analityczny zapis, bez wykorzystania getItemValue
i bez sumatora, który zobrazuje szczegółowo co się wydarzyło.
Optional<Integer> optionalSum = exportOrder.getItems().stream() .map(orderItem -> orderItem.getItem().getPrice() * orderItem.getQuantity()) .reduce((itemValue1, itemValue2) -> itemValue1 + itemValue2);
Jak widzisz, reduktor pobiera dwa elementy a zwraca jeden.
Posortujmy zamówienia. Jako kryterium sortowania przyjmiemy wartość zamówionych pozycji.
orderList.stream().sorted((order1, order2) -> { int value1 = order1.getItems().stream() .map(OrderItem::getItemValue) .reduce(Integer::sum) .orElse(0); int value2 = order2.getItems().stream() .map(OrderItem::getItemValue) .reduce(Integer::sum) .orElse(0); return Integer.compare(value1, value2); }) .forEach(System.out::println);
Gdybyśmy chcieli uzyskać tylko pierwszy element (zamówienie o najniższej wartości), można skorzystać z findFirst
. Zmodyfikujmy również nieco kod, poprzez dodanie do klasy Order
implementacji interfejsu Comparable
.
@Data @AllArgsConstructor public class Order implements Comparable<Order> { private Integer orderNumber; private Customer customer; private List<OrderItem> items = new ArrayList<>(); @Override public int compareTo(Order o) { int thisValue = this.getItems().stream() .map(OrderItem::getItemValue) .reduce(Integer::sum) .orElse(0); int otherValue = o.getItems().stream() .map(OrderItem::getItemValue) .reduce(Integer::sum) .orElse(0); return Integer.compare(thisValue, otherValue); } }
Wyświetlmy zamówienie o najmniejszej wartości.
orderList.stream() .sorted() .findFirst() .ifPresent(System.out::println);
Zamówienie o największej wartości możemy uzyskać np. poprzez użycie reduktora, który zostawiał będzie zawsze drugi element (w wyniku jego działania na “końcu” zostanie ostatnie zamówienie, czyli to o najwyższej wartości).
orderList.stream() .sorted() .reduce((order, order2) -> order2) .ifPresent(System.out::println);
Na koniec pogrupujmy zamówienia wg kraju dostawy. Wykorzystamy w tym celu kolektor Collectors.groupingBy
.
Map<String, List<Order>> ordersByCountry = orderList.stream() .collect(Collectors.groupingBy(order -> order.getCustomer().getAddress().getCountry()));