1. Elenco dei metodi della Streamclasse

La Streamclasse è stata creata per semplificare la costruzione di catene di flussi di dati. Per ottenere ciò, la Stream<T>classe dispone di metodi che restituiscono nuovi Streamoggetti.

Ciascuno di questi flussi di dati esegue una semplice azione, ma se li combini in catene e aggiungi interessanti funzioni lambda , allora hai un potente meccanismo per generare l'output che desideri. Presto lo vedrai di persona.

Ecco i metodi della Streamclasse (solo i più basilari):

Metodi Descrizione
Stream<T> of()
Crea un flusso da un insieme di oggetti
Stream<T> generate()
Genera un flusso in base alla regola specificata
Stream<T> concat()
Concatena due flussi
Stream<T> filter()
Filtra i dati, trasmettendo solo i dati che corrispondono alla regola specificata
Stream<T> distinct()
Rimuove i duplicati. Non passa i dati che sono già stati rilevati
Stream<T> sorted()
Ordina i dati
Stream<T> peek()
Esegue un'azione su ogni elemento nel flusso
Stream<T> limit(n)
Restituisce un flusso troncato in modo che non superi il limite specificato
Stream<T> skip(n)
Salta i primi n elementi
Stream<R> map()
Converte i dati da un tipo all'altro
Stream<R> flatMap()
Converte i dati da un tipo all'altro
boolean anyMatch()
Verifica se nel flusso è presente almeno un elemento che corrisponde alla regola specificata
boolean allMatch()
Controlla se tutti gli elementi nel flusso corrispondono alla regola specificata
boolean noneMatch()
Controlla se nessuno degli elementi nel flusso corrisponde alla regola specificata
Optional<T> findFirst()
Restituisce il primo elemento trovato che corrisponde alla regola
Optional<T> findAny()
Restituisce qualsiasi elemento nel flusso che corrisponde alla regola
Optional<T> min()
Cerca l'elemento minimo nel flusso di dati
Optional<T> max()
Restituisce l'elemento massimo nel flusso di dati
long count()
Restituisce il numero di elementi nel flusso di dati
R collect()
Legge tutti i dati dal flusso e li restituisce come raccolta

2. Operazioni intermedie e terminali della Streamclasse

Come puoi vedere, non tutti i metodi nella tabella sopra restituiscono un file Stream. Ciò è legato al fatto che i metodi della Streamclasse possono essere suddivisi in metodi intermedi (noti anche come non terminali ) e metodi terminali .

Metodi intermedi

I metodi intermedi restituiscono un oggetto che implementa l' Streaminterfaccia e possono essere concatenati insieme.

Metodi terminali

I metodi terminali restituiscono un valore diverso da Stream.

Pipeline di chiamata al metodo

Pertanto, è possibile creare una pipeline di flusso costituita da un numero qualsiasi di metodi intermedi e una singola chiamata di metodo terminale alla fine. Questo approccio consente di implementare una logica piuttosto complessa, aumentando al contempo la leggibilità del codice.

Pipeline di chiamata al metodo

I dati all'interno di un flusso di dati non cambiano affatto. Una catena di metodi intermedi è un modo semplice (dichiarativo) per specificare una pipeline di elaborazione dati che verrà eseguita dopo la chiamata al metodo terminale.

In altre parole, se il metodo terminale non viene chiamato, i dati nel flusso di dati non vengono elaborati in alcun modo. Solo dopo che il metodo terminale è stato chiamato, i dati iniziano a essere elaborati in base alle regole specificate nella pipeline del flusso.

stream()
  .intemediateOperation1()
  .intemediateOperation2()
  ...
  .intemediateOperationN()
  .terminalOperation();
Aspetto generale di una pipeline

Confronto tra metodi intermedi e terminali:

intermedio terminale
Tipo di ritorno Stream non unStream
Può essere combinato con più metodi dello stesso tipo per formare una pipeline NO
Numero di metodi in una singola pipeline Qualunque non più di uno
Produce il risultato finale NO
Avvia l'elaborazione dei dati nel flusso NO

Diamo un'occhiata a un esempio.

Supponiamo di avere un club per gli amanti degli animali. Domani il club celebra il Ginger Cat Day. Il club ha proprietari di animali domestici, ognuno dei quali ha un elenco di animali domestici. Non sono limitati ai gatti.

Compito: devi identificare tutti i nomi di tutti i gatti rossi per creare per loro biglietti di auguri personalizzati per la "vacanza professionale" di domani. I biglietti di auguri dovrebbero essere ordinati per età del gatto, dal più vecchio al più giovane.

Innanzitutto, forniamo alcune classi per aiutare a risolvere questo compito:

public enum Color {
   WHITE,
   BLACK,
   DARK_GREY,
   LIGHT_GREY,
   FOXY,
   GREEN,
   YELLOW,
   BLUE,
   MAGENTA
}
public abstract class Animal {
   private String name;
   private Color color;
   private int age;

