Utilizzo di For-each loop vs operation funzionale vs Java8 stream

0

Quali sono i vantaggi in termini di velocità di elaborazione effettiva (la verosimiglianza del codice viene messa da parte) e per quale caso ciascuno sarebbe consigliato, come un buon modello di progettazione? Mi piacerebbe sapere quanto sopra in generale, ma anche se aiuta nel contesto del seguente che può essere riscritto in tutti e 3 i modi

static void parallelTestsExecution(String name,Runnable ... runnables){
    ArrayList<BaseTestThread> list = new ArrayList();
    int counter =0; // use local variable counter to name threads accordingly
    for(Runnable runnable : runnables){//runnable contains test logic
        counter++;
        BaseTestThread t = new BaseTestThread(runnable);
        t.setName(name+" #"+counter);
        list.add(t);
    }

    for(BaseTestThread thread : list ){//must first start all threads
        thread.start();
    }

    for(BaseTestThread thread : list ){//start joins only after all have started
        try {
            thread.join();
        } catch (InterruptedException ex) {
            throw new RuntimeException("Interrupted - This should normally not happen.");
        }
    }

    for(BaseTestThread thread : list ){
        assertTrue("Thread: "+thread.getName() , thread.isSuccessfull());
    }

}

vs

 static void parallelTestsExecution(String name,Runnable ... runnables){
    ArrayList<BaseTestThread> list = new ArrayList();
    int counter =0; // use local variable counter to name threads accordingly
    for(Runnable runnable : runnables){
        counter++;
        BaseTestThread t = new BaseTestThread(runnable);//BaseTestThread extends thread
        t.setName(name+" #"+counter);
        list.add(t);
    }

    list.forEach((thread) -> {
        thread.start();
    });

    list.forEach((thread) -> {
        try {
            thread.join();
        } catch (InterruptedException ex) {
            throw new RuntimeException("Interrupted - This should normally not happen.");
        }
    });

    list.forEach((thread) -> {
        assertTrue("Thread: "+thread.getName() , thread.isSuccessfull());
    });

}

vs Compilare l'elenco con i thread associati a un eseguibile e utilizzabile list.Stream () ... ecc (attualmente non abbastanza sicuro di questo per pubblicare qualcosa di più concreto)

Sono specificamente interessato ad apprendere come funzionano ciascuno di questi meccanismi diversi e quali sono i loro punti di forza / debolezza per applicare il mio giudizio migliore ogni volta che è necessario per decidere cosa usare come best practice. La questione non riguarda la micro-ottimizzazione in generale e l'importanza di essa.

    
posta Leon 15.06.2017 - 14:35
fonte

1 risposta

2

Prestazioni

Nel tuo esempio, non riesco a immaginare alcuna significativa differenza di prestazioni tra i diversi stili, poiché in entrambi i casi il lavoro viene svolto in N thread paralleli. La creazione e la gestione dei thread domineranno enormemente il tempo di esecuzione, anche se i thread stessi non fanno nulla.

I diversi stili si applicano solo al modo in cui crei i thread, avvia i thread e attendi il completamento dei thread.

Analogie

Entrambi gli stili hanno in comune il modo base di iterare. Sia lo stile di loop Java5 che l'implementazione (predefinita) dello stile lambda Java8 utilizzano l'iteratore () dell'array / list / collection e i suoi metodi next () e hasNext ().

Le differenze

Ciò che è diverso è il modo in cui il corpo viene eseguito.

Il ciclo di stile Java5 utilizza immediatamente il tuo codice come corpo del loop (all'interno del tuo metodo parallelTestsExecution ()), mentre lo stile lambda Java8 incapsula la funzionalità nel metodo accept () di un oggetto Acceptor anonimo chiamato per ogni iterazione. [Dai un'occhiata ai file di classe creati dalla tua fonte. Con il secondo stile, ci saranno alcuni xxx $ 1 e classi simili per i suddetti Accettatori.]

Quindi, c'è un sovraccarico quando si usa lo stile lambda Java8, ma immagino che il compilatore Hotspot possa ottimizzare questo per correre veloce come un normale ciclo.

Ma questo stile influenza l'accessibilità delle variabili locali che risiedono al di fuori del corpo del ciclo. Con i loop Java5, sono completamente accessibili, letti e scritti, perché sono nello stesso metodo. Con Java8 lambda, puoi solo leggere i locali e solo se sono dichiarati definitivi [* vedi sotto] - il tuo corpo del loop risiede in una classe diversa, e l'accesso alle variabili dal tuo metodo parallelTestsExecution () è impossibile per la JVM. Il trucco che utilizza il compilatore consiste nel passare il valore della variabile nel costruttore nascosto della classe Acceptor anonima e fare in modo che la classe Acceptor memorizzi questa copia come campo di istanza. Quindi, non puoi trasformare il tuo primo ciclo in stile lambda perché modifichi la variabile "contatore" all'interno della quale è impossibile da una classe anonima.

Influisce anche sul debug. Supponiamo di aver impostato un breakpoint sulla linea "thread.start ();". Nel primo approccio, ti troverai immediatamente nel tuo metodo parallelTestsExecution (). Con lambdas, vedrai un metodo accept () all'interno di un metodo forEach () all'interno del tuo metodo parallelTestsExecution (), magari con un po 'di colla intermedia.

[*] Commento su "final": come ha correttamente commentato Jules, la dichiarazione "finale" non è più necessaria. Se si utilizza una variabile locale all'interno di una lambda, deve comunque essere effettivamente definitiva. Questo non ti dà più possibilità, ti fa semplicemente risparmiare la parola "finale".

    
risposta data 23.06.2017 - 23:11
fonte

Leggi altre domande sui tag