Accueil Nos publications Blog Java 8 – JSR-335 – 2/3 – Références et Defenders

Java 8 – JSR-335 – 2/3 – Références et Defenders

Les expressions Lambda ne sont pas les seules nouveautés de la JSR-335. Avec Java 8, il sera possible d’utiliser des références de méthode et de constructeur, à la place d’expressions Lambda, mais aussi d’avoir des “defender methods” (nommées aussi Virtual Extension Methods) permettant, entre autres, une évolution simplifiée des APIs standards.

Note importante : La JSR n’étant pas encore à l’état « final », il est possible qu’il y ait des différences entre les fonctionnalités décrites dans cet article et les fonctionnalités proposées dans la version finale.

Pour cet article la JDK 8 build b64 avec support des lambdas a été utilisée. Le dernier build peut être téléchargé depuis java.net.

L’intégralité des sources est disponible via Github.

Références de méthode et de constructeur

Une référence de méthode ou de constructeur est utilisée pour définir une méthode ou un constructeur en tant qu’implémentation de la méthode abstraite d’une interface fonctionnelle, à l’aide du nouvel opérateur « :: ».

Tous constructeurs et méthodes peuvent être utilisés comme référence, à condition qu’il n’y ait aucune ambigüité.

Lorsque plusieurs méthodes ont le même nom, ou lorsqu’une classe a plus d’un constructeur, la méthode ou le constructeur approprié est sélectionné en fonction de la méthode abstraite de l’interface fonctionnelle à laquelle fait référence l’expression.

Classes et interfaces utilisées dans les parties suivantes :


        /**
         * org.isk.references.part1.Point.java
         */
        public class Point {
public int x;
public int y;

public Point() {
super();
}

public Point(int x, int y) {
super();
this.x = x;
this.y = y;
}
        }

        /**
         * org.isk.references.part1.PointMover.java
         */
        public interface PointMover {
void movePoint(Point point);
        }

        /**
         * org.isk.references.part1.PointReader.java
         */
        public interface PointReader {
Point getPoint();
        }

        /**
         * org.isk.references.part1.PointHolder.java
         */
        public class PointHolder {
public Point point;

public PointHolder() {
this.point = new Point();
point.x = 10;
point.y = 20;
}

public PointHolder(Point point) {
this();
point.x = this.point.x;
point.y = this.point.y;
}

public Point buildPoint() {
return new Point(5, 15);
}

public static Point buildStaticPoint() {
return new Point(5, 15);
}
        }
        

Méthodes statiques

Format : [Classe]::[méthode], où [Classe] est le nom d’une classe et [méthode] est le nom d’une méthode statique de la classe.


        /**
         * org.isk.references.ReferencesTest.java
         */

        @Test
        public void referenceMethod1() {
final PointReader fromMethod = PointHolder::buildStaticPoint;
final Point point = fromMethod.getPoint();

Assert.assertEquals(new Point(5, 15), point);
        }
        

La ligne suivante :


        final PointReader fromMethod = PointHolder::buildStaticPoint;
        

est équivalente à :


        final PointReader fromMethod = () -> new Point(5, 15);
        

Méthodes d’instance d’un objet en particulier

Format : [objet]::[méthode], où [objet] est le nom d’une instance d’une classe et [méthode] est le nom d’une méthode d’instance de cette classe.


        /**
         * org.isk.references.ReferencesTest.java
         */

        @Test
        public void referenceMethod2() {
final PointHolder pointHolder = new PointHolder();
final PointReader fromMethod = pointHolder::buildPoint;
final Point point = fromMethod.getPoint();

Assert.assertEquals(new Point(5, 15), point);
        }

        //-------------------------------------------------------------

        public Point buildPointUsingThis() {
final PointHolder pointHolder = new PointHolder();
return pointHolder.point;
        }

        @Test
        public void referenceMethod3() {
final PointReader fromMethod = this::buildPointUsingThis;
final Point point = fromMethod.getPoint();

Assert.assertEquals(new Point(10, 20), point);
        }
        

La ligne suivante :


        final PointReader fromMethod = pointHolder::buildPoint;
        

est équivalente à :


        final PointReader fromMethod = () -> new Point(5, 15);
        

Vous pouvez aussi noter que l’utilisation de this:: est autorisée (ligne 23), tout comme super::

