[x^2 for x in lst]

Nyheter i Java 8 - del 1

2014-07-18

Det här är första delen i en liten miniserie om nyheterna i Java 8. Den kommer att handla om det som är nytt i själva språket Java och lite om några av core-paketen i JDK:n. Den gör inga anspråk på att vara komplett utan anvhandlar bara det som jag själv har tyckt varit intressant att lära mig om Java 8. Första delen handlar om interface, som för första gången sedan Java 1.0 har genomgått en förändring. Senare delar är tänkta att handla om Streams, ändringar på collections mm.

Interface

Interface har i Java 8 blivit mer lika abstrakta klasser. De kan nu mera innehålla defaultmetoder och statiska metoder.

Defaultmetoder

Vad är en defaultmetod?

En defaultmetod är precis som det låter en metod som det skapas en defaultimplementation för direkt i interfacet. Det som anger att en metod är en defaultmetod är nyckelordet default används samt att metoden har en kropp. Ex:


public interface Drivable {
  default void honk() {
    System.out.println("Tuut from Drivable");
  }        
}

Interfacet ovan innehåller endast en metod. Eftersom den är en defaultmetod kan en implementation av interfacet vara helt tomt:


public class Car implements Drivable {
}
Det går bra att skapa en instans av implementationsklassen och anropa honkHorn()-metoden:

public class TestClass {
  public static void main(String[] args) {
    Car car = new Car();
    car.honk();    
  } 
}
Kör man ovanstående klass fås följande resultat:

lars@larsLilla:~/text/blogg/140716$ java TestClass
Tuut from Drivable
lars@larsLilla:~/text/blogg/140716$ 

Vad är de bra för?

Det mest uppenbara användningsområdet för defaultmetoder, tycker jag, är om man har ett befintligt interface i produktion som man behöver lägga till en ny metod till. Tidigare läts inte detta göras med mindre än att man uppdaterade alla klasser som implementerade interfacet, vilket ofta var helt omöjligt. I alla fall om man själv inte förfogade över hela kodbasen som använde interfacet, vilket man ju ofta inte gör om interfacet används i till exempel ett publikt API och/eller ett open source-projekt. Eftersom det finns nya funktioner i Java 8 som har krävt ändringar på befintliga interface i JDK:n (t.ex. i Iterable) så gissar jag på man infört defaultmetoder för att kunna lösa detta. Vilket ju är trevligt även för oss vanliga dödliga som skapar i alla fall halvpublika interface som tidigare varit svåra att utöka.

Ett annat användningområde för defaultmetoder är om ett nytt interface skapas och det innehåller en metod som har en mycket uppenbar implementation. Ex:


public interface AnInterface {
  void doStuff();
  default void logToConsole(String msg) {
    System.out.println(msg); 
 }
}

Kan det bli problem?

I och med att en klass kan implementera flera interface kan man ju undra om inte en variant på det vanliga "diamantproblemet" kan uppkomma. Svaret är att det kan det, men att javac då vägrar att kompilera klassen och istället ger ifrån sig ett felmeddelande. Vi provar. Först skapar vi ett interface Honkable med en defaultmetod med samma namn som defaultmetoden ovan:

public interface Honkable {
  default void honk() {
    System.out.println("Tööööööt");
  }        
}
Sedan skapar vi en klass som implementerar båda interfacen...

public class SportsCar implements Drivable, Honkable
{
}
...och försöker kompilera den:

lars@larsLilla:~/text/blogg/140716$ javac SportsCar.java
SportCar.java:1: error: class SportsCar inherits unrelated defaults for honk() from types Drivable and Honkable
public class SportsCar implements Drivable, Honkable
       ^
1 error
Hur fixar man detta då? Det man får göra är att man skapar en implementation av den problematiska metoden i sin implementationsklass. Metoden kan på vanligt sätt innehålla vilken syntaktiskt korrekt Java-kod som helst utan någon som helst koppling till defaultimplementationerna i de implementerade interfacen.

public class SportsCar implements Drivable, Honkable {
  public void honk() {
    System.out.println("Tuuuut from SportsCar!"); 
  }        
}
Klassen kompilerar nu fint och ger det förväntade resultatet om man testkör den:

lars@larsLilla:~/text/blogg/140716$ javac SportsCar.java TestClassSportsCar.java 
lars@larsLilla:~/text/blogg/140716$ java TestClassSportsCar
Tuuuut from SportsCar!        
Om man vill referera kan man referera till en av de implementaterade interfacens default implementationer genom att använda nyckelordet super i kombination med namnet på interfacet vars defaultimplementation man vill anropa. Ex:

public class SportsCar implements Drivable, Honkable {
  public void honk() {
    Drivable.super.honk();
  }        
}
Kör man testklassen nu ser man att defaultmetoden i Drivable anropas:

lars@larsLilla:~/text/blogg/140716$ javac SportsCar.java TestClassSportsCar.java 
lars@larsLilla:~/text/blogg/140716$ java TestClassSportsCar
Tuut from Drivable

Skillnader mot abstrakta klasser

Vad är då skillnaden mellan en defaultmetod i ett interface och en (överridningsbar) metod i en abstrakt klass? En skillnad är att defaultmetoden inte har åtkomst till något tillstånd. Den kan endast använda sina inparametrar och sitt eventuella returvärde för att kommunicera med omvärlden. En metod i en abstrakt klass har ju precis som vilken annan klass som helst tillgång till de fält som finns definierade i klassen och i eventuella superklasser.

Att en defaultmetod inte har tillgång till något tillstånd förutom via sina inparametrar innebär ju även att de enda metoder en defaultmetod kan anropa är andra metoder på interfacet den är definierad i samt metoder på inparametrarna.

Statiska metoder

Vad är en statisk metod i ett interface?

En statisk metod i ett interface är en metod som kan anropas utan att ha en instans av en implementation av interfacet. Detta är helt analogt med statiska metoder i klasser. Syntaxen som används är Interfacenamn.metodnamn även det helt analogt med klassfallet.

För att definera en statisk metod i ett interface används nyckelordet static. Metoden ges även en kropp. Ex:

public interface InterfaceContainingStaticMethods {
  static void performSomeStuff() {
    System.out.println("Inside the static method"); 
  }
}

Exempel på användning:

public class TestClassStaticMehod {
  public static void main(String[] args) {
    InterfaceContainingStaticMethods.performSomeStuff();
  } 
}

Kompilering och testkörning:

lars@larsLilla:~/text/blogg/140716$ javac InterfaceContainingStaticMethods.java TestClassStaticMehod.java 
lars@larsLilla:~/text/blogg/140716$ java TestClassStaticMehod
Inside the static method
lars@larsLilla:~/text/blogg/140716$

Vad är statiska metoder i interface bra för?

Ett användingsområde för statiska metoder i interface är när man vill gruppera ihop ett antal metoder som opererar på en viss interface-klass. Tidigare fick man skapa en separat klass för detta och i den lägga statiska metoder, men nu kan man istället implementera de statiska metoderna direkt i interfacet. Ett exempel från JDK:n på kombinationen "interface - klass med statiska metoder som operarar på interfacet" är Collection - Collections. Med Java 8 hade de statiska metoderna i Collections kunnat ligga direkt i Collection-interfacet.

Avslutning del 1

Det var det om interface. Tycker att tilläggen med defaultmetoder och statiska metoder i Java 8 är väldigt trevliga. Speciellt möjligheten att implementera defaultmeoder i redan produktionssatta interface verkar mycket användbar. Med den kan man rätta till i alla fall vissa av de misstag man ofta gör när man skapar API:er.

Nästa del i serien om nyheter i Java 8 kommer att handla om Stream:s som är grundläggande för att kunna programmera funktionellt i Java.

Referenser

Subramaniam V, Functional Programming in Java, första utgåvan, The Pragmatic Programmers, 2014.
Muhammad Khojaye, Interface Default Methods in Java 8
The Java Tutorial - Default methods

Nyheter i Java 8 - del 2 - Streams:s och lambda expressions

2014-07-31

Det här är andra delen i en liten miniserie om nyheterna i Java 8. Den första delen handlade om de ändringar på interface som har gjorts i Java 8. Den här delen kommer att undersöka bland annat vad Stream och lambda expressions är för något med avsikt lägga grunden för hur man kan programmera funktionellt med Java.

Stream

Vad är en Stream?

En instans av en klass som implementerar interfacet Stream är ett objekt som modellerar en ström av element som det går att utföra sekventiella eller parallella operationer på med avsikten att producera ett aggregerat resultat.

Det låter väl lagom luddigt? Vad är då skillnaden mellan en Stream och en Collection? Jo, enligt dokumentationen är den största skillnaden, bortsett från att de innehåller helt olika operationer, intentionen med dem. Syftet med en Collection är att hålla ihop och ge åtkomst till antal element. Kanske utför man någon operation på elementen, kanske gör man det inte. Poängen är att elementen hålls ihop som en enhet. Hela syftet med en Stream är att utföra operationer på elementen i strömmen tills dess att ett slutgiltigt resultat har producerats. Detta kan göras i flera delsteg där varje delsteg som inte producerar det slutgiltiga resultatet istället producerar en ny Stream som kan användas för nästa steg i kedjan.

Delsteg som producerar nya Stream:s kallas för intermediate operations och det delsteg som producerar det slutgiltiga resultatet kallas för terminal operation. En sådan kedja av noll till många intermediate operations och en terminal operation kallas för en stream pipeline. En sådan måste alltid starta med en källström. Hur får man då tag på en sådan? Jo...

Hur får man tag på en Stream?

Det finns några olika sätt att få tag på en Stream som man kan börja arbeta med. Alla (som jag vet om) bygger på den nya funktionalitet med defaultmetoder och statiska metoder på interface som lagts till i Java 8.

För det första så innehåller interfacet Stream några statiska metoder som returnerar ett objekt av typen Stream. Exempel på sådana är empty() som (hör och häpna) returnerar en tom ström, of(T...) som genererar en ström som emitterar elementen (av typen T) i en array och of(T) som skapar en Stream innehållande endast ett enskilt element.

Det finns även statiska metoder för att generera oändliga strömmar, strömmar baserat på Supplier:s och StreamBuilder:s som i sin tur kan generera Stream:s. Mer om dessa senare i avsnittet och serien.

Det andra och antagligen det vanligaste sättet att få tag på en Stream är genom att använda den nya defaultmetoden stream() på interfacet Collection. Den generar en ström över elementen i kollektionen, vilket är mycket användbart som vi kommer att se senare.

Det finns även andra sätt att få tag på Stream:s via JDK API:et, t.ex. innehåller java.nio.file.Files metoder för att returnera sådana för att bl.a. lista filer i kataloger och innehållet i filer, men jag gissar på att sätten beskrivna ovan är de som kommer att användas mest frekvent.

Vad kan man göra med en Stream?

En massa roliga saker. De flesta av dem involverar lambda expressions, men innan vi pratar om sådana måste vi snabbt avhandla vad ett functional interface är för något.

Functional interface och andra funktionella interface

I Java 8 har det tillkommit en rad interface för att modellera olika saker som ofta används vid funktionell programmering i Java. Det här avsnittet går igenom några av dem, så att vi har en stabil grund när vi senare pratar om lambda expressions och de roliga sakerna man kan göra med Stream:s.

Functional interface

Ett functional interface är ett interface med exakt en abstrakt metod. Interfacet får innehålla noll till många defaultmetoder och noll till många statiska metoder, men det måste alltså innehålla precis en abstrakt metod.

Ett funktionellt interface kan annoteras med @FunctionalInterface. Detta är dock inte obligatoriskt, utan tjänar mer som ett uttryck för intentionen med interfacet.

Exempel på ett funktionellt interface:


@FunctionalInterface
public interface AFunctionalInterface {
  void theOnlyAbstractMethod(String s);
}

Om man skapar ett interface som innehåller fler än en abstrakt metod och annoterar detta med @FunctionalInterface så kommer kompilatorn att klaga. Försöker man t.ex. kompilera följande interface:


@FunctionalInterface
public interface AnotherFunctionalInterface {
  void anAbstractMethod(String s);
  void anotherAbstractMethod(String s);
}
så ger javac ifrån sig följande felmeddelande:

lars@larsLilla:~/text/blogg/140719$ javac AnotherFunctionalInterface.java 
AnotherFunctionalInterface.java:1: error: Unexpected @FunctionalInterface annotation
@FunctionalInterface
^
  AnotherFunctionalInterface is not a functional interface
    multiple non-overriding abstract methods found in interface AnotherFunctionalInterface
1 error

Vad är då poängen med funktioella interface? Jo, om en metod tar en inparameter som är av en typ som är ett funktionellt interface kan vi (mha lite JDK-magi) skicka in ett lambda expression, en method reference eller en constructor reference som värde för parametern. Mer info om lambda expressions kommer nedan och mer info om de övriga två kommer senare i serien. Vi kan även skicka in en instans av en anonym inre klass, men det har vi ju kunnat sedan Java 1.1 så det är ju inte direkt några nyheter.

Predicate

java.util.function.Predicate är ett funktionellt interface för att modellera ett (surprise!) predikat, dvs en funktion som tar in ett argument av typen T, avgör om argumentet uppfyller något eller några kriteria och returnerar sant eller falskt baserat på om kriterierna är uppfyllda eller inte.

Den abstrakta metoden i Predicate heter test(). Dessutom finns det default-metoder and, or och not för att utföre booleska operationer på ett Predicate. Utöver dessa finns också en statisk metod isEqual() för att bestämma om två instanser av Predicate är lika eller inte.

Det finns flera specialliseringar av Predicate för olika värden av typparametern T i JDK:n. Dessa är bland annat DoublePredicate, IntPredicate och LongPredicate.

Predicate-interfacet används flitigt i funktionell programmering i Java 8. Bland annat tar metoden filter(Predicate) i Stream ett argument av den typen. Detta argument används för att bestämma om ett element skall filtreras bort från strömmen eller inte.

Exempel:

List<String> nobelPrizeWinners = Arrays.asList("Einstein", "Feynman", "Bohr");
 Predicate<String> startsWithBPredicate = new Predicate<String>() {
   public boolean test(String s) {
     return s.startsWith("B");
   }
 };
 Object[] nobelPrizeWinnerStartingWithB = nobelPrizeWinners.stream().filter(startsWithBPredicate).toArray();
 System.out.println(Arrays.toString(nobelPrizeWinnerStartingWithB));
Vid körning ger programmet ovan följande utskrift:
[Bohr]

Supplier

java.util.function.Supplier representerar en producent av värden av typen T. Supplier är ett funktionellt interface och har den abstrakta metoden get() som alltså har T som returtyp.

Det finns specialiseringar av Supplier för flera olika värden av typparametern T: BooleanSupplier, DoubleSupplier, IntSupplier och LongSupplier.

Supplier saknar defaultmetoder och statiska metoder.

Supplier används bland annat för att skapa oändliga strömmar.

Consumer

java.util.function.Consumer är ett funktionellt interface för att modellera en konsument av värden. Det innehåller den abstrakta metoden accept(T) som skall konsumera ett värde och typen T. Metoden returnerar inte något utan förväntas ha sidoeffekter.

Consumer har en defaultmetod andThen(Consumer after) som kan användas för att skapa en kedja av konsumenter som skall anropas i sekvens.

Precis som med Predicate och Supplier finns ett antal specialliseringar av Consumer i JDK:n.

Consumer används bland annat som parametertyp till metoden forEach() i Stream. På detta sätt kan man skapa ett objekt som anropas en gång för varje element i en ström och då får elementet som argument.

Exempel:

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);
Vid körning ger programmet utskriften:
Einstein
Feynman
Bohr

Function

java.util.function.Function är ett funktionellt interface för att transformera ett värde av typen T till ett värde av typen R.

Function har en defaultmetod: compose(Function before) som används för att skapa kedjor av transformationer. Det har även en statisk metod

Även Function har specialliseringar för olika primitiva värden av typparametern T.

i JDK:n används Function bland annat som parametertyp till metoden map i Stream. Det map() gör är att anropa Function-argumentet en gång för varje element i strömmen och på så sätt transformera dem på något visst givet sätt.

Exempel:

List<String> nobelPrizeWinners = Arrays.asList("Einstein", "Feynman", "Bohr");
 Function capitalizeFunction = new Function() {
      public String apply(String s) {
        return s.toUpperCase();
      }
    };
 Object[] capitalizedNobelPrizeWinners = nobelPrizeWinners.stream().map(capitalizeFunction).toArray();
 System.out.println(Arrays.toString(capitalizedNobelPrizeWinners));
Man får följande utskrift när man kör programmet:

[EINSTEIN, FEYNMAN, BOHR]

En praktiskt klass

När jag ändå är igång så är det lika bra att beskriva en klass som används på ett flertal ställen i de delar av Java 8 API:t som berör funktionell programmering.

Optional

java.util.Optional är en klass som representerar ett värde som eventuellt finns. Den innehåller en metod boolean isPresent() som kan användas för att ta reda på om en instans innehåller ett värde eller inte. Om ett värde finns kan det hämtas med metoden T get().

Det finns fler metoder i Optional än bara isPresent() och get(). Bland annat finns det metoder för att skapa en tom instans (inte innehållande ett värde), för att applicera en Function på ett värde (om det finns) och för att applicera ett PredicateOptional-instansen och därigenom få en ny Optional-instans.

Det finns även en trevlig metod T orElse(T other) som returnerar Optional-instansens värde om något sådant existerar och other om det inte existerar.

Optional används som sagt på ett antal olika ställen i API:t. Bland annat returnerar metoden findFirst() i Stream ett Optional-objekt.

Exempel:

List<String> nobelPrizeWinners = Arrays.asList("Einstein", "Feynman", "Bohr");
 Optional firstWinner = nobelPrizeWinners.stream().findFirst();
 if (firstWinner.isPresent()) {
   System.out.println("First winner: " + firstWinner.get()); 
 }
Utskriften vid körning av programmet blir:
First winner: Einstein

Lambda expressions

Vad är det för något?

Ett lambda expression kan ovetenskapligt beskrivas som en kodsnutt som kan sparas i en variabel, skickas som parameter till en metod eller returneras som returvärde från en metod. Lambda-uttrycket implementerar den abstrakta metoden i något funktionellt interface. Vilket interface som implementeras ges av kontexten dvs typen på variabeln som lambda-uttrycket sparas i, typen på parametern till metoden som lambda-uttrycket skickas till eller returtypen på metoden som lambdauttrycket returneras från. Antalet parametrar samt deras typer måste överrensstämma mellan lambda-uttrycket och det funktionella interface som det implementerar.

Lambda expressions är ingen nyhet i datorvärlden. De har funnits i många andra programeringsspråk länge.

Hur funkar det rent tekniskt?

Java-kompilatorn översätter lambda-uttrycket till en anonym inre klass som implementerar det funktionella interface som det för sig om. Lambdauttrycket kommer i princip att utgöra kroppen på implementationen av den abstrakta metoden i det funktionella interfacet. Jag skrev i princip eftersom Java-kompilatorn lägger till lite saker för oss i implementationen om dessa utelämnats i lambdauttrycket, vilket gör att vi kan skriva lite kortare och enklare kod.

Vad är det bra för?

Syntaxen för att skapa ett lambda expression är betydligt mer kompakt än den för att skapa en anonym inre klass. När man vant sig med den är den mycket mer läsbar och ger kortare och, enligt min mening, trevligare kod.

Hur ser ett lambda expression ut?

Det beror på hur många parametrar det skall ha och om det ryms på en rad eller inte.

OK. Hur ser ett lambda expression som inte tar några parametrar ut då?

Exempelvis så här:

() -> 42

Ovanstående lambda expression är en implementation av ett funktionellt interface vars abstrakta metod inte tar några parametrar och som returnerar ett heltal. Notera att de tomma paranteserna måste vara med. Notera också att ett return-statement inte får vara med. Ett sådant lägger Java-kompilatorn till själv.

Och ett lambda expression som tar en parameter?

Så här:

(final String stringToPrint) -> System.out.println(stringToPrint)

Lambdauttrycket ovan är alltså en implementation av ett funktionellt interface där den abstrakta metoden tar in en String-parameter och inte returnerar något.

Kan man förenkla det (hint, hint...)?

Japp. Om det är tydligt (vilket det ofta är) vilken typ som parametern har så kan javac själv lista ut detta och i sådana fall behövs typen inte tas med i lambdauttrycket. Exemplet ovan kan alltså förenklas till:

(stringToPrint) -> System.out.println(stringToPrint)

De där paranteserna runt parametern verkar onödiga...

Det är de. Om lambdauttrycket bara tar en parameter och dess typ inte är explicit angiven, dvs javac får lista ut den själv, behövs inte paranterserna runt parametern vara med. Eftersom så är fallet i exemplet ovan kan det förenklas som följer:

stringToPrint -> System.out.println(stringToPrint)

Om man vill returnera ett värde från ett en-parameters lambdauttryck då?

Värdet av uttrycket som står efter -> i lambdauttrycket kommer att returneras. Precis som för lambdauttryck utan parametrar så skall nyckelordet return inte vara med explicit. Detta lägger Java-kompilatorn till själv.

Nog om en-parameters lambdauttryck. Hur ser ett lambda expression som tar fler än en parameter ut?

Precis som ett lambda expression med en parameter, fast man skapar en kommaseparerad parameterlista instället för att bara ha en parameter innanför paranteserna.

(final String stringToPrint, final String anotherStringToPrint) -> System.out.println(stringToPrint + " " + anotherStringToPrint)

Om det inte är oklart vilka parametrarnas typer är kan dessa utelämnas precis som för lambdauttryck med en parameter.

(stringToPrint, anotherStringToPrint) -> System.out.println(stringToPrint + " " + anotherStringToPrint)

Det är dock inte tillåtet att ta bort paranteserna runt parameterlistan. Följande lambdautryck kompilerar alltså inte:

stringToPrint, anotherStringToPrint -> System.out.println(stringToPrint + " " + anotherStringToPrint)

Och lambdauttryck som spänner över flera rader?

Vill man skapa sådana så är det bara att innesluta lambdauttrycket i { och }.

(stringToPrint, anotherStringToPrint) -> {
      System.out.println(stringToPrint);
      System.out.println(anotherStringToPrint); 
}

En skillnad mellan enraders lambdauttryck och lambdauttryck som spänner över flera rader är att när det gäller multiraders lambdauttryck så måste return anges explicit om lambdauttrycket skall returnera något värde.

Hur kan ett lambdauttryck sparas i en variabel?

För att spara ett lambdauttryck i en variabel skapar man bara en variabel av det funktionella interface man vill att lambdauttrycket skall implementera och assignar lambdauttrycket till den.

Consumer<String> consumer = stringToPrint -> System.out.println(stringToPrint);

Hur kan ett lambdauttryck skickas med som parameter till en metod?

Genom att (surprise!) ersätta parametervärdet med lambdauttrycket.

Har man t.ex. följande metod:

private static void testSupplier(Supplier<Integer> s) {
  System.out.println("Supplier: " + s.get()); 
}
Kan denna anropas med ett lambdauttryck () -> 42 på följande sätt:
testSupplier(() -> 42);

Hur kan ett lambdauttryck returneras från en metod?

Man sätter bara return framför lambdauttrycket som skall returneras:

  private static Supplier<Integer> createSupplier() {
    return () -> 42;
  }

Closures - lexical scoping

Ett closure är ett lambda expression som accessar ett värde från omgivningen där det skapades. Ett exempel:

private static Consumer<String> createGreetingLambda(final String extraGreeting) {
  return greeting -> System.out.println(extraGreeting + " " + greeting); 
}
I ovanstående metod skapas och returneras ett lambdauttryck som skriver ut en hälsning på konsolen. Texten som skrivs ut består dels av den text som skickas som parameter till uttrycket och dels av det värde som parametern till metoden som skapar lambdauttrycket har. Det är alltså det senare som gör lambda uttrycket till ett closure. Att lambdauttrycket accessar ett värde från omgivningen från vilken det skapades gör att man säger att uttrycket har lexical scoping.

Vad är då closures bra för? De är bra för att skapa varianter av lambdauttryck där variationen skall kunna styras programmatiskt baserat på ett eller flera variabelvärden. Det blir alltså två nivåer på parametrisering: en nivå där lambdauttrycket skapas och en nivå när det anropas för att användas.

Ovanstående metod kan till exempel användas för att skapa lambda expressions som skriver ut olika typer av hälsningar på skärmen. Man kan använda den för att skapa ett lambdauttryck som genererar hälsningar på formen "Hej, namn " där namn skickas som parameter till uttrycket.

Consumer<String> greetingClosure = createGreetingLambda("Hej, ");
greetingClosure.accept("Lars");
...
Hej, Lars     
greetingClosure.accept("Mats");
...
Hej, Mats     
Man kan också använda metoden för att skapa lambda uttryck som producerar hälsningar av typen "Var hälsad namn ".
Consumer<String> greetingClosure2 = createGreetingLambda("Var hälsad, ");
greetingClosure2.accept("Kalle Anka");
...
Var hälsad, Kalle Anka
greetingClosure2.accept("du store indian!");
...
Var hälsad, du store indian!

Det här är så klart ett leksaksexempel, men det illustrerar hur man kan skapa återanvändningsbara lambdauttryck som är varianter på samma "grund-lambdauttryck".

Vad finns det då för restriktioner på hur closures kan skapas? Jo, variablerna som används i closure:t måste vara final eller effectively final, vilket innebär att de inte ändrats efter det att de initialiserats. Om lokala variabler används måste de även ha initialiseras innan de används i closure:t.

Avslutning del 2

Den här delen har lagt grundstenarna för att kunna använda funktionell programmering i Java. Det har gått igenom Stream:s som ärpunkten varifrån man startar när funktionell programmering skall användas samt lambdauttryck som utnyttjas för att parametrisera de olika funktionella anropen man vill göra. Det har även behandlat ett antal fördefinierade funktionella interface som finns med i API:t.

Inlägget blev bra mycket längre än jag hade planerat, men så blir det lätt när man skriver om roliga saker ;)

Nästa del i serien om nyheter i Java 8 kommer att handla om hur man kan använda Stream:s och lambdauttryck för att programmera funktionellt i Java.

Tidigare delar i serien

Del 1: Interface

Referenser

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

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

Nyheter i Java 8 - del 5 - parallella strömmar

2014-09-09

Detta är den femte delen i miniserien om nyheterna i Java 8. De tidigare delarna har bland annat handlat om de nya sakerna som finns kan finnas i interface samt om strömmar och lambdauttryck och vad man kan göra med dem.

Den här delen skall handla om hur man kan parallellisera exekveringen av vissa typer av program genom att använda parallella strömmar.

Parallella strömmar

Naiv metod för att bestämma om ett tal är ett primtal eller inte

Som ett exempel på hur man kan parallellisera exekveringen av vissa operationer som utförs på strömmar skall vi kolla på hur man kan hitta alla primtal mellan 1 och 10 miljoner. Vi börjar med att skriva en liten, mycket naiv, metod för att bestämma om ett tal är ett primtal eller inte. Notera som sagt att den är mycket naiv. Om någon skulle anklaga mig för att vara upphovsmannen till den skulle jag förneka detta kategoriskt ;) Men den duger bra för att exemplifiera konceptet med parallell exekvering av strömmar.

// Most inefficient algorithm.
private static boolean isPrime(int n) {
  if (n == 2) {
    return true;
  } else if (n <= 1 || n % 2 == 0) {
    return false;
  }

  for (int possibleDivisor = 3; possibleDivisor <= Math.ceil(Math.sqrt(n)); possibleDivisor += 2) {
    if (n % possibleDivisor == 0) {
      return false;
    }
  }

  return true;
}

Beräkna primtal sekventiellt

Vi kan nu använda den statiska metoden rangeClosed() i klassen IntStream (som vi ju pratade om i del 3) för att generera en ström som innehåller en uppsättning med heltal. Vi är intresserade av heltalen mellan 1 och 10 miljoner, så för att generera en ström som innehåller dessa tal kan vi göra så här:

IntStream.rangeClosed(1, 10000000)

För att extrahera ut de av talen som är primtal kan vi sen applicera metoden filter() på strömmern och använda en metodreferens till vår naiva primtalkollarmetod (som finns i klassen MainClass):

IntStream.rangeClosed(1, 10000000).filter(MainClass::isPrime)

Vill vi sedan samla upp primtalen för vidare bearbetning kan vi använda collect(). Vi passar även på att lägga en utskrft på hur lång tid exekveringen tar, så vi har något att jämföra med senare. Med dessa ändringar ser vårt lilla program ut så här:

long startTime = System.currentTimeMillis();
List<Integer> primes = IntStream.rangeClosed(1, 10000000).filter(MainClass::isPrime).collect(ArrayList::new, ArrayList::add, ArrayList::addAll);
primes.stream().forEach(System.out::println);  
long endTime = System.currentTimeMillis();
System.out.println("Execution took: " + (endTime - startTime) + "ms"); 

I exemplet så har "den första" varianten av collect() använts. Detta eftersom det är den enda som finns i klassen IntStream.

Om man kör ovanstående program på min, väldigt långsamma, "bussdator" ser det ut så här:

lars@larsLilla:~/text/blogg/140903$ java -classpath . MainClass
Execution took: 53197ms
lars@larsLilla:~/text/blogg/140903$ 

Beräkna primtal parallellt

Hur gör vi nu om vi vill parallellisera exekveringen av primtalssökningen? Ja, först kan vi konstatera att metoden som kollar om ett tal är ett primtal eller inte är tillståndslös. Det är ju bra, då har den ju inga konstiga sidoeffekter som vi behöver ta hänsyn till. Dock modifierar ju metoden filter() strömmen. Det är ju hela poängen med den metoden. Så hur hanterar vi detta om vi nu vill parallellisera exekveringen?

Tittar man på API-dokumentationen av IntStream så ser man att den har en metod parallel() som det står "Returns an equivalent stream that is parallel.". Lysande! Vi provar:

long startTime = System.currentTimeMillis();
List<Integer> primes = IntStream.rangeClosed(1, 10000000).parallel().filter(MainClass::isPrime).collect(ArrayList::new, ArrayList::add, ArrayList::addAll);
long endTime = System.currentTimeMillis();
System.out.println("Execution took: " + (endTime - startTime) + "ms"); 
...
Execution took: 44177ms

Det var allt vi behövde göra för att parallellisera exekveringen av primtalssökningen. Inte en så stor ändring, eller hur?

Nu var i och för sig minskningen av exekveringstid inte gigantisk. Den blev ungefär 17% kortare, men detta kan nog till viss del bero på att den lilla, klena dator jag använder när jag sitter på bussen och kodar och skriver (som jag gör nu) bara har två kärnor. Kör man exempelprogrammet på den dator jag använder på jobbet blir tiderna istället 3,7s respektive 1,2s. I detta fall går alltså det parallella programmet tre gånger så snabbt som det sekventiella. Inte dåligt!

Man kan också konstatera att jag skulle behöva en ny bussdator.

Andra parallella strömmar

Så vad finns det då för stöd i API:t för att parallellisera exekveringen av operationer på strömmar? Vi har ju redan sett metoden parallel() i IntStream. Egentligen är det så att parallel() finns i interfacet BaseStream som förutom IntStream också implementeras av DoubleStream och LongStream. Men om det inte är en numerisk ström man vill använda då? Det visar sig att klassen Collection förutom att ha en metod stream() också har en metod som heter parallelStream(). Super! Istället för att använda stream() och få en sekventiell ström kan vi använda parallelStream() och få en parallell ström. Vi behöver inte skapa några egna trådar eller trådpoler, bara anropa parallelStream().

Varför inte alltid använda parallella strömmar? - sidoeffekter

Det låter ju nästan alltför bra att vi bara genom att anropa BaseStream.parallel() eller Collection.parallelStream() kan parallellisera alla program som använder strömmar. Finns det inga hakar då? Såklart det gör. Som med all programmering där parallellism är inblandat så får man hålla tungan rätt i mun. Har man lambdauttryck som uppdaterar något gemensamt tillstånd så är det ingen bra ide att använda parallella strömmar. Man får fundera igenom om operationerna man vi ha utförda verkligen går att exekvera parallellt innan man använder BaseStream.parallel() eller Collection.parallelStream().

Som ett exempel på när det inte blir ett korrekt resultat om parallella strömmar används skall vi skriva ett litet program som givet en lista med heltal ökar varje heltal i listan med en ökande offset (1, 2, ...).

Vi skapar en metod testAddToStream(IntStream stream) som tar en IntStream in som parameter. Den använder sedan map för att addera allt större tal till elementen i strömmen. Vilket tal som skall adderas hålls i en instansvariabel. Detta är ju jättekorkat egentligen eftersom det hade fungerat lika bra med en lokal variabel. Dessutom hade det blivit ett korrekt resultat även för parallella strömmar om så hade skett, men vi gör så bara för att visa ett exempel på hur det kan gå snett med parallella strömmar. Metoden ser i alla fall ut så här:

public class MutableTest
{
  private int offset;
...
  private Set<Integer> testAddToStream(IntStream stream) {
    offset = 0;
    return stream.map(i -> i + offset++).collect(HashSet::new, HashSet::add, HashSet::addAll);
  }
}

Fyller vi sedan på klassen med en metod som anropar testAddToStream() med en sekventiell ström och en metod som anropar den med en parallell ström och skriver ut hur många unika heltal som returneras ser klassen ut så här:

public class MutableTest
{
  private int offset;

  public static void main(String[] args) {
    MutableTest instanceOne = new MutableTest();
    instanceOne.testSequential();

    MutableTest instanceTwo = new MutableTest();
    instanceTwo.testParallel();
  }

  private void testSequential() {
    Set<Integer> uniqueNumbers = testAddToStream(IntStream.rangeClosed(1, 100));
    System.out.println("Number of unique numbers using sequential stream: " + uniqueNumbers.size()); 
  }

  private void testParallel() {
    Set<Integer> uniqueNumbers = testAddToStream(IntStream.rangeClosed(1, 100).parallel());
    System.out.println("Number of unique numbers using parallel stream: " + uniqueNumbers.size()); 
  }

  private Set<Integer> testAddToStream(IntStream stream) {
    offset = 0;
    return stream.map(i -> i + offset++).collect(HashSet::new, HashSet::add, HashSet::addAll);
  }
}
Eftersom vi skapar en ström med 100 heltal förväntar vi oss att utskrifterna skall indikera att testSequential() och testParallel() rapporterar att 100 heltal returnerats från testAddToStream(). Blir det så då?

Nej, inte direkt. Kör man ovanstående klass fås, på min maskin, följande resultat:

ars@larsLilla:~/text/blogg/140903$ java -classpath . MutableTest 
Number of unique numbers using sequential stream: 100
Number of unique numbers using parallel stream: 74
lars@larsLilla:~/text/blogg/140903$ 

Resultatet från olika körningar ger lite olika resultat av testet med den parallella strömmen. 74 och 75 verkar vara de vanligaste resultaten. Inte en enda gång har det blivit 100 i alla fall. Orsaken är givetvis att flera trådar använder samma värde av offset, vilket gör att duplikat uppträder ni resultatströmmen. När denna görs om till ett Set elimineras resultaten och antalet unika heltal i minskar. Inga konstigheter, men ett exempel på hur man inte skall använda parallella strömmar.

Varför inte alltid använda parallella strömmar? - gemensam trådpool

Googlar man lite på parallellism i java 8 så hittar man snabbt flera texter som varnar lite för användandet av parallella strömmar. Se till exempel [3] och [4] nedan. Orsaken till varför de råder en att tänka över användandet av parallella strömmar är att de är implementerade med hjälp av JVM:ens fork-join-pool (fjp). Var är det som är farligt med det då? Jo, tydligen delas JVM:ens fjp mellan alla trådar i hela JVM:en. Till saken hör också att fjp:n är implementerad på så sätt att om en en trådarna i poolen hänger på ett blockerande anrop kommer poolen inte att fyllas på med en nystartad tråd, utan antalet tillgängliga trådar i poolen kommer att minska med en. Sammantaget innebär detta att om flera delar av en applikation använder fjp:n, t.ex. genom att använda parallella strömmar, kommer de att kunna störa varandra om pooltrådarna används till blockerande anrop. Om man använder parallella strömmar har man har alltså inte någon kontroll på hur många trådar, om ens någon, som kommer att exekvera ens lambdauttryck. Allt hänger på hur resten av applikatonen använder JVM:ens fjp.

Skriver man en singeltrådad applikation, eller en multitrådad applikation där endast en tråd använder fjp:n, är detta givetvis inte något problem. Det kan dock vara bra att veta om hur kopplingen mellan parallella strömmar och fjp:n ser ut så man kan undvika att använda dem då sådana här krockar skulle kunna inträffa.

Avslutning del 5

Den här gången har vi gått igenom hur parallella strömmar kan användas. Och när de inte bör användas. Jag tycker att de är ett alldeles utmärkt verktyg för vissa sorters problem. I vissa Project Euler-uppgifter hade de snabbat upp mina lösningar avsevärt (det vill säga om jag skrivit dem i Java och inte exekverat dem på min bussdator). Det är nästan så jag vill skriva om en del av mina lösningar till att använda parallella strömmar för att se hur mycket snabbare jag kan få dem att exekvera.

I nästa del i serien skall vi kolla lite på hur man kan parallellisera vissa operationer på arrayer med metoder som är nytillagda i Java 8.

Tidigare delar i serien

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

Referenser

[1] Subramaniam V, Functional Programming in Java, första utgåvan, The Pragmatic Programmers, 2014.
[2] The Java Tutorial - Parallelism
[3] Think twice before using Java 8 parallel streams
[4] Java Parallel Streams Are Bad for Your Health!

Nyheter i Java 8 - del 6 - nyheter i klassen Arrays

2014-09-24

Nu är det dags för den sjätte delen i miniserien om nyheterna i Java 8. Förra delen handlade om parallell hantering av strömmar. Den här delen skall handla om de nya metoderna i klassen Arrays.

Nyheter i Arrays

I Java 8 kom det en hel del nyheter i klassen Arrays. Några av dessa underlättar parallell exekvering av operationer på arrayer. Vi skall kolla närmare på dessa nu. När vi ändå håller på, så passar vi på att ta en kik på de nya metoder som inte har med parallellism att göra också.

I korthet kan man säga att de nya metoderna i Arrays är följande:

  • <T extends Comparable<? super T>> void parallelSort(T[] a)
  • <T> void parallelPrefix(T[] array, BinaryOperator<T> op)
  • <T> void setAll(T[] array, IntFunction<? extends T> generator)
  • <T> void parallelSetAll(T[] array, IntFunction<? extends T> generator)
  • <T> Spliterator spliterator(T[] array)
  • <T> Stream stream(T[] array)
Många av metoderna finns även i överlagrade versioner som hanterar arrayer av de primitiva typerna. Det finns i många fall också överlagrade versioner som tar ett startindex och ett slutindex av arrayen och då bara opererar på den angivna delen av arrayen. I genomgången nedan kommer det att noteras vilka överlagrade varianter av respektive metod som finns.

parallelSort()

Vad gör metoden

parallelSort() är, inte helt oväntat, en parallell version av Arrays.sort(). Dvs den sorterar den inkommande arrayen "in place".

Notera att parallelSort() använder JVM:ens fork-join-pool, med de nackdelerar detta innebär. Se diskussionen angående fork-join-poolen i förra delen av serien, då avseende dess användande i samband med parallella strömmar.

Exempel

Som ett litet exempel på den variant av parallelSort() som tar en int[] som inparameter kan vi använda följande lilla klass:

import java.util.Arrays;
import java.util.Random;
import java.util.stream.IntStream;

public class MainClass {
  private final static int ARRAY_SIZE = 10;
  public static void main(String[] args) {
    // Generate array of intetegers.
    Random random = new Random();
    IntStream intStream = random.ints(ARRAY_SIZE);
    int[] randomNumbers = intStream.toArray();

    // Sort it an measure how long it takes.
    System.out.println("Before: " + Arrays.toString(randomNumbers)); 
    long startTime = System.currentTimeMillis();        
    Arrays.parallelSort(randomNumbers);
    long endTime = System.currentTimeMillis();    
    System.out.println("Execution time: " + (endTime - startTime) + "ms"); 
    System.out.println("After: " + Arrays.toString(randomNumbers));
  }
}

Exemplet ovan använder den nya metoden ints(int size) i Random för att generera en IntStream innehållande size slumptal. Sedan görs talen i IntStream:en om till en array med hjälp av metoden IntStream.toArray(). Ganska smidigt sätt att generera en array med slumptal eller hur?

För att göra det tydligt i vilken klass som ints(int) och toArray() fanns användes lokala variabler spara returvärdena från dessa metoder. Undviker man det blir genereringen av arrayen med slumptal mycket kort och koncis:

int[] randomNumbers = new Random().ints(ARRAY_SIZE).toArray();

Resultatet av en körning av ovanstående program kan se ut så här:

Before: [2128764741, 1109516321, -1918587090, -1349245711, 194244467, 2121767928, -434729300, -830024849, -14951130, 369065649]
Execution time: 1ms
After: [-1918587090, -1349245711, -830024849, -434729300, -14951130, 194244467, 369065649, 1109516321, 2121767928, 2128764741]

Nu är det ju ingen större bedrift att sortera tio stycken heltal. Vad vi egentligen vill testa är om Arrays.parallelSort() exekverar snabbare än Arrays.sort() då indatat är mycket större än så. För att prova det så sätter vi istället ARRAY_SIZE till 100 miljoner och kör igen. (Fast vi passar på att kommentera bort utskriften av arrayen före och efter...) Resultatet blir:

Execution time: 33707ms

Byter vi ut Arrays.parallelSort() mot Arrays.sort() och kör igen blir resultatet:

Execution time: 32719ms
Det var inte riktigt det resultat vi ville ha ;) Nu exekverades dock programmet på min ytterst slöa bussdator. Kör vi istället testerna på min jobbdator blir tiderna 10671ms respektive 4184ms. Nu ser det mer ut som förväntat.

Överlagrade varianter

parallelSort() finns i följande överlagrade varianter:

  • <T extends Comparable<? super T>> void parallelSort(T[] a)
  • <T extends Comparable<? super T>> void parallelSort(T[] a, int fromIndex, int toIndex)
  • <T> void parallelSort(T[] a, Comparator<? super T> cmp)
  • <T> void parallelSort(T[] a, int fromIndex, int toIndex, Comparator<? super T> cmp)
Dessutom finns det överlagrade versioner av parallelSort() för byte[], short[], int[], long[], float[], double[] och char[] i två varianter: dels den varianten som bara tar in själva arrayen och dels i den varianten som även tar in ett startindex och ett slutindex.

parallelPrefix()

Vad gör metoden

parallelPrefix() är en metod som går igenom indataarrayen element för element och applicerar en BinaryOperator på varje element. Inparametrar till anropen till BinaryOperator är elementet i fråga samt närmast föregående element.

I engelskan verkar en sådan operation beskrivas som att man cumulates varje element genom att applicera en funktion. Jag har inte hittat något bra svenskt ord för det. Är det någon som har koll på vad det heter på svenska så får ni gärna tipsa mig. Det är alltid roligt att utöka sitt ordförråd :)

parallelPrefix() arbetar, inte helt överraskande givet dess namn, parallellt så den BinaryOperator som man anger till metoden bör vara sidoeffektsfri.

Exempel

Som ett exempel på parallelPrefix() skall vi skriva ett litet program som adderar varje element i en int-array med närmast föregående element. Programmet ser ut så här:

import java.util.Arrays;

public class Prefix {
  public static void main(String[] args) {
    int[] array = new int[]{1,2,3,4,5,6,7,8,9};
    Arrays.parallelPrefix(array, (a, b) -> a + b);
    System.out.println(Arrays.toString(array));
  }
}
...
[1, 3, 6, 10, 15, 21, 28, 36, 45]

Vi har här använt lambdauttrycket (a, b) -> a + b som BinaryOperator.

Överlagrade varianter

parallelSort() finns i följande överlagrade varianter:

  • <T> void parallelPrefix(T[] array, BinaryOperator<T> op)
  • <T> void parallelPrefix(T[] array, int fromIndex, int toIndex, BinaryOperator<T> op)
Utöver dessa finns det versioner av för double[], int[] och long[] i både den varianten som enbart tar en array som inparameter och den varianten som utöver arrayen även tar in start- och slutindex.

setAll()

Vad gör metoden

setAll() initialiserar alla elementen i en array till värden som ges av en generatorfunktion. Generatorfunktionens typ är olika för de olika överlagrade varianterna av setAll(). Den är IntUnaryFunction när det gäller den varianten som operarar på en int[], IntToDouble när det gäller variantern för double[] och så vidare.

För varje element i arrayen som skall initialiseras anropas generatorfunktionen med elementets index som parameter (det är därför generatorfunktionerna har typen IntToSomething). Detta index kan sedan användas, om så önskas, till att bestämma vilket värde som elementet skall få.

Exempel

Ett exemelprogram som iniatialiserar en liten int[] så att varje element får ett värde som är dubbelt så stort som elementets index kan se ut så här:


import java.util.Arrays;

public class SetAll {
  public static void main(String[] args) {
    int[] array = new int[10];
    Arrays.setAll(array, a -> a * 2);
    System.out.println(Arrays.toString(array));
  }
}
...
[0, 2, 4, 6, 8, 10, 12, 14, 16, 18]

Överlagrade varianter

setAll() finns i de här varianterna:

  • <T> void setAll(T[] array, IntFunction<? extends T> generator)
  • void setAll(double[] array, IntToDoubleFunction generator)
  • void setAll(int[] array, IntUnaryOperator generator)
  • void setAll(long[] array, IntToLongFunction generator)

parallelSetAll()

Vad gör metoden

parallelSetAll() gör samma sak som setAll(), dvs den initialiserar en array givet en generatorfunktion, men den gör det parallellt.

Exempel

Motsvarande exempel som ovan använde setAll() ser med parallelSetAll() ut så här:


import java.util.Arrays;

public class ParallelSetAll {
  public static void main(String[] args) {
    int[] array = new int[10];
    Arrays.parallelSetAll(array, a -> a * 2);
    System.out.println(Arrays.toString(array));
  }
}
...
[0, 2, 4, 6, 8, 10, 12, 14, 16, 18]
Vi ser att resultatet blev detsamma som i det föregående exemplet, vilket ju känns bra.

Överlagrade varianter

parallelSetAll() har motsvarande varianter som setAll() det vill säga:

  • <T> void parallelSetAll(T[] array, IntFunction<? extends T> generator)
  • void parallelSetAll(double[] array, IntToDoubleFunction generator)
  • void parallelSetAll(int[] array, IntUnaryOperator generator)
  • void parallelSetAll(long[] array, IntToLongFunction generator)

spliterator()

Vad gör metoden

spliterator() returnerar en Spliterator för arrayen i fråga.

Vad är då en Spliterator? Ja, Spliterator är ett interface som enligt dokumentationen är tillför att, fritt översatt, "traversera och partionera elementen från en källa". Och vad betyder det då? Det betyder att Spliterator liknar Iterator på så sätt att Spliterator innehåller metoder för att stega igenom en uppsättning element på något sätt. Det finns dock två stora skillnader mellan Spliterator och Iterator.

Den första är att istället för att som Iterator ha separata metoder för att kolla om det finns fler element och för att hämta ut nästa element har Spliterator kombinerat dessa till samma metod som då tar en Consumer som appliceras på elementet om det finns något element kvar att applicera den på. Spliterator har två sådana metoder: void tryAdvance(Consumer<? super T> action) och void forEachRemaining(Consumer<? super T> action). Det som skiljer dessa två metoder åt är att tryAdvance() applicerar Consumer:n på nästa element medan forEachRemaining() applicerar den på alla resterande element.

Den andra och konceptuellt viktigaste skillnaden mellan Iterator och Spliterator är att det går att dela upp elementen i grupper och låta separata Spliterator:s hantera varje grupp. Det är detta som dokumentationen menar om när den talar om att partionera elementen. Uppdelningen i grupper görs med metoden Spliterator<T> trySplit() som returnerar en ny Spliterator som innehåller ett antal element från Spliterator:n på vilken trySplit() anropades. Notera att elementen efter anropet till trySplit() inte längre finns med i den ursprungliga Spliterator:n. Det är alltså frågan om en flytt av element, inte en kopierring. Notera också att man vid anropet till trySplilt() inte kan påverka vilka eller om ens några element som den Spliterator som returneras innehåller. Det är upp till implementationen av Spliterator:n att bestämma.

Vad är då poängen med att splitta? Den är att man på så sätt får möjlighet att parallellisera itereringen av elementen. Varje grupp kan itereras parallellt.

Den finns ett antal specialiseringar av Spliterator för de primitiva typerna double, int och long. Dessa är implementerade som inre-interface i Spliterator och de heter Spliterator.OfDouble, Spliterator.OfInt respektive Spliterator.OfLong.

Exempel

Det första exemplet visar hur forEachRemaining() kan användas för att behandla alla element i en array. I exemplet innebär hanteringen bara att elementet skrivs ut, men i verkliga livet kan givetvis valfria operationer göras i lambdauttrycket som ges som argument till forEachRemaining().

int[] oneToNine = new int[] {1,2,3,4,5,6,7,8,9};
Spliterator.OfInt spliterator = Arrays.spliterator(oneToNine);
spliterator.forEachRemaining((int i) -> System.out.println(i));
...
1
2
3
4
5
6
7
8
9

Det andra exemplet skriver först ut några av de inledande elementer mha tryAdvance() för att sedan anropa forEachRemaining() för att skriva ut resten.

System.out.println("Now we will run tryAdvance() a few times:"); 
int[] tenToNineteen = new int[] {10,11,12,13,14,15,16,17,18,19};
Spliterator.OfInt spliterator2 = Arrays.spliterator(tenToNineteen);
spliterator2.tryAdvance((int i) -> System.out.println(i));
spliterator2.tryAdvance((int i) -> System.out.println(i));
spliterator2.tryAdvance((int i) -> System.out.println(i));
spliterator2.tryAdvance((int i) -> System.out.println(i));
spliterator2.tryAdvance((int i) -> System.out.println(i));
System.out.println("-----------------------------"); 
System.out.println("Now we will run forEachRemaining():"); 
spliterator2.forEachRemaining((int i) -> System.out.println(i));

...
Now we will run tryAdvance() a few times:
10
11
12
13
14
-----------------------------
Now we will run forEachRemaining():
15
16
17
18
19

I det sista exemplet så splittar vi en Spliterator i två delar och behandlar sen varje Spliterator var för sig.

int[] twentyToTwentynine = new int[] {20,21,22,23,24,25,26,27,28,29};
Spliterator.OfInt firstSpliterator = Arrays.spliterator(twentyToTwentynine);
Spliterator.OfInt secondSpliterator = firstSpliterator.trySplit();
System.out.println("First spliterator contains these numbers: "); 
firstSpliterator.forEachRemaining((int i) -> System.out.println(i));
System.out.println("Second spliterator contains these numbers: "); 
secondSpliterator.forEachRemaining((int i) -> System.out.println(i));
...
First spliterator contains these numbers: 
25
26
27
28
29
Second spliterator contains these numbers: 
20
21
22
23
24
Vi ser att trySplit() har delat elementen så att de lägsta talen splittas av till den andra Spliterator:n medan de högsta talen blir kvar i den ursprungliga. Detta är dock inget vi direkt kan påverka.

Överlagrade varianter

Följande varianter finns av Spliterator:

  • <T> Spliterator<T> spliterator(T[] array)
  • <T> Spliterator<T> spliterator(T[] array, int startInclusive, int endExclusive)
  • Spliterator.OfDouble spliterator(double[] array)
  • Spliterator.OfDouble spliterator(double[] array, int startInclusive, int endExclusive)
  • Spliterator.OfInt spliterator(int[] array)
  • Spliterator.OfInt spliterator(int[] array, int startInclusive, int endExclusive)
  • Spliterator.OfLong spliterator(long[] array)
  • Spliterator.OfLong spliterator(long[] array, int startInclusive, int endExclusive)

stream()

Vad gör metoden

Metoden stream() returnerar en ström som backas av elementen i arrayen som ges som parameter till metoden. Den ger oss alltså möjlighet att på ett lätt sätt gå från en (gammal, tråkig) array till en (ny och skinande) Stream och därigenom kunna utnyttja alla de nya och roliga saker man kan göra med en sådan ;)

Exempel

Vi har ju använt Stream:s in rad exempel tidigare, så följande exempel kommer inte att vara så spännande. Det visar mest på hur man kan komma över till en Strean från en array. Vad vi sedan kan göra med strömmen i fråga har vi ju avhandlat i flera av de tidigare delearna i serien. I exemplet skriver vi bara ut elementen i strömmen till standard out.

int[] oneTonNine = new int[] {2,4,6,8};
IntStream stream = Arrays.stream(oneTonNine);
stream.forEach(System.out::println);
...
2
4
6
8

Överlagrade varianter

De här överlagrade versionerna finns av stream():
  • <T> Stream<T> stream(T[] array)
  • <T> Stream<T> stream(T[] array, int startInclusive, int endExclusive)
  • DoubleStream stream(double[] array)
  • DoubleStream stream(double[] array, int startInclusive, int endExclusive)
  • IntStream stream(int[] array)
  • IntStream stream(int[] array, int startInclusive, int endExclusive)
  • LongStream stream(long[] array)
  • LongStream stream(long[] array, int startInclusive, int endExclusive)

Avslutning del 6

I den här delen har vi gått igenom de metoder i Arrays som är nya i Java 8. De flesta metoder har något med parallellism att göra. Om jag får våga mig på en gissning så kommer stream() att vara den av de nya metoderna som man får mest nytta av. Med hjälp av den kan man behandla en arrays element som en ström och då få tillgång till allt godis som man kan göra med strömmar i Java 8.

En annan metod som jag kan tänka mig att man kan få nytta av är parallelSort(). Sortera gör man ju titt som tätt, så den kommer man säkert kunna använda.

Skall jag vara helt ärlig så är jag mer tveksam till hur ofta man kommer att använda de andra nya metoderna i Arrays. De känns inte lika användbara som de metoder jag nyss nämnde. Fast det är klart, kan spliterator() kombineras ihop på något bra sätt med ett ramverk som erbjuder parallell exekvering så kanske det blir användbart.

I nästa del, vilket antagligen blir den sista delen i serien, skall vi kolla lite översiktligt på övriga, icke-parallellism-relaterade nyheter i Java 8.

Tidigare delar i serien

Del 1: Interface
Del 2: Streams och lambda expressions
Del 3: Metoder i Stream, metod- och konstruktorreferenser
Del 4: Metoder klassen Collectors
Del 5: Parallella strömmar

Referenser

[1] API-dokumentationen för Arrays

Nyheter i Java 8 - del 7 - Lösa trådar och uppsummering

2014-12-18

Efter ett längre uppehåll är det nu dags för den sjunde och sista delen i miniserien om nyheterna i Java 8. Förra delen handlade om nyheter i klassen Arrays. Den här delen listar lite av de nyheter i Java 8 som inte tidigare tagits upp i serien samt summerar mina intryck av Java 8 och hur det har varit att skriva den här serien av blogginlägg.

Fler nyheter i Java 8

Vi har ju gått igenom en massa nyheter i Java 8. Streams, lambdauttryck med mera. Vad finns det då ytterligare för nyheter i Java 8? Om man läser på Oracles "officiella" sida What's New in JDK 8 så ser man att det är en hel del nyhter som vi inte tagit upp (det är dock roligt att se att det är rätt mycket som vi har tagit upp också). En del av de saker vi inte tagit upp är följande:

  • Förändringar i annoteringsramverket.
  • Förbättringar när det gäller säkerhetsrelaterade saker.
  • Förbättringar av JavaFX.
  • Nya verktyg (jjs) tillagda och förbättringar är gjorda på redan befintliga.
  • Nya Date - Time-paket
  • Ny JavaScript-motor: Nashorn
  • JDBC 4.2

Flera av sakerna ovan låter intressanta att kolla på. Speciellt Nashorn borde man ju ta sig en närmare titt på. Får bli nästa år. Så mycket att göra och så lite tid... ;)

Litet Java 8-nyheter-letar-program

För skojs skull gjorde jag ett litet Java-program som kravlar igenom API-dokumentationen för Java 8 och letar efter alla klasser som innehåller "since 1.8" (se nedan) och skriver ut namnen på dessa (egentligen den fulla path:en till javadoc-html-filen) på std out. Det hittade över 300 klasser. Det är alltså en hel del klasser som är nya eller har fått nya metoder/fält i 1.8.

Angående programmet så kompliceras det lite grand av att "Since" och "1.8" inte finns på samma rad i API-dokumentationen. Programmet måste därför undersöka två rader i taget tills det antingen får en träff eller kommer till slutet på filen. I övrigt är det ett rätt okomplicerat program som startar i en startkatalog, undersöker html-filerna i den katalogen och sedan fortsätter att undesöka filerna i dess underkataloger rekursivt. Jag har inte försökt optimera programmet på något sätt, så det finns (som vanligt ;) säkert rum för förbättringar.

Programmet använder som ni set Java 8-specifika features som strömmar och lambdauttryck, vilket gör att de delar som innehåller "affärslogiken" blir ganska korta och koncisa. För att jämföra skrev jag ett motsvarande program som inte använder Java 8-features. Det blev ca 15 rader längre. Inte så stor skillnad kanske, men om man ser på programmen så är de delar som verkligen gör något avsevärt kortare i Java 8-varianten. Detta främst på grund av den trevliga metoden Stream.filter() som vi ju pratat om tidigare.

package since18Finder;

import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;

import java.io.File;
import java.io.FileFilter;
import java.io.Reader;
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.FileNotFoundException;
import java.io.IOException;

import java.util.List;
import java.util.ArrayList;
import java.util.Arrays;

public class Since18Crawler
{
  public final static String START_DIR = "/home/lars/text/docs/java/1.8/docs/api/java/";
  public final static String SINCE_1_8_ROW_1 = "Since:";
  public final static String SINCE_1_8_ROW_2 = "1.8";

  public static void main(String[] args) throws IOException
  {
    visitDirectory(Paths.get(START_DIR));
  } 

  private static void visitDirectory(Path path)
  {
    try {      
      checkFilesInDirectory(path);
      visitSubDirectories(path);
    } catch(IOException ioe) {
      ioe.printStackTrace();
    }
  }

  private static void visitSubDirectories(Path path) throws IOException {
    Files.list(path).filter(Files::isDirectory).forEach(f -> visitDirectory(f));
  }
  
  private static void checkFilesInDirectory(Path path) throws IOException {
    Files.list(path).
     filter(p -> Since18Crawler.isFileToLookIn(p)).
     filter(p -> Since18Crawler.containsString(p, SINCE_1_8_ROW_1, SINCE_1_8_ROW_2)).
     forEach(p -> System.out.println(p.toFile().getAbsolutePath()));    
  }

  private static boolean isFileToLookIn(Path path) {
    File f = path.toFile();
    return f.isFile() && f.getName().endsWith(".html") && Character.isUpperCase(f.getName().charAt(0));
  }

  private static boolean containsString(final Path pathToFileToExamine, final String stringToLookForInRow1, 
                                        final String stringToLookForInRow2) {
    try {
      File fileToExamine = pathToFileToExamine.toFile();
      try(final BufferedReader reader = new BufferedReader(new FileReader(fileToExamine))) {
          String line;
          while ((line = reader.readLine()) != null) {
            if (line.contains(stringToLookForInRow1)) {
              if ((line = reader.readLine()) != null && line.contains(stringToLookForInRow2)) {
                return true;
              }
            }
          }          
        }
    } catch (IOException e) {
      e.printStackTrace();
    }
    
    return false;          
  }
}

Sammanfattning av serien

Det har varit mycket lärorikt att skriva ihop den här lilla miniserien. Jag har verkligen lärt mig jättemycket, och jag tror även att jag kommer att komma ihåg sakerna bättre nu när jag skrivit om dem.

Angående själva innehållet i Java 8 så är jag imponerad av de delar som jag undersökt. Statiska- och defaultmetoder gör att det är lättare att utöka interface än vad det varit förrut. Tidigare har jag reflexmässigt nästan alltid skapat en abstrakt klass som implementerat ett interface och lagt denna mellan interfacet och den konkreta implementation. Detta behov minskar nu när man kan lägga till defaultmetoder både då interfacet skapas men även i efterhand.

De nya funktionella konstruktionerna i Java 8 är givetvis den största förändringen. Tycker att Oracle-folket fått till en riktig trevlig implementation av lambdas och closures. Det känns som om det hänger ihop på ett logiskt och bra sätt. Är det något man är osäker på syntaxtmässigt så kan man gissa "hur det borde vara" och ha en ganska hög sannolikhet att det man chansat på är rätt, vilket är ett bra betyg tycker jag.

Det känns även som om man fått in de funktionella förändringarna på alla ställen i JDK:n där de passar in; givetvis i collections-ramverket och Arrays men även i de klasser och paket som hanterar I/O (exempelvis i Files, se koden ovan). Det finns inget ställe som det är uppenbart att de missat att lägga till det funktionella stuffet.

Sammanfattningsvis har jag tyckt att det varit väldigt roligt att skriva den här lilla serien. Ser fram emot att skriva en liknande om Java 9 när nu den kommer ut. Hoppas det inte dröjer för länge!

Tidigare delar i serien

Del 1: Interface
Del 2: Streams och lambda expressions
Del 3: Metoder i Stream, metod- och konstruktorreferenser
Del 4: Metoder klassen Collectors
Del 5: Parallella strömmar
Del 6: Nyheter i Arrays

Referenser

[1] What's New in JDK 8