CodeGym /Java blog /Tilfældig /En forklaring af lambda-udtryk i Java. Med eksempler og o...
John Squirrels
Niveau
San Francisco

En forklaring af lambda-udtryk i Java. Med eksempler og opgaver. Del 1

Udgivet i gruppen
Hvem er denne artikel til?
  • Det er for folk, der tror, ​​de allerede kender Java Core godt, men som ikke har nogen anelse om lambda-udtryk i Java. Eller måske har de hørt noget om lambdaudtryk, men detaljerne mangler
  • Det er for folk, der har en vis forståelse af lambda-udtryk, men som stadig er forskrækket af dem og uvant til at bruge dem.
En forklaring af lambda-udtryk i Java.  Med eksempler og opgaver.  Del 1 - 1Hvis du ikke passer ind i en af ​​disse kategorier, kan du finde denne artikel kedelig, mangelfuld eller generelt ikke din kop te. I dette tilfælde er du velkommen til at gå videre til andre ting, eller, hvis du er velbevandret i emnet, bedes du komme med forslag i kommentarerne til, hvordan jeg kan forbedre eller supplere artiklen. Materialet hævder ikke at have nogen akademisk værdi, endsige nyhed. Tværtimod: Jeg vil forsøge at beskrive ting, der er komplekse (for nogle mennesker) så enkelt som muligt. En anmodning om at forklare Stream API inspirerede mig til at skrive dette. Jeg tænkte over det og besluttede, at nogle af mine stream-eksempler ville være uforståelige uden en forståelse af lambda-udtryk. Så vi starter med lambda-udtryk. Hvad skal du vide for at forstå denne artikel?
  1. Du bør forstå objektorienteret programmering (OOP), nemlig:

    • klasser, objekter og forskellen mellem dem;
    • grænseflader, hvordan de adskiller sig fra klasser, og forholdet mellem grænseflader og klasser;
    • metoder, hvordan man kalder dem, abstrakte metoder (dvs. metoder uden implementering), metodeparametre, metodeargumenter og hvordan man videregiver dem;
    • adgangsmodifikatorer, statiske metoder/variabler, endelige metoder/variabler;
    • nedarvning af klasser og grænseflader, multipel nedarvning af grænseflader.
  2. Kendskab til Java Core: generiske typer (generiske), samlinger (lister), tråde.
Nå, lad os komme til det.

Lidt historie

Lambda-udtryk kom til Java fra funktionel programmering og dertil fra matematik. I USA i midten af ​​det 20. århundrede arbejdede Alonzo Church, som var meget glad for matematik og alle former for abstraktioner, på Princeton University. Det var Alonzo Church, der opfandt lambda-regningen, som oprindeligt var et sæt abstrakte ideer, der var fuldstændig uden relation til programmering. Matematikere som Alan Turing og John von Neumann arbejdede på Princeton University på samme tid. Alt kom sammen: Church kom med lambda-regningen. Turing udviklede sin abstrakte computermaskine, nu kendt som "Turing-maskinen". Og von Neumann foreslog en computerarkitektur, der har dannet grundlaget for moderne computere (nu kaldet en "von Neumann-arkitektur"). På det tidspunkt var Alonzo Church' s ideer blev ikke så kendte som hans kollegers værker (med undtagelse af området ren matematik). Men lidt senere blev John McCarthy (også uddannet Princeton University og, på tidspunktet for vores historie, ansat ved Massachusetts Institute of Technology) interesseret i Kirkens ideer. I 1958 skabte han det første funktionelle programmeringssprog, LISP, baseret på disse ideer. Og 58 år senere lækket ideerne om funktionel programmering ind i Java 8. Ikke engang 70 år er gået... Helt ærligt, det er ikke den længste tid, det har taget for en matematisk idé at blive anvendt i praksis. en ansat ved Massachusetts Institute of Technology) blev interesseret i Kirkens ideer. I 1958 skabte han det første funktionelle programmeringssprog, LISP, baseret på disse ideer. Og 58 år senere lækket ideerne om funktionel programmering ind i Java 8. Ikke engang 70 år er gået... Helt ærligt, det er ikke den længste tid, det har taget for en matematisk idé at blive anvendt i praksis. en ansat ved Massachusetts Institute of Technology) blev interesseret i Kirkens ideer. I 1958 skabte han det første funktionelle programmeringssprog, LISP, baseret på disse ideer. Og 58 år senere lækket ideerne om funktionel programmering ind i Java 8. Ikke engang 70 år er gået... Helt ærligt, det er ikke den længste tid, det har taget for en matematisk idé at blive anvendt i praksis.

Sagens kerne

Et lambdaudtryk er en slags funktion. Du kan betragte det som en almindelig Java-metode, men med den karakteristiske evne til at blive overført til andre metoder som argument. Det er rigtigt. Det er blevet muligt at overføre ikke kun tal, strenge og katte til metoder, men også andre metoder! Hvornår kan vi få brug for dette? Det ville for eksempel være nyttigt, hvis vi ønsker at videregive en tilbagekaldsmetode. Det vil sige, hvis vi har brug for den metode, vi kalder, for at have evnen til at kalde en anden metode, som vi videregiver til den. Med andre ord, så vi har mulighed for at sende et tilbagekald under visse omstændigheder og et andet tilbagekald i andre. Og så vores metode, der modtager vores tilbagekald, kalder dem. Sortering er et simpelt eksempel. Antag, at vi skriver en smart sorteringsalgoritme, der ser sådan ud:

public void mySuperSort() { 
    // We do something here 
    if(compare(obj1, obj2) > 0) 
    // And then we do something here 
}
I iferklæringen kalder vi compare()metoden, at indsætte to objekter, der skal sammenlignes, og vi vil vide, hvilket af disse objekter der er "størst". Vi antager, at den "større" kommer før den "mindre". Jeg sætter "større" i anførselstegn, fordi vi skriver en universel metode, der vil vide, hvordan man sorterer ikke kun i stigende rækkefølge, men også i faldende rækkefølge (i dette tilfælde vil det "større" objekt faktisk være det "mindre" objekt , og omvendt). For at indstille den specifikke algoritme til vores sortering, har vi brug for en eller anden mekanisme til at overføre den til vores mySuperSort()metode. På den måde vil vi være i stand til at "kontrollere" vores metode, når den kaldes. Selvfølgelig kunne vi skrive to separate metoder - mySuperSortAscend()ogmySuperSortDescend()— til sortering i stigende og faldende rækkefølge. Eller vi kunne overføre et eller andet argument til metoden (for eksempel en boolsk variabel; hvis sand, sorteres i stigende rækkefølge, og hvis falsk, så i faldende rækkefølge). Men hvad nu hvis vi vil sortere noget kompliceret, såsom en liste over strenge-arrays? Hvordan vil vores mySuperSort()metode vide, hvordan man sorterer disse streng-arrays? Efter størrelse? Ved den kumulative længde af alle ordene? Måske alfabetisk baseret på den første streng i arrayet? Og hvad nu hvis vi skal sortere listen over arrays efter array-størrelse i nogle tilfælde og efter den kumulative længde af alle ordene i hver array i andre tilfælde? Jeg forventer, at du allerede har hørt om komparatorer, og at vi i dette tilfælde blot ville overføre et komparatorobjekt til vores sorteringsmetode, der beskriver den ønskede sorteringsalgoritme. Fordi standardensort()metode er implementeret ud fra samme princip som mySuperSort(), vil jeg bruge sort()i mine eksempler.

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
Her er arrays sorteret efter antallet af ord i hver array. En matrix med færre ord betragtes som "mindre". Derfor kommer det først. En række med flere ord betragtes som "større" og placeres i slutningen. Hvis vi sender en anden komparator til sort()metoden, såsom sortByCumulativeWordLength, får vi et andet resultat:

  1. if then else
  2. Dota GTA5 Halo
  3. I really love Java
Nu er arrays sorteret efter det samlede antal bogstaver i arrayets ord. I det første array er der 10 bogstaver, i det andet - 12 og i det tredje - 15. Hvis vi kun har en enkelt komparator, behøver vi ikke at erklære en separat variabel for den. I stedet kan vi simpelthen oprette en anonym klasse lige på tidspunktet for opkaldet til metoden sort(). Noget som dette:

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 får det samme resultat som i det første tilfælde. Opgave 1. Omskriv dette eksempel, så det sorterer arrays ikke i stigende rækkefølge efter antallet af ord i hver array, men i faldende rækkefølge. Alt dette ved vi allerede. Vi ved, hvordan man sender objekter til metoder. Afhængigt af hvad vi har brug for i øjeblikket, kan vi videregive forskellige objekter til en metode, som derefter vil påberåbe sig den metode, vi implementerede. Dette rejser spørgsmålet: hvorfor i alverden har vi brug for et lambdaudtryk her?  Fordi et lambda-udtryk er et objekt, der har præcis én metode. Som et "metodeobjekt". En metode pakket i et objekt. Den har bare en lidt ukendt syntaks (men mere om det senere). Lad os se på denne kode igen:

arrays.sort(new Comparator<String[]>() { 
    @Override 
    public int compare(String[] o1, String[] o2) { 
        return o1.length - o2.length; 
    } 
});
Her tager vi vores array-liste og kalder dens sort()metode, hvortil vi sender et komparatorobjekt med en enkelt compare()metode (dets navn betyder ikke noget for os - det er trods alt dette objekts eneste metode, så vi kan ikke gå galt). Denne metode har to parametre, som vi vil arbejde med. Hvis du arbejder i IntelliJ IDEA, har du sandsynligvis set det tilbyde at kondensere koden betydeligt som følger:

arrays.sort((o1, o2) -> o1.length - o2.length);
Dette reducerer seks linjer til en enkelt kort. 6 linjer omskrives som en kort. Noget forsvandt, men jeg garanterer, at det ikke var noget vigtigt. Denne kode vil fungere nøjagtigt på samme måde, som den ville gøre med en anonym klasse. Opgave 2. Gæt på at omskrive løsningen til opgave 1 ved hjælp af et lambdaudtryk (bed i det mindste IntelliJ IDEA om at konvertere din anonyme klasse til et lambdaudtryk).

Lad os tale om grænseflader

I princippet er en grænseflade blot en liste over abstrakte metoder. Når vi opretter en klasse, der implementerer en eller anden grænseflade, skal vores klasse implementere de metoder, der er inkluderet i grænsefladen (eller vi skal gøre klassen abstrakt). Der er grænseflader med mange forskellige metoder (for eksempel  List), og der er grænseflader med kun én metode (for eksempel Comparatoreller Runnable). Der er grænseflader, der ikke har en enkelt metode (såkaldte markørgrænseflader som f.eks. Serializable). Interfaces, der kun har én metode, kaldes også funktionelle grænseflader . I Java 8 er de endda markeret med en særlig anmærkning:@FunctionalInterface. Det er disse enkelt-metode-grænseflader, der er egnede som måltyper for lambda-udtryk. Som jeg sagde ovenfor, er et lambda-udtryk en metode pakket ind i et objekt. Og når vi passerer et sådant objekt, passerer vi i det væsentlige denne enkelte metode. Det viser sig, at vi er ligeglade med, hvad metoden hedder. Det eneste, der betyder noget for os, er metodeparametrene og selvfølgelig metodens krop. I det væsentlige er et lambda-udtryk implementeringen af ​​en funktionel grænseflade. Uanset hvor vi ser en grænseflade med en enkelt metode, kan en anonym klasse omskrives som en lambda. Hvis grænsefladen har mere eller mindre end én metode, vil et lambda-udtryk ikke fungere, og vi vil i stedet bruge en anonym klasse eller endda en forekomst af en almindelig klasse. Nu er det tid til at grave lidt i lambdas. :)

Syntaks

Den generelle syntaks er sådan her:

(parameters) -> {method body}
Det vil sige parenteser, der omgiver metodeparametrene, en "pil" (dannet af en bindestreg og større-end-tegn) og derefter metodens krop i klammer, som altid. Parametrene svarer til dem, der er angivet i interfacemetoden. Hvis variabeltyper entydigt kan bestemmes af compileren (i vores tilfælde ved den, at vi arbejder med string-arrays, fordi vores Listobjekt er skrevet med String[]), så behøver du ikke at angive deres typer.
Hvis de er tvetydige, så angiv typen. IDEA vil farve det gråt, hvis det ikke er nødvendigt.
Du kan læse mere i denne Oracle-tutorial og andre steder. Dette kaldes " target typing ". Du kan navngive variablerne, hvad du vil - du behøver ikke bruge de samme navne, der er angivet i grænsefladen. Hvis der ikke er nogen parametre, skal du blot angive tomme parenteser. Hvis der kun er én parameter, skal du blot angive variabelnavnet uden nogen parentes. Nu hvor vi forstår parametrene, er det tid til at diskutere kroppen af ​​lambda-udtrykket. Inde i de krøllede seler skriver du kode, ligesom du ville gøre for en almindelig metode. Hvis din kode består af en enkelt linje, kan du helt udelade de krøllede klammeparenteser (svarende til if-sætninger og for-løkker). Hvis din single-line lambda returnerer noget, behøver du ikke inkludere enreturnudmelding. Men hvis du bruger krøllede seler, så skal du eksplicit inkludere et returnudsagn, ligesom du ville gøre ved en almindelig metode.

Eksempler

Eksempel 1.

() -> {}
Det enkleste eksempel. Og det mest meningsløse :), da det ikke gør noget. Eksempel 2.

() -> ""
Endnu et interessant eksempel. Det tager intet og returnerer en tom streng ( returner udeladt, fordi det er unødvendigt). Her er det samme, men med return:

() -> { 
    return ""; 
}
Eksempel 3. "Hej, verden!" bruger lambdaer

() -> System.out.println("Hello, World!")
Den tager intet og returnerer intet (vi kan ikke sætte returnfør kaldet til System.out.println(), fordi println()metodens returtype er void). Det viser blot hilsenen. Dette er ideelt til en implementering af grænsefladen Runnable. Følgende eksempel er mere komplet:

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

public class Main { 
    public static void main(String[] args) { 
        Thread t = new Thread(() -> System.out.println("Hello, World!")); 
        t.start();
    } 
}
Eller vi kan endda gemme lambda-udtrykket som et Runnableobjekt og derefter videregive det til konstruktøren Thread:

public class Main { 
    public static void main(String[] args) { 
        Runnable runnable = () -> System.out.println("Hello, World!"); 
        Thread t = new Thread(runnable); 
        t.start(); 
    } 
}
Lad os se nærmere på det øjeblik, hvor et lambda-udtryk gemmes i en variabel. Grænsefladen Runnablefortæller os, at dens objekter skal have en public void run()metode. Ifølge grænsefladen runtager metoden ingen parametre. Og den returnerer intet, dvs. dens returtype er void. Derfor vil denne kode skabe et objekt med en metode, der ikke tager eller returnerer noget. Dette passer perfekt til Runnablegrænsefladens run()metode. Det er derfor, vi var i stand til at sætte dette lambda-udtryk i en Runnablevariabel.  Eksempel 4.

() -> 42
Igen tager det ingenting, men det returnerer tallet 42. Sådan et lambda-udtryk kan sættes i en Callablevariabel, fordi denne grænseflade kun har én metode, der ser sådan ud:

V call(),
hvor  V er returtypen (i vores tilfælde  int). Derfor kan vi gemme et lambda-udtryk som følger:

Callable<Integer> c = () -> 42;
Eksempel 5. Et lambda-udtryk, der involverer flere linjer

() -> { 
    String[] helloWorld = {"Hello", "World!"}; 
    System.out.println(helloWorld[0]); 
    System.out.println(helloWorld[1]); 
}
Igen er dette et lambda-udtryk uden parametre og en voidreturtype (fordi der ikke er nogen returnsætning).  Eksempel 6

x -> x
Her tager vi en xvariabel og returnerer den. Bemærk venligst, at hvis der kun er én parameter, så kan du udelade parenteserne omkring den. Her er det samme, men med parentes:

(x) -> x
Og her er et eksempel med en eksplicit returerklæring:

x -> { 
    return x;
}
Eller sådan med parentes og en returerklæring:

(x) -> { 
    return x;
}
Eller med en eksplicit angivelse af typen (og dermed med parentes):

(int x) -> x
Eksempel 7

x -> ++x
Vi tager xog returnerer den, men først efter at have tilføjet 1. Du kan omskrive den lambda sådan her:

x -> x + 1
I begge tilfælde udelader vi parenteserne omkring parameter- og metodelegemet sammen med sætningen return, da de er valgfrie. Versioner med parenteser og en returerklæring er angivet i eksempel 6. Eksempel 8

(x, y) -> x % y
Vi tager xog yog returnerer resten af ​​divisionen af x​​med y. Her kræves parentesen omkring parametrene. De er kun valgfrie, når der kun er én parameter. Her er det med en eksplicit angivelse af typerne:

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

(Cat cat, String name, int age) -> {
    cat.setName(name); 
    cat.setAge(age); 
}
Vi tager et Catobjekt, et Stringnavn og en int alder. I selve metoden bruger vi det beståede navn og alder til at indstille variabler på katten. Fordi vores catobjekt er en referencetype, vil det blive ændret uden for lambda-udtrykket (det vil få det beståede navn og alder). Her er en lidt mere kompliceret version, der bruger en lignende 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, Cathavde objektet én tilstand, og så ændrede tilstanden sig, efter at vi brugte lambda-udtrykket. Lambda-udtryk kombineres perfekt med generiske. Og hvis vi skal lave en Dogklasse, der også implementerer HasNameAndAge, så kan vi udføre de samme operationer på Dogi main() metoden uden at ændre lambda-udtrykket. Opgave 3. Skriv en funktionel grænseflade med en metode, der tager et tal og returnerer en boolsk værdi. Skriv en implementering af en sådan grænseflade som et lambda-udtryk, der returnerer sandt, hvis det beståede tal er deleligt med 13. Opgave 4.Skriv en funktionel grænseflade med en metode, der tager to strenge og også returnerer en streng. Skriv en implementering af en sådan grænseflade som et lambda-udtryk, der returnerer den længere streng. Opgave 5. Skriv en funktionel grænseflade med en metode, der tager tre flydende tal: a, b og c og også returnerer et flydende deal. Skriv en implementering af en sådan grænseflade som et lambda-udtryk, der returnerer diskriminanten. Hvis du har glemt det, er det D = b^2 — 4ac. Opgave 6. Brug den funktionelle grænseflade fra Opgave 5 til at skrive et lambda-udtryk, der returnerer resultatet af a * b^c. En forklaring af lambda-udtryk i Java. Med eksempler og opgaver. Del 2
Kommentarer
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION