| author | Carsten Gips (HSBI) |
|---|---|
| title | Lambda-Ausdrücke und funktionale Interfaces |
::: tldr Mit einer anonymen inneren Klasse erstellt man gewissermaßen ein Objekt einer "Wegwerf"-Klasse: Man leitet on-the-fly von einem Interface ab oder erweitert eine Klasse und implementiert die benötigten Methoden und erzeugt von dieser Klasse sofort eine Instanz (Objekt). Diese neue Klasse ist im restlichen Code nicht sichtbar.
Anonyme innere Klassen sind beispielsweise in Swing recht nützlich, wenn man einer Komponente einen Listener mitgeben will: Hier erzeugt man eine anonyme innere Klasse basierend auf dem passenden Listener-Interface, implementiert die entsprechenden Methoden und übergibt das mit dieser Klasse erzeugte Objekt als neuen Listener der Swing-Komponente.
Mit Java 8 können unter gewissen Bedingungen diese anonymen inneren Klassen zu Lambda-Ausdrücken (und Methoden-Referenzen) vereinfacht werden. Dazu muss die anonyme innere Klasse ein sogenanntes funktionales Interface implementieren.
Funktionale Interfaces sind Interfaces mit genau einer abstrakten Methode. Es
können beliebig viele Default-Methoden im Interface enthalten sein, und es können
public sichtbare abstrakte Methoden von java.lang.Object geerbt/überschrieben
werden.
Die Lambda-Ausdrücke entsprechen einer anonymen Methode: Die Parameter werden aufgelistet (in Klammern), und hinter einem Pfeil kommt entweder ein Ausdruck (Wert - gleichzeitig Rückgabewert des Lambda-Ausdrucks) oder beliebig viele Anweisungen (in geschweiften Klammern, mit Semikolon):
- Form 1:
(parameters) -> expression - Form 2:
(parameters) -> { statements; }
Der Lambda-Ausdruck muss von der Signatur her genau der einen abstrakten Methode im unterliegenden funktionalen Interface entsprechen. :::
::: youtube
- VL Lambda-Ausdrücke und funktionale Interfaces
- Demo Anonyme innere Klasse
- Demo Lambda-Ausdruck
- Demo Funktionale Interfaces selbst definiert
- Demo Vordefinierte funktionale Interfaces im JDK :::
List<Studi> sl = new ArrayList<>();
// Liste sortieren?
sl.sort(???); // Parameter: java.util.Comparator<Studi>\pause \bigskip
public class MyCompare implements Comparator<Studi> {
@Override public int compare(Studi o1, Studi o2) {
return o1.getCredits() - o2.getCredits();
}
}// Liste sortieren?
MyCompare mc = new MyCompare();
sl.sort(mc);::: notes
Da Comparator<T> ein Interface ist, muss man eine extra Klasse anlegen, die die
abstrakte Methode aus dem Interface implementiert und ein Objekt von dieser Klasse
erzeugen und dieses dann der sort()-Methode übergeben.
Die Klasse bekommt wie in Java üblich eine eigene Datei und ist damit in der Package-Struktur offen sichtbar und "verstopft" mir damit die Strukturen: Diese Klasse ist doch nur eine Hilfsklasse ... Noch schlimmer: Ich brauche einen Namen für diese Klasse!
Den ersten Punkt könnte man über verschachtelte Klassen lösen: Die Hilfsklasse wird innerhalb der Klasse definiert, die das Objekt benötigt. Für den zweiten Punkt brauchen wir mehr Anlauf ... :::
::: notes
Man kann Klassen innerhalb von Klassen definieren: Verschachtelte Klassen.
- Implizite Referenz auf Instanz der äußeren Klasse, Zugriff auf alle Elemente
- Begriffe:
- "normale" innere Klassen: "inner classes"
- statische innere Klassen: "static nested classes"
- Einsatzzweck:
- Hilfsklassen: Zusätzliche Funktionalität kapseln; Nutzung nur in äußerer Klasse
- Kapselung von Rückgabewerten
Sichtbarkeit: Wird u.U. von äußerer Klasse "überstimmt"
- Objekt der äußeren Klasse muss existieren
- Innere Klasse ist normales Member der äußeren Klasse
- Implizite Referenz auf Instanz äußerer Klasse
- Zugriff auf alle Elemente der äußeren Klasse
- Sonderfall: Definition innerhalb von Methoden ("local classes")
- Nur innerhalb der Methode sichtbar
- Kennt zusätzlich
finalAttribute der Methode
Beispiel:
public class Outer {
...
private class Inner {
...
}
Outer.Inner inner = new Outer().new Inner();
}[Beispiel mit Iterator als innere Klasse: nested.StudiListNested]{.ex href="https://github.com/Programmiermethoden-CampusMinden/Prog2-Lecture/blob/master/lecture/java-modern/src/nested/StudiListNested.java"}
- Keine implizite Referenz auf Objekt
- Nur Zugriff auf Klassenmethoden und -attribute
Beispiel:
class Outer {
...
static class StaticNested {
...
}
}
Outer.StaticNested nested = new Outer.StaticNested();:::
List<Studi> sl = new ArrayList<>();
// Parametrisierung mit anonymer Klasse
sl.sort(
new Comparator<Studi>() {
@Override
public int compare(Studi o1, Studi o2) {
return o1.getCredits() - o2.getCredits();
}
}); // Semikolon nicht vergessen!!!::: notes
=> Instanz einer anonymen inneren Klasse, die das Interface Comparator<Studi>
implementiert
- Für spezielle, einmalige Aufgabe: nur eine Instanz möglich
- Kein Name, kein Konstruktor, oft nur eine Methode
- Müssen Interface implementieren oder andere Klasse erweitern
- Achtung Schreibweise: ohne
implementsoderextends!
- Achtung Schreibweise: ohne
- Konstruktor kann auch Parameter aufweisen
- Zugriff auf alle Attribute der äußeren Klasse plus alle
finallokalen Variablen - Nutzung typischerweise bei GUIs: Event-Handler etc. :::
[Demo: nested.DemoAnonymousInnerClass]{.ex href="https://github.com/Programmiermethoden-CampusMinden/Prog2-Lecture/blob/master/lecture/java-modern/src/nested/DemoAnonymousInnerClass.java"}
List<Studi> sl = new ArrayList<>();
// Parametrisierung mit anonymer Klasse
sl.sort(
new Comparator<Studi>() {
@Override
public int compare(Studi o1, Studi o2) {
return o1.getCredits() - o2.getCredits();
}
}); // Semikolon nicht vergessen!!!
// Parametrisierung mit Lambda-Ausdruck
sl.sort( (Studi o1, Studi o2) -> o1.getCredits() - o2.getCredits() );[[Hinweis auf funktionales Interface]{.ex}]{.slides}
::: notes Anmerkung: Damit für den Parameter alternativ auch ein Lambda-Ausdruck verwendet werden kann, muss der erwartete Parameter vom Typ her ein "funktionales Interface" (s.u.) sein! :::
[Demo: nested.DemoLambda]{.ex href="https://github.com/Programmiermethoden-CampusMinden/Prog2-Lecture/blob/master/lecture/java-modern/src/nested/DemoLambda.java"}
(Studi o1, Studi o2) -> o1.getCredits() - o2.getCredits()::: notes Ein Lambda-Ausdruck ist eine Funktion ohne Namen und besteht aus drei Teilen:
- Parameterliste (in runden Klammern),
- Pfeil
- Funktionskörper (rechte Seite)
Falls es genau einen Parameter gibt, können die runden Klammern um den Parameter entfallen.
Dabei kann der Funktionskörper aus einem Ausdruck ("expression") bestehen oder einer Menge von Anweisungen ("statements"), die dann in geschweifte Klammern eingeschlossen werden müssen (Block mit Anweisungen).
Der Wert des Ausdrucks ist zugleich der Rückgabewert des Lambda-Ausdrucks. :::
\bigskip \bigskip
Varianten:
(parameters) -> expression
\smallskip
(parameters) -> { statements; }
() -> {}() -> "wuppie"() -> { return "fluppie"; }(Integer i) -> return i + 42;(String s) -> { "foo"; }(String s) -> s.length()(Studi s) -> s.getCredits() > 300(List<Studi> sl) -> sl.isEmpty()(int x, int y) -> { System.out.println("Erg: "); System.out.println(x+y); }() -> new Studi()s -> s.getCps() > 100 && s.getCps() < 300s -> { return s.getCps() > 100 && s.getCps() < 300; }
:::: notes ::: details Auflösung: (4) und (5)
return ist eine Anweisung, d.h. bei (4) fehlen die geschweiften Klammern. "foo"
ist ein String und als solcher ein Ausdruck, d.h. hier sind die geschweiften
Klammern zu viel (oder man ergänze den String mit einem return, also
return "foo"; ...).
:::
::::
@FunctionalInterface
public interface Wuppie<T> {
int wuppie(T obj);
boolean equals(Object obj);
default int fluppie() { return 42; }
}\bigskip
Wuppie<T> ist ein funktionales Interface ("functional interface") [(seit
Java 8)]{.notes}
- Hat genau eine abstrakte Methode
- Hat evtl. weitere Default-Methoden
- Hat evtl. weitere abstrakte Methoden, die
publicMethoden vonjava.lang.Objectüberschreiben
::: notes
Die Annotation @FunctionalInterface selbst ist nur für den Compiler: Falls das
Interface kein funktionales Interface ist, würde er beim Vorhandensein dieser
Annotation einen Fehler werfen. Oder anders herum: Allein durch das Annotieren mit
@FunctionalInterface wird aus einem Interface noch kein funktionales Interface!
Vergleichbar mit @Override ...
Während man für eine anonyme Klasse lediglich ein "normales" Interface (oder eine Klasse) benötigt, braucht man für Lambda-Ausdrücke zwingend ein passendes funktionales Interface!
Anmerkung: Es scheint keine einheitliche deutsche Übersetzung für den Begriff functional interface zu geben. Es wird häufig mit "funktionales Interface", manchmal aber auch mit "Funktionsinterface" übersetzt.
Das in den obigen Beispielen eingesetzte Interface java.util.Comparator<T> ist
also ein funktionales Interface: Es hat nur eine eigene abstrakte Methode
int compare(T o1, T o2);.
Im Package
java.util.function
sind einige wichtige funktionale Interfaces bereits vordefiniert, beispielsweise
Predicate (Test, ob eine Bedingung erfüllt ist) und Function (verarbeite einen
Wert und liefere einen passenden Ergebniswert). Diese kann man auch in eigenen
Projekten nutzen!
:::
public interface Wuppie {
int wuppie(int a);
}
public interface Fluppie extends Wuppie {
int wuppie(double a);
}
public interface Foo {
}
public interface Bar extends Wuppie {
default int bar() { return 42; }
}:::: notes ::: details Auflösung:
Wuppiehat genau eine abstrakte Methode => funktionales InterfaceFluppiehat zwei abstrakte Methoden => kein funktionales InterfaceFoohat gar keine abstrakte Methode => kein funktionales InterfaceBarhat genau eine abstrakte Methode (und eine Default-Methode) => funktionales Interface ::: ::::
interface java.util.Comparator<T> {
int compare(T o1, T o2); // abstrakte Methode
}\bigskip
// Verwendung ohne weitere Typinferenz
Comparator<Studi> c1 = (Studi o1, Studi o2) -> o1.getCredits() - o2.getCredits();
// Verwendung mit Typinferenz
Comparator<Studi> c2 = (o1, o2) -> o1.getCredits() - o2.getCredits();::: notes Der Compiler prüft in etwa folgende Schritte, wenn er über einen Lambda-Ausdruck stolpert:
- In welchem Kontext habe ich den Lambda-Ausdruck gesehen?
- OK, der Zieltyp ist hier
Comparator<Studi>. - Wie lautet die eine abstrakte Methode im
Comparator<T>-Interface? - OK, das ist
int compare(T o1, T o2); - Da
Thier anStudigebunden ist, muss der Lambda-Ausdruck der Methodeint compare(Studi o1, Studi o2);entsprechen: 2xStudials Parameter und als Ergebnis einint - Ergebnis:
a) Cool, passt zum Lambda-Ausdruck
c1. Fertig. b) D.h. inc2müsseno1undo2vom TypStudisein. Cool, passt zum Lambda-Ausdruckc2. Fertig. :::
- Anonyme Klassen: "Wegwerf"-Innere Klassen
- Müssen Interface implementieren oder Klasse erweitern
\smallskip
- Java8: Lambda-Ausdrücke statt anonymer Klassen (funktionales Interface
nötig)
- Zwei mögliche Formen:
- Form 1:
(parameters) -> expression - Form 2:
(parameters) -> { statements; }
- Form 1:
- Im jeweiligen Kontext muss ein funktionales Interface verwendet werden, d.h. ein Interface mit genau einer abstrakten Methode
- Der Lambda-Ausdruck muss von der Signatur her dieser einen abstrakten Methode entsprechen
- Zwei mögliche Formen:
::: readings
- @Java-SE-Tutorial
- @Urma2014 [Kap. 3]
- @Ullenboom2021 [Kap. 12] :::
::: outcomes
- k2: Ich kenne die Definition 'Funktionales Interface'
- k3: Ich kann innere und anonyme Klassen praktisch einsetzen
- k3: Ich kann eigene funktionale Interfaces erstellen
- k3: Ich kann Lambda-Ausdrücke formulieren und einsetzen :::
::: challenges Beispiel aus einem Code-Review im Dungeon-CampusMinden/Dungeon
Erklären Sie folgenden Code:
public interface IFightAI {
void fight(Entity entity);
}
public class AIComponent extends Component {
private final IFightAI fightAI;
fightAI =
entity1 -> {
System.out.println("TIME TO FIGHT!");
// todo replace with melee skill
};
}Spielen mit Lambdas
Sie finden in einem Spiel folgenden Code:
public class Main {
public static void main(String[] args) {
DoorTile door = new DoorTile();
Entity lever1 = new Entity(), lever2 = new Entity(), lever3 = new Entity();
// ganz viel Code
if (!door.isOpen() && (lever1.isOn() && (lever2.isOn() || lever3.isOn()))) door.open();
// ganz viel Code
}
}
class DoorTile {
public boolean isOpen() { return false; }
public void open() { }
}
class Entity {
public boolean isOn() { return false; }
}Dabei stört, dass die Verknüpfung der konkreten Objekte und Zustände zum Öffnen der konkreten Tür fest (und zudem mitten) im Programm hinterlegt ist.
Schreiben Sie diesen Code um: Definieren Sie eine statische Hilfsmethode, die ein
Door-Tile und drei Entitäten als Argument entgegen nimmt und dafür einen
Lambda-Ausdruck zurückliefert, mit dem (a) die gezeigte Bedingung überprüft werden
kann, und mit dem (falls die Bedingung erfüllt ist) (b) die Aktion (door.open())
ausgeführt werden kann. Statt der gezeigten fest codierten if-Abfrage soll dieser
Lambda-Ausdruck ausgewertet werden: doorhandle.test().accept();.
Damit haben Sie sich eine "Factory-Method" geschrieben (Entwurfsmuster), mit der diese Bedingung dynamisch erzeugt werden kann (auch für andere Objekte).
Hinweis: Der Lambda-Ausdruck wird "zweistufig" sein müssen ...
Sortieren mit Lambdas und funktionalen Interfaces
Betrachten Sie die Klasse Student.
- Definieren Sie eine Methode, die das Sortieren einer
Student-Liste erlaubt. Übergeben Sie die Liste als Parameter. - Schaffen Sie es, das Sortierkriterium ebenfalls als Parameter zu übergeben (als Lambda-Ausdruck)?
- Definieren Sie eine weitere Methode, die wieder eine
Student-Liste als Parameter bekommt und liefern sie das ersteStudent-Objekt zurück, welches einer als Lambda-Ausdruck übergebenen Bedingung genügt. - Definieren Sie noch eine Methode, die wieder eine
Student-Liste als Parameter bekommt sowie einen Lambda-Ausdruck, welcher aus einemStudent-Objekt ein Objekt eines anderen TypenTberechnet. Wenden Sie in der Methode den Lambda-Ausdruck auf jedes Objekt der Liste an und geben sie die resultierende neue Liste als Ergebnis zurück.
Verwenden Sie in dieser Aufgabe jeweils Lambda-Ausdrücke. Rufen Sie alle drei/vier Methoden an einem kleinen Beispiel auf.
:::