Vereisten: Om dit concept te begrijpen dien je comfortabel te zijn met het idee van Interfaces in Java:
public interface Printer {
void print(String text);
}
Wanneer de bovengenoemde Printer
interface wordt geimplementeerd,
kan de print methode bepalen wat het moet doen met de geleverde attribuut.
Bijvoorbeeld, print naar de SaxionApp
. Later kan ik er misschien voor kiezen
om te printen op een andere manier zonder dat ik de rest van de code hoef aan te passen.
public class MyPrinter implements Printer {
@Override
public void print(String text) {
SaxionApp.printLine(text);
}
}
Nu kan ik de instance van deze klasse een andere methode geven om iets te printen.
public class Main {
public static void main(String[] args) {
Printer printer = new MyPrinter();
doSomethingComplicated(printer);
}
private doSomethingComplicated(Printer printer) {
// In deze methode gebruiken we printer.print("message")
// om berichten weer te geven.
}
}
Nu kunnen we er voor kiezen om doSomethingComplicated
to voorzien van een willekeurige instantie,
zolang de methode print(String text)
is geimplementeerd:
public class SilentPrinter implements Printer {
@Override
public void print(String text) {
// Doe helemaal niks.
}
}
public class Main {
public static void main(String[] args) {
Printer printer = new SilentPrinter();
doSomethingComplicated(printer);
}
}
Door eenvoudigweg een andere klasse aan te biende die de Printer
interface te gebruikt
in dit tweede voorbeeld, zal dezelfde functie doSomthingComplicated
niks printen.
Dit maakt de doSomethingComplicated
method heel erg flexibel. Door een andere Printer
aan te leveren kan de doSomethingComplicated
method naar een bestand schrijven, database,
System.out, of niks doen.
Deze techniek heet code doorgeven (passing code) of gedrag doorgeven (passing behaviour).
In het vorige voorbeeld hadden we een hele klasse gemaakt, waarschijnlijk in een eigen bestand:
public class MyPrinter implements Printer {
@Override
public void print(String text) {
/* You choose */
}
}
Maar dit is niet altijd nodig. Je kunt het volgende doen:
public class Main {
public static void main(String[] args) {
Printer p = new Printer() {
@Override
public void print(String text) {
System.out.println(text);
}
};
p.print("Look mom! No actual class!");
doSomethingComplicated(p);
}
}
Deze code definieert niet een klasse met een eigen naam maar plaatst de
implementatie gelijk achter de new Printer()
statement.
De code boven p
is geinstantieerd als een anonieme klasse. (anonymous class)
Voor anonieme klassen geld het volgende:
Wanneer een interface een enkele methode definieerd, dan heb je de mogelijkheid om de code nog korter te maken.
// Anonymous class
Printer p = new Printer() {
@Override
public void print(String text) {
System.out.println(text);
}
};
p.print("This works!");
kan verandert worden in
// Lambda expression
Printer p = (text) -> {
System.out.println(text);
};
p.print("This STILL works!");
Herinner de inteface definitie:
@FunctionalInterface
public interface Printer {
void print(String text); // One single method!!!
}
Omdat de Printer
interface alleen een enkele methode definieerd, weet de compiler al
welke functie je wil gaan implementeren. Dit heet een functional interface
.
Je kunt zelfs de annotatie @FunctionalInterface
toevoegen om aan te geven dat het jouw
intentie is dat deze gebruikt moet worden als lambda expressie.
(Al kun je nog steeds klasses maken welke deze interfaces implementeren als je dat echt wil.)
Nu wordt het patroon:
Printer p = (text) -> { /* Doe iets met de text. */ };
De compiler weet ook het type en het aantal argumenten van deze methode.
Dus (text)
wordt gebruikt als enkele String
argument van de methode.
Tussen de accolades { /* hier */}
kun je nu elke willekeurie instructie plaatsen die
je nodig hebt.
Wanneer je implementatie gebruik maakt van een enkele regel code, dan kun je zelfs de accolades weglaten en de instructie direct typen:
// Lambda expressie zonder accolades.
Printer p = (t) -> System.out.println(t);
p.print("Dit werkt nog STEEDS!");
Variabelenamen met een enkele letter zijn Heel Slecht, maar dit toont aan dat je elke
variabele naam kunt gebruiken om iets door te geven aan de lambda expressie.
In dit geval q
in plaats van test
;
Gooi de haakjes weg:
Printer p = t -> System.out.println(t);
p.print("Dit werkt nog STEEDS!");
Hier zijn de haakjes verwijdert rondom het argument t
.
De uiteindelijke vorm van een lambda expressie ziet er als volgt uit:
Printer p = System.out::println;
p.print("Dit werkt nog STEEDS!");
Deze code zegt effectief: De println
methode in System.out
heeft dezelfde definitie als de
print
methode gedefinieerd in de Printer
interface, dus gebruik die methode.
Je kunt ook refereren naar statische methode ClassName
::
methodName
:
De ::
operator wordt ook wel de methode referentie genoemd.
Printer p = MyClass::myStaticPrintMethod;
p.print("Dit werkt met statische methodes in een statische class.");
Bedenk hoe ongelogelijk krachtig dit is: Je kunt eenvoudigweg de refereren naar een methode als een parameter in je eigen code, zodat jouw code het gedrag veranderd afhankelijk van de methode die wordt doorgegeven. Dit heet *gedrag doorgeven (passing behaviour).
Voor meer informattie hierover: We waren geinspireerd door dit boek: “Java 8 in Action” (ISBN: 9781617291999)
Een belangrijk verschil tussen lambda expressies en anonieme klassen.
Bestudeer de volgende code:
// Anonymous class
Printer p = new Printer() {
private int nrOfPrints = 0;
@Override
public void print(String text) {
nrOfPrints++;
System.out.println("[" + nrOfPrints + "] " + text);
}
};
p.print("Test");
De anonieme implementatie van Printer
slaat een attribuut op dat elke regel telt.
Elke keer wanneer de print methode wordt aangeroepen wordt zal de interne staat van de class
bijgewerkt worden.
Er is geen manier om dit te doen met lambda expressies!