JavaRush /Java Blog /Random-IT /Guida Java8. 1 parte.
ramhead
Livello 13

Guida Java8. 1 parte.

Pubblicato nel gruppo Random-IT

"Java è ancora vivo e la gente sta iniziando a capirlo."

Benvenuto nella mia introduzione a Java 8. Questa guida ti accompagnerà passo dopo passo attraverso tutte le nuove funzionalità del linguaggio. Attraverso esempi di codice brevi e semplici, imparerai come utilizzare i metodi predefiniti dell'interfaccia , le espressioni lambda , i metodi di riferimento e le annotazioni ripetibili . Alla fine dell'articolo avrai familiarità con le ultime modifiche alle API come flussi, interfacce di funzioni, estensioni di associazione e la nuova API Date. Nessun muro di testo noioso: solo un mucchio di frammenti di codice commentati. Godere!

Metodi predefiniti per le interfacce

Java 8 ci consente di aggiungere metodi non astratti implementati nell'interfaccia attraverso l'uso della parola chiave predefinita . Questa funzionalità è nota anche come metodi di estensione . Ecco il nostro primo esempio: interface Formula { double calculate(int a); default double sqrt(int a) { return Math.sqrt(a); } } oltre al metodo astratto calcola , l' interfaccia Formula definisce anche un metodo predefinito sqrt . Le classi che implementano l' interfaccia Formula implementano solo il metodo di calcolo astratto . Il metodo sqrt predefinito può essere utilizzato immediatamente. Formula formula = new Formula() { @Override public double calculate(int a) { return sqrt(a * 100); } }; formula.calculate(100); // 100.0 formula.sqrt(16); // 4.0 L' oggetto formula è implementato come oggetto anonimo. Il codice è piuttosto impressionante: 6 righe di codice per calcolare semplicemente sqrt(a * 100) . Come vedremo più avanti nella sezione successiva, esiste un modo più interessante per implementare oggetti a metodo singolo in Java 8.

Espressioni lambda

Cominciamo con un semplice esempio di come ordinare un array di stringhe nelle prime versioni di Java: il metodo di supporto statistico Collections.sort accetta un elenco e un comparatore per ordinare gli elementi dell'elenco fornito. Ciò che accade spesso è che crei comparatori anonimi e li passi ai metodi di ordinamento. Invece di creare continuamente oggetti anonimi, Java 8 ti dà la possibilità di utilizzare molta meno sintassi, espressioni lambda : come puoi vedere, il codice è molto più breve e più facile da leggere. Ma qui le cose diventano ancora più brevi: per un metodo a riga singola è possibile eliminare le parentesi graffe {} e la parola chiave return . Ma qui è dove il codice diventa ancora più breve: il compilatore Java è a conoscenza dei tipi di parametri, quindi puoi tralasciare anche quelli. Ora approfondiamo il modo in cui le espressioni lambda possono essere utilizzate nella vita reale. List names = Arrays.asList("peter", "anna", "mike", "xenia"); Collections.sort(names, new Comparator () { @Override public int compare(String a, String b) { return b.compareTo(a); } }); Collections.sort(names, (String a, String b) -> { return b.compareTo(a); }); Collections.sort(names, (String a, String b) -> b.compareTo(a)); Collections.sort(names, (a, b) -> b.compareTo(a));

Interfacce funzionali

Come si inseriscono le espressioni lambda nel sistema di tipi Java? Ogni lambda corrisponde ad un dato tipo definito da un'interfaccia. E la cosiddetta interfaccia funzionale deve contenere esattamente un metodo astratto dichiarato. Ogni espressione lambda di un determinato tipo corrisponderà a questo metodo astratto. Poiché i metodi predefiniti non sono metodi astratti, sei libero di aggiungere metodi predefiniti alla tua interfaccia funzionale. Possiamo utilizzare un'interfaccia arbitraria come espressione lambda, a condizione che l'interfaccia contenga un solo metodo astratto. Per garantire che la tua interfaccia soddisfi queste condizioni, devi aggiungere l' annotazione @FunctionalInterface . Il compilatore verrà informato da questa annotazione che l'interfaccia deve contenere un solo metodo e, se incontra un secondo metodo astratto in questa interfaccia, genererà un errore. Esempio: Tieni presente che questo codice sarebbe valido anche se l' annotazione @FunctionalInterface non fosse stata dichiarata. @FunctionalInterface interface Converter { T convert(F from); } Converter converter = (from) -> Integer.valueOf(from); Integer converted = converter.convert("123"); System.out.println(converted); // 123

Riferimenti a metodi e costruttori

L'esempio sopra può essere ulteriormente semplificato utilizzando un riferimento al metodo statistico: Java 8 consente di passare riferimenti a metodi e costruttori utilizzando i simboli delle parole chiave :: . L'esempio sopra mostra come possono essere utilizzati i metodi statistici. Ma possiamo anche fare riferimento a metodi sugli oggetti: diamo un'occhiata a come funziona :: per i costruttori. Innanzitutto, definiamo un esempio con diversi costruttori: Successivamente, definiamo l' interfaccia factory PersonFactory per la creazione di nuovi oggetti persona : Converter converter = Integer::valueOf; Integer converted = converter.convert("123"); System.out.println(converted); // 123 class Something { String startsWith(String s) { return String.valueOf(s.charAt(0)); } } Something something = new Something(); Converter converter = something::startsWith; String converted = converter.convert("Java"); System.out.println(converted); // "J" class Person { String firstName; String lastName; Person() {} Person(String firstName, String lastName) { this.firstName = firstName; this.lastName = lastName; } } interface PersonFactory

{ P create(String firstName, String lastName); } Invece di implementare manualmente la factory, leghiamo tutto insieme utilizzando un riferimento al costruttore: creiamo un riferimento al costruttore della classe Person tramite Person::new . Il compilatore Java chiamerà automaticamente il costruttore appropriato confrontando la firma dei costruttori con la firma del metodo PersonFactory.create . PersonFactory personFactory = Person::new; Person person = personFactory.create("Peter", "Parker");

Regione Lambda

Organizzare l'accesso alle variabili di ambito esterno dalle espressioni lambda è simile all'accesso da un oggetto anonimo. È possibile accedere alle variabili finali dall'ambito locale, nonché ai campi di istanza e alle variabili aggregate.
Accesso alle variabili locali
Possiamo leggere una variabile locale con il modificatore final dall'ambito di un'espressione lambda: ma a differenza degli oggetti anonimi, le variabili non hanno bisogno di essere dichiarate final per essere accessibili da un'espressione lambda . Anche questo codice è corretto: tuttavia, la variabile num deve rimanere immutabile, cioè essere implicito finale per la compilazione del codice. Il codice seguente non verrà compilato: Non sono consentite nemmeno modifiche a num all'interno di un'espressione lambda. final int num = 1; Converter stringConverter = (from) -> String.valueOf(from + num); stringConverter.convert(2); // 3 int num = 1; Converter stringConverter = (from) -> String.valueOf(from + num); stringConverter.convert(2); // 3 int num = 1; Converter stringConverter = (from) -> String.valueOf(from + num); num = 3;
Accesso ai campi istanza e alle variabili statistiche
A differenza delle variabili locali, possiamo leggere e modificare campi di istanza e variabili statistiche all'interno delle espressioni lambda. Conosciamo questo comportamento da oggetti anonimi. class Lambda4 { static int outerStaticNum; int outerNum; void testScopes() { Converter stringConverter1 = (from) -> { outerNum = 23; return String.valueOf(from); }; Converter stringConverter2 = (from) -> { outerStaticNum = 72; return String.valueOf(from); }; } }
Accesso ai metodi predefiniti delle interfacce
Ricordi l'esempio con l' istanza della formula della prima sezione? L' interfaccia Formula definisce un metodo sqrt predefinito a cui è possibile accedere da ogni istanza di formula , inclusi gli oggetti anonimi. Questo non funziona con le espressioni lambda. Non è possibile accedere ai metodi predefiniti all'interno delle espressioni lambda. Il seguente codice non viene compilato: Formula formula = (a) -> sqrt( a * 100);

Interfacce funzionali integrate

L'API JDK 1.8 contiene molte interfacce funzionali integrate. Alcuni di essi sono ben noti dalle versioni precedenti di Java. Ad esempio Comparator o Runnable . Queste interfacce vengono estese per includere il supporto lambda utilizzando l' annotazione @FunctionalInterface . Ma l'API Java 8 è anche ricca di nuove interfacce funzionali che ti semplificheranno la vita. Alcune di queste interfacce sono ben note dalla libreria Guava di Google . Anche se hai familiarità con questa libreria, dovresti dare un'occhiata più da vicino a come vengono estese queste interfacce, con alcuni utili metodi di estensione.
Predicati
I predicati sono funzioni booleane con un argomento. L'interfaccia contiene vari metodi predefiniti per creare espressioni logiche complesse (e/o negare) utilizzando i predicati Predicate predicate = (s) -> s.length() > 0; predicate.test("foo"); // true predicate.negate().test("foo"); // false Predicate nonNull = Objects::nonNull; Predicate isNull = Objects::isNull; Predicate isEmpty = String::isEmpty; Predicate isNotEmpty = isEmpty.negate();
Funzioni
Le funzioni accettano un argomento e producono un risultato. I metodi predefiniti possono essere utilizzati per combinare diverse funzioni insieme in un'unica catena (compose e Then). Function toInteger = Integer::valueOf; Function backToString = toInteger.andThen(String::valueOf); backToString.apply("123"); // "123"
Fornitori
I fornitori restituiscono un risultato (istanza) di un tipo o di un altro. A differenza delle funzioni, i provider non accettano argomenti. Supplier personSupplier = Person::new; personSupplier.get(); // new Person
Consumatori
I consumatori rappresentano metodi di interfaccia con un singolo argomento. Consumer greeter = (p) -> System.out.println("Hello, " + p.firstName); greeter.accept(new Person("Luke", "Skywalker"));
Comparatori
I comparatori ci sono noti dalle versioni precedenti di Java. Java 8 consente di aggiungere vari metodi predefiniti alle interfacce. Comparator comparator = (p1, p2) -> p1.firstName.compareTo(p2.firstName); Person p1 = new Person("John", "Doe"); Person p2 = new Person("Alice", "Wonderland"); comparator.compare(p1, p2); // > 0 comparator.reversed().compare(p1, p2); // < 0
Opzionali
L'interfaccia Optionals non è funzionale, ma è un'ottima utilità per prevenire NullPointerException . Questo è un punto importante per la sezione successiva, quindi diamo una rapida occhiata a come funziona questa interfaccia. L'interfaccia Opzionale è un semplice contenitore per valori che possono essere nulli o non nulli. Immagina che un metodo possa restituire un valore o niente. In Java 8, invece di restituire null , restituisci un'istanza opzionale . Comparator comparator = (p1, p2) -> p1.firstName.compareTo(p2.firstName); Person p1 = new Person("John", "Doe"); Person p2 = new Person("Alice", "Wonderland"); comparator.compare(p1, p2); // > 0 comparator.reversed().compare(p1, p2); // < 0

Flusso

java.util.Stream è una sequenza di elementi su cui vengono eseguite una o più operazioni. Ogni operazione di flusso è intermedia o terminale. Le operazioni terminali restituiscono un risultato di un tipo specifico, mentre le operazioni intermedie restituiscono l'oggetto flusso stesso, consentendo la creazione di una catena di chiamate al metodo. Stream è un'interfaccia, come java.util.Collection per elenchi e insiemi (le mappe non sono supportate). Ogni operazione Stream può essere eseguita in sequenza o in parallelo. Diamo un'occhiata a come funziona lo streaming. Per prima cosa creeremo un codice di esempio sotto forma di un elenco di stringhe: le raccolte in Java 8 sono state migliorate in modo da poter creare flussi semplicemente chiamando Collection.stream() o Collection.parallelStream() . La sezione successiva spiegherà le operazioni di flusso più importanti e semplici. List stringCollection = new ArrayList<>(); stringCollection.add("ddd2"); stringCollection.add("aaa2"); stringCollection.add("bbb1"); stringCollection.add("aaa1"); stringCollection.add("bbb3"); stringCollection.add("ccc"); stringCollection.add("bbb2"); stringCollection.add("ddd1");
Filtro
Il filtro accetta predicati per filtrare tutti gli elementi del flusso. Questa operazione è intermedia, che ci consente di chiamare altre operazioni di flusso (ad esempio forEach) sul risultato risultante (filtrato). ForEach accetta un'operazione che verrà eseguita su ciascun elemento del flusso già filtrato. ForEach è un'operazione terminale. Inoltre, chiamare altre operazioni è impossibile. stringCollection .stream() .filter((s) -> s.startsWith("a")) .forEach(System.out::println); // "aaa2", "aaa1"
smistato
Sorted è un'operazione intermedia che restituisce una rappresentazione ordinata del flusso. Gli elementi vengono ordinati nell'ordine corretto a meno che non specifichi il tuo Comparator . stringCollection .stream() .sorted() .filter((s) -> s.startsWith("a")) .forEach(System.out::println); // "aaa1", "aaa2" Tieni presente che sorted crea una rappresentazione ordinata del flusso senza influire sulla raccolta stessa. L'ordine degli elementi stringCollection rimane invariato: System.out.println(stringCollection); // ddd2, aaa2, bbb1, aaa1, bbb3, ccc, bbb2, ddd1
Carta geografica
L'operazione di mappatura intermedia converte ciascun elemento in un altro oggetto utilizzando la funzione risultante. L'esempio seguente converte ogni stringa in una stringa maiuscola. Ma puoi anche usare map per convertire ogni oggetto in un tipo diverso. Il tipo degli oggetti stream risultanti dipende dal tipo di funzione passata alla mappa. stringCollection .stream() .map(String::toUpperCase) .sorted((a, b) -> b.compareTo(a)) .forEach(System.out::println); // "DDD2", "DDD1", "CCC", "BBB3", "BBB2", "AAA2", "AAA1"
Incontro
È possibile utilizzare varie operazioni di corrispondenza per verificare la verità di un particolare predicato nella relazione di flusso. Tutte le operazioni di corrispondenza sono terminali e restituiscono un risultato booleano. boolean anyStartsWithA = stringCollection .stream() .anyMatch((s) -> s.startsWith("a")); System.out.println(anyStartsWithA); // true boolean allStartsWithA = stringCollection .stream() .allMatch((s) -> s.startsWith("a")); System.out.println(allStartsWithA); // false boolean noneStartsWithZ = stringCollection .stream() .noneMatch((s) -> s.startsWith("z")); System.out.println(noneStartsWithZ); // true
Contare
Count è un'operazione terminale che restituisce il numero di elementi di stream come long . long startsWithB = stringCollection .stream() .filter((s) -> s.startsWith("b")) .count(); System.out.println(startsWithB); // 3
Ridurre
Questa è un'operazione terminale che accorcia gli elementi dello stream utilizzando la funzione passata. Il risultato sarà un Opzionale contenente il valore abbreviato. Optional reduced = stringCollection .stream() .sorted() .reduce((s1, s2) -> s1 + "#" + s2); reduced.ifPresent(System.out::println); // "aaa1#aaa2#bbb1#bbb2#bbb3#ccc#ddd1#ddd2"

