[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


Leave a reply

Your name as it will be displayed when the comment is posted on the page. Your email address will not be published.