Fonctionnalités de Java 8 avec des exemples

Java 8 a été publié le 18 mars 2014. C’est il y a longtemps, mais encore beaucoup de projets fonctionnent sur Java 8. C’est parce que c’était une version majeure avec beaucoup de nouvelles fonctionnalités. Regardons toutes les fonctionnalités passionnantes et majeures de Java 8 avec du code exemple.

Aperçu rapide des fonctionnalités de Java 8

Certaines des fonctionnalités importantes de Java 8 sont;

  1. La méthode forEach() dans l’interface Iterable
  2. Les méthodes par défaut et statiques dans les interfaces
  3. Interfaces fonctionnelles et expressions Lambda
  4. L’API Stream Java pour les opérations de données en vrac sur les collections
  5. L’API Java Time
  6. Améliorations de l’API Collection
  7. Améliorations de l’API de concurrence
  8. Améliorations de l’API Java IO

Jetons un coup d’œil sur ces fonctionnalités de Java 8. Je fournirai des extraits de code pour mieux comprendre les fonctionnalités de manière simple.

1. Méthode forEach() dans l’interface Iterable

Chaque fois que nous avons besoin de parcourir une Collection, nous devons créer un Iterator dont le seul but est de parcourir, puis nous avons la logique métier dans une boucle pour chacun des éléments de la Collection. Nous pourrions obtenir une ConcurrentModificationException si l’itérateur n’est pas utilisé correctement.

Java 8 a introduit la méthode forEach dans l’interface java.lang.Iterable afin que lors de l’écriture du code, nous nous concentrions sur la logique métier. La méthode forEach prend un objet java.util.function.Consumer en argument, ce qui aide à avoir notre logique métier à un emplacement séparé que nous pouvons réutiliser. Voyons l’utilisation de forEach avec un exemple simple.

package com.journaldev.java8.foreach;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.function.Consumer;
import java.lang.Integer;

public class Java8ForEachExample {

	public static void main(String[] args) {
		
		// création d'une Collection d'exemple
		List<Integer> myList = new ArrayList<Integer>();
		for(int i=0; i<10; i++) myList.add(i);
		
		// parcours à l'aide d'un Iterator
		Iterator<Integer> it = myList.iterator();
		while(it.hasNext()){
			Integer i = it.next();
			System.out.println("Iterator Value::"+i);
		}
		
		// parcours via la méthode forEach de l'interface Iterable avec une classe anonyme
		myList.forEach(new Consumer<Integer>() {

			public void accept(Integer t) {
				System.out.println("forEach anonymous class Value::"+t);
			}

		});
		
		// parcours avec une implémentation de l'interface Consumer
		MyConsumer action = new MyConsumer();
		myList.forEach(action);
		
	}

}

// Implémentation de Consumer qui peut être réutilisée
class MyConsumer implements Consumer<Integer>{

	public void accept(Integer t) {
		System.out.println("Consumer impl Value::"+t);
	}
}

Le nombre de lignes peut augmenter mais la méthode forEach aide à avoir la logique d’itération et la logique métier à des endroits distincts, ce qui résulte en une meilleure séparation des préoccupations et un code plus propre.

2. Méthodes par défaut et statiques dans les interfaces

Si vous lisez attentivement les détails de la méthode forEach, vous remarquerez qu’elle est définie dans l’interface Iterable mais nous savons que les interfaces ne peuvent pas avoir de corps de méthode. À partir de Java 8, les interfaces sont améliorées pour avoir une méthode avec une implémentation. Nous pouvons utiliser le mot-clé default et static pour créer des interfaces avec une implémentation de méthode. L’implémentation de la méthode forEach dans l’interface Iterable est la suivante :

default void forEach(Consumer<? super T> action) {
    Objects.requireNonNull(action);
    for (T t : this) {
        action.accept(t);
    }
}

Nous savons que Java ne fournit pas l’héritage multiple dans les classes car cela entraîne un problème de diamant. Alors comment cela sera-t-il géré avec les interfaces maintenant que les interfaces sont désormais similaires aux classes abstraites?

La solution est que le compilateur lancera une exception dans ce scénario et nous devrons fournir la logique d’implémentation dans la classe implémentant les interfaces.

package com.journaldev.java8.defaultmethod;

@FunctionalInterface
public interface Interface1 {

	void method1(String str);
	
	default void log(String str){
		System.out.println("I1 logging::"+str);
	}
	
	static void print(String str){
		System.out.println("Printing "+str);
	}
	
	//Tenter de remplacer la méthode Object donne une erreur de compilation comme
	//"Une méthode par défaut ne peut pas remplacer une méthode de java.lang.Object"
	
//	default String toString(){
//		return "i1";
//	}
	
}
package com.journaldev.java8.defaultmethod;

@FunctionalInterface
public interface Interface2 {

	void method2();
	
	default void log(String str){
		System.out.println("I2 logging::"+str);
	}

}

Notez que les deux interfaces ont une méthode commune log() avec une logique d’implémentation.

package com.journaldev.java8.defaultmethod;

public class MyClass implements Interface1, Interface2 {

	@Override
	public void method2() {
	}

	@Override
	public void method1(String str) {
	}

	//MyClass ne compilera pas sans avoir sa propre implémentation log()
	@Override
	public void log(String str){
		System.out.println("MyClass logging::"+str);
		Interface1.print("abc");
	}
	
}

Comme vous pouvez le voir, Interface1 a une implémentation de méthode statique qui est utilisée dans MyClass.log(). Java 8 utilise largement les méthodes par défaut et statiques dans l’API Collection et les méthodes par défaut sont ajoutées pour que notre code reste rétrocompatible.

Si une classe dans la hiérarchie a une méthode avec la même signature, alors les méthodes par défaut deviennent sans importance. Object est la classe de base, donc si nous avons des méthodes equals(), hashCode() par défaut dans l’interface, elles deviendront sans importance. C’est pourquoi, pour une meilleure clarté, les interfaces ne sont pas autorisées à avoir des méthodes par défaut Object.

Pour plus de détails sur les changements d’interface dans Java 8, veuillez lire Les changements d’interface Java 8.

3. Interfaces Fonctionnelles et Expressions Lambda

Si vous remarquez le code d’interface ci-dessus, vous remarquerez l’annotation @FunctionalInterface. Les interfaces fonctionnelles sont un nouveau concept introduit dans Java 8. Une interface avec exactement une méthode abstraite devient une interface fonctionnelle. Nous n’avons pas besoin d’utiliser l’annotation @FunctionalInterface pour marquer une interface comme une interface fonctionnelle.

L’annotation @FunctionalInterface est une facilité pour éviter l’ajout accidentel de méthodes abstraites dans les interfaces fonctionnelles. Vous pouvez le considérer comme l’annotation @Override et il est recommandé de l’utiliser. java.lang.Runnable avec une seule méthode abstraite run() est un excellent exemple d’une interface fonctionnelle.

Un des principaux avantages de l’interface fonctionnelle est la possibilité d’utiliser des expressions lambda pour les instancier. Nous pouvons instancier une interface avec une classe anonyme mais le code semble volumineux.

Runnable r = new Runnable(){
			@Override
			public void run() {
				System.out.println("My Runnable");
			}};

Comme les interfaces fonctionnelles n’ont qu’une seule méthode, les expressions lambda peuvent facilement fournir l’implémentation de la méthode. Nous devons simplement fournir les arguments de méthode et la logique métier. Par exemple, nous pouvons écrire l’implémentation ci-dessus en utilisant une expression lambda comme suit:Si vous avez une seule déclaration dans l’implémentation de la méthode, nous n’avons pas besoin de crochets non plus. Par exemple, la classe anonyme Interface1 ci-dessus peut être instanciée à l’aide d’une lambda comme suit:

Runnable r1 = () -> {
			System.out.println("My Runnable");
		};

Si vous avez une seule instruction dans la mise en œuvre de la méthode, nous n’avons pas besoin des accolades non plus. Par exemple, la classe anonyme de l’interface Interface1 ci-dessus peut être instanciée à l’aide d’une expression lambda comme suit :

Interface1 i1 = (s) -> System.out.println(s);
		
i1.method1("abc");

Les expressions lambda sont donc un moyen de créer facilement des classes anonymes d’interfaces fonctionnelles. Il n’y a pas de bénéfices en matière de performances à utiliser des expressions lambda, donc je l’utiliserai avec précaution car je n’ai pas peur d’écrire quelques lignes de code supplémentaires.

A new package java.util.function has been added with bunch of functional interfaces to provide target types for lambda expressions and method references. Lambda expressions are a huge topic, I will write a separate article on that in the future.

Vous pouvez lire le tutoriel complet à Tutoriel sur les Expressions Lambda Java 8.

4. API Java Stream pour les opérations de données en masse sur les collections

A new java.util.stream has been added in Java 8 to perform filter/map/reduce like operations with the collection. Stream API will allow sequential as well as parallel execution. This is one of the best features for me because I work a lot with Collections and usually with Big Data, we need to filter out them based on some conditions.

L’interface Collection a été étendue avec stream() et parallelStream() les méthodes par défaut pour obtenir le Stream pour l’exécution séquentielle et parallèle. Voyons leur utilisation avec un exemple simple.

package com.journaldev.java8.stream;

import java.util.ArrayList;
import java.util.List;
import java.util.stream.Stream;

public class StreamExample {

	public static void main(String[] args) {
		
		List<Integer> myList = new ArrayList<>();
		for(int i=0; i<100; i++) myList.add(i);
		
		 // flux séquentiel 
		Stream<Integer> sequentialStream = myList.stream();
		
		 // flux parallèle 
		Stream<Integer> parallelStream = myList.parallelStream();
		
		 //utilisation de lambda avec l'API Stream, exemple de filtrage 
		Stream<Integer> highNums = parallelStream.filter(p -> p > 90);
		 //utilisation de lambda dans forEach 
		highNums.forEach(p -> System.out.println("High Nums parallel="+p));
		
		Stream<Integer> highNumsSeq = sequentialStream.filter(p -> p > 90);
		highNumsSeq.forEach(p -> System.out.println("High Nums sequential="+p));

	}

}

Si vous exécutez le code exemple ci-dessus, vous obtiendrez un résultat similaire à celui-ci :

High Nums parallel=91
High Nums parallel=96
High Nums parallel=93
High Nums parallel=98
High Nums parallel=94
High Nums parallel=95
High Nums parallel=97
High Nums parallel=92
High Nums parallel=99
High Nums sequential=91
High Nums sequential=92
High Nums sequential=93
High Nums sequential=94
High Nums sequential=95
High Nums sequential=96
High Nums sequential=97
High Nums sequential=98
High Nums sequential=99

Notez que les valeurs traitées en parallèle ne sont pas dans l’ordre, donc le traitement parallèle sera très utile lors de la manipulation de grandes collections.

Il n’est pas possible de couvrir tout ce qui concerne l’API Stream dans ce post, vous pouvez tout lire sur l’API Stream à Exemple de tutoriel sur l’API Stream Java 8.

5. API Java Time

Il a toujours été difficile de travailler avec Date, Time et Time Zones en java. Il n’y avait pas d’approche standard ou d’API en java pour la date et l’heure en Java. Une des bonnes additions dans Java 8 est le package java.time qui va faciliter le processus de travail avec le temps en java.

En regardant simplement les packages de l’API Java Time, je peux sentir qu’ils seront très faciles à utiliser. Il a quelques sous-packages java.time.format qui fournit des classes pour imprimer et analyser les dates et les heures et java.time.zone qui fournit un soutien pour les fuseaux horaires et leurs règles.

La nouvelle API Temps préfère les énumérations aux constantes entières pour les mois et les jours de la semaine. Une des classes utiles est DateTimeFormatter pour convertir les objets DateTime en chaînes de caractères. Pour un tutoriel complet, rendez-vous à Exemple de tutoriel sur l’API Java Date Time.

6. Améliorations de l’API de collection

Nous avons déjà vu la méthode forEach() et l’API Stream pour les collections. Certaines nouvelles méthodes ajoutées dans l’API Collection sont :

  • Iterator méthode par défaut forEachRemaining(Consumer action) pour effectuer l’action donnée pour chaque élément restant jusqu’à ce que tous les éléments aient été traités ou que l’action lève une exception.
  • Collection méthode par défaut removeIf(Predicate filter) pour supprimer tous les éléments de cette collection qui satisfont le prédicat donné.
  • Collection méthode spliterator() renvoyant une instance de Spliterator qui peut être utilisée pour parcourir les éléments de manière séquentielle ou en parallèle.
  • Map méthodes replaceAll(), compute(), merge().
  • Amélioration des performances pour la classe HashMap avec collisions de clés

7. Améliorations de l’API de concurrence

Certaines améliorations importantes de l’API concurrente sont :

  • ConcurrentHashMap méthodes compute(), forEach(), forEachEntry(), forEachKey(), forEachValue(), merge(), reduce() et search().
  • CompletableFuture qui peut être explicitement terminé (définissant sa valeur et son état).
  • Executors newWorkStealingPool() méthode pour créer un pool de threads de vol de travail en utilisant tous les processeurs disponibles comme niveau cible de parallélisme.

8. Améliorations des E/S Java

Certaines améliorations des E/S que je connais sont :

  • Files.list(Path dir) qui renvoie un Stream populaire de manière paresseuse, les éléments étant les entrées dans le répertoire.
  • Files.lines(Path path) qui lit toutes les lignes d’un fichier comme un Stream.
  • Files.find() qui renvoie un Stream qui est populaire de manière paresseuse avec Path en recherchant des fichiers dans un arbre de fichiers enraciné à un fichier de départ donné.
  • BufferedReader.lines() qui renvoie un Stream, les éléments étant les lignes lues à partir de ce BufferedReader.

Améliorations diverses du noyau API Java 8

Certaines améliorations d’API diverses qui pourraient être utiles sont :

  1. ThreadLocal méthode statique withInitial(Supplier fournisseur) pour créer des instances facilement.
  2. L’interface Comparator a été étendue avec de nombreuses méthodes statiques et par défaut pour l’ordre naturel, l’ordre inverse, etc.
  3. Les méthodes min(), max() et sum() dans les classes d’emballage Integer, Long et Double.
  4. Les méthodes logicalAnd(), logicalOr() et logicalXor() dans la classe Boolean.
  5. ZipFile.stream() méthode pour obtenir un Stream ordonné sur les entrées du fichier ZIP. Les entrées apparaissent dans le Stream dans l’ordre dans lequel elles apparaissent dans le répertoire central du fichier ZIP.
  6. Plusieurs méthodes utilitaires dans la classe Math.
  7. La commande jjs est ajoutée pour invoquer l’Engine Nashorn.
  8. La commande jdeps est ajoutée pour analyser les fichiers de classe
  9. Le pont JDBC-ODBC a été supprimé.
  10. L’espace mémoire PermGen a été supprimé

C’est tout pour les fonctionnalités de Java 8 avec des programmes d’exemple. Si j’ai manqué certaines fonctionnalités importantes de Java 8, faites-le moi savoir via les commentaires.

Source:
https://www.digitalocean.com/community/tutorials/java-8-features-with-examples