JavaRush /Blog Java /Random-FR /Guide Java 8. 1 partie.
ramhead
Niveau 13

Guide Java 8. 1 partie.

Publié dans le groupe Random-FR

"Java est toujours vivant et les gens commencent à le comprendre."

Bienvenue dans mon introduction à Java 8. Ce guide vous guidera étape par étape à travers toutes les nouvelles fonctionnalités du langage. Grâce à des exemples de code courts et simples, vous apprendrez à utiliser les méthodes par défaut de l'interface , les expressions lambda , les méthodes de référence et les annotations répétables . À la fin de l'article, vous serez familiarisé avec les dernières modifications apportées aux API telles que les flux, les interfaces de fonction, les extensions d'association et la nouvelle API Date. Pas de murs de texte ennuyeux - juste un tas d'extraits de code commentés. Apprécier!

Méthodes par défaut pour les interfaces

Java 8 nous permet d'ajouter des méthodes non abstraites implémentées dans l'interface grâce à l'utilisation du mot-clé par défaut . Cette fonctionnalité est également connue sous le nom de méthodes d'extension . Voici notre premier exemple : interface Formula { double calculate(int a); default double sqrt(int a) { return Math.sqrt(a); } } En plus de la méthode abstraite calculate , l' interface Formula définit également une méthode par défaut sqrt . Les classes qui implémentent l' interface Formula implémentent uniquement la méthode de calcul abstraite . La méthode sqrt par défaut peut être utilisée directement. 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' objet formule est implémenté en tant qu'objet anonyme. Le code est assez impressionnant : 6 lignes de code pour calculer simplement sqrt(a * 100) . Comme nous le verrons plus loin dans la section suivante, il existe une manière plus attrayante d'implémenter des objets à méthode unique dans Java 8.

Expressions lambda

Commençons par un exemple simple de comment trier un tableau de chaînes dans les premières versions de Java : La méthode d'assistance statistique Collections.sort prend une liste et un Comparator pour trier les éléments de la liste donnée. Ce qui arrive souvent, c'est que vous créez des comparateurs anonymes et que vous les transmettez aux méthodes de tri. Au lieu de créer des objets anonymes à tout moment, Java 8 vous offre la possibilité d'utiliser beaucoup moins de syntaxe, d'expressions lambda : Comme vous pouvez le constater, le code est beaucoup plus court et plus facile à lire. Mais ici, cela devient encore plus court : pour une méthode sur une seule ligne, vous pouvez vous débarrasser des accolades {} et du mot-clé return . Mais voici où le code devient encore plus court : le compilateur Java connaît les types de paramètres, vous pouvez donc également les omettre. Voyons maintenant plus en détail comment les expressions lambda peuvent être utilisées dans la vie réelle. 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));

Interfaces fonctionnelles

Comment les expressions lambda s’intègrent-elles dans le système de types Java ? Chaque lambda correspond à un type donné défini par une interface. Et la soi-disant interface fonctionnelle doit contenir exactement une méthode abstraite déclarée. Chaque expression lambda d'un type donné correspondra à cette méthode abstraite. Puisque les méthodes par défaut ne sont pas des méthodes abstraites, vous êtes libre d'ajouter des méthodes par défaut à votre interface fonctionnelle. Nous pouvons utiliser une interface arbitraire comme expression lambda, à condition que l'interface ne contienne qu'une seule méthode abstraite. Pour vous assurer que votre interface remplit ces conditions, vous devez ajouter l' annotation @FunctionalInterface . Le compilateur sera informé par cette annotation que l'interface ne doit contenir qu'une seule méthode, et s'il rencontre une deuxième méthode abstraite dans cette interface, il générera une erreur. Exemple : gardez à l'esprit que ce code serait également valide même si l' annotation @FunctionalInterface n'avait pas été déclarée. @FunctionalInterface interface Converter { T convert(F from); } Converter converter = (from) -> Integer.valueOf(from); Integer converted = converter.convert("123"); System.out.println(converted); // 123

Références aux méthodes et aux constructeurs

L'exemple ci-dessus peut être encore simplifié en utilisant une référence de méthode statistique : Java 8 vous permet de transmettre des références aux méthodes et aux constructeurs à l'aide des symboles de mot-clé :: . L'exemple ci-dessus montre comment les méthodes statistiques peuvent être utilisées. Mais nous pouvons également référencer des méthodes sur des objets : Voyons comment l'utilisation de :: fonctionne pour les constructeurs. Tout d'abord, définissons un exemple avec différents constructeurs : Ensuite, nous définissons l' interface de fabrique PersonFactory pour créer de nouveaux objets personne : 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); } Au lieu d'implémenter la fabrique manuellement, nous lions le tout à l'aide d'une référence de constructeur : Nous créons une référence au constructeur de la classe Person via Person::new . Le compilateur Java appellera automatiquement le constructeur approprié en comparant la signature des constructeurs avec la signature de la méthode PersonFactory.create . PersonFactory personFactory = Person::new; Person person = personFactory.create("Peter", "Parker");

