it-swarm.com.ru

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

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

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

Проблема в том, что runCount должен быть окончательным, поэтому он не может быть int. Это не может быть целое число, потому что оно неизменно. Я мог бы сделать это переменной уровня класса (то есть полем), но она мне понадобится только в этом блоке кода ........ Я знаю, что есть разные способы, мне просто интересно, каково ваше предпочтительное решение для этого? Используете ли вы AtomicInteger или ссылку на массив или каким-либо другим способом? 

53
user4159962

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

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, что означает отсутствие возвращаемого значения. Вы можете изменить forEach на peek, который пропускает элементы, а затем подсчитывает их:

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

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

52
Stuart Marks

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

    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
    });
24
Duke Spray
AtomicInteger runCount = 0L;
long runCount = myStream.stream()
    .filter(...)
    .peek(item -> { 
        foo();
        bar();
        runCount.incrementAndGet();
    });
System.out.println("The lambda ran " + runCount.incrementAndGet() + "times");
6
Leandruz

Другой способ сделать это (полезно, если вы хотите, чтобы ваш счет увеличивался только в некоторых случаях, например, если операция прошла успешно), это что-то вроде этого, используя 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
A. D.

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

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

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

0
shmosel