Le didacticiel de l’API Java 8 Stream

Didacticiel de l'API Java 8 Stream

1. Vue d'ensemble

Dans ce didacticiel détaillé, nous allons passer en revue l'utilisation pratique de Java 8 Streams de la création à l'exécution parallèle.

Pour comprendre ce matériel, les lecteurs doivent avoir une connaissance de base de Java 8 (expressions lambda, références de méthodeOptional,) et de l'API Stream. Si vous n'êtes pas familier avec ces sujets, veuillez consulter nos articles précédents -New Features in Java 8 etIntroduction to Java 8 Streams.

Lectures complémentaires:

Expressions lambda et interfaces fonctionnelles: conseils et meilleures pratiques

Conseils et meilleures pratiques d'utilisation des lambdas et interfaces fonctionnelles Java 8.

Read more

Guide des collectionneurs de Java 8

Cet article traite des collecteurs Java 8, en présentant des exemples de collecteurs intégrés et explique comment créer un collecteur personnalisé.

Read more

2. Création de flux

Il existe de nombreuses façons de créer une instance de flux de différentes sources. Une fois créée, l'instancewill not modify its source, permet donc la création de plusieurs instances à partir d'une même source.

2.1. Flux vide

La méthodeempty() doit être utilisée en cas de création d'un flux vide:

Stream streamEmpty = Stream.empty();

C'est souvent le cas où la méthodeempty() est utilisée lors de la création pour éviter de renvoyernull pour les flux sans élément:

public Stream streamOf(List list) {
    return list == null || list.isEmpty() ? Stream.empty() : list.stream();
}

2.2. Flux deCollection

Le flux peut également être créé de tout type deCollection (Collection, List, Set):

Collection collection = Arrays.asList("a", "b", "c");
Stream streamOfCollection = collection.stream();

2.3. Stream of Array

Un tableau peut également être une source de flux:

Stream streamOfArray = Stream.of("a", "b", "c");

Ils peuvent également être créés à partir d'un tableau existant ou d'une partie d'un tableau:

String[] arr = new String[]{"a", "b", "c"};
Stream streamOfArrayFull = Arrays.stream(arr);
Stream streamOfArrayPart = Arrays.stream(arr, 1, 3);

2.4. Stream.builder()

When builder is usedthe desired type should be additionally specified in the right part of the statement, sinon la méthodebuild() créera une instance desStream<Object>:

Stream streamBuilder =
  Stream.builder().add("a").add("b").add("c").build();

2.5. Stream.generate()

La méthodegenerate() accepte unSupplier<T> pour la génération d'élément. Comme le flux résultant est infini, le développeur doit spécifier la taille souhaitée ou la méthodegenerate() fonctionnera jusqu'à ce qu'elle atteigne la limite de mémoire:

Stream streamGenerated =
  Stream.generate(() -> "element").limit(10);

Le code ci-dessus crée une séquence de dix chaînes avec la valeur -“element”.

2.6. Stream.iterate()

Une autre façon de créer un flux infini consiste à utiliser la méthodeiterate():

Stream streamIterated = Stream.iterate(40, n -> n + 2).limit(20);

Le premier élément du flux résultant est un premier paramètre de la méthodeiterate(). Pour créer chaque élément suivant, la fonction spécifiée est appliquée à l'élément précédent. Dans l'exemple ci-dessus, le deuxième élément sera 42.

2.7. Flux de primitifs

Java 8 offre la possibilité de créer des flux à partir de trois types primitifs:int, long etdouble. carStream<T> est une interface générique et il n'y a aucun moyen d'utiliser des primitives comme paramètre de type avec des génériques, trois nouvelles interfaces spéciales ont été créées:IntStream, LongStream, DoubleStream.

L'utilisation des nouvelles interfaces réduit les cas inutiles d'auto-boxing, permettant une productivité accrue:

IntStream intStream = IntStream.range(1, 3);
LongStream longStream = LongStream.rangeClosed(1, 3);

La méthoderange(int startInclusive, int endExclusive) crée un flux ordonné du premier paramètre au deuxième paramètre. Il incrémente la valeur des éléments suivants avec le pas égal à 1. Le résultat n'inclut pas le dernier paramètre, il s'agit simplement d'une limite supérieure de la séquence.

La méthoderangeClosed(int startInclusive, int endInclusive) fait la même chose avec une seule différence - le deuxième élément est inclus. Ces deux méthodes peuvent être utilisées pour générer l’un des trois types de flux de primitives.

Depuis Java 8, la classeRandom fournit un large éventail de méthodes pour générer des flux de primitives. Par exemple, le code suivant crée unDoubleStream, qui comporte trois éléments:

Random random = new Random();
DoubleStream doubleStream = random.doubles(3);

2.8. Flux deString

String peut également être utilisé comme source pour créer un flux.

Avec l'aide de la méthodechars() de la classeString. Puisqu'il n'y a pas d'interfaceCharStream dans JDK, leIntStream est utilisé pour représenter un flux de caractères à la place.

IntStream streamOfChars = "abc".chars();

L'exemple suivant divise unString en sous-chaînes selon lesRegEx spécifiés:

Stream streamOfString =
  Pattern.compile(", ").splitAsStream("a, b, c");

2.9. Flux de fichier

La classe Java NIOFiles permet de générer unStream<String> d'un fichier texte via la méthodelines(). Chaque ligne du texte devient un élément du flux:

Path path = Paths.get("C:\\file.txt");
Stream streamOfStrings = Files.lines(path);
Stream streamWithCharset =
  Files.lines(path, Charset.forName("UTF-8"));

LesCharset peuvent être spécifiés comme argument de la méthodelines().

3. Référencement dea Stream

Il est possible d'instancier un flux et d'y avoir une référence accessible tant que seules les opérations intermédiaires ont été appelées. L'exécution d'une opération de terminal rend un flux inaccessible.

Pour démontrer cela, nous oublierons pendant un moment que la meilleure pratique consiste à enchaîner les opérations. En plus de sa verbosité inutile, techniquement, le code suivant est valide:

Stream stream =
  Stream.of("a", "b", "c").filter(element -> element.contains("b"));
Optional anyElement = stream.findAny();

Mais une tentative de réutiliser la même référence après avoir appelé l'opération de terminal déclenchera lesIllegalStateException:

Optional firstElement = stream.findFirst();

Comme leIllegalStateException est unRuntimeException, un compilateur ne signalera pas un problème. Donc, il est très important de se rappeler queJava 8streams can’t be reused.

Ce type de comportement est logique car les flux ont été conçus pour permettre d'appliquer une séquence d'opérations finie à la source d'éléments dans un style fonctionnel, mais pas de stocker des éléments.

Donc, pour que le code précédent fonctionne correctement, certaines modifications doivent être effectuées:

List elements =
  Stream.of("a", "b", "c").filter(element -> element.contains("b"))
    .collect(Collectors.toList());
Optional anyElement = elements.stream().findAny();
Optional firstElement = elements.stream().findFirst();

4. Pipeline de flux

Pour effectuer une séquence d'opérations sur les éléments de la source de données et agréger leurs résultats, trois parties sont nécessaires - lessource,intermediate operation(s) et aterminal operation.

Les opérations intermédiaires renvoient un nouveau flux modifié. Par exemple, pour créer un nouveau flux du flux existant sans quelques éléments, la méthodeskip() doit être utilisée:

Stream onceModifiedStream =
  Stream.of("abcd", "bbcd", "cbcd").skip(1);

Si plusieurs modifications sont nécessaires, des opérations intermédiaires peuvent être chaînées. Supposons que nous devions également remplacer chaque élément deStream<String> actuel par une sous-chaîne de quelques premiers caractères. Cela se fera en chaînant les méthodesskip() etmap():

Stream twiceModifiedStream =
  stream.skip(1).map(element -> element.substring(0, 3));

Comme vous pouvez le voir, la méthodemap() prend une expression lambda comme paramètre. Si vous voulez en savoir plus sur les lambdas, jetez un œil à notre tutorielLambda Expressions and Functional Interfaces: Tips and Best Practices.

Un flux en lui-même est sans valeur, la véritable chose qui intéresse un utilisateur est le résultat de l'opération du terminal, qui peut être une valeur d'un type ou une action appliquée à chaque élément du flux. Only one terminal operation can be used per stream.

La manière la plus appropriée et la plus pratique d'utiliser les flux est par unstream pipeline, which is a chain of stream source, intermediate operations, and a terminal operation. Par exemple:

List list = Arrays.asList("abc1", "abc2", "abc3");
long size = list.stream().skip(1)
  .map(element -> element.substring(0, 3)).sorted().count();

5. Invocation paresseuse

Intermediate operations are lazy. Cela signifie quethey will be invoked only if it is necessary for the terminal operation execution.

Pour le démontrer, imaginez que nous ayons la méthodewasCalled(), qui incrémente un compteur interne à chaque fois qu'elle est appelée:

private long counter;

private void wasCalled() {
    counter++;
}

La méthode d’appel étaitCalled() de l’opérationfilter():

List list = Arrays.asList(“abc1”, “abc2”, “abc3”);
counter = 0;
Stream stream = list.stream().filter(element -> {
    wasCalled();
    return element.contains("2");
});

Comme nous avons une source de trois éléments, nous pouvons supposer que la méthodefilter() sera appelée trois fois et que la valeur de la variablecounter sera 3. Mais exécuter ce code ne change pas du toutcounter, il est toujours nul, donc la méthodefilter() n’a pas été appelée une seule fois. La raison pour laquelle - manque dans le fonctionnement du terminal.

Réécrivons un peu ce code en ajoutant une opérationmap() et une opération de terminal -findFirst(). Nous ajouterons également une possibilité de suivre un ordre d'appels de méthode à l'aide de la journalisation:

Optional stream = list.stream().filter(element -> {
    log.info("filter() was called");
    return element.contains("2");
}).map(element -> {
    log.info("map() was called");
    return element.toUpperCase();
}).findFirst();

Le journal résultant montre que la méthodefilter() a été appelée deux fois et la méthodemap() une seule fois. C'est parce que le pipeline s'exécute verticalement. Dans notre exemple, le premier élément du flux ne satisfaisait pas au prédicat du filtre, puis la méthodefilter() a été invoquée pour le deuxième élément, qui passait le filtre. Sans appeler lesfilter() pour le troisième élément, nous sommes passés par pipeline à la méthodemap().

L'opérationfindFirst() satisfait par un seul élément. Ainsi, dans cet exemple particulier, l'invocation paresseuse permettait d'éviter deux appels de méthode - un pour lesfilter() et un pour lesmap().

6. Ordre d'exécution

Du point de vue des performances,the right order is one of the most important aspects of chaining operations in the stream pipeline:

long size = list.stream().map(element -> {
    wasCalled();
    return element.substring(0, 3);
}).skip(2).count();

L'exécution de ce code augmentera la valeur du compteur de trois. Cela signifie que la méthodemap() du flux a été appelée trois fois. Mais la valeur dessize est un. Ainsi, le flux résultant n'a qu'un seul élément et nous avons exécuté les opérationsmap() coûteuses sans raison deux fois sur trois.

Si nous changeons l'ordre des méthodesskip() etmap(),, lescounter n'augmenteront que de un. Ainsi, la méthodemap() ne sera appelée qu'une seule fois:

long size = list.stream().skip(2).map(element -> {
    wasCalled();
    return element.substring(0, 3);
}).count();

Cela nous amène à la règle:intermediate operations which reduce the size of the stream should be placed before operations which are applying to each element. Donc, gardez des méthodes telles que skip(), filter(), distinct() en haut de votre pipeline de flux.

7. Réduction de flux

L'API a de nombreuses opérations de terminal qui agrègent un flux à un type ou à une primitive, par exemple,count(), max(), min(), sum(), mais ces opérations fonctionnent selon l'implémentation prédéfinie. Et ce queif a developer needs to customize a Stream’s reduction mechanism? Il existe deux méthodes qui permettent de faire cela - les méthodesreduce() et les méthodescollect().

7.1. La méthodereduce()

Il existe trois variantes de cette méthode, qui diffèrent par leurs signatures et leurs types renvoyés. Ils peuvent avoir les paramètres suivants:

identity – la valeur initiale d'un accumulateur ou une valeur par défaut si un flux est vide et qu'il n'y a rien à accumuler;

accumulator – une fonction qui spécifie une logique d'agrégation d'éléments. Comme l'accumulateur crée une nouvelle valeur pour chaque étape de réduction, la quantité de nouvelles valeurs est égale à la taille du flux et seule la dernière valeur est utile. Ce n'est pas très bon pour la performance.

combiner – une fonction qui agrège les résultats de l'accumulateur. Combiner est appelé uniquement en mode parallèle pour réduire les résultats des accumulateurs de différents threads.

Voyons donc ces trois méthodes en action:

OptionalInt reduced =
  IntStream.range(1, 4).reduce((a, b) -> a + b);

reduced = 6 (1 + 2 + 3)

int reducedTwoParams =
  IntStream.range(1, 4).reduce(10, (a, b) -> a + b);

reducedTwoParams = 16 (10 + 1 + 2 + 3)

int reducedParams = Stream.of(1, 2, 3)
  .reduce(10, (a, b) -> a + b, (a, b) -> {
     log.info("combiner was called");
     return a + b;
  });

Le résultat sera le même que dans l'exemple précédent (16) et il n'y aura pas de connexion, ce qui signifie que le combineur n'a pas été appelé. Pour faire fonctionner un combineur, un flux doit être parallèle:

int reducedParallel = Arrays.asList(1, 2, 3).parallelStream()
    .reduce(10, (a, b) -> a + b, (a, b) -> {
       log.info("combiner was called");
       return a + b;
    });

Le résultat ici est différent (36) et le combineur a été appelé deux fois. Ici, la réduction fonctionne par l'algorithme suivant: l'accumulateur a été exécuté trois fois en ajoutant chaque élément du flux àidentity à chaque élément du flux. Ces actions se font en parallèle. En conséquence, ils ont (10 + 1 = 11; 10 + 2 = 12; 10 + 3 = 13;). Le combineur peut maintenant fusionner ces trois résultats. Il faut pour cela deux itérations (12 + 13 = 25; 25 + 11 = 36).

7.2. La méthodecollect()

La réduction d'un flux peut également être exécutée par une autre opération de terminal - la méthodecollect(). Il accepte un argument de typeCollector, qui spécifie le mécanisme de réduction. Des collecteurs prédéfinis sont déjà créés pour la plupart des opérations courantes. Ils sont accessibles à l'aide du typeCollectors.

Dans cette section, nous utiliserons lesList suivants comme source pour tous les flux:

List productList = Arrays.asList(new Product(23, "potatoes"),
  new Product(14, "orange"), new Product(13, "lemon"),
  new Product(23, "bread"), new Product(13, "sugar"));

Conversion d'un flux enCollection (Collection, List ouSet):

List collectorCollection =
  productList.stream().map(Product::getName).collect(Collectors.toList());

Réduction àString:

String listToString = productList.stream().map(Product::getName)
  .collect(Collectors.joining(", ", "[", "]"));

La méthodejoiner() peut avoir de un à trois paramètres (délimiteur, préfixe, suffixe). La chose la plus pratique à propos de l'utilisation dejoiner() - le développeur n'a pas besoin de vérifier si le flux arrive à sa fin pour appliquer le suffixe et non pour appliquer un délimiteur. Collector s'en chargera.

Traitement de la valeur moyenne de tous les éléments numériques du flux:

double averagePrice = productList.stream()
  .collect(Collectors.averagingInt(Product::getPrice));

Traitement de la somme de tous les éléments numériques du flux:

int summingPrice = productList.stream()
  .collect(Collectors.summingInt(Product::getPrice));

Les méthodesaveragingXX(), summingXX() etsummarizingXX() peuvent fonctionner comme avec les primitives (int, long, double) comme avec leurs classes wrapper (Integer, Long, Double). Une fonctionnalité plus puissante de ces méthodes fournit le mappage. Ainsi, le développeur n'a pas besoin d'utiliser une opérationmap() supplémentaire avant la méthodecollect().

Collecte d'informations statistiques sur les éléments du flux:

IntSummaryStatistics statistics = productList.stream()
  .collect(Collectors.summarizingInt(Product::getPrice));

En utilisant l'instance résultante de typeIntSummaryStatistics, le développeur peut créer un rapport statistique en appliquant la méthodetoString(). Le résultat sera unString commun à celui-ci“IntSummaryStatistics\{count=5, sum=86, min=13, average=17,200000, max=23}”.

Il est également facile d'extraire de cet objet des valeurs séparées pourcount, sum, min, average en appliquant les méthodesgetCount(), getSum(), getMin(), getAverage(), getMax(). Toutes ces valeurs peuvent être extraites d'un seul pipeline.

Regroupement des éléments du flux selon la fonction spécifiée:

Map> collectorMapOfLists = productList.stream()
  .collect(Collectors.groupingBy(Product::getPrice));

Dans l'exemple ci-dessus, le flux a été réduit auMap qui regroupe tous les produits par leur prix.

Diviser les éléments du flux en groupes selon un prédicat:

Map> mapPartioned = productList.stream()
  .collect(Collectors.partitioningBy(element -> element.getPrice() > 15));

Pousser le collecteur pour effectuer une transformation supplémentaire:

Set unmodifiableSet = productList.stream()
  .collect(Collectors.collectingAndThen(Collectors.toSet(),
  Collections::unmodifiableSet));

Dans ce cas particulier, le collecteur a converti un flux en unSet, puis en a créé lesSetnon modifiables.

Collecteur personnalisé:

Si pour une raison quelconque, un collecteur personnalisé doit être créé, le moyen le plus simple et le moins détaillé de le faire est d'utiliser la méthodeof() du typeCollector.

Collector> toLinkedList =
  Collector.of(LinkedList::new, LinkedList::add,
    (first, second) -> {
       first.addAll(second);
       return first;
    });

LinkedList linkedListOfPersons =
  productList.stream().collect(toLinkedList);

Dans cet exemple, une instance desCollector a été réduite auxLinkedList<Persone>.

Flux parallèles

Avant Java 8, la parallélisation était complexe. L’émergence duExecutorService et duForkJoin a un peu simplifié la vie des développeurs, mais ils doivent tout de même garder à l’esprit comment créer un exécuteur spécifique, comment l’exécuter, etc. Java 8 a introduit un moyen de réaliser le parallélisme dans un style fonctionnel.

L'API permet de créer des flux parallèles, qui effectuent des opérations en mode parallèle. Lorsque la source d'un flux est unCollection ou unarray, cela peut être réalisé à l'aide de la méthodeparallelStream():

Stream streamOfCollection = productList.parallelStream();
boolean isParallel = streamOfCollection.isParallel();
boolean bigPrice = streamOfCollection
  .map(product -> product.getPrice() * 12)
  .anyMatch(price -> price > 200);

Si la source du flux est différente de celle d'unCollection ou d'unarray, la méthodeparallel() doit être utilisée:

IntStream intStreamParallel = IntStream.range(1, 150).parallel();
boolean isParallel = intStreamParallel.isParallel();

Sous le capot, Stream API utilise automatiquement le frameworkForkJoin pour exécuter des opérations en parallèle. Par défaut, le pool de threads commun sera utilisé et il n'y a aucun moyen (du moins pour le moment) de lui affecter un pool de threads personnalisé. This can be overcome by using a custom set of parallel collectors.

Lorsque vous utilisez des flux en mode parallèle, évitez les opérations de blocage et utilisez le mode parallèle lorsque des tâches ont besoin de la même durée (si elles durent beaucoup plus longtemps que l’autre, cela peut ralentir le flux de travail complet de l’application).

Le flux en mode parallèle peut être reconverti en mode séquentiel en utilisant la méthodesequential():

IntStream intStreamSequential = intStreamParallel.sequential();
boolean isParallel = intStreamSequential.isParallel();

Conclusions

L'API Stream est un ensemble d'outils puissant mais simple à comprendre permettant de traiter une séquence d'éléments. Cela nous permet de réduire énormément le code standard, de créer des programmes plus lisibles et d’améliorer la productivité des applications utilisées correctement.

Dans la plupart des exemples de code présentés dans cet article, les flux n'ont pas été utilisés (nous n'avons pas appliqué la méthodeclose() ni une opération de terminal). Dans une vraie application,don’t leave an instantiated streams unconsumed as that will lead to memory leaks.

Les exemples de code complets qui accompagnent l'article sont disponiblesover on GitHub.