Frage zu einem Java 8 Feature: Lambdas

Ich nutze wenig von den Möglichkeiten, allerdings verwende ich schon Kurzschreibweisen wie statt

button.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { machWas(); } });

    button.addActionListener(e -> machWas());

Ich frag mich nur, woher weiß der Compiler eigentlich genau, was er da machen soll. Meine Vermutung

  1. Die Methode addActionListener() hat genau einen Parameter und dieser ist vom bekannten Typ ActionListener, also muss der Compiler davon einen erzeugen.
  2. Der Typ ActionListener hat nur eine Methode, die man zu überschrieben hat, nämlich actionPerformed(). Also muss wohl gemeint sein, dass deren Verhalten hier festgelegt werden soll.
  3. Die Methode actionPerformed() hat nur genau einen Parameter, nämlich das ActionEvent .
  4. Also muss mit e ein solches ActionEvent gemeint sein.

Stimmt das? Irgendwie wirkt das schon ein bisschen wie Voodoo, aber um dafür den ganzen „nutzlosen“ und Zeilen fressenden Code zur Erstellung des ActionListeners von Hand zu vermeiden, nutze ich es trotzdem. Ich hätte nur gern ein besseres Gefühl dabei.

Bei einer eigenen Suche bin ich auf die Seite

gestoßen. Dort entnehme ich, dass „mein“ obiger Code ein verkürzter Lamdaausdruck zu

    button.addActionListener( (ActionEvent e) -> (machWas();) );

ist, wenn ich das richtig verstanden habe.

Außerdem steht dort

Das klingt irgendwie ziemlich cool, aber offenbar hab ich diese Lambdas noch nicht ausreichend benutzt, um gefühlsmäßig mit ihnen vertraut zu sein. Vielleicht hat jemand hier ja noch einen guten Tipp für mein Verständnis oder weitere schlaue Links?

Hab deinen Post jetzt mehr überflogen aber es erscheint mir richtig was du bisher rausgefunden hast.

Das auffinden, welche Methode implementiert werden soll ist dabei für die VM sehr einfach. Wenn diese “gefunden” ist, dann sind auch die Parameter kein Problem mehr (selbe typen, selbe Reihenfolge).

Den “einfachen” Fall hast du ja bereits rausgefunden: ein Interface mit nur einer Methode. Es gibt aber durchaus auch die Möglichkeit, dass ein Interface mehrere Methoden hat aber trotzdem zulässig. In dem Fall müssen aber alle bis auf eine Methode eine default Implementierung besitzen:

interface Abc {
    void doSomething(String test);

    default void doSomethingElse() {}
}

Per lamda kann man dann jetzt die Methode #doSomething implementieren:

doIt(test -> {
    System.out.println("Implementiert doSomething mit dem Parameter " + test);
});

Zu soetwas hatte ich (vor dem Umzug) mal einen Blog Post verfasst. Keine Ahnung ob ich den hier wieder finde. Falls doch füge ich hier noch die Verlinkung hinzu.

1 Like

Sollte addActionListener Method-Overloading haben, dann kann es auch nötig sein, auf den Korrekten Typ zu Casten

button.addActionListener((ActionListener) e -> machWas());

Ein weiterer Punkt, den man früher (Studium, Anfänger usw.) öfter gesehen hat ist die Klasse ActionListener implementieren zu lassen und dann die Methode actionPerformed zu implementieren.

Nun geht auch folgendes ohne ActionListener zu implementieren mittels Method References

button.addActionListener(this::doIt);
…
private void doIt(ActionEvent e) {
  machWas();
}
1 Like

Von den aufgeführten Punkten ist hauptsächlich Punkt 2. relevant: Der Typ hat nur eine Methode, die man zu überschreiben hat.

Anfangs (früher, damals) hießen diese Typen noch „Single Abstract Method“-Types (oder SAM-Types). Was dann dort gemacht wird, ist in der Tat viel „schwarze Magie“.

(Das ist viel komplizierter, als man sich das im ersten Moment denken würde, und wenn man sich auch nur ein bißchen damit beschäftigt, stellt man fest, dass es nicht nur viel komplizierter ist, als man denken würde, sondern viel, VIEEEL komplizierter, als man sich vorstellen konnte)

Aber zum Glück kümmern sich da andere drum :slight_smile: Was für einen als Programmierer übrig bleibt, ist relativ einfaches „pattern matching“: Wenn das Lambda ~von der Struktur her zu dem Typen passt, dann passt es einfach.

interface SomeType  {  String myMethod(String someString); }
interface OtherType {  String    booya(Object yourMom   ); }

void example() {

    // Der Parameter ist irgendein "x". 
    // Der Rückabewert ist ein String.
    // Das passt für beides: Das "selbe" Lambda kann für beide verwendet werden:
 
    SomeType  a = x -> String.valueOf(x);
    OtherType b = x -> String.valueOf(x);
}

Das ganze funktioniert eben auch, wenn die „single abstract method“ mehrere parameter bekommt.

BiFunction>Integer, Integer, String> b = (i0, i1) -> String.valueOf(i0+i1);

(Punkt 1. ist natürlich auch teilweise relevant: Natürlich muss das dort übergebene Objekt von Typ ActionListener sein. Aber auch, wenn addActionListener mehrere Parameter hätte, könnten genau diejenigen davon ein Lambda sein, die SAM-Types sind)

2 Likes

Danke für eure Erläuterungen.

Gut, dann werde ich mich damit nicht auseinandersetzen, sondern es einfach benutzen.

Bei regulären Ausdrücken hab ich mich damals ja noch recht tief in das ganze dahinter stehende Prozedere gewühlt.

Wie sieht es denn hier aus?

Ich habe mich daran versucht, aber da die Methode run() keinen Parameter gibt, kann ich ja auch keinen vor das -> schreiben. Und einfach mit -> zu starten, geht auch nicht.

    SwingUtilities.invokeLater(new Runnable() {
        @Override
        public void run() {
            machWasImEDT();
        }
    });

Probiert hatte ich:

    SwingUtilities.invokeLater(run -> machWasImEDT());

    SwingUtilities.invokeLater(-> machWasImEDT());

    SwingUtilities.invokeLater(void -> machWasImEDT());

Was alles nicht geht.

Lass diese -> und das Prä-Gedöns komplett weg. :wink:

SwingUtilities.invokeLater(machWasImEDT());

Geht natrülich auch nicht, da die Methode kein Runnable() zurückgibt.

Entweder mit Method References

this::machWasImEDT

oder einfach

()->machWasImEDT()
2 Likes