Мотивация для Stream API

от
Java    stream api, java 8

Как я уже писал ранее, новый функциональный стиль в Java 8 — это переломный момент в истории языка, новый мир для разработчиков Java, поэтому настало время приспособиться к нему! В этой статье мы рассмотрим некоторые альтернативы традиционным циклам.

Функциональный стиль позволяет сказать, что мы хотим получить, вместо того чтобы говорить как этого можно добиться. Но причём здесь циклы, спросите вы? Безусловно они обладают гибкостью, но эта гибкость не обходится даром. Выражения return, break, continue изменяют поведение цикла так, что помимо основной задачи разобраться в коде, вам нужно понять и как цикл работает.

Stream API в Java 8 даёт нам качественный функционал для работы с коллекциями, давайте посмотрим как мы можем заменить циклы на более краткий и читаемый код.
  shot-20150319T223426.png

Да будет код!На этот раз мы собираемся работать со статьями. В каждой статье содержится заголовок, имя автора и несколько тегов.

  1. private class Article {
  2.  
  3.     private final String title;
  4.     private final String author;
  5.     private final List <String> tags;
  6.  
  7.     private Article(String title, String author, List <String> tags) {
  8.         this.title = title;
  9.         this.author = author;
  10.         this.tags = tags;
  11.     }
  12.  
  13.     public String getTitle() {
  14.         return title;
  15.     }
  16.  
  17.     public String getAuthor() {
  18.         return author;
  19.     }
  20.  
  21.     public List <String> getTags() {
  22.         return tags;
  23.     }
  24. }

В этом примере мы хотим найти первую статью, которая содержит тег "Java".

Взглянем на стандартный подход.
  1. public Article getFirstJavaArticle() {
  2.  
  3.     for (Article article: articles) {
  4.         if (article.getTags().contains("Java")) {
  5.             return article;
  6.         }
  7.     }
  8.  
  9.     return null;
  10. }

Решим задачу, используя stream API.
  1. public Optional<Article> getFirstJavaArticle() {  
  2.     return articles.stream()
  3.         .filter(article -> article.getTags().contains("Java"))
  4.         .findFirst();
  5. }

Довольно круто, правда? Сначала мы используем filter, чтобы найти все статьи, которые содержат тег Java, далее с помощью findFirst получаем первое включение. Так как потоки... ленивы и filter возвращает поток, вычисления происходят до тех пор пока не будет найдено первое включение.


Давайте теперь получим все элементы, удовлетворяющие условию.

Сначала решение с циклом.
  1. public List<Article> getAllJavaArticles() {
  2.  
  3.     List<Article> result = new ArrayList<>();
  4.  
  5.     for (Article article : articles) {
  6.         if (article.getTags().contains("Java")) {
  7.             result.add(article);
  8.         }
  9.     }
  10.  
  11.     return result;
  12. }

Перепишем на потоки.
  1. public List<Article> getAllJavaArticles() {
  2.     return articles.stream()
  3.         .filter(article -> article.getTags().contains("Java"))
  4.         .collect(Collectors.toList());
  5. }

Здесь мы воспользовались операцией collect, чтобы представить результирующий поток в виде коллекции.

Достаточно просто. Что ж, посмотрим на более впечатляющие примеры!


Сгруппируем все статьи по авторам.

И как всегда, решение с циклом.
  1. public Map<String, List<Article>> groupByAuthor() {
  2.  
  3.     Map<String, List<Article>> result = new HashMap<>();
  4.  
  5.     for (Article article : articles) {
  6.         if (result.containsKey(article.getAuthor())) {
  7.             result.get(article.getAuthor()).add(article);
  8.         } else {
  9.             ArrayList<Article> articles = new ArrayList<>();
  10.             articles.add(article);
  11.             result.put(article.getAuthor(), articles);
  12.         }
  13.     }
  14.  
  15.     return result;
  16. }

Сможем ли мы найти чистое решение, использующее stream API? Вполне.
  1. public Map<String, List<Article>> groupByAuthor() {  
  2.     return articles.stream()
  3.         .collect(Collectors.groupingBy(Article::getAuthor));
  4. }

Чудесно! Достаточно лишь ссылки на getAuthor и операции groupingBy .


А теперь найдём все различные теги в нашей коллекции.

Начнём с цикла.
  1. public Set<String> getDistinctTags() {
  2.  
  3.     Set<String> result = new HashSet<>();
  4.  
  5.     for (Article article : articles) {
  6.         result.addAll(article.getTags());
  7.     }
  8.  
  9.     return result;
  10. }

Посмотрим, как это выглядит с потоками.
  1. public Set<String> getDistinctTags() {  
  2.     return articles.stream()
  3.         .flatMap(article -> article.getTags().stream())
  4.         .collect(Collectors.toSet());
  5. }

Миленько. flatmap позволяет нам совместить списки тегов в один результирующий поток, после чего используем collect, чтобы вернуть множество.


Бесконечные возможности
Это были лишь 4 примера, которые показали как можно заменить циклы на более читаемый код. Чтобы узнать больше, смотри документацию.


Автор: Marius Herring