Java 8, bir değeri veya işlevi tekrarlamak için iyi bir yol sağlar mı?


118

Diğer birçok dilde, örn. Haskell, bir değeri veya işlevi birden çok kez tekrarlamak kolaydır, örn. 1 değerinin 8 kopyasının bir listesini almak için:

take 8 (repeat 1)

ama bunu henüz Java 8'de bulamadım. Java 8'in JDK'sinde böyle bir işlev var mı?

Veya alternatif olarak bir aralığa eşdeğer bir şey

[1..8]

Java'daki gibi ayrıntılı bir ifadenin yerine geçecek gibi görünüyor.

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

gibi bir şeye sahip olmak

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

bu belirli örnek aslında çok daha özlü görünmese de ... ama umarım daha okunabilir.


2
Streams API'sini incelediniz mi? JDK söz konusu olduğunda en iyi bahsiniz bu olmalıdır. Bir aralık işlevi var, şimdiye kadar bulduğum şey bu.
Marko Topolnik

1
@MarkoTopolnik Streams sınıfı kaldırıldı (daha doğrusu birkaç başka sınıf arasında bölündü ve bazı yöntemler tamamen kaldırıldı).
assylias

3
Bir for döngüsü ayrıntılı olarak çağırırsınız! Cobol günlerinde ortalarda bulunmamış olman iyi bir şey. Artan sayıları göstermek için Cobol'da 10'dan fazla bildirimsel ifade aldı. Bugünlerde gençler ne kadar iyi sahip olduklarını takdir etmiyorlar.
Gilbert Le Blanc

1
@GilbertLeBlanc ayrıntılarının bununla hiçbir ilgisi yok. Döngüler oluşturulamaz, Akışlar öyledir. Akışlar yeniden kullanıma izin verirken döngüler kaçınılmaz tekrarlara yol açar. Bu tür Akışlar, döngülerden niceliksel olarak daha iyi bir soyutlamadır ve tercih edilmelidir.
Alain O'Dea

2
@GilbertLeBlanc ve biz karda çıplak ayakla kodlamamız gerekiyordu.
Dawood ibn Kareem

Yanıtlar:


155

Bu özel örnek için şunları yapabilirsiniz:

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

1'den farklı bir adıma ihtiyacınız varsa, örneğin 2'lik bir adım için bir eşleme işlevi kullanabilirsiniz:

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

Veya özel bir yineleme oluşturun ve yinelemenin boyutunu sınırlayın:

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

4
Kapanışlar Java kodunu tamamen daha iyi hale getirecek. O günü dört gözle bekliyorum ...
Marko Topolnik

1
@jwenting Bu gerçekten bağlıdır - genellikle anonim sınıflar nedeniyle çok sayıda kazan plakasını kaldıran GUI öğelerine (Swing veya JavaFX) bağlıdır .
assylias

8
@jwenting FP tecrübesi olan herkes için, üst düzey işlevler etrafında dönen kod saf bir kazançtır. Bu deneyimi olmayan herkes için, becerilerinizi geliştirmenin veya geride bırakılma riskini almanın zamanı geldi.
Marko Topolnik

2
@MarkoTopolnik Javadoc'un biraz daha yeni bir sürümünü kullanmak isteyebilirsiniz (78'i oluşturmaya çalışıyorsunuz, en son sürüm 105: download.java.net/lambda/b105/docs/api/java/util/stream/… )
Mark Rotteveel

1
@GraemeMoss Yine aynı kalıbı ( IntStream.rangeClosed(1, 8).forEach(i -> methodNoArgs());) kullanabilirsiniz, ancak bu IMO'yu karıştırır ve bu durumda bir döngü gösterilir.
assylias

65

İşte geçen gün karşılaştığım başka bir teknik:

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

Collections.nCopiesArama bir oluşturur Listiçeren nVerdiğiniz her türlü değer kopyalarını. Bu durumda kutulu Integerdeğer 1'dir. Elbette aslında nöğeler içeren bir liste oluşturmaz ; yalnızca değeri ve uzunluğu içeren "sanallaştırılmış" bir liste oluşturur ve getaralık içindeki herhangi bir çağrı yalnızca değeri döndürür. nCopiesKoleksiyonlar Çerçeve JDK 1.2 yolu geri tanıtıldı beri yöntem civarında olmuştur. Elbette, sonucundan bir akış oluşturma yeteneği Java SE 8'e eklendi.

Büyük bir anlaşma, aynı şeyi aynı sayıda satırda yapmanın başka bir yolu.

Bununla birlikte, bu teknik IntStream.generateve IntStream.iterateyaklaşımlarından daha hızlıdır ve şaşırtıcı bir şekilde yaklaşımdan da daha hızlıdır IntStream.range.

İçin iterateve generatesonuç belki de şaşırtıcı değildir. Akış çerçevesi (gerçekten, bu akışlar için Ayırıcılar), lambdaların potansiyel olarak her seferinde farklı değerler üreteceği ve sınırsız sayıda sonuç üreteceği varsayımı üzerine inşa edilmiştir. Bu, paralel bölmeyi özellikle zorlaştırır. iterateHer çağrı öncekinin sonucunu gerektirdiğinden yöntem de bu durum için problemlidir. Bu nedenle, kullanılan generateve iteratetekrarlanan sabitler oluşturmak için çok iyi olmayan akımlar .

Nispeten düşük performans rangeşaşırtıcı. Bu da sanallaştırılmıştır, bu nedenle elemanların tamamı bellekte mevcut değildir ve boyutu önceden bilinir. Bu, hızlı ve kolayca paralelleştirilebilir bir ayırıcı yapmalıdır. Ama şaşırtıcı bir şekilde pek de başarılı olmadı. Belki de nedeni, rangearalığın her bir öğesi için bir değer hesaplamak ve ardından bunun üzerinde bir işlev çağırmak zorunda olmasıdır. Ancak bu işlev girdisini görmezden gelir ve bir sabit döndürür, bu yüzden bunun satır içi ve öldürülmemiş olmasına şaşırdım.

Collections.nCopiesTeknik hiçbir ilkel uzmanlık olmadığından, değerlerini işlemek amacıyla unboxing / boks yapmak zorunda List. Değer her seferinde aynı olduğundan, temelde bir kez kutulanır ve bu kutu tüm nkopyalar tarafından paylaşılır . Kutulama / kutudan çıkarma işleminin son derece optimize edildiğinden, hatta içselleştirildiğinden şüpheleniyorum ve iyi bir şekilde satır içine alınabilir.

İşte kod:

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

Ve işte JMH sonuçları: (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

Ncopies sürümünde oldukça fazla farklılık var, ancak genel olarak rahat bir şekilde 20 kat daha hızlı görünüyor. (Yine de yanlış bir şey yaptığıma inanmaya oldukça istekli olurum.)

nCopiesTekniğin ne kadar iyi çalıştığına şaşırdım . Dahili olarak çok özel bir şey yapmaz, sanallaştırılmış listenin akışı basitçe kullanılarak gerçekleştirilir IntStream.range! Bunun hızlı ilerlemesi için özel bir ayırıcı yaratmanın gerekli olacağını ummuştum, ama şimdiden oldukça iyi görünüyor.


6
Daha az tecrübeli geliştiricilerin nCopiesaslında hiçbir şeyi kopyalamadığını ve "kopyaların" hepsinin tek bir nesneyi işaret ettiğini öğrendiklerinde kafası karışabilir veya başları belaya girebilir . Bu örnekteki kutulu bir ilkel gibi nesnenin değişmez olması her zaman güvenlidir . Bunu "bir kez kutulu" ifadenizde ima ediyorsunuz, ancak buradaki uyarıları açıkça belirtmek güzel olabilir, çünkü bu davranış otomatik kutulamaya özgü değildir.
William Price

1
Bu, bunun LongStream.rangeönemli ölçüde daha yavaş olduğu anlamına IntStream.rangemı geliyor? Bu nedenle, bir teklif vermeme IntStream(ancak LongStreamtüm tam sayı türleri için kullanın) fikrinin kaldırılmış olması iyi bir şey . Sıralı kullanım durumunda, akışı kullanmak için hiçbir neden olmadığını unutmayın: Collections.nCopies(8, 1).forEach(i -> System.out.println(i));aynı şeyi yapar, Collections.nCopies(8, 1).stream().forEach(i -> System.out.println(i));ancak daha da verimli olabilirCollections.<Runnable>nCopies(8, () -> System.out.println(1)).forEach(Runnable::run);
Holger

1
@Holger, bu testler temiz tip profil üzerinde gerçekleştirildi, bu yüzden gerçek dünyayla ilgisi yok. Muhtemelen LongStream.rangebu iki haritalar olduğundan, daha kötü performans LongFunctioniçten ederken, ncopiesüç haritalar vardır IntFunction, ToLongFunctionve LongFunctionböylece tüm lambda'lar monomorfiktirler. Bu testi önceden kirlenmiş tip profil üzerinde (gerçek dünya durumuna daha yakın olan) çalıştırmak, bunun ncopies1,5 kat daha yavaş olduğunu gösterir .
Tagir Valeev

1
Erken Optimizasyon FTW
Rafael Bugajewski

1
Bütünlük adına, bu tekniklerin her ikisini de düz eski bir fordöngü ile karşılaştıran bir kıyaslama görmek güzel olurdu . Çözümünüz Streamkoddan daha hızlı olsa da, tahminimce bir fordöngü bunlardan herhangi birini önemli bir farkla geçecektir.
typeracer

35

Bütünlük için ve ayrıca kendime yardım edemediğim için :)

Sınırlı bir sabit dizisi oluşturmak, Haskell'de göreceğinize oldukça yakındır, yalnızca Java düzeyinde ayrıntıyla.

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

() -> 1sadece 1'ler oluşturur, bu amaçlanıyor mu? Böylece çıktı olacaktır 1 1 1 1 1 1 1 1.
Christian Ullenboom

4
Evet, OP'nin ilk Haskell örneğine göre take 8 (repeat 1). asylias hemen hemen tüm diğer davaları kapsamıştır.
clstrfsck

3
Stream<T>generateaynı şekilde sınırlandırılabilen başka türden sonsuz bir akışı elde etmek için genel bir yönteme de sahiptir .
zstewart

11

Tekrarlama işlevi bir yerde şöyle tanımlanır:

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

Şimdi ve sonra şu şekilde kullanabilirsiniz, örneğin:

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

Haskell'in almak ve eşdeğer

take 8 (repeat 1)

Yazabilirsin

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

2
Bu harika. Ancak, iterasyon numarasını, olarak değiştirip sonra kullanarak geri Runnablevermek için değiştirdim . Function<Integer, ?>f.apply(i)
Fons

0

Bu, zamanlar işlevini uygulamak için benim çözümüm. Ben gençim, bu yüzden ideal olmayabileceğini kabul ediyorum, her ne sebeple olursa olsun bunun iyi bir fikir olup olmadığını duymaktan memnuniyet duyarım.

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

İşte bazı örnek kullanım:

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

times(3, greet, "Hello World!");
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.