Ricerca…


introduzione

Un Stream rappresenta una sequenza di elementi e supporta diversi tipi di operazioni per eseguire calcoli su tali elementi. Con Java 8, l'interfaccia Collection ha due metodi per generare un Stream : stream() e parallelStream() . Stream operazioni di Stream sono intermedie o terminali. Le operazioni intermedie restituiscono un Stream modo che più operazioni intermedie possano essere concatenate prima che il Stream venga chiuso. Le operazioni terminal sono nulle o restituiscono un risultato non stream.

Sintassi

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

Usando i flussi

Un Stream è una sequenza di elementi su cui è possibile eseguire operazioni di aggregazione sequenziali e parallele. Qualsiasi Stream dato può potenzialmente contenere una quantità illimitata di dati. Di conseguenza, i dati ricevuti da un Stream vengono elaborati singolarmente al loro arrivo, anziché eseguire l'elaborazione batch sui dati del tutto. Quando combinati con espressioni lambda forniscono un modo conciso per eseguire operazioni su sequenze di dati utilizzando un approccio funzionale.

Esempio: ( vedi funziona su 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);

Produzione:

MELA
BANANA
ARANCIA
PERA

Le operazioni eseguite dal codice precedente possono essere riassunte come segue:

  1. Crea uno Stream<String> contenente un Stream ordinato di elementi String di Stream ordinati utilizzando il metodo statico di fabbrica Stream.of(values) .

  2. L'operazione filter() conserva solo gli elementi che corrispondono a un determinato predicato (gli elementi che vengono testati dal predicato restituiscono true). In questo caso, mantiene gli elementi contenenti una "a" . Il predicato è dato come espressione lambda .

  3. L'operazione map() trasforma ogni elemento usando una determinata funzione, chiamata mapper. In questo caso, ogni Fruit String viene associata alla sua versione String maiuscolo utilizzando il metodo method: String::toUppercase .

    Si noti che l'operazione map() restituirà un flusso con un diverso tipo generico se la funzione di mappatura restituisce un tipo diverso dal proprio parametro di input. Ad esempio su uno Stream<String> chiama .map(String::isEmpty) restituisce un Stream<Boolean>

  4. L'operazione sort sorted() ordina gli elementi del Stream base al loro ordinamento naturale (lessicograficamente, nel caso di String ).

  5. Infine, l'operazione forEach(action) esegue un'azione che agisce su ciascun elemento del Stream , passandolo a un consumatore . Nell'esempio, ogni elemento viene semplicemente stampato sulla console. Questa operazione è un'operazione terminale, rendendo impossibile l'operazione su di esso.

    Si noti che le operazioni definite sullo Stream vengono eseguite a causa dell'operazione del terminale. Senza un'operazione terminale, lo stream non viene elaborato. Gli stream non possono essere riutilizzati. Una volta che viene chiamata un'operazione terminale, l'oggetto Stream diventa inutilizzabile.

Operazioni concatenate

Le operazioni (come visto sopra) sono concatenate per formare ciò che può essere visto come una query sui dati.


Flussi di chiusura

Nota che un Stream generalmente non deve essere chiuso. È richiesto solo per chiudere i flussi che operano su canali IO. La maggior parte dei tipi di Stream non funziona su risorse e pertanto non richiede la chiusura.

L'interfaccia Stream estende AutoCloseable . Gli stream possono essere chiusi chiamando il metodo close o usando le istruzioni try-with-resource.

Un esempio di caso d'uso in cui un Stream dovrebbe essere chiuso è quando crei un Stream di linee da un file:

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

L'interfaccia Stream dichiara anche il metodo Stream.onClose() che consente di registrare i gestori Runnable che verranno chiamati quando lo stream viene chiuso. Un caso di utilizzo di esempio è dove il codice che produce un flusso deve sapere quando viene utilizzato per eseguire una pulizia.

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

Il gestore di esecuzione verrà eseguito solo se viene chiamato il metodo close() , esplicitamente o implicitamente da un'istruzione try-with-resources.


