Насколько бесполезна лень Java 8 на практике? - PullRequest
0 голосов
/ 07 октября 2018

В последнее время я много читал о потоках Java 8, а также несколько статей о отложенной загрузке потоков Java 8: здесь и здесь .Кажется, я не могу поколебать ощущение, что отложенная загрузка ПОЛНОСТЬЮ бесполезна (или, в лучшем случае, незначительное синтаксическое удобство, предлагающее нулевое значение производительности).

Давайте возьмем этот код в качестве примера:

int[] myInts = new int[]{1,2,3,5,8,13,21};

IntStream myIntStream = IntStream.of(myInts);

int[] myChangedArray = myIntStream
                        .peek(n -> System.out.println("About to square: " + n))
                        .map(n -> (int)Math.pow(n, 2))
                        .peek(n -> System.out.println("Done squaring, result: " + n))
                        .toArray();

Это войдет в консоль, потому что terminal operation, в данном случае toArray(), вызывается, и наш поток ленив и выполняется только при вызове операции терминала.Конечно, я также могу сделать это:

  IntStream myChangedInts = myIntStream
    .peek(n -> System.out.println("About to square: " + n))
    .map(n -> (int)Math.pow(n, 2))
    .peek(n -> System.out.println("Done squaring, result: " + n));

И ничего не будет напечатано, потому что карта не происходит, потому что мне не нужны данные.Пока я не назову это:

  int[] myChangedArray = myChangedInts.toArray();

И вуаля, я получаю свои сопоставленные данные и журналы консоли.За исключением того, что я вижу нулевую пользу для него вообще.Я понимаю, что могу определить код фильтра задолго до того, как я вызову toArray(), и я могу обойти этот «не очень фильтрованный поток», но что с того? Это единственное преимущество?

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

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

и

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

Звучит буквальноЭто похоже на разрыв цикла и вовсе не связано с ленью.

Наконец, во второй статье есть такая недоумение:

Ленивые операции достигают эффективности.Это способ не работать с устаревшими данными.Ленивые операции могут быть полезны в ситуациях, когда входные данные потребляются постепенно, а не с полным набором элементов заранее.Например, рассмотрим ситуации, когда бесконечный поток был создан с использованием Stream # generate (Supplier ), и предоставленная функция Supplier постепенно получает данные с удаленного сервера.В таких ситуациях серверный вызов будет выполняться только при работе терминала, когда это необходимо.

Не работает с устаревшими данными?Какие?Как отложенная загрузка мешает кому-то работать с устаревшими данными?


TLDR: есть ли польза от отложенной загрузки, кроме возможности запускать операции фильтра / отображения / уменьшения / любой другой операции в более позднее время (чтопредлагает нулевой выигрыш в производительности)?

Если да, то каков реальный вариант использования?

Ответы [ 7 ]

0 голосов
/ 07 октября 2018

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

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

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

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

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

Итак, лучшая абстракция, лучшая производительность, лучшая читаемость кода иремонтопригодность, звучит как победа для меня.:)

0 голосов
/ 07 октября 2018

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

0 голосов
/ 07 октября 2018

Лень может быть очень полезна для пользователей вашего API, особенно когда конечный результат оценки конвейера Stream может быть очень большим!

Простой пример - Files.lines метод в самом Java API.Если вы не хотите читать весь файл в память и вам нужны только первые N строк, просто напишите:

Stream<String> stream = Files.lines(path); // lazy operation

List<String> result = stream.limit(N).collect(Collectors.toList()); // read and collect
0 голосов
/ 07 октября 2018

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

У нас есть служба, которая нуждается в List<CustomService>, я должен это назвать.Теперь, чтобы вызвать его, я собираюсь в базу данных (намного проще, чем реальность) и получаю List<DBObject>;для того, чтобы получить List<CustomService> из этого, необходимо выполнить несколько тяжелых преобразований.

И вот мой выбор, трансформируйтесь на месте и пропустите список.Простой, но, вероятно, не такой оптимальный.Второй вариант - рефакторинг сервиса, чтобы принять List<DBObject> и Function<DBObject, CustomService>.И это звучит тривиально, но оно включает лень (среди прочего)Этому сервису иногда могут понадобиться только несколько элементов из этого Списка, или иногда max по какому-либо свойству и т. Д. - поэтому мне не нужно выполнять тяжелое преобразование для всех элементов , вот где Stream API лень на основе тяги - победитель.

До того, как существовали потоки, мы использовали guava.У него был Lists.transform( list, function), который тоже был ленивым.

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

0 голосов
/ 07 октября 2018

Хороший вопрос.

Если предположить, что вы пишете идеальный код для учебника, разница в производительности между должным образом оптимизированными for и stream не заметна (потоки имеют тенденцию быть немного лучше в плане загрузки классов, нов большинстве случаев разница не должна быть заметной).

Рассмотрим следующий пример.

// Some lengthy computation
private static int doStuff(int i) {
    try { Thread.sleep(1000); } catch (InterruptedException e) { }
    return i;
}

public static OptionalInt findFirstGreaterThanStream(int value) {
    return IntStream
            .of(MY_INTS)
            .map(Main::doStuff)
            .filter(x -> x > value)
            .findFirst();
}

public static OptionalInt findFirstGreaterThanFor(int value) {
    for (int i = 0; i < MY_INTS.length; i++) {
        int mapped = Main.doStuff(MY_INTS[i]);
        if(mapped > value){
            return OptionalInt.of(mapped);
        }
    }
    return OptionalInt.empty();
}

Учитывая приведенные выше методы, следующий тест должен показать, что они выполняются примерно в одно и то же время.

public static void main(String[] args) {
    long begin;
    long end;

    begin = System.currentTimeMillis();
    System.out.println(findFirstGreaterThanStream(5));
    end = System.currentTimeMillis();
    System.out.println(end-begin);

    begin = System.currentTimeMillis();
    System.out.println(findFirstGreaterThanFor(5));
    end = System.currentTimeMillis();
    System.out.println(end-begin);
}

OptionalInt [8]

5119

OptionalInt [8]

5001

В любом случае,мы проводим большую часть времени в методе doStuff.Допустим, мы хотим добавить больше потоков в смесь.

Настройка метода потока тривиальна (учитывая, что ваши операции удовлетворяют предварительным условиям параллельных потоков).

public static OptionalInt findFirstGreaterThanParallelStream(int value) {
    return IntStream
            .of(MY_INTS)
            .parallel()
            .map(Main::doStuff)
            .filter(x -> x > value)
            .findFirst();
}

Достижение того же поведения безпотоки могут быть сложными.

public static OptionalInt findFirstGreaterThanParallelFor(int value, Executor executor) {
    AtomicInteger counter = new AtomicInteger(0);

    CompletableFuture<OptionalInt> cf = CompletableFuture.supplyAsync(() -> {
        while(counter.get() != MY_INTS.length-1);
        return OptionalInt.empty();
    });

    for (int i = 0; i < MY_INTS.length; i++) {
        final int current = MY_INTS[i];
        executor.execute(() -> {
            int mapped = Main.doStuff(current);
            if(mapped > value){
                cf.complete(OptionalInt.of(mapped));
            } else {
                counter.incrementAndGet();
            }
        });
    }

    try {
        return cf.get();
    } catch (InterruptedException | ExecutionException e) {
        e.printStackTrace();
        return OptionalInt.empty();
    }
}

Тесты выполняются примерно в то же время снова.

public static void main(String[] args) {
    long begin;
    long end;

    begin = System.currentTimeMillis();
    System.out.println(findFirstGreaterThanParallelStream(5));
    end = System.currentTimeMillis();
    System.out.println(end-begin);

    ExecutorService executor = Executors.newFixedThreadPool(10);
    begin = System.currentTimeMillis();
    System.out.println(findFirstGreaterThanParallelFor(5678, executor));
    end = System.currentTimeMillis();
    System.out.println(end-begin);

    executor.shutdown();
    executor.awaitTermination(10, TimeUnit.SECONDS);
    executor.shutdownNow();
}

OptionalInt [8]

1004

OptionalInt [8]

1004

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

A (немного не по теме) заключительное примечание:

Asс помощью языков программирования абстракции более высокого уровня (streams относительно fors) упрощают разработку и снижают производительность.Мы не перешли от ассемблера к процедурным языкам к объектно-ориентированным языкам, потому что последние предложили большую производительность.Мы переехали, потому что это сделало нас более продуктивными (разработать то же самое с меньшими затратами).Если вы можете получить такую ​​же производительность из потока, как и с for и правильно написанным многопоточным кодом, я бы сказал, что это уже выигрыш.

0 голосов
/ 07 октября 2018

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

Некоторые терминальные операции этого не делают.И для них было бы пустой тратой, если бы потоки не выполнялись лениво.Два примера:

//example 1: print first element of 1000 after transformations
IntStream.range(0, 1000)
    .peek(System.out::println)
    .mapToObj(String::valueOf)
    .peek(System.out::println)
    .findFirst()
    .ifPresent(System.out::println);

//example 2: check if any value has an even key
boolean valid = records.
    .map(this::heavyConversion)
    .filter(this::checkWithWebService)
    .mapToInt(Record::getKey)
    .anyMatch(i -> i % 2 == 0)

Первый поток напечатает:

0
0
0

То есть промежуточные операции будут выполняться только для одного элемента.Это важная оптимизация.Если бы это не было лениво, то все вызовы peek() должны были бы выполняться на всех элементах ( абсолютно ненужный , поскольку вы заинтересованы только в одном элементе).Промежуточные операции могут быть дорогими (например, во втором примере)

Работа терминала с коротким замыканием (которого нет toArray) делает эту оптимизацию возможной.

0 голосов
/ 07 октября 2018

Вы правы, что map().reduce() или map().collect() не принесет выгоды, но есть довольно очевидные преимущества с findAny() findFirst(), anyMatch(), allMatch() и т. Д. В основном,любая операция, которая может быть закорочена.

...