- Het is voor mensen die het eerste deel van dit artikel hebben gelezen;
- Het is voor mensen die denken dat ze Java Core al goed kennen, maar geen idee hebben van lambda-expressies in Java. Of misschien hebben ze iets gehoord over lambda-uitdrukkingen, maar ontbreken de details.
- Het is voor mensen die een zeker begrip hebben van lambda-uitdrukkingen, maar er nog steeds door worden afgeschrikt en niet gewend zijn om ze te gebruiken.
Toegang tot externe variabelen
Wordt deze code gecompileerd met een anonieme klasse?
int counter = 0;
Runnable r = new Runnable() {
@Override
public void run() {
counter++;
}
};
Nee. De counter
variabele moet zijn final
. Of zo niet final
, dan kan het in ieder geval zijn waarde niet veranderen. Hetzelfde principe is van toepassing op lambda-expressies. Ze hebben toegang tot alle variabelen die ze kunnen "zien" vanaf de plaats waar ze zijn gedeclareerd. Maar een lambda mag ze niet wijzigen (er een nieuwe waarde aan toekennen). Er is echter een manier om deze beperking in anonieme klassen te omzeilen. Maak gewoon een referentievariabele aan en wijzig de interne status van het object. Daarbij verandert de variabele zelf niet (verwijst naar hetzelfde object) en kan veilig worden gemarkeerd als final
.
final AtomicInteger counter = new AtomicInteger(0);
Runnable r = new Runnable() {
@Override
public void run() {
counter.incrementAndGet();
}
};
Hier counter
is onze variabele een verwijzing naar een AtomicInteger
object. En de incrementAndGet()
methode wordt gebruikt om de status van dit object te wijzigen. De waarde van de variabele zelf verandert niet terwijl het programma draait. Het verwijst altijd naar hetzelfde object, waardoor we de variabele met het laatste trefwoord kunnen declareren. Hier zijn dezelfde voorbeelden, maar met lambda-expressies:
int counter = 0;
Runnable r = () -> counter++;
Deze compileert niet om dezelfde reden als de versie met een anonieme klasse: counter
mag niet veranderen terwijl het programma draait. Maar alles komt goed als we het zo doen:
final AtomicInteger counter = new AtomicInteger(0);
Runnable r = () -> counter.incrementAndGet();
Dit geldt ook voor aanroepmethoden. Binnen lambda-expressies hebt u niet alleen toegang tot alle "zichtbare" variabelen, maar kunt u ook alle toegankelijke methoden aanroepen.
public class Main {
public static void main(String[] args) {
Runnable runnable = () -> staticMethod();
new Thread(runnable).start();
}
private static void staticMethod() {
System.out.println("I'm staticMethod(), and someone just called me!");
}
}
Hoewel staticMethod()
het privé is, is het toegankelijk binnen de main()
methode, dus het kan ook worden aangeroepen vanuit een lambda die in de main
methode is gemaakt.
Wanneer wordt een lambda-expressie uitgevoerd?
Misschien vindt u de volgende vraag te simpel, maar stel hem evengoed: wanneer wordt de code in de lambda-expressie uitgevoerd? Wanneer is het gemaakt? Of wanneer er gebeld wordt (wat nu nog niet bekend is)? Dit is vrij eenvoudig te controleren.
System.out.println("Program start");
// All sorts of code here
// ...
System.out.println("Before lambda declaration");
Runnable runnable = () -> System.out.println("I'm a lambda!");
System.out.println("After lambda declaration");
// All sorts of other code here
// ...
System.out.println("Before passing the lambda to the thread");
new Thread(runnable).start();
Schermuitvoer:
Program start
Before lambda declaration
After lambda declaration
Before passing the lambda to the thread
I'm a lambda!
Je kunt zien dat de lambda-expressie helemaal aan het einde werd uitgevoerd, nadat de thread was gemaakt en alleen wanneer de uitvoering van het programma de run()
methode bereikt. Zeker niet als het wordt aangekondigd. Door een lambda-expressie te declareren, hebben we alleen een Runnable
object gemaakt en beschreven hoe de run()
methode zich gedraagt. De methode zelf wordt veel later uitgevoerd.
Methode referenties?
Methodeverwijzingen zijn niet direct gerelateerd aan lambda's, maar ik denk dat het zinvol is om er in dit artikel een paar woorden over te zeggen. Stel dat we een lambda-expressie hebben die niets bijzonders doet, maar gewoon een methode aanroept.
x -> System.out.println(x)
Het ontvangt wat x
en belt gewoon System.out.println()
, komt binnen x
. In dit geval kunnen we het vervangen door een verwijzing naar de gewenste methode. Soortgelijk:
System.out::println
Dat klopt - geen haakjes aan het eind! Hier is een vollediger voorbeeld:
List<String> strings = new LinkedList<>();
strings.add("Dota");
strings.add("GTA5");
strings.add("Halo");
strings.forEach(x -> System.out.println(x));
In de laatste regel gebruiken we de forEach()
methode, die een object nodig heeft dat de Consumer
interface implementeert. Nogmaals, dit is een functionele interface, met slechts één void accept(T t)
methode. Dienovereenkomstig schrijven we een lambda-expressie die één parameter heeft (omdat het in de interface zelf wordt getypt, specificeren we het parametertype niet; we geven alleen aan dat we het zullen noemen x
). In de body van de lambda-expressie schrijven we de code die wordt uitgevoerd wanneer de accept()
methode wordt aangeroepen. Hier laten we eenvoudig zien wat er in de variabele terecht is gekomen x
. Deze zelfde forEach()
methode doorloopt alle elementen in de verzameling en roept de accept()
methode aan op de implementatie van deConsumer
interface (onze lambda), waarbij elk item in de collectie wordt doorgegeven. Zoals ik al zei, kunnen we zo'n lambda-expressie (een die simpelweg een andere methode classificeert) vervangen door een verwijzing naar de gewenste methode. Dan ziet onze code er zo uit:
List<String> strings = new LinkedList<>();
strings.add("Dota");
strings.add("GTA5");
strings.add("Halo");
strings.forEach(System.out::println);
Het belangrijkste is dat de parameters van de methoden println()
en accept()
overeenkomen. Omdat de println()
methode alles kan accepteren (het is overbelast voor alle typen primitieven en alle objecten), kunnen we in plaats van lambda-expressies eenvoudig een verwijzing naar de println()
methode doorgeven aan forEach()
. Vervolgens forEach()
neemt elk element in de verzameling en geeft het rechtstreeks door aan de println()
methode. Voor iedereen die dit voor het eerst tegenkomt, houd er rekening mee dat we niet bellen System.out.println()
(met punten tussen woorden en met haakjes aan het einde). In plaats daarvan geven we een verwijzing naar deze methode door. Als we dit schrijven
strings.forEach(System.out.println());
we zullen een compilatiefout hebben. Voor de aanroep naar forEach()
, ziet Java dat System.out.println()
wordt aangeroepen, dus het begrijpt dat de retourwaarde is void
en zal proberen door te geven void
aan forEach()
, die in plaats daarvan een Consumer
object verwacht.
Syntaxis voor methodeverwijzingen
Het is heel simpel:-
We geven een verwijzing door naar een statische methode zoals deze:
ClassName::staticMethodName
public class Main { public static void main(String[] args) { List<String> strings = new LinkedList<>(); strings.add("Dota"); strings.add("GTA5"); strings.add("Halo"); strings.forEach(Main::staticMethod); } private static void staticMethod(String s) { // Do something } }
-
We geven een verwijzing door naar een niet-statische methode met behulp van een bestaand object, zoals dit:
objectName::instanceMethodName
public class Main { public static void main(String[] args) { List<String> strings = new LinkedList<>(); strings.add("Dota"); strings.add("GTA5"); strings.add("Halo"); Main instance = new Main(); strings.forEach(instance::nonStaticMethod); } private void nonStaticMethod(String s) { // Do something } }
-
We geven een verwijzing door naar een niet-statische methode met behulp van de klasse die deze als volgt implementeert:
ClassName::methodName
public class Main { public static void main(String[] args) { List<User> users = new LinkedList<>(); users.add (new User("John")); users.add(new User("Paul")); users.add(new User("George")); users.forEach(User::print); } private static class User { private String name; private User(String name) { this.name = name; } private void print() { System.out.println(name); } } }
-
We geven een verwijzing naar een constructor als volgt door:
ClassName::new
Methodereferenties zijn erg handig als je al een methode hebt die perfect zou werken als callback. In dit geval, in plaats van een lambda-expressie te schrijven die de code van de methode bevat, of een lambda-expressie te schrijven die simpelweg de methode aanroept, geven we er gewoon een verwijzing naar door. En dat is het.
Een interessant onderscheid tussen anonieme klassen en lambda-expressies
In een anonieme klassethis
verwijst het trefwoord naar een object van de anonieme klasse. Maar als we dit in een lambda gebruiken, krijgen we toegang tot het object van de bevattende klasse. Degene waar we eigenlijk de lambda-expressie hebben geschreven. Dit gebeurt omdat lambda-expressies worden gecompileerd in een privémethode van de klasse waarin ze zijn geschreven. Ik zou het gebruik van deze "functie" niet aanraden, omdat het een neveneffect heeft en dat in tegenspraak is met de principes van functioneel programmeren. Dat gezegd hebbende, deze aanpak is volledig in overeenstemming met OOP. ;)
Waar heb ik mijn informatie vandaan en wat moet u nog meer lezen?
- Zelfstudie op de officiële website van Oracle. Veel gedetailleerde informatie, inclusief voorbeelden.
- Hoofdstuk over methodereferenties in dezelfde Oracle-tutorial.
- Laat je meeslepen in Wikipedia als je echt nieuwsgierig bent.
GO TO FULL VERSION