Java 8 è uscito nella versione definitiva a Marzo, ne abbiamo già parlato ampiamente su CoseNonJaviste in in cui abbiamo visto soprattutto della teoria. E’ arrivato il momento di mettere in pratica i concetti che abbiamo visto con un esempio un po’ più complesso. In questo post descriveremo l’esempio e vedremo come filtrare e trasformare i dati in uno stream. Nel prossimo post parleremo dei method references e vedremo come sfruttarli per ordinare i dati di uno stream.
Iniziamo esponendo il problema che vogliamo risolvere utilizzando le nuove feature introdotte in Java 8. Abbiamo un file di testo con tutti i risultati della seria A di calcio formattato in questo modo:
Giornata Nº 1 Data Incontro Risultato 24/08/2013 Verona Milan 2 1 24/08/2013 Sampdoria Juventus 0 1 25/08/2013 Inter Genoa 2 0 25/08/2013 Torino Sassuolo 2 0 ....
Per ogni giornata c’è un titolo e una intestazione, fra una giornata e la successiva è presente una riga vuota. Le informazioni di ogni partita sono su una riga con i dati separati da tab. Partendo da questo file vogliamo calcolare la classifica del campionato, analizzando ogni partita e assegnando i punti per le vittorie e i pareggi.
Iniziamo subito vedendo il risultato finale in puro stile Java 8:
String path = getClass().getResource("results.txt").getPath(); Liststrings = Files.readAllLines(Paths.get(path)); strings.stream() .filter( not(String::isEmpty) .and(not(startsWith("Giornata"))) .and(not(startsWith("Data"))) ) .flatMap(TeamItem::createItems) .collect( groupingBy( TeamItem::getTeam, summingInt(TeamItem::getPoints) ) ) .entrySet().stream() .map(e -> new TeamItem(e.getKey(), e.getValue())) .sorted( comparingInt(TeamItem::getPoints).reversed() .thenComparing(comparing(TeamItem::getTeam)) ) .forEach(System.out::println);
Poche righe di codice ma molto dense di significato! Qualcuno probabilmente si starà chiedendo: in che linguaggio di programmazione è scritto? Tranquilli, è sempre codice Java, il System.out
scritto nell’ultima riga risulta familiare!
Filtro dei dati di uno stream in Java 8
Iniziamo a analizzare il codice appena visto, abbiamo una lista di stringhe (le righe del file da analizzare) e vogliamo togliere le righe vuote e quelle che non contengono i risultati delle partite. Per far questo possiamo creare lo stream a partire dalla nostra lista e applicare tre volte il metodo filter
che accetta come parametro una lambda expression (il Predicate
che sarà usato per capire se un elemento deve essere incluso nei risultati):
strings.stream() .filter(s -> !s.isEmpty()) .filter(s -> !s.startsWith("Giornata")) .filter(s -> !s.startsWith("Data"));
In questo modo filtriamo i dati togliendo le righe di testo che non ci interessano. Pur essendo già abbastanza breve questo blocco di codice può essere semplificato ancora, per questo definiamo un metodo statico di utilità che esegue la negazione di un oggetto Predicate
:
public staticPredicate not(Predicate t) { return t.negate(); }
L’interfaccia Predicate
è una interfaccia funzionale, contiene solo un metodo da implementare. Grazie ai aggiunti in Java 8 questa interfaccia contiene altri metodi già implementati, come ad esempio il metodo negate
che utilizziamo in questo esempio.
Riscriviamo il codice che abbiamo visto usando questo metodo:
strings.stream() .filter(not(s -> s.isEmpty())) .filter(not(s -> s.startsWith("Giornata"))) .filter(not(s -> s.startsWith("Data")));
Ok, non abbiamo fatto un gran che e probabilmente abbiamo complicato il codice! Ma adesso possiamo togliere la lambda expression che richiama il metodo isEmpty
e passare direttamente il riferimento al metodo:
strings.stream() .filter(not(String::isEmpty)) .filter(not(s -> s.startsWith("Giornata"))) .filter(not(s -> s.startsWith("Data")));
C’è un’altra cosa che possiamo migliorare in questo codice: è presente una piccola duplicazione nella seconda e terza chiamata al metodo filter. Per evitare questa duplicazione possiamo creare un metodo che data una stringa ritorna un Predicate
che controlla se una stringa inizia con quella passata:
public static PredicatestartsWith(String start) { return s -> s.startsWith(start); }
In pratica è una funzione che invece di eseguire un blocco di codice ritorna una funzione. Questi tipi di metodo vengono chiamati higher-order function, sono da sempre abbastanza comuni in Javascript e, grazie soprattutto all’introduzione delle lambda, diverranno molto utilizzati anche in Java. All’inizio può sembrare un po’ complesso, non è semplice capire che start
è un parametro del metodo statico e s
è il parametro della funzione ritornata dal metodo statico.
Usando questo metodo possiamo semplificare il nostro esempio
strings.stream() .filter(not(String::isEmpty)) .filter(not(startsWith("Giornata"))) .filter(not(startsWith("Data")));
A questo punto possiamo usare un altro metodo di default dell’interfaccia funzionale Predicate
per invocare una sola volta il metodo filter
sullo stream:
StreamstringsStream = strings.stream() .filter( not(String::isEmpty) .and(not(startsWith("Giornata"))) .and(not(startsWith("Data"))) )
Con poche righe di codice (e neanche un ciclo for) siamo riusciti a filtrare i dati per escludere le righe che non ci interessano. Siamo pronti per trasformare i dati in modo da poter calcolare la classifica.
Trasformazione dei dati: map, reduce e collect
A questo punto abbiamo una lista (o meglio, uno stream) di stringhe che contiene tutti i risultati della serie A. Vogliamo trasformarla in una lista di oggetti TeamItem
, questi oggetti contengono il riferimento al numero di punti che una squadra ha ottenuto in una partita e serviranno per calcolare la classifica. La classe TeamItem
non è niente di particolare, è un classico bean Java con i getter e l’implementazione del metodo toString
:
public class TeamItem { private String team; private int points; public TeamItem(String team, int points) { this.team = team; this.points = points; } public String getTeam() { return team; } public int getPoints() { return points; } @Override public String toString() { return points + "\t" + team; } }
Come abbiamo visto nel per cambiare il tipo degli oggetti possiamo utilizzare il metodo map
. Questo metodo permette di trasformare ad uno ad uno gli oggetti contenuti dello stream invocando la lambda expression passata come parametro. In questo caso però non vogliamo fare un mapping uno ad uno, infatti nel caso di pareggio dobbiamo assegnare un punto ad ogni squadra e quindi creare due oggetti di tipo TeamItem partendo da una sola stringa. Per questo possiamo utilizzare il metodo flatMap
che accetta una lambda expression che invece di ritornare un singolo oggetto ritorna a sua volta uno stream di oggetti. In pratica nel metodo flatMap
la lambda expression passata viene invocata per ogni oggetto dello stream di partenza, il risultato di queste invocazioni, ovvero uno stream di stream, viene appiattito per avere uno stream di oggetti.
Scriviamo anche un metodo statico che esegue il parsing di una stringa e, in base ai goal segnati dalle due squadre, ritorna uno stream di oggetti TeamItem
. In pratica in caso di vittoria vengono assegnati tre punti alla squadra vincente altrimenti c’è un pareggio e quindi ognuna delle due squadre ottiene un punto:
public static StreamcreateItems(String s) { String[] split = s.split("\t"); int goal1 = Integer.parseInt(split[3]); int goal2 = Integer.parseInt(split[4]); if (goal1 > goal2) { return Stream.of(new TeamItem(split[1], 3)); } else if (goal1 == goal2) { return Stream.of( new TeamItem(split[1], 1), new TeamItem(split[2], 1) ); } else { return Stream.of(new TeamItem(split[2], 3)); } }
A questo punto possiamo invocare il metodo flatMap
passando il riferimento al metodo statico appena scritto:
StreamteamItemStream = stringsStream.flatMap(TeamItem::createItems);
Siamo arrivati ad avere una lista di punteggi ottenuti dalle squadre nelle varie partite, a questo punto dobbiamo fare la somma per ottenere la classifica. Per prima cosa dobbiamo raggruppare i dati per squadra, possiamo farlo in modo semplice con il metodo collect
passando un oggetto di tipo Collector
:
Map> map = teamItemStream.collect(groupingBy(TeamItem::getTeam));
In questo caso per avere il parametro da passare richiamiamo il metodo statico groupingBy
della classe Collectors
. Questa classe contiene vari metodi statici per creare gli oggetti Collector
più comunemente usati. Con la riga di codice appena vista creiamo una mappa di oggetti in cui raggruppiamo i dati per squadra.
A questo punto abbiamo i dati raggruppati, dobbiamo sommare i punti e creare una lista di oggetti TeamItem
. Per questo possiamo creare uno stream delle entry presenti nella mappa e trasformare le varie entry in un oggetto TeamItem
. Come già detto quando vogliamo trasformare oggetti in uno stream possiamo usare il metodo map
passando una lambda expression:
StreamitemsStream = map.entrySet().stream().map( e -> { TeamItem identity = new TeamItem(e.getKey(), 0); Stream stream = e.getValue().stream(); return stream.reduce(identity, (t1, t2) -> new TeamItem(t1.getTeam(), t1.getPoints() + t2.getPoints()) ); } );
Nella lambda expression vogliamo creare un oggetto TeamItem
partendo da una lista di oggetti TeamItem
. Questa operazione viene comunemente chiamata reduce, per questo gli stream di Java 8 contengono il metodo reduce
. In pratica in questo metodo vengono processati i dati a due a due, da ogni coppia viene creato un nuovo oggetto contenente un risultato parziale dell’operazione (nel nostro caso la somma dei punti). In questo caso passiamo anche un oggetto identity
che viene utilizzato per partire nella computazione e per essere sicuri di avere un risultato anche con uno stream vuoto. Esiste anche una versione del metodo reduce che non ha fra i parametri l’oggetto identity
, in questo caso viene ritornato un .
I metodi map
e reduce
sono molto spesso utilizzati insieme per eseguire un calcolo, per questo si parla spesso di MapReduce. Il concetto di MapReduce è alla base di varie tecnologie, da hadoop a mongodb a molti altri sistemi distribuiti. Si adatta bene ad essere utilizzato in sistemi distribuiti in quanto le varie operazioni di map e reduce possono essere facilmente parallelizzate anche su più server in contemporanea.
Per questo esempio possiamo utilizzare anche un altro approccio, possiamo trasformare il nostro stream di oggetti in un IntStream
usando il metodo mapToInt
. Questo metodo trasforma oggetti in interi (il tipo primitivo int) applicando la lambda expression passata di tipo ToIntFunction
:
StreamitemsStream = map.entrySet().stream().map( e -> new TeamItem( e.getKey(), e.getValue().stream().mapToInt(TeamItem::getPoints).sum() ) );
La classe IntStream
contiene il metodo sum
che esegue una operazione di reduce per ottenere la somma dei valori dello stream. In questo modo non dobbiamo preoccuparci di passare una lambda expression, pensa a tutto lo stream di interi.
Un approccio ancora diverso consiste nell’eseguire il raggruppamento e la somma contemporaneamente. Per questo possiamo usare sempre il metodo groupingBy
della classe Collectors
ma nella versione con due parametri. Il secondo parametro è un oggetto Collector
che sarà utilizzato per calcolare il valore da inserire nella mappa. Nell’esempio precedente non avendo passato niente viene utilizzato il metodo toList
che accumula i vari elementi in una lista di oggetti. Volendo eseguire direttamente la somma usiamo il metodo summingInt
sempre della classe Collectors
:
Mapmap = teamItemStream.collect( groupingBy( TeamItem::getTeam, summingInt(TeamItem::getPoints) ) );
A questo punto il grosso del lavoro è già fatto, dobbiamo solo creare una lista di oggetti a partire dalla mappa. Anche in questo caso usiamo il metodo map
passando una lambda expression:
StreamitemsStream = map.entrySet().stream().map(e -> new TeamItem(e.getKey(), e.getValue()));
Conclusioni
Siamo arrivati a un buon punto, abbiamo la classifica con tutte le squadre e i relativi punti ma non sono ordinati. L’ordinamento di uno stream sarà proprio l’argomento del prossimo post, ovviamente utilizzeremo un oggetto Comparator
ma vedremo che, anche in questo caso, ci sono diverse novità di Java 8 che possono darci una mano. Alla prossima!