Vòng lặp Java 8 Iterable.forEach () vs foreach


465

Điều nào sau đây là thực hành tốt hơn trong Java 8?

Java 8:

joins.forEach(join -> mIrc.join(mSession, join));

Java 7:

for (String join : joins) {
    mIrc.join(mSession, join);
}

Tôi có rất nhiều vòng lặp có thể "đơn giản hóa" với lambdas, nhưng có thực sự có lợi thế nào khi sử dụng chúng không? Nó sẽ cải thiện hiệu suất và khả năng đọc của họ?

BIÊN TẬP

Tôi cũng sẽ mở rộng câu hỏi này cho các phương pháp dài hơn. Tôi biết rằng bạn không thể trả lại hoặc phá vỡ chức năng cha mẹ từ lambda và điều này cũng nên được xem xét khi so sánh chúng, nhưng còn điều gì khác cần phải xem xét không?


10
Không có lợi thế hiệu suất thực sự của một so với nhau. Tùy chọn đầu tiên là một cái gì đó lấy cảm hứng từ FP (whitch thường được nói đến như cách "đẹp" hơn và "rõ ràng" để thể hiện mã của bạn). Trong thực tế - đây là câu hỏi khá "phong cách".
Eugene Loy

5
@Dwb: trong trường hợp này, điều đó không liên quan. forEach không được định nghĩa là song song hoặc bất cứ điều gì tương tự, vì vậy hai điều này tương đương về mặt ngữ nghĩa. Tất nhiên có thể thực hiện một phiên bản song song của forEach (và một phiên bản có thể đã có trong thư viện chuẩn) và trong trường hợp như vậy, cú pháp biểu thức lambda sẽ rất hữu ích.
AardvarkSoup

8
@AardvarkSoup Ví dụ mà forEach được gọi là Luồng ( lambdadoc.net/api/java/util/stream/Stream.html ). Để yêu cầu thực hiện song song, người ta có thể viết joins.pool (). ForEach (...)
mschenk74

13
joins.forEach((join) -> mIrc.join(mSession, join));thực sự là một "đơn giản hóa" của for (String join : joins) { mIrc.join(mSession, join); }? Bạn đã tăng số lần chấm câu từ 9 lên 12, vì lợi ích của việc ẩn loại join. Những gì bạn đã thực sự làm là đặt hai câu lệnh lên một dòng.
Tom Hawtin - tackline

7
Một điểm khác cần xem xét là khả năng nắm bắt biến hạn chế của Java. Với Luồng
RedGlyph

Câu trả lời:


511

Thực hành tốt hơn là sử dụng for-each. Bên cạnh việc vi phạm nguyên tắc Keep It Simple, St ngu ngốc , răng nanh mới forEach()có ít nhất những thiếu sót sau:

  • Không thể sử dụng các biến không phải là cuối cùng . Vì vậy, mã như sau không thể được biến thành forEach lambda:

    Object prev = null;
    for(Object curr : list)
    {
        if( prev != null )
            foo(prev, curr);
        prev = curr;
    }
    
  • Không thể xử lý các trường hợp ngoại lệ được kiểm tra . Lambdas không thực sự bị cấm ném ngoại lệ được kiểm tra, nhưng các giao diện chức năng phổ biến như Consumerkhông khai báo bất kỳ. Do đó, bất kỳ mã nào ném ngoại lệ được kiểm tra phải bọc chúng trong try-catchhoặc Throwables.propagate(). Nhưng ngay cả khi bạn làm điều đó, không phải lúc nào cũng rõ ràng những gì xảy ra với ngoại lệ bị ném. Nó có thể bị nuốt ở đâu đó trong ruột củaforEach()

  • Kiểm soát dòng chảy hạn chế . A returntrong lambda bằng a continuetrong mỗi for, nhưng không có tương đương với a break. Cũng khó thực hiện những việc như trả về giá trị, đoản mạch hoặc đặt cờ (điều này sẽ làm giảm bớt mọi thứ một chút, nếu đó không phải là vi phạm quy tắc không có biến cuối cùng ). "Đây không chỉ là một tối ưu hóa, nhưng rất quan trọng khi bạn xem xét rằng một số trình tự (như đọc các dòng trong tệp) có thể có tác dụng phụ hoặc bạn có thể có một chuỗi vô hạn."

  • Có thể thực thi song song , đó là một điều khủng khiếp, khủng khiếp đối với tất cả, ngoại trừ 0,1% mã của bạn cần được tối ưu hóa. Bất kỳ mã song song nào cũng phải được suy nghĩ kỹ (ngay cả khi nó không sử dụng khóa, chất bay hơi và các khía cạnh đặc biệt khó chịu khác của thực thi đa luồng truyền thống). Bất kỳ lỗi sẽ khó tìm.

  • Có thể ảnh hưởng đến hiệu suất , vì JIT không thể tối ưu hóa forEach () + lambda ở cùng mức độ với các vòng lặp đơn giản, đặc biệt là bây giờ lambdas mới. Bằng cách "tối ưu hóa" tôi không có nghĩa là chi phí chung của việc gọi lambdas (vốn nhỏ), mà là phân tích và chuyển đổi tinh vi mà trình biên dịch JIT hiện đại thực hiện khi chạy mã.

  • Nếu bạn cần song song, có thể nhanh hơn nhiều và không khó để sử dụng ExecutorService . Các luồng đều tự động (đọc: không biết nhiều về vấn đề của bạn) sử dụng chiến lược song song hóa chuyên biệt (đọc: không hiệu quả cho trường hợp chung) ( phân tách đệ quy nối-nối ).

  • Làm cho việc gỡ lỗi trở nên khó hiểu hơn , bởi vì hệ thống phân cấp cuộc gọi lồng nhau và, thần cấm, thực thi song song. Trình gỡ lỗi có thể có các vấn đề hiển thị các biến từ mã xung quanh và những thứ như bước qua có thể không hoạt động như mong đợi.

  • Các luồng nói chung khó mã hóa, đọc và gỡ lỗi hơn . Trên thực tế, điều này đúng với các API " thông thạo " phức tạp nói chung. Sự kết hợp của các câu lệnh đơn phức tạp, sử dụng nhiều tổng quát và thiếu các biến trung gian để tạo ra các thông báo lỗi khó hiểu và gỡ lỗi. Thay vì "phương pháp này không gây quá tải cho loại X", bạn nhận được thông báo lỗi gần hơn với "nơi nào đó bạn đã nhầm lẫn các loại, nhưng chúng tôi không biết ở đâu và như thế nào." Tương tự, bạn không thể bước qua và kiểm tra mọi thứ trong trình gỡ lỗi dễ dàng như khi mã được chia thành nhiều câu lệnh và các giá trị trung gian được lưu vào các biến. Cuối cùng, đọc mã và hiểu các loại và hành vi ở mỗi giai đoạn thực hiện có thể là không tầm thường.

  • Gậy ra như ngón tay cái đau . Ngôn ngữ Java đã có câu lệnh for-every. Tại sao thay thế nó bằng một cuộc gọi chức năng? Tại sao khuyến khích che giấu tác dụng phụ ở đâu đó trong biểu thức? Tại sao khuyến khích một lớp lót khó sử dụng? Trộn thường xuyên cho mỗi và forEach willy-nilly mới là phong cách xấu. Mã nên nói theo thành ngữ (các mẫu nhanh để hiểu do sự lặp lại của chúng) và càng ít thành ngữ được sử dụng thì mã càng rõ ràng và mất ít thời gian hơn để quyết định sử dụng thành ngữ nào (một thời gian lớn cho những người cầu toàn như tôi! ).

Như bạn có thể thấy, tôi không phải là một fan hâm mộ lớn của forEach () trừ những trường hợp có ý nghĩa.

Đặc biệt gây khó chịu cho tôi là thực tế Streamkhông thực hiện Iterable(mặc dù thực sự có phương pháp iterator) và không thể được sử dụng trong mỗi for, chỉ với forEach (). Tôi khuyên bạn nên truyền Streams vào Iterables với (Iterable<T>)stream::iterator. Một cách khác tốt hơn là sử dụng StreamEx để khắc phục một số vấn đề về API Stream, bao gồm cả việc triển khai Iterable.

Điều đó nói rằng, forEach()rất hữu ích cho những điều sau đây:

  • Lặp lại nguyên tử trên một danh sách đồng bộ . Trước đó, một danh sách được tạo với Collections.synchronizedList()nguyên tử liên quan đến những thứ như get hoặc set, nhưng không an toàn cho chủ đề khi lặp.

  • Thực hiện song song (sử dụng một luồng song song thích hợp) . Điều này giúp bạn tiết kiệm một vài dòng mã so với sử dụng ExecutorService, nếu vấn đề của bạn phù hợp với các giả định hiệu suất được tích hợp trong Luồng và Bộ chia.

  • Các thùng chứa cụ thể , giống như danh sách được đồng bộ hóa, được hưởng lợi từ việc kiểm soát vòng lặp (mặc dù điều này phần lớn là lý thuyết trừ khi mọi người có thể đưa ra nhiều ví dụ hơn)

  • Gọi một hàm đơn giản hơn bằng cách sử dụng forEach()và một đối số tham chiếu phương thức (nghĩa là list.forEach (obj::someMethod)). Tuy nhiên, hãy ghi nhớ các điểm về các ngoại lệ được kiểm tra, gỡ lỗi khó hơn và giảm số lượng thành ngữ bạn sử dụng khi viết mã.

Các bài viết tôi sử dụng để tham khảo:

EDIT: Có vẻ như một số đề xuất ban đầu cho lambdas (chẳng hạn như http://www.javac.info/closures-v06a.html ) đã giải quyết một số vấn đề tôi đã đề cập (tất nhiên là thêm các biến chứng của riêng họ).


95
Tại sao khuyến khích ẩn tác dụng phụ ở đâu đó trong biểu thức? là câu hỏi sai Các chức năng forEachlà có để khuyến khích phong cách chức năng, tức là sử dụng các biểu thức mà không có tác dụng phụ. Nếu bạn gặp phải tình huống forEachnày không hoạt động tốt với các tác dụng phụ của bạn, bạn sẽ có cảm giác rằng bạn không sử dụng đúng công cụ cho công việc. Sau đó, câu trả lời đơn giản là, đó là vì cảm giác của bạn là đúng, vì vậy hãy giữ nguyên vòng lặp cho điều đó. forVòng lặp cổ điển không trở nên chán nản
Holger

17
@Holger Làm thế nào có thể forEachđược sử dụng mà không có tác dụng phụ?
Alexanderr Dubinsky

14
Được rồi, tôi không đủ chính xác, forEachlà hoạt động luồng duy nhất dành cho các hiệu ứng phụ, nhưng nó không dành cho các hiệu ứng phụ như mã ví dụ của bạn, đếm là một reducehoạt động điển hình . Tôi sẽ đề nghị, như một quy tắc của đập, để giữ mọi hoạt động thao túng các biến cục bộ hoặc sẽ ảnh hưởng đến luồng điều khiển (bao gồm xử lý ngoại lệ) trong một forvòng lặp cổ điển . Về câu hỏi ban đầu tôi nghĩ, vấn đề bắt nguồn từ việc ai đó sử dụng một luồng trong đó một forvòng lặp đơn giản trên nguồn của luồng là đủ. Sử dụng một luồng forEach()chỉ hoạt động
Holger

8
@Holger Ví dụ về tác dụng phụ forEachsẽ phù hợp với điều gì?
Alexanderr Dubinsky

29
Một cái gì đó xử lý từng mục riêng lẻ và không cố gắng biến đổi các biến cục bộ. Ví dụ, tự thao tác với các mục hoặc in chúng, viết / gửi chúng vào một tệp, luồng mạng, v.v ... Không có vấn đề gì với tôi nếu bạn đặt câu hỏi về các ví dụ này và không thấy bất kỳ ứng dụng nào cho nó; lọc, ánh xạ, thu nhỏ, tìm kiếm và thu thập (ở mức độ thấp hơn) là các hoạt động ưa thích của một luồng. ForEach có vẻ thuận tiện cho tôi khi liên kết với các API hiện có. Và cho các hoạt động song song, tất nhiên. Chúng sẽ không hoạt động với forcác vòng lặp.
Holger

169

Lợi thế xuất hiện khi các hoạt động có thể được thực hiện song song. (Xem http://java.dzone.com/articles/devoxx-2012-java-8-lambda-and - phần về lặp lại bên trong và bên ngoài)

  • Ưu điểm chính theo quan điểm của tôi là việc thực hiện những gì sẽ được thực hiện trong vòng lặp có thể được xác định mà không phải quyết định liệu nó sẽ được thực hiện song song hay tuần tự

  • Nếu bạn muốn vòng lặp của mình được thực thi song song, bạn có thể chỉ cần viết

     joins.parallelStream().forEach(join -> mIrc.join(mSession, join));

    Bạn sẽ phải viết thêm một số mã để xử lý luồng, v.v.

Lưu ý: Đối với câu trả lời của tôi, tôi giả sử tham gia thực hiện java.util.Streamgiao diện. Nếu tham gia chỉ thực hiện java.util.Iterablegiao diện thì điều này không còn đúng nữa.


4
Các slide của một kỹ sư tiên tri mà anh ta đang đề cập đến ( blog.oracle.com/darcy/resource/Devoxx/ mẹo ) không đề cập đến sự song song trong các biểu thức lambda. Sự song song có thể xảy ra trong các phương pháp thu thập số lượng lớn như map& foldkhông thực sự liên quan đến lambdas.
Thomas Jungblut

1
Có vẻ như mã của OP sẽ không được hưởng lợi từ sự song song tự động ở đây (đặc biệt là vì không có gì đảm bảo rằng sẽ có một mã). Chúng tôi không thực sự biết "mIrc" là gì, nhưng "tham gia" không thực sự giống như một thứ gì đó có thể bị lỗi thời.
Eugene Loy

11
Stream#forEachIterable#forEachkhông giống nhau OP đang hỏi về Iterable#forEach.
gvlasov

2
Tôi đã sử dụng kiểu UPDATEX vì có những thay đổi trong thông số kỹ thuật giữa thời gian câu hỏi được hỏi và thời gian câu trả lời được cập nhật. Nếu không có lịch sử của câu trả lời, tôi sẽ còn bối rối hơn nữa.
mschenk74

1
Bất cứ ai có thể vui lòng giải thích cho tôi tại sao câu trả lời này không hợp lệ nếu joinsđược thực hiện Iterablethay vì Stream? Từ một vài điều tôi đã đọc, OP sẽ có thể thực hiện joins.stream().forEach((join) -> mIrc.join(mSession, join));joins.parallelStream().forEach((join) -> mIrc.join(mSession, join));nếu joinsthực hiệnIterable
Blueriver

112

Khi đọc câu hỏi này, người ta có thể có ấn tượng, rằng Iterable#forEachkết hợp với biểu thức lambda là một phím tắt / thay thế để viết một vòng lặp truyền thống cho mỗi vòng lặp. Đơn giản là nó sai. Mã này từ OP:

joins.forEach(join -> mIrc.join(mSession, join));

không có ý định như một phím tắt để viết

for (String join : joins) {
    mIrc.join(mSession, join);
}

và chắc chắn không nên được sử dụng theo cách này. Thay vào đó, nó được dùng như một phím tắt (mặc dù nó không hoàn toàn giống nhau) để viết

joins.forEach(new Consumer<T>() {
    @Override
    public void accept(T join) {
        mIrc.join(mSession, join);
    }
});

Và nó là sự thay thế cho mã Java 7 sau:

final Consumer<T> c = new Consumer<T>() {
    @Override
    public void accept(T join) {
        mIrc.join(mSession, join);
    }
};
for (T t : joins) {
    c.accept(t);
}

Thay thế phần thân của một vòng lặp bằng một giao diện chức năng, như trong các ví dụ ở trên, làm cho mã của bạn rõ ràng hơn: Bạn đang nói rằng (1) phần thân của vòng lặp không ảnh hưởng đến mã xung quanh và luồng điều khiển và (2) phần thân của vòng lặp có thể được thay thế bằng cách thực hiện hàm khác, mà không ảnh hưởng đến mã xung quanh. Không thể truy cập các biến không cuối cùng của phạm vi bên ngoài không phải là sự thiếu hụt các hàm / lambdas, đó là một tính năng phân biệt ngữ nghĩa của Iterable#forEachngữ nghĩa của một vòng lặp truyền thống. Khi một người đã quen với cú pháp của Iterable#forEachnó, nó làm cho mã dễ đọc hơn, bởi vì bạn ngay lập tức nhận được thông tin bổ sung này về mã.

Mỗi vòng lặp truyền thống chắc chắn sẽ duy trì thực hành tốt (để tránh thuật ngữ " thực hành tốt nhất " được sử dụng quá mức ) trong Java. Nhưng điều này không có nghĩa, điều đó Iterable#forEachnên được coi là thực hành xấu hoặc phong cách xấu. Luôn luôn là một thực hành tốt, sử dụng đúng công cụ để thực hiện công việc và điều này bao gồm việc trộn các vòng lặp truyền thống cho từng vòng Iterable#forEach, với ý nghĩa của nó.

Vì những nhược điểm Iterable#forEachđã được thảo luận trong chủ đề này, đây là một số lý do, tại sao bạn có thể muốn sử dụng Iterable#forEach:

  • Để làm cho mã của bạn rõ ràng hơn: Như được mô tả ở trên, Iterable#forEach có thể làm cho mã của bạn rõ ràng hơn và dễ đọc hơn trong một số tình huống.

  • Để làm cho mã của bạn có thể mở rộng và duy trì nhiều hơn: Sử dụng một hàm làm phần thân của vòng lặp cho phép bạn thay thế hàm này bằng các triển khai khác nhau (xem Mẫu chiến lược ). Ví dụ, bạn có thể dễ dàng thay thế biểu thức lambda bằng một cuộc gọi phương thức, có thể được ghi đè bởi các lớp con:

    joins.forEach(getJoinStrategy());

    Sau đó, bạn có thể cung cấp các chiến lược mặc định bằng cách sử dụng enum, thực hiện giao diện chức năng. Điều này không chỉ làm cho mã của bạn mở rộng hơn mà còn tăng khả năng bảo trì vì nó tách riêng việc thực hiện vòng lặp khỏi khai báo vòng lặp.

  • Để làm cho mã của bạn dễ gỡ lỗi hơn : Việc triển khai thực hiện vòng lặp từ khai báo cũng có thể giúp việc gỡ lỗi dễ dàng hơn, bởi vì bạn có thể có một triển khai gỡ lỗi chuyên biệt, in ra các thông báo gỡ lỗi mà không cần phải làm lộn xộn mã chính của bạn if(DEBUG)System.out.println(). Việc thực hiện gỡ lỗi có thể là một đại biểu , trang trí cho việc thực hiện chức năng thực tế.

  • Để tối ưu hóa hiệu suất đang quan trọng: Trái ngược với một số khẳng định trong chủ đề này, Iterable#forEach không đã cung cấp hiệu suất tốt hơn so với một truyền thống cho-mỗi vòng lặp, ít nhất là khi sử dụng ArrayList và chạy Hotspot trong chế độ "-Khách hàng". Mặc dù hiệu suất tăng này là nhỏ và không đáng kể đối với hầu hết các trường hợp sử dụng, có những tình huống, trong đó hiệu suất bổ sung này có thể tạo ra sự khác biệt. Ví dụ, những người bảo trì thư viện chắc chắn sẽ muốn đánh giá, nếu một số triển khai vòng lặp hiện có của họ nên được thay thế bằng Iterable#forEach.

    Để hỗ trợ cho tuyên bố này với sự thật, tôi đã thực hiện một số điểm chuẩn vi mô với Caliper . Đây là mã kiểm tra (cần có Caliper mới nhất từ ​​git):

    @VmOptions("-server")
    public class Java8IterationBenchmarks {
    
        public static class TestObject {
            public int result;
        }
    
        public @Param({"100", "10000"}) int elementCount;
    
        ArrayList<TestObject> list;
        TestObject[] array;
    
        @BeforeExperiment
        public void setup(){
            list = new ArrayList<>(elementCount);
            for (int i = 0; i < elementCount; i++) {
                list.add(new TestObject());
            }
            array = list.toArray(new TestObject[list.size()]);
        }
    
        @Benchmark
        public void timeTraditionalForEach(int reps){
            for (int i = 0; i < reps; i++) {
                for (TestObject t : list) {
                    t.result++;
                }
            }
            return;
        }
    
        @Benchmark
        public void timeForEachAnonymousClass(int reps){
            for (int i = 0; i < reps; i++) {
                list.forEach(new Consumer<TestObject>() {
                    @Override
                    public void accept(TestObject t) {
                        t.result++;
                    }
                });
            }
            return;
        }
    
        @Benchmark
        public void timeForEachLambda(int reps){
            for (int i = 0; i < reps; i++) {
                list.forEach(t -> t.result++);
            }
            return;
        }
    
        @Benchmark
        public void timeForEachOverArray(int reps){
            for (int i = 0; i < reps; i++) {
                for (TestObject t : array) {
                    t.result++;
                }
            }
        }
    }
    

    Và đây là kết quả:

    Khi chạy với "-client", Iterable#forEachvượt trội hơn vòng lặp for truyền thống trên một ArrayList, nhưng vẫn chậm hơn so với việc lặp trực tiếp qua một mảng. Khi chạy với "-erver", hiệu suất của tất cả các phương pháp đều giống nhau.

  • Để cung cấp hỗ trợ tùy chọn cho thực thi song song: Ở đây đã nói rằng khả năng thực thi giao diện chức năng Iterable#forEachsong song sử dụng các luồng , chắc chắn là một khía cạnh quan trọng. Vì Collection#parallelStream()không đảm bảo, vòng lặp thực sự được thực thi song song, người ta phải coi đây là một tính năng tùy chọn . Bằng cách lặp lại danh sách của bạn với list.parallelStream().forEach(...);, bạn nói rõ ràng: Vòng lặp này hỗ trợ thực hiện song song, nhưng nó không phụ thuộc vào nó. Một lần nữa, đây là một tính năng và không thâm hụt!

    Bằng cách di chuyển quyết định thực thi song song khỏi thực thi vòng lặp thực tế của bạn, bạn cho phép tối ưu hóa tùy chọn mã của mình, mà không ảnh hưởng đến chính mã, đó là một điều tốt. Ngoài ra, nếu việc triển khai luồng song song mặc định không phù hợp với nhu cầu của bạn, không ai ngăn cản bạn cung cấp triển khai của riêng bạn. Ví dụ, bạn có thể cung cấp một bộ sưu tập được tối ưu hóa tùy thuộc vào hệ điều hành cơ bản, về kích thước của bộ sưu tập, vào số lượng lõi và trên một số cài đặt tùy chọn:

    public abstract class MyOptimizedCollection<E> implements Collection<E>{
        private enum OperatingSystem{
            LINUX, WINDOWS, ANDROID
        }
        private OperatingSystem operatingSystem = OperatingSystem.WINDOWS;
        private int numberOfCores = Runtime.getRuntime().availableProcessors();
        private Collection<E> delegate;
    
        @Override
        public Stream<E> parallelStream() {
            if (!System.getProperty("parallelSupport").equals("true")) {
                return this.delegate.stream();
            }
            switch (operatingSystem) {
                case WINDOWS:
                    if (numberOfCores > 3 && delegate.size() > 10000) {
                        return this.delegate.parallelStream();
                    }else{
                        return this.delegate.stream();
                    }
                case LINUX:
                    return SomeVerySpecialStreamImplementation.stream(this.delegate.spliterator());
                case ANDROID:
                default:
                    return this.delegate.stream();
            }
        }
    }
    

    Điều tuyệt vời ở đây là, việc thực hiện vòng lặp của bạn không cần biết hoặc quan tâm đến những chi tiết này.


5
Bạn có một cái nhìn thú vị trong cuộc thảo luận này và đưa ra một số điểm. Tôi sẽ cố gắng giải quyết chúng. Bạn đề xuất chuyển đổi giữa forEachfor-eachdựa trên một số tiêu chí liên quan đến bản chất của thân vòng lặp. Trí tuệ và kỷ luật tuân theo các quy tắc như vậy là đặc điểm của một lập trình viên giỏi. Những quy tắc như vậy cũng là nguyên nhân của anh ta, bởi vì những người xung quanh anh ta không tuân theo họ hoặc không đồng ý. Ví dụ: sử dụng Ngoại lệ được kiểm tra và không được kiểm tra. Tình huống này dường như còn nhiều sắc thái hơn. Nhưng, nếu phần thân "không ảnh hưởng đến mã xung quanh hoặc điều khiển luồng", thì không bao giờ coi nó là một chức năng tốt hơn?
Alexanderr Dubinsky

4
Cảm ơn các ý kiến ​​chi tiết Aleksandr. But, if the body "does not affect surround code or flow control," isn't factoring it out as a function better?. Vâng, điều này thường sẽ là trường hợp theo ý kiến ​​của tôi - bao gồm các vòng lặp này vì các chức năng là một hệ quả tự nhiên.
Balder

2
Liên quan đến vấn đề hiệu suất - tôi đoán nó phụ thuộc rất nhiều vào bản chất của vòng lặp. Trong một dự án tôi đang thực hiện, tôi đã sử dụng các vòng lặp kiểu hàm tương tự như Iterable#forEachtrước Java 8 chỉ vì tăng hiệu năng. Dự án được đề cập có một vòng lặp chính tương tự như vòng lặp trò chơi, với số vòng lặp phụ lồng nhau không xác định, trong đó khách hàng có thể tham gia vòng lặp trình cắm thêm dưới dạng hàm. Cấu trúc phần mềm như vậy được hưởng lợi rất nhiều từ Iteable#forEach.
Balder

7
Có một câu ở cuối bài phê bình của tôi: "Mã nên nói theo thành ngữ và càng ít thành ngữ được sử dụng thì mã càng rõ ràng và càng mất ít thời gian để quyết định sử dụng thành ngữ nào". Tôi bắt đầu đánh giá cao điểm này khi tôi chuyển từ C # sang Java.
Alexanderr Dubinsky

7
Đó là một cuộc tranh luận tồi tệ. Bạn có thể sử dụng nó để biện minh cho bất cứ điều gì bạn muốn: tại sao bạn không nên sử dụng vòng lặp for, bởi vì vòng lặp while là đủ tốt và đó là một thành ngữ ít hơn. Heck, tại sao sử dụng bất kỳ câu lệnh lặp, chuyển đổi hoặc thử / bắt khi goto có thể làm tất cả điều đó và hơn thế nữa.
tapichu

13

forEach()có thể được thực hiện để nhanh hơn vòng lặp for, vì vòng lặp biết cách tốt nhất để lặp lại các phần tử của nó, trái ngược với cách lặp lặp tiêu chuẩn. Vì vậy, sự khác biệt là vòng lặp bên trong hoặc vòng lặp bên ngoài.

Ví dụ ArrayList.forEach(action)có thể được thực hiện đơn giản như

for(int i=0; i<size; i++)
    action.accept(elements[i])

trái ngược với vòng lặp for đòi hỏi rất nhiều giàn giáo

Iterator iter = list.iterator();
while(iter.hasNext())
    Object next = iter.next();
    do something with `next`

Tuy nhiên, chúng ta cũng cần tính đến hai chi phí trên không bằng cách sử dụng forEach(), một là tạo đối tượng lambda, hai là gọi phương thức lambda. Chúng có lẽ không đáng kể.

xem thêm http: // journal. warewith ware.com/2013/01/13/iteration-inside-and-out/ để so sánh các lần lặp bên trong / bên ngoài cho các trường hợp sử dụng khác nhau.


8
Tại sao iterable biết cách tốt nhất nhưng iterator thì không?
mschenk74

2
không có sự khác biệt cơ bản, nhưng mã bổ sung là cần thiết để phù hợp với giao diện lặp, có thể tốn kém hơn.
ZhongYu

1
@ zhong.j.yu nếu bạn triển khai Bộ sưu tập, bạn cũng sẽ triển khai Iterable. Vì vậy, không có chi phí mã nào về mặt "thêm mã để triển khai các phương thức giao diện bị thiếu", nếu đó là quan điểm của bạn. Như mschenk74 cho biết dường như không có lý do nào khiến bạn không thể điều chỉnh trình vòng lặp của mình để biết cách lặp lại bộ sưu tập của mình theo cách tốt nhất có thể. Tôi đồng ý rằng có thể có chi phí cho việc tạo iterator, nhưng nghiêm túc mà nói, những thứ đó thường rẻ đến mức bạn có thể nói rằng chúng có chi phí bằng 0 ...
Eugene Loy

4
ví dụ lặp lại một cái cây : void forEach(Consumer<T> v){leftTree.forEach(v);v.accept(rootElem);rightTree.forEach(v);}, cái này thanh lịch hơn so với việc lặp bên ngoài và bạn có thể quyết định cách đồng bộ hóa tốt nhất
ratchet freak

1
Thật thú vị, nhận xét duy nhất trong các String.joinphương thức (được, tham gia sai) là "Số phần tử không có khả năng có giá trị Arrays.flow trên đầu". vì vậy họ sử dụng một posh cho vòng lặp.
Tom Hawtin - tackline

7

TL; DR : List.stream().forEach()là nhanh nhất.

Tôi cảm thấy tôi nên thêm kết quả của mình từ việc lặp lại điểm chuẩn. Tôi đã thực hiện một cách tiếp cận rất đơn giản (không có khung điểm chuẩn) và điểm chuẩn 5 phương pháp khác nhau:

  1. cổ điển for
  2. foreach cổ điển
  3. List.forEach()
  4. List.stream().forEach()
  5. List.parallelStream().forEach

quy trình và thông số kiểm tra

private List<Integer> list;
private final int size = 1_000_000;

public MyClass(){
    list = new ArrayList<>();
    Random rand = new Random();
    for (int i = 0; i < size; ++i) {
        list.add(rand.nextInt(size * 50));
    }    
}
private void doIt(Integer i) {
    i *= 2; //so it won't get JITed out
}

Danh sách trong lớp này sẽ được lặp đi lặp lại và có một số doIt(Integer i)áp dụng cho tất cả các thành viên của nó, mỗi lần thông qua một phương thức khác nhau. trong lớp Main, tôi chạy phương thức được thử nghiệm ba lần để làm nóng JVM. Sau đó tôi chạy phương thức kiểm tra 1000 lần tổng hợp thời gian cần thiết cho mỗi phương pháp lặp (sử dụng System.nanoTime()). Sau khi xong, tôi chia số tiền đó cho 1000 và đó là kết quả, thời gian trung bình. thí dụ:

myClass.fored();
myClass.fored();
myClass.fored();
for (int i = 0; i < reps; ++i) {
    begin = System.nanoTime();
    myClass.fored();
    end = System.nanoTime();
    nanoSum += end - begin;
}
System.out.println(nanoSum / reps);

Tôi đã chạy nó trên CPU i5 4 nhân, với phiên bản java 1.8.0_05

cổ điển for

for(int i = 0, l = list.size(); i < l; ++i) {
    doIt(list.get(i));
}

thời gian thực hiện: 4,21 ms

foreach cổ điển

for(Integer i : list) {
    doIt(i);
}

thời gian thực hiện: 5,95 ms

List.forEach()

list.forEach((i) -> doIt(i));

thời gian thực hiện: 3,11 ms

List.stream().forEach()

list.stream().forEach((i) -> doIt(i));

thời gian thực hiện: 2,79 ms

List.parallelStream().forEach

list.parallelStream().forEach((i) -> doIt(i));

thời gian thực hiện: 3,6 ms


23
Làm thế nào để bạn có được những con số đó? Khuôn khổ nào cho điểm chuẩn bạn đang sử dụng? Nếu bạn không sử dụng và chỉ đơn giản System.out.printlnđể hiển thị dữ liệu này một cách ngây thơ, thì tất cả các kết quả đều vô dụng.
Luiggi Mendoza

2
Không có khung. Tôi sử dụng System.nanoTime(). Nếu bạn đọc câu trả lời, bạn sẽ thấy nó đã được thực hiện như thế nào. Tôi không nghĩ rằng nó làm cho nó vô dụng vì đây là một câu hỏi tương đối . Tôi không quan tâm đến việc một phương pháp nhất định đã làm tốt như thế nào, tôi quan tâm nó đã làm tốt như thế nào so với các phương pháp khác.
Assaf

31
Và đó là mục đích của một điểm chuẩn vi mô tốt. Vì bạn không đáp ứng các yêu cầu như vậy, kết quả là vô ích.
Luiggi Mendoza

6
Tôi có thể khuyên bạn nên tìm hiểu JMH, đây là những gì đang được sử dụng cho chính Java và nỗ lực rất nhiều để có được các số chính xác: openjdk.java.net/projects/code-tools/jmh
dsvensson

1
Tôi đồng ý với @LuiggiMendoza. Không có cách nào để biết rằng những kết quả này là phù hợp hoặc hợp lệ. Chúa biết có bao nhiêu điểm chuẩn tôi đã thực hiện để tiếp tục báo cáo các kết quả khác nhau, đặc biệt tùy thuộc vào thứ tự lặp, kích thước và những gì không.
mmm

6

Tôi cảm thấy rằng tôi cần phải mở rộng nhận xét của mình một chút ...

Về mô hình \ phong cách

Đó có lẽ là khía cạnh đáng chú ý nhất. FP trở nên phổ biến do những gì bạn có thể tránh được tác dụng phụ. Tôi sẽ không đi sâu vào những ưu điểm bạn có thể nhận được từ việc này, vì điều này không liên quan đến câu hỏi.

Tuy nhiên, tôi sẽ nói rằng phép lặp sử dụng Iterable.forEach được lấy cảm hứng từ FP và thay vào đó là mang lại nhiều FP hơn cho Java (trớ trêu thay, tôi nói rằng không có nhiều sử dụng cho forEach trong FP thuần túy, vì nó không có gì ngoài việc giới thiệu phản ứng phụ).

Cuối cùng tôi sẽ nói rằng đó là vấn đề của hương vị \ style \ paradigm mà bạn hiện đang viết.

Về song song.

Từ quan điểm hiệu suất, không có lợi ích đáng chú ý nào được hứa hẹn từ việc sử dụng Iterable.forEach trên foreach (...).

Theo các tài liệu chính thức trên Iterable.forEach :

Thực hiện hành động đã cho trên nội dung của Iterable, theo thứ tự các phần tử xảy ra khi lặp, cho đến khi tất cả các phần tử đã được xử lý hoặc hành động ném ngoại lệ.

... tức là các tài liệu khá rõ ràng rằng sẽ không có sự song song ngầm định. Thêm một sẽ là vi phạm LSP.

Bây giờ, có "các bộ sưu tập parallell" được hứa hẹn trong Java 8, nhưng để làm việc với những bộ bạn cần tôi rõ ràng hơn và hãy cẩn thận hơn để sử dụng chúng (ví dụ như câu trả lời của mschenk74).

BTW: trong trường hợp này Stream.forEach sẽ được sử dụng và không đảm bảo rằng công việc thực tế sẽ được thực hiện song song (phụ thuộc vào bộ sưu tập cơ bản).

CẬP NHẬT: có thể không rõ ràng và có một chút kéo dài trong nháy mắt nhưng có một khía cạnh khác của phong cách và quan điểm dễ đọc.

Trước hết - những chiếc vòng cũ đơn giản là cũ và cũ. Mọi người đã biết họ.

Thứ hai, và quan trọng hơn - bạn có thể muốn sử dụng Iterable.forEach chỉ với lambdas một lớp. Nếu "cơ thể" trở nên nặng hơn - chúng có xu hướng không thể đọc được. Bạn có 2 tùy chọn từ đây - sử dụng các lớp bên trong (yuck) hoặc sử dụng forloop cũ đơn giản. Mọi người thường cảm thấy khó chịu khi họ nhìn thấy những điều tương tự (iteratins trên các bộ sưu tập) được thực hiện các vays / phong cách khác nhau trong cùng một cơ sở mã, và điều này dường như là trường hợp.

Một lần nữa, điều này có thể hoặc không thể là một vấn đề. Phụ thuộc vào những người làm việc trên mã.


1
Song song không cần "bộ sưu tập song song" mới. Nó chỉ phụ thuộc vào việc bạn đã yêu cầu một luồng liên tiếp (sử dụng bộ sưu tập.stream ()) hay cho một luồng song song (sử dụng bộ sưu tập.poolStream ()).
JB Nizet

@JBNizet Theo docs Collection.poolStream () không đảm bảo rằng việc thực hiện bộ sưu tập sẽ trả về luồng parallell. Tôi, thực sự tự hỏi bản thân mình, khi điều này có thể xảy ra, nhưng, có lẽ điều này phụ thuộc vào bộ sưu tập.
Eugene Loy

đã đồng ý. Nó cũng phụ thuộc vào bộ sưu tập. Nhưng quan điểm của tôi là các vòng lặp foreach song song đã có sẵn với tất cả các bộ sưu tập tiêu chuẩn (ArrayList, v.v.). Không cần phải đợi "bộ sưu tập song song".
JB Nizet

@JBNizet đồng ý với quan điểm của bạn, nhưng đó không thực sự là ý của "bộ sưu tập song song" ở nơi đầu tiên. Tôi tham chiếu Collection.poolStream () đã được thêm vào trong Java 8 dưới dạng "các bộ sưu tập song song" theo cách tương tự với khái niệm của Scala, khá giống nhau. Ngoài ra, không chắc nó được gọi như thế nào trong bit của JSR, tôi đã thấy một vài bài viết sử dụng cùng một thuật ngữ cho tính năng Java 8 này.
Eugene Loy

1
cho đoạn cuối bạn có thể sử dụng một tham chiếu hàm:collection.forEach(MyClass::loopBody);
ratchet freak

6

Một trong những forEachhạn chế lớn nhất của chức năng là thiếu hỗ trợ ngoại lệ được kiểm tra.

Một cách giải quyết khác có thể là thay thế thiết bị đầu cuối forEachbằng vòng lặp foreach cũ:

    Stream<String> stream = Stream.of("", "1", "2", "3").filter(s -> !s.isEmpty());
    Iterable<String> iterable = stream::iterator;
    for (String s : iterable) {
        fileWriter.append(s);
    }

Dưới đây là danh sách các câu hỏi phổ biến nhất với các cách giải quyết khác về xử lý ngoại lệ được kiểm tra trong lambdas và luồng:

Hàm Lambda Java 8 có ném ngoại lệ không?

Java 8: Lambda-Streams, Lọc theo phương thức với ngoại lệ

Làm cách nào tôi có thể ném các ngoại lệ CHECKED từ bên trong các luồng Java 8?

Java 8: Xử lý ngoại lệ được kiểm tra bắt buộc trong các biểu thức lambda. Tại sao bắt buộc, không bắt buộc?


3

Ưu điểm của phương pháp Java 1.8 forEach trên 1.7 Được cải tiến cho vòng lặp là trong khi viết mã, bạn chỉ có thể tập trung vào logic nghiệp vụ.

Phương thức forEach lấy đối tượng java.util.feft.Consumer làm đối số, vì vậy nó giúp có logic kinh doanh của chúng tôi tại một vị trí riêng biệt mà bạn có thể sử dụng lại bất cứ lúc nào.

Hãy nhìn vào đoạn trích dưới đây,

  • Ở đây tôi đã tạo Lớp mới sẽ ghi đè phương thức lớp chấp nhận từ Lớp người tiêu dùng, nơi bạn có thể thêm chức năng bổ sung, Hơn cả Lặp lại .. !!!!!!

    class MyConsumer implements Consumer<Integer>{
    
        @Override
        public void accept(Integer o) {
            System.out.println("Here you can also add your business logic that will work with Iteration and you can reuse it."+o);
        }
    }
    
    public class ForEachConsumer {
    
        public static void main(String[] args) {
    
            // Creating simple ArrayList.
            ArrayList<Integer> aList = new ArrayList<>();
            for(int i=1;i<=10;i++) aList.add(i);
    
            //Calling forEach with customized Iterator.
            MyConsumer consumer = new MyConsumer();
            aList.forEach(consumer);
    
    
            // Using Lambda Expression for Consumer. (Functional Interface) 
            Consumer<Integer> lambda = (Integer o) ->{
                System.out.println("Using Lambda Expression to iterate and do something else(BI).. "+o);
            };
            aList.forEach(lambda);
    
            // Using Anonymous Inner Class.
            aList.forEach(new Consumer<Integer>(){
                @Override
                public void accept(Integer o) {
                    System.out.println("Calling with Anonymous Inner Class "+o);
                }
            });
        }
    }
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.