Статьи

Функциональность потоковых коллекций в JDK 8

В этом посте представлено применение JDK 8 – представленных потоков с коллекциями для более краткого выполнения обычно желаемой функциональности, связанной с коллекциями . Попутно будет продемонстрировано и кратко объяснено несколько ключевых аспектов использования потоков Java . Обратите внимание, что хотя потоки JDK 8 обеспечивают потенциальные преимущества в производительности благодаря поддержке распараллеливания , это не тема этого поста.

Образец коллекции и записи коллекции

Для целей этого поста экземпляры Movie будут храниться в коллекции. Следующий фрагмент кода предназначен для простого класса Movie используемого в этих примерах.

Movie.java

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
package dustin.examples.jdk8.streams;
 
import java.util.Objects;
 
/**
 * Basic characteristics of a motion picture.
 *
 * @author Dustin
 */
public class Movie
{
   /** Title of movie. */
   private String title;
 
   /** Year of movie's release. */
   private int yearReleased;
 
   /** Movie genre. */
   private Genre genre;
 
   /** MPAA Rating. */
   private MpaaRating mpaaRating;
 
   /** imdb.com Rating. */
   private int imdbTopRating;
 
   public Movie(final String newTitle, final int newYearReleased,
                final Genre newGenre, final MpaaRating newMpaaRating,
                final int newImdbTopRating)
   {
      this.title = newTitle;
      this.yearReleased = newYearReleased;
      this.genre = newGenre;
      this.mpaaRating = newMpaaRating;
      this.imdbTopRating = newImdbTopRating;
   }
 
   public String getTitle()
   {
      return this.title;
   }
 
   public int getYearReleased()
   {
      return this.yearReleased;
   }
 
   public Genre getGenre()
   {
      return this.genre;
   }
 
   public MpaaRating getMpaaRating()
   {
      return this.mpaaRating;
   }
 
   public int getImdbTopRating()
   {
      return this.imdbTopRating;
   }
 
   @Override
   public boolean equals(Object other)
   {
      if (!(other instanceof Movie))
      {
         return false;
      }
      final Movie otherMovie = (Movie) other;
      return   Objects.equals(this.title, otherMovie.title)
            && Objects.equals(this.yearReleased, otherMovie.yearReleased)
            && Objects.equals(this.genre, otherMovie.genre)
            && Objects.equals(this.mpaaRating, otherMovie.mpaaRating)
            && Objects.equals(this.imdbTopRating, otherMovie.imdbTopRating);
   }
 
   @Override
   public int hashCode()
   {
      return Objects.hash(this.title, this.yearReleased, this.genre, this.mpaaRating, this.imdbTopRating);
   }
 
   @Override
   public String toString()
   {
      return "Movie: " + this.title + " (" + this.yearReleased + "), " + this.genre + ", " + this.mpaaRating + ", "
            + this.imdbTopRating;
   }
}

Несколько экземпляров Movie помещаются в набор Java. Код, который делает это, показан ниже, потому что он также показывает значения, установленные в этих случаях. Этот код объявляет «movies» как статическое поле класса, а затем использует статический блок инициализации, чтобы заполнить это поле пятью экземплярами Movie .

Заполнение фильмов с помощью экземпляров класса Movie

01
02
03
04
05
06
07
08
09
10
11
12
private static final Set<Movie> movies;
 
static
{
   final Set<Movie> tempMovies = new HashSet<>();
   tempMovies.add(new Movie("Raiders of the Lost Ark", 1981, Genre.ACTION, MpaaRating.PG, 31));
   tempMovies.add(new Movie("Star Wars: Episode V - The Empire Strikes Back", 1980, Genre.SCIENCE_FICTION, MpaaRating.PG, 12));
   tempMovies.add(new Movie("Inception", 2010, Genre.SCIENCE_FICTION, MpaaRating.PG13, 13));
   tempMovies.add(new Movie("Back to the Future", 1985, Genre.SCIENCE_FICTION, MpaaRating.PG, 49));
   tempMovies.add(new Movie("The Shawshank Redemption", 1994, Genre.DRAMA, MpaaRating.R, 1));
   movies = Collections.unmodifiableSet(tempMovies);
}

