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 1

Gepubliceerd in de groep Willekeurig
Voor wie is dit artikel?
  • 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.
Een uitleg van lambda-expressies in Java.  Met voorbeelden en opdrachten.  Deel 1 - 1Als 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. Het 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. Wat moet je weten om dit artikel te begrijpen?
  1. U moet objectgeoriënteerd programmeren (OOP) begrijpen, namelijk:

    • klassen, objecten en het verschil daartussen;
    • interfaces, hoe ze verschillen van klassen, en de relatie tussen interfaces en klassen;
    • methoden, hoe ze te noemen, abstracte methoden (dwz methoden zonder implementatie), methodeparameters, methodeargumenten en hoe ze moeten worden doorgegeven;
    • toegangsmodificatoren, statische methoden/variabelen, definitieve methoden/variabelen;
    • overerving van klassen en interfaces, meervoudige overerving van interfaces.
  2. Kennis van Java Core: generische typen (generics), collecties (lijsten), threads.
Welnu, laten we ter zake komen.

Een beetje geschiedenis

Lambda-uitdrukkingen kwamen naar Java vanuit functioneel programmeren en daar vanuit de wiskunde. In de Verenigde Staten in het midden van de 20e eeuw werkte Alonzo Church, die dol was op wiskunde en allerlei abstracties, aan Princeton University. Het was Alonzo Church die de lambda-calculus uitvond, wat aanvankelijk een reeks abstracte ideeën was die totaal niets met programmeren te maken hadden. Wiskundigen als Alan Turing en John von Neumann werkten tegelijkertijd aan Princeton University. Alles kwam samen: Church kwam met de lambdacalculus. Turing ontwikkelde zijn abstracte rekenmachine, nu bekend als de "Turing-machine". En von Neumann stelde een computerarchitectuur voor die de basis heeft gevormd van moderne computers (nu een "von Neumann-architectuur" genoemd). Op dat moment, Alonzo Kerk' Zijn ideeën werden niet zo bekend als de werken van zijn collega's (met uitzondering van de zuivere wiskunde). Even later raakte John McCarthy (ook afgestudeerd aan Princeton University en ten tijde van ons verhaal een medewerker van het Massachusetts Institute of Technology) echter geïnteresseerd in de ideeën van Church. In 1958 creëerde hij op basis van die ideeën de eerste functionele programmeertaal, LISP. En 58 jaar later lekten de ideeën van functioneel programmeren in Java 8. Er zijn nog geen 70 jaar verstreken... Eerlijk gezegd, dit is niet het langste dat een wiskundig idee nodig heeft gehad om in de praktijk te worden toegepast. een medewerker van het Massachusetts Institute of Technology) raakte geïnteresseerd in de ideeën van Church. In 1958 creëerde hij op basis van die ideeën de eerste functionele programmeertaal, LISP. En 58 jaar later lekten de ideeën van functioneel programmeren in Java 8. Er zijn nog geen 70 jaar verstreken... Eerlijk gezegd, dit is niet het langste dat een wiskundig idee nodig heeft gehad om in de praktijk te worden toegepast. een medewerker van het Massachusetts Institute of Technology) raakte geïnteresseerd in de ideeën van Church. In 1958 creëerde hij op basis van die ideeën de eerste functionele programmeertaal, LISP. En 58 jaar later lekten de ideeën van functioneel programmeren in Java 8. Er zijn nog geen 70 jaar verstreken... Eerlijk gezegd, dit is niet het langste dat een wiskundig idee nodig heeft gehad om in de praktijk te worden toegepast.

De kern van de zaak

Een lambda-expressie is een soort functie. U kunt het beschouwen als een gewone Java-methode, maar met het onderscheidende vermogen om als argument aan andere methoden door te geven. Dat is juist. Het is mogelijk geworden om niet alleen getallen, strings en cat's door te geven aan methoden, maar ook aan andere methoden! Wanneer kunnen we dit nodig hebben? Het zou bijvoorbeeld handig zijn als we een callback-methode willen doorgeven. Dat wil zeggen, als we de methode nodig hebben die we aanroepen om de mogelijkheid te hebben om een ​​andere methode aan te roepen die we eraan doorgeven. Met andere woorden, we hebben dus de mogelijkheid om onder bepaalde omstandigheden een callback door te geven en een andere callback in andere. En zodat onze methode die onze callbacks ontvangt, ze aanroept. Sorteren is een eenvoudig voorbeeld. Stel dat we een slim sorteeralgoritme schrijven dat er als volgt uitziet:

public void mySuperSort() { 
    // We do something here 
    if(compare(obj1, obj2) > 0) 
    // And then we do something here 
}
In de ifverklaring noemen we de compare()methode, waarbij we twee te vergelijken objecten doorgeven, en we willen weten welke van deze objecten "groter" is. We nemen aan dat de "grotere" voor de "kleinere" komt. Ik zet "groter" tussen aanhalingstekens, omdat we een universele methode schrijven die niet alleen in oplopende volgorde weet te sorteren, maar ook in aflopende volgorde (in dit geval is het "grotere" object eigenlijk het "kleinere" object , en vice versa). Om het specifieke algoritme voor onze sortering in te stellen, hebben we een mechanisme nodig om het door te geven aan onze mySuperSort()methode. Op die manier kunnen we onze methode "besturen" wanneer deze wordt aangeroepen. Natuurlijk kunnen we twee afzonderlijke methoden schrijven - mySuperSortAscend()enmySuperSortDescend()— voor sorteren in oplopende en aflopende volgorde. Of we kunnen een argument doorgeven aan de methode (bijvoorbeeld een booleaanse variabele; indien waar, sorteer dan in oplopende volgorde, en indien onwaar, dan in aflopende volgorde). Maar wat als we iets ingewikkelds willen sorteren, zoals een lijst met stringarrays? Hoe weet onze mySuperSort()methode hoe deze reeksreeksen moeten worden gesorteerd? Op maat? Door de cumulatieve lengte van alle woorden? Misschien alfabetisch gebaseerd op de eerste string in de array? En wat als we de lijst met arrays in sommige gevallen moeten sorteren op arraygrootte en in andere gevallen op de cumulatieve lengte van alle woorden in elke array? Ik neem aan dat je al hebt gehoord over vergelijkers en dat we in dit geval gewoon een vergelijkerobject aan onze sorteermethode zouden doorgeven dat het gewenste sorteeralgoritme beschrijft. Omdat de standaardsort()methode is geïmplementeerd op basis van hetzelfde principe als , zal ik in mijn voorbeelden mySuperSort()gebruiken .sort()

String[] array1 = {"Dota", "GTA5", "Halo"}; 
String[] array2 = {"I", "really", "love", "Java"}; 
String[] array3 = {"if", "then", "else"}; 

List<String[]> arrays = new ArrayList<>(); 
arrays.add(array1); 
arrays.add(array2); 
arrays.add(array3); 

Comparator<;String[]> sortByLength = new Comparator<String[]>() { 
    @Override 
    public int compare(String[] o1, String[] o2) { 
        return o1.length - o2.length; 
    } 
}; 

Comparator<String[]> sortByCumulativeWordLength = new Comparator<String[]>() { 

    @Override 
    public int compare(String[] o1, String[] o2) { 
        int length1 = 0; 
        int length2 = 0; 
        for (String s : o1) { 
            length1 += s.length(); 
        } 

        for (String s : o2) { 
            length2 += s.length(); 
        } 

        return length1 - length2; 
    } 
};

arrays.sort(sortByLength);
Resultaat:

  1. Dota GTA5 Halo
  2. if then else
  3. I really love Java
Hier worden de arrays gesorteerd op het aantal woorden in elke array. Een array met minder woorden wordt als "minder" beschouwd. Daarom komt het op de eerste plaats. Een array met meer woorden wordt als "groter" beschouwd en wordt aan het einde geplaatst. Als we een andere comparator aan de sort()methode doorgeven, zoals sortByCumulativeWordLength, krijgen we een ander resultaat:

  1. if then else
  2. Dota GTA5 Halo
  3. I really love Java
Nu zijn de arrays gesorteerd op het totale aantal letters in de woorden van de array. In de eerste array zijn er 10 letters, in de tweede - 12 en in de derde - 15. Als we slechts één comparator hebben, hoeven we er geen aparte variabele voor te declareren. In plaats daarvan kunnen we eenvoudigweg een anonieme klasse maken op het moment dat de methode wordt aangeroepen sort(). Iets zoals dit:

String[] array1 = {"Dota", "GTA5", "Halo"}; 
String[] array2 = {"I", "really", "love", "Java"}; 
String[] array3 = {"if", "then", "else"}; 

List<String[]> arrays = new ArrayList<>(); 

arrays.add(array1); 
arrays.add(array2); 
arrays.add(array3); 

arrays.sort(new Comparator<String[]>() { 
    @Override 
    public int compare(String[] o1, String[] o2) { 
        return o1.length - o2.length; 
    } 
}); 
We krijgen hetzelfde resultaat als in het eerste geval. Taak 1. Herschrijf dit voorbeeld zodat het arrays niet sorteert in oplopende volgorde van het aantal woorden in elke array, maar in aflopende volgorde. Dit weten we allemaal al. We weten hoe we objecten aan methoden moeten doorgeven. Afhankelijk van wat we op dat moment nodig hebben, kunnen we verschillende objecten doorgeven aan een methode, die vervolgens de methode zal aanroepen die we hebben geïmplementeerd. Dit roept de vraag op: waarom hebben we hier in vredesnaam een ​​lambda-expressie nodig?  Omdat een lambda-expressie een object is dat precies één methode heeft. Als een "methode-object". Een methode verpakt in een object. Het heeft gewoon een enigszins onbekende syntaxis (maar daarover later meer). Laten we deze code nog eens bekijken:

arrays.sort(new Comparator<String[]>() { 
    @Override 
    public int compare(String[] o1, String[] o2) { 
        return o1.length - o2.length; 
    } 
});
Hier nemen we onze lijst met arrays en roepen we de sort()methode aan, waaraan we een comparatorobject doorgeven met een enkele compare()methode (de naam maakt ons niet uit - het is tenslotte de enige methode van dit object, dus we kunnen niet fout gaan). Deze methode heeft twee parameters waarmee we zullen werken. Als je in IntelliJ IDEA werkt, heb je waarschijnlijk gezien dat het aanbod de code als volgt aanzienlijk verkort:

arrays.sort((o1, o2) -> o1.length - o2.length);
Dit reduceert zes regels tot een enkele korte. 6 regels worden herschreven als één korte. Er is iets verdwenen, maar ik garandeer je dat het niets belangrijks was. Deze code werkt op precies dezelfde manier als bij een anonieme klasse. Taak 2. Probeer de oplossing voor Taak 1 te herschrijven met behulp van een lambda-expressie (vraag op zijn minst IntelliJ IDEA om uw anonieme klasse om te zetten in een lambda-expressie).

Laten we het hebben over interfaces

In principe is een interface gewoon een lijst met abstracte methoden. Wanneer we een klasse maken die een interface implementeert, moet onze klasse de methoden implementeren die in de interface zijn opgenomen (of we moeten de klasse abstract maken). Er zijn interfaces met veel verschillende methoden (bijvoorbeeld  List), en er zijn interfaces met slechts één methode (bijvoorbeeld Comparatorof Runnable). Er zijn interfaces die geen enkele methode hebben (zogenaamde markerinterfaces zoals Serializable). Interfaces die maar één methode hebben, worden ook wel functionele interfaces genoemd . In Java 8 zijn ze zelfs gemarkeerd met een speciale annotatie:@FunctionalInterface. Het zijn deze interfaces met één methode die geschikt zijn als doeltypen voor lambda-expressies. Zoals ik hierboven al zei, is een lambda-expressie een methode verpakt in een object. En als we zo'n object passeren, passeren we in wezen deze ene methode. Het blijkt dat het ons niet kan schelen hoe de methode heet. Het enige dat voor ons van belang is, zijn de parameters van de methode en natuurlijk de body van de methode. In essentie is een lambda-expressie de implementatie van een functionele interface. Overal waar we een interface met een enkele methode zien, kan een anonieme klasse worden herschreven als een lambda. Als de interface meer of minder dan één methode heeft, werkt een lambda-expressie niet en gebruiken we in plaats daarvan een anonieme klasse of zelfs een instantie van een gewone klasse. Nu is het tijd om een ​​beetje in lambda's te graven. :)

Syntaxis

De algemene syntaxis is ongeveer als volgt:

(parameters) -> {method body}
Dat wil zeggen, haakjes rond de parameters van de methode, een "pijl" (gevormd door een koppelteken en een groter-dan-teken), en dan zoals altijd de hoofdtekst van de methode tussen accolades. De parameters komen overeen met die gespecificeerd in de interfacemethode. Als de typen variabelen ondubbelzinnig kunnen worden bepaald door de compiler (in ons geval weet deze dat we werken met string-arrays, omdat ons Listobject wordt getypt met String[]), dan hoeft u hun typen niet aan te geven.
Als ze dubbelzinnig zijn, geef dan het type aan. IDEA kleurt het grijs als het niet nodig is.
U kunt meer lezen in deze Oracle-zelfstudie en elders. Dit wordt " doeltypering " genoemd. U kunt de variabelen elke gewenste naam geven — u hoeft niet dezelfde namen te gebruiken die in de interface zijn gespecificeerd. Als er geen parameters zijn, geef dan gewoon lege haakjes aan. Als er slechts één parameter is, geeft u gewoon de naam van de variabele op zonder haakjes. Nu we de parameters begrijpen, is het tijd om de body van de lambda-expressie te bespreken. Binnen de accolades schrijf je code zoals je zou doen voor een gewone methode. Als je code uit één regel bestaat, kun je de accolades helemaal weglaten (vergelijkbaar met if-statements en for-loops). Als uw single-line lambda iets retourneert, hoeft u geen a op te nemenreturnstelling. Maar als u accolades gebruikt, moet u expliciet een returnstatement opnemen, net zoals u dat bij een gewone methode zou doen.

Voorbeelden

Voorbeeld 1.

() -> {}
Het eenvoudigste voorbeeld. En het meest zinloze :), omdat het niets doet. Voorbeeld 2.

() -> ""
Nog een interessant voorbeeld. Er is niets voor nodig en retourneert een lege string ( returnwordt weggelaten, omdat deze niet nodig is). Hier is hetzelfde, maar dan met return:

() -> { 
    return ""; 
}
Voorbeeld 3. "Hallo wereld!" lambda's gebruiken

() -> System.out.println("Hello, World!")
Het kost niets en retourneert niets (we kunnen niet returnvóór de aanroep van zetten System.out.println(), omdat het println()retourtype van de methode is void). Het toont gewoon de begroeting. Dit is ideaal voor een implementatie van de Runnableinterface. Het volgende voorbeeld is vollediger:

public class Main { 
    public static void main(String[] args) { 
        new Thread(() -> System.out.println("Hello, World!")).start(); 
    } 
}
Of zo:

public class Main { 
    public static void main(String[] args) { 
        Thread t = new Thread(() -> System.out.println("Hello, World!")); 
        t.start();
    } 
}
Of we kunnen de lambda-expressie zelfs opslaan als een Runnableobject en deze vervolgens doorgeven aan de Threadconstructor:

public class Main { 
    public static void main(String[] args) { 
        Runnable runnable = () -> System.out.println("Hello, World!"); 
        Thread t = new Thread(runnable); 
        t.start(); 
    } 
}
Laten we eens nader kijken naar het moment waarop een lambda-expressie wordt opgeslagen in een variabele. De Runnableinterface vertelt ons dat de objecten een public void run()methode moeten hebben. Volgens de interface heeft de runmethode geen parameters nodig. En het retourneert niets, dwz het retourtype is void. Dienovereenkomstig zal deze code een object maken met een methode die niets accepteert of retourneert. Dit past perfect bij de werkwijze Runnablevan de interface run(). Daarom konden we deze lambda-expressie in een Runnablevariabele zetten.  Voorbeeld 4.

() -> 42
Nogmaals, er is niets voor nodig, maar het geeft het getal 42 terug. Zo'n lambda-expressie kan in een Callablevariabele worden geplaatst, omdat deze interface maar één methode heeft die er ongeveer zo uitziet:

V call(),
waar  V is het retourtype (in ons geval  int). Dienovereenkomstig kunnen we een lambda-expressie als volgt opslaan:

Callable<Integer> c = () -> 42;
Voorbeeld 5. Een lambda-expressie met meerdere regels

() -> { 
    String[] helloWorld = {"Hello", "World!"}; 
    System.out.println(helloWorld[0]); 
    System.out.println(helloWorld[1]); 
}
Nogmaals, dit is een lambda-expressie zonder parameters en een voidretourtype (omdat er geen returninstructie is).  Voorbeeld 6

x -> x
Hier nemen we een xvariabele en retourneren deze. Houd er rekening mee dat als er slechts één parameter is, u de haakjes eromheen kunt weglaten. Hier is hetzelfde, maar dan tussen haakjes:

(x) -> x
En hier is een voorbeeld met een expliciete retourverklaring:

x -> { 
    return x;
}
Of zoals dit met haakjes en een return statement:

(x) -> { 
    return x;
}
Of met expliciete aanduiding van het type (en dus tussen haakjes):

(int x) -> x
Voorbeeld 7

x -> ++x
We nemen xhet en geven het terug, maar pas nadat we 1 hebben toegevoegd. Je kunt die lambda als volgt herschrijven:

x -> x + 1
In beide gevallen laten we de haakjes rond de parameter en de body van de methode weg, samen met de returninstructie, aangezien deze optioneel zijn. Versies tussen haakjes en een return-instructie worden gegeven in Voorbeeld 6. Voorbeeld 8

(x, y) -> x % y
We nemen xen yen retourneren de rest van deling xdoor y. De haakjes rond de parameters zijn hier verplicht. Ze zijn alleen optioneel als er slechts één parameter is. Hier is het met een expliciete aanduiding van de typen:

(double x, int y) -> x % y
Voorbeeld 9

(Cat cat, String name, int age) -> {
    cat.setName(name); 
    cat.setAge(age); 
}
We nemen een Catobject, een Stringnaam en een int-leeftijd. In de methode zelf gebruiken we de doorgegeven naam en leeftijd om variabelen op de kat in te stellen. Omdat ons catobject een referentietype is, wordt het buiten de lambda-expressie gewijzigd (het krijgt de doorgegeven naam en leeftijd). Hier is een iets gecompliceerdere versie die een vergelijkbare lambda gebruikt:

public class Main { 

    public static void main(String[] args) { 
        // Create a cat and display it to confirm that it is "empty" 
        Cat myCat = new Cat(); 
        System.out.println(myCat);
 
        // Create a lambda 
        Settable<Cat> s = (obj, name, age) -> { 
            obj.setName(name); 
            obj.setAge(age); 

        }; 

        // Call a method to which we pass the cat and lambda 
        changeEntity(myCat, s); 

        // Display the cat on the screen and see that its state has changed (it has a name and age) 
        System.out.println(myCat); 

    } 

    private static <T extends HasNameAndAge>  void changeEntity(T entity, Settable<T> s) { 
        s.set(entity, "Smokey", 3); 
    }
}

interface HasNameAndAge { 
    void setName(String name); 
    void setAge(int age); 
}

interface Settable<C extends HasNameAndAge> { 
    void set(C entity, String name, int age); 
}

class Cat implements HasNameAndAge { 
    private String name; 
    private int age; 

    @Override 
    public void setName(String name) { 
        this.name = name;
    }

    @Override
    public void setAge(int age) {
        this.age = age; 
    } 

    @Override
    public String toString() {
        return "Cat{" +
                "name='" + name + '\'' + 
                ", age=" + age + 
                '}';
    }
}
Resultaat:

Cat{name='null', age=0}
Cat{name='Smokey', age=3}
Zoals je kunt zien, had het Catobject één status en daarna veranderde de status nadat we de lambda-expressie gebruikten. Lambda-uitdrukkingen combineren perfect met generieke geneesmiddelen. En als we een klasse moeten maken Dogdie ook implementeert HasNameAndAge, dan kunnen we dezelfde bewerkingen uitvoeren in Dogde main() methode zonder de lambda-expressie te wijzigen. Taak 3. Schrijf een functionele interface met een methode die een getal krijgt en een booleaanse waarde retourneert. Schrijf een implementatie van zo'n interface als een lambda-expressie die 'true' retourneert als het doorgegeven getal deelbaar is door 13. Taak 4.Schrijf een functionele interface met een methode waaraan twee strings moeten doorgegeven worden en die ook een string teruggeeft. Schrijf een implementatie van zo'n interface als een lambda-expressie die de langere string teruggeeft. Taak 5. Schrijf een functionele interface met een methode die drie getallen met drijvende komma nodig heeft: a, b en c en die ook een getal met drijvende komma retourneert. Schrijf een implementatie van zo'n interface als een lambda-expressie die de discriminant retourneert. Voor het geval je het vergeten bent, dat is D = b^2 — 4ac. Taak 6. Gebruik de functionele interface van Taak 5 en schrijf een lambda-expressie die het resultaat van retourneert a * b^c. Een uitleg van lambda-expressies in Java. Met voorbeelden en opdrachten. Deel 2
Opmerkingen
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION