Blog Arolla

Java 8 : Le projet Lambda (part 2)

Introduction

Dans le précédent billet, nous avons introduit les objectifs du projet Lambda [sur le court terme il s’agit de simplifier les itérations internes pour la manipulation des collections et sur le long terme d’intégrer dans Java le style de programmation fonctionnelle]. La plupart des changements portent sur l'interface Collection qui n'a pas connu une grande évolution depuis son apparition dans le JDK 1.2. On traitera dans cette partie, avec un peu plus de détails, des concepts clés ajoutés à la plateforme pour faciliter l'intégration des Lambda expressions. Il s'agit :

  • des VEM (Virtual Extension Method)
  • de l'Interface Stream
  • des Interface Fonctionnelles
  • des Méthodes de références
  • de l'Inférence de type

Les VEM (Virtual Extension Method)

L’interface Collection, qui est l’une des plus concernées par le projet, va être retouchée pour prendre en compte les nouvelles opérations. Actuellement plusieurs librairies comme Hibernate offrent déjà des implémentations de cette interface. La grande limite avec les interfaces est qu’une fois implémentées elles deviennent réfractaires à l’évolution; modifier une interface revient à casser ses différentes implémentations. La question est donc d’ajouter de nouvelles méthodes aux interfaces existantes sans toucher aux implémentations.

Pour cela plusieurs solutions ont été étudiées :

  • Ajouter des méthodes statiques à la classe Collections (limite : ne respecte pas vraiment le paradigme Orienté Objet).
  • Ajouter de nouvelles interfaces dédiées aux Lambda Exp, par exemple créer une interface Collection 2(inconvénient : aucun apport pour les programmes séquentiels).
  • Ajouter les nouveaux opérateurs (map, filter, fold) aux implémentations des collections (inconvénient : mauvaise pratiques de la POO, entraîne des usages abusifs de l’opérateur cast, …).

Finalement c’est vers les méthodes virtuelles d’extensions que le choix a été porté.

L’objectif des méthodes virtuelles, anciennement connues sous le nom de « public defender methods », est d’assurer la compatibilité ascendante en faisant évoluer une interface sans casser ses différentes implémentations. Le principe est de fournir à une interface l’implémentation par défaut d’une de ses méthodes, et cette dernière sera utilisée si aucune implémentation n’est définie par la classe qui implémente l’interface.

Exemple :

	public interface A{

		default void foo(){

		}

	}

	public class B implements A {

	}

	A a = new B();

	a.foo();

B hérite implicitement de la méthode foo() (tout comme des méthodes equals(), toString(), hashCode()…) .

Exemple avec List :
On veut étendre la méthode sort() de l’interface List.

	interface List {
		// ...
		default void sort(Comparator<? super T> cmp) {
			Collections.sort(this, cmp);
		}
	}

Le problème du diamant

La notion de VEM rend ainsi possible ce qui était jusque-là impossible en Java, à savoir  l’héritage multiple – rien n’empêche d’implémenter deux interfaces qui fournissent des méthodes virtuelles – et amène en même temps avec elle un problème commun à résoudre, « The diamond problem ». Que faire quand une classe hérite de deux interfaces qui ont des méthodes virtuelles identiques.

Image1problème du diamant

	interface A {
		default void foo() {
			// …
		}
	}

	interface B extends A {

	}

	interface C extends A {

	}

	class D implements B, C {

	}

Quand le Classloader charge D il vérifie si toutes les méthodes des interfaces B et C sont implémentées, si c’est le cas aucune méthode virtuelle ne sera utilisée, sinon le Classloader essaye de compléter avec les méthodes virtuelles des interfaces B et C. En cas d’ambiguïté (deux interfaces avec la même vem) une exception est levée.

Pour résoudre ce problème du diamant on invoque explicitement la méthode virtuelle à hériter.

	class D implements B, C {

		void foo() {

			A.super.foo() // ou B.super.foo()

		}

	}

En dehors de ce problème du diamant, l’introduction de la notion de VEM n’a pas été un choix unanimement apprécié, notamment sur le fait qu’il casse la notion de « contrat » que pouvait représenter une interface entre un fournisseur et un client.

L’interface Stream

Il y a Linq pour .Net, Collections API pour Scala et Stream pour Java. C’est la nouvelle interface qui va prendre en charge le style fonctionnel de l’interface Collection, map(), filter(), reduce(), … Son choix permet de garantir la compatibilité ascendante. Au début il était prévu d’ajouter les fonctions map, filter et reduce à l’interface Collection comme des VEM, mais rien ne garantit qu’il n’existe pas déjà des implémentations de Collection, avec des méthodes map, filter, reduce de mêmes signatures.

Ce choix permet aussi de garder une certaine cohérence : faire une séparation entre les anciennes méthodes de l’interface Collection et les nouvelles qui présentent un style fonctionnel.

En langage fonctionnel, le Stream est défini comme une séquence de valeurs. Il se trouve dans le package, java.lang.stream avec les opérateurs primitifs (IntStream, LongStream, DoubleStream). Dans JDK 8 le Stream joue le même rôle que l’interface Iterator, il permet d’accéder aux données d’une Collection, à la différence qu’il ne fournit aucun mécanisme de stockage, il offre juste un moyen de créer des séquences de données (par exemple les entiers premiers, les nombres pairs, éléments d’une collection satisfaisant un critère, …) Les données ne sont évaluées que quand le programme en a vraiment besoin (lazy evaluation).

	public interface Stream {
		Stream 	filter(Predicate<? super T> predicate) ;
		 Stream  map(Function<? super T,? extends R> mapper) ;
		Optional 	reduce(BinaryOperator accumulator) ;
		Optional 	min(Comparator<? super T> comparator) ;
		Optional 	max(Comparator<? super T> comparator) ;
		long count() ;
		...
	}

L’évaluation lazy a deux avantages, il permet de :

  • représenter un ensemble infini
  • paralléliser des opérations

L’interface Collection est étendue avec la méthode stream(). Et toute la magie des pipelines se trouve à ce niveau. Sur le Stream retourné on peut enchainer plusieurs appels des fonctions filter(), map(), forEach(), … et terminer par un count(), min(), max(), …. Comme nous l’avons évoqué dans la première partie, pour passer d’un traitement séquentiel à un traitement « parallèle » de stream() ce ne sera qu’une question de syntaxe, parallelStream().

	interface Collection {
		// …
		// Returns a sequential Stream with this collection as its source
		default Stream stream() {
			return StreamSupport.stream(spliterator(), false);
		}
		//Returns a possibly parallel Stream with this collection as its source.
		default Stream parallelStream() {
			return StreamSupport.stream(spliterator(), false);
		}
	}

Exemple :

	people.stream()
					.filter(Person p -> p.getAge() >= MAX_AGE))
					.forEach(Person p -> p.setSalary(p.getSalary() * x))
					.mapToInt( p -> p.getSalary())
					.reduce(0, (x,y) -> x+y);

Functional Interface

L’Interface Fonctionnelle fait partie des grandes nouveautés introduites dans la nouvelle version de la plateforme. L’objectif est de typer les expressions lambda.

Dans les exemples fournis dans la première partie de la série on a remplacé une classe anonyme par des lambda expressions pour simplifier la manipulation des collections. Ces classes anonymes qui ont la particularité de n’avoir qu’une seule méthode sont souvent passées en paramètre à des méthodes. C’est le cas pour les interfaces Runnable, Comparator, Callable, … Jusque-là connues sous le nom de SAM (Simple Abstract Method), elles seront les «Functionnal Interface » de Java 8.

Exemple :

	List employees = new ArrayList()<>;

	employees.filter(new Predicate() {
		public boolean test(Employee emp) {
			return emp.age >= 45;
		}
	});

remplacé par :

employees.stream().filter (emp -> emp.age >= 45);

On dispose déjà, depuis les précédentes versions, dans le package java.lang des interfaces fonctionnelles suivantes :

public interface Runnable { void run(); }

public interface Callable { V call() throws Exception; }

public interface ActionListener { void actionPerformed(ActionEvent e); }

public interface Comparator { int compare(T o1, T o2); boolean equals(Object obj); }

Le langage s’enrichit également d’un nouveau package java.util.function qui vient avec les «Functionnal Interface » de base, Consumer, Mapper, Predicate, ….

1. Predicate : Effectue un test sur l’objet en paramètre et retourne un booléen.

	public interface Predicate {
		boolean test(T t);
	}

Exemple d’utilisation :

	List employees = new ArrayList<>();

	employees.filter(new Predicate() {
		public boolean test(Employee emp) {
			return emp.age >= 45;
		}
	});

Version lambda :

employees.stream().filter (emp -> emp.age >= 45);

2. Consumer : Effectue une action sur l’objet en paramètre.

	public interface Consumer {
		void accept(T t);
	}

Exemple d’utilisation :

	employees.forEach(new Consumer() {
		public void accept(Employee emp) {
			System.out.println(emp);
		}
	});

Version lambda :

employees.stream().forEach (emp -> System.out.println(emp) );

3. Mapper : Applique une fonction sur un objet T et retourne un objet de type R.

	public interface Mapper<T,R> {
		R map(T t);
	}

Exemple d’utilisation :

	List employees = new ArrayList<>();

	List ages = employees.map(new Mapper() {
		public String map(Employee emp) {
			return emp.age;
		}
	});

Version lamba :

List = employees.stream().map(emp -> emp.age)

L’Annotation @FuntionlInterface

En Java pour vérifier qu’une méthode a été « overridée » on peut utiliser l’annotation @Override. L’annotation @FuntionlInterface joue le même rôle dans Java 8, elle permet de déterminer qu’une interface peut être inter changée avec une expression lambda, autrement dit elle est fonctionnelle. Son utilisation n’est toutefois pas obligatoire.

L’inférence de type

L’inférence de type, l’une des principales caractéristiques de la programmation fonctionnelle, permet de déterminer dynamiquement le type d’une expression. En effet le type d’une expression lambda est une Interface Fonctionnelle. A la compilation une expression lambda est remplacée par une implémentation de son interface fonctionnelle. Or dans la définition d’une expression lambda l’interface fonctionnelle de l’expression n’intervient pas, le type n’est pas précisé. De ce fait, d’un contexte à un autre, l’expression x -> 2 * x peut correspondre à différentes instances d’interfaces fonctionnelles.

Dans cet exemple :

	public interface IntOperation {
		int operate(int i);
	}

	public interface DoubleOperation {
		double operate(double i);
	}

Les deux écritures suivantes sont toutes correctes :

DoubleOperation do = x -> x * 2;

IntOperation io = x -> x * 2;

Dans le premier exemple l’expression lambda a pour type l’interface DoubleOperation, dans le second l’interface IntOperation. Pour qu’une interface soit candidate il suffit qu’elle soit une Interface Fonctionnelle et ait la même signature que l’expression lambda. Le compilateur utilise le mécanisme d’inférence de type pour déterminer dynamiquement la bonne interface fonctionnelle en fonction du contexte dans lequel l’expression est utilisée. Le contexte peut être représenté par la déclaration, la signature de la méthode, le type de retour, l’affectation ou le cast. Pour chacun de ces cas on a un type d’inférence correspondant.

Inférence par déclaration : Le type est déterminé à la déclaration

	public interface IntOperation {
		int operate(int i);
	}

	public interface DoubleOperation {
		double operate(double i);
	}

	DoubleOperation doubleOp = x -> x * 2;

	IntOperation intOp = x -> x * 2;

Inférence par Affectation : Le type est déterminé à partir du type de la variable de gauche.

	DoubleOperation doubleOp  ;
	doubleOp  = x -> x * 2;

	IntOperation intOp ;
	intOp = x -> x * 2;

Inférence par Arguments de method ou Constructeur :

	Int x =  new Thread (() -> { System.out.println("Running in different thread");}
	).start();

La classe Thread a plusieurs constructeurs, dont Thread(Runnable target) , le seul à avoir un argument comme paramètre. Cet argument Runnable étant une Interface Fonctionnelle, et ayant la même signature que l’expression lambda, il sera le type de l’expression () -> { System.out.println(« Running in different thread »);}.

Inf par type de retour : Le type de l’expression correspond au type de retour

	public Runnable toDoLater() {
		return () -> System.out.println("later");;
	}

Inférence par Cast

On l’utilise pour éviter des problèmes d’ambiguïté, dans des situations ou plusieurs interfaces fonctionnelles sont candidates.

	public interface Runnable {
		void run();
	}

	public interface Callable {
		V call() throws Exception;
	}

	class TestCast{
		public static void invoke(Callable callable ){
			callable.call();
		}
		public static void invoke(Runnable runnable){
			runnable.run();
		}
	}

	TestCast.invoke(() -> System.out.println("Done") );

Ici le paramètre peut autant être un callable qu’un runnable. Pour lever l’ambiguïté on utilise l’opérateur Cast.

	Object runnable =  (Runnable) () -> { System.out.println("Hello"); };
	Object callable = (Callable) () ->{ System.out.println("Done"); };

Les méthodes de références

Comme nous l’avons vu jusque-là, une lambda exp peut être considérée comme une implémentation de la méthode abstraite d’une classe anonyme. Les méthodes de références sont – elles aussi – une autre manière de fournir cette implémentation, toujours dans l’optique de gagner plus encore en simplicité en utilisant des méthodes déjà définies sur des classes ou des objets.

La syntaxe est la suivante : Class_name ::method

Exemple : Pour trier une collection

public static void sort(T[] a, Comparator c);

La méthode sort() prend en paramètre une collection et une interface fonctionnelle de type Comparable. Avec une classe anonyme on a le code suivant :

List employees = new ArrayList()<>;
//...
Arrays.sort(employees, new Comparable () {
      @Override
      public int compareTo(Object o) {
        return 0;  
      }
    });

On peut remplacer la classe anonyme (ou Interface Fonctionnelle) par une expression lambda compatible.

Arrays.sort(employees, (Employee a, Employee b) -> a.compareTo(b)));

Ou tout simplement par une « méthode de référence » :

Arrays.sort(employees, Employee ::empCompareTo);

On n’a pas besoin d’implémenter l’interface Comparable sur la classe Employee. Il suffit juste que la signature de la méthode empCompareTo() soit compatible avec la méthode compareTo() de l'interface Comparable. Si la classe a plusieurs méthodes qui portent le même nom, le choix de la bonne méthode se fera par inférence de type.

On a quatre types de méthodes de référence :

  • Référence sur une méthode statique : Object::toString équivaut à x -> x.toString()
  • Référence sur une méthode d’objet : x::toString équivaut à () -> x.toString()
  • Référence sur une méthode d'une instance d'un type particulier : String::valueOf équivaut à x -> String.valueOf(x)
  • Référence sur un constructeur : ArrayList::new équivaut à () -> new ArrayList<>()

Conclusion

Dans cette série de deux articles nous avons présenté le projet Lambda, son objectif et les évolutions majeures qu’il apporte à la plateforme Java. Java 8 c’est donc le mariage entre le Fonctionnel et l’Orienté Objet pour faciliter la parallélisation dans les programmes et rendre le langage moins verbeux par l’utilisation de lambda expression à la place des classes anonymes. D’autres nouveautés seront également disponibles dans la nouvelle plateforme, notamment des améliorations sur les API Date et Time (http://openjdk.java.net/projects/jdk8/features).

Sur le futur de Java, contrairement aux spéculations de ces dernières années qui prédisaient sa fin, tous les signaux sont verts pour les prochaines années. Il reste le langage le plus populaire avec une communauté très dynamique et le plus multiplateforme (Desktop, Server, Cloud et Mobile). Oracle a dépensé des centaines de millions de dollars pour apporter du sang neuf à Java et le mettre au même niveau que ses concurrents.

A ceux qui restent encore dubitatifs je conseille une lecture de l'avis de Andrew Binstock sur ce prétendu déclin.

But when it comes to Java being in some kind of long-term decline, I see little supporting evidence. The recent JavaOne show, that annual jamboree of Java coders, was clearly larger and better attended than it has been in either of the last two years. Vendors on the exhibiting floor with whom I spoke were unanimous (truly not a single exception) in saying that traffic, leads, and inquiries were up significantly over last year, which itself was better than the year before.

Références

State of the Lambda : http://cr.openjdk.java.net/~briangoetz/lambda/lambda-state-final.html

Lambda Faq : http://www.lambdafaq.org/

Java docs : http://download.java.net/jdk8/docs/

Plus de publications

3 comments for “Java 8 : Le projet Lambda (part 2)

  1. carlos
    25 février 2014 at 12 h 00 min

    Merci pour cette présentation claire et bien illustrée.