Java Closures : Inselupdate für Java 7

Mit den Closures nach Gilad Bracha, Neal Gafter, James Gosling, Peter von der Ahé.

Innere Klassen, insbesondere anonyme innere Klassen, sind bisher die einzige Möglichkeit, um Programmteile an Methoden zu übergeben. Nehmen wir einen Timer als Beispiel. Dem eigentlichen Zeitgeber muss ein Stücken Code übergeben werden, sodass der Timer weiß, was er zu tun hat. Mit dem Zeitgeber, der Klasse Timer, und der abstrakten Basisklasse TimerTask zur Beschreibung der Aufgaben, ist schnell ein Beispiel programmiert, das wie eine Uhr in jeder Sekunde die Zeit auf dem Bildschirm ausgibt:


public class TimerExample
{
public static void main( String[] args )
{
class MyTimer extends java.util.TimerTask {
@Override public void run() {
System.out.println( new java.util.Date() );
}
}

new java.util.Timer().scheduleAtFixedRate( new MyTimer(), 0, 1000 );
}
}

Für die Beschreibung des Programmcodes ist extra eine eigene Klasse erforderlich. Über eine innere anonyme Klasse lässt sich der Programmcode jedoch noch etwas weiter verkürzen:

 public class TimerExample
{
public static void main( String[] args )
{
new java.util.Timer().scheduleAtFixedRate( new java.util.TimerTask() {
@Override public void run() {
System.out.println( new java.util.Date() );
}
}, 0, 1000 );
}
}

Aus dem Programm ist deutlich abzulesen, dass zum "Transport" der println()-Anweisung einiges an Schreibarbeit nötig ist. Wünschenswert ist es aber, wenn der Programmcode leichter an die Funktion scheduleAtFixedRate() zu übergeben wäre. Da die bereitgestellte Funktion bisher nicht so programmiert ist, ist das Ziel, dieses nachzuprogrammieren.

Deklaration eines Closures

Ein Closure repräsentiert einen Block Java-Code. Die allgemeine Schreibweise ist

{ formal parameters => statements expression }

Die Schreibweise definiert eine anonyme Funktion. Vergleichen wir einer bekannten Funktionsdeklaration

void out()
{
System.out.printn("Hallo Welt");
}

mit der Deklaration eines Closures:

{ => System.out.printn("Hallo Welt"); }

Während normale Funktionen mit ihrem Namen aufgerufen werden, steht eine Closure für ein Objekt, welches eine invoke()-Methode anbietet.

public class Closures
{
public static void main( String[] args )
{
{ => System.out.println("Hallo Welt"); }.invoke();
}
}

Aus der allgemeinen Schreibweise

{ formal parameters => statements expression }

lässt sich absehen, dass formale Parameter wie bei normalen Funktionsdeklarationen möglich sind. So lassen sich in den Closure-Block Daten einführen. Vergleichen wir wieder eine Funktions- mit einer Closure-Deklaration:

void quote( String s )
{
System.out.println("'" + s + "'");
}

Der Closure mit Beispiel:

public class Closures
{
public static void main( String[] args )
{
{ String s => System.out.println("'" + s + "'"); }.invoke( "tutego" );
}
}

Zwei Dinge fallen an dem Beispiel auf:
• Formale Parameter haben einen Typ (links vom Pfeil steht String s ).
• Die invoke()-Funktion nimmt immer so viele Argumente an, wie es formale Parameter in der Closure-Deklaration gibt.

Die Beispiele bisher zeigen Closures, die in ihrem Rumpf eine Anweisung tragen. Closures können aber auch Ausdrücke enthalten.

public class Closures
{
public static void main( String[] args )
{
System.out.println( { int a, int b => (a + b) / 2 }.invoke( 10, 20) ); // 15
}
}

Im Rumpf endet die Ausdruck nicht mit einem Semikolon, denn es wäre ja auch verboten,

System.out.println( (a + b) / 2; );

zu schreiben.

Ein auffälliger Unterschied zur Funktion ist die fehlende return-Anweisung. Closures selbst sind quasi an die Aufrufstelle eingesetzte Programmteile und ein return würde die Funktion verlassen!

Funktions-Typ

Closures, wie { int a, int b => (a + b) / 2 }, besitzen einen so genannten Funktions-Typ, der durch die Typen der Parameter und der Rückgabe bestimmt ist. Die allgemeine Notation ist

{ formal parameters => return type }

Gibt eine Funktion nicht zurück, so steht rechts vom Pfeil void. Für unsere drei bisherigen Closures sind die Typen:

{ => void }
{ String => void }
{ int, int => int }

Da in einer Funktionsdeklaration ohne Parameter ja auch kein void steht – void out(void) ist falsch – steht auch im ersten Fall links vom Pfeil nichts.
Mit diesem Funktions-Typ lassen sich die Closures ausgezeichnet referenzieren:

{ => void } printHelloWorld = { => System.out.println("Hallo Welt"); }; 

{ String => void } printQuoted = { String s => System.out.println("'" + s + "'"); };

{ int, int => int } avg = { int a, int b => (a + b) / 2 };

printHelloWorld.invoke(); // Hallo Welt

printQuoted.invoke( "tutego" ); // 'tutego'

System.out.println( avg.invoke( 10, 20 ) ); // 15

Mit diesem Wissen lassen sich Funktionen schreiben, die ein Closure entgegennehmen. Eine Funktion repeat() soll dabei einen Programmcode so oft wie gewünscht aufrufen:

public class Repeater
{
public static void repeat( int times, { => void } block )
{
for ( int i = 0; i < times; i++ )
block.invoke();
}

public static void main( String[] args )
{
repeat( 2, { => System.out.println("Hallo"); } );
}
}

Das Hallo kommt also zweimal auf den Bildschirm.

In der Java-Bibliothek sind nicht alle Funktionen so parametrisiert, so dass Closures als Parameter erlaubt sind. Schreiben wir für den Timer eine eigene Methode scheduleAtFixedRate(), die einen Codeblock entgegennimmt und ausführt:

public class TimerExample
{
public static void scheduleAtFixedRate( { => void } task, long delay, long period )
{
new java.util.Timer().scheduleAtFixedRate( new java.util.TimerTask() {
@Override public void run() {
task.invoke();
}
}, delay, period );
}
public static void main( String[] args )
{
scheduleAtFixedRate(
{ => System.out.println( new java.util.Date() ); }
, 0, 1000 );
}
}

Mit Closures sind auch Funktionszeiger leicht zu realisieren:

public class FunctionPointerWithClosures
{
static void invoker( { => void } block )
{
block.invoke();
}

public static void main( String[] args )
{
{ => void } method1 = { => System.out.println("Hello www.tutego.com"); };
{ => void } method2 = { => System.out.println("Hallo www.tutego.com"); };

invoker( Math.random() > 0.5 ? method1 : method2 );
}
}

Natürlich lassen sich jetzt die Methoden auch in eine Datenstruktur setzen:

Map<String, { => void }> methods = new HashMap<String, { => void }>();
methods.put( "German", { => System.out.println("Hallo www.tutego.com"); } );
methods.put( "English", { => System.out.println("Hello www.tutego.com"); } );

methods.get( "German" ).invoke();

for ( { => void } method : methods.values() )
invoker( method );

In ein Feld können Closures nicht gesetzt werden! Der Grund liegt an der internen Generics-Umsetzung.

Nehmen wir an, wir wollen eine Funktion each() schreiben, die einen String mit einem gegebenen Delimiter zerlegt und dann eine Operation auf den einzelnen Token ausführt. Soll weiterhin die Operation einen String zurückgeben, so kann das in herkömmlichem Java etwa so implementiert werden:

class StringUtils
{
public static interface StringInStringOutBlock
{
String execute( String in );
}

public static String each( String source, String delimiter, StringInStringOutBlock block )
{
StringBuilder result = new StringBuilder();

for ( String token : source.split( delimiter ) )
result.append( block.execute( token ) );

return result.toString();
}
public static void main( String[] args )
{
class QuoterBlock implements StringInStringOutBlock {
@Override public String execute( String in ) {
return "'" + in + "'";
}
}

String s = each( "Hallo Welt!", " ", new QuoterBlock() );
System.out.println( s );
}
}

Closures machen das viel kompakter:

class StringUtils
{
public static String each( String source, String delimiter, { String => String } block )
{
StringBuilder result = new StringBuilder();

for ( String token : source.split( delimiter ) )
result.append( block.invoke( token ) );

return result.toString();
}

public static void main( String[] args )
{
String s = each( "Hallo Welt!", " ", { String in => "'" + in + "'" } );
System.out.println( s );
}
}

Closure mit Variablenzugriff

Closures haben einige Eigenschaften, die innere anonyme Klassen so erst einmal nicht haben. Eine davon ist, dass ein Closure-Block auf auch nicht-finale Werte lesend und schreiben zugreifen kann.

int i = 0;
repeat( 3, { => System.out.println(i); } ); // 0 0 0

@Shared int j = 0;
repeat( 3, { => System.out.println(j++); } ); // 0 1 2
System.out.println( j ); // 3

Auffällig ist die Annotation @Shared. Annotiert sie nicht die Variablen, auf die der Closure schreibend zugreift, gibt es eine Compiler-Warnung (kein Fehler!): "warning: [shared] captured variable j not annotated @Shared".

Closure Conversion

Damit automatisch Java-Entwicklung ohne Anpassung der Bibliotheken von den Closures profitieren, haben die Sprachentwickler einen speziellen Mechanismus eingebaut: Ein Closure kann einem Interface mit einer Operation zugewiesen werden, wenn die Rückgabe- und Parametertypen übereinstimmen. Die Schnittstelle Runnable und ActionListener sind wie folgt deklariert:

public interface Runnable {
void run();
}
public interface ActionListener extends EventListener {
void actionPerformed(ActionEvent e);
}

Kompatible Closures sind:

Runnable run = { => System.out.println("Nebenläufig!"); };
ActionListener listener = { ActionEvent l => System.out.println("Gedrückt!"); };


Ohne Zuweisung an Variablen ist ein nebenläufiges Programm schnell gestartet und ein Ereignisbehandler ohne viel Programmcode an einer Schaltfläche festgemacht:



new Thread( { => System.out.println("Nebenläufig!"); } ).start();
JButton b = new JButton();
b.addActionListener( { ActionEvent l => System.out.println("Gedrückt!"); } );

Diese automatische Konvertierung ist wirklich sehr praktisch, denn viele wichtige Java-Schnittstellen schreiben nur eine Operation vor.

Nach einem Vormittag mit Java-Closures kann ich sagen, dass ich gut mit der Syntax leben kann. Ich mag's! Schauen wir mal, ob sich noch groß etwas ändert.

Labels: ,

4 Antwort(en) auf ›Java Closures : Inselupdate für Java 7‹

  1. # Anonymous Anonym

    Vielleicht könnte man zu den BGGA closures noch etwas zur control invocation syntax sagen.  

  2. # Blogger Christian Ullenboom

    Damit warte ich noch ein bisschen. Ich habe soweit meine Zweifel, ob das a) überhaupt und b) mit dieser Syntax in die finale Version kommt. Ich passe den Artikel nach und nach auf die neue Version an. Ergänzen werde ich bald noch Exceptions und die Umsetzung.  

  3. # Blogger Christian Ullenboom

    Schöner Artikel mit den Closres-Vergleichen: http://www.javaworld.com/javaworld/jw-06-2008/jw-06-closures.html?page=1  

  4. # Anonymous Andreas Profitlich

    Das wars dann wohl mit Closures in Java 7. Hoffen wir auf Java 8.  

Kommentar veröffentlichen