De ce trebuie să faceți deja upgrade la Java 8
Publicat: 2022-03-11Cea mai nouă versiune a platformei Java, Java 8, a fost lansată cu mai bine de un an în urmă. Multe companii și dezvoltatori încă lucrează cu versiuni anterioare, ceea ce este de înțeles, deoarece există o mulțime de probleme cu migrarea de la o versiune de platformă la alta. Chiar și așa, mulți dezvoltatori încă încep aplicații noi cu versiuni vechi de Java. Există foarte puține motive bune pentru a face acest lucru, deoarece Java 8 a adus câteva îmbunătățiri importante limbajului.
Există multe funcții noi în Java 8. Vă voi arăta câteva dintre cele mai utile și mai interesante:
- Expresii lambda
- API Stream pentru lucrul cu Colecții
- Înlănțuire asincronă a sarcinilor cu
CompletableFuture
- Time API nou-nouț
Expresii Lambda
Un lambda este un bloc de cod care poate fi referit și transmis la o altă bucată de cod pentru o execuție viitoare de o dată sau de mai multe ori. De exemplu, funcțiile anonime în alte limbi sunt lambda. La fel ca și funcțiilor, lambda-urilor li se pot transmite argumente în momentul execuției lor, modificându-le rezultatele. Java 8 a introdus expresii lambda , care oferă o sintaxă simplă pentru a crea și utiliza lambda.
Să vedem un exemplu despre cum acest lucru poate îmbunătăți codul nostru. Aici avem un comparator simplu care compară două valori Integer
prin modulo 2:
class BinaryComparator implements Comparator<Integer>{ @Override public int compare(Integer i1, Integer i2) { return i1 % 2 - i2 % 2; } }
O instanță a acestei clase poate fi numită, în viitor, în codul în care este necesar acest comparator, astfel:
... List<Integer> list = ...; Comparator<Integer> comparator = new BinaryComparator(); Collections.sort(list, comparator); ...
Noua sintaxă lambda ne permite să facem acest lucru mai simplu. Iată o expresie lambda simplă care face același lucru ca metoda de compare
din BinaryComparator
:
(Integer i1, Integer i2) -> i1 % 2 - i2 % 2;
Structura are multe asemănări cu o funcție. Între paranteze, am configurat o listă de argumente. Sintaxa ->
arată că acesta este un lambda. Și în partea dreaptă a acestei expresii, am configurat comportamentul lambdei noastre.
Acum putem îmbunătăți exemplul nostru anterior:
... List<Integer> list = ...; Collections.sort(list, (Integer i1, Integer i2) -> i1 % 2 - i2 % 2); ...
Putem defini o variabilă cu acest obiect. Să vedem cum arată:
Comparator<Integer> comparator = (Integer i1, Integer i2) -> i1 % 2 - i2 % 2;
Acum putem reutiliza această funcționalitate, astfel:
... List<Integer> list1 = ...; List<Integer> list2 = ...; Collections.sort(list1, comparator); Collections.sort(list2, comparator); ...
Observați că în aceste exemple, lambda este transmisă metodei sort()
în același mod în care este transmisă instanța BinaryComparator
în exemplul anterior. Cum știe JVM-ul să interpreteze corect lambda?
Pentru a permite funcțiilor să ia lambda drept argumente, Java 8 introduce un nou concept: interfață funcțională . O interfață funcțională este o interfață care are o singură metodă abstractă. De fapt, Java 8 tratează expresiile lambda ca pe o implementare specială a unei interfețe funcționale. Aceasta înseamnă că, pentru a primi un lambda ca argument de metodă, tipul declarat al acelui argument trebuie să fie doar o interfață funcțională.
Când declarăm o interfață funcțională, putem adăuga notația @FunctionalInterface
pentru a arăta dezvoltatorilor ce este:
@FunctionalInterface private interface DTOSender { void send(String accountId, DTO dto); } void sendDTO(BisnessModel object, DTOSender dtoSender) { //some logic for sending... ... dtoSender.send(id, dto); ... }
Acum, putem apela metoda sendDTO
, trecând diferite lambda pentru a obține un comportament diferit, astfel:
sendDTO(object, ((accountId, dto) -> sendToAndroid(accountId, dto))); sendDTO(object, ((accountId, dto) -> sendToIos(accountId, dto)));
Referințe de metodă
Argumentele lambda ne permit să modificăm comportamentul unei funcții sau metode. După cum putem vedea în ultimul exemplu, uneori lambda servește doar la apelarea unei alte metode ( sendToAndroid
sau sendToIos
). Pentru acest caz special, Java 8 introduce o prescurtare convenabilă: referințe de metodă . Această sintaxă abreviată reprezintă o lambda care apelează o metodă și are forma objectName::methodName
. Acest lucru ne permite să facem exemplul anterior și mai concis și mai ușor de citit:
sendDTO(object, this::sendToAndroid); sendDTO(object, this::sendToIos);
În acest caz, metodele sendToAndroid
și sendToIos
sunt implementate în this
clasă. De asemenea, putem face referire la metodele unui alt obiect sau clasă.
API Stream
Java 8 aduce noi abilități de a lucra cu Collections
, sub forma unui nou-nouț API Stream. Această nouă funcționalitate este furnizată de pachetul java.util.stream
și are scopul de a permite o abordare mai funcțională a programării cu colecții. După cum vom vedea, acest lucru este posibil în mare măsură datorită noii sintaxe lambda pe care tocmai am discutat.
Stream API oferă filtrare, numărare și mapare ușoară a colecțiilor, precum și moduri diferite de a obține secțiuni și subseturi de informații din acestea. Datorită sintaxei de stil funcțional, Stream API permite un cod mai scurt și mai elegant pentru lucrul cu colecții.
Să începem cu un exemplu scurt. Vom folosi acest model de date în toate exemplele:
class Author { String name; int countOfBooks; } class Book { String name; int year; Author author; }
Să ne imaginăm că trebuie să tipărim toți autorii dintr-o colecție de books
care au scris o carte după 2005. Cum am face asta în Java 7?
for (Book book : books) { if (book.author != null && book.year > 2005){ System.out.println(book.author.name); } }
Și cum am face-o în 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
Este doar o expresie! Apelarea metodei stream()
pe orice Collection
returnează un obiect Stream
care încapsulează toate elementele acelei colecții. Acest lucru poate fi manipulat cu diferiți modificatori din Stream API, cum ar fi filter()
și map()
. Fiecare modificator returnează un nou obiect Stream
cu rezultatele modificării, care poate fi manipulat în continuare. Metoda .forEach()
ne permite să efectuăm o anumită acțiune pentru fiecare instanță a fluxului rezultat.
Acest exemplu demonstrează, de asemenea, relația strânsă dintre programarea funcțională și expresiile lambda. Observați că argumentul transmis fiecărei metode din flux este fie un lambda personalizat, fie o referință de metodă. Din punct de vedere tehnic, fiecare modificator poate primi orice interfață funcțională, așa cum este descris în secțiunea anterioară.
API-ul Stream îi ajută pe dezvoltatori să privească colecțiile Java dintr-un unghi nou. Imaginați-vă acum că trebuie să obținem o Map
a limbilor disponibile în fiecare țară. Cum ar fi implementat acest lucru în 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()); }
În Java 8, lucrurile sunt puțin mai ordonate:
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())));
Metoda collect()
ne permite să colectăm rezultatele unui flux în moduri diferite. Aici, putem vedea că mai întâi grupează în funcție de țară, apoi cartografiază fiecare grup în funcție de limbă. ( groupingBy()
și toSet()
sunt ambele metode statice din clasa Collectors
.)

Există o mulțime de alte abilități ale Stream API. Documentația completă poate fi găsită aici. Vă recomand să citiți în continuare pentru a obține o înțelegere mai profundă a tuturor instrumentelor puternice pe care le oferă acest pachet.
Înlănțuirea asincronă a sarcinilor cu CompletableFuture
În pachetul java.util.concurrent
din Java 7, există o interfață Future<T>
, care ne permite să obținem starea sau rezultatul unei sarcini asincrone în viitor. Pentru a folosi această funcționalitate, trebuie să:
- Creați un
ExecutorService
, care gestionează execuția sarcinilor asincrone și poate genera obiecteFuture
pentru a urmări progresul acestora. - Creați o sarcină
Runnable
asincron. - Rulați sarcina în
ExecutorService
, care va oferi unFuture
care va oferi acces la stare sau rezultate.
Pentru a utiliza rezultatele unei sarcini asincrone, este necesar să monitorizați progresul acesteia din exterior, folosind metodele interfeței Future
, iar când este gata, să preluați în mod explicit rezultatele și să efectuați acțiuni ulterioare cu acestea. Acest lucru poate fi destul de complex de implementat fără erori, în special în aplicațiile cu un număr mare de sarcini concurente.
În Java 8, însă, conceptul de Future
este dus mai departe, cu interfața CompletableFuture<T>
, care permite crearea și executarea lanțurilor de sarcini asincrone. Este un mecanism puternic de a crea aplicații asincrone în Java 8, deoarece ne permite să procesăm automat rezultatele fiecărei sarcini după finalizare.
Să vedem un exemplu:
import java.util.concurrent.CompletableFuture; ... CompletableFuture<Void> voidCompletableFuture = CompletableFuture.supplyAsync(() -> blockingReadPage()) .thenApply(this::getLinks) .thenAccept(System.out::println);
Metoda CompletableFuture.supplyAsync
creează o nouă sarcină asincronă în Executor
implicit (de obicei ForkJoinPool
). Când sarcina este terminată, rezultatele acesteia vor fi furnizate automat ca argumente pentru funcția this::getLinks
, care este, de asemenea, rulată într-o nouă sarcină asincronă. În cele din urmă, rezultatele acestei a doua etape sunt tipărite automat pe System.out
. thenApply()
și thenAccept()
sunt doar două dintre câteva metode utile disponibile pentru a vă ajuta să creați sarcini concurente fără a utiliza manual Executors
.
CompletableFuture
facilitează gestionarea secvențierii operațiilor asincrone complexe. Să presupunem că trebuie să creăm o operație matematică în mai mulți pași cu trei sarcini. Sarcina 1 și sarcina 2 folosesc algoritmi diferiți pentru a găsi un rezultat pentru primul pas și știm că doar unul dintre ei va funcționa, în timp ce celălalt va eșua. Cu toate acestea, care funcționează depinde de datele de intrare, pe care nu le cunoaștem dinainte. Rezultatul acestor sarcini trebuie însumat cu rezultatul sarcinii 3 . Astfel, trebuie să găsim rezultatul sarcinii 1 sau sarcinii 2 și rezultatul sarcinii 3 . Pentru a realiza acest lucru, putem scrie ceva de genul acesta:
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
Dacă examinăm modul în care Java 8 gestionează acest lucru, vom vedea că toate cele trei sarcini vor fi executate în același timp, asincron. În ciuda faptului că sarcina 2 eșuează cu o excepție, rezultatul final va fi calculat și tipărit cu succes.
CompletableFuture
face mult mai ușoară construirea de sarcini asincrone cu mai multe etape și ne oferă o interfață ușoară pentru a defini exact ce acțiuni ar trebui întreprinse la finalizarea fiecărei etape.
API Java Data and Time
După cum se arată în propria recunoaștere a lui Java:
Înainte de lansarea Java SE 8, mecanismul de dată și oră Java era furnizat de clasele
java.util.Date
,java.util.Calendar
șijava.util.TimeZone
, precum și subclasele lor, cum ar fijava.util.GregorianCalendar
. Aceste clase aveau mai multe dezavantaje, inclusiv
- Clasa Calendar nu a fost tip sigur.
- Deoarece clasele erau mutabile, nu puteau fi utilizate în aplicații cu mai multe fire.
- Erori în codul aplicației au fost comune din cauza numerotării neobișnuite a lunilor și a lipsei de siguranță a tipului.”
Java 8 rezolvă în sfârșit aceste probleme de lungă durată, cu noul pachet java.time
, care conține clase pentru lucrul cu data și ora. Toate sunt imuabile și au API-uri similare cu cadrul popular Joda-Time, pe care aproape toți dezvoltatorii Java îl folosesc în aplicațiile lor în loc de Date
, Calendar
și TimeZone
native.
Iată câteva dintre clasele utile din acest pachet:
-
Clock
- Un ceas pentru a indica ora curentă, inclusiv momentul curent, data și ora cu fusul orar. -
Duration
șiPeriod
- O perioadă de timp.Duration
folosește valori bazate pe timp, cum ar fi „76,8 secunde șiPeriod
, bazate pe dată, cum ar fi „4 ani, 6 luni și 12 zile”. -
Instant
- Un moment instantaneu în timp, în mai multe formate. -
LocalDate
,LocalDateTime
,LocalTime
,Year
,YearMonth
- O dată, oră, an, lună sau o combinație a acestora, fără un fus orar în sistemul calendaristic ISO-8601. -
OffsetDateTime
,OffsetTime
- O dată-oră cu un decalaj față de UTC/Greenwich în sistemul de calendar ISO-8601, cum ar fi „2015-08-29T14:15:30+01:00”. -
ZonedDateTime
- O dată-oră cu un fus orar asociat în sistemul de calendar ISO-8601, cum ar fi „1986-08-29T10:15:30+01:00 Europa/Paris”.
Uneori, trebuie să găsim o dată relativă, cum ar fi „prima marți a lunii”. Pentru aceste cazuri, java.time
oferă o clasă specială TemporalAdjuster
. Clasa TemporalAdjuster
conține un set standard de ajustatori, disponibili ca metode statice. Acestea ne permit să:
- Găsiți prima sau ultima zi a lunii.
- Găsiți prima sau ultima zi a lunii următoare sau anterioare.
- Găsiți prima sau ultima zi a anului.
- Găsiți prima sau ultima zi a anului următor sau precedent.
- Găsiți prima sau ultima zi a săptămânii într-o lună, cum ar fi „prima miercuri din iunie”.
- Găsiți ziua următoare sau anterioară a săptămânii, cum ar fi „joia viitoare”.
Iată un scurt exemplu cum să obțineți prima zi de marți a lunii:
LocalDate getFirstTuesday(int year, int month) { return LocalDate.of(year, month, 1) .with(TemporalAdjusters.nextOrSame(DayOfWeek.TUESDAY)); }
Java 8 în rezumat
După cum putem vedea, Java 8 este o versiune epocală a platformei Java. Există o mulțime de schimbări de limbaj, în special cu introducerea lambdas, care reprezintă o mișcare de a aduce mai multe abilități de programare funcționale în Java. API-ul Stream este un exemplu bun de modul în care lambdas poate schimba modul în care lucrăm cu instrumente Java standard cu care suntem deja obișnuiți.
De asemenea, Java 8 aduce câteva funcții noi pentru lucrul cu programarea asincronă și o revizuire atât de necesară a instrumentelor sale de dată și oră.
Împreună, aceste schimbări reprezintă un mare pas înainte pentru limbajul Java, făcând dezvoltarea Java mai interesantă și mai eficientă.