   public Animal(String name, Color color, int age) {
      this.name = name;
      this.color = color;
      this.age = age;
   }

   public String getName() {
      return name;
   }

   public Color getColor() {
      return color;
   }

   public int getAge() {
      return age;
   }
}
public class Cat extends Animal {
   public Cat(String name, Color color, int age) {
      super(name, color, age);
   }
}
public class Dog extends Animal {
   public Dog(String name, Color color, int age) {
      super(name, color, age);
   }
}
public class Parrot extends Animal {
   public Parrot(String name, Color color, int age) {
      super(name, color, age);
   }
}
public class Pig extends Animal {
   public Pig(String name, Color color, int age) {
      super(name, color, age);
   }
}
public class Snake extends Animal {
   public Snake(String name, Color color, int age) {
      super(name, color, age);
   }
}
public class Owner {
   private String name;
   private List<Animal> pets = new ArrayList<>();

   public Owner(String name) {
      this.name = name;
   }

   public List<Animal> getPets() {
      return pets;
   }
}

Ora diamo un'occhiata alla Selectorclasse, dove verrà effettuata la selezione in base ai criteri specificati:

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;

public class Selector {
   private static List<Owner> owners;

   private static void initData() {
      final Owner owner1 = new Owner("Ronan Turner");
      owner1.getPets().addAll(List.of(
            new Cat("Baron", Color.BLACK, 3),
            new Cat("Sultan", Color.DARK_GREY, 4),
            new Dog("Elsa", Color.WHITE, 0)
      ));

      final Owner owner2 = new Owner("Scarlet Murray");
      owner2.getPets().addAll(List.of(
            new Cat("Ginger", Color.FOXY, 7),
            new Cat("Oscar", Color.FOXY, 5),
            new Parrot("Admiral", Color.BLUE, 3)
      ));

      final Owner owner3 = new Owner("Felicity Mason");
      owner3.getPets().addAll(List.of(
            new Dog("Arnold", Color.FOXY, 3),
            new Pig("Vacuum Cleaner", Color.LIGHT_GREY, 8)
      ));

      final Owner owner4 = new Owner("Mitchell Stone");
      owner4.getPets().addAll(List.of(
            new Snake("Mr. Boa", Color.DARK_GREY, 2)
      ));

      final Owner owner5 = new Owner("Jonathan Snyder");
      owner5.getPets().addAll(List.of(
            new Cat("Fisher", Color.BLACK, 16),
            new Cat("Zorro", Color.FOXY, 14),
            new Cat("Margo", Color.WHITE, 3),
            new Cat("Brawler", Color.DARK_GREY, 1)
      ));

      owners = List.of(owner1, owner2, owner3, owner4, owner5);
   }
}

Resta da aggiungere codice al mainmetodo. Attualmente, chiamiamo prima il initData()metodo, che popola l'elenco dei proprietari di animali domestici nel club. Quindi selezioniamo i nomi dei gatti rossi ordinati per età in ordine decrescente.

Innanzitutto, diamo un'occhiata al codice che non utilizza i flussi per risolvere questo compito:

public static void main(String[] args) {
   initData();

   List<String> findNames = new ArrayList<>();
   List<Cat> findCats = new ArrayList<>();
   for (Owner owner : owners) {
      for (Animal pet : owner.getPets()) {
         if (Cat.class.equals(pet.getClass()) && Color.FOXY == pet.getColor()) {
            findCats.add((Cat) pet);
         }
      }
   }

   Collections.sort(findCats, new Comparator<Cat>() {
      public int compare(Cat o1, Cat o2) {
         return o2.getAge() - o1.getAge();
      }
   });

   for (Cat cat : findCats) {
      findNames.add(cat.getName());
   }

   findNames.forEach(System.out::println);
}

Ora diamo un'occhiata a un'alternativa:

public static void main(String[] args) {
   initData();

   final List<String> findNames = owners.stream()
           .flatMap(owner -> owner.getPets().stream())
           .filter(pet -> Cat.class.equals(pet.getClass()))
           .filter(cat -> Color.FOXY == cat.getColor())
           .sorted((o1, o2) -> o2.getAge() - o1.getAge())
           .map(Animal::getName)
           .collect(Collectors.toList());

   findNames.forEach(System.out::println);
}

Come puoi vedere, il codice è molto più compatto. Inoltre, ogni riga della pipeline del flusso è una singola azione, quindi possono essere lette come frasi in inglese:

.flatMap(owner -> owner.getPets().stream())
Passa da a Stream<Owner>ad aStream<Pet>
.filter(pet -> Cat.class.equals(pet.getClass()))
Mantieni solo i gatti nel flusso di dati
.filter(cat -> Color.FOXY == cat.getColor())
Mantieni solo i gatti rossi nel flusso di dati
.sorted((o1, o2) -> o2.getAge() - o1.getAge())
Ordina per età in ordine decrescente
.map(Animal::getName)
Ottieni i nomi
.collect(Collectors.toList())
Inserisci il risultato in un elenco

3. Creazione di flussi

La Streamclasse ha tre metodi che non abbiamo ancora trattato. Lo scopo di questi tre metodi è creare nuovi thread.

Stream<T>.of(T obj)metodo

Il of()metodo crea un flusso costituito da un singolo elemento. Questo di solito è necessario quando, diciamo, una funzione prende un Stream<T>oggetto come argomento, ma tu hai solo un Toggetto. Quindi puoi facilmente e semplicemente utilizzare il of()metodo per ottenere un flusso costituito da un singolo elemento .

Esempio:

Stream<Integer> stream = Stream.of(1);

Stream<T> Stream.of(T obj1, T obj2, T obj3, ...)metodo

Il of()metodo crea un flusso costituito da elementi passati . È consentito qualsiasi numero di elementi. Esempio:

Stream<Integer> stream = Stream.of(1, 2, 3, 4, 5);

Stream<T> Stream.generate(Supplier<T> obj)metodo

Il generate()metodo consente di impostare una regola che verrà utilizzata per generare l'elemento successivo del flusso quando viene richiesto. Ad esempio, puoi dare un numero casuale ogni volta.

Esempio:

Stream<Double> s = Stream.generate(Math::random);

Stream<T> Stream.concat(Stream<T> a, Stream<T> b)metodo

Il concat()metodo concatena i due flussi passati in uno . Quando i dati vengono letti, vengono letti prima dal primo flusso e poi dal secondo. Esempio:

Stream<Integer> stream1 = Stream.of(1, 2, 3, 4, 5);
Stream<Integer> stream2 = Stream.of(10, 11, 12, 13, 14);
Stream<Integer> result = Stream.concat(stream1, stream2);

4. Filtraggio dei dati

Altri 6 metodi creano nuovi flussi di dati, consentendo di combinare i flussi in catene (o pipeline) di varia complessità.

Stream<T> filter(Predicate<T>)metodo

Questo metodo restituisce un nuovo flusso di dati che filtra il flusso di dati di origine in base alla regola passata . Il metodo deve essere chiamato su un oggetto il cui tipo è Stream<T>.

È possibile specificare la regola di filtro utilizzando una funzione lambda , che il compilatore convertirà quindi in un Predicate<T>oggetto.

Esempi:

Flussi incatenati Spiegazione
Stream<Integer> stream = Stream.of(1, 2, 3, 4, 5);
Stream<Integer> stream2 = stream.filter(x -> (x < 3));

Conserva solo i numeri inferiori a tre
Stream<Integer> stream = Stream.of(1, -2, 3, -4, 5);
Stream<Integer> stream2 = stream.filter(x -> (x > 0));

Conserva solo i numeri maggiori di zero

Stream<T> sorted(Comparator<T>)metodo

Questo metodo restituisce un nuovo flusso di dati che ordina i dati nel flusso di origine . Passi in un comparator , che imposta le regole per confrontare due elementi del flusso di dati.

Stream<T> distinct()metodo

Questo metodo restituisce un nuovo flusso di dati che contiene solo gli elementi univoci nel flusso di dati di origine . Tutti i dati duplicati vengono eliminati. Esempio:

Stream<Integer> stream = Stream.of(1, 2, 3, 4, 5, 2, 2, 2, 3, 4);
Stream<Integer> stream2 = stream.distinct(); // 1, 2, 3, 4, 5

Stream<T> peek(Consumer<T>)metodo

Questo metodo restituisce un nuovo flusso di dati , sebbene i dati in esso contenuti siano gli stessi del flusso di origine. Ma quando l'elemento successivo viene richiesto dallo stream, la funzione che hai passato al peek()metodo viene chiamata con esso.

Se passi la funzione System.out::printlnal peek()metodo, tutti gli oggetti verranno visualizzati quando passeranno attraverso il flusso.

Stream<T> limit(int n)metodo

Questo metodo restituisce un nuovo flusso di dati che contiene solo i primi nelementi nel flusso di dati di origine . Tutti gli altri dati vengono eliminati. Esempio:

Stream<Integer> stream = Stream.of(1, 2, 3, 4, 5, 2, 2, 2, 3, 4);
Stream<Integer> stream2 = stream.limit(3); // 1, 2, 3

Stream<T> skip(int n)metodo

Questo metodo restituisce un nuovo flusso di dati che contiene tutti gli stessi elementi del flusso di origine , ma salta (ignora) i primi nelementi. Esempio:

Stream<Integer> stream = Stream.of(1, 2, 3, 4, 5, 2, 2, 2, 3, 4);
Stream<Integer> stream2 = stream.skip(3); // 4, 5, 2, 2, 2, 3, 4