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
@Florian Ви повинні використовувати 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, а це означає, що не повертається значення. Ви можете змінити значення forEachна a 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потоків. Це може ніколи не бути проблемою з filterпотоком ed, але може створити великі сюрпризи, якщо хтось змінить код пізніше ...
Холгер

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

2
Якби Java реалізовувала інтерфейси, такі як Incrementableчислові класи, в тому числі AtomicInteger, і оголошувала оператори на зразок ++вигадливих функцій, нам не потрібно було б перевантажувати оператори і все ще мати дуже читабельний код.
Severity,

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
Будь ласка, відредагуйте з додатковою інформацією. Відповіді лише для коду та "спробувати це" не рекомендується , оскільки вони не містять вмісту для пошуку, і не пояснюйте, чому хтось повинен "спробувати це". Ми докладаємо зусиль, щоб стати джерелом знань.
Могсдад,

7
Ваша відповідь мене бентежить. Ви отримали дві змінні, названі обома runCount. Підозрюю, ви мали намір мати лише одного з них, але якого?
Оле В. В.

1
Я виявив, що runCount.getAndIncrement () є більш придатним. Чудова відповідь!
Косполь

2
AtomicIntegerДопоміг мені , але я б ініціювати йогоnew AtomicInteger(0)
Стефан Höltker

1) Цей код не компілюється: потік не має термінальної операції, яка повертає довгий 2) Навіть якби це було так, значення 'runCount' завжди було б '1': - потік не виконував терміналу, тому peek () лямбда-аргумент ніколи не буде викликано - System.out рядок збільшує кількість прогонів перед його відображенням
Cédric

9

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

Коли справа стосується вашої проблеми;

Тримач можна використовувати для утримання та збільшення його всередині лямбда. І після того, як ви можете отримати його, зателефонувавши 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;

Дивно, я знаю. Але це тимчасово змінна виключає навіть місцевий обсяг.


2
що тут відбувається, потрібно трохи більше пояснень
Олександр Міллс,

По суті, це збільшення та отримання поля в анонімному класі. Якщо ви скажете мені, чим вас особливо бентежить, я можу спробувати пояснити.
shmosel

1
@MrCholo Це блок ініціалізації . Він працює перед конструктором.
shmosel

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

1
@MrCholo Анонімний клас не може мати явно оголошений конструктор.
shmosel

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.