Recherche…


Introduction

Un Stream représente une séquence d'éléments et prend en charge différents types d'opérations pour effectuer des calculs sur ces éléments. Avec Java 8, l'interface de la Collection dispose de deux méthodes pour générer un Stream : stream() et parallelStream() . Stream opérations de Stream sont soit intermédiaires, soit terminales. Les opérations intermédiaires renvoient un Stream afin que plusieurs opérations intermédiaires puissent être chaînées avant la fermeture du Stream . Les opérations du terminal sont soit vides, soit renvoient un résultat non-flux.

Syntaxe

  • collection.stream ()
  • Arrays.stream (array)
  • Stream.iterate (firstValue, currentValue -> nextValue)
  • Stream.generate (() -> valeur)
  • Stream.of (elementOfT [, elementOfT, ...])
  • Stream.empty ()
  • StreamSupport.stream (iterable.spliterator (), false)

Utiliser des flux

Un Stream est une séquence d'éléments sur laquelle des opérations d'agrégation séquentielles et parallèles peuvent être effectuées. Un Stream donné peut potentiellement contenir une quantité illimitée de données. En conséquence, les données reçues d'un Stream sont traitées individuellement à son arrivée, au lieu d'effectuer un traitement par lot sur les données. Lorsqu'elles sont combinées avec des expressions lambda, elles fournissent un moyen concis d'effectuer des opérations sur des séquences de données en utilisant une approche fonctionnelle.

Exemple: ( voir ça marche sur Ideone )

Stream<String> fruitStream = Stream.of("apple", "banana", "pear", "kiwi", "orange");

fruitStream.filter(s -> s.contains("a"))
           .map(String::toUpperCase)
           .sorted()
           .forEach(System.out::println);

Sortie:

POMME
BANANE
ORANGE
POIRE

Les opérations effectuées par le code ci-dessus peuvent être résumées comme suit:

  1. Créez un Stream<String> contenant un Stream ordonné d’éléments de String de caractères fruit à l’aide de la méthode de fabrique statique Stream.of(values) .

  2. L'opération filter() conserve uniquement les éléments correspondant à un prédicat donné (les éléments testés par le prédicat retournent true). Dans ce cas, il conserve les éléments contenant un "a" . Le prédicat est donné sous la forme d'une expression lambda .

  3. L'opération map() transforme chaque élément en utilisant une fonction donnée, appelée mappeur. Dans ce cas, chaque String Fruit est mappée sur sa version String majuscule à l'aide de la référence de méthode String::toUppercase .

    Notez que l'opération map() renverra un flux avec un type générique différent si la fonction de mappage renvoie un type différent de son paramètre d'entrée. Par exemple, sur un Stream<String> appel de .map(String::isEmpty) renvoie un Stream<Boolean>

  4. L'opération sort sorted() trie les éléments du Stream fonction de leur ordre naturel (lexicographiquement, dans le cas de la String ).

  5. Enfin, l'opération forEach(action) exécute une action qui agit sur chaque élément du Stream , en le transmettant à un consommateur . Dans l'exemple, chaque élément est simplement imprimé sur la console. Cette opération est une opération de terminal, ce qui rend son fonctionnement impossible.

    Notez que les opérations définies sur le Stream sont effectuées en raison du fonctionnement du terminal. Sans opération de terminal, le flux n'est pas traité. Les flux ne peuvent pas être réutilisés. Une fois qu'une opération de terminal est appelée, l'objet Stream devient inutilisable.

Opérations en chaîne

Les opérations (comme vu ci-dessus) sont enchaînées pour former ce qui peut être vu comme une requête sur les données.


Fermeture des cours d'eau

Notez qu'un Stream n'a généralement pas besoin d'être fermé. Il est seulement nécessaire de fermer les flux qui fonctionnent sur les canaux IO. La plupart des types de Stream ne fonctionnent pas sur les ressources et ne nécessitent donc pas de fermeture.

L'interface Stream étend AutoCloseable . Les flux peuvent être fermés en appelant la méthode close ou en utilisant des instructions try-with-resource.

Un exemple de cas d'utilisation où un Stream doit être fermé est lorsque vous créez un Stream de lignes à partir d'un fichier:

try (Stream<String> lines = Files.lines(Paths.get("somePath"))) {
    lines.forEach(System.out::println);
}

L'interface Stream déclare également la méthode Stream.onClose() qui vous permet d'enregistrer les gestionnaires Runnable qui seront appelés lorsque le flux sera fermé. Un exemple de cas d'utilisation est celui où le code qui produit un flux doit savoir quand il est utilisé pour effectuer un nettoyage.

public Stream<String>streamAndDelete(Path path) throws IOException {
    return Files.lines(path).onClose(() -> someClass.deletePath(path));
}

Le gestionnaire d'exécution ne s'exécutera que si la méthode close() est appelée, explicitement ou implicitement, par une instruction try-with-resources.


Commande en traitement

Le traitement d'un objet Stream peut être séquentiel ou parallèle .

En mode séquentiel , les éléments sont traités dans l'ordre de la source du Stream . Si le Stream est commandé (par exemple, une implémentation SortedMap ou une List ), le traitement est garanti pour correspondre à l'ordre de la source. Dans d'autres cas, toutefois, il convient de ne pas dépendre de la commande (voir: l'ordre d'itération keySet() Java HashMap keySet() cohérent? ).

Exemple:

List<Integer> integerList = Arrays.asList(0, 1, 2, 3, 42); 

// sequential 
long howManyOddNumbers = integerList.stream()
                                    .filter(e -> (e % 2) == 1)
                                    .count(); 

System.out.println(howManyOddNumbers); // Output: 2

Vivre sur Ideone

Le mode parallèle permet l'utilisation de plusieurs threads sur plusieurs cœurs, mais il n'y a aucune garantie sur l'ordre dans lequel les éléments sont traités.

Si plusieurs méthodes sont appelées sur un Stream séquentiel, toutes les méthodes ne doivent pas être appelées. Par exemple, si un Stream est filtré et que le nombre d'éléments est réduit à un, aucun appel ultérieur à une méthode telle que le sort ne se produira. Cela peut augmenter les performances d'un Stream séquentiel - une optimisation impossible avec un Stream parallèle.

Exemple:

// parallel
long howManyOddNumbersParallel = integerList.parallelStream()
                                            .filter(e -> (e % 2) == 1)
                                            .count();

System.out.println(howManyOddNumbersParallel); // Output: 2

Vivre sur Ideone


Différences par rapport aux conteneurs (ou aux collections )

Bien que certaines actions puissent être effectuées à la fois sur les conteneurs et les flux, elles servent en fin de compte à des objectifs différents et prennent en charge différentes opérations. Les conteneurs se concentrent davantage sur la manière dont les éléments sont stockés et sur la manière dont ces éléments peuvent être utilisés efficacement. Un Stream , d'autre part, ne fournit pas un accès et une manipulation directs à ses éléments; Il est plus dédié au groupe d'objets en tant qu'entité collective et effectue des opérations sur cette entité dans son ensemble. Stream et Collection sont des abstractions de haut niveau distinctes pour ces différents objectifs.

Collecte des éléments d'un flux dans une collection

Recueillir avec toList() et toSet()

Les éléments d'un Stream peuvent être facilement collectés dans un conteneur à l'aide de l'opération Stream.collect :

System.out.println(Arrays
    .asList("apple", "banana", "pear", "kiwi", "orange")
    .stream()
    .filter(s -> s.contains("a"))
    .collect(Collectors.toList())
);
// prints: [apple, banana, pear, orange]

D'autres instances de collection, telles qu'un Set , peuvent être créées à l'aide d'autres méthodes intégrées de Collectors . Par exemple, Collectors.toSet() collecte les éléments d'un Stream dans un Set .


Contrôle explicite de l'implémentation de List ou Set

Selon la documentation de Collectors#toList() et Collectors#toSet() , il n’ya aucune garantie sur le type, la mutabilité, la sérialisabilité ou la sécurité des threads de la List ou de l’ Set renvoyés.

Pour que le contrôle explicite de l'implémentation soit renvoyé, Collectors#toCollection(Supplier) peut être utilisé à la place, où le fournisseur donné retourne une nouvelle collection vide.

// syntax with method reference
System.out.println(strings
        .stream()
        .filter(s -> s != null && s.length() <= 3)
        .collect(Collectors.toCollection(ArrayList::new))
);

// syntax with lambda
System.out.println(strings
        .stream()
        .filter(s -> s != null && s.length() <= 3)
        .collect(Collectors.toCollection(() -> new LinkedHashSet<>()))
);

Collecter des éléments en utilisant toMap

Le collecteur accumule des éléments dans une carte, où la clé est l'ID de l'étudiant et la valeur est la valeur de l'étudiant.

  List<Student> students = new ArrayList<Student>(); 
    students.add(new Student(1,"test1"));
    students.add(new Student(2,"test2"));
    students.add(new Student(3,"test3"));
    
    Map<Integer, String> IdToName = students.stream()
        .collect(Collectors.toMap(Student::getId, Student::getName));
    System.out.println(IdToName);

Sortie:

{1=test1, 2=test2, 3=test3}

Le Collectors.toMap a une autre implémentation Collector<T, ?, Map<K,U>> toMap(Function<? super T, ? extends K> keyMapper, Function<? super T, ? extends U> valueMapper, BinaryOperator<U> mergeFunction) .La fonction mergeFunction est principalement utilisée pour sélectionner une nouvelle valeur ou conserver l'ancienne valeur si la clé est répétée lors de l'ajout d'un nouveau membre dans la carte à partir d'une liste.

La fonction de fusion ressemble souvent à: (s1, s2) -> s1 pour conserver la valeur correspondant à la clé répétée ou (s1, s2) -> s2 pour mettre une nouvelle valeur pour la clé répétée.

Collecte des éléments à la carte des collections

Exemple: de ArrayList à mapper <String, List <>>

Souvent, il faut créer une carte de liste à partir d'une liste primaire. Exemple: À partir d'un élève de la liste, nous devons faire une carte de la liste des matières pour chaque élève.

    List<Student> list = new ArrayList<>();
    list.add(new Student("Davis", SUBJECT.MATH, 35.0));
    list.add(new Student("Davis", SUBJECT.SCIENCE, 12.9));
    list.add(new Student("Davis", SUBJECT.GEOGRAPHY, 37.0));

    list.add(new Student("Sascha", SUBJECT.ENGLISH, 85.0));
    list.add(new Student("Sascha", SUBJECT.MATH, 80.0));
    list.add(new Student("Sascha", SUBJECT.SCIENCE, 12.0));
    list.add(new Student("Sascha", SUBJECT.LITERATURE, 50.0));

    list.add(new Student("Robert", SUBJECT.LITERATURE, 12.0));

    Map<String, List<SUBJECT>> map = new HashMap<>();
    list.stream().forEach(s -> {
                map.computeIfAbsent(s.getName(), x -> new ArrayList<>()).add(s.getSubject());
            });
    System.out.println(map);

Sortie:

{ Robert=[LITERATURE], 
Sascha=[ENGLISH, MATH, SCIENCE, LITERATURE], 
Davis=[MATH, SCIENCE, GEOGRAPHY] }

Exemple: de ArrayList à Map <String, Map <>>

    List<Student> list = new ArrayList<>();
    list.add(new Student("Davis", SUBJECT.MATH, 1, 35.0));
    list.add(new Student("Davis", SUBJECT.SCIENCE, 2, 12.9));
    list.add(new Student("Davis", SUBJECT.MATH, 3, 37.0));
    list.add(new Student("Davis", SUBJECT.SCIENCE, 4, 37.0));

    list.add(new Student("Sascha", SUBJECT.ENGLISH, 5, 85.0));
    list.add(new Student("Sascha", SUBJECT.MATH, 1, 80.0));
    list.add(new Student("Sascha", SUBJECT.ENGLISH, 6, 12.0));
    list.add(new Student("Sascha", SUBJECT.MATH, 3, 50.0));

    list.add(new Student("Robert", SUBJECT.ENGLISH, 5, 12.0));

    Map<String, Map<SUBJECT, List<Double>>> map = new HashMap<>();

    list.stream().forEach(student -> {
        map.computeIfAbsent(student.getName(), s -> new HashMap<>())
                .computeIfAbsent(student.getSubject(), s -> new ArrayList<>())
                .add(student.getMarks());
    });

    System.out.println(map);

Sortie:

{ Robert={ENGLISH=[12.0]}, 
Sascha={MATH=[80.0, 50.0], ENGLISH=[85.0, 12.0]}, 
Davis={MATH=[35.0, 37.0], SCIENCE=[12.9, 37.0]} }

Cheat-Sheet

Objectif Code
Recueillir dans une List Collectors.toList()
Recueillir dans une ArrayList avec une taille pré-allouée Collectors.toCollection(() -> new ArrayList<>(size))
Recueillir à un Set Collectors.toSet()
Recueillir dans un Set avec une meilleure performance d'itération Collectors.toCollection(() -> new LinkedHashSet<>())
Recueillir dans un Set<String> insensible à la casse Set<String> Collectors.toCollection(() -> new TreeSet<>(String.CASE_INSENSITIVE_ORDER))
Recueillir dans un EnumSet<AnEnum> (meilleures performances pour les énumérations) Collectors.toCollection(() -> EnumSet.noneOf(AnEnum.class))
Recueillir sur une Map<K,V> avec des clés uniques Collectors.toMap(keyFunc,valFunc)
Mappez MyObject.getter () sur un objet unique MyObject Collectors.toMap(MyObject::getter, Function.identity())
Mappez MyObject.getter () sur plusieurs MyObjects Collectors.groupingBy(MyObject::getter)

Streams infinis

Il est possible de générer un Stream qui ne se termine pas. L'appel d'une méthode de terminal sur un Stream infini entraîne l'entrée du Stream dans une boucle infinie. La méthode de limit d'un Stream peut être utilisée pour limiter le nombre de termes du Stream traité par Java.

Cet exemple génère un Stream de tous les nombres naturels, en commençant par le nombre 1. Chaque terme successif du Stream est supérieur à celui précédent. En appelant la méthode des limites de ce Stream , seuls les cinq premiers termes du Stream sont pris en compte et imprimés.

// Generate infinite stream - 1, 2, 3, 4, 5, 6, 7, ...
IntStream naturalNumbers = IntStream.iterate(1, x -> x + 1);

// Print out only the first 5 terms
naturalNumbers.limit(5).forEach(System.out::println);

Sortie:

1
2
3
4
5


Une autre façon de générer un flux infini consiste à utiliser la méthode Stream.generate . Cette méthode prend un lambda de type fournisseur .

// Generate an infinite stream of random numbers
Stream<Double> infiniteRandomNumbers = Stream.generate(Math::random);

// Print out only the first 10 random numbers
infiniteRandomNumbers.limit(10).forEach(System.out::println);

Consommer des flux

Un Stream ne sera parcouru que s'il y a une opération de terminal , comme count() , collect() ou forEach() . Sinon, aucune opération sur le Stream ne sera effectuée.

Dans l'exemple suivant, aucune opération de terminal n'est ajoutée au Stream . Par conséquent, l'opération filter() ne sera pas appelée et aucune sortie ne sera produite car peek() N'EST PAS une opération de terminal .

IntStream.range(1, 10).filter(a -> a % 2 == 0).peek(System.out::println);

Vivre sur Ideone

Ceci est une séquence de Stream avec une opération de terminal valide, donc une sortie est produite.

Vous pouvez également utiliser forEach au lieu de peek :

IntStream.range(1, 10).filter(a -> a % 2 == 0).forEach(System.out::println); 

Vivre sur Ideone

Sortie:

2
4
6
8

Une fois l'opération du terminal effectuée, le Stream est consommé et ne peut pas être réutilisé.


Bien qu'un objet flux donné ne peut pas être réutilisé, il est facile de créer un réutilisable Iterable que les délégués à un pipeline de flux. Cela peut être utile pour renvoyer une vue modifiée d'un ensemble de données en direct sans avoir à collecter les résultats dans une structure temporaire.

List<String> list = Arrays.asList("FOO", "BAR");
Iterable<String> iterable = () -> list.stream().map(String::toLowerCase).iterator();

for (String str : iterable) {
    System.out.println(str);
}
for (String str : iterable) {
    System.out.println(str);
}

Sortie:

foo
bar
foo
bar

Cela fonctionne car Iterable déclare une seule méthode abstraite Iterator<T> iterator() . Cela en fait une interface fonctionnelle, implémentée par un lambda qui crée un nouveau flux à chaque appel.


En général, un Stream fonctionne comme indiqué dans l'image suivante:

Fonctionnement du flux

REMARQUE : Les vérifications d'argument sont toujours effectuées, même sans opération de terminal :

try {
    IntStream.range(1, 10).filter(null);
} catch (NullPointerException e) {
    System.out.println("We got a NullPointerException as null was passed as an argument to filter()");
}

Vivre sur Ideone

Sortie:

Nous avons une exception NullPointerException car null a été passé en argument à filter ()

Créer une carte de fréquence

Le collecteur groupingBy(classifier, downstream) permet la collecte d'éléments Stream dans une Map en classant chaque élément dans un groupe et en effectuant une opération en aval sur les éléments classés dans le même groupe.

Un exemple classique de ce principe consiste à utiliser une Map pour compter les occurrences d'éléments dans un Stream . Dans cet exemple, le classificateur est simplement la fonction d'identité, qui renvoie l'élément tel quel. L'opération en aval compte le nombre d'éléments égaux, en utilisant le counting() .

Stream.of("apple", "orange", "banana", "apple")
      .collect(Collectors.groupingBy(Function.identity(), Collectors.counting()))
      .entrySet()
      .forEach(System.out::println);

L'opération en aval est elle-même un collecteur ( Collectors.counting() ) qui opère sur des éléments de type String et produit un résultat de type Long . Le résultat de l'appel de la méthode de collect est un Map<String, Long> .

Cela produirait la sortie suivante:

banane = 1
orange = 1
apple = 2

Flux parallèle

Remarque: Avant de décider quel Stream utiliser, consultez le comportement ParallelStream vs Sequential Stream .

Lorsque vous souhaitez effectuer des opérations de Stream simultanément, vous pouvez utiliser l'une de ces méthodes.

List<String> data = Arrays.asList("One", "Two", "Three", "Four", "Five");
Stream<String> aParallelStream = data.stream().parallel();

Ou:

Stream<String> aParallelStream = data.parallelStream();

Pour exécuter les opérations définies pour le flux parallèle, appelez un opérateur de terminal:

aParallelStream.forEach(System.out::println);

(A possible) sortie du Stream parallèle:

Trois
Quatre
Un
Deux
Cinq

L'ordre peut changer car tous les éléments sont traités en parallèle (ce qui peut le rendre plus rapide). Utilisez parallelStream lorsque la commande n'a pas d'importance.

Impact sur la performance

Dans le cas où la mise en réseau est impliquée, les Stream parallèles peuvent dégrader les performances globales d'une application car tous les Stream parallèles utilisent un pool de threads de jointure commune pour le réseau.

D'autre part, les Stream parallèles peuvent améliorer considérablement les performances dans de nombreux autres cas, en fonction du nombre de cœurs disponibles dans le processeur en cours d'exécution.

Conversion d'un flux de données facultatif en un flux de valeurs

Vous devrez peut-être convertir un Stream émetteur Optional en un Stream de valeurs, en n'émettant que des valeurs de Optional existant. (c.-à-d. sans valeur null et ne pas traiter avec Optional.empty() ).

 Optional<String> op1 = Optional.empty();
 Optional<String> op2 = Optional.of("Hello World");

 List<String> result = Stream.of(op1, op2)
                             .filter(Optional::isPresent)
                             .map(Optional::get)
                             .collect(Collectors.toList());

 System.out.println(result); //[Hello World]

Créer un flux

Tous les java Collection<E> ont des méthodes stream() et parallelStream() partir desquelles un Stream<E> peut être construit:

Collection<String> stringList = new ArrayList<>();
Stream<String> stringStream = stringList.parallelStream();

Un Stream<E> peut être créé à partir d'un tableau en utilisant l'une des deux méthodes suivantes:

String[] values = { "aaa", "bbbb", "ddd", "cccc" };
Stream<String> stringStream = Arrays.stream(values);
Stream<String> stringStreamAlternative = Stream.of(values);

La différence entre Arrays.stream() et Stream.of() est que Stream.of() a un paramètre varargs, donc il peut être utilisé comme:

Stream<Integer> integerStream = Stream.of(1, 2, 3);

Il existe également des Stream primitifs que vous pouvez utiliser. Par exemple:

IntStream intStream = IntStream.of(1, 2, 3);
DoubleStream doubleStream = DoubleStream.of(1.0, 2.0, 3.0);

Ces flux primitifs peuvent également être construits à l'aide de la méthode Arrays.stream() :

IntStream intStream = Arrays.stream(new int[]{ 1, 2, 3 });

Il est possible de créer un Stream partir d'un tableau avec une plage spécifiée.

int[] values= new int[]{1, 2, 3, 4, 5};
IntStream intStram = Arrays.stream(values, 1, 3);

Notez que tout flux primitif peut être converti en flux de type boîte en utilisant la méthode des boxed :

Stream<Integer> integerStream = intStream.boxed();

Cela peut être utile dans certains cas si vous souhaitez collecter les données, car le flux primitif ne possède aucune méthode de collect prenant un Collector comme argument.

Réutilisation des opérations intermédiaires d'une chaîne de flux

Le flux est fermé lorsque le terminal est appelé. Réutiliser le flux d'opérations intermédiaires, lorsque seul le fonctionnement du terminal ne fait que varier. nous pourrions créer un fournisseur de flux pour construire un nouveau flux avec toutes les opérations intermédiaires déjà configurées.

Supplier<Stream<String>> streamSupplier = () -> Stream.of("apple", "banana","orange", "grapes", "melon","blueberry","blackberry")
.map(String::toUpperCase).sorted();

  streamSupplier.get().filter(s ->   s.startsWith("A")).forEach(System.out::println);

// APPLE

  streamSupplier.get().filter(s -> s.startsWith("B")).forEach(System.out::println);

  // BANANA
  // BLACKBERRY
  // BLUEBERRY

int[] tableaux int[] peuvent être convertis en List<Integer> aide de flux

int[] ints = {1,2,3};
List<Integer> list = IntStream.of(ints).boxed().collect(Collectors.toList());

Recherche de statistiques sur les flux numériques

Java 8 fournit des classes appelées IntSummaryStatistics , DoubleSummaryStatistics et LongSummaryStatistics qui fournissent un objet d'état pour collecter des statistiques telles que count , min , max , sum et average .

Java SE 8
List<Integer> naturalNumbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
IntSummaryStatistics stats = naturalNumbers.stream()
                                           .mapToInt((x) -> x)
                                           .summaryStatistics();
System.out.println(stats);

Ce qui entraînera:

Java SE 8
IntSummaryStatistics{count=10, sum=55, min=1, max=10, average=5.500000}

Obtenir une tranche d'un flux

Exemple: Obtenez un Stream de 30 éléments, contenant du 21ème au 50ème élément (inclus) d'une collection.

final long n = 20L; // the number of elements to skip
final long maxSize = 30L; // the number of elements the stream should be limited to
final Stream<T> slice = collection.stream().skip(n).limit(maxSize);

Remarques:

  • IllegalArgumentException est IllegalArgumentException si n est négatif ou maxSize négatif
  • skip(long) et limit(long) sont des opérations intermédiaires
  • si un flux contient moins de n éléments, alors skip(n) renvoie un flux vide
  • skip(long) et limit(long) sont des opérations peu coûteuses sur des pipelines de flux séquentiels, mais peuvent coûter très cher sur des pipelines parallèles ordonnés

Concatenate Streams

Déclaration de variable pour des exemples:

Collection<String> abc = Arrays.asList("a", "b", "c");
Collection<String> digits = Arrays.asList("1", "2", "3");
Collection<String> greekAbc = Arrays.asList("alpha", "beta", "gamma");

Exemple 1 - Concaténer deux Stream s

final Stream<String> concat1 = Stream.concat(abc.stream(), digits.stream());

concat1.forEach(System.out::print);
// prints: abc123

Exemple 2 - Concaténer plus de deux Stream s

final Stream<String> concat2 = Stream.concat(
    Stream.concat(abc.stream(), digits.stream()),
    greekAbc.stream());

System.out.println(concat2.collect(Collectors.joining(", ")));
// prints: a, b, c, 1, 2, 3, alpha, beta, gamma

Alternativement, pour simplifier la concat() imbriquée de concat() les Stream peuvent également être concaténés avec flatMap() :

final Stream<String> concat3 = Stream.of(
    abc.stream(), digits.stream(), greekAbc.stream())
    .flatMap(s -> s);
    // or `.flatMap(Function.identity());` (java.util.function.Function)

System.out.println(concat3.collect(Collectors.joining(", ")));
// prints: a, b, c, 1, 2, 3, alpha, beta, gamma

Soyez prudent lors de la construction de Stream s à partir de concaténations répétées, car l'accès à un élément d'un Stream profondément concaténé peut entraîner des chaînes d'appel profondes ou même une StackOverflowException .

IntStream en chaîne

Java ne possède pas de flux de caractères , donc lorsque vous travaillez avec des String et que vous construisez un Stream de Character , une option consiste à obtenir un IntStream de points de code en utilisant la méthode String.codePoints() . Donc, IntStream peut être obtenu comme ci-dessous:

public IntStream stringToIntStream(String in) {
  return in.codePoints();
}

Il est un peu plus compliqué de faire la conversion autrement que IntStreamToString. Cela peut se faire comme suit:

public String intStreamToString(IntStream intStream) {
  return intStream.collect(StringBuilder::new, StringBuilder::appendCodePoint, StringBuilder::append).toString();
}

Trier avec Stream

List<String> data = new ArrayList<>();
data.add("Sydney");
data.add("London");
data.add("New York");
data.add("Amsterdam");
data.add("Mumbai");
data.add("California");

System.out.println(data);

List<String> sortedData = data.stream().sorted().collect(Collectors.toList());

System.out.println(sortedData);

Sortie:

[Sydney, London, New York, Amsterdam, Mumbai, California]
[Amsterdam, California, London, Mumbai, New York, Sydney]

Il est également possible d'utiliser un mécanisme de comparaison différent car il existe une version sorted surchargée qui prend un comparateur comme argument.

En outre, vous pouvez utiliser une expression lambda pour le tri:

List<String> sortedData2 = data.stream().sorted((s1,s2) -> s2.compareTo(s1)).collect(Collectors.toList());

Cela produirait [Sydney, New York, Mumbai, London, California, Amsterdam]

Vous pouvez utiliser Comparator.reverseOrder() pour avoir un comparateur qui impose l' reverse de l'ordre naturel.

List<String> reverseSortedData = data.stream().sorted(Comparator.reverseOrder()).collect(Collectors.toList());

Flux de primitifs

Java fournit des Stream spécialisés pour trois types de primitives IntStream (for int s), LongStream (for s long ) et DoubleStream (for double s). En plus d'être des implémentations optimisées pour leurs primitives respectives, elles fournissent également plusieurs méthodes de terminal spécifiques, généralement pour des opérations mathématiques. Par exemple:

IntStream is = IntStream.of(10, 20, 30);
double average = is.average().getAsDouble(); // average is 20.0

Recueillir les résultats d'un flux dans un tableau

Analogue pour obtenir une collection pour un Stream par collect() un tableau peut être obtenu par la méthode Stream.toArray() :

List<String> fruits = Arrays.asList("apple", "banana", "pear", "kiwi", "orange");

String[] filteredFruits = fruits.stream()
    .filter(s -> s.contains("a"))
    .toArray(String[]::new);     

// prints: [apple, banana, pear, orange]
System.out.println(Arrays.toString(filteredFruits));

String[]::new est un type spécial de référence de méthode: une référence de constructeur.

Trouver le premier élément qui correspond à un prédicat

Il est possible de trouver le premier élément d'un Stream correspondant à une condition.

Pour cet exemple, nous trouverons le premier Integer dont le carré est supérieur à 50000 .

IntStream.iterate(1, i -> i + 1) // Generate an infinite stream 1,2,3,4...
    .filter(i -> (i*i) > 50000) // Filter to find elements where the square is >50000
    .findFirst(); // Find the first filtered element

Cette expression retournera un OptionalInt avec le résultat.

Notez qu'avec un Stream infini, Java continuera à vérifier chaque élément jusqu'à ce qu'il trouve un résultat. Avec un Stream fini, si Java est à court d'éléments mais ne peut toujours pas trouver de résultat, il retourne un OptionalInt vide.

Utiliser IntStream pour itérer sur les index

Stream d'éléments ne permettent généralement pas d'accéder à la valeur d'index de l'élément en cours. Pour parcourir un tableau ou ArrayList tout en ayant accès aux index, utilisez IntStream.range(start, endExclusive) .

String[] names = { "Jon", "Darin", "Bauke", "Hans", "Marc" };

IntStream.range(0, names.length)
    .mapToObj(i -> String.format("#%d %s", i + 1, names[i]))
    .forEach(System.out::println);

La méthode range(start, endExclusive) renvoie un autre ÌntStream et mapToObj(mapper) renvoie un flux de String .

Sortie:

# 1 Jon
# 2 Darin
# 3 Bauke
# 4 Hans
# 5 Marc

Ceci est très similaire à l' aide d' une normale for la boucle avec un compteur, mais avec l'avantage de pipelining et parallélisation:

for (int i = 0; i < names.length; i++) {
    String newName = String.format("#%d %s", i + 1, names[i]);
    System.out.println(newName);
}

Aplatir les Streams avec flatMap ()

Un Stream d'éléments pouvant être à leur tour diffusés peut être mis à plat en un seul Stream continu:

Un tableau de liste d'éléments peut être converti en une seule liste.

List<String> list1 =  Arrays.asList("one", "two");
      List<String> list2 =  Arrays.asList("three","four","five");
      List<String> list3 =  Arrays.asList("six");
          List<String> finalList = Stream.of(list1, list2, list3).flatMap(Collection::stream).collect(Collectors.toList());
System.out.println(finalList);

// [one, two, three, four, five, six]

Une carte contenant une liste d'éléments en tant que valeurs peut être aplatie en une liste combinée

Map<String, List<Integer>> map = new LinkedHashMap<>();
map.put("a", Arrays.asList(1, 2, 3));
map.put("b", Arrays.asList(4, 5, 6));

List<Integer> allValues = map.values() // Collection<List<Integer>>
        .stream()                      // Stream<List<Integer>>
        .flatMap(List::stream)         // Stream<Integer>
        .collect(Collectors.toList());

System.out.println(allValues);
// [1, 2, 3, 4, 5, 6]

List de Map peut être mise à plat en un seul Stream continu

List<Map<String, String>> list = new ArrayList<>();
Map<String,String> map1 = new HashMap();
map1.put("1", "one");
map1.put("2", "two");

Map<String,String> map2 = new HashMap();
map2.put("3", "three");
map2.put("4", "four");
list.add(map1);
list.add(map2);


Set<String> output= list.stream()  //  Stream<Map<String, String>>
    .map(Map::values)              // Stream<List<String>>
    .flatMap(Collection::stream)   // Stream<String>
    .collect(Collectors.toSet());  //Set<String>
// [one, two, three,four]

Créer une carte basée sur un flux

Cas simple sans clés en double

Stream<String> characters = Stream.of("A", "B", "C");

Map<Integer, String> map = characters
            .collect(Collectors.toMap(element -> element.hashCode(), element -> element));
// map = {65=A, 66=B, 67=C}

Pour rendre les choses plus déclaratives, nous pouvons utiliser la méthode statique dans Function interface de Function.identity() - Function.identity() . Nous pouvons remplacer cet element -> element lambda element -> element par Function.identity() .

Cas où il pourrait y avoir des clés en double

Le javadoc pour Collectors.toMap indique:

Si les clés mis en correspondance contient des doublons (selon Object.equals(Object) ), une IllegalStateException est levée lorsque l'opération de collecte est effectuée. Si les clés mappées peuvent avoir des doublons, utilisez toMap(Function, Function, BinaryOperator) .

Stream<String> characters = Stream.of("A", "B", "B", "C");

Map<Integer, String> map = characters
            .collect(Collectors.toMap(
                element -> element.hashCode(),
                element -> element,
                (existingVal, newVal) -> (existingVal + newVal)));

// map = {65=A, 66=BB, 67=C}

Le BinaryOperator transmis à Collectors.toMap(...) génère la valeur à stocker en cas de collision. Ça peut:

  • renvoyer l'ancienne valeur, de sorte que la première valeur du flux prenne le pas,
  • renvoyer la nouvelle valeur, de sorte que la dernière valeur du flux soit prioritaire, ou
  • combiner les anciennes et les nouvelles valeurs

Regroupement par valeur

Vous pouvez utiliser Collectors.groupingBy lorsque vous devez effectuer l'équivalent d'une opération "group by" en cascade dans une base de données. Pour illustrer cela, ce qui suit crée une carte dans laquelle les noms des personnes sont mappés aux noms de famille:

List<Person> people = Arrays.asList(
    new Person("Sam", "Rossi"),
    new Person("Sam", "Verdi"),
    new Person("John", "Bianchi"),
    new Person("John", "Rossi"),
    new Person("John", "Verdi")
);

Map<String, List<String>> map = people.stream()
        .collect(
                // function mapping input elements to keys
                Collectors.groupingBy(Person::getName, 
                // function mapping input elements to values,
                // how to store values
                Collectors.mapping(Person::getSurname, Collectors.toList()))
        );

// map = {John=[Bianchi, Rossi, Verdi], Sam=[Rossi, Verdi]}

Vivre sur Ideone

Génération de chaînes aléatoires à l'aide de flux

Il est parfois utile de créer des Strings aléatoires, peut-être comme ID de session pour un service Web ou un mot de passe initial après l'inscription à une application. Cela peut être facilement réalisé en utilisant les Stream .

Nous devons d'abord initialiser un générateur de nombres aléatoires. Pour améliorer la sécurité des String générées, il est SecureRandom utiliser SecureRandom .

Remarque : la création d'un SecureRandom est assez onéreuse, il est donc SecureRandom de ne le faire qu'une seule fois et d'appeler de temps en temps l'une de ses méthodes setSeed() pour le réamorcer.

private static final SecureRandom rng = new SecureRandom(SecureRandom.generateSeed(20)); 
//20 Bytes as a seed is rather arbitrary, it is the number used in the JavaDoc example

Lors de la création de String aléatoires, nous souhaitons généralement qu'elles utilisent uniquement certains caractères (par exemple, uniquement des lettres et des chiffres). Nous pouvons donc créer une méthode renvoyant un boolean qui pourra ensuite être utilisé pour filtrer le Stream .

//returns true for all chars in 0-9, a-z and A-Z
boolean useThisCharacter(char c){
    //check for range to avoid using all unicode Letter (e.g. some chinese symbols)
    return c >= '0' && c <= 'z' && Character.isLetterOrDigit(c);
}

Ensuite, nous pouvons utiliser le RNG pour générer une chaîne aléatoire de longueur spécifique contenant le jeu de caractères qui passe notre vérification useThisCharacter .

public String generateRandomString(long length){
    //Since there is no native CharStream, we use an IntStream instead
    //and convert it to a Stream<Character> using mapToObj.
    //We need to specify the boundaries for the int values to ensure they can safely be cast to char
    Stream<Character> randomCharStream = rng.ints(Character.MIN_CODE_POINT, Character.MAX_CODE_POINT).mapToObj(i -> (char)i).filter(c -> this::useThisCharacter).limit(length);

    //now we can use this Stream to build a String utilizing the collect method.
    String randomString = randomCharStream.collect(StringBuilder::new, StringBuilder::append, StringBuilder::append).toString();
    return randomString;
}

Utilisation de flux pour implémenter des fonctions mathématiques

Stream , et en particulier IntStream , constituent un moyen élégant d'implémenter des termes de sommation (∑). Les plages du Stream peuvent être utilisées comme limites de la somme.

Par exemple, l'approximation de Pi par Madhava est donnée par la formule (Source: wikipedia ): L'approximation de Madhava

Cela peut être calculé avec une précision arbitraire. Par exemple, pour 101 termes:

double pi = Math.sqrt(12) * 
            IntStream.rangeClosed(0, 100)
                     .mapToDouble(k -> Math.pow(-3, -1 * k) / (2 * k + 1))
                     .sum();

Note: Avec la précision du double , sélectionner une limite supérieure de 29 est suffisant pour obtenir un résultat indiscernable de Math.Pi

Utilisation de références de flux et de méthode pour écrire des processus auto-documentés

Les références de méthode constituent un excellent code auto-documenté, et l'utilisation de références de méthode avec Stream facilite la lisibilité et la compréhension des processus complexes. Considérez le code suivant:

public interface Ordered {
    default int getOrder(){
        return 0;
    }
}

public interface Valued<V extends Ordered> {
    boolean hasPropertyTwo();
    V getValue();
}

public interface Thing<V extends Ordered> {
    boolean hasPropertyOne();
    Valued<V> getValuedProperty();
}

public <V extends Ordered> List<V> myMethod(List<Thing<V>> things) {
    List<V> results = new ArrayList<V>();
    for (Thing<V> thing : things) {
        if (thing.hasPropertyOne()) {
            Valued<V> valued = thing.getValuedProperty();
            if (valued != null && valued.hasPropertyTwo()){
                V value = valued.getValue();
                if (value != null){
                    results.add(value);
                }
            }
        }
    }
    results.sort((a, b)->{
        return Integer.compare(a.getOrder(), b.getOrder());
    });
    return results;
}

Cette dernière méthode réécrite à l'aide de Stream et de références de méthode est beaucoup plus lisible et chaque étape du processus est rapidement et facilement comprise - elle n'est pas seulement plus courte, elle montre également les interfaces et les classes responsables du code dans chaque étape:

public <V extends Ordered> List<V> myMethod(List<Thing<V>> things) {
    return things.stream()
        .filter(Thing::hasPropertyOne)
        .map(Thing::getValuedProperty)
        .filter(Objects::nonNull)
        .filter(Valued::hasPropertyTwo)
        .map(Valued::getValue)
        .filter(Objects::nonNull)
        .sorted(Comparator.comparing(Ordered::getOrder))
        .collect(Collectors.toList());
}    

Utilisation de flux de Map.Entry pour conserver les valeurs initiales après le mappage

Lorsque vous devez mapper un Stream mais que vous souhaitez également conserver les valeurs initiales, vous pouvez mapper le Stream vers un Map.Entry<K,V> utilisant une méthode d’utilitaire comme celle-ci:

public static <K, V> Function<K, Map.Entry<K, V>> entryMapper(Function<K, V> mapper){
    return (k)->new AbstractMap.SimpleEntry<>(k, mapper.apply(k));
}

Vous pouvez ensuite utiliser votre convertisseur pour traiter les Stream ayant accès aux valeurs d'origine et mappées:

Set<K> mySet;
Function<K, V> transformer = SomeClass::transformerMethod;
Stream<Map.Entry<K, V>> entryStream = mySet.stream()
    .map(entryMapper(transformer));

Vous pouvez ensuite continuer à traiter ce Stream comme d'habitude. Cela évite de créer une collection intermédiaire.

Catégories d'opérations de flux

Les opérations de flux se répartissent en deux catégories principales, les opérations intermédiaires et terminales, et deux sous-catégories, sans état et avec état.


Opérations intermédiaires:

Une opération intermédiaire est toujours paresseuse , telle qu'une simple Stream.map . Il n'est pas appelé tant que le flux n'est pas réellement consommé. Cela peut être vérifié facilement:

Arrays.asList(1, 2 ,3).stream().map(i -> {
    throw new RuntimeException("not gonna happen");
    return i;
});

Les opérations intermédiaires sont les blocs de construction communs d'un flux, chaînés après la source et sont généralement suivis d'une opération de terminal déclenchant la chaîne de flux.


Opérations Terminal

Les opérations terminales sont ce qui déclenche la consommation d'un flux. Les plus courants sont Stream.forEach ou Stream.collect . Ils sont généralement placés après une chaîne d'opérations intermédiaires et sont presque toujours impatients .


Opérations apatrides

L'apatridie signifie que chaque élément est traité sans le contexte des autres éléments. Les opérations sans état permettent un traitement efficace des flux dans la mémoire. Les opérations telles que Stream.map et Stream.filter ne nécessitant pas d'informations sur d'autres éléments du flux sont considérées comme étant sans état.


Opérations avec état

Statefulness signifie que l'opération sur chaque élément dépend de (certains) autres éléments du flux. Cela nécessite un état à préserver. Les opérations d'état peuvent se rompre avec des flux longs ou infinis. Les opérations telles que Stream.sorted requièrent que l'intégralité du flux soit traitée avant que tout élément ne soit émis, ce qui entraînera un flux d'éléments suffisamment long. Cela peut être démontré par un long flux ( exécuté à vos risques et périls ):

// works - stateless stream
long BIG_ENOUGH_NUMBER = 999999999;
IntStream.iterate(0, i -> i + 1).limit(BIG_ENOUGH_NUMBER).forEach(System.out::println);

Cela provoquera un manque de mémoire dû à l'état de Stream.sorted :

// Out of memory - stateful stream
IntStream.iterate(0, i -> i + 1).limit(BIG_ENOUGH_NUMBER).sorted().forEach(System.out::println);

Conversion d'un itérateur en flux

Utilisez Spliterators.spliterator() ou Spliterators.spliteratorUnknownSize() pour convertir un itérateur en flux:

Iterator<String> iterator = Arrays.asList("A", "B", "C").iterator();    
Spliterator<String> spliterator = Spliterators.spliteratorUnknownSize(iterator, 0);
Stream<String> stream = StreamSupport.stream(spliterator, false);

Réduction avec des flux

La réduction est le processus consistant à appliquer un opérateur binaire à chaque élément d'un flux pour obtenir une valeur.

La méthode sum() d'un IntStream est un exemple de réduction; il applique une addition à chaque terme du Stream, résultant en une valeur finale: Réduction de somme

Ceci est équivalent à (((1+2)+3)+4)

La méthode de reduce d'un flux permet de créer une réduction personnalisée. Il est possible d'utiliser la méthode reduce pour implémenter la méthode sum() :

IntStream istr;
    
//Initialize istr
    
OptionalInt istr.reduce((a,b)->a+b);

La version Optional est renvoyée afin que les flux vides puissent être traités de manière appropriée.

Un autre exemple de réduction consiste à combiner un Stream<LinkedList<T>> en un seul LinkedList<T> :

Stream<LinkedList<T>> listStream;
    
//Create a Stream<LinkedList<T>>
    
Optional<LinkedList<T>> bigList = listStream.reduce((LinkedList<T> list1, LinkedList<T> list2)->{
    LinkedList<T> retList = new LinkedList<T>();
    retList.addAll(list1);
    retList.addAll(list2);
    return retList;
});

Vous pouvez également fournir un élément d'identité . Par exemple, l'élément d'identité pour l'addition est 0, comme x+0==x . Pour la multiplication, l'élément d'identité est 1, comme x*1==x . Dans le cas ci-dessus, l'élément identity est une LinkedList<T> vide LinkedList<T> , car si vous ajoutez une liste vide à une autre liste, la liste à laquelle vous "ajoutez" ne change pas:

Stream<LinkedList<T>> listStream;

//Create a Stream<LinkedList<T>>

LinkedList<T> bigList = listStream.reduce(new LinkedList<T>(), (LinkedList<T> list1, LinkedList<T> list2)->{
    LinkedList<T> retList = new LinkedList<T>();
    retList.addAll(list1);
    retList.addAll(list2);
    return retList;
});

Notez que lorsqu'un élément d'identité est fourni, la valeur de retour n'est pas encapsulée dans un élément Optional -si appelé sur un flux vide, reduce() renvoie l'élément d'identité.

L'opérateur binaire doit également être associatif , ce qui signifie que (a+b)+c==a+(b+c) . C'est parce que les éléments peuvent être réduits dans n'importe quel ordre. Par exemple, la réduction d'addition ci-dessus peut être effectuée comme suit:

Autre réduction de somme

Cette réduction équivaut à écrire ((1+2)+(3+4)) . La propriété d'associativité permet également à Java de réduire le Stream en parallèle. Une partie du flux peut être réduite par chaque processeur, avec une réduction combinant le résultat de chaque processeur à la fin.

Joindre un flux à une seule chaîne

Un cas d'utilisation fréquemment rencontré consiste à créer une String partir d'un flux, où les éléments de flux sont séparés par un certain caractère. La méthode Collectors.joining() peut être utilisée pour cela, comme dans l'exemple suivant:

Stream<String> fruitStream = Stream.of("apple", "banana", "pear", "kiwi", "orange");

String result = fruitStream.filter(s -> s.contains("a"))
           .map(String::toUpperCase)
           .sorted()
           .collect(Collectors.joining(", "));
           
System.out.println(result);

Sortie:

POMME, BANANE, ORANGE, POIRE

La méthode Collectors.joining() peut également prendre en charge les pré-et postfixes:

String result = fruitStream.filter(s -> s.contains("e"))
           .map(String::toUpperCase)
           .sorted()
           .collect(Collectors.joining(", ", "Fruits: ", "."));
           
System.out.println(result);

Sortie:

Fruits: POMME, ORANGE, POIRE.

Vivre sur Ideone



Modified text is an extract of the original Stack Overflow Documentation
Sous licence CC BY-SA 3.0
Non affilié à Stack Overflow