Perché è necessario eseguire già l'aggiornamento a Java 8

Pubblicato: 2022-03-11

La versione più recente della piattaforma Java, Java 8, è stata rilasciata più di un anno fa. Molte aziende e sviluppatori stanno ancora lavorando con le versioni precedenti, il che è comprensibile, poiché ci sono molti problemi con la migrazione da una versione della piattaforma all'altra. Anche così, molti sviluppatori stanno ancora avviando nuove applicazioni con vecchie versioni di Java. Ci sono pochissime buone ragioni per farlo, perché Java 8 ha apportato alcuni importanti miglioramenti al linguaggio.

Ci sono molte nuove funzionalità in Java 8. Ti mostrerò alcune delle più utili e interessanti:

  • Espressioni Lambda
  • Stream API per lavorare con le raccolte
  • Concatenamento di attività asincrono con CompletableFuture
  • API Time nuova di zecca

Espressioni Lambda

Un lambda è un blocco di codice a cui è possibile fare riferimento e passare a un altro pezzo di codice per l'esecuzione futura una o più volte. Ad esempio, le funzioni anonime in altre lingue sono lambda. Come le funzioni, i lambda possono essere passati argomenti al momento della loro esecuzione, modificandone i risultati. Java 8 ha introdotto le espressioni lambda , che offrono una semplice sintassi per creare e utilizzare le lambda.

Vediamo un esempio di come questo può migliorare il nostro codice. Qui abbiamo un semplice comparatore che confronta due valori Integer dal loro modulo 2:

 class BinaryComparator implements Comparator<Integer>{ @Override public int compare(Integer i1, Integer i2) { return i1 % 2 - i2 % 2; } }

Un'istanza di questa classe potrebbe essere chiamata, in futuro, nel codice in cui è necessario questo comparatore, in questo modo:

 ... List<Integer> list = ...; Comparator<Integer> comparator = new BinaryComparator(); Collections.sort(list, comparator); ...

La nuova sintassi lambda ci consente di farlo in modo più semplice. Ecco una semplice espressione lambda che fa la stessa cosa del metodo di compare di BinaryComparator :

 (Integer i1, Integer i2) -> i1 % 2 - i2 % 2;

La struttura ha molte somiglianze con una funzione. Tra parentesi, impostiamo un elenco di argomenti. La sintassi -> mostra che questo è un lambda. E nella parte destra di questa espressione, impostiamo il comportamento della nostra lambda.

JAVA 8 LAMBDA ESPRESSIONE

Ora possiamo migliorare il nostro esempio precedente:

 ... List<Integer> list = ...; Collections.sort(list, (Integer i1, Integer i2) -> i1 % 2 - i2 % 2); ...

Possiamo definire una variabile con questo oggetto. Vediamo come appare:

 Comparator<Integer> comparator = (Integer i1, Integer i2) -> i1 % 2 - i2 % 2;

Ora possiamo riutilizzare questa funzionalità, in questo modo:

 ... List<Integer> list1 = ...; List<Integer> list2 = ...; Collections.sort(list1, comparator); Collections.sort(list2, comparator); ...

Si noti che in questi esempi, la lambda viene passata al metodo sort() nello stesso modo in cui viene passata l'istanza di BinaryComparator nell'esempio precedente. Come fa la JVM a interpretare correttamente la lambda?

Per consentire alle funzioni di prendere lambda come argomenti, Java 8 introduce un nuovo concetto: interfaccia funzionale . Un'interfaccia funzionale è un'interfaccia che ha un solo metodo astratto. Infatti, Java 8 tratta le espressioni lambda come un'implementazione speciale di un'interfaccia funzionale. Ciò significa che, per ricevere una lambda come argomento del metodo, il tipo dichiarato di tale argomento deve essere solo un'interfaccia funzionale.

