Java 8: предпочтительный способ подсчета итераций лямбды?


84

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

Например:

myStream.stream().filter(...).forEach(item -> { ... ; runCount++});
System.out.println("The lambda ran " + runCount + "times");

Проблема в том, что runCount должен быть final, поэтому он не может быть int. Это не может быть, Integerпотому что это неизменяемо . Я мог бы сделать это переменной уровня класса (то есть полем), но мне это понадобится только в этом блоке кода.

Я знаю, что есть разные способы, мне просто интересно, какое решение вы предпочитаете для этого?
Вы используете AtomicIntegerссылку на массив или другой способ?


7
@ Sliver2009 Нет, это не так.
Рохит Джайн

4
@ Флориан. Тебе здесь нужно использовать AtomicInteger.
Рохит Джайн

Ответы:


72

Позвольте мне немного переформатировать ваш пример для обсуждения:

long runCount = 0L;
myStream.stream()
    .filter(...)
    .forEach(item -> { 
        foo();
        bar();
        runCount++; // doesn't work
    });
System.out.println("The lambda ran " + runCount + " times");

Если вам действительно нужно увеличить счетчик из лямбды, типичный способ сделать это - сделать счетчик AtomicIntegerилиAtomicLong а затем вызвать для него один из методов увеличения.

Вы можете использовать одноэлементный intилиlong массив, но это будет иметь условия гонки, если поток будет выполняться параллельно.

Но обратите внимание, что поток заканчивается forEach, а это означает, что возвращаемого значения нет. Вы можете изменить на forEacha peek, который передает элементы, а затем подсчитывает их:

long runCount = myStream.stream()
    .filter(...)
    .peek(item -> { 
        foo();
        bar();
    })
    .count();
System.out.println("The lambda ran " + runCount + " times");

Это немного лучше, но все же немного странно. Причина заключается в том, что forEachи peekможет сделать только свою работу с помощью побочных эффектов. Новый функциональный стиль Java 8 призван избежать побочных эффектов. Мы немного поработали, извлекая приращение счетчика в countоперацию в потоке. Другими типичными побочными эффектами являются добавление элементов в коллекции. Обычно их можно заменить с помощью коллекторов. Но, не зная, какую реальную работу вы пытаетесь выполнить, я не могу предложить ничего более конкретного.


9
Следует отметить, что он peekперестает работать, как только countреализации начинают использовать ярлыки для SIZEDпотоков. Возможно, это никогда не будет проблемой с filtered stream, но может создать большие сюрпризы, если кто-то изменит код позже…
Хольгер

16
Объявите final AtomicInteger i = new AtomicInteger(1);и где-нибудь в вашем лямбде используйте i.getAndAdd(1). Остановись и вспомни, как хорошо было int i=1; ... i++раньше.
aliopi 08

2
Если бы в Java реализовывались интерфейсы, например, Incrementableдля числовых классов, включая AtomicIntegerи объявляли операторы, ++чтобы они выглядели причудливыми функциями, нам не потребовалась бы перегрузка операторов, и мы все равно имели бы очень читаемый код.
SeverityOne

38

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

    int[] iarr = {0}; // final not neccessary here if no other array is assigned
    stringList.forEach(item -> {
            iarr[0]++;
            // iarr = {1}; Error if iarr gets other array assigned
    });

Если вы хотите убедиться, что ссылке не назначен другой массив, вы можете объявить iarr как конечную переменную. Но, как указывает @pisaruk, это не будет работать параллельно.
математик

1
Я думаю, что для простого foreachнепосредственно при сборе (без потоков) это достаточно хороший подход. благодаря !!
Сабир Хан

Это самое простое решение, если вы не запускаете вещи параллельно.
Дэвид ДеМар

17
AtomicInteger runCount = 0L;
long runCount = myStream.stream()
    .filter(...)
    .peek(item -> { 
        foo();
        bar();
        runCount.incrementAndGet();
    });
System.out.println("The lambda ran " + runCount.incrementAndGet() + "times");

15
Пожалуйста, отредактируйте с дополнительной информацией. Ответы только на код и «попробуйте это» не приветствуются , потому что они не содержат доступного для поиска контента и не объясняют, почему кто-то должен «попробовать это». Мы стараемся быть здесь источником знаний.
Mogsdad

7
Ваш ответ меня смущает. У вас есть две переменные с именами runCount. Я подозреваю, что вы намеревались иметь только один из них, но какой?
Ole VV

1
Я обнаружил, что runCount.getAndIncrement () более подходит. Отличный ответ!
коспол

2
AtomicIntegerПомог мне , но я бы инициализировать егоnew AtomicInteger(0)
Стефан Höltker

