CodeGym /Java blogg /Slumpmässig /En förklaring av lambda-uttryck i Java. Med exempel och u...
John Squirrels
Nivå
San Francisco

En förklaring av lambda-uttryck i Java. Med exempel och uppgifter. Del 1

Publicerad i gruppen
Vem är den här artikeln till för?
  • Det är för personer som tror att de redan känner till Java Core väl, men som inte har någon aning om lambda-uttryck i Java. Eller så har de kanske hört något om lambda-uttryck, men detaljerna saknas
  • Det är för personer som har en viss förståelse för lambda-uttryck, men som fortfarande är skrämda av dem och ovana vid att använda dem.
En förklaring av lambda-uttryck i Java.  Med exempel och uppgifter.  Del 1 - 1Om du inte passar in i någon av dessa kategorier kan du tycka att den här artikeln är tråkig, felaktig eller i allmänhet inte din kopp te. I det här fallet, gå gärna vidare till andra saker eller, om du är väl insatt i ämnet, vänligen ge förslag i kommentarerna om hur jag kan förbättra eller komplettera artikeln. Materialet gör inte anspråk på att ha något akademiskt värde, än mindre nyhet. Snarare tvärtom: Jag ska försöka beskriva saker som är komplexa (för vissa personer) så enkelt som möjligt. En begäran om att förklara Stream API inspirerade mig att skriva detta. Jag funderade på det och bestämde mig för att några av mina streamexempel skulle vara obegripliga utan förståelse för lambda-uttryck. Så vi börjar med lambda-uttryck. Vad behöver du veta för att förstå den här artikeln?
  1. Du bör förstå objektorienterad programmering (OOP), nämligen:

    • klasser, objekt och skillnaden mellan dem;
    • gränssnitt, hur de skiljer sig från klasser och förhållandet mellan gränssnitt och klasser;
    • metoder, hur man kallar dem, abstrakta metoder (dvs. metoder utan implementering), metodparametrar, metodargument och hur man klarar dem;
    • åtkomstmodifierare, statiska metoder/variabler, slutliga metoder/variabler;
    • arv av klasser och gränssnitt, multipelt arv av gränssnitt.
  2. Kunskaper om Java Core: generiska typer (generika), samlingar (listor), trådar.
Nåväl, låt oss komma till det.

Lite historia

Lambda-uttryck kom till Java från funktionell programmering och dit från matematik. I USA i mitten av 1900-talet arbetade Alonzo Church, som var mycket förtjust i matematik och alla sorters abstraktioner, vid Princeton University. Det var Alonzo Church som uppfann lambdakalkylen, som från början var en uppsättning abstrakta idéer som inte var relaterade till programmering. Matematiker som Alan Turing och John von Neumann arbetade vid Princeton University samtidigt. Allt kom ihop: Church kom med lambdakalkylen. Turing utvecklade sin abstrakta dator, nu känd som "Turing-maskinen". Och von Neumann föreslog en datorarkitektur som har legat till grund för moderna datorer (nu kallad "von Neumann-arkitektur"). På den tiden, Alonzo Church' s idéer blev inte så välkända som hans kollegors verk (med undantag för området ren matematik). Men lite senare blev John McCarthy (också en examen från Princeton University och, vid tidpunkten för vår berättelse, anställd vid Massachusetts Institute of Technology) intresserad av kyrkans idéer. 1958 skapade han det första funktionella programmeringsspråket, LISP, baserat på dessa idéer. Och 58 år senare läckte idéerna om funktionell programmering in i Java 8. Inte ens 70 år har gått... Ärligt talat, det här är inte det längsta som det har tagit för en matematisk idé att tillämpas i praktiken. en anställd vid Massachusetts Institute of Technology) blev intresserad av kyrkans idéer. 1958 skapade han det första funktionella programmeringsspråket, LISP, baserat på dessa idéer. Och 58 år senare läckte idéerna om funktionell programmering in i Java 8. Inte ens 70 år har gått... Ärligt talat, det här är inte det längsta som det har tagit för en matematisk idé att tillämpas i praktiken. en anställd vid Massachusetts Institute of Technology) blev intresserad av kyrkans idéer. 1958 skapade han det första funktionella programmeringsspråket, LISP, baserat på dessa idéer. Och 58 år senare läckte idéerna om funktionell programmering in i Java 8. Inte ens 70 år har gått... Ärligt talat, det här är inte det längsta som det har tagit för en matematisk idé att tillämpas i praktiken.

Kärnan i saken