Ordine di elaborazione

L'elaborazione di un oggetto Stream può essere sequenziale o parallela .

In una modalità sequenziale , gli elementi vengono elaborati nell'ordine della sorgente del Stream . Se il Stream è ordinato (come un'implementazione SortedMap o un List ), l'elaborazione è garantita per corrispondere all'ordine della fonte. In altri casi, tuttavia, è necessario prestare attenzione a non dipendere dall'ordinamento (si veda: l'ordine di iterazione di Java HashMap keySet() coerente? ).

Esempio:

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

Vivi su Ideone

La modalità parallela consente l'utilizzo di più thread su più core, ma non vi è alcuna garanzia dell'ordine in cui vengono elaborati gli elementi.

Se più metodi vengono richiamati su un Stream sequenziale, non tutti i metodi devono essere richiamati. Ad esempio, se un Stream viene filtrato e il numero di elementi è ridotto a uno, non verrà eseguita una chiamata successiva a un metodo come l' sort . Ciò può aumentare le prestazioni di un Stream sequenziale: un'ottimizzazione che non è possibile con un Stream parallelo.

Esempio:

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

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

Vivi su Ideone


Differenze da contenitori (o collezioni )

Mentre alcune azioni possono essere eseguite sia su Containers che su Stream, alla fine hanno scopi diversi e supportano diverse operazioni. I contenitori sono più focalizzati su come gli elementi sono memorizzati e su come è possibile accedere a tali elementi in modo efficiente. Un Stream , d'altra parte, non fornisce accesso diretto e manipolazione ai suoi elementi; è più dedicato al gruppo di oggetti come entità collettiva ed esegue operazioni su quell'entità nel suo complesso. Stream e Collection sono astrazioni di alto livello separate per questi scopi diversi.

Raccogli elementi di un flusso in una raccolta

Raccogli con toList() e toSet()

Gli elementi di un Stream possono essere facilmente raccolti in un contenitore usando l'operazione 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]

Altre istanze di raccolta, come un Set , possono essere create usando altri metodi built-in di Collectors . Ad esempio, Collectors.toSet() raccoglie gli elementi di un Stream in un Set .


Controllo esplicito sull'implementazione di List o Set

Secondo la documentazione di Collectors#toList() e Collectors#toSet() , non ci sono garanzie sul tipo, sulla mutevolezza, sulla serializzabilità o sulla sicurezza del thread List o del Set restituito.

Per il controllo esplicito dell'implementazione da restituire, è possibile utilizzare Collectors#toCollection(Supplier) , in cui il fornitore specificato restituisce una raccolta nuova e vuota.

// 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<>()))
);

Raccolta di elementi usando toMap

Il raccoglitore accumula elementi in una mappa, dove la chiave è l'id dello studente e il valore è il valore dello studente.

  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);

Produzione :

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

Collectors.toMap ha un'altra implementazione Collector<T, ?, Map<K,U>> toMap(Function<? super T, ? extends K> keyMapper, Function<? super T, ? extends U> valueMapper, BinaryOperator<U> mergeFunction) . La funzione mergeFunction viene utilizzata principalmente per selezionare un nuovo valore o mantenere il vecchio valore se la chiave viene ripetuta quando si aggiunge un nuovo membro nella mappa da un elenco.

Spesso la mergeFunction assomiglia a: (s1, s2) -> s1 per mantenere il valore corrispondente alla chiave ripetuta, o (s1, s2) -> s2 per inserire un nuovo valore per la chiave ripetuta.

Raccolta di elementi per la mappa delle collezioni

Esempio: da ArrayList a Map <String, List <>>

Spesso è necessario creare una mappa dell'elenco da un elenco principale. Esempio: da uno studente della lista, abbiamo bisogno di fare una mappa dell'elenco delle materie per ogni studente.

    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);

Produzione:

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

Esempio: da ArrayList a 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);

Produzione:

{ 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

Obbiettivo Codice
Raccogli a una List Collectors.toList()
Raccogli a un ArrayList con dimensioni pre-allocate Collectors.toCollection(() -> new ArrayList<>(size))
Raccogli per un Set Collectors.toSet()
Raccogli in un Set con prestazioni di iterazione migliori Collectors.toCollection(() -> new LinkedHashSet<>())
Raccogli in un Set<String> insensibile alle maiuscole / minuscole Set<String> Collectors.toCollection(() -> new TreeSet<>(String.CASE_INSENSITIVE_ORDER))
Raccogli a EnumSet<AnEnum> (miglior rendimento per enumerazioni) Collectors.toCollection(() -> EnumSet.noneOf(AnEnum.class))
Raccogli a una Map<K,V> con chiavi univoche Collectors.toMap(keyFunc,valFunc)
Mappare MyObject.getter () su MyObject univoco Collectors.toMap(MyObject::getter, Function.identity())
Mappare MyObject.getter () su più MyObjects Collectors.groupingBy(MyObject::getter)

Stream infiniti

È possibile generare un Stream che non termina. Chiamando un metodo terminale su un Stream infinito Stream il Stream entra in un ciclo infinito. Il metodo limit di un Stream può essere utilizzato per limitare il numero di termini del Stream che Java elabora.

Questo esempio genera un Stream di tutti i numeri naturali, a partire dal numero 1. Ogni successivo termine del Stream è uno più alto del precedente. Chiamando il metodo limite di questo Stream , vengono considerati e stampati solo i primi cinque termini del Stream .

// 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);

Produzione:

1
2
3
4
5


Un altro modo per generare un flusso infinito è utilizzare il metodo Stream.generate . Questo metodo richiede un lambda di tipo Supplier .

// 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);

Flussi di consumo

Un Stream sarà attraversato solo quando c'è un'operazione terminale , come count() , collect() o forEach() . In caso contrario, non verrà eseguita alcuna operazione sul Stream .

Nell'esempio seguente, nessuna operazione terminale viene aggiunta al Stream , quindi l'operazione filter() non verrà invocata e non verrà prodotto alcun output perché peek() NON è un'operazione terminale .

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

Vivi su Ideone

Questa è una sequenza Stream con un'operazione di terminale valida, quindi viene prodotta un'uscita.

Puoi anche usare forEach invece di peek :

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

Vivi su Ideone

Produzione:

2
4
6
8

Dopo aver eseguito l'operazione del terminale, il Stream viene consumato e non può essere riutilizzato.


Sebbene un determinato oggetto flusso non possa essere riutilizzato, è facile creare un Iterable riutilizzabile che deleghi ad una pipeline di flusso. Questo può essere utile per restituire una vista modificata di un set di dati live senza dover raccogliere i risultati in una struttura temporanea.

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);
}

Produzione:

foo
bar
foo
bar

Questo funziona perché Iterable dichiara un singolo metodo astratto Iterator<T> iterator() . Ciò lo rende efficacemente un'interfaccia funzionale, implementata da un lambda che crea un nuovo stream per ogni chiamata.


In generale, un Stream funziona come mostrato nell'immagine seguente:

Streaming

NOTA : i controlli degli argomenti vengono sempre eseguiti, anche senza un'operazione di terminale :

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()");
}

Vivi su Ideone

Produzione:

Abbiamo ottenuto una NullPointerException in quanto null è stato passato come argomento a filter ()

Creazione di una mappa di frequenza

Il collector groupingBy(classifier, downstream) consente la raccolta di elementi Stream in una Map classificando ciascun elemento in un gruppo ed eseguendo un'operazione downstream sugli elementi classificati nello stesso gruppo.

Un classico esempio di questo principio è l'utilizzo di una Map per contare le occorrenze di elementi in un Stream . In questo esempio, il classificatore è semplicemente la funzione di identità, che restituisce l'elemento così com'è. L'operazione downstream conta il numero di elementi uguali, usando il counting() .

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

