Пожалуйста, избегайте общих ошибок при программировании потоковой передачи Java Stream.

Java
Пожалуйста, избегайте общих ошибок при программировании потоковой передачи Java Stream.

эта статьяgithub/JavaMapОн был включен, есть карта расширенных технических знаний Java-программистов и моя серия статей, добро пожаловать в Star.

Выпущенная Oracle в 2014 году, Java8 является самой революционной версией со времен Java5.

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

1. Что такое поток?

Stream — это интерфейс, добавленный в Java 8, который позволяет декларативную обработку коллекций данных. Stream не является типом коллекции и не хранит данные, его можно рассматривать как расширенный итератор (Iterator), который проходит коллекцию данных.

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

Потоки неотделимы от лямбда-выражений, по умолчанию вы освоили основы лямбда-выражений.

2. Особенности потока

  • Его можно пройти (использовать) только один раз. Экземпляр Stream можно пройти только один раз, и один обход заканчивается после терминальной операции, а другой обход должен регенерировать экземпляр, что похоже на итератор Iterator.
  • Защитите источники данных. Изменение любого элемента в потоке не приведет к изменению источника данных, например, при фильтрации и удалении элемента в потоке, и повторный обход источника данных все еще может получить элемент.
  • ленивый. Операции фильтра и карты объединены в цепочку, образуя серию промежуточных операций, которые никогда не будут выполняться без терминальной операции (например, сбора).

3. Как создать экземпляр Stream

(1) Создайте экземпляр Stream с указанным значением

// of为Stream的静态方法
Stream<String> strStream = Stream.of("hello", "java8", "stream");
// 或者使用基本类型流
IntStream intStream = IntStream.of(1, 2, 3);

(2) Используйте коллекцию для создания экземпляра потока (общий способ)

// 使用guava库,初始化一个不可变的list对象
ImmutableList<Integer> integers = ImmutableList.of(1, 2, 3);
// List接口继承Collection接口,java8在Collection接口中添加了stream方法
Stream<Integer> stream = integers.stream();

(3) Создание экземпляра потока с использованием массива

// 初始化一个数组
Integer[] array = {1, 2, 3};
// 使用Arrays的静态方法stream
Stream<Integer> stream = Arrays.stream(array);

(4) Используйте генератор для создания экземпляра Stream

// 随机生成100个整数
Random random = new Random();
// 加上limit否则就是无限流了
Stream<Integer> stream = Stream.generate(random::nextInt).limit(100);

(5) Используйте итератор для создания экземпляра Stream

// 生成100个奇数,加上limit否则就是无限流了
Stream<Integer> stream = Stream.iterate(1, n -> n + 2).limit(100);
stream.forEach(System.out::println);

(6) Используйте интерфейс ввода-вывода для создания экземпляра Stream.

// 获取指定路径下文件信息,list方法返回Stream类型
Stream<Path> pathStream = Files.list(Paths.get("/"));

4. Потоковые общие операции

В интерфейсе Stream определено множество операций, которые можно условно разделить на две категории: одна — промежуточная операция, а другая — терминальная операция;


(1) Промежуточная операция

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

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

Общие промежуточные операции описаны ниже:

операция карты

map заключается в сопоставлении каждого элемента входного потока с другим элементом для формирования выходного потока.

// 初始化一个不可变字符串
List<String> words = ImmutableList.of("hello", "java8", "stream");
// 计算列表中每个单词的长度
List<Integer> list = words.stream()
        .map(String::length)
        .collect(Collectors.toList());
// output: 5 5 6
list.forEach(System.out::println);

операция flatMap

List<String[]> list1 = words.stream()
        .map(word -> word.split("-"))
        .collect(Collectors.toList());
        
// output: [Ljava.lang.String;@59f95c5d, 
//             [Ljava.lang.String;@5ccd43c2
list1.forEach(System.out::println);

Нари? Вы ожидали список, но вернули List, потому что метод разделения вернул String[]

В это время можно подумать о преобразовании массива в поток, поэтому есть второй вариант

Stream<Stream<String>> arrStream = words.stream()
        .map(word -> word.split("-"))
        .map(Arrays::stream);
        
// output: java.util.stream.ReferencePipeline$Head@2c13da15, 
// java.util.stream.ReferencePipeline$Head@77556fd
arrStream.forEach(System.out::println);

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

Stream<String> strStream = words.stream()
        .map(word -> word.split("-"))
        .flatMap(Arrays::stream)
        .distinct();
// output: hello java8 stream
strStream.forEach(System.out::println);

операция фильтра

filter получает объект Predicate, фильтрует в соответствии с условиями, и элементы, удовлетворяющие условиям, генерируют другой поток.

// 过滤出单词长度大于5的单词,并打印出来
List<String> words = ImmutableList.of("hello", "java8", "hello", "stream");
words.stream()
        .filter(word -> word.length() > 5)
        .collect(Collectors.toList())
        .forEach(System.out::println);
// output: stream

(2) Работа терминала

Терминальная операция преобразует поток в определенное возвращаемое значение, такое как список, целое число и т. д. Общие терминальные операции: foreach, min, max, count и т. д.

Foreach очень распространен, давайте возьмем пример макс.

// 找出最大的值
List<Integer> integers = Arrays.asList(6, 20, 19);
integers.stream()
        .max(Integer::compareTo)
        .ifPresent(System.out::println);
// output: 20

5. Настоящий бой: используйте Stream для рефакторинга старого кода

Предположим, есть потребность: отфильтровать учащихся, чей возраст больше 20 и чей балл больше 95.

Используйте цикл for для записи:

private List<Student> getStudents() {
    Student s1 = new Student("xiaoli", 18, 95);
    Student s2 = new Student("xiaoming", 21, 100);
    Student s3 = new Student("xiaohua", 19, 98);
    List<Student> studentList = Lists.newArrayList();
    studentList.add(s1);
    studentList.add(s2);
    studentList.add(s3);
    return studentList;
}
public void refactorBefore() {
    List<Student> studentList = getStudents();
    // 使用临时list
    List<Student> resultList = Lists.newArrayList();
    for (Student s : studentList) {
        if (s.getAge() > 20 && s.getScore() > 95) {
            resultList.add(s);
        }
    }
    // output: Student{name=xiaoming, age=21, score=100}
    resultList.forEach(System.out::println);
}

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

После рефакторинга с лямбдой и потоком:

public void refactorAfter() {
    List<Student> studentLists = getStudents();
    // output: Student{name=xiaoming, age=21, score=100}
   studentLists.stream().filter(this::filterStudents).forEach(System.out::println);
}
private boolean filterStudents(Student student) {
    // 过滤出年龄大于20岁并且分数大于95的学生
    return student.getAge() > 20 && student.getScore() > 95;
}

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

6. Использование распространенных заблуждений о потоке

(1) Непонимание 1: повторное потребление объектов потока

После того, как объект потока будет использован, он не может быть использован снова.

List<String> strings = Arrays.asList("hello", "java8", "stream");
Stream<String> stream = strings.stream();
stream.forEach(System.out::println); // ok
stream.forEach(System.out::println); // IllegalStateException

После выполнения вышеуказанного кода сообщается об ошибке:

java.lang.IllegalStateException: stream has already been operated upon or closed

(2) Непонимание 2: изменить источник данных

Попытка добавить новый строковый объект во время потоковой операции приводит к ошибке:

List<String> strings = Arrays.asList("hello", "java8", "stream");
// expect: HELLO JAVA8 STREAM WORLD, but throw UnsupportedOperationException
strings.stream()
        .map(s -> {
            strings.add("world");
            return s.toUpperCase();
        }).forEach(System.out::println);

ПРИМЕЧАНИЕ. Обязательно не изменять источник данных во время манипуляции потока.

Суммировать

Потоковое программирование Java8 может сделать код в определенной степени красивым, но также избежать общих ошибок, таких как: не использовать объекты повторно, не изменять источник данных.

-- END --

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

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

Публичный номер поиска в Wechat [Архитектор, который любит смеяться], у меня есть технологии и истории, которые ждут вас.

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