Статьи

Знание новых возможностей Java 8: потоки

В этом посте, втором в серии, мы говорим о потоках, новом способе манипулирования коллекциями.

Итак, без дальнейших задержек, давайте начнем наше путешествие через эту функцию!

Streams

Streams был представлен на Java 8 как способ создания новой формы управления коллекциями. Обычно, когда мы используем коллекцию, мы подготавливаем список элементов, выполняем несколько операций с этой коллекцией, такие как фильтрация, суммы и т. Д., И, наконец, используем конечный результат, который можно оценить как одну операцию. В этом и заключается цель API потоков: позволить нам программировать логику нашей Коллекции как одну операцию, используя парадигму функционального программирования.

Итак, начнем с подготовки примеров.

Сначала мы создаем класс Client, который мы будем использовать в качестве POJO для наших примеров:

public class Client {

private String name;

private Long phone;

private String sex;

private List<Order> orders;

public List<Order> getOrders() {
return orders;
}

public void setOrders(List<Order> orders) {
this.orders = orders;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public Long getPhone() {
return phone;
}

public void setPhone(Long phone) {
this.phone = phone;
}

public String getSex() {
return sex;
}

public void setSex(String sex) {
this.sex = sex;
}

public void markClientSpecial() {

System.out.println("The client " + getName() + " is special! ");

}

}

На этот раз наш класс Client имеет ссылку на другой POJO, класс Order, который мы будем использовать для обогащения наших примеров:

public class Order {

private Long id;

private String description;

private Double total;

public Long getId() {
return id;
}

public void setId(Long id) {
this.id = id;
}

public String getDescription() {
return description;
}

public void setDescription(String description) {
this.description = description;
}

public Double getTotal() {
return total;
}

public void setTotal(Double total) {
this.total = total;
}

}

Наконец, для всех примеров мы будем использовать данные одной коллекции, поэтому мы создадим класс Utility для заполнения наших данных:

public class CollectionUtils {

public static List<Client> getData() {

List<Client> list = new ArrayList<>();

List<Order> orders;

Order order;

Client clientData = new Client();

clientData.setName("Alexandre Eleuterio Santos Lourenco");
clientData.setPhone(33455676l);
clientData.setSex("M");

list.add(clientData);

orders = new ArrayList<>();

order = new Order();

order.setDescription("description 1");
order.setId(1l);
order.setTotal(32.33);
orders.add(order);

order = new Order();

order.setDescription("description 2");
order.setId(2l);
order.setTotal(42.33);
orders.add(order);

order = new Order();

order.setDescription("description 3");
order.setId(3l);
order.setTotal(72.54);
orders.add(order);

clientData.setOrders(orders);

clientData = new Client();

clientData.setName("Lucebiane Santos Lourenco");
clientData.setPhone(456782387l);
clientData.setSex("F");

list.add(clientData);

orders = new ArrayList<>();

order = new Order();

order.setDescription("description 4");
order.setId(4l);
order.setTotal(52.33);
orders.add(order);

order = new Order();

order.setDescription("description 2");
order.setId(5l);
order.setTotal(102.33);
orders.add(order);

order = new Order();

order.setDescription("description 5");
order.setId(6l);
order.setTotal(12.54);
orders.add(order);

clientData.setOrders(orders);

clientData = new Client();

clientData.setName("Ana Carolina Fernandes do Sim");
clientData.setPhone(345622189l);
clientData.setSex("F");

list.add(clientData);

orders = new ArrayList<>();

order = new Order();

order.setDescription("description 6");
order.setId(7l);
order.setTotal(12.43);
orders.add(order);

order = new Order();

order.setDescription("description 7");
order.setId(8l);
order.setTotal(98.11);
orders.add(order);

order = new Order();

order.setDescription("description 8");
order.setId(9l);
order.setTotal(130.22);
orders.add(order);

clientData.setOrders(orders);

return list;

}

}

Итак, начнем с примеров!

Чтобы использовать потоковый API, все, что нам нужно, это использовать метод  stream ()  в API Коллекции, чтобы получить поток, уже подготовленный для нашего использования. Интерфейс Stream использует функцию методов по умолчанию, поэтому нам не нужно реализовывать методы интерфейса. Еще один хороший момент в этом подходе заключается в том, что, следовательно, все коллекции уже поддерживают функцию Streams, поэтому, если у читателя есть эта любимая среда для коллекций (например, общедоступная из Apache), все, что вам нужно сделать, это обновить JVM ваших проектов. и поддержка добавлена!

Первое, что следует заметить о потоках, это то, что они  не меняют коллекцию . Это означает, что если мы сделаем что-то вроде этого:

public class StreamsExample {

public static void main(String[] args) {

List<Client> clients = CollectionUtils.getData();

clients.stream().filter(
c -> c.getName().equals("Alexandre Eleuterio Santos Lourenco"));

clients.forEach(c -> System.out.println(c.getName()));

}

}

Запустив код, мы увидим, что Коллекция будет по-прежнему печатать 3 клиента из тестовых данных нашей Коллекции, а не только тот, который мы отфильтровали в нашем потоке! Это важная концепция, чтобы помнить об этом, поскольку это означает, что нам не нужно заполнять несколько коллекций разными данными, чтобы выполнять разную логику.

Итак, как мы можем напечатать результат нашего предыдущего фильтра? Все, что нам нужно сделать, это связать методы, вот так:

.

.

.

clients.stream()
.filter(c -> c.getName().equals(
"Alexandre Eleuterio Santos Lourenco"))
.forEach(c -> System.out.println(c.getName()));

если мы снова запустим наш код, мы увидим, что теперь код печатает только элементы, которые мы отфильтровали. В этом примере, как уже было сказано, мы не получили отфильтрованный список. Если нам нужно извлечь коллекцию, сформированную из преобразований, которые мы сделали в наших потоках, мы можем использовать метод сбора. Этот метод получает 3 функциональных интерфейса в качестве параметров, но, к счастью, Java 8 уже поставляется с другим интерфейсом, называемым  Collectors , который предоставляет общие реализации для интерфейсов, которые мы должны предоставить для метода collect. Используя эти функции, мы могли бы получить кодировку Collection следующим образом:

.

.

.

List<Client> filteredList = clients
.stream()
.filter(c -> c.getName().equals(
"Alexandre Eleuterio Santos Lourenco"))
.collect(Collectors.toList());

filteredList.forEach(c -> System.out.println(c.getName()));

В наших предыдущих примерах мы извлекали целые объекты Client при нашей фильтрации. Но а если бы мы хотели получить список с именами клиентов, у которых есть заказы с общим количеством> 90, и распечатать на консоли? Мы могли бы сделать это:

.

.

.

System.out.println("USING THE MAP METHOD!");

clients.stream()
.filter(c -> c.getOrders().stream()
.anyMatch(o -> o.getTotal() > 90))
.map(Client::getName)
.forEach(System.out::println);

Приведенный выше код поначалу может показаться немного странным, но если мы представим размер кода, то мы сделаем то же самое с традиционным кодом Java — итерация по нескольким коллекциям, создание другой коллекции только с именами и повторение снова для отпечатков — мы видим, что новые функции действительно помогают сделать более простой и понятный код. Мы также видим использование   метода anyMatch , который получает предикат в качестве параметра и возвращает истину или ложь, если какой-либо элемент в потоке завершается успешно в предикате.

Помимо универсального   метода отображения , есть и другие реализации, специфичные для целых, длинных и двойных чисел. Причина этого состоит в том, чтобы предотвратить так называемый «эффект упаковки», когда примитивные значения будут обернуты и развернуты в операциях, что приведет к снижению производительности, и, поскольку мы уже сообщили тип значения, с которым мы работаем, эти реализации предоставляют некоторые интересные методы, которые возвращают такие вещи, как среднее или максимальное значение нашего отображения. Давайте посмотрим на пример. Представьте, что мы хотим получить максимальную сумму из заказов на каждом клиенте и напечатать имя и сумму на консоли. Мы могли бы сделать так:

.

.

.
clients.stream().forEach(
c -> System.out.println("Name: "
+ c.getName()
+ " Highest Order Total: "
+ c.getOrders().stream().mapToDouble(Order::getTotal)
.max().getAsDouble()));

Читатель может заметить, что возвращаемым методом max является не сам примитив, а Объект. Этот объект является  OptionalDouble , который вместе с другими классами, такими как java.util.Optional, предоставляет реализацию, которая позволяет нам обеспечивать поведение по умолчанию для случаев, когда операция использовалась с Optional — в нашем случае,  max ()  метод — имеет некоторый  нулевой  элемент среди значений. Например, если мы хотим, чтобы в нашей предыдущей операции max возвращало 0 в случае, если какой-либо из элементов был нулевым, мы могли бы изменить код следующим образом:

.

.

.

clients.stream().forEach(
c -> System.out.println("Name: "
+ c.getName()
+ " Highest Order Total: "
+ c.getOrders().stream().mapToDouble(Order::getTotal)
.max().orElse(0)));

Одним интересным поведением потоков является их ленивое поведение. Это означает, что когда мы создаем поток — также называемый конвейером — потоковых операций, операции всегда будут выполняться только в то время, когда они действительно необходимы для получения окончательного результата. Мы можем увидеть это поведение, используя один метод с именем  peek ().  Давайте посмотрим на пример, который ясно показывает это поведение:

.

.

.

clients.stream()

.filter(c -> c.getName().equals(
"Alexandre Eleuterio Santos Lourenco"))
.peek(System.out::println);

System.out.println("*********** SECOND PEEK TEST ******************");

clients.stream()
.filter(c -> c.getName().equals(
"Alexandre Eleuterio Santos Lourenco"))
.peek(System.out::println)
.forEach(c -> System.out.println(c.getName()));

Если мы запустим приведенный выше пример, мы увидим, что в первом потоке метод peek ничего не печатает. Это потому , что работы фильтра был  не  выполнен, так как мы ничего не делали с потоком после фильтрации. Во втором потоке мы  впоследствии использовали  операцию foreach , поэтому метод peek выведет  toString ()  всех объектов внутри отфильтрованного потока.

В наших предыдущих примерах мы видим   метод max , который возвращает значение max из потока чисел. Операция такого типа, которая возвращает единственный результат из потока, называется  операцией сокращения . Мы можем сделать наши собственные сокращения операций, просто обеспечивая начальное значение и саму операцию, с помощью  уменьшения  метода. Например, если мы хотим вычесть значения из потока:

.

.

.

clients.stream().forEach(
c -> System.out.println("Name: "
+ c.getName()
+ " TOTAL SUBTRACTED: "
+ c.getOrders().stream().mapToDouble(Order::getTotal)
.reduce(0, (a, b) -> a - b)));

Это действительно полезная функция, которую нужно иметь в виду, когда стандартных арифметических операций недостаточно.

Параллельные потоки

Наконец, давайте поговорим о последней теме в путешествии наших потоков: параллельные потоки. При использовании параллельных потоков мы выполняем все операции, которые мы видели ранее, в режиме параллельной обработки вместо обычного основного потока. JDK выберет количество потоков, как разбить сегменты обработки и как соединить части в конечный результат. Читатель может спросить: «Что мне нужно пройти, чтобы помочь jdk в этих настройках?» Ответ: ничего! Правильно, все, что нам нужно сделать, чтобы использовать параллельные потоки, это изменить начало наших команд, как в примере ниже:

.

.

.
clients.parallelStream()
.filter(c -> c.getOrders().stream()
.anyMatch(o -> o.getTotal() > 90)).map(Client::getName)
.forEach(System.out::println);

Как мы видим, все, что нам нужно сделать, — это перейти с  stream ()  на  parallelStream (). Важно помнить, когда использовать параллельные потоки. Поскольку существует полезная нагрузка по подготовке пула потоков и управлению сегментацией и объединением результатов, если у нас нет действительно большого объема данных или очень тяжелой операции с данными, мы обычно будем использовать потоки с одним потоком.

Другие свойства

Конечно, в этом посте мы могли бы обсудить больше возможностей, таких как метод sort, который, как следует из названия, производит сортировку элементов в наших потоках. Еще одна действительно мощная функция — это методы Collectors, которые имеют впечатляющие параметры преобразования, такие как группировка, разбиение, объединение и так далее. Тем не менее, с этим постом мы очень хорошо начали с использования этой функции, заложив путь для его принятия.

Вывод 

And so we conclude another part of our series. As we can easily see, streams is a very powerful tool, which can help us a lot on keeping a really short code when processing our collections. That is one of the keys — or maybe the master key — of the Java 8 philosophy. For years, the Java scenario was plagued with «accusations» of not being a simple language, since it is so verbose, specially with the appearance of languages like Python or Ruby, for example. With this new features, maybe the burden of «being complex» for Java will finally begone. I thank the reader for following me on another post and invite you to please return to the last part of our series, when we will talk about the last of our pillars, the new Date API. Until next time.

Source-code (Github)