JavaRush /Java Blog /Random-IT /Traduzione del libro. Programmazione funzionale in Java. ...
timurnav
Livello 21

Traduzione del libro. Programmazione funzionale in Java. Capitolo 1

Pubblicato nel gruppo Random-IT
Sarò felice di aiutarti a trovare errori e migliorare la qualità della traduzione. Traduco per migliorare le mie competenze in lingua inglese e, se leggi e cerchi errori di traduzione, migliorerai ancora meglio di me. L'autore del libro scrive che il libro presuppone molta esperienza di lavoro con Java; ad essere sincero, io stesso non sono particolarmente esperto, ma ho capito il materiale del libro. Il libro tratta alcune teorie difficili da spiegare con le dita. Se ci sono articoli decenti sul wiki, fornirò i collegamenti ad essi, ma per una migliore comprensione ti consiglio di cercarli tu stesso su Google. buona fortuna a tutti. :) Per coloro che vogliono correggere la mia traduzione, così come per coloro che la trovano troppo scarsa per leggere in russo, potete scaricare il libro originale qui . Sommario Capitolo 1 Ciao, espressioni Lambda - attualmente in lettura Capitolo 2 Utilizzo delle raccolte - in sviluppo Capitolo 3 Stringhe, comparatori e filtri - in sviluppo Capitolo 4 Sviluppo con espressioni Lambda - in sviluppo Capitolo 5 Lavorare con le risorse - in sviluppo Capitolo 6 Essere pigri - in sviluppo Capitolo 7 Ottimizzazione delle risorse - in sviluppo Capitolo 8 Layout con espressioni lambda - in sviluppo Capitolo 9 Mettere tutto insieme - in sviluppo

Capitolo 1 Ciao, espressioni Lambda!

Il nostro codice Java è pronto per trasformazioni notevoli. Le attività quotidiane che svolgiamo diventano più semplici, più facili e più espressive. Il nuovo modo di programmare Java è stato utilizzato per decenni in altri linguaggi. Con queste modifiche a Java possiamo scrivere codice conciso, elegante ed espressivo con meno errori. Possiamo usarlo per applicare facilmente standard e implementare modelli di progettazione comuni con meno righe di codice. In questo libro esploriamo lo stile funzionale della programmazione utilizzando semplici esempi di problemi che svolgiamo ogni giorno. Prima di immergerci in questo stile elegante e in questo nuovo modo di sviluppare software, vediamo perché è migliore.
Cambia il tuo modo di pensare
Lo stile imperativo è ciò che Java ci ha dato sin dall'inizio del linguaggio. Questo stile suggerisce di descrivere a Java ogni passaggio di ciò che vogliamo che il linguaggio faccia e quindi di assicurarci semplicemente che tali passaggi siano seguiti fedelmente. Ha funzionato benissimo, ma è ancora di basso livello. Il codice risultava troppo prolisso e spesso volevamo un linguaggio un po' più intelligente. Potremmo quindi dirlo in modo dichiarativo: cosa vogliamo e non approfondire come farlo. Grazie agli sviluppatori, Java ora può aiutarci a farlo. Diamo un'occhiata ad alcuni esempi per comprendere i vantaggi e le differenze tra questi approcci.
Il solito modo
Cominciamo con le basi familiari per vedere i due paradigmi in azione. Utilizza un metodo imperativo per cercare Chicago nella raccolta delle città: gli elenchi in questo libro mostrano solo frammenti di codice. boolean found = false; for(String city : cities) { if(city.equals("Chicago")) { found = true; break; } } System.out.println("Found chicago?:" + found); La versione imperativa del codice è rumorosa (cosa c'entra questa parola?) e di basso livello, ci sono diverse parti mutabili. Per prima cosa creiamo questo puzzolente flag booleano chiamato find e poi iteriamo su ogni elemento della raccolta. Se troviamo la città che stiamo cercando, impostiamo il flag su true e interrompiamo il loop. Infine stampiamo il risultato della nostra ricerca sulla console.
C'è un modo migliore
Come programmatori Java attenti, uno sguardo rapido a questo codice può trasformarlo in qualcosa di più espressivo e più facile da leggere, come questo: System.out.println("Found chicago?:" + cities.contains("Chicago")); Ecco un esempio dello stile dichiarativo: il metodo contiene() ci aiuta ad arrivare direttamente a ciò di cui abbiamo bisogno.
Cambiamenti effettivi
Queste modifiche apporteranno una discreta quantità di miglioramenti al nostro codice:
  • Nessun problema con le variabili mutabili
  • Le iterazioni del loop sono nascoste sotto il cofano
  • Meno confusione di codici
  • Maggiore chiarezza del codice, focalizza l'attenzione
  • Meno impedenza; il codice segue da vicino l'intento aziendale
  • Meno possibilità di errore
  • Più facile da comprendere e supportare
Oltre i casi semplici
Questo era un semplice esempio di funzione dichiarativa che verifica la presenza di un elemento in una raccolta; è stata utilizzata per molto tempo in Java. Ora immagina di non dover scrivere codice imperativo per operazioni più avanzate come l'analisi di file, l'utilizzo di database, l'invio di richieste di servizi Web, la creazione di multithreading, ecc. Java ora rende possibile scrivere codice conciso ed elegante che rende più difficile commettere errori, non solo nelle operazioni semplici, ma nell'intera applicazione.
Alla vecchia maniera
Diamo un'occhiata a un altro esempio. Stiamo creando una collezione con i prezzi e proveremo diversi modi per calcolare la somma di tutti i prezzi scontati. Supponiamo che ci venga chiesto di sommare tutti i prezzi il cui valore supera i 20 dollari, con uno sconto del 10%. Facciamolo prima nel solito modo Java. Questo codice dovrebbe esserci molto familiare: per prima cosa creiamo una variabile mutabile totalOfDiscountedPrices in cui memorizzeremo il valore risultante. Quindi passiamo in rassegna la raccolta dei prezzi, selezioniamo i prezzi superiori a $ 20, otteniamo il prezzo scontato e aggiungiamo quel valore a totalOfDiscountedPrices . Alla fine visualizziamo la somma di tutti i prezzi tenendo conto dello sconto. Di seguito è riportato ciò che viene visualizzato sulla console final List prices = Arrays.asList( new BigDecimal("10"), new BigDecimal("30"), new BigDecimal("17"), new BigDecimal("20"), new BigDecimal("15"), new BigDecimal("18"), new BigDecimal("45"), new BigDecimal("12")); BigDecimal totalOfDiscountedPrices = BigDecimal.ZERO; for(BigDecimal price : prices) { if(price.compareTo(BigDecimal.valueOf(20)) > 0) totalOfDiscountedPrices = totalOfDiscountedPrices.add(price.multiply(BigDecimal.valueOf(0.9))); } System.out.println("Total of discounted prices: " + totalOfDiscountedPrices);
Totale prezzi scontati: 67,5
Funziona, ma il codice sembra confuso. Ma non è colpa nostra, abbiamo utilizzato quello che c'era a disposizione. Il codice è di livello piuttosto basso: soffre di un'ossessione per i primitivi (cercalo su Google, cose interessanti) e va contro il principio di responsabilità unica . Quelli di noi che lavorano a casa dovrebbero tenere questo codice lontano dagli occhi dei bambini che aspirano a diventare programmatori, potrebbe allarmare le loro fragili menti, essere preparati alla domanda "È questo quello che devi fare per sopravvivere?"
C'è un modo migliore, un altro
Ora possiamo fare meglio, molto meglio. Il nostro codice potrebbe somigliare a un requisito di specifica. Ciò ci aiuterà a ridurre il divario tra le esigenze aziendali e il codice che le implementa, riducendo ulteriormente la probabilità che i requisiti vengano interpretati erroneamente. Invece di creare una variabile e poi modificarla ripetutamente, lavoriamo a un livello di astrazione più elevato, come nel seguente elenco. final BigDecimal totalOfDiscountedPrices = prices.stream() .filter(price -> price.compareTo(BigDecimal.valueOf(20)) > 0) .map(price -> price.multiply(BigDecimal.valueOf(0.9))) .reduce(BigDecimal.ZERO, BigDecimal::add); System.out.println("Total of discounted prices: " + totalOfDiscountedPrices); Leggiamolo ad alta voce: il filtro prezzo è maggiore di 20, mappa (crea coppie "chiave" "valore") utilizzando la chiave "prezzo", prezzo compreso lo sconto, quindi aggiungili
- per commento del traduttore si intendono le parole che appaiono nella tua testa mentre leggi il codice .filter(price -> price.compareTo(BigDecimal.valueOf(20)) > 0)
Il codice viene eseguito insieme nella stessa sequenza logica che abbiamo letto. Il codice è stato abbreviato, ma abbiamo utilizzato un sacco di novità da Java 8. Per prima cosa abbiamo chiamato il metodo stream() nel listino prezzi . Ciò apre le porte a un iteratore personalizzato con un ricco set di funzionalità utili di cui parleremo più avanti. Invece di scorrere direttamente tutti i valori nel listino prezzi , utilizziamo diversi metodi speciali come filter() e map() . A differenza dei metodi utilizzati in Java e JDK, questi metodi accettano una funzione anonima, un'espressione lambda, come parametro tra parentesi. Lo studieremo più in dettaglio più avanti. Chiamando il metodo reduce() , calcoliamo la somma dei valori (prezzo scontato) ottenuti nel metodo map() . Il ciclo viene nascosto nello stesso modo in cui lo era quando si utilizzava il metodo contiene() . I metodi filter() e map() sono tuttavia ancora più complessi. Per ogni prezzo nel listino prezzi , chiamano la funzione lambda passata e la salvano in una nuova raccolta. Il metodo reduce() viene chiamato su questa raccolta per produrre il risultato finale. Di seguito è riportato ciò che viene visualizzato sulla console
Totale prezzi scontati: 67,5
I cambiamenti
Di seguito le modifiche rispetto alla modalità consueta:
  • Il codice è gradevole alla vista e non ingombrante.
  • Nessuna operazione di basso livello
  • È più facile migliorare o cambiare logica
  • L'iterazione è controllata da una libreria di metodi
  • Valutazione efficiente e lazy loop
  • Più facile da parallelizzare secondo necessità
Discuteremo più avanti di come Java fornisce questi miglioramenti.
Lambda in soccorso :)
Lambda è la chiave funzionale per liberarci dai fastidi della programmazione imperativa. Cambiando il modo in cui programmiamo, con le ultime funzionalità di Java, possiamo scrivere codice che non solo è elegante e conciso, ma anche meno soggetto a errori, più efficiente e più facile da ottimizzare, migliorare e rendere multi-thread.
Vinci alla grande con la programmazione funzionale
Lo stile di programmazione funzionale ha un rapporto segnale-rumore più elevato ; Scriviamo meno righe di codice, ma ogni riga o espressione esegue più funzionalità. Abbiamo guadagnato poco dalla versione funzionale del codice rispetto all'imperativo:
  • Abbiamo evitato modifiche indesiderate o riassegnazioni di variabili, che sono fonte di errori e rendono difficile l'elaborazione simultanea di codice da thread diversi. Nella versione imperativa, impostiamo valori diversi per la variabile totalOfDiscountedPrices durante il ciclo . Nella versione funzionale non vi è alcuna modifica esplicita nella variabile nel codice. Meno modifiche portano a meno bug nel codice.
  • La versione funzionale del codice è più semplice da parallelizzare. Anche se i calcoli nel metodo map() fossero lunghi, possiamo eseguirli in parallelo senza timore di nulla. Se accediamo a codice in stile imperativo da thread diversi, dovremo preoccuparci di modificare contemporaneamente la variabile totalOfDiscountedPrices . Nella versione funzionale accediamo alla variabile solo dopo che sono state apportate tutte le modifiche, questo ci libera dal preoccuparci della thread safety del codice.
  • Il codice è più espressivo. Invece di eseguire il codice in più passaggi - creando e inizializzando una variabile con un valore fittizio, scorrendo l'elenco dei prezzi, aggiungendo prezzi scontati alla variabile e così via - chiediamo semplicemente al metodo map() della lista di restituire un'altra lista dei prezzi scontati e sommarli .
  • Lo stile funzionale è più conciso: sono necessarie meno righe di codice rispetto alla versione imperativa. Un codice più compatto significa meno cose da scrivere, meno da leggere e più facile da mantenere.
  • La versione funzionale del codice è intuitiva e di facile comprensione, una volta conosciuta la sua sintassi. Il metodo map() applica la funzione passata (che calcola il prezzo scontato) ad ogni elemento della collezione e produce una collezione con il risultato, come possiamo vedere nell'immagine qui sotto.

Immagine Figura 1 - il metodo map applica la funzione passata a ciascun elemento della raccolta
Con il supporto delle espressioni lambda, possiamo sfruttare appieno la potenza dello stile funzionale di programmazione in Java. Se padroneggiamo questo stile, possiamo creare un codice più espressivo e più conciso con meno modifiche ed errori. In precedenza, uno dei principali vantaggi di Java era il supporto del paradigma orientato agli oggetti. E lo stile funzionale non contraddice l'OOP. Vera eccellenza nel passaggio dalla programmazione imperativa a quella dichiarativa. Con Java 8 possiamo combinare in modo abbastanza efficace la programmazione funzionale con uno stile orientato agli oggetti. Possiamo continuare ad applicare lo stile OO agli oggetti, al loro ambito, stato e relazioni. Inoltre, possiamo modellare il comportamento e lo stato di cambiamento, i processi aziendali e l'elaborazione dei dati come una serie di insiemi di funzioni.
Perché codificare in stile funzionale?
Abbiamo visto i vantaggi generali dello stile di programmazione funzionale, ma vale la pena imparare questo nuovo stile? Si tratterà di un piccolo cambiamento nel linguaggio o cambierà le nostre vite? Dobbiamo ottenere risposte a queste domande prima di sprecare tempo ed energie. Scrivere il codice Java non è così difficile; la sintassi del linguaggio è semplice. Ci sentiamo a nostro agio con librerie e API familiari. Ciò che veramente ci richiede di impegnarci per scrivere e mantenere il codice sono le tipiche applicazioni aziendali in cui utilizziamo Java per lo sviluppo. Dobbiamo assicurarci che i colleghi programmatori chiudano le connessioni al database al momento giusto, che non lo trattengano o non eseguano transazioni più a lungo del necessario, che catturino le eccezioni completamente e al livello corretto, che applichino e rilascino i blocchi correttamente. ...questo foglio può essere continuato per molto tempo. Ciascuno degli argomenti di cui sopra da solo non ha peso, ma insieme, se combinato con le complessità di implementazione intrinseche, diventa travolgente, dispendioso in termini di tempo e difficile da implementare. E se potessimo incapsulare queste complessità in minuscoli pezzi di codice in grado di gestirle bene? Allora non spenderemmo costantemente le nostre energie per implementare gli standard. Ciò fornirebbe un vantaggio significativo, quindi diamo un'occhiata a come uno stile funzionale può aiutare.
chiede Joe
Un codice breve* significa semplicemente meno lettere del codice?
* stiamo parlando della parola conciso , che caratterizza lo stile funzionale del codice utilizzando le espressioni lambda
In questo contesto, il codice vuole essere conciso, senza fronzoli e ridotto all’impatto diretto per trasmettere in modo più efficace l’intento. Questi sono vantaggi di vasta portata. Scrivere codice è come mettere insieme gli ingredienti: renderlo conciso è come aggiungervi della salsa. A volte è necessario uno sforzo maggiore per scrivere tale codice. Meno codice da leggere, ma rende il codice più trasparente. È importante mantenere chiaro il codice quando lo si accorcia. Il codice conciso è simile ai trucchi di progettazione. Questo codice richiede meno di ballare con il tamburello. Ciò significa che possiamo implementare rapidamente le nostre idee e andare avanti se funzionano e abbandonarle se non sono all’altezza delle aspettative.
Iterazioni sugli steroidi
Utilizziamo gli iteratori per elaborare elenchi di oggetti, nonché per lavorare con set e mappe. Gli iteratori che utilizziamo in Java ci sono familiari; sebbene siano primitivi, non sono semplici. Non solo occupano numerose righe di codice, ma sono anche piuttosto difficili da scrivere. Come iteriamo attraverso tutti gli elementi di una raccolta? Potremmo usare un ciclo for. Come selezioniamo alcuni elementi della collezione? Utilizzando lo stesso ciclo for, ma utilizzando alcune variabili mutabili aggiuntive che devono essere confrontate con qualcosa della raccolta. Quindi, dopo aver selezionato un valore specifico, come possiamo eseguire operazioni su un singolo valore, come il minimo, il massimo o un valore medio? Ancora cicli, ancora nuove variabili. Questo ricorda il proverbio, non si vedono gli alberi a causa della foresta (l'originale usa un gioco di parole legato alle iterazioni e significa “Tutto si accetta, ma non tutto riesce” - nota del traduttore). jdk ora fornisce iteratori interni per varie istruzioni: uno per semplificare il loop, uno per associare la dipendenza del risultato richiesta, uno per filtrare i valori di output, uno per restituire i valori e diverse funzioni utili per ottenere minimo, massimo, medie, ecc. Inoltre, le funzionalità di queste operazioni possono essere combinate molto facilmente, in modo da poter combinare diversi insiemi di esse per implementare la logica aziendale con maggiore facilità e meno codice. Una volta terminato, il codice sarà più semplice da comprendere poiché crea una soluzione logica nella sequenza richiesta dal problema. Vedremo alcuni esempi di tale codice nel Capitolo 2 e più avanti in questo libro.
Applicazione di algoritmi
Gli algoritmi guidano le applicazioni aziendali. Ad esempio, dobbiamo fornire un'operazione che richieda il controllo dell'autorizzazione. Dovremo assicurarci che le transazioni vengano completate rapidamente e che i controlli vengano completati correttamente. Tali compiti sono spesso ridotti a un metodo molto ordinario, come nell'elenco seguente: Transaction transaction = getFromTransactionFactory(); //... Операция выполняющаяся во время транзакции... checkProgressAndCommitOrRollbackTransaction(); UpdateAuditTrail(); Ci sono due problemi con questo approccio. In primo luogo, ciò porta spesso a un raddoppio dello sforzo di sviluppo, che a sua volta comporta un aumento dei costi di manutenzione dell'applicazione. In secondo luogo, è molto facile non notare le eccezioni che potrebbero essere inserite nel codice di questa applicazione, mettendo così a repentaglio l'esecuzione della transazione e il passaggio degli assegni. Possiamo usare un vero e proprio blocco try-finally, ma ogni volta che qualcuno tocca questo codice, dovremo ricontrollare che la logica del codice non sia stata infranta. Altrimenti potremmo abbandonare la fabbrica e ribaltare l’intero codice. Invece di ricevere transazioni, potremmo inviare il codice di elaborazione a una funzione ben gestita, come il codice riportato di seguito. runWithinTransaction((Transaction transaction) -> { //... Операция выполняющаяся во время транзакции... }); Queste piccole modifiche si traducono in enormi risparmi. All'algoritmo per il controllo dello stato e dei controlli dell'applicazione viene assegnato un nuovo livello di astrazione e viene incapsulato utilizzando il metodo runWithinTransaction() . In questo metodo inseriamo un pezzo di codice che dovrebbe essere eseguito nel contesto di una transazione. Non dobbiamo più preoccuparci di dimenticare di fare qualcosa o di aver colto l'eccezione nel posto giusto. Le funzioni algoritmiche si occupano di questo. Questo problema sarà discusso più dettagliatamente nel capitolo 5.
Estensioni dell'algoritmo
Gli algoritmi vengono utilizzati sempre più spesso, ma affinché possano essere pienamente utilizzati nello sviluppo di applicazioni aziendali, sono necessari modi per estenderli.
Commenti
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION