Вступление
Основная тема этой статьи - темы расширенной обработки данных с использованием новой функциональности, добавленной в Java 8 - Stream API и Collector API.
Чтобы получить максимальную отдачу от этой статьи, вы уже должны быть
знакомы с основными API-интерфейсами Java, Object
и String
и
API-интерфейсом Collection.
Stream API
Пакет java.util.stream
состоит из классов, интерфейсов и многих типов,
позволяющих выполнять операции функционального стиля над элементами. В
Java 8 представлена концепция Stream, которая позволяет программисту
описательно обрабатывать данные и полагаться на многоядерную архитектуру
без необходимости писать какой-либо специальный код.
Что такое поток?
Stream
представляет собой последовательность объектов, полученных из
источника, над которыми могут выполняться агрегированные операции.
С чисто технической точки зрения Stream - это типизированный интерфейс - поток T. Это означает, что поток может быть определен для любого типа объекта , потока чисел, потока символов, потока людей или даже потока города.
С точки зрения разработчика, это новая концепция, которая может выглядеть как Коллекция, но на самом деле она сильно отличается от Коллекции.
Есть несколько ключевых определений, которые нам нужно пройти, чтобы понять это понятие Stream и почему он отличается от Collection:
Поток не содержит никаких данных
Наиболее распространенное заблуждение, к которому я хотел бы обратиться в первую очередь, - поток не содержит никаких данных. Это очень важно помнить и понимать.
В потоке нет данных, но есть данные, хранящиеся в коллекции .
Collection
- это структура, в которой хранятся ее данные. Stream
предназначен только для обработки данных и извлечения их из заданного
источника или перемещения в место назначения. Источником может быть
Коллекция, хотя это также может быть массив или ресурс ввода-вывода.
Поток будет подключаться к источнику, потреблять данные и каким-то
образом обрабатывать элементы в нем.
Поток не должен изменять источник
Поток не должен изменять источник обрабатываемых данных. На самом деле это не обеспечивается компилятором самой JVM, поэтому это просто контракт. Если мне нужно создать собственную реализацию потока, мне не следует изменять источник обрабатываемых данных. Хотя вполне нормально изменять данные в потоке.
Почему это так? Потому что, если мы хотим обрабатывать эти данные параллельно, мы собираемся распределить их между всеми ядрами наших процессоров, и мы не хотим иметь никаких проблем с видимостью или синхронизацией, которые могут привести к плохой производительности или ошибкам. Избежание такого рода помех означает, что мы не должны изменять источник данных во время их обработки.
Источник может быть неограниченным
Вероятно, самая сильная точка из этих трех. Это означает, что поток сам по себе может обрабатывать столько данных, сколько мы хотим. Неограниченный не означает, что источник должен быть бесконечным. Фактически, источник может быть конечным, но у нас может не быть доступа к элементам, содержащимся в этом источнике.
Предположим, что источник - простой текстовый файл. Текстовый файл имеет известный размер, даже если он очень большой. Также предположим, что элементы этого источника на самом деле являются строками этого текстового файла.
Теперь мы можем знать точный размер этого текстового файла, но если мы не откроем его и не пройдемся по содержимому вручную, мы никогда не узнаем, сколько в нем строк. Вот что означает неограниченный - мы не всегда можем заранее знать количество элементов, которые поток будет обрабатывать из источника.
Это три определения потока. Итак, из этих трех определений видно, что поток на самом деле не имеет ничего общего с коллекцией. Коллекция хранит свои данные. Коллекция может изменять содержащиеся в ней данные. И, конечно же, коллекция содержит известный и конечный объем данных.
Характеристики потока
- Последовательность элементов - потоки последовательно предоставляют набор элементов определенного типа. Поток получает элемент по запросу и никогда не сохраняет элемент.
- Источник - потоки берут коллекцию, массив или ресурсы ввода-вывода в качестве источника данных.
- Агрегированные операции - потоки поддерживают агрегированные операции, такие как forEach , фильтр , сопоставление , сортировка , сопоставление и другие.
- Переопределение - большинство операций над Stream возвращает
Stream, что означает, что их результаты могут быть связаны. Функция
этих операций состоит в том, чтобы принимать входные данные,
обрабатывать их и возвращать целевой выход. Метод
collect()
- это терминальная операция, которая обычно присутствует в конце операций, чтобы указать конец обработки Stream. - Автоматические итерации - операции Stream выполняют итерации внутри источника элементов, в отличие от коллекций, где требуется явная итерация.
Создание потока
Мы можем сгенерировать поток с помощью нескольких методов:
поток()
Метод stream()
возвращает последовательный поток с Collection в
качестве источника. В качестве источника можно использовать любую
коллекцию объектов:
private List<String> list = new Arrays.asList("Scott", "David", "Josh");
list.stream();
parallelStream ()
Метод parallelStream()
возвращает параллельный поток с Collection в
качестве источника:
private List<String> list = new Arrays.asList("Scott", "David", "Josh");
list.parallelStream().forEach(element -> method(element));
Проблема с параллельными потоками заключается в том, что при выполнении
такой операции среда выполнения Java разделяет поток на несколько
подпотоков. Он выполняет совокупные операции и объединяет результат. В
нашем случае он вызывает method
параллельно с каждым элементом потока.
Хотя это может быть палкой о двух концах, поскольку выполнение тяжелых операций таким образом может заблокировать другие параллельные потоки, поскольку оно блокирует потоки в пуле.
Stream.of ()
Метод static of()
можно использовать для создания Stream из массива
объектов или отдельных объектов:
Stream.of(new Employee("David"), new Employee("Scott"), new Employee("Josh"));
Stream.builder ()
И, наконец, вы можете использовать статический .builder()
для создания
Stream объектов:
Stream.builder<String> streamBuilder = Stream.builder();
streamBuilder.accept("David");
streamBuilder.accept("Scott");
streamBuilder.accept("Josh");
Stream<String> stream = streamBuilder.build();
.build()
, мы упаковываем принятые объекты в обычный Stream.
Фильтрация с помощью потока
public class FilterExample {
public static void main(String[] args) {
List<String> fruits = Arrays.asList("Apple", "Banana", "Cherry", "Orange");
// Traditional approach
for (String fruit : fruits) {
if (!fruit.equals("Orange")) {
System.out.println(fruit + " ");
}
}
// Stream approach
fruits.stream()
.filter(fruit -> !fruit.equals("Orange"))
.forEach(fruit -> System.out.println(fruit));
}
}
Традиционный подход к фильтрации одного фрукта - это классический цикл for-each.
Второй подход использует Stream для фильтрации элементов Stream, соответствующих данному предикату, в новый Stream, возвращаемый методом.
Кроме того, этот подход использует forEach()
, который выполняет
действие для каждого элемента возвращаемого потока. Вы можете заменить
это чем-то, что называется ссылкой на метод. В Java 8 ссылка на метод
- это сокращенный синтаксис лямбда-выражения, который выполняет только один метод.
{.ezlazyload .img-responsive}
Синтаксис ссылки на метод прост, и вы даже можете заменить им предыдущее
лямбда-выражение .filter(fruit -> !fruit.equals("Orange"))
:
Object::method;
Давайте обновим пример и воспользуемся ссылками на методы и посмотрим, как это выглядит:
public class FilterExample {
public static void main(String[] args) {
List<String> fruits = Arrays.asList("Apple", "Banana", "Cherry", "Orange");
fruits.stream()
.filter(FilterExample::isNotOrange)
.forEach(System.out::println);
}
private static boolean isNotOrange(String fruit) {
return !fruit.equals("Orange");
}
}
Потоки проще и удобнее использовать с лямбда-выражениями, и этот пример показывает, насколько простым и чистым выглядит синтаксис по сравнению с традиционным подходом.
Отображение с потоком
Традиционный подход заключался бы в переборе списка с расширенным циклом for:
List<String> models = Arrays.asList("BMW", "Audi", "Peugeot", "Fiat");
System.out.print("Imperative style: " + "\n");
for (String car : models) {
if (!car.equals("Fiat")) {
Car model = new Car(car);
System.out.println(model);
}
}
С другой стороны, более современный подход - использовать Stream для отображения:
List<String> models = Arrays.asList("BMW", "Audi", "Peugeot", "Fiat");
System.out.print("Functional style: " + "\n");
models.stream()
.filter(model -> !model.equals("Fiat"))
// .map(Car::new) // Method reference approach
// .map(model -> new Car(model)) // Lambda approach
.forEach(System.out::println);
Чтобы проиллюстрировать отображение, рассмотрим этот класс:
private String name;
public Car(String model) {
this.name = model;
}
// getters and setters
@Override
public String toString() {
return "name='" + name + "'";
}
Важно отметить, что models
- это список строк, а не список Car
.
Метод .map()
ожидает объект типа T и возвращает объект типа R.
По сути, мы преобразовываем String в тип автомобиля.
Если вы запустите этот код, императивный стиль и функциональный стиль должны вернуть одно и то же.
Сбор с помощью ручья
Иногда вам нужно преобразовать поток в коллекцию или карту . Использование служебного класса Collectors и его функциональных возможностей:
List<String> models = Arrays.asList("BMW", "Audi", "Peugeot", "Fiat");
List<Car> carList = models.stream()
.filter(model -> !model.equals("Fiat"))
.map(Car::new)
.collect(Collectors.toList());
Соответствие с потоком
Классическая задача - распределить объекты по определенным критериям. Мы можем сделать это, сопоставив необходимую информацию с информацией об объекте и проверив, что нам нужно:
List<Car> models = Arrays.asList(new Car("BMW", 2011), new Car("Audi", 2018), new Car("Peugeot", 2015));
boolean all = models.stream().allMatch(model -> model.getYear() > 2010);
System.out.println("Are all of the models newer than 2010: " + all);
boolean any = models.stream().anyMatch(model -> model.getYear() > 2016);
System.out.println("Are there any models newer than 2016: " + any);
boolean none = models.stream().noneMatch(model -> model.getYear() < 2010);
System.out.println("Is there a car older than 2010: " + none);
allMatch()
- возвращаетtrue
если все элементы этого потока соответствуют предоставленному предикату.anyMatch()
- возвращаетtrue
если какой-либо элемент этого потока соответствует указанному предикату.noneMatch()
- возвращаетtrue
если ни один элемент этого потока не соответствует указанному предикату.
В предыдущем примере кода все указанные предикаты удовлетворены, и все
вернут true
.
Заключение
Большинство людей сегодня используют Java 8. Хотя не все используют Streams. Просто потому, что они представляют новый подход к программированию и представляют собой прикосновение к программированию в функциональном стиле вместе с лямбда-выражениями для Java, не обязательно означает, что это лучший подход. Они просто предлагают новый способ ведения дел. Разработчики сами решают, следует ли полагаться на программирование в функциональном или императивном стиле. При достаточном уровне упражнений сочетание обоих принципов может помочь вам улучшить свое программное обеспечение.
Как всегда, мы рекомендуем вам ознакомиться с официальной документацией для получения дополнительной информации.