Первый взгляд на потоки JDK 8 с фильтрацией

Одним из типов функциональности, обычно выполняемой в коллекциях, является фильтрация. В следующем листинге кода показано, как отфильтровать «фильмы», Set для всех фильмов с рейтингом PG. Я выделю некоторые наблюдения, которые можно сделать из этого кода после листинга.

Фильтрация фильмов с рейтингом PG

01
02
03
04
05
06
07
08
09
10
11
12
/**
 * Demonstrate using .filter() on Movies stream to filter by PG ratings
 * and collect() as a Set.
 */
private void demonstrateFilteringByRating()
{
   printHeader("Filter PG Movies");
   final Set<Movie> pgMovies =
      movies.stream().filter(movie > movie.getMpaaRating() == MpaaRating.PG)
            .collect(Collectors.toSet());
   out.println(pgMovies);
}

Одна вещь, которую этот первый пример включает в себя и во всех примерах в этом посте, – это вызов метода stream () в коллекции. Этот метод возвращает объект, реализующий интерфейс java.util.Stream . Каждый из этих возвращаемых потоков использует коллекцию, для которой вызывается метод stream() качестве источника данных. Все операции на этом этапе выполняются в Stream а не в коллекции, которая является источником данных для Stream .

В приведенном выше листинге кода метод filter ( Predicate ) вызывается в Stream основе Set «movies». В этом случае Predicate задается лямбда-выражением movie -> movie.getMpaaRating() == MpaaRating.PG . Это довольно читаемое представление говорит нам, что предикатом является каждый фильм в базовых данных, который имеет рейтинг MPAA PG.

Метод Stream.filter (Predicate) является промежуточной операцией , что означает, что он возвращает экземпляр Stream который может быть использован другими операциями. В этом случае есть другая операция collect (Collector) , которая вызывается для Stream возвращаемого Stream.filter(Predicate) . Класс Collectors содержит множество статических методов, каждый из которых предоставляет реализацию Collector, которая может быть предоставлена ​​этому методу collect(Collector) . В этом случае Collectors.toSet () используется для получения Collector который будет указывать, что результаты потока должны быть расположены в Set . Метод Stream.collect(Collector) является терминальной операцией. Это означает, что это конец строки, и он НЕ возвращает экземпляр Stream поэтому после выполнения этого сбора больше операций Stream может быть выполнено.

Когда приведенный выше код выполняется, он генерирует вывод, подобный следующему:

1
2
3
4
===========================================================
= Filter PG Movies
===========================================================
[Movie: Raiders of the Lost Ark (1981), ACTION, PG, 31, Movie: Back to the Future (1985), SCIENCE_FICTION, PG, 49, Movie: Star Wars: Episode V - The Empire Strikes Back (1980), SCIENCE_FICTION, PG, 12]

Фильтрация для одного (первого) результата

01
02
03
04
05
06
07
08
09
10
11
/** 
 * Demonstrate using .filter() on Movies stream to filter by #1 imdb.com
 * rating and using .findFirst() to get first (presumably only) match.
 */
private void demonstrateSingleResultImdbRating()
{
   printHeader("Display One and Only #1 IMDB Movie");
   final Optional<Movie> topMovie =
      movies.stream().filter(movie -> movie.getImdbTopRating() == 1).findFirst();
   out.println(topMovie.isPresent() ? topMovie.get() : "none");
}

Этот пример имеет много общего с предыдущим примером. Как и в предыдущем листинге кода, этот листинг показывает использование Stream.filter(Predicate) , но на этот раз предикатом является лямбда-выражение movie -> movie.getImdbTopRating() == 1) . Другими словами, Stream полученный из этого фильтра, должен содержать только экземпляры Movie с методом getImdbTopRating() возвращающим число 1. Затем завершается операция Stream.findFirst () для Stream возвращаемого Stream.filter(Predicate) , Это возвращает первую запись, обнаруженную в потоке, и, поскольку в наших базовых экземплярах Set of Movie только один экземпляр с рейтингом IMDb Top 250 , равным 1, это будет первая и единственная запись, доступная в потоке в результате фильтра.

