Tutoriel sur les nouveautés du langage 8 : le projet Lambda

Image non disponible

Cet article s'intéresse à détailler le projet Lambda pour la future version de Java 8. Il a été rédigé quand 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 réagir au contenu de cet article, un espace de dialogue vous est proposé sur le forum 7 commentaires Donner une note à l'article (5).

Article lu   fois.

Les deux auteurs

Profil ProSite personnel

Profil ProSite personnel

Liens sociaux

Viadeo Twitter Facebook Share on Google+   

I. Introduction

Aprè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 JEP, mais uniquement des plus importantes, aussi bien en termes d'avancées pour les développeurs, que de prouesses techniques.

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.

II. Encore et toujours des Lambda - 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 diverses méthodes doivent être disponibles pour nous permettre de récupérer des sous-listes de personnes.

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
/*
 * 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.

II-A. The old-fashioned way

En Java 7, et antérieur, 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.

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
/*
 * 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.

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
/*
 * 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 :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
/*
 * 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 un caractère comme paramètre. L'implémentation de

 
Sélectionnez
getSublist(final int minAge, final int maxAge)

et

 
Sélectionnez
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.

II-B. SAM ou 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 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éfinies 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 :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
/*
 * org.isk.lambda.sams.SamPredicate
 */
@FunctionalInterface
public interface SamPredicate<E> {
    boolean test(E e);
}

Source.

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
/*
 * 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 devront 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 :

 
Sélectionnez
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 » :

 
Sélectionnez
person.getLastname().startsWith("D")

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

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
/*
 * 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.

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
@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.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 :

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

Et :

 
Sélectionnez
(person) -> person.getLastname().startsWith("D")

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

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
/*
 * 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.

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
@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 ».

 
Sélectionnez
() -> x

II-C. 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.

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
/*
 * 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.

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
/*
 * 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)));
}

II-D. 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 par 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 devrait ê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.

II-E. 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 leur instanciation.

II-F. java.util.function

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

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

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
/*
 * 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.

II-F-1. 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ée 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.

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
/*
 * 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.

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
/*
 * 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.

II-F-2. java.util.function.Consumer

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

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
/*
 * 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.

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
/*
 * 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.

II-F-3. 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() prend 2 paramètres et retourne une valeur, tous trois de même type.

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
/*
 * 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.

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
/*
 * 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 vue des performances.

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

II-G. java.util.stream

Le nouveau package java.util.stream permet le support d'opérations de type fonctionnel sur des objets Stream (flux) de valeurs.

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

 
Sélectionnez
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é. À noter qu'un Stream peut aussi être infini.

Les objets Stream 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 soit « intermédiaires », soit « terminales ».

  • Intermédiaire : une opération intermédiaire garde le Stream ouvert et permet d'effectuer d'autres opérations. Les méthodes filter() et map() sont des exemples d'opérations intermédiaires, elles retournent le Stream courant, ce qui permet de chaîner plusieurs opérations.
  • 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 objets Stream 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 objets Stream 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 seule passe. Dans le cas de certaines opérations stateful, il est parfois nécessaire d'effectuer une seconde passe.

Les objets Stream essaient - dans la mesure du possible - de faire un minimum de choses. Il y a de micro-optimisations, telles qu'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 objets Stream spécialisés pour les primitifs.

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

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
/*
 * 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.

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
/*
 * 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 objets Stream voyons comment créer une sous-liste à l'aide d'un Stream et de la méthode filter().

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
/*
 * 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.

II-H. API Collections - ajouts

Le fait que nous puissions à présent définir des 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 changeront 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 :

 
Sélectionnez
1.
Collections.sort(list, comparator);

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

 
Sélectionnez
1.
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.

III. 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.

IV. Remerciements

Cet article a été publié avec l'aimable autorisation de la société SoatSoat.

Nous tenons à remercier Philippe Duval pour sa relecture attentive de cet article et Mickaël Baron pour la mise au gabarit.

Vous avez aimé ce tutoriel ? Alors partagez-le en cliquant sur les boutons suivants : Viadeo Twitter Facebook Share on Google+   

  

Copyright © 2014 SOAT. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.