Die Umgebung der Lambda-Ausdrücke und Variablenzugriffe

Ein Lambda-Ausdruck „sieht“ seine Umgebung genauso wie der Code, der vor oder nach dem Lambda-Ausdruck steht. Insbesondere hat ein Lambda-Ausdruck vollständigen Zugriff auf alle Eigenschaften der Klasse, genauso wie auch der einschließende äußere Block sie hat. Es gibt keinen besonderen Namensraum, sondern nur neue und vielleicht überdeckte Variablen durch die Parameter. Das ist einer der grundlegenden Unterschiede zwischen Lambda-Ausdrücken und inneren Klassen. Somit ist auch die Bedeutung von this und super bei Lambda-Ausdrücken und inneren Klassen unterschiedlich.

Zugriff auf finale, lokale Variablen/Parametervariablen

Lambda-Ausdrücke können problemlos auf Objektvariablen und Klassenvariablen lesend und schreibend zugreifen. Auch auf lokale Variablen und Parameter hat ein Lambda-Ausdruck Zugriff. Doch greift ein Lambda-Ausdruck auf lokale Variablen bzw. Parametervariablen zu, müssen diese final sein. Dass eine Variable final ist, muss nicht extra mit einem Modifizierer geschrieben werden, aber sie muss effektiv final (engl. effectively final) sein. Effektiv final ist eine Variable, wenn sie nach der Initialisierung nicht mehr beschrieben wird.

Ein Beispiel: Der Benutzer soll über eine Eingabe die Möglichkeit bekommen zu bestimmen, ob String-Vergleiche mit unserem trimmenden Comparator unabhängig von der Groß-/Kleinschreibung stattfinden sollen.

public class CompareIgnoreCase {
  public static void main( String[] args ) {
    /*final*/ boolean compareIgnoreCase = new Scanner( System.in ).nextBoolean();
    Comparator<String> c = (s1, s2) -> compareIgnoreCase ?
           s1.trim().compareToIgnoreCase( s2.trim() ) :
           s1.trim().compareTo( s2.trim() );
    String[] words = { "M", "\nSkyfall", " Q", "\t\tAdele\t" };
    Arrays.sort( words, c );
    System.out.println( Arrays.toString( words ) );
  }
}

Ob compareIgnoreCase von uns final gesetzt wird oder nicht ist egal, denn die Variable wird hier effektiv final verwendet. Natürlich kann es nicht schaden, final als Modifizierer immer davor zu setzen, um dem Leser des Codes diese Tatsache bewusst zu machen.

Neu eingeschobene Lambda-Ausdrücke, die auf lokale Variablen bzw. Parametervariablen zugreifen, können also im Nachhinein zu Compilerfehlern führen. Folgendes Segment ist ohne Lambda-Ausdruck korrekt:

/*1*/ boolean compareIgnoreCase = new Scanner( System.in ).nextBoolean();
/*2*/ …
/*3*/ compareIgnoreCase = true;

Schiebt sich zwischen Zeile 1 und 3 nachträglich ein Lambda-Ausdruck rein, der auf compareIgnoreCase zugreift, gibt es anschließend einen Compilerfehler. Allerdings liegt der Fehler nicht in Zeile 3, sondern beim Lambda-Ausdruck. Denn die Variable compareIgnoreCase ist nach der Änderung nicht mehr effektiv final, was sie aber sein müsste, um in dem Lambda-Ausdruck verwendet zu werden.

Tipp

Lambda-Ausdrücke verhalten sich genauso wie innere anonyme Klassen, die auch nur auf finale Variablen zugreifen können. Mit Behältern, wie einem Feld oder den speziellen AtomicXXX-Klassen aus dem java.util.concurrent.atomic-Paket, lässt sich das Problem im Prinzip lösen. Denn greift ein Lambda-Ausdruck etwa auf das Feld boolean[] compareIgnoreCase = new boolean[1]; zu, so ist die Variable compareIgnoreCase selbst final, aber compareIgnoreCase[0] = true; ist erlaubt und ein Schreibzugriff auf das Feld, nicht der Variablen. Je nach Code besteht jedoch die Gefahr, dass Lambda-Ausdrücke parallel ausgeführt werden. Wird etwa ein Lambda-Ausdruck mit Veränderung auf diesem Feldinhalt parallel ausgeführt, so ist der Zugriff nicht synchronisiert und das Ergebnis kann „kaputt“ sein, denn paralleler Zugriff auf Variablen muss immer koordiniert vorgenommen werden.

