Pourquoi vous devez déjà mettre à niveau vers Java 8

Publié: 2022-03-11

La dernière version de la plate-forme Java, Java 8, est sortie il y a plus d'un an. De nombreuses entreprises et développeurs travaillent encore avec des versions précédentes, ce qui est compréhensible, car la migration d'une version de plate-forme à une autre pose de nombreux problèmes. Même ainsi, de nombreux développeurs lancent encore de nouvelles applications avec d'anciennes versions de Java. Il y a très peu de bonnes raisons de le faire, car Java 8 a apporté des améliorations importantes au langage.

Il existe de nombreuses nouvelles fonctionnalités dans Java 8. Je vais vous montrer quelques-unes des plus utiles et intéressantes :

  • Expressions lambda
  • API de flux pour travailler avec les collections
  • Enchaînement de tâches asynchrones avec CompletableFuture
  • Nouvelle API Time

Expressions lambda

Un lambda est un bloc de code qui peut être référencé et transmis à un autre morceau de code pour une exécution future une ou plusieurs fois. Par exemple, les fonctions anonymes dans d'autres langages sont des lambdas. Comme les fonctions, les lambdas peuvent recevoir des arguments au moment de leur exécution, modifiant leurs résultats. Java 8 a introduit les expressions lambda , qui offrent une syntaxe simple pour créer et utiliser des expressions lambda.

Voyons un exemple de la façon dont cela peut améliorer notre code. Nous avons ici un comparateur simple qui compare deux valeurs Integer par leur modulo 2 :

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

Une instance de cette classe pourra être appelée, à l'avenir, dans le code où ce comparateur est nécessaire, comme ceci :

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

La nouvelle syntaxe lambda nous permet de faire cela plus simplement. Voici une simple expression lambda qui fait la même chose que la méthode compare de BinaryComparator :

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

La structure présente de nombreuses similitudes avec une fonction. Entre parenthèses, nous établissons une liste d'arguments. La syntaxe -> montre qu'il s'agit d'un lambda. Et dans la partie droite de cette expression, nous mettons en place le comportement de notre lambda.

EXPRESSION LAMBDA JAVA 8

Nous pouvons maintenant améliorer notre exemple précédent :

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

Nous pouvons définir une variable avec cet objet. Voyons à quoi ça ressemble:

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

Maintenant, nous pouvons réutiliser cette fonctionnalité, comme ceci :

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

Notez que dans ces exemples, le lambda est transmis à la méthode sort() de la même manière que l'instance de BinaryComparator est transmise dans l'exemple précédent. Comment la JVM sait-elle interpréter correctement le lambda ?

Pour permettre aux fonctions de prendre des lambdas comme arguments, Java 8 introduit un nouveau concept : interface fonctionnelle . Une interface fonctionnelle est une interface qui n'a qu'une seule méthode abstraite. En fait, Java 8 traite les expressions lambda comme une implémentation spéciale d'une interface fonctionnelle. Cela signifie que, pour recevoir un lambda en tant qu'argument de méthode, le type déclaré de cet argument doit uniquement être une interface fonctionnelle.

Lorsque nous déclarons une interface fonctionnelle, nous pouvons ajouter la notation @FunctionalInterface pour montrer aux développeurs de quoi il s'agit :

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

Maintenant, nous pouvons appeler la méthode sendDTO , en transmettant différents lambdas pour obtenir un comportement différent, comme ceci :

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

Références de méthode

Les arguments Lambda nous permettent de modifier le comportement d'une fonction ou d'une méthode. Comme nous pouvons le voir dans le dernier exemple, parfois le lambda ne sert qu'à appeler une autre méthode ( sendToAndroid ou sendToIos ). Pour ce cas particulier, Java 8 introduit un raccourci pratique : les références de méthodes . Cette syntaxe abrégée représente un lambda qui appelle une méthode et a la forme objectName::methodName . Cela nous permet de rendre l'exemple précédent encore plus concis et lisible :

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

Dans ce cas, les méthodes sendToAndroid et sendToIos sont implémentées dans this classe. Nous pouvons également référencer les méthodes d'un autre objet ou d'une autre classe.

API de flux

Java 8 apporte de nouvelles capacités pour travailler avec Collections , sous la forme d'une toute nouvelle API Stream. Cette nouvelle fonctionnalité est fournie par le package java.util.stream et vise à permettre une approche plus fonctionnelle de la programmation avec des collections. Comme nous le verrons, cela est possible en grande partie grâce à la nouvelle syntaxe lambda dont nous venons de parler.

