Ny bokrecension: Two Scoops of Django 1.6
2014-07-03Nu har jag skrivit en liten recension av Two Scoops of Django 1.6.
Link to the review hereNu har jag skrivit en liten recension av Two Scoops of Django 1.6.
Link to the review hereDet 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.
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$
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);
}
}
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
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.
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$
Collection
- Collections
. Med Java 8 hade de statiska metoderna i Collections
kunnat ligga direkt i Collection
-interfacet.
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.
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
?
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...
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.
Stream
?Stream
:s.
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 super T> 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 super V, ? extends T> 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]
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 Predicate
på Optional
-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
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.
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.
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.
Det beror på hur många parametrar det skall ha och om det ryms på en rad eller inte.
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.
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.
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)
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)
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.
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)
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.
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);
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);
Man sätter bara return
framför lambdauttrycket som skall returneras:
private static Supplier<Integer> createSupplier() {
return () -> 42;
}
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.
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.