Изучите потоковое программирование на Java 8 за 5 минут

Java задняя часть Android
Изучите потоковое программирование на Java 8 за 5 минут

1 Обзор

  1. В Java8 в Collection был добавлен метод stream(), который возвращает тип Stream. Мы используем этот поток для потокового программирования;
  2. В отличие от коллекций потоки рассчитываются только по запросу, а коллекции уже созданы и хранятся в кеше;
  3. Как и итераторы, потоки можно пройти только один раз.Если вы хотите пройти снова, вы должны снова получить данные из источника данных;
  4. Внешняя итерация относится к необходимости выполнения пользователями итерации, а внутренняя итерация выполняется в библиотеке без реализации пользователем;
  5. Потоковые операции, которые могут быть связаны, называются промежуточными операциями, а операции, закрывающие потоки, называются терминальными операциями (формально, используя.В операции те из промежуточных операций называются промежуточными операциями, а конечная операция называется конечной операцией).

2. Фильтр

2.1 Фильтрация

Stream<T> filter(Predicate<? super T> predicate);

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

List<Integer> list = Arrays.asList(1, 1, 2, 3, 4, 5, 5, 6, 7, 8, 9);
List<Integer> filter = list.stream().filter(integer -> integer > 3).collect(Collectors.toList());
// [4, 5, 5, 6, 7, 8, 9]

2.2 Дедупликация

Stream<T> distinct();

Выше приведено определение метода дедупликации, который будет выполнять дедупликацию в соответствии с методами equal() и hashCode() элементов в потоке. После дедупликации он продолжит возвращать поток, так что это тоже промежуточная операция.

List<Integer> list = Arrays.asList(1, 1, 2, 3, 4, 5, 5, 6, 7, 8, 9);
List<Integer> filter = list.stream().filter(integer -> integer > 3).distinct().collect(Collectors.toList());
// [4, 5, 6, 7, 8, 9]

2.3 Ограничения

Stream<T> limit(long maxSize);

Как и оператор limit в SQL, в потоке есть аналогичный метод limit(). Он используется для ограничения количества возвращаемых результатов. Он будет извлекать фиксированное количество элементов из головы потока. Это также промежуточная операция, которая по-прежнему будет возвращать поток после использования.

List<Integer> list = Arrays.asList(1, 1, 2, 3, 4, 5, 5, 6, 7, 8, 9);
List<Integer> filter = list.stream().filter(integer -> integer > 3).limit(3).collect(Collectors.toList());
// [4, 5, 5]

2.4 Пропустить

Stream<T> skip(long n);

Определение этого метода чем-то похоже на limit(). Это также промежуточная операция, которая пропускает заданное количество элементов из заголовка потока и возвращает поток после использования.

List<Integer> list = Arrays.asList(1, 1, 2, 3, 4, 5, 5, 6, 7, 8, 9);
List<Integer> filter = list.stream().filter(integer -> integer > 3).skip(3).collect(Collectors.toList());
// [6, 7, 8, 9]

3. Отображение

<R> Stream<R> map(Function<? super T, ? extends R> mapper);

Помните методы интерфейса Function? Он позволяет преобразовать тип ввода в другой тип. Выше приведено его применение в методе map(). После использования этого метода в потоковой операции поток пытается преобразовать все элементы в текущем потоке в другой тип. Когда вы вызываете терминальную операцию collect(), вы, естественно, получаете другой тип коллекции.

List<Integer> list = Arrays.asList(1, 1, 2, 3, 4, 5, 5, 6, 7, 8, 9);
List<String> filter = list.stream().map((integer -> String.valueOf(integer) + "-")).collect(Collectors.toList());
// 结果:[1-, 1-, 2-, 3-, 4-, 5-, 5-, 6-, 7-, 8-, 9-]

4. Найдите

Optional<T> findFirst();
Optional<T> findAny();

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

List<Integer> list = Arrays.asList(1, 1, 2, 3, 4, 5, 5, 6, 7, 8, 9);
Optional<Integer> optionalInteger = list.stream().filter(integer -> integer > 10).findAny();
Optional<Integer> optionalInteger = list.stream().filter(integer -> integer > 10).findFirst();

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

Нумерация метод инструкция
1 isPresent() Определяет, существует ли значение, возвращает true, если оно существует, иначе возвращает false
2 isPresent(Consumer block) Выполняет данный код, если значение существует
3 T get() Возвращает значение, если оно существует, в противном случае генерирует исключение NoSuchElement.
4 T orElse(T other) Возвращает значение, если оно существует, в противном случае возвращает другое

5. Матч

boolean allMatch(Predicate<? super T> predicate);
boolean noneMatch(Predicate<? super T> predicate);
boolean anyMatch(Predicate<? super T> predicate);

Согласно приведенному выше определению, указанные выше три метода также являются терминальными операциями. Они используются, чтобы судить: все ли данные в потоке соответствуют заданным условиям, все ли данные в потоке не соответствуют заданным условиям и есть ли в потоке данные, соответствующие заданным условиям. Вот некоторые примеры:

List<Integer> list = Arrays.asList(1, 1, 2, 3, 4, 5, 5, 6, 7, 8, 9);
boolean allMatch = list.stream().allMatch(integer -> integer < 10);
boolean anyMatch = list.stream().anyMatch(integer -> integer > 3);
boolean noneMatch = list.stream().noneMatch(integer -> integer > 100);

6. Сокращение

Optional<T> reduce(BinaryOperator<T> accumulator);
T reduce(T identity, BinaryOperator<T> accumulator);

В интерфейсе Stream есть три перегруженных версии метода reduce, выше мы даем определения двух наиболее часто используемых. Они в принципе похожи, за исключением того, что в списке параметров второго метода есть начальное значение, а тот, что без начального значения, возвращает тип Оптиноал, так что разница невелика, надо только понять его поведение. Вот пример сокращения:

List<String> list = Arrays.asList("a", "b", "c", "d", "e", "f");
String ret = list.stream().reduce("-", (a, b) -> a + b);

Его вывод-abcdef, очевидно, его эффект таков: если,$это какая-то операция, Список это некая "числовая последовательность", то смысл редукции такой初始值$n[0]$n[1]$n[2]$...$n[n-1].

7. Числовой поток

Также из соображений производительности упаковки Java 8 предоставляет числовые потоки для числовых типов: IntStream, DoubleStream и LongStream. Интерфейс Stream предоставляет три промежуточных метода для преобразования произвольных потоков в числовые потоки:

IntStream mapToInt(ToIntFunction<? super T> mapper);
LongStream mapToLong(ToLongFunction<? super T> mapper);
DoubleStream mapToDouble(ToDoubleFunction<? super T> mapper);

Таким образом, вы можете использовать три вышеуказанных метода для получения потока значений из любого потока. Затем используйте метод числового потока для выполнения других операций. Вышеупомянутые три числовых потока и интерфейсы Stream наследуют подчиненный BaseStream, поэтому содержащиеся в них методы все еще различаются, но в целом они похожи. Поток является более общим, а три приведенных выше числовых потока более специфичны, и последний также предоставляет множество удобных методов. Если вы хотите получить поток объектов из потока значений, вы можете вызвать ихboxed()способ получить упакованный поток.

Чтобы упомянуть немного здесь, для Необязательный, Java8 также предоставляет нам соответствующий числовой тип: необязательныйIntOptionalDoubleOptionalLong.

В трех вышеперечисленных числовых потоках есть несколько статических методов для получения потока заданного числового диапазона:

public static LongStream range(long startInclusive, final long endExclusive)
public static LongStream rangeClosed(long startInclusive, final long endInclusive)

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

8. Создайте поток

Когда мы получаем вышеприведенный поток, мы фактически используем метод Collection по умолчанию.stream(), что несколько громоздко. На самом деле Java8 предоставляет нам несколько методов для создания потоков. Здесь мы перечисляем эти методы:

public static<T> Builder<T> builder() // 1
public static<T> Stream<T> empty() // 2
public static<T> Stream<T> of(T t) // 3
public static<T> Stream<T> of(T... values) // 4
public static<T> Stream<T> iterate(final T seed, final UnaryOperator<T> f) // 5
public static<T> Stream<T> generate(Supplier<T> s) // 6 
public static <T> Stream<T> concat(Stream<? extends T> a, Stream<? extends T> b) // 7

Вышеупомянутые методы являются статическими методами в интерфейсе Stream, и мы можем использовать эти методы для получения потока. Ниже мы даем краткое описание каждого метода:

  1. Как видно из названия, здесь используется паттерн Builder, вы можете вызвать Builder'sadd()метод вставляет элемент для создания потока;
  2. используется для создания пустого потока
  3. Создайте поток только с одним элементом
  4. Создает поток, содержащий указанные элементы с неопределенными аргументами
  5. Ключом к выяснению его принципа является понимание значения UnaryOperator, который является функциональным интерфейсом и наследуется от Function.Разница в том, что его типы входных и возвращаемых параметров одинаковы. Принцип этого метода заключается в том, чтобы начать с определенного начального значения и вычислить в соответствии с правилами следующей функции, каждый раз, когда функция выполняется на основе предыдущего значения. такStream.iterate(2, n -> n * n).limit(3)будет возвращено2 4 16Состоит из потока.
  6. Поставщик здесь также является функциональным интерфейсом, он имеет только один метод get(), без параметров и принимает только возвращаемое значение указанного типа. Итак, этот метод требует, чтобы вы предоставили функцию (или правило) для генерации значений, например Math.random() и так далее.
  7. Это относительно легко понять, то есть новый поток получается путем слияния двух потоков.

9. Коллекционер

Выше мы видели операции сокращения потока, но эти операции все еще относительно наивны. Сборщик Java8 предоставляет нам более мощную функцию редукции.

