Stream

Progettazione e Sviluppo del Software

C.D.L. Tecnologie dei Sistemi Informatici

Gianluca Aguzzi — gianluca.aguzzi@unibo.it

Angelo Filaseta — angelo.filaseta@unibo.it

versione stampabile

Riconoscimenti

  • Questo materiale è ampiamente basato su quello realizzato dai Prof. Mirko Viroli e Roberto Casadei, che ringrazio.

  • Ogni errore riscontratovi è esclusiva responsabilità degli autori di questo documento.

Outline

Goal della lezione

  • Mostrare la gestione funzionale degli Stream
  • Discutere altri aspetti relativi alle novità di Java 8

Stream

Il concetto di Stream

Idee

  • Uno Stream rappresenta un flusso sequenziale (anche infinito) di dati omogenei, usabile una volta sola, e dal quale si vuole ottenere una informazione complessiva e/o aggregata
  • Assomiglia al concetto di Iteratore, ma lo Stream è più dichiarativo, perché non indica passo-passo come l’informazione viene processata, e quindi è concettualmente più astratto
  • Ove possibile, uno Stream manipola i suoi elementi in modo “lazy” (ritardato): i dati vengono processati mano a mano che servono, non sono memorizzati tutti assieme come nelle Collection
  • E’ possibile creare “catene” di trasformazioni di Stream (implementate con decorazioni successive) in modo funzionale, per ottenere flessibilmente computazioni non banali dei loro elementi, con codice più compatto e leggibile
  • Questa modalità di lavoro rende le computazioni (automaticamente) parallelizzabili, ossia computabili da un set arbitrario di Thread

Computazioni con gli Stream

Struttura a pipeline

  • Una sorgente o sink:
    • Una Collection/array, un dispositivo di I/O, una funzione generatrice
  • Una sequenza di trasformazioni:
    • mappe e filtri, ma non solo..
  • Un terminatore, che aggrega i dati dello Stream:
    • una riduzione ad un valore, una Collection/array, un Iteratore

Esempio: con persone con nome, città e reddito

  • Data una List<Persona> con proprietà reddito e città, ottenere la somma dei redditi di tutte le persone di Cesena
  • Come lo realizziamo tramite una pipeline?
    • Sorgente: la lista
    • Trasformazione 1: filtro sulle persone di Cesena
    • Trasformazione 2: si mappa ogni persona sul suo reddito
    • Terminazione: sommo
  • Aspetto cruciale: le fasi intermedie (dopo le trasformazioni), non generano collezioni temporanee

Classe Personequals, hashCode e toString omessi

public class Person {
	private final String name;
	private final Optional<String> city;
	private final double income;
	private final Set<String> jobs;
	
	public Person(String name, String city, double income, String... jobs) {
		this.name = Objects.requireNonNull(name);
		this.city = Optional.ofNullable(city); // null in ingresso indica città assente
		this.income = income;
		this.jobs = new HashSet<>(Arrays.asList(jobs)); // conversione a set
	}

	public String getName() {
		return this.name;
	}

	public Optional<String> getCity() {
		return this.city;
	}

	public double getIncome() {
		return this.income;
	}
	
	public Set<String> getJobs(){
		return Collections.unmodifiableSet(this.jobs);	// copia difensiva
	}
	
	//.. seguono hashCode, equals e toString

Realizzazione dell’esempio in Java 8

public class UseStreamsOnPerson {
	public static void main(String[] args) {
		final List<Person> list = new ArrayList<>();
		list.add(new Person("Mario","Cesena",20000,"Teacher"));
		list.add(new Person("Rino","Forlì",50000,"Professor"));
		list.add(new Person("Lino","Cesena",110000,"Professor","Dean"));
		list.add(new Person("Ugo","Cesena",20000,"Secretary"));
		list.add(new Person("Marco",null,4000,"Contractor"));
		
		final double result = list.stream()
								  .filter(p->p.getCity().isPresent())
								  .filter(p->p.getCity().get().equals("Cesena"))
								  .mapToDouble(Person::getIncome)
								  .sum();
	
		System.out.println(result);
		
		// alternativa con iteratore: qual è la più leggibile?
		double res2 = 0.0;
		for (final Person p: list){
			if (p.getCity().isPresent() && p.getCity().get().equals("Cesena")){
				System.out.println(p);
				res2 = res2 + p.getIncome();
			}
		}
		System.out.println(res2);
	}
}   

La libreria degli Stream

Struttura

  • Package java.util.stream: interfacce e classi per gli stream
  • Interfaccia Stream<X>: stream e metodi statici di “factory”
  • Interfaccia BaseStream<X,B>: sopra-interfaccia di Stream con i metodi base
  • Interfaccia DoubleStream: stream di double, con metodi base e specifici
  • Interfacce IntStream, LongStream: simili
  • Interfaccia Collector<T,A,R>: rappresenta una operazione di riduzione
  • Classe Collectors: fornisce una serie di collettori
  • ..altre classi di Java creano degli stream

Le collection generano Stream!

public interface Collection<E> extends Iterable<E> {

    ..

    Iterator<E> iterator();

    default boolean removeIf(Predicate<? super E> filter) {..}

    default Spliterator<E> spliterator() {..}
    
    default Stream<E> stream() {..}
    
    default Stream<E> parallelStream() {..}
}
 

L’interfaccia java.util.BaseStream

public interface BaseStream<T, S extends BaseStream<T, S>> extends ... {

    // Torna un iteratore sugli elementi rimasti dello stream, e lo chiude
    Iterator<T> iterator();

    // spliterator è un iteratore che supporta parallelismo..
    Spliterator<T> spliterator();
    
    // è uno stream gestibili in modalità parallela
    boolean isParallel();

    // torna una variante sequenziale dello stream
    S sequential();

    // torna una variante parallela dello stream
    S parallel();

    // torna una variante non ordinata dello stream
    S unordered();
  
    // associa un handler chiamato alla chiusura dello stream
    S onClose(Runnable closeHandler);

    void close();
}
 

Riassunto delle funzionalità di una pipeline per Stream<X>

Creazione

  • empty, of, iterate, generate, concat

Trasformazione

  • filter, map, flatMap, distinct, sorted, peek, limit, skip, mapToXYZ,..

Terminazione

  • forEach, forEachOrdered, toArray, reduce, collect, min, max, count, anyMatch, allMatch, noneMatch, findFirst, findAny,..

Una nota sulle classi DoubleStream e simili

  • sono più specializzate e performanti, non avendo il boxing
  • non hanno tutte le funzionalità di cui sopra, se vi servono vi dovete riportare ad un Stream<X> con un trasformatore mapToObj() o boxed()
  • ne hanno qualcuna in più specifica, ad esempio sum

java.util.Stream: costruzione stream, 1/3

public interface Stream<T> extends BaseStream<T, Stream<T>> {

    // Static factories

    public static<T> Stream<T> empty() {..}
    public static<T> Stream<T> of(T t) {..}
    public static<T> Stream<T> of(T... values) {..}
    public static<T> Stream<T> iterate(final T seed, final UnaryOperator<T> f) {..}
    public static<T> Stream<T> generate(Supplier<T> s) {..}
    public static <T> Stream<T> concat(Stream<? extends T> a, Stream<? extends T> b) {..}
    // also recall method Collection.stream() and Collection.parallelStream()
    
    public static<T> Builder<T> builder() {..}
    
    public interface Builder<T> extends Consumer<T> {

        void accept(T t);

        default Builder<T> add(T t) {
            accept(t);
            return this;
        }

        Stream<T> build();
    }

java.util.Stream: trasformazione stream, 2/3


    // Stream transformation
    
    Stream<T> filter(Predicate<? super T> predicate);
    <R> Stream<R> map(Function<? super T, ? extends R> mapper);
    <R> Stream<R> flatMap(Function<? super T, ? extends Stream<? extends R>> mapper);
    Stream<T> distinct();
    Stream<T> sorted();
    Stream<T> sorted(Comparator<? super T> comparator);
    Stream<T> peek(Consumer<? super T> action);
    Stream<T> limit(long maxSize);
    Stream<T> skip(long n);
    
    IntStream mapToInt(ToIntFunction<? super T> mapper);
    LongStream mapToLong(ToLongFunction<? super T> mapper);
    DoubleStream mapToDouble(ToDoubleFunction<? super T> mapper);
    IntStream flatMapToInt(Function<? super T, ? extends IntStream> mapper);
    LongStream flatMapToLong(Function<? super T, ? extends LongStream> mapper);
    DoubleStream flatMapToDouble(Function<? super T, ? extends DoubleStream> mapper);

java.util.Stream: terminazione stream, 3/3

    // Terminal Operations
    
    void forEach(Consumer<? super T> action);
    void forEachOrdered(Consumer<? super T> action);
    Object[] toArray();
    <A> A[] toArray(IntFunction<A[]> generator);
    T reduce(T identity, BinaryOperator<T> accumulator);
    Optional<T> reduce(BinaryOperator<T> accumulator);
    <U> U reduce(U identity, BiFunction<U, ? super T, U> accumulator, BinaryOperator<U> combiner);
    <R> R collect(Supplier<R> supplier, BiConsumer<R, ? super T> accumulator, BiConsumer<R, R> combiner);
    <R, A> R collect(Collector<? super T, A, R> collector);
    Optional<T> min(Comparator<? super T> comparator);
    Optional<T> max(Comparator<? super T> comparator);
    long count();
    boolean anyMatch(Predicate<? super T> predicate);
    boolean allMatch(Predicate<? super T> predicate);
    boolean noneMatch(Predicate<? super T> predicate);
    Optional<T> findFirst();
    Optional<T> findAny();

}
 

Trasformazioni di Stream: esempi

public class UseTransformations {
	public static void main(String[] args) {
		final List<Integer> li = List.of(10, 20, 30, 5, 6, 7, 10, 20, 100);

		System.out.print("All\t\t :");
		li.stream().forEach(i -> System.out.print(" " + i));

		System.out.print("\nFilter(>10)\t :");
		li.stream()
				.filter(i -> i > 10) // fa passare solo certi elementi
				.forEach(i -> System.out.print(" " + i));

		System.out.print("\nMap(N:i+1)\t :");
		li.stream()
				.map(i -> "N:" + (i + 1)) // trasforma ogni elemento
				.forEach(i -> System.out.print(" " + i));

		System.out.print("\nflatMap(i,i+1)\t :");
		li.stream()
				.flatMap(i -> List.of(i, i + 1).stream()) // trasforma e appiattisce
				.map(String::valueOf) // invece del forEach..
				.map(" "::concat)
				.forEach(System.out::print);
	}
}

Trasformazioni di Stream: esempi pt.2

public class UseTransformations2 {
	public static void main(String[] args) {
		final List<Integer> li = List.of(10,20,30,5,6,7,10,20,100);
		
		System.out.print("\nDistinct\t :");
		li.stream().distinct()	// elimina le ripetizioni
		  .forEach(i->System.out.print(" "+i));
		
		System.out.print("\nSorted(down)\t :");
		li.stream().sorted((i,j)->j-i) // ordina
		  .forEach(i->System.out.print(" "+i));
		
		System.out.print("\nPeek(.)\t\t :");
		li.stream().peek(i->System.out.print(".")) // esegue una azione per ognuno
		  .forEach(i->System.out.print(" "+i));
		
		System.out.print("\nLimit(5)\t :");
		li.stream().limit(5)  // solo i primi 5
		  .forEach(i->System.out.print(" "+i));
		
		System.out.print("\nSkip(5)\t\t :");
		li.stream().skip(5)   // salta i primi 5
		  .forEach(i->System.out.print(" "+i));
	}
}

Creazione di Stream: esempi

public class UseFactories {
	public static void main(String[] args) {
		final List<Integer> li = List.of(10,20,30,5,6,7,10,20,100);
		System.out.print("Collection: ");
		li.stream()
		  .forEach(i->System.out.print(" "+i));
		
		System.out.print("\nEmpty: ");
		Stream.empty()
		      .forEach(i->System.out.print(" "+i));
		
		System.out.print("\nFromValues: ");
		Stream.of("a","b","c")
		      .forEach(i->System.out.print(" "+i));
		
		System.out.print("\nIterate(+1): ");
		Stream.iterate(0,i->i+1)  // 0,1,2,3,...
		      .limit(20)
		      .forEach(i->System.out.print(" "+i));
	}
}

Creazione di Stream: esempi pt.2

public class UseFactories2 {
	public static void main(String[] args) {
		System.out.print("\nSuppl(random): ");
		Stream.generate(()->Math.random()) // rand,rand,rand,...
		      .limit(5)
		      .forEach(i->System.out.print(" "+i));
		//DoubleStream.generate(()->Math.random())... stream unboxed
		
		System.out.print("\nConcat: ");
		Stream.concat(Stream.of("a","b"),Stream.of(1,2))
		      .forEach(i->System.out.print(" "+i));
		
		System.out.print("\nBuilder: ");
		Stream.builder()
		      .add(1)
		      .add(2)
		      .build()
		      .forEach(i->System.out.print(" "+i));
		
		System.out.print("\nRange: ");
		IntStream.range(0,20) // 0,1,..,19
		         .forEach(i->System.out.print(" "+i));
	}
}

Creazione di Stream: file di testo e stringhe

public class UseOtherFactories {
	private final static String aDir = "/home/mirko/aula";
	private final static String aFile = "/home/mirko/aula/oop/17/Counter.java";
	
	public static void main(String[] args) throws Exception {
		final Path dirPath = FileSystems.getDefault().getPath(aDir);
		
		System.out.println("Found below "+aDir);
		Files.find(dirPath, 2, (a,b)->true).forEach(System.out::println);
	    
		System.out.println("List directory "+aDir);
		Files.list(dirPath).forEach(System.out::println);
	    
		final Path filePath = FileSystems.getDefault().getPath(aFile);
	    
		System.out.println("Contenuto of "+aFile);
		Files.lines(filePath).forEach(System.out::println);
	    
		System.out.println("Contenuto of "+aFile+" in altra codifica");
		Files.lines(filePath,StandardCharsets.ISO_8859_1).forEach(System.out::println);
	    
		// Si veda il sorgente di BufferedReader.lines() per capire come si realizza
		// uno stream a partire da un iteratore
		
		System.out.println("Stream da una stringa..");
		"Hellò!".chars().mapToObj(i->(char)i).forEach(System.out::println);
	}
}

Terminazioni ad-hoc di Stream: esempi

public class UseTerminations {
	public static void main(String[] args) {
		final List<Integer> li = List.of(10,20,30,5,6,7,10,20,100);
		System.out.print("ForEach:\t ");
		li.stream().forEach(i->System.out.print(" "+i));
		
		System.out.print("\nForEachOrdered: ");
		li.stream().forEachOrdered(i->System.out.print(" "+i));
		
		final Integer[] array = li.stream().toArray(i->new Integer[i]);
		System.out.println("\nToArray:\t "+Arrays.toString(array));
		
		//Integer sum = li.stream().reduce(0,(x,y)->x+y);
		final Integer sum = li.stream().reduce(0,Integer::sum);
		System.out.println("Sum:\t\t "+sum);
		
		//Optional<Integer> max = li.stream().max((x,y)->x-y);
		final Optional<Integer> max = li.stream().max(Integer::compare);
		System.out.println("Max:\t\t "+max);
				
		final long count = li.stream().count();
		System.out.println("Count:\t\t "+count);
		
		final boolean anyMatch = li.stream().anyMatch(x -> x==100);
		System.out.println("AnyMatch:\t "+anyMatch);
		
		final Optional<Integer> findAny = li.stream().findAny();
		System.out.println("FindAny:\t "+findAny);
	}
}

Terminazione generalizzata con Stream.collect

public class UseGeneralizedCollectors {
	public static void main(String[] args) {
		final List<Integer> li = List.of(10,20,30,5,6,7,10,20,100);
		
		// Uso collect a tre argomenti
		final Set<Integer> set = li.stream().collect(
				()->new HashSet<>(), 		// oggetto collettore
				(h,i)->h.add(i),			// aggiunta di un elemento
				(h,h2)->h.addAll(h2));		// concatenazione due collettori
		System.out.println("Set: "+set);    // un HashSet coi valori dello stream
		
		// Più frequente: uso collect passandogli un collettore general-purpose
		final Set<Integer> set2 = li.stream().collect(Collector.of(
				HashSet::new, 							// oggetto collettore
				HashSet::add,							// aggiunta di un elemento
				(h,h2)->{h.addAll(h2); return h;}));	// concatenazione due collettori
		System.out.println("Set: "+set2);
		
		// cosa fa questo collettore? (.. un po' complicato)
		final int res=li.stream().collect(Collector.of(
				()->Arrays.<Integer>asList(0), 				// oggetto collettore
				(l,i)->l.set(0,i+l.get(0)),					// aggiunta di un elemento
				(l,l2)->{l.set(0,l.get(0)+l2.get(0)); return l;}))	// concatenazione
				.get(0);									// estrazione risultato
		System.out.println("Res: "+res);		
	}
}

Collettori di libreria: la classe Collectors

class Collectors { // some methods, all public static and generic..
   
    Collector<T, ?, C> toCollection(Supplier<C> collectionFactory) 
    Collector<T, ?, List<T>> toList() 
    Collector<T, ?, Set<T>> toSet() 
    
    Collector<CharSequence, ?, String> joining(CharSequence delimiter,
                                               CharSequence prefix,
                                               CharSequence suffix) 
                                                             
    Collector<T, ?, Optional<T>>  minBy(Comparator<? super T> comparator) 
    Collector<T, ?, Optional<T>>  maxBy(Comparator<? super T> comparator) 
    
    Collector<T, ?, Integer> summingInt(ToIntFunction<? super T> mapper) 
    Collector<T, ?, Long> summingLong(ToLongFunction<? super T> mapper)     
    
    Collector<T, ?, Map<K, List<T>>> groupingBy(Function<? super T, ? extends K> classifier) 
    
    Collector<T, ?, Map<K, D>> groupingBy(Function<? super T, ? extends K> classifier,
                                          Collector<? super T, A, D> downstream) 
    
    Collector<T, ?, Map<K,U>> toMap(Function<? super T, ? extends K> keyMapper,
                                    Function<? super T, ? extends U> valueMapper) 

    Collector<T, ?, Map<K,U>> toMap(Function<? super T, ? extends K> keyMapper,
                                    Function<? super T, ? extends U> valueMapper,
                                    BinaryOperator<U> mergeFunction) 
                                       
    Collector<T, ?, DoubleSummaryStatistics> 
                              summarizingDouble(ToDoubleFunction<? super T> mapper) 
}
 

UseCollectors: collettori di base

import static java.util.stream.Collectors.*;

public class UseCollectors {
	public static void main(String[] args) {
		final List<Integer> li = List.of(10,20,30,5,6,7,10,20,100);
		// una List
		System.out.println(li.stream().collect(toList()));
		// un Set
		System.out.println(li.stream().collect(toSet())); 
		// un TreeSet
		// System.out.println(li.stream().collect(toCollection(TreeSet::new)));
		System.out.println(li.stream().collect(minBy(Integer::compare)));
		System.out.println(li.stream().collect(summingInt(Number::intValue)).toString());
		System.out.println(li.stream().map(i->i.toString())
		                              .collect(joining(",","(",")")));
		// (10,20,30,5,6,7,10,20,100)
	}
}

UseCollectors: collettori di base pt 2

import static java.util.stream.Collectors.*;

public class UseCollectors2 {
	public static void main(String[] args) {
		final List<Integer> li = List.of(10,20,30,5,6,7,10,20,100);
		
		final Map<Integer,List<Integer>> map = li.stream()
				                                 .collect(groupingBy(x -> x/10));
		System.out.println(map); // {0=[5,6,7], 1=[10,10], ..}
		
		final Map<Boolean,Optional<Integer>> map2 = li.stream()
							.collect(groupingBy(x->x%2==0,minBy(Integer::compare)));
		System.out.println(map2); // minimo dei pari e minimo dei dispari
		
		final Map<Integer,Integer> map3 = li.stream()
				                            .distinct()
				                            .collect(toMap(x->x,x->x+1));
		System.out.println(map3); // mappa ogni x in x+1
		
		final Map<Integer,Integer> map4 = li.stream().collect(toMap(x->x/10,x->x,(x,y)->x+y));
		System.out.println(map4); // somma degli elementi in ogni decina
		System.out.println(li.stream().collect(summarizingInt(Number::intValue)).toString());		
	}
}

Esempi avanzati su Person

	public static void main(String[] args) {
		final List<Person> list = new ArrayList<>();
		list.add(new Person("Mario","Cesena",20000,"Teacher"));
		list.add(new Person("Rino","Forlì",50000,"Professor"));
		list.add(new Person("Lino","Cesena",110000,"Professor","Dean","Secretary"));
		list.add(new Person("Ugo","Cesena",20000,"Secretary"));
		list.add(new Person("Marco",null,4000,"Contractor"));
		
		// Jobs of people from Cesena
		final String res = 
				list.stream()
				    .filter(p->p.getCity().filter(x->x.equals("Cesena")).isPresent())
					.flatMap(p->p.getJobs().stream())
					.distinct()
					.collect(Collectors.joining("|", "[[", "]]"));
		System.out.println(res);
		
		// Average income of professors
		final double avg = 
				list.stream()
				    .filter(p->p.getJobs().contains("Professor"))
					.mapToDouble(Person::getIncome)
					.average().getAsDouble();
		
		System.out.println(avg);
		System.out.println(
				list.stream()
				    .filter(p->p.getJobs().contains("Professor"))
				    .mapToDouble(Person::getIncome).average());
	}
}   

Algoritmi funzionali – cosa realizzano?

public class UseStreamsForAlgorithms {
	public static void main(String[] args) {
		System.out.println(
			LongStream.iterate(2, x->x+1)
				.filter((i)->LongStream.range(2, i/2+1).noneMatch(j -> i%j==0))
				.limit(1000)
				.mapToObj(String::valueOf)
				.collect(Collectors.joining(",","[","]")));
		
		final Random r = new Random();
		System.out.println(
			IntStream.range(0, 10000)
				.map(i->r.nextInt(6)+r.nextInt(6)+2)
				.boxed() // da int a Integer
				.collect(groupingBy(x->x, collectingAndThen(counting(), d->d/10000.0))));
		System.out.println(
			"Prova di testo: indovina cosa produce la seguente computazione......"
				.chars()
				.mapToObj(x->String.valueOf((char)x))
				.collect(groupingBy(x->x,counting()))
				.entrySet()
				.stream()
				.sorted((e1,e2)->-Long.compare(e1.getValue(),e2.getValue()))
				.limit(3)
				.map(String::valueOf)
				.collect(Collectors.joining(",","[","]")));
	}
}