Java 8 fornisce un buon modo per ripetere un valore o una funzione?


118

In molte altre lingue, ad es. Haskell, è facile ripetere un valore o una funzione più volte, ad es. per ottenere un elenco di 8 copie del valore 1:

take 8 (repeat 1)

ma non l'ho ancora trovato in Java 8. Esiste una funzione del genere nel JDK di Java 8?

O in alternativa qualcosa di equivalente a un intervallo come

[1..8]

Sembrerebbe un ovvio sostituto di una dichiarazione dettagliata in Java come

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

avere qualcosa di simile

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

anche se questo particolare esempio non sembra molto più conciso in realtà ... ma si spera che sia più leggibile.


2
Hai studiato l' API Streams ? Questa dovrebbe essere la soluzione migliore per quanto riguarda il JDK. Ha una funzione di portata , questo è quello che ho trovato finora.
Marko Topolnik

1
@MarkoTopolnik La classe Streams è stata rimossa (più precisamente è stata divisa tra molte altre classi e alcuni metodi sono stati completamente rimossi).
assylias

3
Chiami un ciclo for prolisso! È un bene che tu non fossi in giro ai tempi dei Cobol. Ci sono volute più di 10 dichiarazioni dichiarative in Cobol per visualizzare numeri crescenti. I giovani di questi tempi non apprezzano quanto sono bravi.
Gilbert Le Blanc

1
La verbosità di @GilbertLeBlanc non ha nulla a che fare con questo. I loop non sono componibili, gli stream lo sono. I loop portano a ripetizioni inevitabili, mentre gli stream ne consentono il riutilizzo. In quanto tali, gli stream sono un'astrazione quantitativamente migliore dei loop e dovrebbero essere preferiti.
Alain O'Dea

2
@GilbertLeBlanc e abbiamo dovuto programmare a piedi nudi, sulla neve.
Dawood ibn Kareem

Risposte:


155

Per questo esempio specifico, potresti fare:

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

Se hai bisogno di un passaggio diverso da 1, puoi utilizzare una funzione di mappatura, ad esempio, per un passaggio di 2:

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

Oppure crea un'iterazione personalizzata e limita la dimensione dell'iterazione:

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

4
Le chiusure trasformeranno completamente il codice Java, in meglio. Non
vedo l'

1
@jwenting Dipende davvero, tipicamente con roba GUI (Swing o JavaFX), che rimuove un sacco di boiler plate a causa di classi anonime.
assylias

8
@jwenting Per chiunque abbia esperienza in FP, il codice che ruota attorno a funzioni di ordine superiore è una pura vittoria. Per chiunque non abbia quell'esperienza, è tempo di migliorare le tue abilità --- o rischiare di essere lasciato indietro nella polvere.
Marko Topolnik

2
@MarkoTopolnik Potresti voler utilizzare una versione leggermente più recente di javadoc (stai indicando la build 78, l'ultima è la build 105: download.java.net/lambda/b105/docs/api/java/util/stream/… )
Mark Rotteveel

1
@GraemeMoss Potresti ancora usare lo stesso pattern ( IntStream.rangeClosed(1, 8).forEach(i -> methodNoArgs());) ma confonde la cosa IMO e in quel caso sembra indicato un ciclo.
assylias

65

Ecco un'altra tecnica che ho incontrato l'altro giorno:

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

La Collections.nCopieschiamata crea una copia Listcontenente nqualsiasi valore fornito. In questo caso è il Integervalore boxed 1. Ovviamente in realtà non crea una lista con nelementi; crea un elenco "virtualizzato" che contiene solo il valore e la lunghezza, e qualsiasi chiamata a getall'interno dell'intervallo restituisce semplicemente il valore. Il nCopiesmetodo esiste da quando il framework delle raccolte è stato introdotto in JDK 1.2. Ovviamente, la possibilità di creare un flusso dal suo risultato è stata aggiunta in Java SE 8.

Grande affare, un altro modo per fare la stessa cosa in circa lo stesso numero di righe.

Tuttavia, questa tecnica è più veloce dell'approccio IntStream.generatee IntStream.iteratee, sorprendentemente, è anche più veloce IntStream.rangedell'approccio.

Perché iteratee generateil risultato forse non è troppo sorprendente. Il framework dei flussi (in realtà, gli Spliterator per questi flussi) si basa sul presupposto che i lambda genereranno potenzialmente valori diversi ogni volta e che genereranno un numero illimitato di risultati. Ciò rende la divisione parallela particolarmente difficile. Il iteratemetodo è problematico anche in questo caso perché ogni chiamata richiede il risultato di quella precedente. Quindi i flussi che utilizzano generatee iteratenon funzionano molto bene per la generazione di costanti ripetute.

La prestazione relativamente scarsa di rangeè sorprendente. Anche questo è virtualizzato, quindi gli elementi in realtà non esistono tutti in memoria e la dimensione è nota in anticipo. Questo dovrebbe creare uno spliterator veloce e facilmente parallelizzabile. Ma sorprendentemente non è andata molto bene. Forse il motivo è che rangedeve calcolare un valore per ogni elemento dell'intervallo e quindi chiamare una funzione su di esso. Ma questa funzione ignora semplicemente il suo input e restituisce una costante, quindi sono sorpreso che non sia inline e ucciso.

La Collections.nCopiestecnica deve fare boxing / unboxing per poter gestire i valori, poiché non ci sono specializzazioni primitive di List. Poiché il valore è lo stesso ogni volta, è fondamentalmente inscatolato una volta e quella casella è condivisa da tutte le ncopie. Sospetto che il pugilato / unboxing sia altamente ottimizzato, persino intrinseco e possa essere integrato bene.

Ecco il codice:

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

Ed ecco i risultati 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

C'è una discreta quantità di varianza nella versione ncopies, ma nel complesso sembra comodamente 20 volte più veloce della versione range. (Sarei abbastanza disposto a credere di aver fatto qualcosa di sbagliato, però.)

Sono sorpreso di quanto bene nCopiesfunzioni la tecnica. Internamente non fa molto speciale, con il flusso dell'elenco virtualizzato che viene semplicemente implementato usando IntStream.range! Mi aspettavo che sarebbe stato necessario creare uno spliteratore specializzato per farlo funzionare velocemente, ma sembra già essere abbastanza buono.


6
Gli sviluppatori meno esperti potrebbero essere confusi o finire nei guai quando scoprono che in nCopiesrealtà non copia nulla e le "copie" puntano tutte a quell'unico oggetto. È sempre sicuro se quell'oggetto è immutabile , come una primitiva in scatola in questo esempio. Alludi a questo nella tua dichiarazione "boxed once", ma potrebbe essere bello richiamare esplicitamente le avvertenze qui perché quel comportamento non è specifico dell'auto-boxing.
William Price

1
Quindi questo implica che LongStream.rangeè significativamente più lento di IntStream.range? Quindi è una buona cosa che l'idea di non offrire un IntStream(ma l'uso LongStreamper tutti i tipi interi) sia stata abbandonata. Nota che per il caso d'uso sequenziale, non c'è affatto un motivo per usare lo streaming: Collections.nCopies(8, 1).forEach(i -> System.out.println(i));fa lo stesso Collections.nCopies(8, 1).stream().forEach(i -> System.out.println(i));ma potrebbe essere ancora più efficienteCollections.<Runnable>nCopies(8, () -> System.out.println(1)).forEach(Runnable::run);
Holger

1
@Holger, questi test sono stati eseguiti su un profilo di tipo pulito, quindi non sono correlati al mondo reale. Probabilmente LongStream.rangefunziona peggio, perché ha due mappe con LongFunctiondentro, mentre ncopiesha tre mappe con IntFunction, ToLongFunctione LongFunction, quindi tutti i lambda sono monomorfi. L'esecuzione di questo test su un profilo di tipo pre-inquinato (che è più vicino al caso del mondo reale) mostra che ncopiesè 1,5 volte più lento.
Tagir Valeev

1
Ottimizzazione prematura FTW
Rafael Bugajewski

1
Per ragioni di completezza, sarebbe bello vedere un benchmark che confronta entrambe queste tecniche con un semplice vecchio forloop. Sebbene la tua soluzione sia più veloce del Streamcodice, la mia ipotesi è che un forciclo potrebbe battere uno di questi con un margine significativo.
typeracer

35

Per completezza, e anche perché non ho potuto farne a meno :)

La generazione di una sequenza limitata di costanti è abbastanza simile a ciò che vedresti in Haskell, solo con verbosità di livello Java.

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

() -> 1genererebbe solo 1, è inteso? Quindi l'output sarebbe 1 1 1 1 1 1 1 1.
Christian Ullenboom

4
Sì, secondo il primo esempio Haskell dell'OP take 8 (repeat 1). assylias ha praticamente coperto tutti gli altri casi.
clstrfsck

3
Stream<T>ha anche un generatemetodo generico per ottenere un flusso infinito di un altro tipo, che può essere limitato allo stesso modo.
zstewart

11

Una volta che una funzione di ripetizione è da qualche parte definita come

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

Puoi usarlo di tanto in tanto in questo modo, ad esempio:

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

Per ottenere e equivalente a Haskell's

take 8 (repeat 1)

Potresti scrivere

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

2
Questo è fantastico. Tuttavia l'ho modificato per fornire il numero dell'iterazione, cambiando Runnablein Function<Integer, ?>e quindi utilizzando f.apply(i).
Fons

0

Questa è la mia soluzione per implementare la funzione dei tempi. Sono un junior quindi ammetto che potrebbe non essere l'ideale, sarei felice di sapere se questa non è una buona idea per qualsiasi motivo.

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

Ecco alcuni esempi di utilizzo:

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

times(3, greet, "Hello World!");
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.