1) Этот код не компилируется: поток не имеет терминальной операции, которая возвращает длинный 2) Даже если бы это было, значение runCount всегда было бы '1': - у потока нет терминальной операции, поэтому лямбда-аргумент peek () никогда не будет вызываться - строка System.out увеличивает счетчик запусков перед его отображением
Седрик

9

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

Когда дело доходит до вашей проблемы;

Holder может использоваться для удержания и увеличения его внутри лямбды. И после этого вы можете получить его, вызвав runCount.value

Holder<Integer> runCount = new Holder<>(0);

myStream.stream()
    .filter(...)
    .forEach(item -> { 
        foo();
        bar();
        runCount.value++; // now it's work fine!
    });
System.out.println("The lambda ran " + runCount + " times");

В JDK есть несколько классов Holder. Кажется, это один javax.xml.ws.Holder.
Брэд Купит,

1
В самом деле? И почему?
lkahtz

1
Я согласен - если я знаю, что не выполняю никаких параллельных операций в lamda / stream, зачем мне использовать AtomicInteger, предназначенный для обеспечения параллелизма - это может привести к блокировке и т. Д., То есть причина, по которой много лет назад JDK представил новый набор коллекций и их итераторы, которые не выполняли никаких блокировок - зачем обременять что-то возможностью снижения производительности, например, блокировка, когда блокировка не требуется для многих сценариев.
Volksman

5

Для меня это помогло, надеюсь, кому-то это пригодится:

AtomicInteger runCount = new AtomicInteger(0);
myStream.stream().filter(...).forEach(item -> runCount.getAndIncrement());
System.out.println("The lambda ran " + runCount.get() + "times");

getAndIncrement() В документации Java указано:

Атомарно увеличивает текущее значение с эффектами памяти, как указано в VarHandle.getAndAdd. Эквивалентно getAndAdd (1).


3

Другой способ сделать это (полезно, если вы хотите, чтобы ваш счетчик увеличивался только в некоторых случаях, например, если операция была успешной) - это что-то вроде этого, используя mapToInt()и sum():

int count = myStream.stream()
    .filter(...)
    .mapToInt(item -> { 
        foo();
        if (bar()){
           return 1;
        } else {
           return 0;
    })
    .sum();
System.out.println("The lambda ran " + count + "times");

Как заметил Стюарт Маркс, это все еще несколько странно, потому что это не позволяет полностью избежать побочных эффектов (в зависимости от того, что foo()иbar() делают).

И еще один способ увеличения переменной в лямбде, доступной вне ее, - это использовать переменную класса:

public class MyClass {
    private int myCount;

    // Constructor, other methods here

    void myMethod(){
        // does something to get myStream
        myCount = 0;
        myStream.stream()
            .filter(...)
            .forEach(item->{
               foo(); 
               myCount++;
        });
    }
}

В этом примере использование переменной класса для счетчика в одном методе, вероятно, не имеет смысла, поэтому я бы предостерегал от этого, если для этого нет веской причины. Сохранение переменных класса, finalесли это возможно, может быть полезно с точки зрения безопасности потоков и т. Д. (См. Http://www.javapractices.com/topic/TopicAction.do?Id=23 для обсуждения использования final).

Чтобы лучше понять, почему лямбды работают именно так, как они работают, https://www.infoq.com/articles/Java-8-Lambdas-A-Peek-Under-the-Hood имеет подробный обзор.


3

Если вы не хотите создавать поле, потому что оно вам нужно только локально, вы можете сохранить его в анонимном классе:

int runCount = new Object() {
    int runCount = 0;
    {
        myStream.stream()
                .filter(...)
                .peek(x -> runCount++)
                .forEach(...);
    }
}.runCount;

Странно, я знаю. Но при этом временная переменная не попадает даже в локальную область видимости.



По сути, он увеличивает и получает поле анонимного класса. Если вы мне скажете, что вас конкретно смущает, могу попытаться уточнить.
shmosel 05

1
@MrCholo Это блок инициализатора . Он запускается перед конструктором.
shmosel

1
@MrCholo Нет, это инициализатор экземпляра.
shmosel 06

1
@MrCholo Анонимный класс не может иметь явно объявленного конструктора.
shmosel 06

1

сокращение также работает, вы можете использовать его вот так

myStream.stream().filter(...).reduce((item, sum) -> sum += item);

1

Другой альтернативой является использование Apache Commons MutableInt.

MutableInt cnt = new MutableInt(0);
myStream.stream()
    .filter(...)
    .forEach(item -> { 
        foo();
        bar();
        cnt.increment();
    });
System.out.println("The lambda ran " + cnt.getValue() + " times");

0
AtomicInteger runCount = new AtomicInteger(0);

elements.stream()
  //...
  .peek(runCount.incrementAndGet())
  .collect(Collectors.toList());

// runCount.get() should have the num of times lambda code was executed
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.