Région Lambda

L'organisation de l'accès aux variables de portée externe à partir d'expressions lambda est similaire à l'accès à partir d'un objet anonyme. Vous pouvez accéder aux variables finales à partir de la portée locale, ainsi qu'aux champs d'instance et aux variables agrégées.
Accéder aux variables locales
Nous pouvons lire une variable locale avec le modificateur final depuis la portée d'une expression lambda : Mais contrairement aux objets anonymes, les variables n'ont pas besoin d'être déclarées finales pour être accessibles depuis une expression lambda . Ce code est également correct : Cependant, la variable num doit rester immuable, c'est-à-dire être implicite final pour la compilation du code. Le code suivant ne sera pas compilé : Les modifications apportées à num dans une expression lambda ne sont pas non plus autorisées. 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;
Accès aux champs d'instance et aux variables statistiques
Contrairement aux variables locales, nous pouvons lire et modifier les champs d'instance et les variables statistiques dans les expressions lambda. Nous connaissons ce comportement à partir d'objets anonymes. 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); }; } }
Accès aux méthodes par défaut des interfaces
Vous vous souvenez de l'exemple avec l' instance de formule de la première section ? L' interface Formula définit une méthode sqrt par défaut accessible à partir de chaque instance de formula , y compris les objets anonymes. Cela ne fonctionne pas avec les expressions lambda. Les méthodes par défaut ne sont pas accessibles dans les expressions lambda. Le code suivant ne compile pas : Formula formula = (a) -> sqrt( a * 100);

Interfaces fonctionnelles intégrées

L'API JDK 1.8 contient de nombreuses interfaces fonctionnelles intégrées. Certains d’entre eux sont bien connus des versions précédentes de Java. Par exemple Comparator ou Runnable . Ces interfaces sont étendues pour inclure la prise en charge lambda à l'aide de l' annotation @FunctionalInterface . Mais l’API Java 8 regorge également de nouvelles interfaces fonctionnelles qui vous faciliteront la vie. Certaines de ces interfaces sont bien connues grâce à la bibliothèque Guava de Google . Même si vous connaissez cette bibliothèque, vous devriez examiner de plus près comment ces interfaces sont étendues, avec quelques méthodes d'extension utiles.
Prédicats
Les prédicats sont des fonctions booléennes avec un seul argument. L'interface contient diverses méthodes par défaut pour créer des expressions logiques complexes (et/ou nier) à l'aide de prédicats. 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();
Les fonctions
Les fonctions prennent un argument et produisent un résultat. Les méthodes par défaut peuvent être utilisées pour combiner plusieurs fonctions en une seule chaîne (compose, andThen). Function toInteger = Integer::valueOf; Function backToString = toInteger.andThen(String::valueOf); backToString.apply("123"); // "123"
Fournisseurs
Les fournisseurs renvoient un résultat (instance) d'un type ou d'un autre. Contrairement aux fonctions, les fournisseurs n'acceptent pas d'arguments. Supplier personSupplier = Person::new; personSupplier.get(); // new Person
Consommateurs
Les consommateurs représentent les méthodes d'interface avec un seul argument. Consumer greeter = (p) -> System.out.println("Hello, " + p.firstName); greeter.accept(new Person("Luke", "Skywalker"));
Comparateurs
Les comparateurs nous sont connus grâce aux versions précédentes de Java. Java 8 vous permet d'ajouter diverses méthodes par défaut aux interfaces. 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
Options
L'interfaceOptionals n'est pas fonctionnelle, mais c'est un excellent utilitaire pour empêcher NullPointerException . C’est un point important pour la section suivante, jetons donc un coup d’œil rapide au fonctionnement de cette interface. L'interface facultative est un simple conteneur pour les valeurs qui peuvent être nulles ou non nulles. Imaginez qu'une méthode puisse renvoyer une valeur ou rien. Dans Java 8, au lieu de renvoyer null , vous renvoyez une instance Facultative . 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

Flux

java.util.Stream est une séquence d'éléments sur lesquels une ou plusieurs opérations sont effectuées. Chaque opération Stream est soit intermédiaire, soit terminale. Les opérations de terminal renvoient un résultat d'un type spécifique, tandis que les opérations intermédiaires renvoient l'objet flux lui-même, permettant de créer une chaîne d'appels de méthode. Stream est une interface, comme java.util.Collection pour les listes et les ensembles (les cartes ne sont pas prises en charge). Chaque opération Stream peut être exécutée séquentiellement ou en parallèle. Jetons un coup d'œil au fonctionnement du flux. Tout d'abord, nous allons créer un exemple de code sous la forme d'une liste de chaînes : les collections dans Java 8 sont améliorées afin que vous puissiez créer des flux tout simplement en appelant Collection.stream() ou Collection.parallelStream() . La section suivante expliquera les opérations de flux simples et les plus importantes. 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");
Filtre
Filter accepte les prédicats pour filtrer tous les éléments du flux. Cette opération est intermédiaire, ce qui permet d'appeler d'autres opérations de flux (par exemple forEach) sur le résultat résultant (filtré). ForEach accepte une opération qui sera effectuée sur chaque élément du flux déjà filtré. ForEach est une opération de terminal. De plus, il est impossible d’appeler d’autres opérations. stringCollection .stream() .filter((s) -> s.startsWith("a")) .forEach(System.out::println); // "aaa2", "aaa1"
Trié
Sorted est une opération intermédiaire qui renvoie une représentation triée du flux. Les éléments sont triés dans le bon ordre sauf si vous spécifiez votre Comparator . stringCollection .stream() .sorted() .filter((s) -> s.startsWith("a")) .forEach(System.out::println); // "aaa1", "aaa2" Gardez à l’esprit que sorted crée une représentation triée du flux sans affecter la collection elle-même. L’ordre des éléments stringCollection reste inchangé : System.out.println(stringCollection); // ddd2, aaa2, bbb1, aaa1, bbb3, ccc, bbb2, ddd1
Carte
L'opération de mappage intermédiaire convertit chaque élément en un autre objet à l'aide de la fonction résultante. L'exemple suivant convertit chaque chaîne en chaîne majuscule. Mais vous pouvez également utiliser map pour convertir chaque objet en un type différent. Le type des objets de flux résultants dépend du type de fonction que vous transmettez à la carte. stringCollection .stream() .map(String::toUpperCase) .sorted((a, b) -> b.compareTo(a)) .forEach(System.out::println); // "DDD2", "DDD1", "CCC", "BBB3", "BBB2", "AAA2", "AAA1"
Correspondre
Diverses opérations de correspondance peuvent être utilisées pour tester la vérité d'un prédicat particulier dans la relation de flux. Toutes les opérations de correspondance sont terminales et renvoient un résultat booléen. 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
Compter
Count est une opération de terminal qui renvoie le nombre d'éléments du flux sous forme de fichier long . long startsWithB = stringCollection .stream() .filter((s) -> s.startsWith("b")) .count(); System.out.println(startsWithB); // 3
Réduire
Il s'agit d'une opération de terminal qui raccourcit les éléments du flux à l'aide de la fonction passée. Le résultat sera un facultatif contenant la valeur raccourcie. Optional reduced = stringCollection .stream() .sorted() .reduce((s1, s2) -> s1 + "#" + s2); reduced.ifPresent(System.out::println); // "aaa1#aaa2#bbb1#bbb2#bbb3#ccc#ddd1#ddd2"

Flux parallèles

Comme mentionné ci-dessus, les flux peuvent être séquentiels ou parallèles. Les opérations de flux séquentiel sont effectuées sur un thread série, tandis que les opérations de flux parallèles sont effectuées sur plusieurs threads parallèles. L'exemple suivant montre comment augmenter facilement les performances à l'aide d'un flux parallèle. Tout d'abord, créons une grande liste d'éléments uniques : nous allons maintenant déterminer le temps passé à trier le flux de cette collection. int max = 1000000; List values = new ArrayList<>(max); for (int i = 0; i < max; i++) { UUID uuid = UUID.randomUUID(); values.add(uuid.toString()); }
Flux série
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
Flux parallèle
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 Comme vous pouvez le constater, les deux fragments sont presque identiques, mais le tri parallèle est 50 % plus rapide. Tout ce dont vous avez besoin est de changer stream() en parallelStream() .

Carte

Comme déjà mentionné, les cartes ne prennent pas en charge les flux. Au lieu de cela, Map a commencé à prendre en charge de nouvelles méthodes utiles pour résoudre des problèmes courants. Le code ci-dessus doit être intuitif : putIfAbsent nous met en garde contre l'écriture de vérifications nulles supplémentaires. forEach accepte une fonction à exécuter pour chacune des valeurs de la carte. Cet exemple montre comment les opérations sont effectuées sur les valeurs de la carte à l'aide de fonctions : Ensuite, nous apprendrons comment supprimer une entrée pour une clé donnée uniquement si elle correspond à une valeur donnée : Une autre bonne méthode : La fusion des entrées de la carte est assez simple : Fusionner soit insérera la clé/valeur dans le map , s'il n'y a pas d'entrée pour la clé donnée, soit la fonction de fusion sera appelée, ce qui modifiera la valeur de l'entrée existante. 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
Commentaires
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION