You are viewing an old version of this page. View the current version.

Compare with Current View Page History

« Previous Version 7 Next »

Java 8 introduserer såkalte lambda-uttrykk som kan være svært nyttige i å forenkle kode og utføre funksjonell programmering i Java. For å kunne skjønne lambda-uttrykk bør vi først nevne litt om funksjonelle grensesnitt.

Funksjonelle grensesnitt

Definisjonen av funksjonelle grensesnitt er veldig enkel: Det er nemlig grensesnitt som har kun én metode definert. La oss definere et eksempel på dette:

Multiplier
public interface Multiplier {
    public double multiply(double x, double y);
}

Grensesnittet over er et funksjonelt grensesnitt, for det har bare én metode, nemlig multiply.

Vi kan ta i bruk dette grensesnittet på den tradisjonelle måten ved å opprette en egen klasse som implementerer Multiplier, og deretter opprette en instans av denne klassen, som vi kaller multiply fra. Men dette blir utrolig tungvint for å bruke en så enkel metode som multiply. For å slippe dette lar Java oss definere et objekt som implementerer grensesnittet direkte, ved at man definerer metoden(e) til grensesnittet i initaliseringen av objektet (se venstre kolonne under). Merk at dette fungerer også for grensesnitt med flere metoder.

Denne direkte implementasjonen sparer oss mye kode, men det er fortsatt ganske tungvint. Lambda-uttrykk lar oss gjøre dette mye enklere. Høyre kolonne viser hvordan man kan gjøre nøyaktig det samme, på én linje.

 

Direkte implementasjon
Multiplier multiplier = new Multiplier() {
    @Override
    public double multiply(double x, double y) {
        return x * y;
    }
};

 

 

Med lambda
Multiplier multiplier = (x, y) -> x * y;

 

Lambda-uttrykk

Den direkte implementasjonen over er på seks linjer, men det er kun linje fire som er interessant: Hva er det multiply skal returnere? Resten av linjene er unødvendige, for vi vet dem fra før av – det samme står i definisjonen av grensesnittet. Vi vet at en implementasjon av Multiplier må ha en metode som heter multiply, den må være public, den må ta to doubles som argumenter, og returnere en double. Lambda-operatoren (->) lar oss forenkle dette. Til venstre for pila står argumentene, og til høyre for pila står hva som skal returneres. Merk at dette fungerer kun for funksjonelle grensesnitt, for ellers vil ikke lambda-operatoren vite hvilken metode som skal kalles.

Innebygde funksjonelle grensesnitt i Java 8

For å kunne utnytte kraften i lambda til det fulle, er de oftest brukte funksjonelle grensesnittene implementert i Java, så man slipper å definere dem selv. Vi skal snakke litt om grensesnittene Predicate og Consumer.

Predicate og Consumer

Predicate-grensesnittet har metoden test, som tar inn et objekt av hvilken som helst type som argument, og returnerer en boolean.

Consumer-grensesnittet har metoden accept, som tar inn et objekt av hvilken som helst type som argument, og returnerer ingenting (void).

Bruken av disse skal vi demonstrere senere.

Stream

Streams gjør det veldig enkelt for oss å utføre operasjoner på lister på en kort og elegant måte, i kombinasjon med lambda og ofte Predicate og Consumer. I stedet for å forklare hva streams er, skal vi heller vise eksempler på bruken av det. Vi definerer en Person-klasse for å ha noe å leke med:

Person.java
public class Person {
    private String name;
    private int age;
    private char gender;

    public Person(String name, int age, char gender) {
        setName(name);
        setAge(age);
        setGender(gender);
    }

    public String getName() { return name; }

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

    public int getAge() { return age; }

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

    public char getGender() { return gender; }

    public void setGender(char gender) { this.gender = gender; }

    public String toString() { return name + " " + age + " " + gender; }
}

 

Vi oppretter også en PersonMain-klasse som skal inneholde en liste over Person-objekter, så vi kan demonstrere bruken av streams:

PersonMain.java
public class Main {

    List<Person> persons = new ArrayList<Person>();

    public void init() {
    	persons.add(new Person("Ola", 10, 'M'));
        persons.add(new Person("Kari", 12, 'F'));
        persons.add(new Person("Per", 22, 'M'));
        persons.add(new Person("Pål", 17, 'M'));
        persons.add(new Person("Espen", 19, 'M'));
    }

    public void run() {
    }

    public static void main(String[] args) {
        Main main = new Main();
		main.init();
        main.run();
    }

Eksemplene under vil være forskjellige implementasjoner av run()-metoden til PersonMain.java.

 

anyMatch

Vi begynner med et enkelt og nyttig eksempel på bruk av Predicate-grensesnittet og streams. anyMatch er en metode som tar inn en Predicate-instans og returnerer true dersom minst ett av elementene i den aktuelle streamen tilfredsstiller predikatet. For eksempel, finnes det en kvinne i lista vår?

Tradisjonell måte
boolean womanExists = false; 
for (Person p : persons) {
    if (p.getGender() == 'F') {
        womanExists = true;
		break;
    }
}
Med stream og lambda
 boolean womanExists = persons.stream().anyMatch(p -> p.getGender() == 'F');

Til høyre tar vi lista vår persons, kaller metoden stream() på den for å gjøre den til en stream og få tak i den innebygde anyMatch-metoden. anyMatch tar som kjent et predikatobjekt som argument, som vi definerer på lambdavis. Predicate-instansen vi oppretter får inn et Person-objekt (p) som argument, og returnerer true dersom den aktuelle personens kjønn er kvinne. 

Det er flere andre metoder som ligner på anyMatch: allMatch (alle element i en stream tilfredsstiller predikatet), noneMatch (ingen matcher), findAny (returnerer et element som tilfredsstiller predikatet), findFirst (returnerer det første elementet som tilfredsstiller predikatet).

 

Filter

Filter er en svært vanlig operasjon på lister (i likhet med map og reduce som vi nevner senere), som er mange programmeringsspråk har støtte for. Filter kalles på en liste, og returnerer en ny liste med kun de elementene som tilfredsstiller et gitt predikat. For eksempel: Hvilke personer er over 18 år?

 

Tradisjonell måte
List<Person> overEighteen = new ArrayList<Person>();
for (Person p : persons) {
    if (p.getAge() >= 18) {
        overEighteen.add(p);
    }
}
System.out.println(overEighteen);

 

 

Med stream og lambda
 System.out.println(persons.stream().filter(p -> p.getAge() >= 18).collect(Collectors.toList()));

 

Siden filter-funksjonen returnerer en stream, bruker vi collect for å gjøre den til en List.

 

forEach

forEach tar inn en Consumer-instans og kaller denne instansens ene metode på alle elementene i streamen. La oss legge til et år på alle personenes alder.

 

Tradisjonell måte
for (Person p : persons) {
    p.setAge(p.getAge() + 1);
}

 

 

Med lambda
persons.stream().forEach(p -> p.setAge(p.getAge() + 1));

 

Vi kan også kombinere funksjoner på streams, for eksempel filter og forEach. La oss si vi vil legge til et år på alderen til alle gutter under 18:

 

Tradisjonell måte
for (Person p : persons) {
    if (p.getAge() < 18 && p.getGender() == 'M')
        p.setAge(p.getAge() + 1);
}

 

 

Med lambda
persons.stream()
	.filter(p -> p.getAge() < 18 && p.getGender() == 'M')
	.forEach(p -> p.setAge(p.getAge() + 1));

 

 

 

  • No labels