Ett lambdauttryck är en sorts funktion. Du kan betrakta det som en vanlig Java-metod men med den särskiljande förmågan att överföras till andra metoder som argument. Det är rätt. Det har blivit möjligt att överföra inte bara siffror, strängar och katter till metoder, utan även andra metoder! När kan vi behöva detta? Det skulle till exempel vara till hjälp om vi vill skicka någon återuppringningsmetod. Det vill säga, om vi behöver metoden vi anropar för att ha förmågan att anropa någon annan metod som vi överför till den. Med andra ord, så vi har möjligheten att skicka en återuppringning under vissa omständigheter och en annan återuppringning i andra. Och så att vår metod som tar emot våra callbacks ringer dem. Sortering är ett enkelt exempel. Anta att vi skriver någon smart sorteringsalgoritm som ser ut så här:

public void mySuperSort() { 
    // We do something here 
    if(compare(obj1, obj2) > 0) 
    // And then we do something here 
}
I ifuttalandet kallar vi compare()metoden, att skicka in två objekt som ska jämföras, och vi vill veta vilket av dessa objekt som är "större". Vi antar att den "större" kommer före den "mindre". Jag sätter "större" inom citattecken, eftersom vi skriver en universell metod som kommer att veta hur man sorterar inte bara i stigande ordning, utan också i fallande ordning (i det här fallet kommer det "större" objektet faktiskt att vara det "mindre" objektet , och vice versa). För att ställa in den specifika algoritmen för vår sortering behöver vi någon mekanism för att överföra den till vår mySuperSort()metod. På så sätt kommer vi att kunna "kontrollera" vår metod när den anropas. Naturligtvis kan vi skriva två separata metoder - mySuperSortAscend()ochmySuperSortDescend()— för sortering i stigande och fallande ordning. Eller så kan vi skicka något argument till metoden (till exempel en boolesk variabel; om sant, sortera sedan i stigande ordning, och om falskt, sedan i fallande ordning). Men vad händer om vi vill sortera något komplicerat som en lista med strängarrayer? Hur kommer vår mySuperSort()metod att veta hur man sorterar dessa strängarrayer? Efter storlek? Med den kumulativa längden på alla orden? Kanske alfabetiskt baserat på den första strängen i arrayen? Och vad händer om vi behöver sortera listan med arrayer efter arraystorlek i vissa fall och efter den kumulativa längden på alla ord i varje array i andra fall? Jag förväntar mig att du redan har hört talas om komparatorer och att vi i det här fallet helt enkelt skulle överföra ett komparatorobjekt till vår sorteringsmetod som beskriver den önskade sorteringsalgoritmen. Eftersom standardensort()Metoden implementeras utifrån samma princip som , mySuperSort()jag kommer att använda sort()i mina exempel.

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);
Resultat:

  1. Dota GTA5 Halo
  2. if then else
  3. I really love Java
Här sorteras arrayerna efter antalet ord i varje array. En array med färre ord anses vara "mindre". Det är därför det kommer först. En array med fler ord anses vara "större" och placeras i slutet. Om vi ​​skickar en annan komparator till sort()metoden, till exempel sortByCumulativeWordLength, får vi ett annat resultat:

  1. if then else
  2. Dota GTA5 Halo
  3. I really love Java
Nu är arrayerna sorterade efter det totala antalet bokstäver i arrayens ord. I den första matrisen finns det 10 bokstäver, i den andra - 12 och i den tredje - 15. Om vi ​​bara har en enda komparator behöver vi inte deklarera en separat variabel för den. Istället kan vi helt enkelt skapa en anonym klass precis vid tidpunkten för anropet till metoden sort(). Något som det här:

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; 
    } 
}); 
Vi kommer att få samma resultat som i det första fallet. Uppgift 1. Skriv om det här exemplet så att det sorterar arrayer inte i stigande ordning av antalet ord i varje array, utan i fallande ordning. Vi vet redan allt detta. Vi vet hur man skickar objekt till metoder. Beroende på vad vi behöver för tillfället kan vi skicka olika objekt till en metod, som sedan kommer att anropa metoden som vi implementerade. Detta väcker frågan: varför i hela friden behöver vi ett lambdauttryck här?  Eftersom ett lambdauttryck är ett objekt som har exakt en metod. Som ett "metodobjekt". En metod paketerad i ett objekt. Den har bara en lite obekant syntax (men mer om det senare). Låt oss ta en ny titt på den här koden:

arrays.sort(new Comparator<String[]>() { 
    @Override 
    public int compare(String[] o1, String[] o2) { 
        return o1.length - o2.length; 
    } 
});
Här tar vi vår arraylista och anropar dess sort()metod, till vilken vi skickar ett komparatorobjekt med en enda compare()metod (dets namn spelar ingen roll för oss - trots allt är det detta objekts enda metod, så vi kan inte gå fel). Denna metod har två parametrar som vi kommer att arbeta med. Om du arbetar i IntelliJ IDEA, har du förmodligen sett att det erbjuder att avsevärt kondensera koden enligt följande:

arrays.sort((o1, o2) -> o1.length - o2.length);
Detta reducerar sex rader till en enda kort. 6 rader skrivs om till en kort. Något försvann, men jag garanterar att det inte var något viktigt. Den här koden kommer att fungera på exakt samma sätt som den skulle göra med en anonym klass. Uppgift 2. Ta en gissning om att skriva om lösningen till uppgift 1 med ett lambdauttryck (be åtminstone IntelliJ IDEA att konvertera din anonyma klass till ett lambdauttryck).

Låt oss prata om gränssnitt

I princip är ett gränssnitt helt enkelt en lista över abstrakta metoder. När vi skapar en klass som implementerar något gränssnitt måste vår klass implementera de metoder som ingår i gränssnittet (eller så måste vi göra klassen abstrakt). Det finns gränssnitt med många olika metoder (till exempel  List), och det finns gränssnitt med bara en metod (till exempel Comparatoreller Runnable). Det finns gränssnitt som inte har en enda metod (så kallade markörgränssnitt som ) Serializable. Gränssnitt som bara har en metod kallas även funktionella gränssnitt . I Java 8 är de till och med markerade med en speciell anteckning:@FunctionalInterface. Det är dessa enmetodsgränssnitt som är lämpliga som måltyper för lambda-uttryck. Som jag sa ovan är ett lambda-uttryck en metod som är insvept i ett objekt. Och när vi passerar ett sådant objekt, passerar vi i huvudsak denna enda metod. Det visar sig att vi inte bryr oss om vad metoden heter. Det enda som betyder något för oss är metodparametrarna och, naturligtvis, metoden. I huvudsak är ett lambda-uttryck implementeringen av ett funktionellt gränssnitt. Varhelst vi ser ett gränssnitt med en enda metod kan en anonym klass skrivas om till en lambda. Om gränssnittet har mer eller mindre än en metod, fungerar inte ett lambda-uttryck och vi kommer istället att använda en anonym klass eller till och med en instans av en vanlig klass. Nu är det dags att gräva lite i lambdas. :)

Syntax

Den allmänna syntaxen är ungefär så här:

(parameters) -> {method body}
Det vill säga parenteser som omger metodparametrarna, en "pil" (bildad av ett bindestreck och ett större-än-tecken), och sedan metodkroppen inom klammerparenteser, som alltid. Parametrarna motsvarar de som anges i gränssnittsmetoden. Om variabeltyper entydigt kan bestämmas av kompilatorn (i vårt fall vet den att vi arbetar med strängmatriser, eftersom vårt Listobjekt skrivs med String[]), så behöver du inte ange deras typer.
Om de är tvetydiga, ange sedan typen. IDEA kommer att färga den grå om den inte behövs.
Du kan läsa mer i denna Oracle-handledning och på andra ställen. Detta kallas " target typing ". Du kan namnge variablerna vad du vill – du behöver inte använda samma namn som anges i gränssnittet. Om det inte finns några parametrar, ange bara tomma parenteser. Om det bara finns en parameter, ange bara variabelnamnet utan några parenteser. Nu när vi förstår parametrarna är det dags att diskutera innehållet i lambdauttrycket. Innanför de lockiga hängslen skriver du kod precis som du skulle göra för en vanlig metod. Om din kod består av en enda rad kan du utelämna de krulliga klammerparenteserna helt (liknande if-statement och for-loops). Om din enradiga lambda ger något, behöver du inte inkludera enreturnpåstående. Men om du använder lockiga hängslen, måste du uttryckligen inkludera ett returnuttalande, precis som du skulle göra med en vanlig metod.

Exempel

Exempel 1.

() -> {}
Det enklaste exemplet. Och det mest meningslösa :), eftersom det inte gör någonting. Exempel 2.

() -> ""
Ett annat intressant exempel. Det tar ingenting och returnerar en tom sträng ( returnutelämnas, eftersom det är onödigt). Här är samma sak, men med return:

() -> { 
    return ""; 
}
Exempel 3. "Hej världen!" använder lambdas

() -> System.out.println("Hello, World!")
Det tar ingenting och returnerar ingenting (vi kan inte lägga returnföre anropet till System.out.println(), eftersom println()metodens returtyp är void). Det visar helt enkelt hälsningen. Detta är idealiskt för en implementering av Runnablegränssnittet. Följande exempel är mer komplett:

public class Main { 
    public static void main(String[] args) { 
        new Thread(() -> System.out.println("Hello, World!")).start(); 
    } 
}
Eller så här:

public class Main { 
    public static void main(String[] args) { 
        Thread t = new Thread(() -> System.out.println("Hello, World!")); 
        t.start();
    } 
}
Eller så kan vi till och med spara lambda-uttrycket som ett Runnableobjekt och sedan skicka det till Threadkonstruktorn:

public class Main { 
    public static void main(String[] args) { 
        Runnable runnable = () -> System.out.println("Hello, World!"); 
        Thread t = new Thread(runnable); 
        t.start(); 
    } 
}
Låt oss ta en närmare titt på ögonblicket när ett lambda-uttryck sparas till en variabel. Gränssnittet Runnabletalar om för oss att dess objekt måste ha en public void run()metod. Enligt gränssnittet runtar metoden inga parametrar. Och den returnerar ingenting, dvs dess returtyp är void. Följaktligen kommer den här koden att skapa ett objekt med en metod som inte tar eller returnerar något. Detta matchar perfekt Runnablegränssnittets run()metod. Det är därför vi kunde sätta detta lambdauttryck i en Runnablevariabel.  Exempel 4.

() -> 42
Återigen, det tar ingenting, men det returnerar talet 42. Ett sådant lambdauttryck kan sättas i en Callablevariabel, eftersom det här gränssnittet bara har en metod som ser ut ungefär så här:

V call(),
var  V är returtypen (i vårt fall )  int. Följaktligen kan vi spara ett lambda-uttryck enligt följande:

Callable<Integer> c = () -> 42;
Exempel 5. Ett lambdauttryck som involverar flera linjer

() -> { 
    String[] helloWorld = {"Hello", "World!"}; 
    System.out.println(helloWorld[0]); 
    System.out.println(helloWorld[1]); 
}
Återigen, detta är ett lambda-uttryck utan parametrar och en voidreturtyp (eftersom det inte finns något returnuttalande).  Exempel 6

x -> x
Här tar vi en xvariabel och returnerar den. Observera att om det bara finns en parameter kan du utelämna parentesen runt den. Här är samma sak, men med parentes:

(x) -> x
Och här är ett exempel med ett uttryckligt returmeddelande:

x -> { 
    return x;
}
Eller så här med parentes och ett returmeddelande:

(x) -> { 
    return x;
}
Eller med en uttrycklig angivelse av typen (och därmed med parentes):

(int x) -> x
Exempel 7

x -> ++x
Vi tar xoch returnerar den, men bara efter att ha lagt till 1. Du kan skriva om den lambdan så här:

x -> x + 1
I båda fallen utelämnar vi parentesen runt parametern och metodkroppen, tillsammans med satsen, returneftersom de är valfria. Versioner med parentes och en retursats ges i exempel 6. Exempel 8

(x, y) -> x % y
Vi tar xoch yoch returnerar resten av divisionen av xmed y. Parentesen runt parametrarna krävs här. De är valfria endast när det bara finns en parameter. Här är det med en tydlig indikation av typerna:

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

(Cat cat, String name, int age) -> {
    cat.setName(name); 
    cat.setAge(age); 
}
Vi tar ett Catobjekt, ett Stringnamn och en int ålder. I själva metoden använder vi det godkända namnet och åldern för att ställa in variabler på katten. Eftersom vårt catobjekt är en referenstyp kommer det att ändras utanför lambda-uttrycket (det kommer att få det godkända namnet och åldern). Här är en lite mer komplicerad version som använder en liknande lambda:

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 + 
                '}';
    }
}
Resultat:

Cat{name='null', age=0}
Cat{name='Smokey', age=3}
Som du kan se Cathade objektet ett tillstånd, och sedan ändrades tillståndet efter att vi använde lambda-uttrycket. Lambda-uttryck kombineras perfekt med generika. Och om vi behöver skapa en Dogklass som också implementerar HasNameAndAge, då kan vi utföra samma operationer i Dogmetoden main() utan att ändra lambda-uttrycket. Uppgift 3. Skriv ett funktionellt gränssnitt med en metod som tar ett tal och returnerar ett booleskt värde. Skriv en implementering av ett sådant gränssnitt som ett lambdauttryck som returnerar sant om det godkända talet är delbart med 13. Uppgift 4.Skriv ett funktionellt gränssnitt med en metod som tar två strängar och som även returnerar en sträng. Skriv en implementering av ett sådant gränssnitt som ett lambda-uttryck som returnerar den längre strängen. Uppgift 5. Skriv ett funktionellt gränssnitt med en metod som tar tre flyttal: a, b och c och som även returnerar ett flyttal. Skriv en implementering av ett sådant gränssnitt som ett lambdauttryck som returnerar diskriminanten. Om du glömde, det är D = b^2 — 4ac. Uppgift 6. Använd det funktionella gränssnittet från uppgift 5 och skriv ett lambdauttryck som returnerar resultatet av a * b^c. En förklaring av lambda-uttryck i Java. Med exempel och uppgifter. Del 2
Kommentarer
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION