Java 8 có cung cấp cách tốt để lặp lại giá trị hoặc hàm không?


118

Trong nhiều ngôn ngữ khác, ví dụ. Haskell, rất dễ dàng để lặp lại một giá trị hoặc hàm nhiều lần, ví dụ: để nhận danh sách 8 bản sao của giá trị 1:

take 8 (repeat 1)

nhưng tôi chưa tìm thấy điều này trong Java 8. Có chức năng như vậy trong JDK của Java 8 không?

Hoặc cách khác tương đương với một phạm vi như

[1..8]

Nó dường như là một sự thay thế rõ ràng cho một câu lệnh dài dòng trong Java như

for (int i = 1; i <= 8; i++) {
    System.out.println(i);
}

để có một cái gì đó giống như

Range.from(1, 8).forEach(i -> System.out.println(i))

mặc dù ví dụ cụ thể này thực sự trông không ngắn gọn hơn nhiều ... nhưng hy vọng nó dễ đọc hơn.


2
Bạn đã nghiên cứu API luồng chưa? Đó nên là đặt cược tốt nhất của bạn theo JDK có liên quan. Nó có một chức năng phạm vi , đó là những gì tôi đã tìm thấy cho đến nay.
Marko Topolnik

1
@MarkoTopolnik Lớp Streams đã bị xóa (chính xác hơn là nó đã được chia thành nhiều lớp khác và một số phương thức đã bị xóa hoàn toàn).
assylias

3
Bạn gọi một vòng lặp for dài dòng! Đó là một điều tốt khi bạn không có mặt trong những ngày Cobol. Phải mất hơn 10 câu lệnh khai báo trong Cobol để hiển thị các số tăng dần. Những người trẻ ngày nay không đánh giá cao việc họ có nó tốt như thế nào.
Gilbert Le Blanc,

1
@GilbertLeBlanc độ dài không liên quan gì đến nó. Không thể tổng hợp các vòng lặp, Luồng thì có. Các vòng lặp dẫn đến việc lặp lại không thể tránh khỏi, trong khi Luồng cho phép sử dụng lại. Vì các luồng như vậy là một sự trừu tượng tốt hơn về mặt định lượng so với các vòng lặp và nên được ưu tiên hơn.
Alain O'Dea

2
@GilbertLeBlanc và chúng tôi phải viết mã bằng chân trần, trên tuyết.
Dawood ibn Kareem

Câu trả lời:


155

Đối với ví dụ cụ thể này, bạn có thể làm:

IntStream.rangeClosed(1, 8)
         .forEach(System.out::println);

Nếu bạn cần một bước khác với 1, bạn có thể sử dụng chức năng ánh xạ, ví dụ: cho bước 2:

IntStream.rangeClosed(1, 8)
         .map(i -> 2 * i - 1)
         .forEach(System.out::println);

Hoặc tạo một lần lặp tùy chỉnh và giới hạn kích thước của lần lặp:

IntStream.iterate(1, i -> i + 2)
         .limit(8)
         .forEach(System.out::println);

4
Closures sẽ biến đổi hoàn toàn mã Java, tốt hơn. Nhìn về phía trước để ngày hôm đó ...
Marko Topolnik

1
@jwenting Nó thực sự phụ thuộc - thường là với nội dung GUI (Swing hoặc JavaFX), loại bỏ rất nhiều đĩa nồi hơi do các lớp ẩn danh.
assylias

8
@jwenting Đối với bất kỳ ai có kinh nghiệm về FP, mã xoay quanh các hàm bậc cao hơn là một chiến thắng thuần túy. Đối với bất kỳ ai không có kinh nghiệm đó, đã đến lúc nâng cấp kỹ năng của bạn --- hoặc có nguy cơ bị bỏ lại trong bụi.
Marko Topolnik

2
@MarkoTopolnik Bạn có thể muốn sử dụng phiên bản javadoc mới hơn một chút (bạn đang trỏ đến bản dựng 78, mới nhất là bản dựng 105: download.java.net/lambda/b105/docs/api/java/util/stream/… )
Đánh dấu Rotteveel

1
@GraemeMoss Bạn vẫn có thể sử dụng cùng một mẫu ( IntStream.rangeClosed(1, 8).forEach(i -> methodNoArgs());) nhưng nó gây nhầm lẫn cho IMO và trong trường hợp đó, một vòng lặp dường như được chỉ ra.
assylias

65

Đây là một kỹ thuật khác mà tôi đã thử nghiệm vào ngày hôm trước:

Collections.nCopies(8, 1)
           .stream()
           .forEach(i -> System.out.println(i));

Cuộc Collections.nCopiesgọi tạo ra một bản sao Listcó chứa nbất kỳ giá trị nào bạn cung cấp. Trong trường hợp này, đó là Integergiá trị được đóng hộp 1. Tất nhiên nó không thực sự tạo một danh sách với ncác phần tử; nó tạo ra một danh sách "ảo hóa" chỉ chứa giá trị và độ dài, và bất kỳ lệnh gọi nào đến gettrong phạm vi đều chỉ trả về giá trị. Các nCopiesphương pháp đã được khoảng từ Framework Các bộ sưu tập được giới thiệu cách trở lại trong JDK 1.2. Tất nhiên, khả năng tạo luồng từ kết quả của nó đã được thêm vào trong Java SE 8.

Việc lớn, một cách khác để làm điều tương tự với cùng một số dòng.

Tuy nhiên, kỹ thuật này nhanh hơn phương pháp IntStream.generateIntStream.iteratephương pháp tiếp cận, và đáng ngạc nhiên là nó cũng nhanh hơn IntStream.rangephương pháp tiếp cận.

Đối với iterategeneratekết quả có lẽ không quá ngạc nhiên. Khung luồng (thực sự là Spliterator cho các luồng này) được xây dựng dựa trên giả định rằng các lambdas sẽ có khả năng tạo ra các giá trị khác nhau mỗi lần và chúng sẽ tạo ra một số lượng kết quả không giới hạn. Điều này làm cho việc tách song song trở nên đặc biệt khó khăn. Các iteratephương pháp cũng là vấn đề đối với trường hợp này bởi vì mỗi cuộc gọi đòi hỏi kết quả của tuần trước. Vì vậy, các luồng sử dụng generateiteratekhông hoạt động rất tốt trong việc tạo các hằng số lặp lại.

Hiệu suất tương đối kém của rangelà đáng ngạc nhiên. Điều này cũng được ảo hóa, vì vậy các phần tử không thực sự tồn tại trong bộ nhớ và kích thước được biết trước. Điều này sẽ tạo ra một bộ tách sóng song song nhanh và dễ dàng. Nhưng đáng ngạc nhiên là nó không hoạt động tốt lắm. Có lẽ lý do là rangephải tính toán một giá trị cho mỗi phần tử của phạm vi và sau đó gọi một hàm trên đó. Nhưng hàm này chỉ bỏ qua đầu vào của nó và trả về một hằng số, vì vậy tôi ngạc nhiên vì điều này không được nội tuyến và bị giết.

Các Collections.nCopieskỹ thuật đã làm boxing / unboxing để xử lý các giá trị, vì không có chuyên ngành nguyên thủy của List. Vì giá trị của mỗi lần như nhau , về cơ bản nó được đóng hộp một lần và hộp đó được chia sẻ bởi tất cả các nbản sao. Tôi nghi ngờ quyền anh / unboxing được tối ưu hóa rất cao, thậm chí còn hấp dẫn, và nó có thể được sắp xếp tốt.

Đây là mã:

    public static final int LIMIT = 500_000_000;
    public static final long VALUE = 3L;

    public long range() {
        return
            LongStream.range(0, LIMIT)
                .parallel()
                .map(i -> VALUE)
                .map(i -> i % 73 % 13)
                .sum();
}

    public long ncopies() {
        return
            Collections.nCopies(LIMIT, VALUE)
                .parallelStream()
                .mapToLong(i -> i)
                .map(i -> i % 73 % 13)
                .sum();
}

Và đây là kết quả JMH: (Core2Duo 2,8GHz)

Benchmark                    Mode   Samples         Mean   Mean error    Units
c.s.q.SO18532488.ncopies    thrpt         5        7.547        2.904    ops/s
c.s.q.SO18532488.range      thrpt         5        0.317        0.064    ops/s

Có một số lượng lớn sự khác biệt trong phiên bản ncopies, nhưng nhìn chung, nó có vẻ nhanh hơn 20 lần so với phiên bản phạm vi. (Tuy nhiên, tôi khá sẵn lòng tin rằng tôi đã làm sai điều gì đó.)

Tôi ngạc nhiên về cách nCopieshoạt động của kỹ thuật này. Bên trong nó không có gì đặc biệt lắm, với luồng danh sách ảo hóa được triển khai đơn giản bằng cách sử dụng IntStream.range! Tôi đã nghĩ rằng cần phải tạo một trình phân tách chuyên biệt để làm cho việc này diễn ra nhanh chóng, nhưng có vẻ như nó đã khá ổn.


6
Các nhà phát triển ít kinh nghiệm hơn có thể bối rối hoặc gặp rắc rối khi họ biết rằng nCopieskhông thực sự sao chép bất cứ thứ gì và các "bản sao" đều trỏ đến một đối tượng duy nhất. Sẽ luôn an toàn nếu đối tượng đó là bất biến , chẳng hạn như nguyên thủy đóng hộp trong ví dụ này. Bạn ám chỉ điều này trong tuyên bố "đóng hộp một lần" của mình, nhưng có thể tốt hơn nếu gọi rõ ràng các cảnh báo ở đây vì hành vi đó không dành riêng cho quyền tự động.
William Price

1
Vì vậy, điều đó có nghĩa LongStream.rangelà chậm hơn đáng kể so với IntStream.range? Vì vậy, thật tốt khi ý tưởng không cung cấp IntStream(nhưng sử dụng LongStreamcho tất cả các kiểu số nguyên) đã bị loại bỏ. Lưu ý rằng đối với trường hợp sử dụng liên tục, không có một lý do để sử dụng dòng nào cả: Collections.nCopies(8, 1).forEach(i -> System.out.println(i));không giống như Collections.nCopies(8, 1).stream().forEach(i -> System.out.println(i));nhưng thậm chí hiệu quả hơn có thể làCollections.<Runnable>nCopies(8, () -> System.out.println(1)).forEach(Runnable::run);
Holger

1
@Holger, những thử nghiệm này được thực hiện trên hồ sơ loại sạch, vì vậy chúng không liên quan đến thế giới thực. Có lẽ hoạt động LongStream.rangekém hơn, bởi vì nó có hai bản đồ với LongFunctionbên trong, trong khi ncopiescó ba bản đồ với IntFunction, ToLongFunctionLongFunctiondo đó tất cả lambdas đều là đơn hình. Chạy thử nghiệm này trên cấu hình loại ô nhiễm trước (gần giống với trường hợp thực tế hơn) cho thấy ncopiestốc độ chậm hơn 1,5 lần.
Tagir Valeev

1
Tối ưu hóa sớm FTW
Rafael Bugajewski

1
Vì lợi ích của sự hoàn chỉnh, sẽ rất tuyệt nếu thấy một điểm chuẩn so sánh cả hai kỹ thuật này với một forvòng lặp cũ đơn giản . Mặc dù giải pháp của bạn nhanh hơn so với Streammã, nhưng tôi đoán rằng một forvòng lặp sẽ đánh bại một trong hai giải pháp này bằng một biên độ đáng kể.
typeracer

35

Vì sự hoàn chỉnh, và cũng bởi vì tôi không thể giúp đỡ bản thân mình :)

Việc tạo ra một chuỗi hằng số giới hạn khá gần với những gì bạn sẽ thấy trong Haskell, chỉ với mức độ chi tiết của Java.

IntStream.generate(() -> 1)
         .limit(8)
         .forEach(System.out::println);

() -> 1sẽ chỉ tạo ra 1, điều này có dự định không? Vì vậy, đầu ra sẽ là 1 1 1 1 1 1 1 1.
Christian Ullenboom,

4
Có, theo ví dụ Haskell đầu tiên của OP take 8 (repeat 1). assylias khá nhiều đã bao gồm tất cả các trường hợp khác.
clstrfsck

3
Stream<T>cũng có một generatephương pháp chung để nhận một luồng vô hạn của một số loại khác, có thể bị giới hạn theo cách tương tự.
zstewart

11

Khi một hàm lặp lại ở đâu đó được định nghĩa là

public static BiConsumer<Integer, Runnable> repeat = (n, f) -> {
    for (int i = 1; i <= n; i++)
        f.run();
};

Bạn có thể sử dụng nó ngay bây giờ và sau đó theo cách này, ví dụ:

repeat.accept(8, () -> System.out.println("Yes"));

Để có được và tương đương với Haskell's

take 8 (repeat 1)

Bạn có thể viết

StringBuilder s = new StringBuilder();
repeat.accept(8, () -> s.append("1"));

2
Điều này là tuyệt vời. Tuy nhiên, tôi đã sửa đổi nó để cung cấp số lần lặp lại, bằng cách thay đổi Runnablethành Function<Integer, ?>và sau đó sử dụng f.apply(i).
Fons

0

Đây là giải pháp của tôi để thực hiện chức năng thời gian. Tôi là một học sinh cấp dưới nên tôi thừa nhận rằng nó có thể không lý tưởng, tôi rất vui khi được biết nếu đây không phải là một ý kiến ​​hay vì bất cứ lý do gì.

public static <T extends Object, R extends Void> R times(int count, Function<T, R> f, T t) {
    while (count > 0) {
        f.apply(t);
        count--;
    }
    return null;
}

Đây là một số ví dụ sử dụng:

Function<String, Void> greet = greeting -> {
    System.out.println(greeting);
    return null;
};

times(3, greet, "Hello World!");
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.