Говоря о коллекторах, должно быть два класса Collector и Collectors, какая между ними связь? На самом деле Collector — это просто интерфейс; Collectors — это класс, а статический внутренний класс CollectorImpl реализует интерфейс и используется Collectors для предоставления некоторых функций. В Collectors есть много статических методов для получения экземпляров Collector, и с помощью этих экземпляров мы можем выполнять сложные функции. Конечно, мы также можем определить наши собственные сборщики, реализуя интерфейс Collector.

Метод collect() в Stream имеет три перегруженные версии. Используем коллектор через один из них, вот его определение:

<R, A> R collect(Collector<? super T, A, R> collector);

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

Давайте сначала рассмотрим несколько простых примеров, где поток — это поток, состоящий из объектов Student:

Optional<Student> student = stream.collect(Collectors.maxBy(comparator))  // 需要传入一个比较器到maxBy()方法中
long count = stream.collect(Collectors.counting())

Вышеуказанные два метода относительно бесполезны, поскольку вместо них можно использовать методы count() и max(). Давайте посмотрим на некоторые другие примеры сборщиков.Обратите внимание, что в этих примерах я не использовал лямбда-выражения для упрощения функционального интерфейса, потому что я хотел, чтобы вы более четко видели его универсальные типы и определения методов. Это может помочь вам понять, как работают эти методы.

9.1 Расчет средних и итоговых значений

Следующий оператор используется для вычисления среднего значения, и аналогично summingInt() используется для вычисления итога. Их использование аналогично.

Double d = stream.collect(Collectors.averagingInt(new ToIntFunction<Student>() {
    @Override
    public int applyAsInt(Student value) {
        return value.getGrade();
    }
}));

Из вышеизложенного видно, что при вызове метода averagingInt() нам необходимо передать функциональный интерфейс ToIntFunction, который используется для возврата целочисленного значения в соответствии с указанным типом.

9.2 Строки подключения

Присоединение () фабричный метод специально используется для подключения строк. Это требует, чтобы поток был строковым потоком, поэтому перед сращиванием Student Stream он должен быть сопоставлен на строковый поток:

String members = stream.map(new Function<Student, String>() {
    @Override
    public String apply(Student student) {
       return student.getName();
    }
}).collect(Collectors.joining(", ")); // 使用','将字符串拼接起来

9.3 Резюме широкого устава

Optional<Student> optional = stream.collect(Collectors.reducing(new BinaryOperator<Student>() {
    @Override
    public Student apply(Student student, Student student2) {
        return student.getGrade() > student2.getGrade() ? student : student2;
    }
}));

Выше приведена функция, используемая для уменьшения. Мы использовали метод редукционной фабрики и передали ему тип BinaryOperator. Здесь мы указываем, что конечным типом возвращаемого значения является Student. Таким образом, действие приведенного выше кода заключается в том, чтобы получить ученика с наивысшей оценкой.

9.4 Группировка

Группировка в Коллекционерах довольно интересная. Давайте сначала посмотрим на определение метода groupingBy:

Collector<T, ?, Map<K, D>> groupingBy(Function<? super T, ? extends K> classifier)
Collector<T, ?, Map<K, D>> groupingBy(Function<? super T, ? extends K> classifier, Collector<? super T, A, D> downstream)

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

Map<Integer, List<Student>> map = stream.collect(Collectors.groupingBy(new Function<Student, Integer>() {
    @Override
    public Integer apply(Student student) {
       return student.getClazz();
    }
}));

Выше приведен первый пример метода groupingBy(). Обратите внимание, что здесь мы группируем, сопоставляя Student по «полю класса» с целым числом. Ниже приведен пример вторичной группировки. Здесь используется второй метод groupingBy(), описанный выше, и указывается другая операция группировки ниже по течению.

Map<Integer, Map<Integer, List<Student>>> map = stream.collect(Collectors.groupingBy(new Function<Student, Integer>() {
    @Override
    public Integer apply(Student student) {
       return student.getClazz();
    }
}, Collectors.groupingBy(new Function<Student, Integer>() {
    @Override
    public Integer apply(Student student) {
        return student.getGrade() == 100 ? 1 : student.getGrade() > 90 ? 2 : student.getGrade() > 80 ? 3 : 4;
    }
})));

9.5 Разделение

Подобно группировке, существует также операция разделения, а разбиение — всего лишь частный случай группировки. Их использование в основном такое же, и его сигнатура метода аналогична методу groupingBy выше. Давайте посмотрим непосредственно на способ его использования:

Map<Boolean, List<Student>> map = stream.collect(Collectors.partitioningBy(new Predicate<Student>() {
    @Override
    public boolean test(Student student) {
        return student.getGrade() > 90;
    }
}));

Вот как используются перегородки. Он сопоставляет указанный тип с логическим типом через указанный функциональный интерфейс. Таким образом, это похоже на группировку, за исключением того, что существует только два результата группировки: либо истина, либо ложь. Конечно, аналогично группировке, вы также можете указать другой сборщик во втором параметре метода partitioningBy(), чтобы последующие операции можно было выполнять над разделенным потоком.

Суммировать:

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

Соответствующий код:Github