Когда этот листинг кода выполняется, его вывод выглядит так, как показано ниже:

1
2
3
4
===========================================================
= Display One and Only #1 IMDB Movie
===========================================================
Movie: The Shawshank Redemption (1994), DRAMA, R, 1

Следующий листинг кода иллюстрирует использование Stream.map (Function) .

01
02
03
04
05
06
07
08
09
10
/**
 * Demonstrate using .map to get only specified attribute from each
 * element of collection.
 */
private void demonstrateMapOnGetTitleFunction()
{
   printHeader("Just the Movie Titles, Please");
   final List<String> titles = movies.stream().map(Movie::getTitle).collect(Collectors.toList());
   out.println(titles.size() + " titles (in " + titles.getClass() +"): " + titles);
}

Метод Stream.map(Function) воздействует на Stream которого он вызывается (в нашем случае это Stream на основе базового Set объектов Movie ), и применяет предоставленную функцию к этому Steam чтобы вернуть новый Stream , полученный в результате применение этой Function к исходному Stream . В этом случае, Function представлена Movie::getTitle , которая является примером ссылки на метод, введенный в JDK 8. Я мог бы использовать лямбда-выражение movie -> movie.getTitle() вместо ссылки на метод Movie::getTitle для тех же результатов. Документация « Ссылки на метод» объясняет, что это именно та ситуация, на которую предназначена ссылка на метод:


Вы используете лямбда-выражения для создания анонимных методов. Иногда, однако, лямбда-выражение делает только вызов существующего метода. В этих случаях часто яснее ссылаться на существующий метод по имени. Ссылки на методы позволяют вам сделать это; они являются компактными, легко читаемыми лямбда-выражениями для методов, которые уже имеют имя.

Как вы можете догадаться из его использования в приведенном выше коде, Stream.map(Function) является промежуточной операцией. Этот листинг кода применяет завершающую операцию Stream.collect(Collector) же, как это делали предыдущие два примера, но в этом случае это Collectors.toList (), который передается ему, и поэтому результирующая структура данных представляет собой List, а не Set ,

Когда приведенный выше листинг кода выполняется, его вывод выглядит так:

1
2
3
4
===========================================================
= Just the Movie Titles, Please
===========================================================
5 titles (in class java.util.ArrayList): [Inception, The Shawshank Redemption, Raiders of the Lost Ark, Back to the Future, Star Wars: Episode V - The Empire Strikes Back]

Операции сокращения (до одного логического) anyMatch и allMatch

В следующем примере не используются Stream.filter(Predicate) , Stream.map(Function) или даже завершающая операция Stream.collect(Collector) , которые использовались в большинстве предыдущих примеров. В этом примере операции сокращения и завершения Stream.allMatch (Predicate) и Stream.anyMatch (Predicate) применяются непосредственно к Stream на основе нашего Set объектов Movie .

01
02
03
04
05
06
07
08
09
10
11
/**
 * Demonstrate .anyMatch and .allMatch on stream.
 */
private void demonstrateAnyMatchAndAllMatchReductions()
{
   printHeader("anyMatch and allMatch");
   out.println("All movies in IMDB Top 250? " + movies.stream().allMatch(movie -> movie.getImdbTopRating() < 250));
   out.println("All movies rated PG? " + movies.stream().allMatch(movie -> movie.getMpaaRating() == MpaaRating.PG));
   out.println("Any movies rated PG? " + movies.stream().anyMatch(movie -> movie.getMpaaRating() == MpaaRating.PG));
   out.println("Any movies not rated? " + movies.stream().anyMatch(movie -> movie.getMpaaRating() == MpaaRating.NA));
}

Листинг кода демонстрирует, что Stream.anyMatch(Predicate) и Stream.allMatch(Predicate) каждый возвращает логическое значение, указывающее, поскольку их имена, соответственно, подразумевают, имеет ли Stream хотя бы одну запись, соответствующую предикату, или все записи, соответствующие предикату. В этом случае все фильмы взяты из Топ 250 imdb.com, так что «allMatch» вернет true . Однако не все фильмы имеют рейтинг PG, поэтому «allMatch» возвращает false . Поскольку как минимум одному фильму присвоен рейтинг PG, «anyMatch» для предиката рейтинга PG возвращает значение « true , а «anyMatch» для предиката оценки «Н / Д» возвращает значение « false поскольку ни один фильм в базовом Set имел рейтинга MpaaRating.NA . Результат выполнения этого кода показан ниже.