L'API Stream offre un filtrage, un comptage et un mappage faciles des collections, ainsi que différentes façons d'en extraire des tranches et des sous-ensembles d'informations. Grâce à la syntaxe de style fonctionnel, l'API Stream permet un code plus court et plus élégant pour travailler avec les collections.

Commençons par un petit exemple. Nous utiliserons ce modèle de données dans tous les exemples :

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

Imaginons que nous ayons besoin d'imprimer tous les auteurs d'une collection de books qui ont écrit un livre après 2005. Comment ferions-nous cela en Java 7 ?

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

Et comment ferions-nous cela en 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

Ce n'est qu'une expression ! L'appel de la méthode stream() sur n'importe quelle Collection renvoie un objet Stream encapsulant tous les éléments de cette collection. Cela peut être manipulé avec différents modificateurs de l'API Stream, tels que filter() et map() . Chaque modificateur renvoie un nouvel objet Stream avec les résultats de la modification, qui peuvent être manipulés davantage. La méthode .forEach() nous permet d'effectuer une action pour chaque instance du flux résultant.

Cet exemple démontre également la relation étroite entre la programmation fonctionnelle et les expressions lambda. Notez que l'argument passé à chaque méthode dans le flux est soit un lambda personnalisé, soit une référence de méthode. Techniquement, chaque modificateur peut recevoir n'importe quelle interface fonctionnelle, comme décrit dans la section précédente.

L'API Stream aide les développeurs à regarder les collections Java sous un nouvel angle. Imaginez maintenant que nous ayons besoin d'obtenir une Map des langues disponibles dans chaque pays. Comment cela serait-il implémenté dans 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()); }

En Java 8, les choses sont un peu plus claires :

 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())));

La méthode collect() nous permet de collecter les résultats d'un flux de différentes manières. Ici, nous pouvons voir qu'il regroupe d'abord par pays, puis cartographie chaque groupe par langue. ( groupingBy() et toSet() sont toutes deux des méthodes statiques de la classe Collectors .)

API JAVA 8 FLUX

Il existe de nombreuses autres fonctionnalités de l'API Stream. La documentation complète est disponible ici. Je recommande de lire plus loin pour acquérir une compréhension plus approfondie de tous les outils puissants que ce package a à offrir.

Enchaînement de tâches asynchrones avec CompletableFuture

Dans le package java.util.concurrent de Java 7, il existe une interface Future<T> , qui nous permet d'obtenir le statut ou le résultat d'une tâche asynchrone dans le futur. Pour utiliser cette fonctionnalité, nous devons :

  1. Créez un ExecutorService , qui gère l'exécution des tâches asynchrones et peut générer des objets Future pour suivre leur progression.
  2. Créez une tâche Runnable de manière asynchrone.
  3. Exécutez la tâche dans ExecutorService , qui fournira un Future donnant accès au statut ou aux résultats.

Afin d'utiliser les résultats d'une tâche asynchrone, il est nécessaire de surveiller son avancement de l'extérieur, en utilisant les méthodes de l'interface Future , et lorsqu'elle est prête, de récupérer explicitement les résultats et d'effectuer d'autres actions avec eux. Cela peut être assez complexe à mettre en œuvre sans erreurs, en particulier dans les applications avec un grand nombre de tâches simultanées.

En Java 8, cependant, le concept Future est poussé plus loin, avec l'interface CompletableFuture<T> , qui permet la création et l'exécution de chaînes de tâches asynchrones. C'est un mécanisme puissant pour créer des applications asynchrones en Java 8, car il nous permet de traiter automatiquement les résultats de chaque tâche à la fin.

Voyons un exemple :

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

La méthode CompletableFuture.supplyAsync crée une nouvelle tâche asynchrone dans l' Executor par défaut (généralement ForkJoinPool ). Lorsque la tâche est terminée, ses résultats seront automatiquement fournis en tant qu'arguments à la fonction this::getLinks , qui est également exécutée dans une nouvelle tâche asynchrone. Enfin, les résultats de cette deuxième étape sont automatiquement imprimés dans System.out . thenApply() et thenAccept() ne sont que deux des nombreuses méthodes utiles disponibles pour vous aider à créer des tâches simultanées sans utiliser manuellement Executors .

Le CompletableFuture facilite la gestion du séquencement d'opérations asynchrones complexes. Disons que nous devons créer une opération mathématique en plusieurs étapes avec trois tâches. La tâche 1 et la tâche 2 utilisent des algorithmes différents pour trouver un résultat pour la première étape, et nous savons qu'un seul d'entre eux fonctionnera tandis que l'autre échouera. Cependant, celui qui fonctionne dépend des données d'entrée, que nous ne connaissons pas à l'avance. Le résultat de ces tâches doit être additionné au résultat de la tâche 3 . Ainsi, nous devons trouver le résultat de la tâche 1 ou de la tâche 2 , et le résultat de la tâche 3 . Pour y parvenir, nous pouvons écrire quelque chose comme ceci :

 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

Si nous examinons comment Java 8 gère cela, nous verrons que les trois tâches seront exécutées en même temps, de manière asynchrone. Malgré l'échec de la tâche 2 avec une exception, le résultat final sera calculé et imprimé avec succès.

PROGRAMMATION ASYNCHRONE JAVA 8 AVEC CompletableFuture

CompletableFuture facilite grandement la création de tâches asynchrones à plusieurs étapes et nous offre une interface simple pour définir exactement les actions à entreprendre à la fin de chaque étape.

API de date et d'heure Java

Comme indiqué par le propre aveu de Java :

Avant la version Java SE 8, le mécanisme de date et d'heure Java était fourni par les classes java.util.Date , java.util.Calendar et java.util.TimeZone , ainsi que leurs sous-classes, telles que java.util.GregorianCalendar . Ces classes présentaient plusieurs inconvénients, notamment

  • La classe Calendar n'était pas de type sûr.
  • Étant donné que les classes étaient modifiables, elles ne pouvaient pas être utilisées dans des applications multithread.
  • Les bogues dans le code d'application étaient courants en raison de la numérotation inhabituelle des mois et du manque de sécurité des types.

Java 8 résout enfin ces problèmes de longue date, avec le nouveau package java.time , qui contient des classes pour travailler avec la date et l'heure. Tous sont immuables et ont des API similaires au framework populaire Joda-Time, que presque tous les développeurs Java utilisent dans leurs applications au lieu des Date , Calendar et TimeZone natifs.

Voici quelques-unes des classes utiles de ce package :

  • Clock - Une horloge pour indiquer l'heure actuelle, y compris l'instant, la date et l'heure actuels avec le fuseau horaire.
  • Duration et Period - Une durée. Duration utilise des valeurs basées sur le temps telles que "76,8 secondes, et Period , basée sur la date, telles que "4 ans, 6 mois et 12 jours".
  • Instant - Un point instantané dans le temps, dans plusieurs formats.
  • LocalDate , LocalDateTime , LocalTime , Year , YearMonth - Une date, une heure, une année, un mois ou une combinaison de ceux-ci, sans fuseau horaire dans le système de calendrier ISO-8601.
  • OffsetDateTime , OffsetTime - Une date-heure avec un décalage par rapport à UTC/Greenwich dans le système de calendrier ISO-8601, comme "2015-08-29T14:15:30+01:00".
  • ZonedDateTime - Une date-heure avec un fuseau horaire associé dans le système de calendrier ISO-8601, tel que "1986-08-29T10:15:30+01:00 Europe/Paris".

API JAVA 8 TIME

Parfois, nous devons trouver une date relative telle que "le premier mardi du mois". Pour ces cas, java.time fournit une classe spéciale TemporalAdjuster . La classe TemporalAdjuster contient un ensemble standard d'ajusteurs, disponibles en tant que méthodes statiques. Ceux-ci nous permettent de :

  • Trouvez le premier ou le dernier jour du mois.
  • Recherchez le premier ou le dernier jour du mois suivant ou précédent.
  • Trouvez le premier ou le dernier jour de l'année.
  • Trouvez le premier ou le dernier jour de l'année suivante ou précédente.
  • Recherchez le premier ou le dernier jour de la semaine d'un mois, par exemple "premier mercredi de juin".
  • Recherchez le jour de la semaine suivant ou précédent, par exemple "jeudi prochain".

Voici un court exemple comment obtenir le premier mardi du mois :

 LocalDate getFirstTuesday(int year, int month) { return LocalDate.of(year, month, 1) .with(TemporalAdjusters.nextOrSame(DayOfWeek.TUESDAY)); }
Vous utilisez toujours Java 7 ? Obtenez avec le programme! #Java8
Tweeter

Java 8 en résumé

Comme nous pouvons le voir, Java 8 est une version historique de la plate-forme Java. Il y a beaucoup de changements de langage, en particulier avec l'introduction de lambdas, qui représente un mouvement pour apporter des capacités de programmation plus fonctionnelles à Java. L'API Stream est un bon exemple de la façon dont les lambdas peuvent changer la façon dont nous travaillons avec les outils Java standard auxquels nous sommes déjà habitués.

De plus, Java 8 apporte de nouvelles fonctionnalités pour travailler avec la programmation asynchrone et une refonte indispensable de ses outils de date et d'heure.

Ensemble, ces changements représentent un grand pas en avant pour le langage Java, rendant le développement Java plus intéressant et plus efficace.