Подтвердить что ты не робот

IntelliJ IDEA предлагает заменить для петель методом foreach. Должен ли я всегда делать это, когда это возможно?

IDEA предлагает заменить, например, следующее:

for (Point2D vertex : graph.vertexSet()) {
  union.addVertex(vertex);
}

с этим:

graph.vertexSet().forEach(union::addVertex);

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

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

Итак, я написал этот не очень исчерпывающий тест:

package org.sample;

import org.openjdk.jmh.annotations.Benchmark;
import org.openjdk.jmh.annotations.Fork;
import org.openjdk.jmh.annotations.Threads;
import org.openjdk.jmh.infra.Blackhole;
import org.tendiwa.geometry.Point2D;

import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.IntStream;

public class LanguageConstructVsForeach {
    private static final int NUMBER_OF_POINTS = 10000;
    private static final List<Point2D> points = IntStream
        .range(0, NUMBER_OF_POINTS)
        .mapToObj(i -> new Point2D(i, i * 2))
        .collect(Collectors.toList());

    @Benchmark
    @Threads(1)
    @Fork(3)
    public void languageConstructToBlackhole(Blackhole bh) {
        for (Point2D point : points) {
            bh.consume(point);
        }
    }
    @Benchmark
    @Threads(1)
    @Fork(3)
    public void foreachToBlackhole(Blackhole bh) {
        points.forEach(bh::consume);
    }
    @Benchmark
    @Threads(1)
    @Fork(3)
    public List<Point2D> languageConstructToList(Blackhole bh) {
        List<Point2D> list = new ArrayList<>(NUMBER_OF_POINTS);
        for (Point2D point : points) {
            list.add(point);
        }
        return list;
    }
    @Benchmark
    @Threads(1)
    @Fork(3)
    public List<Point2D> foreachToList(Blackhole bh) {
        List<Point2D> list = new ArrayList<>(NUMBER_OF_POINTS);
        points.forEach(list::add);
        return list;
    }

}

И получил:

Benchmark                                                       Mode  Samples      Score     Error  Units
o.s.LanguageConstructVsForeach.foreachToBlackhole              thrpt       60  33693.834 ± 894.138  ops/s
o.s.LanguageConstructVsForeach.foreachToList                   thrpt       60   7753.941 ± 239.081  ops/s
o.s.LanguageConstructVsForeach.languageConstructToBlackhole    thrpt       60  16043.548 ± 644.432  ops/s
o.s.LanguageConstructVsForeach.languageConstructToList         thrpt       60   6499.527 ± 202.589  ops/s

Как получается foreach более эффективен в обоих случаях: когда я фактически ничего не делаю и когда делаю какую-то фактическую работу? Не foreach просто инкапсулировать Iterator? Является ли этот показатель даже правильным? Если это так, есть ли какая-нибудь причина сегодня использовать конструкцию старого языка с Java 8?

4b9b3361

Ответ 1

Вы сравниваете цикл "расширенный для" языка с помощью метода Iterable.forEach(). Тест не является явно неправильным, и результаты могут показаться неожиданными, пока вы не вникнете в реализации.

Обратите внимание, что список points - это экземпляр ArrayList, так как тот, что создается сборщиком Collectors.toList().

Цикл расширенного цикла на Iterable получает от него Iterator, а затем вызывает hasNext() и next() несколько раз, пока не будет больше элементов. (Это отличается от цикла расширенного цикла над массивом, который обеспечивает доступ к арифметическому и прямому элементу массива.) Таким образом, при циклизации по Iterable этот цикл будет выполнять как минимум два вызова метода на итерацию.

В отличие от этого вызов ArrayList.forEach() запускает традиционный int-based for-loop над массивом, содержащим элементы списка, и вызывает lambda один раз на итерацию. Здесь только один вызов на итерацию, в отличие от двух вызовов на итерацию для цикла расширенного цикла. Это может объяснить, почему ArrayList.forEach() быстрее в этом случае.

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

Когда цикл выполняет немного работы (добавление в список адресатов), для ArrayList.forEach() все еще есть преимущество скорости, но это намного меньшее различие. Я подозреваю, что если вы будете делать больше работы внутри цикла, преимущество будет еще меньше. Это показывает, что накладные расходы цикла для любой конструкции очень малы. Попробуйте использовать BlackHole.consumeCPU() в цикле. Я не удивлюсь, если результаты между двумя конструктами станут неотличимыми.

Обратите внимание, что преимущество большой скорости происходит, потому что Iterable.forEach() заканчивается тем, что имеет специализированную реализацию в ArrayList.forEach(). Если вы запустили forEach() в другой структуре данных, вы, вероятно, получите разные результаты.

Я бы не использовал это как оправдание для слепо замены всех циклов расширенного доступа на вызовы Iterable.forEach(). Напишите код, который является самым ясным, и это имеет наибольший смысл. Если вы пишете критический код производительности, сравните его! Различные формы будут иметь разную производительность, в зависимости от рабочей нагрузки, структуры данных и т.д.

Ответ 2

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