1
2
3
4
5
6
7
===========================================================
= anyMatch and allMatch
===========================================================
All movies in IMDB Top 250? true
All movies rated PG? false
Any movies rated PG? true
Any movies not rated? false

Легкая идентификация минимума и максимума

Последний пример применения возможностей Stream для манипулирования коллекциями в этом посте демонстрирует использование Stream.reduce (BinaryOperator) с двумя различными экземплярами BinaryOperator : Integer :: min и Integer :: max .

01
02
03
04
05
06
07
08
09
10
private void demonstrateMinMaxReductions()
{
   printHeader("Oldest and Youngest via reduce");
   // Specifying both Predicate for .map and BinaryOperator for .reduce with lambda expressions
   final Optional<Integer> oldestMovie = movies.stream().map(movie -> movie.getYearReleased()).reduce((a,b) -> Integer.min(a,b));
   out.println("Oldest movie was released in " + (oldestMovie.isPresent() ? oldestMovie.get() : "Unknown"));
   // Specifying both Predicate for .map and BinaryOperator for .reduce with method references
   final Optional<Integer> youngestMovie = movies.stream().map(Movie::getYearReleased).reduce(Integer::max);
   out.println("Youngest movie was released in " + (youngestMovie.isPresent() ? youngestMovie.get() : "Unknown"));
}

Этот Integer.min(int,int) пример иллюстрирует использование Integer.min(int,int) для поиска самого старого фильма в базовом Set и использование Integer.max(int,int) для поиска самого нового фильма в Set . Для этого сначала Stream.map использовать Stream.map чтобы получить новый Stream Stream.map предоставленный годом выпуска каждого Movie в исходном Stream . Этот Stream Integer значений затем выполняет Stream.reduce(BinaryOperation) со статическими методами Integer используемыми в качестве BinaryOperation .

Для этого листинга кода я намеренно использовал лямбда-выражения для Predicate и BinaryOperation при вычислении самого старого фильма ( Integer.min(int,int) ) и использовал ссылки на методы вместо лямбда-выражений для Predicate и BinaryOperation используемых при вычислении самого нового фильма ( Integer.max(int,int) ). Это доказывает, что лямбда-выражения или ссылки на методы могут использоваться во многих случаях.

Результат выполнения вышеуказанного кода показан ниже:

1
2
3
4
5
===========================================================
= Oldest and Youngest via reduce
===========================================================
Oldest movie was released in 1980
Youngest movie was released in 2010

Вывод

JDK 8 Streams представляет мощный механизм для работы с коллекциями. Этот пост был посвящен удобочитаемости и краткости, которые дает работа с Streams по сравнению с работой с коллекциями напрямую, но Streams также предлагает потенциальные преимущества в производительности. В этом посте предпринята попытка использовать общие коллекции, обрабатывающие идиомы, в качестве примеров краткости, которую Streams привносит в Java. Попутно обсуждались также некоторые ключевые концепции, связанные с использованием потоков JDK. Наиболее сложными аспектами использования потоков JDK 8 являются привыкание к новым концепциям и новому синтаксису (например, к лямбда-выражениям и ссылкам на методы), но они быстро изучаются после игры с несколькими примерами. Разработчик Java, обладающий даже небольшим опытом работы с понятиями и синтаксисом, может исследовать методы Stream API для гораздо более длинного списка операций, которые могут выполняться над потоками (и, следовательно, с коллекциями, лежащими в основе этих потоков), чем показано в этом посте.

Дополнительные ресурсы

Цель этого поста состояла в том, чтобы дать легкий первый взгляд на потоки JDK 8 на основе простых, но довольно распространенных примеров манипуляции коллекциями. Чтобы глубже погрузиться в потоки JDK 8 и больше идей о том, как потоки JDK 8 облегчают манипулирование коллекциями, см. Следующие статьи: