Apakah Java 8 menyediakan cara yang baik untuk mengulang nilai atau fungsi?

118

Dalam banyak bahasa lain, mis. Haskell, sangat mudah untuk mengulang nilai atau fungsi beberapa kali, misalnya. untuk mendapatkan daftar 8 salinan dari nilai 1:

take 8 (repeat 1)

tapi saya belum menemukan ini di Java 8. Apakah ada fungsi seperti itu di JDK Java 8?

Atau sebagai alternatif sesuatu yang setara dengan rentang seperti

[1..8]

Tampaknya ini adalah pengganti yang jelas untuk pernyataan verbose di Java seperti

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

untuk memiliki sesuatu seperti

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

meskipun contoh khusus ini sebenarnya tidak terlihat lebih ringkas ... tapi semoga lebih mudah dibaca.

Graeme Moss
sumber
2
Sudahkah Anda mempelajari Streams API ? Itu harus menjadi taruhan terbaik Anda sejauh menyangkut JDK. Itu punya fungsi jangkauan , itulah yang saya temukan sejauh ini.
Marko Topolnik
1
@MarkoTopolnik Kelas Stream telah dihapus (lebih tepatnya telah dipecah di antara beberapa kelas lain dan beberapa metode telah dihapus seluruhnya).
assylias
3
Anda menyebut for loop verbose! Untung saja Anda tidak ada di masa Cobol. Butuh lebih dari 10 pernyataan deklaratif di Cobol untuk menampilkan angka naik. Anak muda saat ini tidak menghargai betapa bagusnya mereka memilikinya.
Gilbert Le Blanc
1
@GilbertLeBlanc verbositas tidak ada hubungannya dengan itu. Loop tidak dapat disusun, Stream. Pengulangan menyebabkan pengulangan yang tidak dapat dihindari, sedangkan Aliran mengizinkan penggunaan kembali. Dengan demikian, Stream adalah abstraksi yang secara kuantitatif lebih baik daripada loop dan sebaiknya lebih disukai.
Alain O'Dea
2
@GilbertLeBlanc dan kami harus membuat kode dengan kaki telanjang, di salju.
Dawood ibn Kareem

Jawaban:

155

Untuk contoh khusus ini, Anda dapat melakukan:

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

Jika Anda memerlukan langkah yang berbeda dari 1, Anda dapat menggunakan fungsi pemetaan, misalnya, untuk langkah 2:

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

Atau buat iterasi khusus dan batasi ukuran iterasi:

IntStream.iterate(1, i -> i + 2)
         .limit(8)
         .forEach(System.out::println);
assylias
sumber
4
Penutupan akan sepenuhnya mengubah kode Java menjadi lebih baik. Menantikan hari itu ...
Marko Topolnik
1
@jwenting Itu benar-benar tergantung - biasanya dengan hal-hal GUI (Swing atau JavaFX), yang menghapus banyak plat boiler karena class anonim.
assylias
8
@jwenting Bagi siapa pun yang berpengalaman dalam FP, kode yang berputar di sekitar fungsi tingkat tinggi adalah kemenangan murni. Bagi siapa pun yang tidak memiliki pengalaman itu, inilah waktunya untuk meningkatkan keterampilan Anda --- atau berisiko tertinggal dalam debu.
Marko Topolnik
2
@MarkoTopolnik Anda mungkin ingin menggunakan versi javadoc yang sedikit lebih baru (Anda menunjuk ke build 78, yang terbaru adalah build 105: download.java.net/lambda/b105/docs/api/java/util/stream/… )
Mark Rotteveel
1
@GraemeMoss Anda masih bisa menggunakan pola yang sama ( IntStream.rangeClosed(1, 8).forEach(i -> methodNoArgs());) tetapi itu membingungkan IMO hal dan dalam hal ini loop tampaknya ditunjukkan.
assylias
65

Inilah teknik lain yang saya lakukan beberapa hari yang lalu:

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

The Collections.nCopiespanggilan menciptakan Listmengandung nsalinan dari nilai apa pun yang Anda berikan. Dalam hal ini nilai kotaknya Integer1. Tentu saja itu tidak benar-benar membuat daftar dengan nelemen; itu membuat daftar "tervirtualisasi" yang hanya berisi nilai dan panjang, dan setiap panggilan ke getdalam jangkauan hanya mengembalikan nilai. The nCopiesMetode telah ada sejak Koleksi Kerangka diperkenalkan kembali dengan cara di JDK 1.2. Tentu saja, kemampuan untuk membuat aliran dari hasilnya ditambahkan di Java SE 8.

Masalah besar, cara lain untuk melakukan hal yang sama di sekitar jumlah baris yang sama.

Namun, teknik ini lebih cepat dari IntStream.generatedan IntStream.iteratependekatan, dan mengejutkan, itu juga lebih cepat dari IntStream.rangependekatan.

Untuk iterate dan generatehasilnya mungkin tidak terlalu mengejutkan. Kerangka aliran (sebenarnya, Pemisah untuk aliran ini) dibangun dengan asumsi bahwa lambda akan berpotensi menghasilkan nilai yang berbeda setiap saat, dan bahwa mereka akan menghasilkan jumlah hasil yang tidak terbatas. Hal ini membuat pemisahan paralel menjadi sulit. The iterateMetode juga bermasalah untuk kasus ini karena setiap panggilan membutuhkan hasil dari yang sebelumnya. Jadi aliran menggunakan generatedan iteratetidak melakukannya dengan baik untuk menghasilkan konstanta berulang.

Kinerja yang relatif buruk rangecukup mengejutkan. Ini juga tervirtualisasi, jadi elemen sebenarnya tidak semuanya ada di memori, dan ukurannya diketahui sebelumnya. Ini akan membuat spliterator yang cepat dan mudah diparalelkan. Tapi secara mengejutkan tidak berhasil dengan baik. Mungkin alasannya adalah karena rangeharus menghitung nilai untuk setiap elemen rentang dan kemudian memanggil fungsi di atasnya. Tapi fungsi ini mengabaikan inputnya dan mengembalikan konstanta, jadi saya terkejut ini tidak sebaris dan mati.

The Collections.nCopiesTeknik hubungannya tinju / pembukaan kemasan untuk menangani nilai-nilai, karena tidak ada spesialisasi primitif List. Karena nilainya adalah sama setiap kali, pada dasarnya dikotak sekali dan kotak itu dibagikan oleh semua nsalinan. Saya menduga tinju / unboxing sangat dioptimalkan, bahkan diintrinsifikasi, dan dapat disisipkan dengan baik.

Berikut kodenya:

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

Dan berikut adalah hasil JMH: (2.8GHz Core2Duo)

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

Ada cukup banyak variasi dalam versi ncopies, tetapi secara keseluruhan tampaknya nyaman 20x lebih cepat daripada versi range. (Namun, saya akan sangat bersedia untuk percaya bahwa saya telah melakukan sesuatu yang salah.)

Saya terkejut melihat seberapa baik nCopiesteknik ini bekerja. Secara internal itu tidak terlalu istimewa, dengan aliran daftar virtual yang hanya diimplementasikan menggunakan IntStream.range! Saya berharap akan perlu membuat spliterator khusus agar ini bekerja dengan cepat, tetapi tampaknya sudah cukup bagus.

Stuart Marks
sumber
6
Pengembang yang kurang berpengalaman mungkin bingung atau mendapat masalah ketika mereka mengetahui bahwa nCopiestidak benar-benar menyalin apa pun dan "salinan" semuanya mengarah ke satu objek tunggal itu. Selalu aman jika objek itu tidak dapat diubah , seperti kotak primitif dalam contoh ini. Anda menyinggung hal ini dalam pernyataan "kotak sekali", tetapi mungkin bagus untuk secara eksplisit menyebutkan peringatan di sini karena perilaku tersebut tidak spesifik untuk auto-boxing.
William Harga
1
Jadi itu berarti LongStream.rangesecara signifikan lebih lambat dari IntStream.range? Jadi adalah hal yang baik bahwa gagasan untuk tidak menawarkan IntStream(tetapi digunakan LongStreamuntuk semua tipe integer) telah dibatalkan. Perhatikan bahwa untuk kasus penggunaan berurutan, tidak ada alasan untuk menggunakan streaming sama sekali: Collections.nCopies(8, 1).forEach(i -> System.out.println(i));melakukan hal yang sama Collections.nCopies(8, 1).stream().forEach(i -> System.out.println(i));tetapi mungkin lebih efisienCollections.<Runnable>nCopies(8, () -> System.out.println(1)).forEach(Runnable::run);
Holger
1
@Holger, pengujian ini dilakukan pada profil tipe bersih, sehingga tidak terkait dengan dunia nyata. Mungkin LongStream.rangeberkinerja lebih buruk, karena memiliki dua peta dengan LongFunctiondi dalamnya, sementara ncopiesmemiliki tiga peta dengan IntFunction, ToLongFunctiondan LongFunction, dengan demikian, semua lambda adalah monomorfik. Menjalankan pengujian ini pada profil tipe pra-polusi (yang lebih mendekati kasus dunia nyata) menunjukkan bahwa ncopies1,5x lebih lambat.
Tagir Valeev
1
Optimasi Dini FTW
Rafael Bugajewski
1
Demi kelengkapan, alangkah baiknya melihat tolok ukur yang membandingkan kedua teknik ini dengan forloop lama yang biasa . Meskipun solusi Anda lebih cepat daripada Streamkode, tebakan saya adalah bahwa forloop akan mengalahkan salah satu dari ini dengan margin yang signifikan.
typeracer
35

Untuk kelengkapan, dan juga karena saya tidak bisa menahan diri :)

Menghasilkan urutan konstanta terbatas cukup mirip dengan apa yang akan Anda lihat di Haskell, hanya dengan verboseness level Java.

IntStream.generate(() -> 1)
         .limit(8)
         .forEach(System.out::println);
clstrfsck.dll
sumber
() -> 1hanya akan menghasilkan 1, apakah ini dimaksudkan? Jadi hasilnya akan seperti itu 1 1 1 1 1 1 1 1.
Christian Ullenboom
4
Ya, sesuai contoh Haskell pertama OP take 8 (repeat 1). assylias cukup banyak menutupi semua kasus lainnya.
clstrfsck
3
Stream<T>juga memiliki generatemetode umum untuk mendapatkan aliran tak terbatas dari beberapa jenis lainnya, yang dapat dibatasi dengan cara yang sama.
zstewart
11

Setelah fungsi berulang di suatu tempat didefinisikan sebagai

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

Anda dapat menggunakannya sesekali dengan cara ini, misalnya:

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

Untuk mendapatkan dan setara dengan Haskell's

take 8 (repeat 1)

Anda bisa menulis

StringBuilder s = new StringBuilder();
repeat.accept(8, () -> s.append("1"));
Hartmut P.
sumber
2
Yang ini luar biasa. Namun saya memodifikasinya untuk memberikan nomor iterasi kembali, dengan mengubah Runnablemenjadi Function<Integer, ?>dan kemudian menggunakan f.apply(i).
Fons
0

Ini adalah solusi saya untuk mengimplementasikan fungsi waktu. Saya seorang junior jadi saya akui itu tidak ideal, saya akan senang mendengar jika ini bukan ide yang baik untuk alasan apa pun.

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

Berikut beberapa contoh penggunaan:

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

times(3, greet, "Hello World!");
JH
sumber