L'operazione downstream è essa stessa un collector ( Collectors.counting() ) che opera su elementi di tipo String e produce un risultato di tipo Long . Il risultato della chiamata al metodo collect è una Map<String, Long> .

Questo produrrebbe il seguente risultato:

banane = 1
arancione = 1
mela = 2

Stream parallelo

Nota: prima di decidere quale Stream utilizzare, dare un'occhiata al comportamento di ParallelStream vs Sequential Stream .

Quando si desidera eseguire contemporaneamente operazioni di Stream , è possibile utilizzare uno di questi modi.

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

O:

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

Per eseguire le operazioni definite per lo streaming parallelo, chiamare un operatore di terminale:

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

(Un possibile) output dal Stream parallelo:

Tre
quattro
Uno
Due
Cinque

L'ordine potrebbe cambiare in quanto tutti gli elementi sono elaborati in parallelo (il che potrebbe renderlo più veloce). Usa parallelStream quando l'ordine non ha importanza.

Impatto sulle prestazioni

Nel caso in cui sia coinvolta la rete, i Stream paralleli possono peggiorare le prestazioni generali di un'applicazione in quanto tutti i Stream paralleli utilizzano un comune pool di thread fork-join per la rete.

D'altra parte, parallel Stream s può migliorare significativamente le prestazioni in molti altri casi, a seconda del numero di core disponibili nella CPU in esecuzione al momento.

Conversione di un flusso di facoltativo in un flusso di valori

Potrebbe essere necessario convertire un Stream emette Optional in un Stream di valori, emettendo solo valori da Optional esistente. (vale a dire: senza valore null e senza trattare 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]

Creazione di un flusso

Tutti i Collection<E> s di java Collection<E> hanno i metodi stream() e parallelStream() da cui è possibile costruire uno Stream<E> :

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

È possibile creare uno Stream<E> da una matrice utilizzando uno dei seguenti due metodi:

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

La differenza tra Arrays.stream() e Stream.of() è che Stream.of() ha un parametro varargs, quindi può essere usato come:

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

Ci sono anche i primitivi Stream che puoi usare. Per esempio:

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

Questi stream primitivi possono anche essere costruiti usando il metodo Arrays.stream() :

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

È possibile creare un Stream da una matrice con un intervallo specificato.

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

Si noti che qualsiasi flusso primitivo può essere convertito in stream di tipo in scatola usando il metodo in boxed :

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

Questo può essere utile in alcuni casi se si desidera raccogliere i dati poiché il flusso primitivo non ha alcun metodo di collect che accetta un Collector come argomento.

Riutilizzo delle operazioni intermedie di una catena di flusso

Lo streaming viene chiuso quando viene chiamata un'operazione terminale. Riutilizzare il flusso di operazioni intermedie, quando solo il funzionamento del terminale è solo variabile. potremmo creare un fornitore di stream per costruire un nuovo stream con tutte le operazioni intermedie già configurate.

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[] array int[] possono essere convertiti in List<Integer> utilizzando gli stream

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

Ricerca di statistiche sui flussi numerici

Java 8 fornisce classi chiamate IntSummaryStatistics , DoubleSummaryStatistics e LongSummaryStatistics che forniscono un oggetto di stato per la raccolta di statistiche quali count , min , max , sum e 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);

Quale risulterà in:

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

Ottieni una fetta di un flusso

Esempio: ottieni un Stream di 30 elementi, contenente elementi dal 21 ° al 50 ° (inclusi) di una raccolta.

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);

Gli appunti:

  • IllegalArgumentException viene generato se n è negativo o maxSize è negativo
  • sia skip(long) che limit(long) sono operazioni intermedie
  • se un flusso contiene meno di n elementi, skip(n) restituisce un flusso vuoto
  • sia skip(long) che limit(long) sono operazioni a basso costo su pipeline sequenziali, ma possono essere piuttosto costose su condotte parallele ordinate

Concatenare flussi

Dichiarazione variabile per esempi:

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

Esempio 1 - Concatena due Stream s

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

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

Esempio 2: concatena più di due 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

In alternativa, per semplificare il nidificata concat() Sintassi il Stream s può anche essere concatenato con 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

Fai attenzione quando costruisci Stream s dalla concatenazione ripetuta, perché l'accesso a un elemento di un Stream profondamente concatenato può portare a catene di chiamate profonde o persino a StackOverflowException .

IntStream a stringa

Java non ha un flusso di caratteri , quindi quando si lavora con String s e si costruisce un Stream di Character s, un'opzione è quella di ottenere un IntStream di punti di codice usando il metodo String.codePoints() . Quindi IntStream può essere ottenuto come di seguito:

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

È un po 'più complicato fare la conversione in un altro modo, cioè IntStreamToString. Questo può essere fatto come segue:

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

Ordina utilizzando 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);

Produzione:

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

È anche possibile utilizzare un meccanismo di confronto diverso in quanto esiste una versione sorted sovraccarico che accetta come argomento un comparatore.

Inoltre, puoi usare un'espressione lambda per l'ordinamento:

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

Ciò produrrebbe [Sydney, New York, Mumbai, London, California, Amsterdam]

Puoi usare Comparator.reverseOrder() per avere un comparatore che impone il reverse dell'ordine naturale.

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

Flussi di primitivi

Java fornisce Stream specializzati per tre tipi di primitive IntStream (per int s), LongStream (per long s) e DoubleStream (per double s). Oltre ad essere implementazioni ottimizzate per le rispettive primitive, forniscono anche diversi metodi terminali specifici, in genere per operazioni matematiche. Per esempio:

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

Raccogli i risultati di un flusso in una matrice

Analogico per ottenere una raccolta per uno Stream di collect() possibile ottenere un array con il metodo 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 è un tipo speciale di riferimento al metodo: un riferimento costruttore.

Trovare il primo elemento che corrisponde a un predicato

È possibile trovare il primo elemento di un Stream che corrisponde a una condizione.

Per questo esempio, troveremo il primo numero Integer cui quadrato è superiore a 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

Questa espressione restituirà un OptionalInt con il risultato.

Nota che con un Stream infinito, Java continuerà a controllare ogni elemento fino a quando non troverà un risultato. Con un Stream , se Java esaurisce gli elementi ma non riesce a trovare un risultato, restituisce un oggetto OptionalInt vuoto.

Utilizzo di IntStream per iterare su indici

Stream elementi di Stream di solito non consentono l'accesso al valore dell'indice dell'oggetto corrente. Per scorrere su un array o ArrayList mentre si ha accesso agli indici, utilizzare 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);

Il metodo range(start, endExclusive) restituisce un altro ÌntStream e mapToObj(mapper) restituisce un flusso di String .

Produzione:

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

Questo è molto simile all'uso di un normale for ciclo con un contatore, ma con il vantaggio di pipelining e parallelizzazione:

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

Flattenare i flussi con flatMap ()

Un Stream di elementi a sua volta scorrevoli può essere appiattito in un unico Stream continuo:

La matrice dell'elenco di elementi può essere convertita in una singola lista.

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]

La mappa contenente l'elenco di elementi come valori può essere appiattita in un elenco combinato

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 di Map può essere appiattito in un singolo Stream continuo

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]

Crea una mappa basata su un flusso

Caso semplice senza chiavi duplicate

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}

Per rendere le cose più dichiarative, possiamo usare il metodo statico in Function interface - Function.identity() . Possiamo sostituire questo element -> element lambda element -> element con Function.identity() .

Caso in cui potrebbero esserci chiavi duplicate

Il javadoc per gli stati Collectors.toMap :

Se le chiavi mappate contengono duplicati (in base a Object.equals(Object) ), viene generata una IllegalStateException quando viene eseguita l'operazione di raccolta. Se le chiavi mappate possono avere duplicati, utilizzare invece 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}

BinaryOperator passato a Collectors.toMap(...) genera il valore da memorizzare nel caso di una collisione. Può:

  • restituisce il vecchio valore, in modo che il primo valore nel flusso abbia la precedenza,
  • restituire il nuovo valore, in modo che l'ultimo valore nel flusso abbia la precedenza o
  • combinare i valori vecchi e nuovi

Raggruppamento per valore

È possibile utilizzare Collectors.groupingBy quando è necessario eseguire l'equivalente di un'operazione "raggruppa per" in cascata del database. Per illustrare, il seguente crea una mappa in cui i nomi delle persone sono mappati ai cognomi:

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]}

Vivi su Ideone

Generazione di stringhe casuali usando flussi

A volte è utile creare Strings casuali, magari come ID sessione per un servizio Web o una password iniziale dopo la registrazione per un'applicazione. Questo può essere facilmente ottenuto usando Stream s.

Per prima cosa dobbiamo inizializzare un generatore di numeri casuali. Per migliorare la sicurezza per le String generate, è una buona idea usare SecureRandom .

Nota : la creazione di un SecureRandom è piuttosto costosa, quindi è consigliabile farlo una volta sola e chiamare uno dei suoi metodi setSeed() di volta in volta per renderizzarlo.

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

Quando creiamo String casuali, di solito vogliamo che usino solo determinati caratteri (ad es. Solo lettere e cifre). Pertanto possiamo creare un metodo che restituisce un valore boolean che può essere successivamente utilizzato per filtrare il 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);
}

Quindi possiamo utilizzare l'RNG per generare una stringa casuale di lunghezza specifica contenente il set di caratteri che supera il nostro controllo 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;
}

Utilizzo degli stream per implementare funzioni matematiche

Stream IntStream , e in particolare quelli IntStream , rappresentano un modo elegante per implementare i termini di somma (Σ). Le gamme del Stream possono essere utilizzate come limiti della sommatoria.

Ad esempio, l'approssimazione di Pi di Madhava è data dalla formula (Fonte: wikipedia ): L'approssimazione di Madhava

Questo può essere calcolato con una precisione arbitraria. Ad esempio, per 101 termini:

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

Nota: con la precisione del double , selezionare un limite superiore di 29 è sufficiente per ottenere un risultato indistinguibile da Math.Pi

Utilizzo degli stream e dei riferimenti al metodo per scrivere processi di auto-documentazione

I riferimenti ai metodi costituiscono un codice di auto-documentazione eccellente e l'utilizzo dei riferimenti ai metodi con Stream s semplifica la lettura e la comprensione dei processi complicati. Considera il seguente codice:

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;
}

Quest'ultimo metodo riscritto usando i riferimenti di Stream s e method è molto più leggibile e ogni fase del processo è facilmente e facilmente comprensibile - non è solo più breve, ma mostra anche a colpo d'occhio quali interfacce e classi sono responsabili del codice in ogni passaggio:

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());
}    

Utilizzo degli stream di Map.Entry per preservare i valori iniziali dopo la mappatura

Quando hai uno Stream devi mappare ma vuoi preservare anche i valori iniziali, puoi mappare lo Stream su Map.Entry<K,V> usando un metodo di utilità come il seguente:

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

Quindi è possibile utilizzare il convertitore per elaborare i Stream che hanno accesso sia ai valori originali sia a quelli mappati:

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

È quindi possibile continuare a elaborare quel Stream come di consueto. Ciò evita il sovraccarico di creazione di una raccolta intermedia.

Categorie di operazioni di streaming

Le operazioni di streaming rientrano in due categorie principali, operazioni intermedie e terminali e due sottocategorie, stateless e stateful.


Operazioni intermedie:

Un'operazione intermedia è sempre pigra , ad esempio un semplice Stream.map . Non è invocato fino a quando il flusso non viene effettivamente consumato. Questo può essere verificato facilmente:

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

Le operazioni intermedie sono gli elementi costitutivi comuni di un flusso, concatenati dopo l'origine e sono generalmente seguiti da un'operazione terminale che attiva la catena di flusso.


Operazioni terminalistiche

Le operazioni terminal sono ciò che fa scattare il consumo di un flusso. Alcuni dei più comuni sono Stream.forEach o Stream.collect . Di solito sono posizionati dopo una catena di operazioni intermedie e sono quasi sempre desiderosi .


Operazioni senza stato

Apolidia significa che ogni oggetto viene elaborato senza il contesto di altri elementi. Le operazioni senza stato consentono l'elaborazione efficiente dei flussi di memoria. Le operazioni come Stream.map e Stream.filter che non richiedono informazioni su altri elementi dello stream sono considerate senza stato.


Operazioni stateful

Statefulness significa che l'operazione su ciascun elemento dipende da (alcuni) altri elementi dello stream. Ciò richiede che uno stato sia preservato. Le operazioni di stato possono interrompersi con flussi lunghi o infiniti. Operazioni come Stream.sorted richiedono che l'intero stream venga elaborato prima che venga emesso qualsiasi elemento che si romperà in un flusso di elementi abbastanza lungo. Questo può essere dimostrato da un lungo flusso ( eseguito a proprio rischio ):

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

Ciò causerà una memoria Stream.sorted a causa dello stato di Stream.sorted :

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

Conversione di un iteratore in un flusso

Utilizzare Spliterators.spliterator() o Spliterators.spliteratorUnknownSize() per convertire un iteratore in uno stream:

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

Riduzione con stream

La riduzione è il processo di applicazione di un operatore binario a ogni elemento di un flusso per ottenere un valore.

Il metodo sum() di un IntStream è un esempio di riduzione; applica l'aggiunta a ogni termine del flusso, determinando un valore finale: Riduzione di Somma

Questo è equivalente a (((1+2)+3)+4)

Il metodo di reduce di un flusso consente di creare una riduzione personalizzata. È possibile utilizzare il metodo reduce per implementare il metodo sum() :

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

La versione Optional viene restituita in modo che gli stream vuoti possano essere gestiti in modo appropriato.

Un altro esempio di riduzione è la combinazione di uno Stream<LinkedList<T>> in una singola 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;
});

Puoi anche fornire un elemento di identità . Ad esempio, l'elemento identità per addizione è 0, come x+0==x . Per la moltiplicazione, l'elemento identità è 1, come x*1==x . Nel caso precedente, l'elemento identity è una LinkedList<T> vuota LinkedList<T> , perché se aggiungi una lista vuota ad un'altra lista, la lista che stai "aggiungendo" non cambia:

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;
});

Si noti che quando viene fornito un elemento identità, il valore di ritorno non viene incapsulato in un Optional -if chiamato su un flusso vuoto, reduce() restituirà l'elemento identità.

Anche l'operatore binario deve essere associativo , ovvero (a+b)+c==a+(b+c) . Questo perché gli elementi possono essere ridotti in qualsiasi ordine. Ad esempio, la riduzione di cui sopra potrebbe essere eseguita in questo modo:

Altra riduzione della somma

Questa riduzione equivale a scrivere ((1+2)+(3+4)) . La proprietà di associatività consente inoltre a Java di ridurre il Stream in parallelo: una porzione del flusso può essere ridotta da ciascun processore, con una riduzione che combina il risultato di ciascun processore alla fine.

Unire uno stream a una singola stringa

Un caso d'uso che si incontra frequentemente, è la creazione di una String da uno stream, in cui gli elementi di flusso sono separati da un determinato carattere. Il metodo Collectors.joining() può essere utilizzato per questo, come nell'esempio seguente:

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);

Produzione:

MELA, BANANA, ARANCIA, PERA

Il metodo Collectors.joining() può anche gestire pre e postfix:

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

Produzione:

Frutta: MELA, ARANCIA, PERA.

Vivi su Ideone



Modified text is an extract of the original Stack Overflow Documentation
Autorizzato sotto CC BY-SA 3.0
Non affiliato con Stack Overflow