CodeGym/Java-blogg/Tilfeldig/En forklaring av lambda-uttrykk i Java. Med eksempler og ...
John Squirrels
Nivå
San Francisco

En forklaring av lambda-uttrykk i Java. Med eksempler og oppgaver. Del 1

Publisert i gruppen
Hvem er denne artikkelen for?
  • Det er for folk som tror de allerede kjenner Java Core godt, men som ikke har peiling på lambda-uttrykk i Java. Eller kanskje de har hørt noe om lambda-uttrykk, men detaljene mangler
  • Det er for folk som har en viss forståelse av lambda-uttrykk, men som fortsatt er skremt av dem og uvant med å bruke dem.
En forklaring av lambda-uttrykk i Java.  Med eksempler og oppgaver.  Del 1 - 1Hvis du ikke passer inn i en av disse kategoriene, kan det hende du synes denne artikkelen er kjedelig, defekt eller generelt sett ikke er din kopp te. I dette tilfellet kan du gjerne gå videre til andre ting eller, hvis du er godt kjent med emnet, vennligst kom med forslag i kommentarene om hvordan jeg kan forbedre eller supplere artikkelen. Materialet hevder ikke å ha noen akademisk verdi, enn si nyhet. Tvert imot: Jeg vil prøve å beskrive ting som er komplekse (for noen mennesker) så enkelt som mulig. En forespørsel om å forklare Stream API inspirerte meg til å skrive dette. Jeg tenkte på det og bestemte meg for at noen av strømeksemplene mine ville være uforståelige uten forståelse av lambda-uttrykk. Så vi starter med lambda-uttrykk. Hva trenger du å vite for å forstå denne artikkelen?
  1. Du bør forstå objektorientert programmering (OOP), nemlig:

    • klasser, objekter og forskjellen mellom dem;
    • grensesnitt, hvordan de skiller seg fra klasser, og forholdet mellom grensesnitt og klasser;
    • metoder, hvordan man kaller dem, abstrakte metoder (dvs. metoder uten implementering), metodeparametere, metodeargumenter og hvordan man passerer dem;
    • tilgangsmodifikatorer, statiske metoder/variabler, endelige metoder/variabler;
    • arv av klasser og grensesnitt, multippel arv av grensesnitt.
  2. Kjennskap til Java Core: generiske typer (generiske), samlinger (lister), tråder.
Vel, la oss komme til det.

Litt historie

Lambda-uttrykk kom til Java fra funksjonell programmering, og dit fra matematikk. I USA på midten av 1900-tallet jobbet Alonzo Church, som var veldig glad i matematikk og alle slags abstraksjoner, ved Princeton University. Det var Alonzo Church som oppfant lambda-kalkulen, som opprinnelig var et sett med abstrakte ideer som ikke var relatert til programmering. Matematikere som Alan Turing og John von Neumann jobbet ved Princeton University på samme tid. Alt hang sammen: Church kom med lambdaregningen. Turing utviklet sin abstrakte datamaskin, nå kjent som "Turing-maskinen". Og von Neumann foreslo en datamaskinarkitektur som har dannet grunnlaget for moderne datamaskiner (nå kalt en "von Neumann-arkitektur"). På den tiden, Alonzo Church' s ideer ble ikke så kjent som verkene til kollegene hans (med unntak av feltet ren matematikk). Men litt senere ble John McCarthy (også utdannet Princeton University og, på tidspunktet for vår historie, en ansatt ved Massachusetts Institute of Technology) interessert i Kirkens ideer. I 1958 skapte han det første funksjonelle programmeringsspråket, LISP, basert på disse ideene. Og 58 år senere lekket ideene om funksjonell programmering inn i Java 8. Ikke engang 70 år har gått... Ærlig talt, dette er ikke det lengste det har tatt før en matematisk idé blir brukt i praksis. en ansatt ved Massachusetts Institute of Technology) ble interessert i Kirkens ideer. I 1958 skapte han det første funksjonelle programmeringsspråket, LISP, basert på disse ideene. Og 58 år senere lekket ideene om funksjonell programmering inn i Java 8. Ikke engang 70 år har gått... Ærlig talt, dette er ikke det lengste det har tatt før en matematisk idé blir brukt i praksis. en ansatt ved Massachusetts Institute of Technology) ble interessert i Kirkens ideer. I 1958 skapte han det første funksjonelle programmeringsspråket, LISP, basert på disse ideene. Og 58 år senere lekket ideene om funksjonell programmering inn i Java 8. Ikke engang 70 år har gått... Ærlig talt, dette er ikke det lengste det har tatt før en matematisk idé blir brukt i praksis.

Sakens kjerne

Et lambda-uttrykk er en slags funksjon. Du kan betrakte det som en vanlig Java-metode, men med den særegne evnen til å overføres til andre metoder som argument. Det er riktig. Det har blitt mulig å overføre ikke bare tall, strenger og katter til metoder, men også andre metoder! Når kan vi trenge dette? Det vil for eksempel være nyttig hvis vi ønsker å overføre en tilbakeringingsmetode. Det vil si, hvis vi trenger metoden vi kaller for å ha muligheten til å kalle en annen metode som vi overfører til den. Med andre ord, så vi har muligheten til å sende en tilbakeringing under visse omstendigheter og en annen tilbakeringing i andre. Og slik at metoden vår som mottar tilbakeringingene våre, ringer dem. Sortering er et enkelt eksempel. Anta at vi skriver en smart sorteringsalgoritme som ser slik ut:
public void mySuperSort() {
    // We do something here
    if(compare(obj1, obj2) > 0)
    // And then we do something here
}
I ifutsagnet kaller vi compare()metoden, å sende inn to objekter som skal sammenlignes, og vi vil vite hvilken av disse objektene som er "større". Vi antar at den "større" kommer før den "mindre". Jeg setter "større" i anførselstegn, fordi vi skriver en universell metode som vil vite hvordan man sorterer ikke bare i stigende rekkefølge, men også i synkende rekkefølge (i dette tilfellet vil det "større" objektet faktisk være det "mindre" objektet , og vice versa). For å angi den spesifikke algoritmen for vår sortering, trenger vi en mekanisme for å overføre den til mySuperSort()metoden vår. På den måten vil vi kunne "kontrollere" metoden vår når den kalles. Selvfølgelig kan vi skrive to separate metoder - mySuperSortAscend()ogmySuperSortDescend()— for sortering i stigende og synkende rekkefølge. Eller vi kan sende et eller annet argument til metoden (for eksempel en boolsk variabel; hvis sann, så sorter i stigende rekkefølge, og hvis usann, så i synkende rekkefølge). Men hva om vi vil sortere noe komplisert, for eksempel en liste over strengmatriser? Hvordan vil metoden vår mySuperSort()vite hvordan man sorterer disse strengmatrisene? Etter størrelse? Etter den kumulative lengden på alle ordene? Kanskje alfabetisk basert på den første strengen i matrisen? Og hva om vi trenger å sortere listen over matriser etter matrisestørrelse i noen tilfeller, og etter den kumulative lengden på alle ordene i hver matrise i andre tilfeller? Jeg forventer at du allerede har hørt om komparatorer og at vi i dette tilfellet ganske enkelt vil overføre til vår sorteringsmetode et komparatorobjekt som beskriver den ønskede sorteringsalgoritmen. Fordi standardensort()metode er implementert basert på samme prinsipp som mySuperSort(), jeg vil bruke 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:
Dota GTA5 Halo
if then else
I really love Java
Her er matrisene sortert etter antall ord i hver matrise. En matrise med færre ord anses som "mindre". Det er derfor det kommer først. En matrise med flere ord anses som "større" og blir plassert på slutten. Hvis vi sender en annen komparator til sort()metoden, for eksempel sortByCumulativeWordLength, får vi et annet resultat:
if then else
Dota GTA5 Halo
I really love Java
Nå er are-matrisene sortert etter det totale antallet bokstaver i ordene i matrisen. I den første matrisen er det 10 bokstaver, i den andre - 12, og i den tredje - 15. Hvis vi bare har en enkelt komparator, trenger vi ikke å deklarere en separat variabel for den. I stedet kan vi ganske enkelt opprette en anonym klasse rett på tidspunktet for kallet til metoden sort(). Noe sånt 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 samme resultat som i det første tilfellet. Oppgave 1. Skriv om dette eksempelet slik at det sorterer matriser ikke i stigende rekkefølge etter antall ord i hver matrise, men i synkende rekkefølge. Alt dette vet vi allerede. Vi vet hvordan vi sender objekter til metoder. Avhengig av hva vi trenger for øyeblikket, kan vi sende forskjellige objekter til en metode, som deretter vil påkalle metoden vi implementerte. Dette reiser spørsmålet: hvorfor i all verden trenger vi et lambda-uttrykk her?  Fordi et lambda-uttrykk er et objekt som har nøyaktig én metode. Som et "metodeobjekt". En metode pakket i et objekt. Den har bare en litt ukjent syntaks (men mer om det senere). La oss ta en ny titt på denne koden:
arrays.sort(new Comparator<String[]>() {
    @Override
    public int compare(String[] o1, String[] o2) {
        return o1.length - o2.length;
    }
});
Her tar vi arraylisten vår og kaller sort()metoden dens, som vi sender et komparatorobjekt til med en enkelt compare()metode (navnet har ingen betydning for oss - tross alt er det dette objektets eneste metode, så vi kan ikke gå galt). Denne metoden har to parametere som vi skal jobbe med. Hvis du jobber i IntelliJ IDEA, har du sannsynligvis sett at det tilbyr å kondensere koden betydelig som følger:
arrays.sort((o1, o2) -> o1.length - o2.length);
Dette reduserer seks linjer til en enkelt kort. 6 linjer skrives om til en kort. Noe forsvant, men jeg garanterer at det ikke var noe viktig. Denne koden vil fungere nøyaktig på samme måte som den ville gjort med en anonym klasse. Oppgave 2. Gjett om å omskrive løsningen til Oppgave 1 ved å bruke et lambda-uttrykk (be i det minste IntelliJ IDEA om å konvertere den anonyme klassen din til et lambda-uttrykk).

La oss snakke om grensesnitt

I prinsippet er et grensesnitt ganske enkelt en liste over abstrakte metoder. Når vi lager en klasse som implementerer et eller annet grensesnitt, må klassen vår implementere metodene som er inkludert i grensesnittet (eller vi må gjøre klassen abstrakt). Det finnes grensesnitt med mange forskjellige metoder (for eksempel  List), og det er grensesnitt med bare én metode (for eksempel Comparatoreller Runnable). Det finnes grensesnitt som ikke har en enkelt metode (såkalte markørgrensesnitt som Serializable). Grensesnitt som kun har én metode kalles også funksjonelle grensesnitt . I Java 8 er de til og med merket med en spesiell merknad:@FunctionalInterface. Det er disse enkeltmetodegrensesnittene som egner seg som måltyper for lambda-uttrykk. Som jeg sa ovenfor, er et lambda-uttrykk en metode pakket inn i et objekt. Og når vi passerer et slikt objekt, passerer vi i hovedsak denne enkeltmetoden. Det viser seg at vi ikke bryr oss om hva metoden heter. Det eneste som betyr noe for oss er metodeparameterne og selvfølgelig selve metoden. I hovedsak er et lambda-uttrykk implementeringen av et funksjonelt grensesnitt. Uansett hvor vi ser et grensesnitt med en enkelt metode, kan en anonym klasse skrives om til en lambda. Hvis grensesnittet har mer eller mindre enn én metode, vil ikke et lambda-uttrykk fungere, og vi vil i stedet bruke en anonym klasse eller til og med en forekomst av en vanlig klasse. Nå er det på tide å grave litt i lambdas. :)

Syntaks

Den generelle syntaksen er omtrent slik:
(parameters) -> {method body}
Det vil si parenteser rundt metodeparameterne, en "pil" (dannet av en bindestrek og større enn-tegn), og deretter metodekroppen i parentes, som alltid. Parametrene tilsvarer de som er spesifisert i grensesnittmetoden. Hvis variabeltyper entydig kan bestemmes av kompilatoren (i vårt tilfelle vet den at vi jobber med string arrays, fordi Listobjektet vårt er skrevet med String[]), trenger du ikke å angi typene deres.
Hvis de er tvetydige, angi typen. IDEA vil farge den grå hvis den ikke er nødvendig.
Du kan lese mer i denne Oracle-opplæringen og andre steder. Dette kalles " målskriving ". Du kan navngi variablene hva du vil – du trenger ikke bruke de samme navnene som er spesifisert i grensesnittet. Hvis det ikke er noen parametere, angi bare tomme parenteser. Hvis det bare er én parameter, angi variabelnavnet uten noen parenteser. Nå som vi forstår parametrene, er det på tide å diskutere kroppen til lambda-uttrykket. Inne i de krøllete tannreguleringene skriver du kode akkurat som du ville gjort for en vanlig metode. Hvis koden din består av en enkelt linje, kan du utelate de krøllete klammeparentesene helt (ligner på if-setninger og for-løkker). Hvis din enlinjes lambda returnerer noe, trenger du ikke å inkludere enreturnuttalelse. Men hvis du bruker krøllete tannregulering, må du eksplisitt inkludere et returnutsagn, akkurat som du ville gjort med en vanlig metode.

Eksempler

Eksempel 1.
() -> {}
Det enkleste eksempelet. Og det mest meningsløse :), siden det ikke gjør noe. Eksempel 2.
() -> ""
Et annet interessant eksempel. Den tar ingenting og returnerer en tom streng ( returner utelatt, fordi det er unødvendig). Her er det samme, men med return:
() -> {
    return "";
}
Eksempel 3. "Hei, verden!" bruker lambdaer
() -> System.out.println("Hello, World!")
Den tar ingenting og returnerer ingenting (vi kan ikke sette returnfør kallet til System.out.println(), fordi println()metodens returtype er void). Den viser bare hilsenen. Dette er ideelt for en implementering av Runnablegrensesnittet. Følgende eksempel er mer komplett:
public class Main {
    public static void main(String[] args) {
        new Thread(() -> System.out.println("Hello, World!")).start();
    }
}
Eller slik:
public class Main {
    public static void main(String[] args) {
        Thread t = new Thread(() -> System.out.println("Hello, World!"));
        t.start();
    }
}
Eller vi kan til og med lagre lambda-uttrykket som et Runnableobjekt og deretter sende 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();
    }
}
La oss se nærmere på øyeblikket da et lambda-uttrykk lagres i en variabel. Grensesnittet Runnableforteller oss at objektene må ha en public void run()metode. I henhold til grensesnittet runtar metoden ingen parametere. Og den returnerer ingenting, dvs. returtypen er void. Følgelig vil denne koden lage et objekt med en metode som ikke tar eller returnerer noe. Dette samsvarer perfekt med Runnablegrensesnittets run()metode. Det er derfor vi kunne sette dette lambda-uttrykket i en Runnablevariabel.  Eksempel 4.
() -> 42
Igjen, det tar ingenting, men det returnerer tallet 42. Et slikt lambda-uttrykk kan settes i en Callablevariabel, fordi dette grensesnittet har bare én metode som ser omtrent slik ut:
V call(),
hvor  V er returtypen (i vårt tilfelle,  int). Følgelig kan vi lagre et lambda-uttrykk som følger:
Callable<Integer> c = () -> 42;
Eksempel 5. Et lambda-uttrykk som involverer flere linjer
() -> {
    String[] helloWorld = {"Hello", "World!"};
    System.out.println(helloWorld[0]);
    System.out.println(helloWorld[1]);
}
Igjen, dette er et lambda-uttrykk uten parametere og en voidreturtype (fordi det ikke er noen returnsetning).  Eksempel 6
x -> x
Her tar vi en xvariabel og returnerer den. Vær oppmerksom på at hvis det bare er én parameter, kan du utelate parentesene rundt den. Her er det samme, men med parentes:
(x) -> x
Og her er et eksempel med en eksplisitt returerklæring:
x -> {
    return x;
}
Eller slik med parenteser og en returerklæring:
(x) -> {
    return x;
}
Eller med en eksplisitt angivelse av typen (og dermed med parentes):
(int x) -> x
Eksempel 7
x -> ++x
Vi tar xog returnerer den, men bare etter å ha lagt til 1. Du kan omskrive den lambdaen slik:
x -> x + 1
I begge tilfeller utelater vi parentesene rundt parameteren og metodekroppen, sammen med setningen return, siden de er valgfrie. Versjoner med parenteser og en retursetning er gitt i eksempel 6. Eksempel 8
(x, y) -> x % y
Vi tar xog yog returnerer resten av deling av xmed y. Her kreves parentesene rundt parameterne. De er valgfrie bare når det bare er én parameter. Her er det med en eksplisitt angivelse av typene:
(double x, int y) -> x % y
Eksempel 9
(Cat cat, String name, int age) -> {
    cat.setName(name);
    cat.setAge(age);
}
Vi tar et Catobjekt, et Stringnavn og en int alder. I selve metoden bruker vi bestått navn og alder for å sette variabler på katten. Fordi catobjektet vårt er en referansetype, vil det bli endret utenfor lambda-uttrykket (det vil få bestått navn og alder). Her er en litt mer komplisert versjon som bruker 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 ser Cathadde objektet én tilstand, og så endret tilstanden seg etter at vi brukte lambda-uttrykket. Lambda-uttrykk kombineres perfekt med generiske. Og hvis vi trenger å lage en Dogklasse som også implementerer HasNameAndAge, så kan vi utføre de samme operasjonene på Dogi main() metoden uten å endre lambda-uttrykket. Oppgave 3. Skriv et funksjonelt grensesnitt med en metode som tar et tall og returnerer en boolsk verdi. Skriv en implementering av et slikt grensesnitt som et lambda-uttrykk som returnerer sant hvis det beståtte tallet er delelig med 13. Oppgave 4.Skriv et funksjonelt grensesnitt med en metode som tar to strenger og også returnerer en streng. Skriv en implementering av et slikt grensesnitt som et lambda-uttrykk som returnerer den lengre strengen. Oppgave 5. Skriv et funksjonelt grensesnitt med en metode som tar tre flytende tall: a, b og c og også returnerer et flyttall. Skriv en implementering av et slikt grensesnitt som et lambda-uttrykk som returnerer diskriminanten. I tilfelle du har glemt det, er det D = b^2 — 4ac. Oppgave 6. Bruk det funksjonelle grensesnittet fra Oppgave 5, skriv et lambda-uttrykk som returnerer resultatet av a * b^c. En forklaring av lambda-uttrykk i Java. Med eksempler og oppgaver. Del 2
Kommentarer
  • Populær
  • Ny
  • Gammel
Du må være pålogget for å legge igjen en kommentar
Denne siden har ingen kommentarer ennå