[x^2 for x in lst]

Nyheter i Java 8 - del 3 - metoder i Stream, metod- och konstruktorreferenser

2014-08-20

Det här är tredje delen i den lilla miniserien om nyheterna i Java 8. Tidigare delar har handlat om ändringarna som gjorts på interface (del 1) och de den nya funktionaliteten med streams och lambda expressions (del 2).

Den här delen kommer att behandla metoder som manipulerar strömmar på olika sätt samt beskriva hur vi kan transformera tillbaka en ström till en kollektion. Vissa av dessa saker har varit med som hastigast i kodexempel i del 2, men kommer nu att undersökas lite närmare.

Vi kommer även att ta en titt på några språkkonstruktioner som lagts till i Java 8. Dessa heter metodreferenser och konstruktorreferenser och i vissa fall kan ersätta lambdauutryck och på så sätt göra koden ytterligare lite kortare och mer lättläst.

Metoder för manipulering av strömmar

Det finns många metoder i Java 8-API:t för att manipulera strömmar. Det här avsnittet kommer att behandla några av de, enligt mitt tyckte, viktigaste. Alla metoderna som gås igenom nedan finns i klassen java.util.stream.Stream. API-dokumentationen för Stream finns här.

forEach()

Metoden forEach(Consumer<? super T> action) loopar, som namnet antyder, igenom elementen som finns i strömmen. För varje element anropas accept() på den Consumer som har skickats med till forEach().

Eftersom forEach() inte returnerar en Stream utan istället void, är forEach() en terminal operation.

I del 2 såg vi ett exempel på hur forEach() kunde användas för att skriva ut varje element i en collection på standard out. Eftersom vi inte gått igenom lambda expressions än vid det tillfället användes en anonym inre klass för att implementera Consumern. Koden såg så ut så här:

List<String> nobelPrizeWinners = Arrays.asList("Einstein", "Feynman", "Bohr");
 Consumer<String> nobelPrizeConsumer = new Consumer<String>() {
      public void accept(String nobelPrizeWinner) {
        System.out.println(nobelPrizeWinner);
      }
    };
    nobelPrizeWinners.stream().forEach(nobelPrizeConsumer);

Använder vi oss istället av lambdauttryck kan koden förkortas till följande:

nobelPrizeWinners.stream().forEach(nobelPrizeWinner -> System.out.println(nobelPrizeWinner));

Rätt mycket kortare och mer lättläst eller hur?

Koden ovan kan faktiskt förkortas ytterligare lite med hjälp av så kallade metodreferenser. Mer om sådana finns att läsa lite längre ner i texten .

filter()

filter(Predicate<? super T> predicate) är en intermediate operation som endast låter de element i strömmen som uppfyller det angivna predikatet vara kvar i pipeline:n.

I vårt exempel för interfacet Predicate i del 2 så hade vi ett exempel som filtrerade fram de nobelpristagare i vår exempellista som började på bokstaven B. Med lambdauttryck kan ett exempel liknande detta skrivas som följer.

nobelPrizeWinners.stream().filter(name -> name.startsWith("B")).forEach(name -> System.out.println(name));

I exemplet har vi som ni ser använt forEach() för att skriva ut namnen på nobelpristagarna på konsolen.

map()

Metoden map(Function<? super T,? extends R> mapper) transformerar strömmen till en annan ström innehållande andra element. Sättet den gör detta på är att den applicerar den angivna Function:en på elementen i tur och ordning, och skapar en ny ström med elementen som returneras. Eftersom dessa element kommer att vara av typen som anges av typparametern R till Function:en, kommer även den resulterande strömmen att ha denna typ.

Eftersom map() returnerar en Stream så är den en intermediate operation.

I exemplet för Function i del 2 transformerade vi nobelpristagarnas namn till versaler. Vi kan med lambdauttryck skriva ett liknande exempel:

nobelPrizeWinners.stream().map(name -> name.toUpperCase()).forEach(name -> System.out.println(name));

mapToDouble(), mapToInt(), mapToLong()

Det finns i Stream tre varianter av map() som heter mapToDouble(), mapToInt() och mapToLong(). Poängen med dessa är att de istället för att returnera en vanlig Stream returnerar en DoubleStream, en IntStream eller en LongStream beroende på vilken av de tre metoderna som det gäller. Dessa tre klasser är utökningar av Stream för att specifikt hantera double:s, int:s respektive long:s och gör det lättare att jobba med strömmar för dessa typer. Exempelvis så tar forEach() i IntStream en IntConsumer som parameter och , max() returnerar en IntOptional och så vidare. Detta gör att man får lite mer explicit typning. Inte världens största grej kanske, men kan man vara lite extra tydlig är det väl bra om man är det. Det finns även några trevliga metoder i dessa varianter av Stream som inte finns i den vanliga klassen. Mer om dessa metoder finns i detta avsnitt av inlägget.

Typen på returvärdet från Function:en som skickas med till mapToDouble(), mapToInt() respektive mapToLong() måste vara naturligtvis vara korrekt, dvs double, int respektive long.

Som ett exempel på en av dessa metoder kan vi skriva en liten kodsnutt som skriver ut längden på nobelpristagarnas namn. Den blir som följer:

nobelPrizeWinners.stream().mapToInt(name -> name.length()).forEach(len -> System.out.println(len));

findFirst()

findFirst() är en metod som kan användas för att undersöka det första elementet i en ström. Den returnerar en Optional som talar om i fall det finns något första element i strömmen eller inte. Ett vanligt användsningsområde för findFirst() är att använda den i kombination med filter(). Först används filter() för att filtrera fram de element i en ström som uppfyller något visst kriteria, sedan appliceras findFirst() för att undersöka om det fanns några sådana element överhuvudtaget.

Exempel: Finns det några nobelpristagare som börjar på bokstaven F?

nobelPrizeWinners.stream().filter(name -> name.startsWith("F")).findFirst().ifPresent(name -> System.out.println(name + " börjar på F."));
Utskriften när programmet körs blir:
Feynman börjar på F.

Exempel: Finns det några nobelpristagare som börjar på bokstaven C?

String msg = nobelPrizeWinners.stream().filter(name -> name.startsWith("C")).findFirst().orElse("Ingen nobelpristagare börjar på C.");
System.out.println(msg);
Kör man programmet blir utskriften:
Ingen nobelpristagare börjar på C.

count()

count() räknar, inte helt otippat, antalet element i strömmen och returnerar det som en long.

Exempel:

long noElements = nobelPrizeWinners.stream().count();
System.out.println("Antal nobelpristagare: " + noElements); 
Utskriften blir:
Antal nobelpristagare: 3

reduce()

reduce(BinaryOperator<T> accumulator) är en metod som jämför elementen i strömmen två och två med varandra och vid varje jämförelse väljer ut ett av elementen. Resultatetvärdet av en exekvering av reduce() blir alltså ett och endast ett av elementen i strömmen. Enda undantaget är om strömmen är tom, då det ju inte finns något element att returnera. För att ta hand om detta fall så har reduce() resultattypen Optional. Är strömmen tom kommer det returnerade Optional-objektet vara tomt (isPresent() returnerar false), i alla andra fall kan det resulterade elementet hämtas ut med get(). Vill man försäkra sig om att resultatet av reduce() aldrig skall vara tomt, finns det en överlagrad variant som tar in ett basvärde med vilket den första jämförelsen görs. Är strömmen tom kommer detta basvärde att returneras. Signaturen för den här varianten är T reduce(T, BinaryOperator<T>).

Hur reduce() skall jämföra elementen med varann specificeras genom den BinaryOperator som skickas med som argument till metoden. BinaryOperator är ett funktionellt interface som innehåller den abstrakta metoden T apply(T, T), vilken alltså tar in två parametrar av typen T och returnerar ett värde av samma typ. Det returnerade värdet behöver inte vara något av argumentvärdena, utan kan vara givetvis vara vilket värde som helst av typen T.

Som ett exempel på reduce() så kan vi skriva en liten kodsnutt som skriver ut namnet på den nobelpristagare som har det längsta namnet. Ett lamdauttryck för en implementation av BinaryOperator som jämför två strängar och väljer ut den längsta kan skrivas så här:

(name1, name2) -> name1.length() >= name2.length() ? name1 : name2
Vi kan med det uttrycket använda reduce() för att få fram och skriva ut det längsta namnet på följande sätt:
nobelPrizeWinners.stream().reduce((name1, name2) -> name1.length() >= name2.length() ? name1 : name2).ifPresent(name -> System.out.println(name + " har det längsta namnet."));
Resultatet blir som följer:
Einstein har det längsta namnet.

sorted()

sorted() är en metod som returnerar en ny ström med elementen i den ursprungliga strömmen sorterade i någon viss ordning. Det finns två varianter av sorted(): en som sorterar elementen i naturlig ordning och en som sorterar i en ordning som ges av en Comparator. Vilken Comparator som skall användas anges som parameter till metoden.

Vill vi till exempel skriva ut namnet på nobelpristagarna i alfabetisk ordning kan vi använda sorted() på följande sätt:

nobelPrizeWinners.stream().sorted().forEach(name -> System.out.println(name));
...
Bohr
Einstein
Feynman

Vill vi istället skriva ut dem i omvänd alfabetisk ordning kan vi skapa ett lambdauttryck som implementerar en Comparator som givet två strängar returnerar dem i omvänd ordning. Detta kan se ut så här:

(name1, name2) -> -name1.compareTo(name2)
Hela kodsnutten blir som följer:
nobelPrizeWinners.stream().sorted((name1, name2) -> -name1.compareTo(name2)).forEach(name -> System.out.println(name));
...
Feynman
Einstein
Bohr

max()

max(Comparator) returnerar en Optional som beskriver det största elementet i strömmen. Hur jämförelsen mellan element skall ske specificeras med den Comparator som skickas med som argument till metoden.

För att hitta nobelpristagaren med det "största" namnet kan vi exempelvis använda max() så här:

nobelPrizeWinners.stream().max((name1, name2) -> name1.compareTo(name2)).ifPresent(name -> System.out.println(name + " har det största namnet."));
...
Feynman har det största namnet.

min()

min(Comparator) fungerar analogt med max(), dvs den returnerar en Optional som beskriver det minsta elementet i strömmen.

Vill vi hitta nobelpristagaren med det "minsta" namnet kan vi göra så här:

nobelPrizeWinners.stream().min((name1, name2) -> name1.compareTo(name2)).ifPresent(name -> System.out.println(name + " har det minsta namnet."));
...
Bohr har det minsta namnet.

Metoder i de specialiserade strömmarna IntStream m.fl.

I de specialiserade strömmarna för att hantera double, int och long finns några metoder som är riktigt användbara. Namnen på metoderna är ganska självförklarande, men jag tänkte att jag listar dem ändå för fullständighets skull...

I texten nedan används IntStream genomgående, men motsvarande metoder finns även i DoubleStream och LongStream.

Notera att detta på inget sätt är en uttömmande lista över vilka metoder som finns på IntStream osv. Det finns många fler. Se API-dokumentationen för den fulla listningen av metoder.

average()

average() returnerar en OptionalDouble som beskriver medelvärdet av elementet i strömmen. En OptionalDouble är analogt med Optional en klass för att hantera ett värde som eventuellt kan finnas, fast i OptionalDouble:s fall finns ingen typparameter utan är värdet av typen double direkt. Det finns motsvarande klasser OptionalInt och OptionalLong för att hantera int:s och long:s.

För att räkna ut medellängden av nobelpristagarnas namn kan följande kod användas:

OptionalDouble avgOptional = nobelPrizeWinners.stream().mapToInt(name -> name.length()).average();
System.out.println("Medellängden av namnen är: " + avgOptional.getAsDouble());
...
Medellängden av namnen är: 6.333333333333333

max()

Metoden max() returnerar en OptionalInt som beskriver det största elementet i strömmen.

Exempel:

OptionalInt maxLengthOptional = nobelPrizeWinners.stream().mapToInt(name -> name.length()).max();
System.out.println("Längsta namnet har längden: " + maxLengthOptional.getAsInt()); 
...
Längsta namnet har längden: 8

min()

min() returnerar en OptionalInt som beskriver det minsta elementet i strömmen.

Exempel:

OptionalInt minLengthOptional = nobelPrizeWinners.stream().mapToInt(name -> name.length()).min();
System.out.println("Kortaste namnet har längden: " + minLengthOptional.getAsInt());
...
Kortaste namnet har längden: 4

sorted()

Metoden sorted() returnerar en ny IntStream med elementen i den ursprungliga strömmen sorterade i stigande ordning.

Exempel:

nobelPrizeWinners.stream().mapToInt(name -> name.length()).sorted().forEach(len -> System.out.println(len));
...
4
7
8

sum()

sum() returnerar summan av int:arna i strömmen.

Exempel:

int sumOfLengths = nobelPrizeWinners.stream().mapToInt(name -> name.length()).sum();
System.out.println("The sum of the lengths of the names is: " + sumOfLengths); 
...
The sum of the lengths of the names is: 19

Fler nya språkkonstruktioner - metodreferenser och konstruktorreferenser

Sakerna som gås igenom i det här avsnittet är saker som kanske borde varit med i del 2, men eftersom den delen blev så lång så valde jag att lyfta ur dem och ta med dem här istället.

Metodreferenser

Om ett lambdauttryck endast skickar vidare sina parametrar till en statisk metod eller en instansmetod utan att manipulera dem på något sätt kan lambdauttrycket ersättas med en metodreferens istället. Koden blir då oftast ytterligare lite kortare.

Mer specifikt kan metodreferenser användas på alla ställen där en instans av ett funktionellt interface förväntas. Det javakompilatorn gör är att den tar parametrarna till den abstrakta metoden i implementationen av det funktionella interfacet och anropar metoden som pekas ut av metodreferensen med dem som argument. Antal och typer på parametrarna måste naturligtvis överensstämma mellan den abstrakta metoden och metoden som pekas ut av metodreferensen.

Formatet på en metodreferens är klassnamn::metodnamn för statiska metoder och instans::metodnamn för instansmetoder. Som vanligt kan punktnotation användas för att komma åt nästlade objekt. Exempelvis kan den statiska variabeln out i klassen System kommas åt genom att ange System.out. Metodreferensen för System.out.println() blir alltså System.out::println.

I vårt exempel med metoden forEach() ovan skrev i ut alla element i en lista med strängar med följande kod:

nobelPrizeWinners.stream().forEach(nobelPrizeWinner -> System.out.println(nobelPrizeWinner));

Om vi istället använder en metodreferens till System.out.println kan koden ändras till:

nobelPrizeWinners.stream().forEach(System.out::println);

I exemplet ovan har en metodreferens till en statisk metod används. Det går dock lika bra att använda metodreferenser för att peka på en instansmetod i ett specifikt objekt och en instansmetod i ett godtyckligt objekt.

När det gäller att använda en metodreferens för att peka på en instansmetod i ett godtyckligt objekt gäller det att den abstrakta metoden i det funktionella interfacet som metodreferensen implementerar tar en parameter mer än metoden man vill peka på. Detta eftersom den första parametern till det funktionella interfacets abstrakta metod måste vara objektet vars instansmetod skall anropas. Övriga parametrar kopieras direkt över från anropet på den abstrakta metoden till anropet på instansmetoden.

Det där lät ju lite komplicerat. Ett exempel kanske gör det klarare.

När vi skulle sortera listan med nobelpristagare och skriva ut resultatet använde vi följande kod:

nobelPrizeWinners.stream().sorted((name1, name2) -> -name1.compareTo(name2)).forEach(name -> System.out.println(name));
Vi har här använt metoden sorted(Comparator) i Stream. Comparator är ett funktionellt interface med den abstrakta metoden int compare(T, T). Om vi vill ersätta lambdauttrycket ovan med en metodreferens till en instansmetod i ett godtyckligt objekt måste vi hitta en sådan som tar in en parameter av samma typ som klassen själv är av och som returnerar int. Metoden compareTo(String) i String passar som handen i handsken. Vi kan därför ersätta koden ovan med följande:
nobelPrizeWinners.stream().sorted(String::compareTo).forEach(name -> System.out.println(name));
Tycker att varianten med en metodreferens är lite mer lättläst än den som använder lambdauttryck. Kanske inte någon stor skillnad, men ändock någon.

Konstruktorreferenser

En konstruktorreferens är en speciell typ av metodreferens som istället för att peka ut en metod pekar ut en konstruktor. Formatet på en konstruktorreferens är klassnamn::new, dvs precis samma som för en vanlig metodreferens, men metodnamnet har ersatts med new.

En konstruktorreferens kan användas på ställen där javakompilatorn förväntar sig en instans av ett funktionellt interface vars abstrakta metod inte tar in några parametrar och som returnerar ett objekt av den typ som konstruktorreferensen specifierar.

Ett typexempel på detta är i samband med instantiering av det funktionella interfacet Supplier. Vi gick igenom Supplier i del två av serien, men i korthet så används Supplier när man vill specificera en entitet som producerar objekt av en viss typ. Den abstrakta metoden i Supplier har signaturen T get(), dvs den tar inga inargument och producerar ett objekt av typen T, vilket gör att den uppfyller kraven för konstruktorreferensanvändande.

Ett exempel på ett lambdauttryck som implementerar Supplier är:

() -> new ArrayList()
Ovantsående kod implementerar alltså en Supplier som returnerar en ny ArrayList varje gång dess get()-metod anropas. Med en konstruktorreferens kan användandet av lambdauttrycket istället ersättas med ArrayList::new. Lite kortare och lite tydligare eller hur? Speciellt om man vill anropa metoder som tar in fler än en Supplier som argument i sin parameterlista. Vilket collect() som vi skall prata om nu gör...

Från Stream till Collection

Avsnitten i början av den här delen har gått igenom en hel del metoder för att manipulera strömmar. Många (väldigt många...) Java-program som existerar idag använder dock inte strömmar utan Collection:s av olika slag. Detta dels för att Collections är väldigt användbara för att hålla ihop ett antal element på något visst sätt med något visst syfte och dels för att strömmar inte fanns i versioner av Java som existerade innan Java 8. Introduktionen av Stream i API:et kommer på inget sätt att innebära att användandet av Collection:s försvinner. Det är ju förresten från en Collection som man oftast får tag på en Stream att börja manipulera genom att använda stream()-metoden i Collection-interfacet. Vore det då inte bra om man då kunde transformera tillbaka en Stream till en Collection när man är klar med manipulerandet av strömmen? Det är där metoden collect() kommer in...

Metoden collect() i Stream skapar en Collection från en Stream. Metoden finns i två varianter: en lite mer explicit och en lite mer implicit. Vi börjar med den lite mer explicita varianten.

collect() med explicit supplier, accumulator och combiner

För att collect() skall kunna skapa en Collection från en Stream behöver den få specificerat hur den skall göra följande saker:

  1. Hur den skall skapa en Collection att stoppa element i (vilken supplier den skall använda).
  2. Hur den skall lägga till ett enskilt element till kollektionen (vilken accumulator den skall använda).
  3. Hur den skall merga ihop två Collection:s (vilken combiner den skall använda).

I den här varianten av collect() specas alltså supplier, accumulator och combiner explicit. Detta görs lättast med hjälp av konstruktorreferenser och metodreferenser. Vilken tur att vi gått igenom hur sådana ser ut och fungerar tidigare i avsnittet ;)

Det första argumentet till collect() skall alltså specificera vilken Collection som skall utnyttjas. Argumentet är av typen Supplier vilket, som vi konstaterat ovan, gör att vi kan använda en konstruktorreferens för att tala om vilken Collection vi vill använda. Vill vi till exempel använda en ArrayList kan vi specificera detta på föjande sätt: ArrayList::new.

Med det andra argumentet till collect() vill vi specificera en metod som lägger till ett element i kollektionen. Det ligger ju nära till hands att tänka på ArrayList.add() i detta fall. Funkar det då? Mer specifikt: uppfyller en metodreferens till ArrayList.add() de kriterier som ställs på det andra argumentet till collect()?

Det andra argumentet till den här varianten av collect() har typen BiConsumer. Detta är en speciell typ av Consumer som konsumerar två objekt. Dvs BiConsumer är ett funktionellt interface vars abstrakta metod accept(S, T) har två parametrar. Om vi ser på metoden add() i ArrayList så tar den in en parameter av typen T (typparametern). Vi ser då att om vi gör ansatsen att S == ArrayList så kommer en metodreferens till ArrayList::add att uppfylla kraven på en BiConsumer och vi kan alltså använda ArrayList::add som värde på det andra argumentet till collect. Lysande Sickan.

Med ett analogt resonomang kommer man fram till att ArrayList::addAll kan användas som värde på den tredje parametern till collect() . Ett anrop till collect() kan alltså de ut som följer:

collect(ArrayList::new, ArrayList::add, ArrayList::addAll)

Som ett exempel kan vi skriva ett litet program som samlar ihop alla nobelpristagare som börjar på bokstaven F i en lista:

List<String> winnersOnF = nobelPrizeWinners.stream().filter(name -> name.startsWith("F")).collect(ArrayList::new, ArrayList::add, ArrayList::addAll);

collect() med Collector

Det finns även en annan variant av collect() som tar ett argument av typen Collector. I detta fall är det Collector:n som specificerar vilken supplier, accumulator och combiner som skall användas. Collector är nämligen ett interface som specificerar metoder supplier(), accumulator() och combiner() för att just hämta ut sådana.

På vilket sätt hjälper Collector oss då? Det hjälper oss inte direkt, men det finns en trevlig hjälpklass kallad Collectors som kan göra livet lite lättare för oss. Collectors innehåller nämligen en hel massa användbara metoder som returnerar Collector-instanser av olika slag. En av de mest användbara, skulle jag tippa, heter toList(). Den returnerar en Collector som, hör och häpna, använder en List som supplier. Vi kan med hjälp av den skriva om exemplet ovan så här:

List<String> winnersOnF = nobelPrizeWinners.stream().filter(name -> name.startsWith("F")).collect(Collectors.toList());

Vi kommer att se mer av Collectors-klassen in nästa del av serien.

Avslutning del 3

Den här delen har handlat mycket om vilka metoder som finns i Stream och vad man kan göra med dem. Vi har gått igenom vad metodreferenser och konstruktorreferenser är för något och hur man kan transformera en ström tillbaka till en kollektion. Vi har även tittat lite på de specialiserade strömmarna DoubleStream, IntStream och LongStream.

I nästa del i serien skall vi bland annat fortsätta undersöka Collectors-klassen samt se hur vi kan parallellisera exekveringen av metoder på Stream på ett lätt sätt.

Tidigare delar i serien

Del 1: Interface
Del 2: Streams och lambda expressions

Referenser

Subramaniam V, Functional Programming in Java, första utgåvan, The Pragmatic Programmers, 2014.
The Java Tutorial - Method references

Nyheter i Java 8 - del 4 - metoder i klassen Collectors

2014-08-31

Detta är den fjärde delen i miniserien om nyheterna i Java 8. Del ett handlade om ändringarna som gjorts på interface, del två mestadels om de den nya funktionaliteten med streams och om lambda expressions, och del tre vad man kan göra med metoderna i Stream.

Den här fjärde delen kommer att fortsätta diskutera vad som kan göras med den variant av collect() som tar en Collector som argument och hur man kan utnyttja metoderna i Collectors-klassen för att generera inparametrar till den.

Metoder i Collectors

Vi nosade lite på ytan på den funktionalitet som erbjuds av klassen java.util.stream.Collectors i del tre. Nu skall vi dyka lite djupare ner och undersöka fler metoder i Collectors och se hur vi kan använda dessa för att vända och vrida på datat i en ström.

Detta är inte på något sätt en komplett förteckning av alla metoder i Collectors och beskriver inte alls allt som kan göras med de metoder som behandlas, utan skall nog ses som en aptitretare för vidare utforskning i ämnet. API-dokumentationen för Collectors finns här.

En klass att testa med

I exemplen nedan används en klass som representerar en bok. Den har bara fyra attribut: titel, förlag, publiceringsår och pris, och den ser ut som följer:

public class Book {
  private String title;
  private String publisher;
  private int yearPublished;
  private int price;

  public Book(String title, String publisher, int yearPublished, int price) {
    this.title = title;
    this.publisher = publisher;
    this.yearPublished = yearPublished;
    this.price = price;
  }

  public String getTitle() {
    return title;
  }

  public void setTitle(String title) {
    this.title = title;
  }

  public String getPublisher() {
    return publisher;
  }

  public void set(String publisher) {
    this.publisher = publisher;
  }

  public int getYearPublished() {
    return yearPublished;
  }

  public void setYearPublished(int yearPublished) {
    this.yearPublished = yearPublished;
  }

  public int getPrice() {
    return price;
  }

  public void setPrice(int price) {
    this.price = price;
  }

  public String toString() {
    return title + ", " + publisher + ", " + yearPublished + ", " + price + "kr";
  }
}

I exemplen nedan används en lista med Books som är skapad som följer:

Book functionalProgrammingInJava = new Book("Functional Programming In Java", "The Pragmatic Programmers", 2014, 260);
Book javaPerformance = new Book("Java Performance - the definitive guide", "O'Reilly", 2014, 300);
Book programmingGroovy2 = new Book("Programmming Groovy2", "The Pragmatic Programmers", 2012, 210);
Book effectiveJava = new Book("MongoDB - the definitive guide, 2ed", "O'Reilly", 2014, 260);
Book algorithmsInPython = new Book("Python algorithms", "Apress", 2012, 360);
    
List<Book> books = Arrays.asList(functionalProgrammingInJava, javaPerformance, programmingGroovy2, effectiveJava, algorithmsInPython);

Alla metoder från Collectors som används i exemplen är statiskt importerade. Detta för att spara lite skärmyta genom att slippa skriva Collectors. framför anropen. Och för att jag är lat.

Notera att jag i exemplen nedan ofta sparar undan resultat i lokala variabler innan utskrift på skärmen sker, isället för att direkt chain:a på exempelvis .stream().forEach(String.out::println). Det är gjort på detta sätt för att jag vill poängtera vilken typ resultatet har. Vilket inte alltid är uppenbart...

Vad returnerar Stream.collect()?

Om man tittar på API-beskrivningen till Stream.collect() ser man dess signatur är <R,A> R collect(Collector<? super T,A,R> collector). Den har alltså returtypen R där R är en av typparametrarna till Collector:n som collect() tar som inargument. Metoden kan alltså returnera helt olika, och då menar jag verkligen helt olika, typer beroende på vilken Collector den får som argument. Använder man sig av Collector:erna i Collectors kan det vara en god idé att undersöka javadoc:en för metoderna i fråga för att lista ut vad man skall förvänta sig för returtyp från collect().

Innan jag greppat att det var den sista typparametern till Collector:n som användes som argument till collect() som angav returtypen tyckte jag att många av de exempel jag såg på nätet var luriga att förstå.

Detta var ett tips bara. Ni är sannolikt inte lika tröga som jag och hade säkert fattat exemplen ändå, men just in case ;)

Hitta största elementet - maxBy()

Metoden maxBy() har signaturen static <T> Collector<T,?,Optional<T>> maxBy(Comparator<? super T> comparator). Den letar alltså upp det största elementet, givet någon viss Comparator, i en ström och returnerar detta som en Collector.

Vad är det då för bra med detta? Det finns ju andra metoder, tex Stream.max(), som gör samma sak. Fördelen med maxBy är just att den returnerar en Collector, vilket gör att man kan kombinera den med andra metoder i Collectors för att till exempel leta upp det största elementet i olika grupper av element. Mer om detta nedan.

Som ett litet exempel på maxBy() så kan vi skriva en liten snutt som letar upp den dyraste boken och skriver ut detaljer om den på konsolen. Programmet ser ut som följer:

Optional<Book> mostExpensiveBook = books.stream().collect(maxBy((b1, b2) -> b1.getPrice() - b2.getPrice()));
mostExpensiveBook.ifPresent(System.out::println);
...
Python algorithms, Apress, 2012, 360kr

Notera att returtypen från collect() alltså blir en Optional eftersom det är denna typ som specas som den sista typparametern till maxBy().

Summera - summingInt()

Om man istället för att hitta den dyraste boken vill beräkna summan av hur mycket av ens surt ihoptjänade pengar man lagt på datalitteratur kan man göra detta med metoden summingInt(). Det går exempelvis till på följande sätt:

int totalPrice = books.stream().collect(summingInt(Book::getPrice));
System.out.println(totalPrice); 
...
1390
Om nu bara den verkliga summan hade varit så låg...

Det finns också metoder summingDouble() och summingLong() för att summera double:s och long:s.

mapping()

mapping() har en signatur som får mig att få huvudvärk, nämligen static <T,U,A,R> Collector<T,?,R> mapping(Function<? super T,? extends U> mapper, Collector<? super U,A,R> downstream).

Läser man beskrivningen på vad den gör klarnar det dock lite. Det den gör är att den tar elementen från en kollektor med elementtyp U, applicerar en funktion U -> T och stoppar in resultatet i en annan kollektor med elementtypen T. Åtminstone är det det jag tror den gör ;)

Vi får se om det hjälper med ett exempel. Anta att vi bara vill skriva ut böckernas titlar. Detta kan göras på följande sätt:

List<String> bookTitles = books.stream().collect(mapping(Book::getTitle, toList()));
bookTitles.stream().forEach(System.out::println);
...
Functional Programming In Java
Java Performance - the definitive guide
Programmming Groovy2
MongoDB - the definitive guide, 2ed
Python algorithms

Vi samlade alltså upp alla böckernas titlar i en lista (pga kollektorn som returneras från toList()) och skrev sedan ut dem. Ser ut som det funkar va?

reducing()

reducing() gör motsvarande som reduce() i Stream, dvs den returnerar en collector som givet en BinaryOperator jämför elementen två och två med varandra och vid varje jämförelse väljer ut ett av dem. Resultatet blir att endast ett element återstår till slut.

Vi kan använda reducing() för att extrahera fram boken med den längsta titeln.

Optional<Book> bookWithLongestTitle = books.stream().collect(reducing((b1, b2) -> b1.getTitle().length() - b2.getTitle().length() >= 0 ? b1 : b2));
System.out.println(bookWithLongestTitle.get().getTitle()); 
...
Java Performance - the definitive guide

Gruppering (enkel) - groupingBy()

Ovanstående metoder har väl hittils egentligen inte tillfört något nytt jämfört med de metoder som finns i Stream. Alla exempel ovan hade vi kunnat skriva med Stream-metoder, vilket antagligen hade varit bättre. Nu när vi börjar kolla på groupingBy() kommer vi dock se att vi kan göra fler saker med maxBy(), mapping() med flera än vad vi kan med metoderna i Stream. Så då börjar vi väl då...

Den första varianten av groupingBy() som vi skall kolla på har signaturen static <T,K> Collector<T,?,Map<K,List<T>>>groupingBy(Function<? super T,? extends K> classifier). Den tar alltså in en Function som argument och med hjälp av denna grupperar den ihop elementen i strömmen i grupper. Grupperna returneras i en Map vars nycklar är de unika värden som returnerats från Function:en och vars värden är listor av medlemmarna i varje grupp.

Om vi vill gruppera böckerna i vårt löpande exempel i grupper baserat på deras publiceringsår kan detta göras så här:

Map<Integer, List<Book>> booksByYearPublished = books.stream().collect(groupingBy(Book::getYearPublished));
for (Map.Entry<Integer, List<Book>> entry :  booksByYearPublished.entrySet()) {
  entry.getValue().stream().forEach(System.out::println);
  System.out.println(""); 
}
...
Programmming Groovy2, The Pragmatic Programmers, 2012, 210kr
Python algorithms, Apress, 2012, 360kr

Functional Programming In Java, The Pragmatic Programmers, 2014, 260kr
Java Performance - the definitive guide, O'Reilly, 2014, 300kr
MongoDB - the definitive guide, 2ed, O'Reilly, 2014, 260kr
Vi ser alltså att böckerna grupperats in i två grupper: en innehållande böcker som publicerats 2012 och en med böcker som publicerats 2014.

Jag har i exempelprogrammet ovan använt en gammal hederlig for-loop för att skriva ut böckerna i de olika grupperna. Vill man så kan man istället använda forEach() med ett lambdaexpression för att skriva ut grupperna.

booksByYearPublished.forEach((publishedYear, listOfBooks) -> {
    listOfBooks.stream().forEach(System.out::println); 
    System.out.println("");
});
Notera den extra radbrytningen i slutet av lambdauttrycket som är med för att separera grupperna. Inkluderingen av denna gjorde att vi var tvungna att använda måsvingar runt utskriftsuttrycken.

Gruppering (med subgrupper)

Den andra versionen av groupingBy() som vi skall titta på kan användas för att gruppera element i fler än en nivå. Metoden har signaturen static <T,K,A,D> Collector<T,?,Map<K,D>>groupingBy(Function<? super T,? extends K> classifier, Collector<? super T,A,D> downstream).

I och med att den andra parametern, downstream, har typen Collector är det inget som hindrar att vi skickar in returvärdet från ytterligare ett anrop till groupingBy(). På så sätt kan gruppering ske i obegränsat antal steg. Vårt exempel kommer dock bara att gruppera i två led: böcker grupperat per utgivningsår och förlag. Detta kan föras som följer:

Map<Integer, Map<String, List<Book>>> booksByYearAndPublisher = books.stream().collect(groupingBy(Book::getYearPublished, groupingBy(Book::getPublisher)));
booksByYearAndPublisher.forEach((publishedYear, mapOfBooksPerPublisher) -> {
     System.out.println("*** " + publishedYear); 
     mapOfBooksPerPublisher.forEach((publisher, listOfBooksForPublisher) -> {
         System.out.println("*" + publisher); 
         listOfBooksForPublisher.stream().forEach(System.out::println); 
         System.out.println(""); 
       });
});
...
*** 2012
*The Pragmatic Programmers
Programmming Groovy2, The Pragmatic Programmers, 2012, 210kr

*Apress
Python algorithms, Apress, 2012, 360kr

*** 2014
*The Pragmatic Programmers
Functional Programming In Java, The Pragmatic Programmers, 2014, 260kr

*O'Reilly
Java Performance - the definitive guide, O'Reilly, 2014, 300kr
MongoDB - the definitive guide, 2ed, O'Reilly, 2014, 260kr

Vi ser att grupperna returneras i nästlade Map:ar. Den första Map:en har året i fråga som nyckel och som värde nästa nivå av gruppering i form av en annan Map. Den andra Map:en har förlag som nyckel och en lista med böcker per förlag som värde. Resultatet är alltså en gruppering av böckerna per publiceringsår och förlag. Inte så svårt eller hur?

Grupperingen per år fås alltså genom att skicka in en metodreferens till Book::getYearPublished som första pararameter till groupingBy(). Som andra parameter skickas ytterligare ett groupingBy() anrop, denna gången med metodreferensen Book::getPublisher som parameter, vilket alltså leder till att böckerna subgrupperas per förlag.

Om vi inte hade velat se den lite knöliga returtypen från collect(), dvs Map<Integer, Map<String, List<Book>>> hade vi givetvis kunnat haka på forEach() direkt på collect. Jag gjorde inte så nu eftersom jag ville att det skulle vara tydligt hur grupperingen av böcker såg ut.

Den här varianten av groupingBy() som alltså tar en Collector som andra argument kan ju givetvis användas med alla möjliga Collector:er, inte bara med sådana Collector:er som returneras från ytterligare ett anrop till groupingBy(). Kombinationsmöjligheterna är om inte oändliga, så i alla fall mycket stora. Vi skall kolla på några av dessa kombinationer lite senare i avsnittet, men först skall vi titta lite på en annan metod för att dela upp strömmen i delar.

Dela upp i två delar - partitioningBy()

Metoden partitioningBy() kan användas för att likt groupingBy() dela upp strömmen i delar. Skillnaden är att partitioningBy() tar in ett Predicate istället för en Function. Eftersom den abstrakta metoden i ett Predicate returnerar en boolean innebär detta att partitioningBy() delar in elementen i exakt två delar, inte ett godtyckligt antal som groupingBy() kan göra.

partitioningBy() finns likt groupingBy() i två varianter. Den ena har signaturen public static <T> Collector<T,?,Map<Boolean,List<T>>> partitioningBy(Predicate<? super T> predicate). Den returnerar alltså en Collector med elementen uppdelade i två delar: en för vilket det inskickade predikatet returnerar true och en för vilket det returnerar false. De booleska värdena kommer att vara nycklar i den returnerade Map:en och elementen som hör till respektive grupp kommer att finnas samlade listor som utgör värdena i Map:en.

Med den andra varianten av partitioningBy() kan man rekursivt fortsätta att dela upp delarna i mindre delar (helt analogt med den andra varianten av groupingBy()). Den har signaturen public static <T,D,A> Collector<T,?,Map<Boolean,D>> partitioningBy(Predicate<? super T> predicate, Collector<? super T,A,D> downstream).

Som ett exempel på den första varianten av partitioningBy() skall vi skriva ett litet program som delar upp böckerna i två delar: billiga böcker (som kostar mindre än 300 kronor) och dyra böcker (som kostar 300 kronor eller mer). Programmet kan se ut så här:

Map<Boolean, List<Book>> cheapAndExpensiveBooksMap = books.stream().collect(partitioningBy(book -> book.getPrice() < 300));
cheapAndExpensiveBooksMap.forEach((cheap,  cheapOrExpensiveBooks) -> {
    System.out.println(cheap ? "*** Billiga böcker: " : "*** Dyra böcker: "); 
    cheapOrExpensiveBooks.stream().forEach(System.out::println);
});
...
*** Dyra böcker: 
Java Performance - the definitive guide, O'Reilly, 2014, 300kr
Python algorithms, Apress, 2012, 360kr
*** Billiga böcker: 
Functional Programming In Java, The Pragmatic Programmers, 2014, 260kr
Programmming Groovy2, The Pragmatic Programmers, 2012, 210kr
MongoDB - the definitive guide, 2ed, O'Reilly, 2014, 260kr

Vi skickar in ett litet lambdauttryck book -> book.getPrice() < 300 vilket delar in böckerna i två delar: dyra och billiga. Vi ser vidare att Map:en som returneras innehåller exakt två nycklar (vilket ju är det vi förväntar oss): true och false. Samma resultat hade kunnat uppnås genom att använda groupingBy() med precis samma lambdauttryck. Skillnaden är att när partitioningBy() används så kommuniceras intentionen att uppdelningen skall ske i exakt två delar bättre. Med groupingBy() kan ju grupperna bli godtyckligt många.

Nu har vi gått igenom alla metoder som jag tänkt gå igenom i Collectors. Låt oss nu se hur vi kan kombinera dessa.

Kombinationer - största element i varje grupp

Det första exemplet på kombinationer som vi skall se på är hur vi kan extrahera fram den dyraste boken för varje år. För att hitta den totalt sett dyraste boken använde vi ovan oss av maxBy() och för att gruppera per år använde vi oss alldeles nyss av groupingBy(). Låt oss kombinera dessa två i ett androp till collect().

Map<Integer, Optional<Book>> mostExpensiveByYearPublished = books.stream().collect(groupingBy(Book::getYearPublished, maxBy((b1, b2) -> b1.getPrice() - b2.getPrice())));
mostExpensiveByYearPublished.forEach((year, mostExpensiveBookThatYear) -> {
     System.out.println("Year: " + year + " book: " + mostExpensiveBookThatYear.get()); 
});
...
Year: 2012 book: Python algorithms, Apress, 2012, 360kr
Year: 2014 book: Java Performance - the definitive guide, O'Reilly, 2014, 300kr

Vi har här grupperat böckerna per år genom att ange Book::getYearPublished som värde på Function-argumentet till collect(). Istället för att göra undergrupper med hjälp av ett andra anrop till groupingBy(), så gör vi nu istället ett anrop till maxBy() med en Comparator som väljer ut den dyraste boken av två. Resultatet blir alltså att vi får en Map innehållande de dyraste böckerna per år. Lysande Sickan. Det är förresten tur att jag inte tagit med böckerna jag köpte år 2005 i exemplet. Då köpte jag en inbunden specialvariant av Stroustrups C++-bok för över 900 spänn. Får skylla på att jag var ung och dum. Eller i alla fall dum ;)

Kombinationer - gruppering och mappning

Som ett andra exempel på hur man kan kombinera ihop Collector:s tänkte jag att vi skulle kolla på hur vi kan kombinera groupingBy() och mapping().

Det vi vill åstadkomma är att vi vill gruppera böckerna per förlag och skriva ut dess titlar. Vi kan göra det som följer:

Map<String, List<String>> bookTitleByPublisher = books.stream().collect(groupingBy(Book::getPublisher, mapping(Book::getTitle, toList())));
bookTitleByPublisher.forEach((publisher, bookTitlesForPublisher) -> {
    System.out.println("
*** " + publisher); 
    bookTitlesForPublisher.stream().forEach(System.out::println);        
});
...
*** The Pragmatic Programmers
Functional Programming In Java
Programmming Groovy2

*** O'Reilly
Java Performance - the definitive guide
MongoDB - the definitive guide, 2ed

*** Apress
Python algorithms

Ni börjar känna igen mönstret va? Först en groupingBy() för att gruppera och sedan en Collector (i det här fallet resultatet från mapping()) för att bearbeta grupperna på något sätt.

Kombinationer - gruppering och reducering

Som ett sista exempel skall vi se hur vi hur kan kombinera groupingBy() och reducing() för att hitta den bok per förlag som har den längsta titeln. Så här kan det se ut:

Map<String, Optional<Book>> bookWithLongestTitlePerPublisher = books.stream().collect(groupingBy(Book::getPublisher, reducing((b1, b2) -> b1.getTitle().length() - b2.getTitle().length() >= 0 ? b1 : b2)));
 bookWithLongestTitlePerPublisher.forEach((publisher, bookWithLongestTitleForPublisher) -> {
     System.out.println(publisher + ": " + bookWithLongestTitleForPublisher.get()); 
 });
...
The Pragmatic Programmers: Functional Programming In Java, The Pragmatic Programmers, 2014, 260kr
O'Reilly: Java Performance - the definitive guide, O'Reilly, 2014, 300kr
Apress: Python algorithms, Apress, 2012, 360kr

Exemplet följer samma mönster som de närmast ovan. Vi använder först groupingBy() för att gruppera böckerna och applicerar sedan reducing() på varje grupp för att hitta boken med den längsta titeln per grupp.

Avslutning del 4

I den här delen har vi sett exempel på ytterligare några metoder som finns i Collectors och hur man kan använda dem för att manipulera en ström av element på olika sätt. Den mest intressanta metoden, enligt mitt tycke, är groupingBy() och vi har sett hur vi kan använda denna för att gruppera elementen i strömmen på olika sätt. Vi har även tittat på hur man kan applicera olika funktioner på de olika grupperna om man så vill. Collectors-klassen erbjuder enorma kombinationsmöjligheter där vi kan kombinera ihop groupingBy() och de andra metoderna i närmast oändliga kedjor. Jag kommer nog dock att hålla mig till max två, tre nivåer. Det är allt mitt huvud mäktar med ;)

Jag hade från början tänkt att den här delen också skulle inkludera ett avsnitt om hur man kan parallellisera exekveringen av operationer på strömmar, men inlägget blev såpass långt att jag beslöt att detta ämne får vänta tills nästa inlägg.

Tidigare delar i serien

Del 1: Interface
Del 2: Streams och lambda expressions
Del 3: Metoder i Stream, metod- och konstruktorreferenser

Referenser

Subramaniam V, Functional Programming in Java, första utgåvan, The Pragmatic Programmers, 2014.
Java 8 API - Collectors