Java 8: Cách ưa thích để đếm số lần lặp của lambda?


82

Tôi đối mặt với cùng một vấn đề thường xuyên. Tôi cần đếm số lần chạy của lambda để sử dụng bên ngoài lambda. Ví dụ:

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

Vấn đề là runCount cần phải là số cuối cùng, vì vậy nó không thể là một int. Nó không thể là một Số nguyên bởi vì nó là bất biến. Tôi có thể làm cho nó biến mức lớp (tức là một trường) nhưng tôi sẽ chỉ cần nó trong khối mã này. Tôi biết có nhiều cách khác nhau, tôi chỉ tò mò giải pháp ưa thích của bạn cho việc này là gì? Bạn có sử dụng AtomicInteger hay tham chiếu mảng hay một số cách khác không?


7
@ Sliver2009 Không, không phải.
Rohit Jain

4
@Florian Bạn phải sử dụng AtomicIntegerở đây.
Rohit Jain

Câu trả lời:


70

Hãy để tôi định dạng lại ví dụ của bạn một chút để tiện cho việc thảo luận:

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

Nếu bạn thực sự cần tăng một bộ đếm từ bên trong lambda, cách thông thường để làm như vậy là tạo bộ đếm là một AtomicIntegerhoặc AtomicLongvà sau đó gọi một trong các phương thức tăng trên đó.

Bạn có thể sử dụng một phần tử inthoặc longmảng đơn lẻ , nhưng điều đó sẽ có các điều kiện về chủng tộc nếu luồng được chạy song song.

Nhưng lưu ý rằng luồng kết thúc bằng forEach, có nghĩa là không có giá trị trả về. Bạn có thể thay đổi forEachthành a peek, chuyển các mục đi qua, sau đó đếm chúng:

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

Điều này có phần tốt hơn, nhưng vẫn hơi kỳ quặc. Lý do là forEachpeekchỉ có thể làm công việc của họ thông qua các tác dụng phụ. Phong cách chức năng mới nổi của Java 8 là để tránh các tác dụng phụ. Chúng tôi đã thực hiện một chút điều đó bằng cách trích xuất số gia của bộ đếm thành một counthoạt động trên luồng. Các tác dụng phụ điển hình khác là thêm các mục vào bộ sưu tập. Thông thường chúng có thể được thay thế bằng cách sử dụng bộ sưu tập. Nhưng không biết công việc thực tế bạn đang cố gắng làm là gì, tôi không thể đề xuất điều gì cụ thể hơn.


8
Cần lưu ý rằng peekngừng hoạt động, sau khi counttriển khai bắt đầu sử dụng phím tắt cho SIZEDluồng. Đó có thể không bao giờ là vấn đề với filterluồng ed nhưng có thể tạo ra bất ngờ lớn nếu ai đó thay đổi mã sau đó…
Holger

14
Khai báo final AtomicInteger i = new AtomicInteger(1);và ở đâu đó trong việc sử dụng lambda của bạn i.getAndAdd(1). Hãy dừng lại và nhớ rằng đã từng tốt đẹp như thế nào int i=1; ... i++.
aliopi

2
Nếu Java sẽ triển khai các giao diện như Incrementabletrên các lớp số, bao gồm AtomicIntegervà khai báo các toán tử như là ++các hàm trông lạ mắt, chúng ta sẽ không cần nạp chồng toán tử và vẫn có mã rất dễ đọc.
Mức độ nghiêm trọng Một

37

Để thay thế cho việc đồng bộ AtomicInteger rắc rối, người ta có thể sử dụng một mảng số nguyên . Miễn là tham chiếu đến mảng không nhận được một mảng khác được gán (và đó là điểm), nó có thể được sử dụng như một biến cuối cùng trong khi giá trị của các trường có thể thay đổi tùy ý.

    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
    });

Nếu bạn muốn đảm bảo rằng tham chiếu không được gán một mảng khác, bạn có thể khai báo iarr như một biến cuối cùng. Nhưng như @pisaruk đã chỉ ra, điều này sẽ không hoạt động song song.
themathmagician 24/08/17

1
Tôi nghĩ, để đơn giản foreachtrực tiếp trên bộ sưu tập (không có luồng), đây là một cách tiếp cận đủ tốt. cảm ơn !!
Sabir Khan

Đây là giải pháp đơn giản nhất, miễn là bạn không chạy mọi thứ song song.
David DeMar

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
Vui lòng chỉnh sửa với nhiều thông tin hơn. Các câu trả lời chỉ có mã và "thử cái này" không được khuyến khích vì chúng không chứa nội dung có thể tìm kiếm được và không giải thích tại sao ai đó nên "thử cái này". Chúng tôi nỗ lực ở đây để trở thành một nguồn kiến ​​thức.
Mogsdad

7
Câu trả lời của bạn làm tôi bối rối. Bạn có hai biến đều được đặt tên runCount. Tôi nghi ngờ bạn định chỉ có một trong số họ, nhưng cái nào?
Ole VV

1
Tôi thấy runCount.getAndIncrement () phù hợp hơn. Câu trả lời chính xác!
kospol

2
Điều đó AtomicIntegerđã giúp tôi nhưng tôi sẽ kết thúc nó vớinew AtomicInteger(0)
Stefan Höltker

1) Mã này sẽ không biên dịch: luồng không có hoạt động đầu cuối trả về giá trị dài 2) Ngay cả khi có, giá trị 'runCount' sẽ luôn là '1': - luồng không có hoạt động đầu cuối nên đối số peek () lambda sẽ không bao giờ được gọi - Dòng System.out tăng số lần chạy trước khi hiển thị
Cédric

8

Bạn không nên sử dụng AtomicInteger, bạn không nên sử dụng mọi thứ trừ khi bạn có lý do thực sự chính đáng để sử dụng. Và lý do sử dụng AtomicInteger có thể chỉ cho phép các truy cập đồng thời hoặc tương tự như vậy.

Khi nói đến vấn đề của bạn;

Giá đỡ có thể được sử dụng để giữ và gia tăng nó bên trong lambda. Và sau khi bạn có thể lấy nó bằng cách gọi 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");

Có một vài lớp Holder trong JDK. Điều này dường như là javax.xml.ws.Holder.
Brad Cupit

1
Có thật không? Và tại sao?
lkahtz

1
Tôi đồng ý - nếu tôi biết tôi không thực hiện bất kỳ hoạt động đồng thời nào trong lamda / stream tại sao tôi muốn sử dụng AtomicInteger được thiết kế để phục vụ cho đồng thời - điều đó có thể giới thiệu khóa, v.v., tức là lý do tại sao, nhiều năm trước, JDK đã giới thiệu tập hợp các tập hợp mới và các trình vòng lặp của chúng không thực hiện bất kỳ khóa nào - tại sao lại tạo gánh nặng cho một thứ gì đó với khả năng giảm hiệu suất, ví dụ như khóa khi khóa không cần thiết cho nhiều trường hợp.
Volksman

4

Đối với tôi, đây là mẹo, hy vọng ai đó thấy nó hữu ích:

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

getAndIncrement () Tài liệu Java cho biết:

Tăng nguyên tử giá trị hiện tại, với các hiệu ứng bộ nhớ như được chỉ định bởi VarHandle.getAndAdd. Tương đương với getAndAdd (1).


3

Một cách khác để thực hiện việc này (hữu ích nếu bạn muốn số lượng của mình chỉ tăng lên trong một số trường hợp, chẳng hạn như nếu một hoạt động thành công) là như thế này, sử dụng 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");

Như Stuart Marks đã lưu ý, điều này vẫn còn hơi kỳ lạ, vì nó không hoàn toàn tránh được các tác dụng phụ (tùy thuộc vào những gì đã foo()bar()đang làm).

Và một cách khác để tăng một biến trong lambda có thể truy cập bên ngoài nó là sử dụng một biến lớp:

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++;
        });
    }
}

Trong ví dụ này, việc sử dụng một biến lớp cho một bộ đếm trong một phương thức có thể không hợp lý, vì vậy tôi sẽ thận trọng với nó trừ khi có lý do chính đáng. Giữ các biến lớp finalnếu có thể có thể hữu ích về mặt an toàn luồng, v.v. (xem http://www.javapractices.com/topic/TopicAction.do?Id=23 để thảo luận về cách sử dụngfinal ).

Để hiểu rõ hơn về lý do tại sao lambdas hoạt động theo cách họ làm, hãy https://www.infoq.com/articles/Java-8-Lambdas-A-Peek-Under-the-Hood có một cái nhìn chi tiết.


3

Nếu bạn không muốn tạo một trường vì bạn chỉ cần nó cục bộ, bạn có thể lưu trữ nó trong một lớp ẩn danh:

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

Kỳ lạ, tôi biết. Nhưng nó giữ cho biến tạm thời nằm ngoài phạm vi cục bộ.


2
chuyện quái gì đang xảy ra ở đây, cần được giải thích thêm một chút thx
Alexander Mills

Về cơ bản, nó tăng dần và truy xuất một trường trên một lớp ẩn danh. Nếu bạn cho tôi biết bạn đang bối rối cụ thể về điều gì, tôi có thể cố gắng làm rõ.
shmosel

1
@MrCholo Đó là một khối khởi tạo . Nó chạy trước hàm tạo.
shmosel

1
@MrCholo Không, đó là một trình khởi tạo phiên bản.
shmosel

1
@MrCholo Một lớp ẩn danh không thể có hàm tạo được khai báo rõ ràng.
shmosel

1

Giảm cũng hoạt động, bạn có thể sử dụng nó như thế này

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

1

Một thay thế khác là sử dụng 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
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.