Рубрики
Без рубрики

Введение в потоки Java 8

Автор оригинала: Vuk Skobalj.

Вступление

Основная тема этой статьи – расширенные темы обработки данных с использованием новой функциональности, добавленной в Java 8, – API потока и API сборщика.

Чтобы извлечь максимальную пользу из этой статьи, вы уже должны быть знакомы с основными API Java, классами Object и String , а также API коллекции.

Потоковый API

Пакет java.util.stream состоит из классов, интерфейсов и множества типов, позволяющих выполнять операции над элементами в функциональном стиле. Java 8 вводит концепцию потока , которая позволяет программисту описательно обрабатывать данные и полагаться на многоядерную архитектуру без необходимости написания какого-либо специального кода.

Что такое Поток?

Поток представляет последовательность объектов, полученных из источника, над которыми могут выполняться агрегатные операции.

С чисто технической точки зрения Поток – это типизированный интерфейс-поток T . Это означает , что поток может быть определен для любого вида объекта , потока чисел, потока символов, потока людей или даже потока города.

С точки зрения разработчика, это новая концепция, которая может просто выглядеть как Коллекция, но на самом деле она сильно отличается от Коллекции.

Есть несколько ключевых определений, через которые нам нужно пройти, чтобы понять это понятие потока и почему оно отличается от коллекции:

Поток не содержит никаких данных

Самое распространенное заблуждение, к которому я хотел бы обратиться в первую очередь, – поток не содержит никаких данных. Это очень важно иметь в виду и понимать.

В Потоке нет данных , однако есть данные, хранящиеся в Коллекции .

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

Поток не должен изменять источник

Поток не должен изменять источник данных, которые он обрабатывает. На самом деле это не выполняется компилятором самой JVM, поэтому это просто контракт. Если я должен создать свою собственную реализацию потока, я не должен изменять источник данных, которые я обрабатываю. Хотя совершенно нормально изменять данные в потоке.

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

Источник может быть неограниченным

Вероятно, самая сильная точка из этих трех. Это означает, что поток сам по себе может обрабатывать столько данных, сколько мы захотим. Неограниченность не означает, что источник должен быть бесконечным. На самом деле, источник может быть конечным, но у нас может не быть доступа к элементам, содержащимся в этом источнике.

Предположим, что источником является простой текстовый файл. Текстовый файл имеет известный размер, даже если он очень большой. Также предположим, что элементы этого источника на самом деле являются строками этого текстового файла.

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

Таковы три определения потока. Таким образом, из этих трех определений мы видим, что поток на самом деле не имеет ничего общего с коллекцией. Коллекция содержит свои данные. Коллекция может изменять содержащиеся в ней данные. И, конечно же, коллекция содержит известный и конечный объем данных.

Характеристики потока

  • Последовательность элементов – Потоки предоставляют набор элементов определенного типа в последовательном порядке. Поток получает элемент по требованию и никогда не сохраняет элемент.
  • Источник – Потоки используют коллекцию, массив или ресурсы ввода-вывода в качестве источника своих данных.
  • Агрегированные операции – Потоки поддерживают агрегированные операции, такие как forEach , фильтр , карта , сортировка , сопоставление и другие.
  • Переопределение – Большинство операций над потоком возвращает поток, что означает, что их результаты могут быть связаны. Функция этих операций состоит в том, чтобы принимать входные данные, обрабатывать их и возвращать целевой вывод. Метод collect () – это терминальная операция, которая обычно присутствует в конце операций, чтобы указать конец обработки потока.
  • Автоматические итерации – Потоковые операции выполняют итерации внутри источника элементов, в отличие от коллекций, где требуется явная итерация.

Создание потока

Мы можем сгенерировать поток с помощью нескольких методов:

поток()

Метод stream() возвращает последовательный поток с коллекцией в качестве источника. Вы можете использовать любую коллекцию объектов в качестве источника:

private List list = new Arrays.asList("Scott", "David", "Josh");
list.stream();
Параллельный поток()

Метод parallelStream() возвращает параллельный поток с коллекцией в качестве источника:

private List list = new Arrays.asList("Scott", "David", "Josh");
list.parallelStream().forEach(element -> method(element));

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

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

Поток.из()

Метод static of() может использоваться для создания потока из массива объектов или отдельных объектов:

Stream.of(new Employee("David"), new Employee("Scott"), new Employee("Josh"));
Stream.builder()

И, наконец, вы можете использовать метод static .builder() для создания потока объектов:

Git Essentials

Ознакомьтесь с этим практическим руководством по изучению Git, содержащим лучшие практики и принятые в отрасли стандарты. Прекратите гуглить команды Git и на самом деле изучите это!

Stream.builder streamBuilder = Stream.builder();

streamBuilder.accept("David");
streamBuilder.accept("Scott");
streamBuilder.accept("Josh");

Stream stream = streamBuilder.build();

Вызывая метод .build () , мы упаковываем принятые объекты в обычный поток.

Фильтрация потоком

public class FilterExample {
    public static void main(String[] args) {
    List 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));
    }
}

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

Второй подход использует поток для фильтрации вывода элементов потока, соответствующих данному предикату, в новый поток, возвращаемый методом.

Кроме того, в этом подходе используется метод forEach () , который выполняет действие для каждого элемента возвращаемого потока. Вы можете заменить это чем-то, что называется ссылка на метод . В Java 8 ссылка на метод-это сокращенный синтаксис для лямбда-выражения, которое выполняет только один метод.

Синтаксис ссылки на метод прост, и вы даже можете заменить предыдущее лямбда-выражение .filter(fruit -> !fruit.equals("Оранжевый")) на него:

Object::method;

Давайте обновим пример, используем ссылки на методы и посмотрим, как это выглядит:

public class FilterExample {
    public static void main(String[] args) {
    List 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");
    }
}

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

Сопоставление с потоком

Традиционным подходом было бы повторение списка с помощью расширенного цикла для :

List 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);
    }
}

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

List 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 + "'";
}

Важно отметить, что список модели представляет собой список строк, а не список Автомобилей . Метод .map() ожидает объект типа T и возвращает объект типа R .

По сути, мы превращаем Строку в тип автомобиля.

Если вы запустите этот код, императивный стиль и функциональный стиль должны возвращать одно и то же.

Собирание с потоком

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

List models = Arrays.asList("BMW", "Audi", "Peugeot", "Fiat");

List carList = models.stream()
        .filter(model -> !model.equals("Fiat"))
        .map(Car::new)
        .collect(Collectors.toList());

Сопоставление с потоком

Классическая задача состоит в том, чтобы классифицировать объекты в соответствии с определенными критериями. Мы можем сделать это, сопоставив необходимую информацию с информацией об объекте и проверив, действительно ли это то, что нам нужно:

List 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 , если все элементы этого потока соответствуют предоставленному предикату.
  • любое совпадение() – Возвращает true , если какой-либо элемент этого потока соответствует предоставленному предикату.
  • none Match() – Возвращает true , если ни один элемент этого потока не соответствует предоставленному предикату.

В предыдущем примере кода все заданные предикаты выполнены, и все они вернут true .

Вывод

Большинство людей сегодня используют Java 8. Хотя не все используют потоки. Просто потому, что они представляют собой более новый подход к программированию и представляют собой соприкосновение с программированием в функциональном стиле наряду с лямбда-выражениями для Java, не обязательно означает, что это лучший подход. Они просто предлагают новый способ ведения дел. Сами разработчики должны решить, следует ли полагаться на функциональное или императивное программирование. При достаточном уровне упражнений сочетание обоих принципов может помочь вам улучшить ваше программное обеспечение.

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