Stream

  • Post author:
  • Post category:Java

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()));