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

Перебор элементов списка: forEach

private static void printListElements(List<Integer> elements) {
    for (Integer element : elements) {
        System.out.println(element);
    }
}

private static void printListElementsUsingStreams(List<Integer> elements) {
    elements.stream().forEach(element -> System.out.println(element));
}

private static void printListElementsUsingStreamsAndMethodReference(List<Integer> elements) {
    elements.stream().forEach(System.out::println);
}

Первый способ – традиционный. Простой цикл foreach печатает элементы. Второй использует метод forEach API Streams для печати элементов списка. Здесь мы используем лямбда-выражения, чтобы определить поведение того, что мы будем делать с каждым элементом списка. Третий по-прежнему использует поток, а также использует ссылку на метод для уменьшения детализации.

В API-интерфейсе коллекций также есть метод forEach, но поскольку мы говорим о потоках, мы использовали forEach API-интерфейса потока. Но имейте в виду, что когда вам просто нужно выполнить итерацию по коллекции, вам лучше придерживаться API forEach из коллекций, такого как list.forEach(System.out::println), так как forEach of streams в основном используется для итерации по элементы после набора потоковых операций, таких как фильтрация, сопоставление и т. д.

У нас есть вариант метода forEach, который называется forEachOrdered. В параллельных потоках элементы могут обрабатываться в порядке, отличном от порядка подачи потока. Итак, если мы хотим сохранить порядок, мы можем использовать forEachOrdered.

Элементы фильтрации: фильтр

private static void filterListElements(List<Integer> elements) {
    for (Integer element : elements) {
        if (element % 2 == 0) {
            System.out.println(element);
        }
    }
}

private static void filterListElementUsingStreams(List<Integer> elements) {
    elements.stream().filter(element -> element % 2 == 0).forEach(System.out::println);
}

Когда нам нужно отфильтровать наш список, чтобы он содержал только необходимые элементы, такие как четные целые числа, мы используем блок if внутри цикла for традиционным способом. Что, если мы хотим улучшить наш код с помощью потоков. Как вы можете видеть выше, потоковый API предоставляет метод фильтра, который принимает интерфейс Predicate для удаления элементов.

@FunctionalInterface
public interface Predicate<T> {

    /**
     * Evaluates this predicate on the given argument.
     *
     * @param t the input argument
     * @return {@code true} if the input argument matches the predicate,
     * otherwise {@code false}
     */
    boolean test(T t);
// rest omitted for brevity
}

Наше лямбда-выражение здесь берет элементы списка и запускает над ними заданное выражение.

element -> element % 2 == 0 // --> implementation of the test method in the Predicate interface

Наш предикат проверяет, является ли текущий элемент четным.

Мы также можем использовать несколько интерфейсов фильтров, соединенных один за другим.

private static void filterListElementUsingStreamsWithMultipleFilter(List<Integer> elements) {
    elements.stream()
            .filter(element -> element % 2 == 0)
            .filter(element -> element > 5)
            .forEach(System.out::println);
}

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

Теперь мы можем рассмотреть некоторые вопросы об итерациях и фильтрах.

1- Выведите числа, которые делятся на 7 и меньше -100 или больше 100.

У нас будут такие числа из списка, что ни одно не будет между -100 и 100 и все будут делиться на 7.

Традиционный способ

private static void printNumbersDivisibleBy7AndEitherLowerThanMinus100orGreaterThan100(List<Integer> numbers) {
    for (Integer number : numbers) {
        if (number % 7 == 0 && (number < -100 || number > 100)) {
            System.out.println(number);
        }
    }
}

Поток путь

private static void printNumbersDivisibleBy7AndEitherLowerThanMinus100orGreaterThan100(List<Integer> numbers) {
    numbers.stream()
            .filter(number -> number % 7 == 0)
            .filter(number -> number > 100 || number < -100)
            .forEach(System.out::println);
}

2- Выведите строки, содержащие данный набор и начинающиеся с данной буквы.

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

Традиционный способ

private static void printStringsThatGivenSetContainsAndStartsWithGivenPrefix(List<String> string, Set<String> set, String prefix) {
    for (String s : string) {
        if (s.startsWith(prefix) && set.contains(s)) {
            System.out.println(s);
        }
    }
}

Поток путь

private static void printStringsThatGivenSetContainsAndStartsWithGivenPrefix(List<String> string, Set<String> set, String prefix) {
    string.stream()
            .filter(s -> s.startsWith(prefix))
            .filter(set::contains)
            .forEach(System.out::println);
}

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

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