Flussi paralleli

Come accennato in precedenza, i flussi possono essere sequenziali o paralleli. Le operazioni di flusso sequenziale vengono eseguite su un thread seriale, mentre le operazioni di flusso parallelo vengono eseguite su più thread paralleli. Nell'esempio seguente viene illustrato come aumentare facilmente le prestazioni utilizzando un flusso parallelo. Innanzitutto, creiamo un ampio elenco di elementi univoci: ora determineremo il tempo impiegato per ordinare il flusso di questa raccolta. int max = 1000000; List values = new ArrayList<>(max); for (int i = 0; i < max; i++) { UUID uuid = UUID.randomUUID(); values.add(uuid.toString()); }
Flusso seriale
long t0 = System.nanoTime(); long count = values.stream().sorted().count(); System.out.println(count); long t1 = System.nanoTime(); long millis = TimeUnit.NANOSECONDS.toMillis(t1 - t0); System.out.println(String.format("sequential sort took: %d ms", millis)); // sequential sort took: 899 ms
Flusso parallelo
long t0 = System.nanoTime(); long count = values.parallelStream().sorted().count(); System.out.println(count); long t1 = System.nanoTime(); long millis = TimeUnit.NANOSECONDS.toMillis(t1 - t0); System.out.println(String.format("parallel sort took: %d ms", millis)); // parallel sort took: 472 ms Come puoi vedere, entrambi i frammenti sono quasi identici, ma l'ordinamento parallelo è più veloce del 50%. Tutto ciò che serve è cambiare stream() in parallelStream() .

Carta geografica

Come già accennato, le mappe non supportano i flussi. Invece, la mappa ha iniziato a supportare metodi nuovi e utili per risolvere problemi comuni. Il codice sopra dovrebbe essere intuitivo: putIfAbsent ci avverte di non scrivere ulteriori controlli null. forEach accetta una funzione da eseguire per ciascuno dei valori della mappa. Questo esempio mostra come vengono eseguite le operazioni sui valori della mappa utilizzando le funzioni: Successivamente, impareremo come rimuovere una voce per una determinata chiave solo se mappata su un determinato valore: Un altro buon metodo: Unire le voci della mappa è abbastanza semplice: Unire inserirà la chiave/valore nella mappa, se non è presente alcuna voce per la chiave specificata, oppure verrà chiamata la funzione di unione, che modificherà il valore della voce esistente. Map map = new HashMap<>(); for (int i = 0; i < 10; i++) { map.putIfAbsent(i, "val" + i); } map.forEach((id, val) -> System.out.println(val)); map.computeIfPresent(3, (num, val) -> val + num); map.get(3); // val33 map.computeIfPresent(9, (num, val) -> null); map.containsKey(9); // false map.computeIfAbsent(23, num -> "val" + num); map.containsKey(23); // true map.computeIfAbsent(3, num -> "bam"); map.get(3); // val33 map.remove(3, "val3"); map.get(3); // val33 map.remove(3, "val33"); map.get(3); // null map.getOrDefault(42, "not found"); // not found map.merge(9, "val9", (value, newValue) -> value.concat(newValue)); map.get(9); // val9 map.merge(9, "concat", (value, newValue) -> value.concat(newValue)); map.get(9); // val9concat
Commenti
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION