Accueil Nos publications Blog Java 8 – What’s new ? – 1/3 – Project Lambda

Java 8 – What’s new ? – 1/3 – Project Lambda

Java 8Après avoir attendu presque 5 ans la sortie de Java 7, Oracle a annoncé fin 2011 un planning de livraison des futures versions de Java. Il n’aura pas fallu attendre longtemps pour qu’il remette en cause ce planning. Les itérations de deux ans initialement prévues, à partir de Java 7 (28 juillet 2011), ne seront pas respectées avec Java 8. Mark Reinhold, architecte en chef du Java Platform group, a récemment annoncé que Java 8 serait repoussé au 18 mars 2014, pour résoudre les problèmes de sécurité que connaît Java dans les navigateurs.

L’ironie du sort étant qu’en juillet 2012, ce même Mark Reinhold avait proposé de différer à Java 9 l’un des projets les plus attendus dans le monde Java et ceci depuis 7 ans (Jigsaw – JSR 294), dans le but de ne pas retarder la sortie de Java 8.

Néanmoins, les spécifications des fonctionnalités majeures étant presque toutes à l’état « final » et la JDK8 étant « features complete » (depuis le 13 juin 2013) nous pouvons faire un état des lieux.

Cet article n’a pas pour but de détailler la totalité des cinquante JEPs , mais uniquement des plus importantes, aussi bien en termes d’avancées pour les développeurs, que de prouesses techniques.

Note importante : Le JDK 8 n’étant pas encore à l’état finalisé, il est possible qu’il y ait des différences entre les fonctionnalités décrites dans cet article et celles proposées dans la version finale.

Pour cet article, le JDK 8 build b94 a été utilisée. Le dernier build peut être téléchargé depuis jdk8.java.net.

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

Encore et toujours des Lambdas – JSR 335

Dans les précédents articles concernant le projet Lambda (Expressions Lambda, Références et Defenders, et Impacts sur l’API Collection et utilisation de la JDK 8 Early Access), je me suis attardé sur la syntaxe et les différents cas d’utilisation. Dans cet article, je souhaite aborder le sujet à partir d’une approche différente. En partant d’un exemple simple, – implémenté en utilisant le « style Java 7 » -, les nouveautés introduites avec Java 8 seront présentées de façon graduelle. Prenons le cas d’une liste de Personnes ayant un nom, un prénom et un âge. La liste doit pouvoir être triée en fonction de différents critères et divers méthodes doivent être disponibles pour nous permettre de récupérer des sous-listes de personnes.

/*
 * org.isk.lambda.beans.Person
 */
public class Person {

    String firstname;
    String lastname;
    int age;

    public Person(String firstname, String lastname, int age) {
        this.firstname = firstname;
        this.lastname = lastname;
        this.age = age;
    }

    //...
}

Source

The old-fashioned way

En Java 7, et antérieure, si nous souhaitons avoir une sous-liste de personnes dont l’âge est compris dans un intervalle, le plus simple est d’étendre une classe de type Collection ou de créer une méthode statique prenant en paramètre une liste de type Collection, en plus des bornes inférieure et supérieure. Pour cet exemple, nous allons utiliser la première solution.

/*
 * org.isk.lambda.old.OldList
 */
public class OldList extends ArrayList<Person> {
    public OldList getSubList(final int minAge, final int maxAge) {
        final OldList list = new OldList();

        for (Person p : this) {
            if (p.getAge() >= minAge && p.getAge() <= maxAge) {
                list.add(p);
            }
        }

        return list;
    }
}

Source

Pour pouvoir tester cette nouvelle liste, nous créons une variable statique de type OldList contenant plusieurs Person.

/*
 * org.isk.lambda.old.OldPersonsDB
 */
public class OldPersonsDB {

    public final static OldList PERSONS;

    static {
        PERSONS = new OldList();
        PERSONS.add(new Person("Carson", "Busses", 25));
        PERSONS.add(new Person("Patty", "Cake", 72));
        PERSONS.add(new Person("Anne", "Derri ", 14));
        PERSONS.add(new Person("Moe", "Dess", 47));
        PERSONS.add(new Person("Leda", "Doggslife", 50));
        PERSONS.add(new Person("Dan", "Druff", 38));
        PERSONS.add(new Person("Al", "Fresco", 36));
        PERSONS.add(new Person("Ido", "Hoe", 2));
        PERSONS.add(new Person("Howie", "Kisses", 23));
        PERSONS.add(new Person("Len", "Lease", 63));
    }

    private OldPersonsDB() {}
}

Source

Le test unitaire peut être le suivant :

/*
 * org.isk.lambda.old. OldFashionedWayTest
 */
@Test
public void getSublist() {
    final OldList list = OldPersonsDB.PERSONS.getSubList(14, 25);

    Assert.assertEquals(3, list.size());
    Assert.assertTrue(list.contains(new Person("Carson", "Busses", 25)));
    Assert.assertTrue(list.contains(new Person("Anne", "Derri ", 14)));
    Assert.assertTrue(list.contains(new Person("Howie", "Kisses", 23)));
}

Source

Malheureusement, getSubList() est l’exemple typique, bien trop courant, de méthode non générique. Si l’on souhaite avoir une sous-liste de personnes dont le nom commence par la lettre « D », nous aurions généralement une nouvelle méthode prenant à paramètre un caractère. L’implémentation de

getSublist(final int minAge, final int maxAge)

et

getSublist(final char character)

serait identique, à l’exception de la condition permettant d’ajouter des éléments dans la sous-liste. Mais Java 8 n’est pas nécessaire pour avoir une solution générique. Des interfaces utilisées en tant que classes anonymes répondent parfaitement au besoin.

SAM or BOB

Une interface fonctionnelle est une interface ne possédant qu’une seule méthode abstraite n’étant pas une redéfinition d’une méthode de la classe Object. Ce type de méthodes est aussi appelé Single Abstract Method (SAM). A noter que la JDK <=7 possède déjà de telles interfaces, comme par exemple java.lang.Runnable ou java.util.Comparator.

Java 8 introduit une nouvelle annotation optionnelle permettant d’identifier une interface fonctionnelle (@FunctionalInterface). Tout comme l’annotation @Override, si les règles définis dans l’utilisation de cette annotation ne sont pas respectées, le compilateur refusera de compiler l’interface.

En reprenant l’exemple précédent, nous allons créer une méthode générique à l’aide d’une interface fonctionnelle nommée SamPredicate :

/*
 * org.isk.lambda.sams.SamPredicate
 */
@FunctionalInterface
public interface SamPredicate<E> {
    boolean test(E e);
}

Source

/*
 * org.isk.lambda.sams.SamsList
 */
public class SamsList extends ArrayList<Person> {

    public SamsList getSubList(final SamPredicate<Person> samPredicate) {
        final SamsList list = new SamsList();

        for (Person p : this) {
            if (samPredicate.test(p)) {
                list.add(p);
            }
        }

        return list;
    }
}

Source

Nous constatons, à présent, qu’il suffira de passer en paramètre la condition à laquelle devra répondre les éléments de la sous-liste.

Nous pouvons donc créer une méthode de test reprenant l’exemple précèdent :

person.getAge() >= 14 && person.getAge() <= 25

Et une nouvelle, nous permettant de tester la création d’une sous-liste de personnes dont le nom commence par la lettre « D » :

person.getLastname().startsWith("D")

Le Test Unitaire se charge de passer en paramètre les conditions qui conviennent au besoin :

/*
 * org.isk.lambda.sams.SamsTest
 */
@Test
public void getSublistWithAnonymousClass1() {
    final SamsList list = SamsPersonsDB.PERSONS.getSubList(new SamPredicate<Person>() {
        @Override
        public boolean test(Person person) {
            return person.getAge() >= 14 && person.getAge() <= 25;
        }
    });

    Assert.assertEquals(3, list.size());
    Assert.assertTrue(list.contains(new Person("Carson", "Busses", 25)));
    Assert.assertTrue(list.contains(new Person("Anne", "Derri ", 14)));
    Assert.assertTrue(list.contains(new Person("Howie", "Kisses", 23)));
}

Source

@Test
public void getSublistWithAnonymousClass2() {
    final SamsList list = SamsPersonsDB.PERSONS.getSubList(new SamPredicate<Person>() {
        @Override
        public boolean test(Person person) {
            return person.getLastname().startsWith("D");
        }
    });

    Assert.assertEquals(4, list.size());
    Assert.assertTrue(list.contains(new Person("Anne", "Derri ", 14)));
    Assert.assertTrue(list.contains(new Person("Moe", "Dess", 47)));
    Assert.assertTrue(list.contains(new Person("Leda", "Doggslife", 50)));
    Assert.assertTrue(list.contains(new Person("Dan", "Druff", 38)));
}

Source

Le véritable problème avec les classes anonymes est leur verbosité rendant le code difficile à lire. Et c’est là que tout l’intérêt des fonctions anonymes entre en jeu. Fonctions anonymes Identifiées par une flèche (->), les fonctions anonymes sont à elles seules, la révolution de Java 8. Nous pouvons à présent remplacer les 6 lignes de nos classes anonymes par une seule :

(person) -> person.getAge() >= 14 && person.getAge() <= 25

Et :

(person) -> person.getLastname().startsWith("D")

Nous vérifions bien évidement par un test unitaire, que le résultat est le même.

/*
 * org.isk.lambda.sams.SamsTest
 */
@Test
public void getSublistWithLambda1() {
    final SamsList list = SamsPersonsDB.PERSONS.getSubList((person) -> person.getAge() >= 14 && person.getAge() <= 25);

    Assert.assertEquals(3, list.size());
    Assert.assertTrue(list.contains(new Person("Carson", "Busses", 25)));
    Assert.assertTrue(list.contains(new Person("Anne", "Derri ", 14)));
    Assert.assertTrue(list.contains(new Person("Howie", "Kisses", 23)));
}

Source

@Test
public void getSublistWithLambda2() {
    final SamsList list = SamsPersonsDB.PERSONS.getSubList((person) -> person.getLastname().startsWith("D"));

    Assert.assertEquals(4, list.size());
    Assert.assertTrue(list.contains(new Person("Anne", "Derri ", 14)));
    Assert.assertTrue(list.contains(new Person("Moe", "Dess", 47)));
    Assert.assertTrue(list.contains(new Person("Leda", "Doggslife", 50)));
    Assert.assertTrue(list.contains(new Person("Dan", "Druff", 38)));
}

Source

Note : Une expression lambda n’ayant pas de paramètre a été nommée « burger arrow ».

() -> x

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 ambiguïté.

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.

Grâce aux références de méthodes, nous allons pouvoir effectuer des tris sans avoir à définir de java.util.Comparator.

/*
 * org.isk.lambda.beans.Person
 */
public class Person {
    public static int sortByAge(Person p1, Person p2) {
        if (p1.getAge() > p2.getAge()) {
            return 1;
        } else if (p1.getAge() < p2.getAge()) {
            return -1;
        } else {
            return 0;
        }
    }
}

Source

/*
 * org.isk.lambda.references.ReferencesTest
 */
@Test
public void sortByAge() {
    final List<Person> list = SamsPersonsDB.PERSONS.getSubList((person) -> person.getLastname().startsWith("D"));
    Collections.sort(list, Person::sortByAge);

    Assert.assertTrue(list.get(0).equals(new Person("Anne", "Derri ", 14)));
    Assert.assertTrue(list.get(1).equals(new Person("Dan", "Druff", 38)));
    Assert.assertTrue(list.get(2).equals(new Person("Moe", "Dess", 47)));
    Assert.assertTrue(list.get(3).equals(new Person("Leda", "Doggslife", 50)));
}

Source

Defender methods

Avant Java 8, ajouter une méthode dans une interface de l’API standard était impossible si l’on souhaitait garder une compatibilité avec les versions de JDK précédentes. Les méthodes par défaut (ou « defender methods ») permettent de lever cette limitation en fournissant une implémentation par défaut dans l’interface. La classe implémentant une interface ayant des méthodes par défaut n’est pas conséquent pas dans l’obligation de les implémenter.

De nombreuses méthodes par défaut ont été ajoutées à la JDK 8. Nous en verrons quelques-unes par la suite.

Définir des méthodes par défaut dans des interfaces devraient être réservé aux développeurs de l’API standard. Il n’y a absolument aucune raison pour tout autre développeur d’en faire l’utilisation.

Note : Si une interface contient une méthode abstraite et plusieurs méthodes par défaut, il s’agit toujours d’une interface fonctionnelle.

Méthodes statiques et Interfaces

Avec Java 8, il est à présent possible de définir des méthodes statiques dans des interfaces, rendant obsolètes les classes utilitaires généralement définies avec un constructeur privé – ne faisant rien – pour éviter son instanciation.

java.util.function

Le nouveau package java.util.function définit une quarantaine d’interfaces fonctionnelles. Nous allons voir l’utilisation de quatre qui sont utilisées à de nombreuses reprises dans l’API.

Nous nous intéresserons uniquement aux SAMs. Les méthodes par défaut étant assez simples d’utilisation.

/*
 * org.isk.lambda.functions.FunctionPersonsDB
 */
public class FunctionPersonsDB {

    public final static FunctionsList<Person> PERSONS;

    static {
        PERSONS = new FunctionsList<>();
        PERSONS.add(new Person("Carson", "Busses", 25));
        //...
    }

    private FunctionPersonsDB() {}
}

Source

java.util.function.Predicate

La méthode test() retourne true, si l’objet passé en paramètre répond à certains critères. L’interface java.util.function.Predicate est semblable à l’interface SamsPredicate, que nous avons utilisé jusqu’à présent. java.util.function.Function La méthode apply(), applique une fonction à l’objet passé en argument et retourne un résultat approprié d’un type différent du paramètre de la méthode. Dans l’exemple suivant, la méthode map() prend une Function qui lui permet de créer une nouvelle liste contenant des éléments de type R à partir d’une liste contenant des éléments de type E.

/*
 * org.isk.lambda.functions.FunctionsList
 */
public class FunctionsList<E> extends ArrayList<E> {
    public <R> FunctionsList<R> map(final Function<E, R> function) {
        final FunctionsList<R> list = new FunctionsList<>();

        for(E e : this) {
            list.add(function.apply(e));
        }

        return list;
    }
}

Source

Le test unitaire suivant crée une liste contenant les âges de toutes les personnes de la liste FunctionPersonsDB.PERSONS.

/*
 * org.isk.lambda.functions.FunctionsTest
 */
 @Test
public void map() {
    final AtomicInteger atomicInteger = new AtomicInteger();
    final FunctionsList<Integer> list = FunctionPersonsDB.PERSONS.map((person) -> person.getAge());

    Assert.assertTrue(list.contains(new Integer(25)));
    Assert.assertTrue(list.contains(new Integer(72)));
    Assert.assertTrue(list.contains(new Integer(14)));
    Assert.assertTrue(list.contains(new Integer(47)));
    Assert.assertTrue(list.contains(new Integer(50)));
    Assert.assertTrue(list.contains(new Integer(38)));
    Assert.assertTrue(list.contains(new Integer(36)));
    Assert.assertTrue(list.contains(new Integer(2)));
    Assert.assertTrue(list.contains(new Integer(23)));
    Assert.assertTrue(list.contains(new Integer(63)));
}

Source

java.util.function.Consumer

La méthode accept() prend un objet en paramètre et ne retourne rien.

/*
 * org.isk.lambda.functions.FunctionsList
 */
public class FunctionsList<E> extends ArrayList<E> {
    public void foreach(final Consumer<E> consumer) {
        for(E e : this) {
            consumer.accept(e);
        }
    }
}

Source

Pour chaque élément, accumule l’âge de toutes les personnes.

/*
 * org.isk.lambda.functions.FunctionsTest
 */
@Test
public void foreach() {
    final AtomicInteger accumulatedAges = new AtomicInteger();
    FunctionPersonsDB.PERSONS.foreach((person) -> accumulatedAges.addAndGet(person.getAge()));

    Assert.assertEquals(370, accumulatedAges.get());
}

Source

Note : Une méthode par défaut forEach() (avec un « e » majuscule) a été ajoutée à l’interface java.lang.Iterable.

java.util.function.BinaryOperator

Dans l’exemple précédent, l’utilisation de la méthode foreach() pour effectuer une accumulation n’est pas adaptée, surtout si l’on souhaite paralléliser l’opération. Pour ce faire une méthode reduce() prenant une valeur initiale et un BinaryOperator semble plus adéquate.

La méthode [apply()](https://download.java.net/jdk8/docs/api/java/util/function/BiFunction.html#apply(T, U)) prend 2 paramètres et retourne une valeur, toutes trois de même type.

/*
 * org.isk.lambda.functions.FunctionsList
 */
public class FunctionsList<E> extends ArrayList<E> {
    public E reduce(final E identity, final BinaryOperator<E> binaryOperator) {
        E result = identity;
        for(E e : this) {
            result = binaryOperator.apply(result, e);
        }

        return result;
    }
}

Source

/*
 * org.isk.lambda.functions.FunctionsTest
 */
@Test
public void reduce() {
    final int accumalatedAges =
        FunctionPersonsDB.PERSONS
            .map((person) -> person.getAge())
            .reduce(0, (x, y) -> x + y);

    Assert.assertEquals(370, accumalatedAges);
}

Source

Bien que nous ayons corrigé le problème que nous avions de par l’utilisation de la méthode foreach(), il en reste deux de taille. En chaînant les méthodes (map().reduce() dans l’exemple précédent) nous créons autant de listes qu’il y a de méthodes et nous itérons sur ces dernières autant de fois qu’il y a d’éléments. Heureusement, le nouveau type Stream est là pour éviter ce genre de situations catastrophiques du point de vu des performances.

Note : Il existe aussi des interfaces permettant d’utiliser des primitifs, ce qui évite le boxing et l’unboxing.

java.util.stream

La nouveau package java.util.stream permet le support d’opérations de type fonctionnel sur des streams (flux) de valeurs.

Obtenir un Stream à partir d’une collection s’effectue de la façon suivante :

Stream<T> stream = collection.stream();

Un Stream est comparable à un Iterator. Une fois les valeurs traitées, elles ne sont plus accessibles. Un Stream ne peut être traversé qu’une seule fois. Il est ensuite consommé. A noter qu’un Stream peut aussi être infini.

Les Streams peuvent être séquentiels ou parallèles. Il est possible d’utiliser un type de Stream, puis de le changer entre différentes opérations – stream.sequential() ou stream.parallel() -.

Les actions d’un Stream séquentiel sont sérielles et s’exécutent dans un seul thread. Alors que les actions d’un Stream parallèle peuvent s’exécuter de manière concurrente dans plusieurs threads, mais ce n’est pas une obligation, cela sera fonction de l’implémentation.

Les opérations sur un Stream peuvent être soient « intermédiaires », soient « terminales ».

  • Intermédiaire : une opération intermédiaire garde le Stream ouvert est permet d’effectuer d’autres opérations. Les méthodes filter() et map() sont des exemples d’opérations intermédiaires, elles retourne le Stream courant ce qui permet de chainer plusieurs opération.
  • Terminale : une opération terminale doit être l’opération finale effectuée sur un Stream. Une fois qu’une opération terminale est appelée, le Stream est consommé et il ne peut plus être utilisé. La méthode reduce() en est un exemple.

Les Streams ont plusieurs méthodes permettant d’effectuer deux opérations intéressantes :

  • Stateful : Une opération « avec état » impose une nouvelle propriété sur le Stream, telle que l’unicité des éléments, un nombre maximum d’éléments ou le fait que les éléments soient consommés dans un ordre précis. Il est important de garder à l’esprit que ce type d’opération est plus coûteux qu’une opération « sans état ».
  • Court-circuit : une opération « court-circuit» permet d’interrompre le traitement d’un Stream. Cette propriété est importante lorsque l’on est face à des Streams infinis. Si aucune opération appelée sur un Stream n’est de type « court-circuit», le code peut potentiellement ne jamais s’arrêter.

Comme indiqué dans la javadoc, les opérations intermédiaires sont « lazy ». Seulement une opération terminale commencera le traitement des éléments d’un Stream. Par conséquent, quel que soit le nombre d’opérations intermédiaires les éléments sont généralement consommés en une seulement passe. Dans le cas de certaines opérations stateful, il est parfois nécessaire d’effectuer une seconde passe.

Les Streams essayent – dans la mesure du possible – de faire un minimum chose. Il y a de micro-optimisations telle que ignorer l’opération sorted(), lorsqu’ils peuvent déterminer que les éléments sont déjà ordonnés. Sur les opérations contenant des limit() et substream(), ils peuvent parfois éviter d’effectuer des opérations de mapping sur les éléments dont ils savent qu’ils sont inutiles pour déterminer le résultat.

Concernant le concept de parallélisme, il est important de noter qu’il ne s’agit pas d’une action sans coût. Il y a de nombreux paramètres à prendre à compte avant de rendre une chaîne d’opérations parallèle. Dans les cas suivants le parallélisme n’améliorera pas les performances, au contraire :

  • l’ordre des éléments est important,
  • il y a des opérations stateful et court-circuit dans ma chaîne d’opération,
  • mon stream n’est pas assez grand et/ou les opérations ne sont pas assez complexes.

Tout comme il y a des interfaces fonctionnelles adaptées aux primitifs, il y a des Streams spécialisés pour les primitifs.

Les méthodes filter(), map() et reduce() que nous avons vu précédemment sont présentes dans l’interface Stream, tout comme de nombreuses autres.

/*
 * org.isk.lambda.streams.StreamPersonsDB
 */
public class StreamPersonsDB {
    public final static List<Person> PERSONS;

    static {
        PERSONS = new ArrayList<>();
        PERSONS.add(new Person("Carson", "Busses", 25));
        // ...
    }

    private StreamPersonsDB() {}
}

Source

/*
 * org.isk.lambda.streams.StreamTest
 */
 @Test
public void filterMapReduce() {
    final int accumulatedAges =
            StreamPersonsDB.PERSONS.stream()
                .filter((person) -> person.getLastname().startsWith("D"))
                .map((person) -> person.getAge())
                .reduce(0, (x, y) -> x + y);

    Assert.assertEquals(149, accumulatedAges);
}

Source

Avant de conclure sur les Streams voyons comment créer une sous-liste à l’aide d’un Stream et de la méthode filter().

/*
 * org.isk.lambda.streams.StreamTest
 */
@Test
public void filter() {
    final List<Person> list =
        StreamPersonsDB.PERSONS.stream()
            .filter((person) -> person.getLastname().startsWith("D"))
            .collect(Collectors.toList());

    Assert.assertEquals(4, list.size());
    Assert.assertTrue(list.contains(new Person("Anne", "Derri ", 14)));
    Assert.assertTrue(list.contains(new Person("Moe", "Dess", 47)));
    Assert.assertTrue(list.contains(new Person("Leda", "Doggslife", 50)));
    Assert.assertTrue(list.contains(new Person("Dan", "Druff", 38)));
}

Source

La méthode n’étant pas une opération terminale, nous sommes obligés d’appeler la méthode collection qui transformera le Stream retourné par la méthode filter() en une List.

API Collections – ajouts

Le fait que nous puissions à présent définir de méthodes par défaut dans les interfaces a permis aux auteurs de la JDK d’ajouter de nombreuses choses aux interfaces de l’API collection.

collection.stream() et collection.parallelStream() sont les principales passerelles vers l’API Stream. Il existe d’autres solutions, mais il est fort probable que ces méthodes seront les plus utilisées.

L’une des nouveautés qui changera la vie de tous les développeurs Java est l’introduction de la méthode sort() à l’interface List. Avant, il était nécessaire d’utiliser la classe Collections de cette manière :

Collections.sort(list, comparator);

Aujourd’hui, il nous suffit d’écrire :

list.sort(comparator)

Les autres méthodes n’étant pas révolutionnaires, je vous invite à consulter la javadoc pour connaître toutes les autres nouveautés.

What’s next ?

Dans la partie suivante de cet article nous nous intéresserons à la nouvelle API « Date and Time », aux Annotations et à Nashorn, le nouveau moteur JavaScript.