Utiliser une méthode d’une instance particulière n’a aucun impact si la méthode agit telle une méthode statique. En revanche, si elle accède aux champs de la classe, le résultat est complétement différent. Une fois encore utiliser un tel mécanisme signifie chercher les ennuis.


        /**
         * org.isk.references.part2.ReferenceMethodInstance.java
         */
        public class ReferenceMethodInstance {
private int index;

public ReferenceMethodInstance() {
this.index = 0;
}

public int increment() {
return ++this.index;
}
        }

        /**
         * org.isk.references.part2.Incrementer.java
         */
        public interface Incrementer {
int inc();
        }

        /**
         * org.isk.references.ReferenceInstanceTest.java
         */
        public class ReferenceInstanceTest {
@Test
public void increment() {
final ReferenceMethodInstance rmi = new ReferenceMethodInstance();

// Incrémente la valeur de 'index' de l’instance rmi
int index = rmi.increment();
Assert.assertEquals(1, index);

final Incrementer incrementer = rmi::increment;

// Incrémente la valeur de 'index' de l’instance rmi
// sqns y accéder directement
index = incrementer.inc();
Assert.assertEquals(2, index);

// Incrémente la valeur de 'index' de l’instance rmi
index = rmi.increment();
Assert.assertEquals(3, index);
}
        }
        

Constructeurs

Format : [Classe]::new, où [Classe] est le nom d’une classe.


        /**
         * org.isk.references.ReferencesTest.java
         */
        @Test
        public void referenceConstructor() {
final PointMover fromConstructor = PointHolder::new;

final Point point = new Point(1, 1);
fromConstructor.movePoint(point);

Assert.assertEquals(new Point(10, 20), point);
        }
        

La ligne suivante :


        final PointMover fromConstructor = PointHolder::new;
        

est équivalente à :


        final PointMover fromConstructor = () -> new Point(10, 20);
        

Méthodes d’instance d’aucun objet en particulier

Cette fonctionnalité est probablement la plus compliquée de la JSR. Jusqu’à présent, nous avons vu que les expressions Lambda, les méthodes et les constructeurs référencés doivent avoir exactement les mêmes paramètres que la méthode abstraite de l’interface fonctionnelle. Avec les références de méthodes d’instance d’aucun objet en particulier, ce n’est plus le cas. Le premier paramètre de méthode abstraite de l’interface fonctionnelle doit être du type implémentant la méthode et cette dernière ne doit pas inclure ce paramètre. S’il est utilisé, il doit être accédé par le mot clé this .

Format : [Classe]::[méthode], où [Classe] est le nom d’une classe et [méthode] est le nom d’une méthode d’instance de la classe.

Sans plus attendre voyons un exemple :


        /**
         * org.isk.references.part3.Number.java
         */
        public class Number {
public int num;

public Number(int num) {
this.num = num;
}

public int getInt() {
return this.num;
}

public int compare(Number numToCompareTo) {
if (this.num == numToCompareTo.num) {
return 0;
} else if (this.num > numToCompareTo.num) {
return 1;
} else {
return -1;
}
}
        }

        /**
         * org.isk.references.part3.CustomComparator.java
         */
        public interface CustomComparator {
int compare(Number num1, Number num2);
        }

        /**
         * org.isk.references.NoParticularObjectTest.java
         */
        public class NoParticularObjectTest {
@Test
public void test() {
CustomComparator comparator = Number::compare;

int result = comparator.compare(new Number(5), new Number(10));
Assert.assertEquals(-1, result);
}
        }
        

Defender Methods

Aujourd’hui, modifier une interface implique de modifier toutes ses implémentations. Quand cette interface fait partie de la JDK, cela pose de gros problèmes quant à son évolution au cours des différentes versions si l’on souhaite garder une rétrocompatibilité sur les anciennes versions. Actuellement, la seule solution est de créer une nouvelle API (v2).

Pour éviter ceci, les defender methods ont été incluses dans la JSR. Il s’agit de méthodes concrètes – par défaut – présentes dans une interface.


        /**
         * org.isk.defenders.intro.Reader.java
         */
        public interface Reader {
char next();
boolean hasNext();

void skip(int i) default {
for (; i > 0 && hasNext(); i--) {
next();
}
}
        }

        /**
         * org.isk.defenders.intro.ReaderImpl.java
         */
        public class ReaderImpl implements Reader {
private int counter;
private char[] string;

public ReaderImpl(String string) {
this.counter = 0;
this.string = string.toCharArray();
}

public char next() {
return this.string[this.counter++];
}

public boolean hasNext() {
return this.string.length > this.counter;
}
        }

        /**
         * org.isk.defenders.SimpleDefenderTest.java
         */
        public class SimpleDefenderTest {
@Test
public void defender() {
final Reader reader = new ReaderImpl("Java 8 - Lambda Expressions");

Assert.assertEquals('J', reader.next());
Assert.assertTrue(reader.hasNext());
reader.skip(10);
Assert.assertEquals('m', reader.next());
}
        }
        

La méthode skip() est la méthode par défaut, identifiée par le mot clé default, – jusqu’à présent utilisé dans les switch/case .

A noter que la méthode skip() peut être redéfinie dans la classe concrète ReaderImpl.java.

Héritage multiple ?

Avec Java 8, une interface ressemble de plus en plus à une classe abstraite. Sachant qu’en pratique, rien n’empêche un développeur d’ajouter des getters abstraits dans l’interface.


        /**
         * org.isk.defenders.inheritance.Reader.java
         */
        public interface Reader {
int getCounter();
char[] getString();
void inc();

void skip(int i) default {
for (; i > 0 && hasNext(); i--) {
next();
}
}

public char next() default {
char c = this.getString()[this.getCounter()];
this.inc();
return c;
}

public boolean hasNext() default {
return this.getString().length > this.getCounter();
}
        }

        /**
         * org.isk.defenders.inheritance.ReaderImpl.java
         */
        public class ReaderImpl implements Reader {
private int counter;
private char[] string;

public ReaderImpl(String string) {
this.counter = 0;
this.string = string.toCharArray();
}

public int getCounter() {
return this.counter;
}

public void inc() {
this.counter++;
}

public char[] getString() {
return this.string;
}
        }

        /**
         * org.isk.defenders.HorribleDefenderTest.java
         */
        public class HorribleDefenderTest {
@Test
public void defender() {
final Reader reader = new ReaderImpl("Java 8 - Lambda Expressions");

Assert.assertEquals('J', reader.next());
Assert.assertTrue(reader.hasNext());
reader.skip(10);
Assert.assertEquals('m', reader.next());
}
        }
        

Finalement, nous ne pouvons toujours pas faire d’héritage multiple puisque nous n’avons pas – encore – de champs de classe dans une interface, uniquement des champs statiques sont possibles.

En revanche, je peux transformer ma classe implémentant plusieurs interfaces en un simple JavaBean n’ayant que des getters et des setters…

Héritage de defenders

Si nous souhaitons avoir, dans l’interface enfant, une implémentation par défaut d’une méthode abstraite de l’interface parente, ou si nous souhaitons redéfinir la méthode par défaut, il suffit de procéder de la même manière que dans l’exemple précédent, sans oublier le mot clé default.

Si nous souhaitons “désactiver” la méthode par défaut, nous devons rajouter cette méthode dans l’interface enfant en tant que méthode abstraite.

Dans la dernière version de la JDK disponible (beta 64), l’utilisation des mots clés default none n’est plus nécessaire (ne compile plus) pour désactiver une méthode par défaut d’une interface parente.


        /**
         * org.isk.defenders.override.Reader.java
         */
        public interface Reader {
char next();
boolean hasNext();

void skip(int i) default {
for (; i > 0 && hasNext(); i--) {
next();
}
}
        }

        /**
         * org.isk.defenders.override.StandardReader.java
         */
        public interface StandardReader {
void skip(int i);
        }

        /**
         * org.isk.defenders.override.StandardReaderImpl.java
         */
        public class StandardReaderImpl implements Reader {
private int counter;
private char[] string;

public StandardReaderImpl(String string) {
this.counter = 0;
this.string = string.toCharArray();
}

public char next() {
return this.string[this.counter++];
}

public boolean hasNext() {
return this.string.length > this.counter;
}

public void skip(int i) {
for (; i > 0 && hasNext(); i--) {
this.next();
}
}
        }

        /**
         * org.isk.defenders.OverrideDefenderTest.java
         */
        public class OverrideDefenderTest {
@Test
public void defender() {
final Reader reader = new StandardReaderImpl("Java 8 - Lambda Expressions");

Assert.assertEquals('J', reader.next());
Assert.assertTrue(reader.hasNext());
reader.skip(10);
Assert.assertEquals('m', reader.next());
}
        }
        

Ressources

[-] https://cr.openjdk.java.net/~briangoetz/lambda/Defender%20Methods%20v4.pdf