Les nouvelles syntaxes de Java 8 : les Lambda expressions

Les nouvelles syntaxes de Java 8 : les Lambda expressions

PANO_20131210_195733La sortie de Java 8 étant imminente (elle est prévue pour fin mars 2014), Rémi Forax, membre des deux Expert Groups de la JSR 292 sur invokedynamic et de la JSR 335 sur les Lambda, nous a fait le plaisir de venir à Toulouse le 10 décembre dernier pour nous parler des Lambda le temps d’une soirée.
Derrière cette expression se cache une grande évolution du langage, que certains définissent même comme une révolution !

A travers plusieurs exemples simples, Rémi nous a exposé la réflexion qui permet d’aboutir à l’élaboration de cette nouvelle syntaxe. Et bien plus que l’illumination de quelques développeurs dans leur garage, cette mise à jour est le fruit de plusieurs propositions antérieures : BGGA closure proposal (2006), CICE closure proposal (2006) et FCM closure proposal (2007). Dans cet article, je reviendrai principalement sur les apports syntaxiques que je considère comme majeurs et qui bouleverseront les habitudes des développeurs Java.

La syntaxe des pointeurs de fonctions

L’une des premières problématiques soulevée est la syntaxe des pointeurs de fonctions. En effet, si on prend l’exemple ci-dessous du calcul du min et du max, il s’avère que l’algorithme est le même à une fonction près : dans un cas Math.min et dans l’autre Math.max.

public static int min(int[] values) {
if (values.length == 0) {
throw new IllegalArgumentException();
}
int min = values[0];
for(int i = 1; i < values.length; i++) {
min = Math.min(min, values[i]);
}
return min;
}
public static int max(int[] values) {
if (values.length == 0) {
throw new IllegalArgumentException();
}
int max = values[0];
for(int i = 1; i < values.length; i++) {
max = Math.max(max, values[i]);
}
return max;
}

Il est clair qu’aujourd’hui, une factorisation simple du code consisterait à écrire une interface avec une seule méthode abstraite. Notez que ce type d’interface se dote désormais d’une annotation en Java 8 : @functionnalInterface pour indiquer au compilateur qu’il n’y a qu’une seule méthode abstraite définie dans l’interface.

@FunctionalInterface
public interface IntBinaryOperator {
public abstract int applyAsInt(int left, int right);
}

S’ensuit une méthode que l’on appellera reduce avec en paramètre notre interface à implémenter pour min et pour max.

private static int reduce(int[] values, IntBinaryOperator op) {
if (values.length == 0) {
throw new IllegalArgumentException();
}
int value = values[0];
for(int i = 1; i < values.length; i++) {
value = op.applyAsInt(value, values[i]);
}
return value;

On se retrouve donc avec une interface ne contenant qu’une seule méthode abstraite pour représenter un pointeur vers une fonction : cela est très verbeux et couteux pour l’utilisation souhaitée. Heureusement, les Lambda apportent une première nouveauté conceptuelle : une représentation pour les pointeurs de fonctions. Dans notre exemple, notre Functionnal Interface peut se décrire comme ceci :

(int,int) ->int

Cela représente un type de fonction prenant en paramètre deux int et renvoyant un int. Si on analyse notre exemple, les méthodes statiques min et max de la librairie Math appartiennent au même type de fonction. Comment les passer par référence dans notre méthode reduce? La solution est le :: en l’utilisant comme ceci : Math::min. C’est une nouvelle syntaxe qui n’a pas encore de nom en vogue (« 4 points » peut-être ?!). Ainsi, on arrive à cette simplification :

private static int reduce(int[] values, IntBinaryOperator op) {
if (values.length == 0) {
throw new IllegalArgumentException();
}
int value = values[0];
for(int i = 1; i < values.length; i++) {
value = op.applyAsInt(value, values[i]);
}
return value;
}
public static int min(int[] values) {
return reduce(values, Math::min);
}
public static int max(int[] values) {
return reduce(values, Math::max);
}

Mais la réflexion ne s’arrête pas là. Imaginez maintenant que vous souhaitez ajouter une fonction sum. Deux solutions s’offrent clairement à vous :

  • Soit définir une méthode statique que nous nommerons « les 4 points » :
Class EnclosingClass{
private static int add(int a, int b) {
return a + b;
}
public static int sum(int[] values) {
return reduce(values, EnclosingClass::add);
}
}
  • Soit définir notre méthode abstraite de l’interface IntBinaryOperator via une classe anonyme :
public static int sum(int[] values) {
return reduce(values, new IntBinaryOperator() {
@Override
public int applyAsInt(int a, int b) {
return a + b;
}
});
}

Cette dernière solution est très lourde à écrire et a beaucoup d’impacts sur les performances étant donné qu’on instancie une classe pour une seule fonction. Là encore, les Lambda apportent un grand changement syntaxique pour définir une fonction au lieu d’une classe anonyme :

IntBinaryOperator op = (a, b) -> a + b;

Ainsi, la définition d’une fonction de la plus simple des manières possibles se compose de la déclaration des paramètres entre parenthèses et de son implémentation après ->. Cela semblera familier aux développeurs C# et Scala mais a priori, toute ressemblance avec des symboles existants serait fortuite, puisque comme le dit Remi Forax, « Java is really different from C# and Scala ».
Ainsi, la manière la plus efficace et élégante d’implémenter notre fonction sum sera au final la suivante :

private static int reduce(int[] values, IntBinaryOperator op) {
if (values.length == 0) {
throw new IllegalArgumentException();
}
int value = values[0];
for(int i = 1; i < values.length; i++) {
value = op.applyAsInt(value, values[i]); }
return value; }

public static int sum(int[] values) {
return reduce(values, (a, b) - > a + b);
}

L’utilisation des API existantes avec les Lambda

Après l’exposé de ces nouvelles syntaxes, beaucoup de développeurs se demanderont comment utiliser nouvelles les API existantes avec les Lambda. Les cas les plus évidents sont les functionnals interfaces existantes comme par exemple java.util.Comparator que l’on peut illustrer avec le tri d’une table de String :

public static void main(String[] args)
{
Arrays.sort(args,(s1, s2) -> s1.compareIgnoreCase(s2));
}

L’adaptation des API standards est aussi possible (cela fera l’objet d’un autre article). Par ailleurs, Java 8 revisite les mots clés default et final, mais l’objectif de cet article était de présenter les améliorations syntaxiques majeures apportées par les Lambda.

PANO_20131210_195733

Merci à Rémi Forax pour sa présentation !

Retrouvez-le sur son blog,

Suivez-le sur Twitter,

Et merci au Toulouse Jug

Crédit Photos : Toulouse JUG

Share
Sandy RABIAZA
Sandy RABIAZA

2638

Leave a Reply

Your email address will not be published. Required fields are marked *