Namensräume

Deklariert eine innere anonyme Klasse Variablen innerhalb der Methode, so sind diese immer „neu“, das heißt, die neuen Variablen überlagern vorhandene lokale Variablen aus dem äußeren Kontext. Die Variable compareIgnoreCase kann im Rumpf von compare(…) zum Beispiel problemlos neu deklariert werden:

boolean compareIgnoreCase = true;
Comparator<String> c = new Comparator<String>() {
  @Override public int compare( String s1, String s2 ) {
   boolean compareIgnoreCase = false;        // völlig ok
   return …
  }
};

In einem Lambda-Ausdruck ist das nicht möglich, und folgendes führt zu einer Fehlermeldung des Compilers: „variable compareIgnoreCase ist already defined“.

boolean compareIgnoreCase = true;
Comparator<String> c = (s1, s2) -> {
  boolean compareIgnoreCase = false;  // N Compilerfehler
  return …
}

this-Referenz

Ein Lambda-Ausdruck unterscheidet sich von einer inneren (anonymen) Klasse auch in dem, worauf die this-Referenz verweist:

  • Beim Lambda-Ausdruck zeigt this immer auf das Objekt, in dem der Lambda-Ausdruck eingebettet ist.
  • Bei einer inneren Klasse referenziert this die innere Klasse, und die ist ein komplett neuer Typ.

Folgendes Beispiel macht das deutlich:

class InnerVsLambdaThis {
  InnerVsLambdaThis() {
    Runnable lambdaRun = () -> System.out.println( this.getClass().getName() );
    Runnable innerRun  = new Runnable() {
      @Override public void run() { System.out.println( this.getClass().getName()); }
    };
    lambdaRun.run();      // InnerVsLambdaThis
    innerRun.run();       // InnerVsLambdaThis$1
  }
 
  public static void main( String[] args ) {
    new InnerVsLambdaThis();
  }
}

Als erstes nutzen wir this in einen Lambda-Ausdruck im Konstruktor der Klasse InnerVsLambdaThis. Damit bezieht sich this auf jedes gebaute InnerVsLambdaThis-Objekt. Bei der inneren Klasse referenziert this ein anderes Exemplar und zwar vom Typ Runnable. Da es bei anonymen Kassen keinen Namen hat, trägt es lediglich die Kennung InnerVsLambdaThis$1.

Rekursive Lambda-Ausdrücke

Lambda-Ausdrücke können auf sich selbst verweisen. Da aber ein this zur Selbstreferenz nicht funktioniert, ist ein kleiner Umweg nötig. Erst muss eine Objekt- oder eine Klassenvariable deklariert werden, dann muss dieser Variablen ein Lambda-Ausdruck zugewiesen werden, und dann kann der Lambda-Ausdruck auf diese Variable zugreifen und einen rekursiven Aufruf starten. Für den Klassiker der Fakultät sieht das so aus:

public class RecursiveFactLambda {
  public static IntFunction<Integer> fact = n -> (n == 0) ? 1 : n * fact.apply(n-1);
  public static void main( String[] args ) {
    System.out.println( fact.apply( 5 ) );   // 120
  }
}

IntFunction ist eine funktionale Schnittstelle aus dem Paket java.util.function mit einer Operation T apply(int i). T ist ein generischer Rückgabetyp, den wir hier mit Integer belegt haben.

fact hätte genauso gut als normale Methode deklariert werden können. Großartige Vorteile bietet die Schreibweise mit Lambda-Ausdrücken hier nicht. Zumal jetzt auch der Begriff „anonyme Methode“ nicht mehr so richtig passt, da der Lambda-Ausdruck ja doch einen Namen hat, nämlich fact. Und weil der Lambda-Ausdruck einer Variablen zugewiesen wurde, kann er in dieser Form natürlich auch nicht mehr als Implementierung an eine Methode oder einen Konstruktor übergeben werden, sondern nur als Methoden/Konstruktor-Referenz, dazu später mehr.

Über Christian Ullenboom

Ich bin Christian Ullenboom und Autor der Bücher ›Java ist auch eine Insel. Einführung, Ausbildung, Praxis‹ und ›Java SE 8 Standard-Bibliothek. Das Handbuch für Java-Entwickler‹. Seit 1997 berate ich Unternehmen im Einsatz von Java. Sun ernannte mich 2005 zum ›Java-Champion‹.

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.