Pseudoclosures in Java
closure /ˈkloʊʒɚ/ — anything that closes
Wir zeigen die mit Einschlüssen zusammenhängenden Phänomen in dieser Lektion der Kürze halber zunächst mit Hilfe von Methodenliteralen, jedoch kann man diese auch auf benannte Methoden in Klassen, deren Deklaration sich in einer Methode befindet übertragen.
Die Lebensdauer von Aktivierungsverbunden
Wenn eine Methode aktiviert wird, dann werden ihre Parameter und lokalen Variablen in einem Teil des Speicher angelegt, der Aktivierungsverbund (activation record, stack frame) genannt wird.
Nach dem Ende der Aktivität (Inkarnation) der Methode, wird dieser Aktivierungsverbund wieder aufgelöst. Daher ist es nach dem Ende einer Inkarnation nicht mehr möglich, direkt auf Variablen des Aktivierungsverbundes zuzugreifen.
Das folgende Programm zeigt, daß beim zweiten Aufruf der Methode »run()« ein neuer Aktivierungsverbund angelegt wird, der Wert »27«, welche beim ersten Aufruf nach »a« geschrieben wurde befindet sich nicht in dem Aktivierungsverbund der zweiten Inkarnation.
Main.java
public final class Main
{ public static boolean first = true;public static void run()
{ double a = 0;if( first ){ a = 27; first = false; }
else java.lang.System.out.println( a ); }public static void main( final java.lang.String[] args )
{ Main.run(); Main.run(); }}transcript
0.0
Einschluß von Werten in Methodenliteralen
Wenn der Wert einer Variablen aus einem Aktivierungsverbund in einem Methodenliteral verwendet wird, dann wird die Lebensdauer dieser Variablen aber von der Lebensdauer ihrer ursprünglichen Deklaration auf die des Wertes des Methodenliterals verlängert.
Main.java
public final class Main
{ public static boolean first = true;public static java.util.function.Function< java.lang.Integer, java.lang.Integer >add( final int a )
{ return x -> a + x; }public static void main( final java.lang.String[] args )
{ final java.util.function.Function< java.lang.Integer, java.lang.Integer >add2 = add( 2 );
final java.util.function.Function< java.lang.Integer, java.lang.Integer >add5 = add( 5 );
java.lang.System.out.println( add2.apply( 10 ));
java.lang.System.out.println( add2.apply( 20 ));
java.lang.System.out.println( add5.apply( 10 ));
java.lang.System.out.println( add5.apply( 20 )); }}transcript
12
22
15
25
Der Wert des Parameters »a« ist in den Objekten »add2« und »add5« „eingeschlossen“ worden und existiert damit auch noch nach Beendigung des Aufrufs »add( 2 )« und »add( 5 )«. Daher nennt man solche Objekte auch “closures ” (Einschlüsse).
»final«
Java verlangt jedoch, daß die eingeschlossenen Bezeichner mit »final« deklariert werden oder im Programm wenigstens so behandelt werden, als seien sie mit »final« deklariert worden (“effectively final ”).
Wegen dieser Einschränkung sind die Closures von Java keine „richtigen“ Closures, und wir nennen sie daher auch „Pseudoclosures“.
Main.java
public final class Main
{ public static boolean first = true;public static java.util.function.Function< java.lang.Integer, java.lang.Integer >add( int a )
{ return x -> a + x; }public static void main( final java.lang.String[] args )
{ final java.util.function.Function< java.lang.Integer, java.lang.Integer >add2 = add( 2 );
final java.util.function.Function< java.lang.Integer, java.lang.Integer >add5 = add( 5 );
java.lang.System.out.println( add2.apply( 10 ));
java.lang.System.out.println( add2.apply( 20 ));
java.lang.System.out.println( add5.apply( 10 ));
java.lang.System.out.println( add5.apply( 20 )); }}transcript
12
22
15
25Main.java
public final class Main
{ public static boolean first = true;public static java.util.function.Function< java.lang.Integer, java.lang.Integer >add( int a )
{ a = 3; return x -> a + x; }public static void main( final java.lang.String[] args )
{ final java.util.function.Function< java.lang.Integer, java.lang.Integer >add2 = add( 2 );
final java.util.function.Function< java.lang.Integer, java.lang.Integer >add5 = add( 5 );
java.lang.System.out.println( add2.apply( 10 ));
java.lang.System.out.println( add2.apply( 20 ));
java.lang.System.out.println( add5.apply( 10 ));
java.lang.System.out.println( add5.apply( 20 )); }}transcript
Main.java:5: error: local variables referenced from a lambda expression must be final or effectively final
{ a = 3; return x -> a + x; }
^
1 error
Warum wird dies verlangt? Angeblich hätten sich Java -Anwender dies so gewünscht!
- “Actually, the prototype implementation *did* allow non-final variables to be referenced from within inner classes. There was an outcry from *users*, complaining that they did not want this! The reason was interesting: in order to support such variables, it was necessary to heap-allocate them, and (at that time, at least) the average Java programmer was still pretty skittish about heap allocation and garbage collection and all that. They disapproved of the language performing heap allocation "under the table" when there was no occurrence of the "new" keyword in sight.
- Maybe we could lift that restriction in the next few years...
- I dunno.”
- “One of the early design principles of Java was that heap allocation occurs if and only if a construct involving the "new" keyword is executed. Adding full-blown closures violated this design principle. (Dynamic class loading also violated the principle, but users seemed to be comfortable with that, perhaps because they believed that "once the computation got going" no more heap allocation would occur.)
- Other features for Java release 1.5 will perform certain kinds of autoboxing, which also violates the design principle. This fact will make it easier to argue for restoring full-blown support for closures in the future.”
- Guy Steel zitiert nach http://madbean.com/2003/mb2003-49/
In einigen anderen Programmiersprachen gibt es „richtige Closures“, welche auch veränderliche Variablen aus dem Aktivierungsverbund mit einschließen.
Die Methodenausdrücke von Java werden von manchen Programmierern als „Lambda-Ausdrücke“ bezeichnet, weil Methodenliterale in manchen anderen Programmiersprachen mit einem Lambda geschrieben werden, oder als „Closures“, weil Methodenliterale in manchen anderen Programmiersprachen Closures mit Einschluß von Variablen aus dem Aktivierungsverbund sind. In Java werden Methodenliterale aber nicht mit einem Lambda geschrieben und sie sind auch keine richtigen Closures. Daher sind beide Bezeichnungen unpassend.
Man sollte beachten, daß das »final« nur für die Variable selber gelten muß. Es spricht nichts dagegen, daß sie ein veränderliches Objekt referenziert. Außerdem gilt diese Anforderung nur für Parameter und lokale Variablen, nicht für Felder (Klassen- und Exemplarvariablen) aus der Umgebung eines Methodenliterals.
final-Einschlüsse (final-Closures ) in Exemplarerzeugungsausdrücken
Das folgende Programmbeispiel zeigt den „Einschluß“ einer lokalen Variablen »i« (eigentlich ein Parameter) der Methode »f« in zwei Objekte »c0« und »c1«. Man sieht, daß diese Variable auch nach dem Ende der Inkarnation ihrer Methode in den Objekten weiterlebt. Solch eine Variable muß aber mit »final« deklariert worden sein.
Main.java
public final class Main
{public static java.lang.Runnable f( final int i )
{ return new java.lang.Runnable()
{ public void run()
{ java.lang.System.out.println( i ); }}; }public static void main( final java.lang.String[] args )
{ final java.lang.Runnable c0 = f( 0 );
final java.lang.Runnable c1 = f( 1 );
c0.run();
c1.run(); }}java.lang.System.out
0
1
“The scope of a declaration is the region of the program within which the entity declared by the declaration can be referred to using a simple name, provided it is visible.” — JLS7, 6.3.
“The scope of a local variable declaration in a block is the rest of the block in which the declaration appears, starting with its own initializer and including any further declarators to the right in the local variable declaration statement.” — JLS7, 6.3.
“local variables from the surrounding method may be referred to ” — JLS7, 8.1.3, Example 8.1.3-2.