Quando dichiariamo un'interfaccia funzionale, possiamo aggiungere la notazione @FunctionalInterface per mostrare agli sviluppatori di cosa si tratta:

 @FunctionalInterface private interface DTOSender { void send(String accountId, DTO dto); } void sendDTO(BisnessModel object, DTOSender dtoSender) { //some logic for sending... ... dtoSender.send(id, dto); ... }

Ora possiamo chiamare il metodo sendDTO , passando diversi lambda per ottenere un comportamento diverso, come questo:

 sendDTO(object, ((accountId, dto) -> sendToAndroid(accountId, dto))); sendDTO(object, ((accountId, dto) -> sendToIos(accountId, dto)));

Riferimenti metodologici

Gli argomenti Lambda ci consentono di modificare il comportamento di una funzione o di un metodo. Come possiamo vedere nell'ultimo esempio, a volte lambda serve solo a chiamare un altro metodo ( sendToAndroid o sendToIos ). Per questo caso speciale, Java 8 introduce una comoda scorciatoia: method reference . Questa sintassi abbreviata rappresenta un lambda che chiama un metodo e ha la forma objectName::methodName . Questo ci permette di rendere ancora più conciso e leggibile l'esempio precedente:

 sendDTO(object, this::sendToAndroid); sendDTO(object, this::sendToIos);

In questo caso, in this classe vengono implementati i metodi sendToAndroid e sendToIos . Possiamo anche fare riferimento ai metodi di un altro oggetto o classe.

API di flusso

Java 8 offre nuove capacità per lavorare con le Collections , sotto forma di una nuovissima Stream API. Questa nuova funzionalità è fornita dal pacchetto java.util.stream e mira a consentire un approccio più funzionale alla programmazione con le raccolte. Come vedremo, questo è possibile in gran parte grazie alla nuova sintassi lambda di cui abbiamo appena parlato.

L'API Stream offre un facile filtraggio, conteggio e mappatura delle raccolte, nonché diversi modi per ricavarne sezioni e sottoinsiemi di informazioni. Grazie alla sintassi in stile funzionale, l'API Stream consente un codice più breve ed elegante per lavorare con le raccolte.

Iniziamo con un breve esempio. Useremo questo modello di dati in tutti gli esempi:

 class Author { String name; int countOfBooks; } class Book { String name; int year; Author author; }

Immaginiamo di dover stampare tutti gli autori di una raccolta di books che hanno scritto un libro dopo il 2005. Come lo faremmo in Java 7?

 for (Book book : books) { if (book.author != null && book.year > 2005){ System.out.println(book.author.name); } }

E come lo faremmo in Java 8?

 books.stream() .filter(book -> book.year > 2005) // filter out books published in or before 2005 .map(Book::getAuthor) // get the list of authors for the remaining books .filter(Objects::nonNull) // remove null authors from the list .map(Author::getName) // get the list of names for the remaining authors .forEach(System.out::println); // print the value of each remaining element

È solo un'espressione! La chiamata al metodo stream() su qualsiasi Collection restituisce un oggetto Stream che incapsula tutti gli elementi di quella raccolta. Questo può essere manipolato con diversi modificatori dall'API Stream, come filter() e map() . Ogni modificatore restituisce un nuovo oggetto Stream con i risultati della modifica, che può essere ulteriormente manipolato. Il metodo .forEach() ci consente di eseguire alcune azioni per ogni istanza del flusso risultante.

Questo esempio mostra anche la stretta relazione tra la programmazione funzionale e le espressioni lambda. Si noti che l'argomento passato a ciascun metodo nel flusso è un lambda personalizzato o un riferimento al metodo. Tecnicamente, ogni modificatore può ricevere qualsiasi interfaccia funzionale, come descritto nella sezione precedente.

L'API Stream aiuta gli sviluppatori a guardare le raccolte Java da una nuova prospettiva. Immagina ora di aver bisogno di ottenere una Map delle lingue disponibili in ogni paese. Come sarebbe implementato in Java 7?

 Map<String, Set<String>> countryToSetOfLanguages = new HashMap<>(); for (Locale locale : Locale.getAvailableLocales()){ String country = locale.getDisplayCountry(); if (!countryToSetOfLanguages.containsKey(country)){ countryToSetOfLanguages.put(country, new HashSet<>()); } countryToSetOfLanguages.get(country).add(locale.getDisplayLanguage()); }

In Java 8, le cose sono un po' più ordinate:

 import java.util.stream.*; import static java.util.stream.Collectors.*; ... Map<String, Set<String>> countryToSetOfLanguages = Stream.of(Locale.getAvailableLocales()) .collect(groupingBy(Locale::getDisplayCountry, mapping(Locale::getDisplayLanguage, toSet())));

Il metodo collect() ci permette di raccogliere i risultati di un flusso in diversi modi. Qui possiamo vedere che prima raggruppa per paese, quindi mappa ogni gruppo per lingua. ( groupingBy() e toSet() sono entrambi metodi statici della classe Servizi di Collectors .)

API JAVA 8 STREAM

Ci sono molte altre capacità di Stream API. La documentazione completa può essere trovata qui. Consiglio di leggere ulteriormente per ottenere una comprensione più profonda di tutti i potenti strumenti che questo pacchetto ha da offrire.

Concatenamento di attività asincrono con CompletableFuture

Nel pacchetto java.util.concurrent di Java 7 è presente un'interfaccia Future<T> , che ci consente di ottenere lo stato o il risultato di alcune attività asincrone in futuro. Per utilizzare questa funzionalità, dobbiamo:

  1. Crea un ExecutorService , che gestisce l'esecuzione di attività asincrone e può generare oggetti Future per tracciarne l'avanzamento.
  2. Creare un'attività Runnable in modo asincrono.
  3. Esegui l'attività in ExecutorService , che fornirà un Future che darà accesso allo stato o ai risultati.

Per utilizzare i risultati di un'attività asincrona, è necessario monitorarne l'andamento dall'esterno, utilizzando i metodi dell'interfaccia Future , e quando è pronta, recuperare esplicitamente i risultati ed eseguire ulteriori azioni con essi. Questo può essere piuttosto complesso da implementare senza errori, specialmente in applicazioni con un gran numero di attività simultanee.

In Java 8, invece, il concetto Future viene portato oltre, con l'interfaccia CompletableFuture<T> , che consente la creazione e l'esecuzione di catene di task asincroni. È un potente meccanismo per creare applicazioni asincrone in Java 8, perché ci consente di elaborare automaticamente i risultati di ogni attività al completamento.

Vediamo un esempio:

 import java.util.concurrent.CompletableFuture; ... CompletableFuture<Void> voidCompletableFuture = CompletableFuture.supplyAsync(() -> blockingReadPage()) .thenApply(this::getLinks) .thenAccept(System.out::println);

Il metodo CompletableFuture.supplyAsync crea una nuova attività asincrona ForkJoinPool Executor . Al termine dell'attività, i suoi risultati verranno automaticamente forniti come argomenti alla funzione this::getLinks , che viene eseguita anche in una nuova attività asincrona. Infine, i risultati di questa seconda fase vengono automaticamente stampati su System.out . thenApply() e thenAccept() sono solo due dei numerosi metodi utili disponibili per aiutarti a creare attività simultanee senza utilizzare manualmente Executors .

CompletableFuture semplifica la gestione della sequenza di operazioni asincrone complesse. Supponiamo di dover creare un'operazione matematica in più fasi con tre attività. L' attività 1 e l' attività 2 utilizzano algoritmi diversi per trovare un risultato per il primo passaggio e sappiamo che solo uno di essi funzionerà mentre l'altro fallirà. Tuttavia, quale funziona dipende dai dati di input, che non conosciamo in anticipo. Il risultato di queste attività deve essere sommato al risultato dell'attività 3 . Pertanto, dobbiamo trovare il risultato dell'attività 1 o dell'attività 2 e il risultato dell'attività 3 . Per raggiungere questo obiettivo, possiamo scrivere qualcosa del genere:

 import static java.util.concurrent.CompletableFuture.*; ... Supplier<Integer> task1 = (...) -> { ... // some complex calculation return 1; // example result }; Supplier<Integer> task2 = (...) -> { ... // some complex calculation throw new RuntimeException(); // example exception }; Supplier<Integer> task3 = (...) -> { ... // some complex calculation return 3; // example result }; supplyAsync(task1) // run task1 .applyToEither( // use whichever result is ready first, result of task1 or supplyAsync(task2), // result of task2 (Integer i) -> i) // return result as-is .thenCombine( // combine result supplyAsync(task3), // with result of task3 Integer::sum) // using summation .thenAccept(System.out::println); // print final result after execution

Se esaminiamo come Java 8 gestisce questo, vedremo che tutte e tre le attività verranno eseguite contemporaneamente, in modo asincrono. Nonostante l' attività 2 fallisca con un'eccezione, il risultato finale verrà calcolato e stampato correttamente.

JAVA 8 PROGRAMMAZIONE ASINCRONA CON CompletableFuture

CompletableFuture semplifica notevolmente la creazione di attività asincrone con più fasi e fornisce un'interfaccia semplice per definire esattamente quali azioni devono essere intraprese al completamento di ciascuna fase.

API di data e ora Java

Come affermato dalla stessa ammissione di Java:

Prima del rilascio di Java SE 8, il meccanismo di data e ora Java era fornito dalle classi java.util.Date , java.util.Calendar e java.util.TimeZone , nonché dalle relative sottoclassi, come java.util.GregorianCalendar . Queste classi avevano diversi inconvenienti, tra cui

  • La classe Calendar non era sicura dai tipi.
  • Poiché le classi erano mutabili, non potevano essere utilizzate in applicazioni multithread.
  • I bug nel codice dell'applicazione erano comuni a causa della numerazione insolita dei mesi e della mancanza di sicurezza del tipo.

Java 8 risolve finalmente questi problemi di vecchia data, con il nuovo pacchetto java.time , che contiene classi per lavorare con data e ora. Tutti sono immutabili e hanno API simili al popolare framework Joda-Time, che quasi tutti gli sviluppatori Java usano nelle loro applicazioni invece dei nativi Date , Calendar e TimeZone .

Ecco alcune delle classi utili in questo pacchetto:

  • Clock : un orologio per indicare l'ora corrente, inclusi l'istante corrente, la data e l'ora con il fuso orario.
  • Duration e Period : una quantità di tempo. La Duration utilizza valori basati sul tempo come "76,8 secondi e Period , basati sulla data, come "4 anni, 6 mesi e 12 giorni".
  • Instant - Un momento istantaneo, in diversi formati.
  • LocalDate , LocalDateTime , LocalTime , Year , YearMonth - Una data, ora, anno, mese o una loro combinazione, senza un fuso orario nel sistema di calendario ISO-8601.
  • OffsetDateTime , OffsetTime - Una data-ora con un offset da UTC/Greenwich nel sistema di calendario ISO-8601, ad esempio "2015-08-29T14:15:30+01:00".
  • ZonedDateTime - Una data-ora con un fuso orario associato nel sistema di calendario ISO-8601, ad esempio "1986-08-29T10:15:30+01:00 Europa/Parigi".

API JAVA 8 TEMPO

A volte, abbiamo bisogno di trovare una data relativa come "primo martedì del mese". Per questi casi java.time fornisce una classe speciale TemporalAdjuster . La classe TemporalAdjuster contiene un set standard di regolatori, disponibili come metodi statici. Questi ci consentono di:

  • Trova il primo o l'ultimo giorno del mese.
  • Trova il primo o l'ultimo giorno del mese successivo o precedente.
  • Trova il primo o l'ultimo giorno dell'anno.
  • Trova il primo o l'ultimo giorno dell'anno successivo o precedente.
  • Trova il primo o l'ultimo giorno della settimana in un mese, ad esempio "primo mercoledì di giugno".
  • Trova il giorno della settimana successivo o precedente, ad esempio "giovedì successivo".

Ecco un breve esempio di come ottenere il primo martedì del mese:

 LocalDate getFirstTuesday(int year, int month) { return LocalDate.of(year, month, 1) .with(TemporalAdjusters.nextOrSame(DayOfWeek.TUESDAY)); }
Utilizzi ancora Java 7? Ottenere con il programma! #Java8
Twitta

Java 8 in Riepilogo

Come possiamo vedere, Java 8 è una release epocale della piattaforma Java. Ci sono molte modifiche al linguaggio, in particolare con l'introduzione di lambda, che rappresenta una mossa per portare capacità di programmazione più funzionali in Java. L'API Stream è un buon esempio di come le lambda possono cambiare il modo in cui lavoriamo con gli strumenti Java standard a cui siamo già abituati.

Inoltre, Java 8 offre alcune nuove funzionalità per lavorare con la programmazione asincrona e una revisione tanto necessaria dei suoi strumenti di data e ora.

Insieme, questi cambiamenti rappresentano un grande passo avanti per il linguaggio Java, rendendo lo sviluppo Java più interessante ed efficiente.