CodeGym /Java Blog /Willekeurig /Een uitleg van lambda-expressies in Java. Met voorbeelden...
John Squirrels
Niveau 41
San Francisco

Een uitleg van lambda-expressies in Java. Met voorbeelden en opdrachten. Deel 2

Gepubliceerd in de groep Willekeurig
Voor wie is dit artikel?
  • 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.
Als u niet in een van deze categorieën past, vindt u dit artikel misschien saai, gebrekkig of in het algemeen niet uw ding. Ga in dit geval gerust verder met andere dingen of, als u goed thuis bent in het onderwerp, geef dan suggesties in de opmerkingen over hoe ik het artikel zou kunnen verbeteren of aanvullen. Een uitleg van lambda-expressies in Java.  Met voorbeelden en opdrachten.  Deel 2 - 1Het materiaal claimt geen academische waarde te hebben, laat staan ​​nieuwheid. Integendeel: ik zal proberen dingen die (voor sommige mensen) complex zijn zo eenvoudig mogelijk te beschrijven. Een verzoek om uitleg over de Stream API inspireerde me om dit te schrijven. Ik dacht erover na en besloot dat sommige van mijn streamvoorbeelden onbegrijpelijk zouden zijn zonder begrip van lambda-expressies. We beginnen dus met lambda-expressies.

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 counteris onze variabele een verwijzing naar een AtomicIntegerobject. 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:  countermag 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 mainmethode 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 Runnableobject 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 xen 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 Consumerinterface 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 deConsumerinterface (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 voiden zal proberen door te geven voidaan forEach(), die in plaats daarvan een Consumerobject verwacht.

Syntaxis voor methodeverwijzingen

Het is heel simpel:
  1. 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 
        } 
    }
    
  2. 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 
        } 
    }
    
  3. 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); 
            } 
        } 
    }
    
  4. 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 klasse thisverwijst 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?

En natuurlijk vond ik heel veel dingen op Google :)
Opmerkingen
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION