Erste Libs springen auf Java 8 auf

So etwa http://www.jdbi.org/.

JDBI is a SQL convenience library for Java.

Beispiel von der Seite in herkömmlicher Notation:

DataSource ds = JdbcConnectionPool.create("jdbc:h2:mem:test",
                                          "username",
                                          "password");
DBI dbi = new DBI(ds);
Handle h = dbi.open();
h.execute("create table something (id int primary key, name varchar(100))");

h.execute("insert into something (id, name) values (?, ?)", 1, "Brian");

String name = h.createQuery("select name from something where id = :id")
                    .bind("id", 1)
                    .map(StringMapper.FIRST)
                    .first();
                    
assertThat(name, equalTo("Brian"));

h.close();

http://skife.org/jdbi/2012/12/10/some-jdbi3.html schreibt nun, dass JDBI 3 Lambda-Ausdrücke nutzen wird und gibt folgendes Beispiel an:

Set<Something> things = jdbi.withHandle(h -> {
    h.execute("insert into something (id, name) values (?, ?)", 1, "Brian");
    h.execute("insert into something (id, name) values (?, ?)", 2, "Steven");

    return h.query("select id, name from something")
            .map(rs -> new Something(rs.getInt(1), rs.getString(2)))
            .into(new HashSet<Something>());
});

assertThat(things).isEqualTo(ImmutableSet.of(new Something(1, "Brian"),
                                             new Something(2, "Steven")));

Das geht sicherlich noch etwas kürzer, warten wir’s ab.

Konstruktor-Referenz in Java 8

Um ein Objekt aufzubauen, nutzen wir den new-Operator. Wenn wir new nutzen, dann wird ein Konstruktor aufgerufen, und optional lassen sich Argumente an den Konstruktor übergeben. Die Java-API deklariert aber auch Typen, von denen sich keine Exemplare mit new aufbauen lassen. Stattdessen gibt es statische (oder nicht-statische) Erzeuger, deren Aufgabe es ist, Objekte aufzubauen.

Konstruktor …

… erzeugt

Erzeuger …

… baut

new Integer( "1" )

Integer

Integer.valueOf( "1" )

Integer

new File( "dir" )

File

Paths.get( "dir" )

Path

new BigInteger( val )

BigInteger

BigInteger.valueOf( val )

BigInteger

Beispiele für Konstruktoren und Erzeuger-Methoden

Beide, Konstruktoren und Erzeugen, lassen sich als spezielle Funktionen sehen, den von einem Typ in einem anderen Typ konvertieren. Damit eignen sie sich perfekt für Transformationen und in einem Beispiel haben wir das schon eingesetzt:

Arrays.stream( words )
      . …
      .map( Integer::parseInt )
      . …

Integer.parseInt(string) ist eine Methode, die sich einfach mit einer Methoden-Referenz fassen lässt, und zwar als Integer::parseInt. Aber was ist mit Konstruktoren? Auch sie transformieren! Statt Integer.parseInt(string) hätte ja auch new Integer(string) eingesetzt werden können.

Wo Methoden-Referenzen statische und Objekt-Methoden angeben können, so bieten Konstruktor-Referenzen die Möglichkeit, Konstruktoren anzugeben, sodass diese als Erzeuger an anderer Stelle übergeben werden können. Damit lassen sich elegant Erzeuger angeben, auch wenn diese nicht über Erzeuger-Methoden verfügen. Wie auch bei Methoden-Referenzen spielt eine funktionale Schnittstelle eine entschiedene Rolle, doch dieses Mal ist es die Methode der funktionalen Schnittstelle, die aufgerufen zum Konstruktor-Aufruf führt. Wo syntaktisch bei Methoden-Referenzen rechts vom Doppelpunkt ein Methodenname steht, steht bei Konstruktor-Referenzen new.[1]

Beispiel: Die funktionale Schnittstelle sei:

interface DateFactory { Date create(); }

Die Konstruktor-Referenz bindet den Konstruktor an die Methode create() der funktionalen Schnittstelle.

DateFactory factory = Date::new;

System.out.print( factory.create() ); // z.B. Sat Dec 29 09:56:35 CET 2012

Bzw. die letzten beiden Zeilen zusammengefasst:

System.out.println( ((DateFactory)(Date::new)).create() );

Soll nur der Standard-Konstruktor aufgerufen werden, muss die funktionale Schnittstelle nur eine Methode besitzen, die keinen Parameter besitzt und etwas zurückliefert. Der Rückgabetyp der Methode muss natürlich mit dem Klassentyp zusammen. Das gilt für unseren eigenen Typ DateFactory, doch es geht noch etwas generischer, zum Beispiel mit der vorhandenen funktionalen Schnittstelle Supplier, wie wir gleich sehen werden.

In der API finden sich oftmals Parameter vom Typ Class, die als Typ-Angabe dazu verwendet werden, dass die Methode mit newInstance() Exemplare bilden kann. Class lässt sich durch eine funktionale Schnittstelle ersetzen und Konstruktor-Referenzen lassen sich anstelle von Class-Objekten übergeben.

Standard- und parametrisierte Konstruktoren

Beim Standard-Konstruktor hat die Methode nur eine Rückgabe, bei einem parametrisierten Konstruktor muss die Methode der funktionalen Schnittstelle natürlich über eine kompatible Parameterliste verfügen.

Konstruktor

Date()

Date(long t)

Kompatible funktionale Schnittstelle

interface DateFactory {

Date create();

}

interface DateFactory {

Date create(long t);

}

Konstruktor-Referenz

DateFactory factory = Date::new;

DateFactory factory = Date::new;

Aufruf

factory.create();

Factory.create(1);

Standard- und parametrisierter Konstruktor mit korrespondierenden funktionalen Schnittstellen

Hinweis: Kommt die Typ-Inferenz des Compilers an ihre Grenzen, sind zusätzliche Typinformationen gefordert. In dem Fall werden hinter dem Doppelpunkt in eckigen Klammen weitere Angaben gemacht, etwa Klasse::<Typ1, Typ2>new.

Nützliche vordefinierte Schnittstellen für Konstruktor-Referenzen

Die funktionale Schnittstelle passend für einen Standard-Konstruktor muss eine Rückgabe besitzen und keinen Parameter annehmen; die funktionale Schnittstelle für parametrisierten Konstruktor muss eine entsprechende Parameterliste haben. Es kommt nun häufig vor, dass der Konstruktor ein Standard-Konstruktor ist oder genau einen Parameter annimmt. Hier kommt es entgegen, dass für diesen beiden Fälle die Java API zwei praktische (generische deklarierte) funktionale Schnittstellen mitbringt:

Funktionale Schnittstelle

Funktions-Deskriptor

Abbildung

Passt auf

Supplier<T>

T get()

() -> T

Standard-Konstruktor

Function<T, R>

R apply(T t)

(T) -> R

einfachen parametrisierter Konstruktor

Beispiel: Die funktionale Schnittstelle Supplier<T> hat eine T get()-Methode, die wir mit dem Standard-Konstruktor von Date verbinden können:

Supplier<Date> factory = Date::new;

System.out.print( factory.get() );

Wir nutzen Supplier mit dem Typparameter Date, was den parametrisierten Typ Supplier<Date> ergibt, und get() liefert folglich den Typ Date. Der Aufruf factory.get() führt zum Aufruf des Konstruktors.

Ausblick *

Interessant werden die Konstruktor-Referenzen wieder mit den Möglichkeiten von Java 8. Nehmen wir eine Liste von Zeitstempel an. Der Konstruktor Date(long) nimmt einen solchen Zeitstempel an und mit einem Date-Objekt können wir Vergleiche vornehmen, etwa, ob ein Datum hinter einem anderen Datum liegt. Folgendes Beispiel listet alle Datumswerte auf, die nach dem 1.1.2012 liegen:

Long[] timestamps = { 2432558632L, 1455872986345L };
Date thisYear = new GregorianCalendar( 2012, Calendar.JANUARY, 1 ).getTime();
Arrays.stream( timestamps )
      .map( Date::new )
      .filter( thisYear::before )
      .forEach( System.out::println );  // Fri Feb 19 10:09:46 CET 2016

[1] Da new ein Schlüsselwort ist, kann keine Methode so heißen; der Identifizierer ist also sicher.

filter map map filter map sorted forEach, wird das die Zukunft sein?

Object[] words = { " ", '3', null, "2", 1, "" };
Arrays.stream( words )
      .filter( Predicates.nonNull()::test )
      .map( Objects::toString )
      .map( String::trim )
      .filter( (s) -> ! s.isEmpty() )
      .map( Integer::parseInt )
      .sorted()
      .forEach( System.out::println );   // 1 2 3

Methoden-Referenzen in Java 8

Je größer Software-Systeme werden, desto wichtiger werden Aspekte wie Klarheit, Wiederverwendbarkeit und Dokumentation. Wir haben in für unseren String-Comparator eine Implementierung geschrieben, anfangs über eine innere Klasse, später über einen Lambda-Ausdruck, in jedem Fall haben wir Code geschrieben. Doch was wäre, wenn eine Utility-Klasse schon eine Implementierung hätte? Kann könnte der Lambda-Ausdruck natürlich an die vorhandene Implementierung delegieren.

class StringUtils {
  public static int compareTrimmed( String s1, String s2 ) {
    return s1.trim().compareTo( s2.trim() );
  }     
}

public class CompareIgnoreCase {
  public static void main( String[] args ) throws Exception {
    String[] words = { "A", "B", "a" };
      Arrays.sort( words, (String s1, String s2) -> StringUtils.compareTrimmed(s1, s2) );
      System.out.println( Arrays.toString( words ) );
  }
}

Auffällig bei dem Beispiel ist, dass die referenzierte Methode compareTrimmed(String,String) von den Parametertypen und vom Rückgabetyp genau auf die compare(…)-Methode eines Comparator passt. Für genau solche Fälle gibt es eine weitere syntaktische Verkürzung, dass Entwickler im Code kein Lambda-Ausdruck mehr schreiben müssen.

Definition: Methoden-Referenzen identifizieren Methoden ohne sie aufzurufen. Syntaktisch trennen zwei Doppelpunkte den Klassenamen bzw. die Referenz auf der linken Seite von einem Methodennamen auf der rechten.

Die Zeile

Arrays.sort( words, (String s1, String s2) -> StringUtils.compareTrimmed(s1, s2) );

lässt sich mit Methoden-Referenzen abkürzen zu:

Arrays.sort( words, StringUtils::compareTrimmed );

Die Sortiermethode erwartet vom Comparator eine Methode, die zwei Strings annimmt und eine Ganzzahl zurückgibt. Der Name der Klasse und der Name der Methode ist unerheblich, weshalb Methoden-Referenzen eingesetzt werden können.

Eine Methoden-Referenz ist wie ein Lambda-Ausdruck ein Exemplar einer funktionalen Schnittstelle, jedoch für eine existierende Methode einer bekannten Klasse. Wie üblich bestimmt der Kontext von welchem Typ genau der Ausdruck ist.

Beispiel: Gleicher Code für eine Methoden-Referenz kann zu komplett unterschiedlichen Typen führen – der Kontext macht den Unterschied:

Comparator<String> c = StringUtils::compareTrimmed;

BiFunction<String, String, Integer> c = StringUtils::compareTrimmed;

Im Beispiel war die Methode compareTrimmed(…) statisch, und links vom Doppeltpunkt steht der Name einer Klasse stehen. Doch kann links auch eine Referenz stehen, was dann eine Objektmethode referenziert.

Beispiel: Die statische Variable String.CASE_INSENSITIVE_ORDER enthält eine Referenz auf ein Comparator-Objekt:

Comparator<String> c = String.CASE_INSENSITIVE_ORDER;

Wir können auch mit Methoden-Referenzen arbeiten:

Comparator<String> c = String.CASE_INSENSITIVE_ORDER::compare;

Statt dass der Name einer Referenzvariablen gewählt wird, kann auch this das Objekt beschreiben.

Was soll das alles?

Für Einsteiger in die Sprache Java wird dieses Sprache-Feature wie der größte Zauber auf Erden vorkommen und auch Java-Profis bekommen hier zittrige Finger, entweder vor Angst oder Freunde… In der Vergangenheit musste in Java sehr viel explizit geschrieben werden, aber mit diesen neuen Methoden-Referenzen sieht und macht der Compiler vieles von selbst.

Nützlich wird diese Eigenschaft mit den funktionalen Bibliotheken aus Java 8, die ein eigenes Kapitel einnehmen. Nur kurz:

String[] words = { "3", "2", " 1", "" };
Arrays.stream( words )
      .map( String::trim )
      .filter( (s) -> s != null && ! s.isEmpty() )
      .map( Integer::parseInt )
      .sorted()
      .forEach( System.out::println );   // 1 2 3

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 (nur neue und vielleicht überdeckte Variablen durch die Parameter), und das ist einer der grundlegenden Unterschiede zwischen Lambda-Ausdrücken und inneren Klassen, bei denen this und super eine etwas andere Bedeutung haben.

Lambda-Ausdrücke können problemlos auf Objektvariablen und Klassenvariablen lesend und schreiben zugreifen. Auch auf lokale Variablen und Parameter hat ein Lambda-Ausdruck Zugriff, jedoch gibt es eine Einschränkung: die Variable muss final sein. Dass sie final ist, muss nicht extra mit einem Modifizierer geschrieben werden, aber sie muss effektiv final (engl. effectively final) sein, das heißt, nach der Initialisierung nicht mehr beschrieben werden.

Ein Beispiel. Der Benutzer soll über eine Eingabe die Möglichkeit bekommen zu bestimmen, ob String-Vergleiche mit unserem trimmenden Comparator unabhängig 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 = (String s1, String s2) -> {
      return 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 effektiv final verwendet. Natürlich kann es nicht schaden, final als Modifizierer immer davor zu setzen, um den Leser des Codes diese Tatsache bewusst zu machen.

Nicht AutoCloseable-Typen in try-mit-Ressourcen mithilfe von Lambda-Ausdrücken bzw. Methoden-Referenzen nutzen

Es ist mit einem Trick möglich, auch Exemplare in einem try mit Ressourcen zu nutzen, die nicht vom Typ AutoCloseable sind. Ein Lambda-Ausdruck bzw. eine Methoden-Referenz lässt sich einsetzen, um eine beliebige Methode als close()-Methode einzusetzen. Ein ReentrantLock zum Beispiel ist eine Implementierung eines Lock, um bei nebenläufigen Zugriffen einen Bereich abzuschließen. lock() beginnt den Bereich, unlock() gibt ihn wieder frei. Das unlock() lässt sich über einen Lambda-Ausdruck als close()-Methoden verkaufen.

ReentrantLock lock = new ReentrantLock();

try ( AutoCloseable unlock = lock::unlock ) { // oder () -> {lock.unlock();}

  lock.lock();

}

System.out.println( lock.isLocked() ); // false

Ob dieser „Trick“ sinnvoll ist oder nicht, ist eine andere Frage. Das try mit Ressourcen setzt auf jeden Fall das unlock() in einen internen finally-Block, der über die Konstruktion eingespart wird. Allerdings wird üblicherweise die Ressource im try mit Ressourcen Block auch erst deklariert, was hier vorher gemacht werden muss, außerdem ist die Variable unlock unnütz. Daher ist die Relevanz eher niedrig.

Abkürzende Schreibweisen für Lambda-Ausdrücke

Lambda-Ausdrücke haben wie Methoden mögliche Parameter und Rückgabe. Die Java-Grammatik für die Schreibweise von Lambda-Ausdrücken sieht ein paar syntaktische Abkürzungen vor, dir wir uns nun anschauen wollen.

Typinferenz

Der Java-Compiler kann viele Typen aus dem Kontext ablesen, was Typ-Inferenz genannt wird. Wir kennen so etwas vom Diamanten, wenn wir schreiben List<String> list = new ArrayList<>();.

Statt

Comparator<String> c = (String s1, String s2) -> { return s1.trim().compareTo( s2.trim() ); };

erlaubt der Compiler auch die Abkürzung:

Comparator<String> c = (s1, s2) -> { return s1.trim().compareTo( s2.trim() ); };

Die Parameterliste enthält also deklarierte Parametertypen oder inferred-Typen. Eine Mischung ist nicht erlaubt, der Compiler blockt so etwas wie (String s1, s2) oder (s1, String s2) mit einem Fehler ab.

Lambda-Rumpf ist entweder einzelner Ausdruck oder Block

Besteht der Rumpf eines Lambda-Ausdrucks nur aus einem einzelnen Ausdruck, kann eine verkürzte Schreibweise die Block-Klammern und das Semikolon einsparen. Statt

‚(‚ Parameter ‚)‘ ‚->‘ ‚{‚ Anweisungen; ‚}‘

heißt es dann

‚(‚ Parameter ‚)‘ ‚->‘ Ausdruck

Lambda-Ausdrücke mit einer return–Anweisung im Rumpf kommen häufig vor (es entspricht den typischen Funktionen). Da ist es eine willkommene Verkürzung, wenn die abgekürzte Syntax für Lambda-Ausdrücke lediglich den Ausdruck fordert, der dann die Rückgabe bildet.

Drei Beispiele:

Lange Schreibweise

Abkürzung

(s1, s2) -> { return s1.trim().compareTo( s2.trim() ); }

(s1, s2) -> s1.trim().compareTo( s2.trim() )

(a, b) -> { return a + b; }

(a, b) -> a + b

() -> { System.out.println(); }

() -> System.out.println()

Ausdrücke können in Java auch zu void ausgewertet werden, sodass ohne Probleme ein Aufruf wie System.out.println() in der kompakten Schreibweise ohne Block gesetzt werden kann.

Ob Lambda-Ausdrücke eine Rückgabe geben, drücken zwei Begriffe aus:

· Der Rumpf kann in Anweisungen enden, die nichts zurück geben. Das nennt sich void-kompatibel.

· Der Rumpf beendet den Block mit einer return-Anweisung, die einen Wert zurückgibt. Das nennt sich Wert-kompatibel.

Eine Mischung aus void- und Wert-kompatibel ist nicht erlaubt und führt wie bei Methoden zu einem Compilerfehler.[1]

Einzelner Identifizierer statt Parameterliste und Klammern

Besteht die Parameterliste nur aus einem einzelnen Identifizierer und ist der Typ durch Typ-Inferenz klar, können die runden Klammen wegfallen.

Lange Schreibweise

Typen inferred

Vollständig abgekürzt

(Sting s) -> s.length()

(s) -> s.length()

s -> s.length()

(int i) -> Math.abs( i )

(i) -> Math.abs( i )

i -> Math.abs( i )

Kommen alle Abkürzungen zusammen, lässt sich etwa die Hälfe einsparen. Aus (int i) -> { return Math.abs( i ); } wird dann i -> Math.abs( i ).


[1] Wohl aber gibt es wie bei { throw new RuntimeException(); } Ausnahmen, bei denen Lambda-Ausdrücke beides sind.

Funktionale Schnittstellen und Typ-Inferenz in Java 8

In unserem Beispiel haben wir den Lambda-Ausdruck als Argument von Array.sort(…) eingesetzt:

Arrays.sort( words,
              (String s1, String s2) -> { return s1.trim().compareTo(s2.trim()); } );

Wir hätten aber auch den Lambda-Ausdruck explizit einer lokalen Variablen zuweisen können, was deutlich macht, dass der hier eingesetzte Lambda-Ausdruck vom Typ Comparator ist:

Comparator<String> c = (String s1, String s2) -> {
                           return s1.trim().compareTo( s2.trim() ); }
 Arrays.sort( words, c );

Funktionale Schnittstellen

Nicht zu jeder Schnittstelle gibt es eine Abkürzung über einen Lambda-Ausdruck, und es gibt eine zentrale Bedingung, wann ein Lambda-Ausdruck verwendet werden kann.

Definition: Schnittstellen, die nur eine Operation (abstrakte Methode) besitzen, heißen funktionale Schnittstellen. Ein Funktionsdeskriptor beschreibt diese Methode. Eine abstrakte Klasse mit genau einer abstrakten Methode zählt nicht als funktionale Schnittstelle.

Lambda-Ausdrücke und funktionale Schnittstellen haben eine ganz besondere Beziehung, denn ein Lambda-Ausdruck ist ein Exemplar einer solchen funktionalen Schnittstelle. Natürlich müssen Typen und Ausnahmen passen. Dass funktionale Schnittstellen genau eine abstrakte Methode vorschreiben, ist eine naheliegende Einschränkung, denn gäbe es mehrere, müsste ein Lambda-Ausdruck ja auch mehrere Implementierungen anbieten oder irgendwie eine Methode bevorzugen und andere ausblenden.

Wenn wir ein Objekt vom Typ einer funktionalen Schnittstelle aufbauen möchten, können wir folglich zwei Wege einschlagen: Es lässt sich die traditionelle Konstruktion über die Bildung von Klassen wählen, die funktionale Schnittstellen implementieren, und dann mit new ein Exemplar bilden, oder es lässt sich mit kompakten Lambda-Ausdrücken arbeiten. Moderne IDEs zeigen uns an, wenn kompakte Lambda-Ausdrücke zum Beispiel statt innerer anonymer Klassen genutzt werden können, und bieten uns mögliche Refactorings an. Lambda-Ausdrücke machen den Code kompakter und nach kurzer Eingewöhnung auch lesbarer.

Hinweis: Funktionale Schnittstellen müssen auf genau eine zu implementierende Methode hinauslaufen, auch wenn aus Oberschnittstellen mehrere Operationen vorgeschrieben werden, die sich aber durch den Einsatz von Generics auf eine Operation verdichten:

interface I<S,T extends CharSequence> {
   void len( S text );
   void len( T text );
 }
 interface FI extends I<String,String> { }

FI ist unsere funktionale Schnittstelle mit einer eindeutigen Operation len(String). Statische und Default-Methoden stören in funktionalen Schnittstellen nicht.

Viele funktionale Schnittstellen in der Java-Standardbibliothek

Java bringt schon viele Schnittstellen mit, die als funktionale Schnittstellen gekennzeichnet sind. Darüber hinaus führt das Paket java.util.function mehr als 40 neue funktionale Schnittstellen ein. Eine kleine Auswahl:

  • interfaceRunnable{voidrun();}
  • interfaceSupplier<T>{Tget();}
  • interfaceConsumer<T>{voidaccept(Tt);}
  • interfaceComparator<T>{intcompare(To1,To2);}
  • interfaceActionListener{voidactionPerformed(ActionEvente);}

Ob die Schnittstelle noch andere Default-Methoden hat – also Schnittstellenmethoden mit vorgegebener Implementierung –, ist egal, wichtig ist nur, dass sie genau eine zu implementierende Operation deklariert.

Typ eines Lambda-Ausdrucks ergibt sich durch Zieltyp

In Java hat jeder Ausdruck einen Typ. 1 und 1*2 haben einen Typ (nämlich int), genauso wie „A“ + „B“ (Typ String) oder String.CASE_INSENSITIVE_ORDER (Typ Comparator<String>). Lambda-Ausdrücke haben auch immer einen Typ, denn ein Lambda-Ausdruck ist immer Exemplar einer funktionalen Schnittstelle. Damit steht auch der Typ fest. Allerdings ist es im Vergleich zu Ausdrücken wie 1*2 bei Lambda-Ausdrücken etwas anders gelagert, denn der Typ von Lambda-Ausdrücken ergibt sich ausschließlich aus dem Kontext. Erinnern wir uns an den Aufruf von sort(…):

Arrays.sort( words, (String s1, String s2) -> { return … } );

Dort steht nichts vom Typ Comparator, sondern der Compiler erkennt aus dem Typ des zweiten Parameters von sort(…), der ja Comparator ist, ob der Lambda-Ausdruck auf die Methode des Comparators passt oder nicht.

Der Typ eines Lambda-Ausdrucks ist folglich abhängig davon, welche funktionale Schnittstelle er im jeweiligen Kontext gerade realisiert. Der Compiler kann ohne Kenntnis des Zieltyps (engl. target type) keinen Lambda-Ausdruck aufbauen.

Beispiel: Callable und Supplier sind funktionale Schnittstellen mit Methoden, die keine Parameterlisten deklarieren und eine Referenz zurückgeben; der Code für den Lambda-Ausdruck sieht gleich aus:

java.util.concurrent.Callable<String> c = () -> { return "Rückgabe"; };
 java.util.function.Supplier<String>   s = () -> { return "Rückgabe"; };

Wer bestimmt den Zieltyp?

Gerade weil an dem Lambda-Ausdruck der Typ nicht abzulesen ist, kann er nur dort verwendet werden, wo ausreichend Typinformationen vorhanden sind. Das sind unter anderem die folgenden Stellen:

  • Variablendeklarationen: etwa wie bei Supplier<String> s = () -> { return „“; }
  • Argumente an Methoden oder Konstruktoren: Der Parametertyp gibt alle Typinformationen. Ein Beispiel lieferte sort(…).
  • Methodenrückgaben: Das könnte aussehen wie Comparator<String> trimComparator() { return (s1, s2) -> { return … }; }.
  • Bedingungsoperator: Der ?:-Operator liefert je nach Bedingung einen unterschiedlichen Lambda-Ausdruck. Beispiel: Supplier<Double> randomNegOrPos = random() > 0.5 ? () -> { return Math.random(); } : () -> { return Math.random(); };

Parametertypen

In der Praxis ist der häufigste Fall, dass die Parametertypen von Methoden den Zieltyp vorgeben. Der Einsatz von Lambda-Ausdrücken ändert ein wenig die Sichtweise auf überladene Methoden. Unser Beispiel mit () -> { return „Rückgabe“; } macht das deutlich, denn es „passt“ auf den Zieltyp Callable<String> genauso wie auf Supplier<String>. Nehmen wir zwei überladene Methoden run(…) an:

class OverloadedFuntionalInterfaceMethods {

  static <V> void run( Callable<V> callable ) { }

  static <V> void run( Supplier<V> callable ) { }

}

Spielen wir den Aufruf der Methoden einmal durch:

Callable<String> c = () -> { return "Rückgabe"; };
 Supplier<String> s = () -> { return "Rückgabe"; };
 run( c );
 run( s );
 // run( () -> { return "Rückgabe"; } ); // BANG! Compilerfehler
 run( (Callable<String>) () -> { return "Rückgabe"; } );

Rufen wir run(c) bzw. run(s) auf, ist das kein Problem, denn c und s sind klar typisiert. Aber run(…) mit dem Lambda-Ausdruck aufzurufen funktioniert nicht, denn der Zieltyp (entweder Callable oder Supplier) ist mehrdeutig; der (Eclipse-)Compiler meldet: „The method run(Callable<Object>) is ambiguous for the type T“. Hier sorgt eine explizite Typumwandlung für Abhilfe.

Tipp zum API-Design: Aus Sicht eines API-Designers sind überladene Methoden natürlich schön, aus Sicht des Nutzers sind Typumwandlungen aber nicht schön. Um explizite Typumwandlungen zu vermeiden, sollte auf überladene Methoden verzichtet werden, wenn diese den Parametertyp einer funktionalen Schnittstelle aufweisen. Stattdessen lassen sich die Methoden unterschiedlich benennen (was bei Konstruktoren natürlich nicht funktioniert). Wird in unserem Fall die Methode runCallable(…) und runSupplier(…) genannt, ist keine Typumwandlung mehr nötig, und der Compiler kann den Typ herleiten.

Rückgabetypen

Typ-Inferenz spielt bei Lambda-Ausdrücken eine große Rolle – das gilt insbesondere für die Rückgabetypen, die überhaupt nicht in der Deklaration auftauchen und für die es gar keine Syntax gibt; der Compiler „inferrt“ sie. In unserem Beispiel

Comparator<String> c =
   (String s1, String s2) -> { return s1.trim().compareTo( s2.trim() ); };

ist String als Parametertyp der Comparator-Methode ausdrücklich gegeben; der Rückgabetyp int, den der Ausdruck s1.trim().compareTo( s2.trim()) liefert, taucht dagegen nicht auf.

Mitunter muss dem Compiler etwas geholfen werden: Nehmen wir die funktionale Schnittstelle Supplier<T>, die eine Methode T get() deklariert, für ein Beispiel. Die Zuweisung

Supplier<Long> two  = () -> { return 2; }       // N Compilerfehler

ist nicht korrekt und führt zum Compilerfehler „incompatible types: bad return type in lambda expression“. 2 ist ein Literal vom Typ int, und der Compiler kann es nicht an Long anpassen. Wir müssen schreiben

Supplier<Long> two  = () -> { return 2L };

oder

Supplier<Long> two  = () -> { return (long) 2 };

Bei Lambda-Ausdrücken gelten keine wirklich neuen Regeln im Vergleich zu Methodenrückgaben, denn auch eine Methodendeklaration wie

Long two() { return 2; }      // BANG! Compilerfehler

wird vom Compiler bemängelt. Doch weil Wrapper-Typen durch die Generics bei funktionalen Schnittstellen viel häufiger sind, treten diese Besonderheiten öfter auf als bei Methodendeklarationen.

Sind Lambda-Ausdrücke Objekte?

Ein Lambda-Ausdruck ist ein Exemplar einer funktionalen Schnittstelle und tritt als Objekt auf. Bei Objekten besteht normalerweise zu java.lang.Object immer eine natürliche Ist-eine-Art-von-Beziehung. Fehlt aber der Kontext, ist selbst die Ist-eine-Art-von-Beziehung zu java.lang.Object gestört und Folgendes nicht korrekt:

Object o = () -> {};          // BANG! Compilerfehler

Der Compilerfehler ist: „incompatible types: the target type must be a functional interface“. Nur eine explizite Typumwandlung kann den Fehler korrigieren und dem Compiler den Zieltyp vorgeben:

Object r = (Runnable) () -> {};

Lambda-Ausdrücke haben keinen eigenen Typ an sich, und für das Typsystem von Java ändert sich im Prinzip nichts. Möglicherweise ändert sich das in späteren Java-Versionen.

Hinweis: Dass Lambda-Ausdrücke Objekte sind, ist eine Eigenschaft, die nicht überstrapaziert werden sollte. So sind die üblichen Object-Methoden equals(Object), hashCode(), getClass(), toString() und die zur Thread-Kontrolle ohne besondere Bedeutung. Es sollte auch nie ein Szenario geben, in dem Lambda-Ausdrücke mit == verglichen werden müssen, denn das Ergebnis ist laut Spezifikation undefiniert. Echte Objekte haben eine Identität, einen Identity-Hashcode, lassen sich vergleichen und mit instanceof testen, können mit einem synchronisierten Block abgesichert werden; all dies gilt für Lambda-Ausdrücke nicht. Im Grunde charakterisiert der Begriff „Lambda-Ausdruck“ schon sehr gut, was wir nie vergessen sollten: Es handelt sich um einen Ausdruck, also etwas, was ausgewertet wird und ein Ergebnis produziert.

Deklaration und Syntax eines Java 8 Lambda-Ausdrucks

Ein Lambda-Ausdruck repräsentiert einen Block Java-Code. Wie eine Java-Methode enthält er Programmcode, aber da es keinen Methodennamen gibt, ist auch der Name anonyme Funktion im Gebrauch, sprachlich äquivalent zu anonymen inneren Klassen, die ja auch keinen Namen haben. Auch optisch sind sich ein Lambda-Ausdruck und eine Methodendeklaration ähnlich; was wegfällt sind Modifizierer, der Rückgabetyp, der Methodenname und throws-Klausen.

Methodendeklaration

Lambda-Ausdruck

public int compare

( String s1, String s2 )

 

{ return s1.trim().compareTo( s2.trim() ); }

( String s1, String s2 )

->

{ return s1.trim().compareTo( s2.trim() ); }

Methodendeklaration mit dem Lambda-Ausdruck im Vergleich

Alle Lambda-Ausdrücke lassen sich in einer Syntax formulieren, die folgende allgemeine Form hat:

‚(‚ Parameter ‚)‘ ‚->‘ ‚{‚ Anweisungen; ‚}‘

Es gibt syntaktische Abkürzungen, wie wir später sehen werden, doch vorerst bleiben wir bei dieser Schreibweise.

Einführung in Java 8 Lambda-Ausdrücke: Code sind Daten

Wer den Begriff „Daten“ hört, denkt zunächst einmal an Zahlen, Bytes, Zeichenketten oder auch komplexe Objekte mit ihrem Zustand. Wir wollen in diesem Kapitel diese Sicht ein wenig erweitern und auf Programmcode lenken. Java-Code, versinnbildlicht als Serie von Bytecodes, besteht auch aus Daten. Und wenn wir uns einmal auf diese Sichtweise einlassen, dass Code gleich Daten ist, dann lässt sich Code auch wie Daten übergeben und so von einem Punkt zum anderen übertragen, speichern und später referenzieren. Mit dieser Möglichkeit, Code zu übertragen, lässt sich das Verhalten von Algorithmen leicht anpassen. Beginnen wir mit ein paar Beispielen, bei denen Programmcode übergeben wird, auf den dann später zugegriffen wird:

  • Ein Thread führt Programmcode im Hintergrund aus. Der Programmcode, den der Java-Thread ausführen soll, wird in ein Objekt vom Typ Runnable verpackt, genau genommen in eine run()-Methode gesetzt. Kommt der Thread zum Zuge, ruft er die run()-Methode auf.
  • Ein Timer ist eine util-Klasse, die zu bestimmen Zeitpunkten Programmcode ausführen kann. Der Objektmethode scheduleAtFixedRate(…) wird dabei ein Objekt vom Typ TimerTask übergeben, das den Programmcode enthält.
  • Zum Sortieren von Daten kann eine eigene Ordnung definiert werden, die dem Sortierer als Comparator übergeben werden kann. Der Comparator deklariert eine Vergleichsmethode, an die sich der Sortierer wendet, um zwei Objekte in die gewünschte Reihenfolge zu bringen.
  • Aktiviert der Benutzer auf der Oberfläche eine Schaltfläche, so führt das zu einer Aktion. Der Programmcode steckt – beim UI-Framework Swing – in einem Objekt vom Typ ActionListener und wird an der Schaltfläche JButton mit addActionListener(…) fest gemacht. Kommt es zu einer Schaltflächenaktivierung, arbeitet das UI-System den Programmcode in der Methode actionPerformed(…) des gespeicherten ActionListener

Um Programmcode von einer Stelle zur anderen zu bringen, wird in Java immer der gleiche Mechanismus eingesetzt: Eine Klasse implementiert eine (in der Regel nichtstatische) Methode, in der der auszuführende Programmcode steht. Ein Objekt dieser Klasse wird an eine andere Stelle übergeben, und der Interessent greift dann über die Methode auf den Programmcode zu. Dass ein Objekt noch mehr als diese eine Implementierung enthalten kann, etwa Variablen, Konstanten, Konstruktoren, ist dafür nicht relevant. Diesen Mechanismus schauen wir uns jetzt in verschiedenen Varianten genauer an.

Innere Klassen als Code-Transporter

Bleiben wir bei dem Beispiel mit den Vergleichen. Angenommen, wir sollen Strings so sortieren, dass Leerraum vorne und hinten bei den Vergleichen ignoriert wird, also “ Newton “ gleich „Newton“ ist. Bei Vorgaben dieser Art muss einem Sortieralgorithmus ein Stückchen Code übergeben werden, damit er die korrekte Reihenfolge herstellen kann. Praktisch sieht das so aus:

import java.util.*;
 public class CompareTrimmedStrings {
   public static void main( String[] args ) {
     class TrimmingComparator implements Comparator<String> {
       @Override public int compare( String s1, String s2 ) {
         return s1.trim().compareTo( s2.trim() );
       }
     }
     String[] words = { "M", "\nSkyfall", " Q", "\t\tAdele\t" };
     Arrays.sort( words, new TrimmingComparator() );
     System.out.println( Arrays.toString( words ) );
   }
 }

Die Ausgabe ist:

[        Adele    , M,  Q, 
 Skyfall]

Der TrimmingComparator enthält in der compare(…)-Methode den Programmcode für die Vergleichslogik. Ein Exemplar vom TrimmingComparator wird aufgebaut und Arrays.sort(…) übergeben. Das geht mit weniger Code!

Innere anonyme Klassen als Code-Transporter

Klassen enthalten Programmcode, und Exemplare der Klassen werden an Methoden wie sort(…) übergeben, damit der Programmcode dort hinkommt, wo er gebraucht wird. Doch elegant ist das nicht. Für die Beschreibung des Programmcodes ist extra eine eigene Klasse erforderlich. Das ist viel Schreibarbeit, und über eine innere anonyme Klasse lässt sich der Programmcode schon ein wenig verkürzen:

String[] words = { "M", "\nSkyfall", " Q", "\t\tAdele\t" };
 Arrays.sort( words, new Comparator<String>() {
   @Override public int compare( String s1, String s2 ) {
     return s1.trim().compareTo( s2.trim() );
   } } );
 System.out.println( Arrays.toString( words ) );

Allerdings ist das immer noch aufwändig: Wir müssen eine Methode überschreiben und dann ein Objekt aufbauen. Für Programmautoren ist das lästig, und die JVM hat es mit vielen überflüssigen Klassendeklarationen zu tun. Die Frage ist: Wenn der Compiler weiß, dass bei sort(…) ein Comparator nötig ist, und wenn ein Comparator sowieso nur eine Methode hat, muss dann Comparator und compare(…) überhaupt genannt werden?

Abkürzende Schreibweise durch Lambda-Ausdrücke

Mit Lambda-Ausdrücken lässt sich Programmcode leichter an eine Methode übergeben, denn es gibt eine kompakte Syntax für die Implementierung von Schnittstellen mit einer Operation. Für unser Beispiel sieht das so aus:

String[] words = { "M", "\nSkyfall", " Q", "\t\tAdele\t" };
 Arrays.sort( words,
             (String s1, String s2) -> { return s1.trim().compareTo(s2.trim()); } );
 System.out.println( Arrays.toString( words ) );

Der in fett gesetzte Ausdruck nennt sich Lambda-Ausdruck. Er ist eine kompakte Art und Weise, Schnittstellen mit genau einer Methode zu implementieren; die Schnittstelle Comparator hat genau eine Operation compare(…).

Optisch sind sich ein Lambda-Ausdruck und eine Methodendeklaration ähnlich; was wegfällt sind Modifizierer, der Rückgabetyp, der Methodenname und (mögliche) throws-Klauseln.

Methodendeklaration Lambda-Ausdruck
public int compare
( String s1, String s2 )

{ return s1.trim().compareTo( s2.trim() ); }

( String s1, String s2 )
->
{ return s1.trim().compareTo( s2.trim() ); }

Tabelle 1.1: Vergleich der Methodendeklaration einer Schnittstelle mit dem Lambda-Ausdruck

Wenn wir uns den Lambda-Ausdruck als Implementierung dieser Schnittstelle anschauen, dann lässt sich dort nichts von Comparator oder compare(…) ablesen – ein Lambda-Ausdruck repräsentiert mehr oder weniger nur den Java-Code und lässt das, was der Compiler aus dem Kontext herleiten kann, weg.

Alle Lambda-Ausdrücke lassen sich in einer Syntax formulieren, die die folgende allgemeine Form hat:

( LambdaParameter ) -> { Anweisungen }

Lambda-Parameter sind sozusagen die Eingabewerte für die Anweisungen. Die Parameterliste wird so deklariert, wie von Methoden oder Konstruktoren bekannt, allerdings gibt es keine Varargs. Es gibt syntaktische Abkürzungen, wie wir später sehen werden, doch vorerst bleiben wir bei dieser Schreibweise.

Geschichte: Der Java-Begriff „Lambda-Ausdruck“ geht auf das Lambda-Kalkül (in der englischen Literatur Lambda calculus genannt, auch geschrieben als λ-calculus) aus den 1930er Jahren zurück und ist eine formale Sprache zur Untersuchung von Funktionen.

Default-Methoden, Teil 2, Default-Methoden zur Entwicklung von Bausteinen nutzen

Bevor wir zu nächsten Punkt kommen, müssen wir noch einmal inne halten und uns fragen, was denn das Kernkonzept der objektorientierten Programmierung ist. Wohl ohne zu Zögern können wir Klassen und Kapselung nennen. Klassen und Klassenbeziehungen das Gerüst jedes Java-Programms. Schauen wir uns Vererbung noch einmal genauer an, so wissen wir, das Unterklassen Spezialisierungen sind, und das Liskovsche Substitutionsprinzip gilt: Falls ein Typ gefordert ist, können wir auch einen Untertyp übergeben. So sollte perfekte Vererbung aussehen: Eine Unterklasse sollte das Verhalten spezialisieren, aber nicht einfach von einer Klasse erben, weil sie nützliche Funktionalität hat. Aber warum eigentlich nicht? Ein Problem ist, das uns die Einfachvererbung nur eine einzige Oberklasse erlaubt. Wenn eine Klasse so etwas Nützliches wie Logging anbietet, und unsere Klasse davon erbt, kann sie nicht gleichzeitig von einer anderen Klasse erben, um zum Beispiel Zustände in Konfigurationsdaten festzuhalten. Das Problem bei der „Funktionalitätsvererbung“ ist also, dass wir uns nur einmal festlegen können. Wenn eine Klasse eine gewisse Funktionalität einfach braucht, woher soll sie denn dann kommen, wenn nicht aus der Oberklasse? Eigentlich gibt es hier nur eine naheliegende Variante: Die Klasse greift auf andere Objekte zurück per Delegation. Das ist interessant, aber auch nicht optimal, insbesondere gilt dann nicht die ist-eine-Art-von-Beziehung. Falls das nicht gewünscht ist, ist das in Ordnung, doch wenn über diesen Typ eine Abstraktion läuft, ist das ungünstig.

Ein Dilemma. Gut wäre eine Technik, die einen Programmbaustein in eine Klasse setzen kann. Im Grunde so etwas wie Mehrfachvererbung, aber doch anders, weil die Bausteine nicht als komplette Typen auftreten – der Baustein selbst ist nur ein Implantat und alleine uninteressant. Auch ein Objekt kann von diesem Baustein-Typ nicht erzeugt werden.

Am ehesten sind die Bausteine mit abstrakten Klassen vergleichbar, doch das wären Klassen und Nutzer könnten nur einmal von diesem Baustein erben. Mit Java 8 gibt es aber eine ganz neue Möglichkeit, und zwar mit den erweiterten Schnittstelle: Sie bilden die Bausteine, von denen Klassen Funktionalität bekommen können. Andere Programmiersprachen bieten so etwas Ähnliches und das Konzept wird dort Mixin oder Trait genannt.[1] Diese Bausteine sind nützlich, denn so lässt sich ein Algorithmus in eine extra Compilationseinheit setzen und leichter wiederverwenden. Ein Beispiel.

Nehmen wir zwei erweiterte Schnittelle an: PersistentPreference und Logged. Die erste erweiterte Schnittstelle soll mit store() Schlüssel/Werte-Paare in die zentrale Konfiguration schreiben und get() soll sie auslesen:

import java.util.prefs.Preferences;

interface PersistentPreference {

default void store( String key, String value ) {

  Preferences.userRoot().put( key, value );

}

default String get( String key ) {

  return Preferences.userRoot().get( key, "" );

}

}

Die zweite erweiterte Schnittstelle ist Logged und bietet drei kompakte Logger-Methoden:

import java.util.logging.*;

interface Logged {

default void error( String message ) {

  Logger.getLogger( getClass().getName() ).log( Level.SEVERE, message );

}

default void warn( String message ) {

  Logger.getLogger( getClass().getName() ).log( Level.WARNING, message );

}

default void info( String message ) {

  Logger.getLogger( getClass().getName() ).log( Level.INFO, message );

}

}

Eine Klasse kann diese Bausteine nun einbauen:

class Player implements PersistentPreference, Logged {

// …

}

Die Methoden sind nun Teil vom Player und können auch von Unterklassen überschrieben werden. Als Aufgabe für den Leser bleibt, die Implementierung von store() im Player zu verändern, dass der Schlüssel immer mit „player.“ beginnt. Die Frage, die Leser beantworten sollten ist, ob store() von Player auf das store() von der erweiterten Schnittstelle zugreifen kann.

Default-Methoden weiter gedacht

Für diese Bausteine, also die erweiterten Schnittstellen, gibt es viele Anwendungsfälle. Da die Java- Bibliothek schon an die 20 Jahre als ist, würden heute einige Typen anders aussehen. Dass sich Objekte mit equals() vergleichen lassen können, könnte heute zum Beispiel in einer erweiterten Schnittstelle stehen, etwa so: interface Equals { boolean equals( Object that ) default { return this == that; } }. So müsste java.lang.Object die Methode nicht für alle vorschreiben, wobei das sicherlich jetzt kein Nachteil ist. Natürlich gilt das gleiche auf für die hashCode()-Methode, die heutzutage aus einer erweiterten Schnittstelle Hashable stammen könnte.

Und java.lang.Number ist ein weiters Beispiel. Die abstrakte Basisklasse für Werte-repräsentierende Objekte deklariert die abstrakten Methoden doubleValue(), floatValue(), intValue(), longValue() und die konkreten Methoden byteValue() und shortValue(). Bisher erben AtomicInteger, AtomicLong, BigDecimal, BigInteger, Byte, Double, Float, Integer, Long, Short von dieser Oberklasse. Auch diese Funktionalität ließe sich mit einer erweiterten Schnittstelle umsetzen.

Da Schnittstellen auch Generics haben können, werden Default-Methoden noch vielseitiger. Baustein können auch andere Bausteine erweitern, da eine Schnittstelle andere Schnittstellen extenden kann. Es ist dabei egal, ob die die Schnitten erweitert sind oder nicht.

Zustand in den Bausteinen?

Nicht jeder wünschenswerte Baustein ist mit erweiterten Schnittstellen möglich. Ein Grund ist, dass die Schnittstellen keinen Zustand einbringen können. Einen Baustein für einen Container können wir nicht so einfach implementieren, da ein Container Kinder verwaltet, und hierfür ist eine Objektvariable für den Zustand nötig. Schnittstellen haben nur statische Variablen und die sind für alle sichtbar und selbst wenn die Schnittstelle eine modifizierbare Datenstruktur referenzieren würde, würde jeder Nutzer des Container-Bausteins von den Veränderungen betroffen sein. Da es keinen Zustand gibt, existieren auch für Schnittstellen keine Konstruktoren und folglich auch nicht für solche Bausteine. Denn wo es keinen Zustand gibt, gib es nichts zu initialisieren. Wenn eine Default-Methode einen Zustand benötigt, müssen sie selbst diesen Zustand erfragen. Wie das geht zeigt folgendes Beispiel.

Repräsentiert eine Klasse eine Menge von Objekten, die sich sortieren lassen können, können wir einen Baustein Sortable mit einer Methode sort() realisieren. Allerdings muss die Implementierung irgendwie an die Daten kommen und hier kommt der Trick ins Spiel: Zwar ist sort() eine Default-Methode, doch die erweiterte Schnittstelle besitzt Methoden, die die Klasse implementieren muss, die dem Sortierer die Daten geben. Im Quellcode sieht das so aus:

Teil 1:

import java.util.*;

interface Sortable<T extends Comparable> {

  T[] getValues();

  void setValues( T[] values );

  default void sort() {

    T[] values = getValues();

    Arrays.sort( values );

    setValues( values );

  };

}

Damit sort() an die Daten kommt, erwartet Sortable von den implementieren Klassen eine Methode getValues(). Und damit die Daten nach dem Sortieren wieder zurückgeschrieben werden können, eine zweite Methode setValues(…). Der Clou ist, das die Klasse, die später Sortable realisieren wird, mit den beiden Methoden dem Sortierer Zugriff auf den Daten gewährt – allerdings auch jedem anderem Stück Code da die Methoden öffentlich sind. Da bleibt ein Geschmäckle.

Ein Nutzer vor Sortable soll RandomValues sein; die Klasse erzeugt intern Zufallszahlen.

Teil 2:

class RandomValues implements Sortable<Integer>

{

  private List<Integer> values = new ArrayList<>();

  public RandomValues() {

    Random r = new Random();

    for ( int i = r.nextInt( 20 ) + 1; i > 0; i– )

    values.add( r.nextInt(10000) );

  }

  @Override public Integer[] getValues() {

    return values.toArray( new Integer[values.size()] );

  }

  @Override public void setValues( Integer[] values ) {

    this.values.clear();

   Collections.addAll( this.values, values );

  }

}

Damit sind die Typen vorbereitet und ein Demo schließt das Beispiel ab:

Teil 3:

public class SortableDemo {

  public static void main( String[] args ) {

    RandomValues r = new RandomValues();

    System.out.println( Arrays.toString( r.getValues() ) );

    r.sort();

    System.out.println( Arrays.toString( r.getValues() ) );

  }

}

Aufgerufen kommt auf die Konsole zum Beispiel:

[2732, 4568, 4708, 4302, 4315, 5946, 2004]

[2004, 2732, 4302, 4315, 4568, 4708, 5946]

So interessant diese Möglichkeit auch ist, ein Problem wurde schon angesprochen: Jede Methode in einer Schnittstelle ist public, ob sie nun eine abstrakte oder Default-Methode ist. Es wäre schön, wenn die Datenzugriffsmethoden nicht öffentlich sein würden, aber das geht nicht.

Wo wir gerade bei der Sichtbarkeit sind. Gibt es im Default-Code Code-Duplizierung, so kann der gemeinsame Code bisher nicht in private Methoden ausgelagert werden, da es private Operationen in Schnittstellen nicht gibt. Allerdings läuft gerade ein Test, ob so etwas eingeführt werden soll.

Warnung!

Natürlich lässt sich mit Rumgetrickse ein Speicherort finden, der Exemplarzustände speichert. Es lässt sich zum Beispiel in der Schnittstelle ein Assoziativspeicher referenzieren, der eine this-Instanz mit einem Objekt assoziiert. Ein Container-Baustein, der mit add() Objekte in eine Liste setzt und sie mit iterable() herausgibt, könnte so aussehen:

interface ListContainer<T> {

Map<Object, List<Object>> $ = new HashMap<>();

default void add( T e ) {

  if ( ! $.containsKey( this ) )

   $.put( this, new ArrayList<Object>() );

$.get( this ).add( e );

}

default public Iterable<T> iterable() {

  if ( ! $.containsKey( this ) )

   return Collections.emptyList();

  return (Iterable<T>) $.get( this );

}

}

Nicht nur die öffentliche Konstante $ ist ein Problem, sondern auch, dass es ein großartiges doppeltes Speicherloch ist. Ein Exemplar der Klasse, die diese erweitert Schnittstelle nutzt, kann nicht so einfach entfernt werden, denn in der Sammlung ist noch eine Referenz auf das Objekt, die das Garbage Collection verhindert. Und selbst wenn dieses Objekt weg wäre, hätten wir noch all die referenzierten Kinder der Sammlung in der Map. Und das Problem ist nicht wirklich zu lösen, und hier müsste tief mit schwachen Referenzen in die Java-Voodoo-Kiste gegriffen werden. Alles in allem, keine gute Idee und Java-Chefentwickler Brian Goetz macht auch klar: „Please don’t encourage techniques like this. There are a zillion "clever" things you can do in Java, but shouldn’t. We knew it wouldn’t be long before someone suggested this, and we can’t stop you. But please, use your power for good, and not for evil. Teach people to do it right, not to abuse it.”[2] Daher: Es ist eine schöne Spielerei, aber Zustand sollte eine Aufgabe der abstrakten Basisklassen oder vom Delegate sein.

Zusammenfassung

Was wir in den letzten Beispielen gemacht haben war, ein Standardverhalten in Klassen einzubauen, ohne das dabei der Zugriff auf die einmalige Basisklasse nötig war und ohne das die Klasse an Hilfsklassen delegiert. In dieser Arbeitsweise können Unterklassen in jedem Fall die Methoden überschreiben und spezialisieren. Wie haben es also mit üblichen Klassen zu tun und mit erweiterten Schnittstellen, die nicht selbst eigenständige Entitäten bilden. In der Praxis wird es immer Fälle geben, in denen für eine Umsetzung eines Problems entweder eine abstrakte Klasse oder eine erweiterte Schnittstelle in Frage kommt. Wir sollten und dann noch einmal an die Unterschiede erinnern: Eine abstrakten Klasse kann Methoden aller Sichtbarkeiten haben und sie auch final setzen, sodass sie nicht mehr überschrieben werden können. Eine Schnittstelle dagegen ist mit puren virtuellen und öffentlichen Methoden darauf ausgelegt, dass eben die Implementierung überschrieben werden kann.


[1] Siehe etwa http://scg.unibe.ch/archive/papers/Scha02aTraitsPlusGlue2002.pdf.

[2] http://mail.openjdk.java.net/pipermail/lambda-dev/2012-July/005166.html

Klassenimplementierung geht vor Default-Methoden

So wie zwar eine Sonnenfinsternis selten ist, aber vorkommt, so kann auch die seltene Konstellation eintreten, dass eine Klasse von zwei Seiten eine Implementierung bekommt. Erbt zum Beispiel eine Klasse A eine Implementierung für eine Methode f() und implementiert die Klasse A gleichzeitig eine erweiterte Schnittstelle I, von der sie eine Default-Methode f() vorgeschlagen bekommt, ist vom Compiler bzw. der Laufzeitumgebung eine Entscheidung gefragt. Zunächst muss der Compiler entscheiden, ob so etwas überhaupt syntaktisch korrekt ist. Die Antwort lautet: ja.

Ein Beispiel soll denn Fall demonstrieren:

interface Buyable {

double price();

default boolean hasPrice() { return price() > 0; }

}

abstract class NotBuyable implements Buyable {

@Override public boolean hasPrice() { return false; }

}

public class Love extends NotBuyable implements Buyable {

@Override public double price() { return 10_01; }

public static void main(String[] args) {

System.out.println( new Love().hasPrice() ); // false

}

}

Wieder ist Buyable eine Schnittstelle mit einer Default-Methode hasPrice(). Neu ist eine abstrakte Klasse NotBuyable, die hasPrice() mit false beantwortet. Eine Klasse Love erweitert erstens NotBuyable und bekommt von dort hasPrice() und implementiert zweitens Buyable, was ebenfalls eine Implementierung über die Default-Methode hasPrice() mitbringt. Nachdem wir festgestellt haben, dass dieses Szenario syntaktisch korrekt ist, muss die Laufzeitumgebung eine Entscheidung fällen. Die sieht so aus, dass die Implementierung aus einer Klasse gewinnt. Ausgabe ist also false.

Bleibt abschließend die Frage, wie sich der Compiler verhält, wenn einer Klasse aus zwei erweiterten Schnittstellen eine Default-Methode angeboten wird. Kurz und knapp: Das führt zu einem Compilerfehler. Die Klasse RockAndRoll zeigt dieses Dilemma:

interface Buyable {

boolean hasPrice();

}

interface Love extends Buyable {

@Override default public boolean hasPrice() { return false; }

}

interface Guitar extends Buyable {

@Override default public boolean hasPrice() { return false; }

}

public class RockAndRoll implements Love, Guitar { } // Compilerfehler

Default-Methoden aus speziellen Oberschnittstellen ansprechen

Eine Unterklasse kann eine konkrete Methode der Oberklasse überschreiben, aber dennoch auf die Implementierung der überschriebenen Methode zugreifen. Allerdings muss der Aufruf über super erfolgen, da sich sonst ein Methodenaufruf rekursiv verfängt.

Default-Methoden können andere Default-Methoden aus Oberschnittstellen ebenfalls überschreiben und mit neuem Verhalten implementieren. Doch genauso wie normale Methoden können sie mit super auf Default-Verhalten aus dem übergeordneten Typ zurückgreifen.

Nehmen wir für ein Beispiel wieder zwei erweiterte Schnittstelle Prices und Buyable an:

interface Priced {

double price();

default boolean hasPrice() { return price() > 0; }

}

interface Buyable extends Priced {

@Override double price();

@Override default boolean hasPrice() { return Priced.super.hasPrice() || price() == 0; }

}

In der Schnittstelle Priced sagt der Default-Code von hasPrice() aus, dass alles einen Preis hat, was echt über 0 liegt. Buyable dagegen nutzt eine andere Definition und implementiert daher das Default-Verhalten neu. Denn Buyable definiert hasPrice() so, dass auch ein Preis von 0 letztendlich bedeutet, dass es einen Preis hat. In der Implementierung von hasPrice() greift Buyable auf den Default-Code von Priced zurück, um vom Obertyp eine Entscheidung über die Preiseigenschaft zu bekommen, die aber mit der Oder-Verknüpfung noch verallgemeinert wird.

Default-Methoden, Teil 1, Update mit aktueller Java 8 Syntax

Ist eine Schnittstelle einmal verbreitet, so sollte es dennoch möglich sein, Operationen hinzuzufügen. Java 8 bringt dafür eine Sprachänderung mit, die es Entwicklern erlaubt, neue Operationen einzuführen, ohne dass Unterklassen verpflichtet werden, diese Methoden zu implementieren. Damit das möglich ist, muss die Schnittstelle eine Standard-Implementierung mitbringen. Auf diese Weise ist das Problem gelöst, denn wenn eine Implementierung vorhanden ist, haben die implementierenden Klassen nichts zu meckern, und wenn sie das Standardverhalten überschreiben möchten, können sie das gerne machen. Oracle nennt diese Methoden in Schnittstelle mit vordefinierter Implementierung Default-Methoden[1]. Schnittstellen mit Default-Methoden heißen erweiterte Schnittstellen.

Eine Default-Methode unterscheidet sich syntaktisch in zwei Aspekten von herkömmlichen implizit abstrakten Methoden-Deklarationen.

· Die Deklaration einer Default-Methode beginnt mit dem Schlüsselwort default.

· Statt dass ein Semikolon das Ende der Deklaration anzeigt, steht bei einer Default-Methode stattdessen in geschweiften Klammen ein Block mit Implementierung. Die Implementierung wollen wir Default-Code nennen.

Sonst verhalten sich erweiterte Schnittstellen wie normale Schnittstellen. Eine Klasse, die eine Schnittstelle implementiert, erbt alle Operationen, sei es die abstrakten Methoden oder die Default-Methoden. Falls die Klasse nicht abstrakt sein soll muss sie alle von der Schnittstelle geerbten abstrakten Methoden realisieren; sie kann die Default-Methoden überschreiben, muss das aber nicht, denn eine Vorimplementierung ist ja schon gegeben.

Realisieren wir dies in einem Beispiel. Für Spielobjekte soll ein Lebenszyklus möglich sein; der besteht aus start() und finish(). Der Lebenszyklus ist als Schnittstelle vorgegeben, die Spielobjektklasse implementieren können. Version 1 der Schnittstelle sieht also aus:

interface GameLifecycle {

void start();

void finish();

}

Klassen wie Player, Room, Door können die Schnittstellen erweitern, und wenn sie dies tun, müssen sie die beiden Methoden implementieren. Bei Spielobjekten, die diese Schnittstelle implementieren, kann unser Hauptprogramm, das Spiel, diese Methoden aufrufen und den Spielobjekten Rückmeldung geben, ob sie gerade in das Spiel gebracht wurden, oder sie aus dem Spiel entfernt wurden.

Je länger Software lebt, desto mehr bedauern Entwickler Designentscheidungen. Die Umstellung einer ganzen Architektur ist eine Mammutaufgabe, einfache Änderungen wie das Umbenennen sind über ein Refactoring schnell erledigt. Nehmen wir an, auch bei unserer Schnittstelle gibt es einen Änderungswunsch – nur die Initialisierung und das Ende zu melden reicht nicht. Geht das Spiel in einen Pausenmodus, soll ein Spielobjekt die Möglichkeit bekommen, im Hintergrund laufende Programme anzuhalten. Das soll durch eine zusätzliche pause()-Methode in der Schnittstelle realisiert werden. Hier spielen uns die Default-Methoden perfekt in die Hand, denn wir können die Schnittstelle erweitern, aber eine leere Standardimplementierung mitgeben. So müssen Unterklassen die pause()-Methode nicht implementieren, können dies aber; Version 2 der nun erweiterten Schnittstelle GameLifecycle:

interface GameLifecycle {

void start();

void finish();

default void pause() {}

}

Klassen, die GameLifecycle schon genutzt haben, bekommen von der Änderung nichts mit. Der Vorteil: Die Schnittstelle kann sich weiter entwickeln, aber alles bleibt binärkompatibel und nichts muss neu compiliert werden. Vorhandener Code kann auf die neue Methode zurückgreifen, die automatisch mit der Implementierung vorhanden ist. Weiterhin verhalten sich Default-Methoden wie andere Methoden von Schnittstellen auch: es bleibt bei der dynamischen Bindung, wenn implementierende Klassen die Methoden überschreiben. Wenn eine Unterklasse wie Flower zum Beispiel bei der Spielpause nicht mehr blühen möchte, so überschreibt sie die Methode und lässt den Timer pausieren. Eine Tür dagegen hat nichts zu stoppen und kann pause() mit dem Default-Code so übernehmen.

Hinweis

Statt des leeren Blocks könnte der Rumpf auch throw new UnsupportedOperationException("Not yet implemented"); beinhalten, um anzukündigen, dass es keine Implementierung gibt. So führt eine hinzugenommene Default-Methode zwar zu keinem Compilerfehler, aber zur Laufzeit führen nicht überschriebene Methoden zu einer Ausnahme. Erreicht ist das Gegenteil vom Default-Code, weil eben keine Logik standardmäßig ausgeführt wird;das Auslösen einer Ausnahme zum Melden eines Fehlers wollen wir nicht als Logik ansehen.

Kontext der Default-Methoden

Default-Methoden verhalten sich wie Methoden in abstrakten Klassen und können alle Methoden der Schnittstelle (inklusive der geerbten Methoden) aufrufen. Die Methoden werden später dynamisch zur Laufzeit gebunden.

Nehmen wir eine Schnittstelle Buyable für käufliche Objekte:

interface Buyable {
  double price();
}

Leider schreibt die Schnittstelle nicht vor, ob Dinge überhaupt käuflich sind. Eine Methode wie isBuyable() wäre in Buyable ganz gut aufgehoben. Was kann aber die Default-Implementierung sein? Wir können auf price() zurückgreifen und testen, ob die Rückgabe ein gültiger Preis ist. Das soll gegeben sein, wenn der Preis echt größer 0 ist.

interface Buyable {
  double price();

default boolean isBuyable() { return price() > 0; }
}

Implementierende Klassen erben die Methode isBuyable() und beim Aufruf geht der interne Aufruf von price() an genau die Klasse, die Buyable und die Methode implementiert.

Hinweis

Eine Schnittstelle kann die Methoden der absoluten Oberklasse java.lang.Object ebenfalls deklarieren, etwa um mit Javadoc eine Beschreibung hinzuzufügen. Allerdings ist es nicht möglich, mit Default-Code Methoden wie toString() oder hashCode() vorzubelegen.

Neben der Möglichkeit auf Methoden zuzugreifen, steht auch die this-Referenz zur Verfügung. Das ist sehr wichtig, denn so kann der Default-Code an Utility-Methoden weiterreichen und einen Verweis auf sich selbst übergeben. Hätten wir zum Beispiel schon eine isBuyable(Buyable)-Methode in einer Utiltiy-Klasse PriceUtils implementiert, so könnte der Default-Code aus einer einfachen Weiterleitung bestehen:

class PriceUtils {

public static boolean isBuyable( Buyable b ) { return b.price() > 0; }

}

interface Buyable {

  double price();

default boolean isBuyable() { return PriceUtils.isBuyable( this ); }
}

Dass die Methode PriceUtils.isBuyable(Buyable) für den Parameter den Typ Buyable vorsieht und sich der Default-Code mit this auf genau so ein Buyable-Objekt bezieht, ist natürlich kein Zufall, sondern bewusst gewählt. Der Typ der this-Referenz zur Laufzeit entspricht dem der Klasse, die die Schnittstelle implementiert hat und dessen Objektexemplar gebildet wurde.

Haben die Default-Methoden weitere Parameter, so lassen sie auch diese weiter an die statische Methode reichen:

class PriceUtils {

public static boolean isBuyable( Buyable b ) { return b.price() > 0; }

public static double defaultPrice( Buyable b, double defaultPrice ) {

if ( b != null && b.price() > 0 )

return b.price();

return defaultPrice;

}

}

interface Buyable {
  double price();

default boolean isBuyable() { return PriceUtils.isBuyable( this ); }

default double defaultPrice( double defaultPrice ) {

return PriceUtils.defaultPrice( this, defaultPrice ); }
}

Es ist vorzuziehen, die Implementierung auszulagern, um die Schnittstellen nicht so Code-lastig werden zu lassen. Nutzt das JDK Default-Code, so gibt es in der Regel immer eine statische Methode in einer Utility-Klasse.

Neue Möglichkeiten mit Default-Methoden *

Default-Methoden geben Bibliotheksdesignern ganz neue Möglichkeiten. Heute ist noch gar nicht richtig abzusehen, was Entwickler damit machen werden und welche Richtung die Java-API einschlagen wird. Auf jeden Fall wird sich die Frage stellen, ob Standard-Implementierung als Default-Code in Schnittstellen wandert, oder wie bisher, Standard-Implementierungen als abstrakte Klasse bereitgestellt wird, von dem wiederum andere Klassen ableiten. Als Beispiel sei auf die Datenstrukturen verwiesen: Eine Schnittstelle Collection schreibt Standardverhalten vor, AbstractCollection gibt eine Implementierung soweit möglich vor, und Unterklassen wie Listen setzen dann noch einmal auf diese Basisimplementierung auf. Erweiterte Schnittstellen können Hierarchien abbauen, denn auf eine abstrakte Basisimplementierung kann verzichtet werden. Auf der anderen Seite kann aber eine abstrakte Klasse Zustand über Objektvariablen einführen, was eine Schnittstelle nie könnte.

Default-Methoden können aber noch etwas ganz anderes: Sie können als Bauelemente für Klassen dienen. Eine Klasse kann mehrere Schnittstellen mit Default-Methoden implementieren und erbt im Grunde damit Basisfunktionalität von verschiedenen Stellen. In anderen Programmiersprachen ist das als Mixin bzw. Trait bekannt.


[1] Der Name hat sich während der Planung für dieses Feature mehrfach gewandelt. Ganz am Anfang war der Name „defender methods“ im Umlauf, dann lange Zeit virtuelle Erweiterungsmethoden (engl. virtual extension methods).

Repeating Annotations in Java 8 Build 68 eingezogen

Was das ist: http://openjdk.java.net/jeps/120

In der Klasse Class ist hinzugekommen:

public <A extends Annotation> A[] getAnnotations(Class<A> annotationClass)

public <A extends Annotation> A getDeclaredAnnotation(Class<A> annotationClass)

public <A extends Annotation> A[] getDeclaredAnnotations(Class<A> annotationClass)

Und noch ein paar weitere Ergänzungen in Package, System, …

Neu sind ebenfalls ContainerFor und ConainedBy.

http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=7154390, http://hg.openjdk.java.net/jdk8/jdk8/jdk/rev/735b93462eed

jdep: Paket-Abhängigkeiten anzeigen, neues Tool für Java 8 in Planung

http://mail.openjdk.java.net/pipermail/core-libs-dev/2012-December/012684.html. Aufruf:

$ jdep -h
Usage: jdeps <options> <files....>
where possible options include:
   -version                 Version information
   -classpath <path>        Specify where to find class files
   -summary                 Print dependency summary only
   -v:class                 Print class-level dependencies
   -v:package               Print package-level dependencies
   -p <package name>        Restrict analysis to classes in this package
                            (may be given multiple times)
   -e <regex>               Restrict analysis to packages matching pattern
                            (-p and -e are exclusive)
   -P  --profile            Show profile or the file containing a package
   -R  --recursive          Traverse all dependencies recursively
   -all                     Process all classes specified in -classpath

$ jdep Notepad.jar Ensemble.jar
Notepad.jar -> D:\tools\devtools\jdk8\windows-i586\jre\lib\rt.jar
<unnamed> (Notepad.jar)
       -> java.awt
       -> java.awt.event
       -> java.beans
       -> java.io
       -> java.lang
       -> java.net
       -> java.util
       -> java.util.logging
       -> javax.swing
       -> javax.swing.border
       -> javax.swing.event
       -> javax.swing.text
       -> javax.swing.tree
       -> javax.swing.undo

Ensemble.jar -> D:\tools\devtools\jdk8\windows-i586\jre\lib\jfxrt.jar
Ensemble.jar -> D:\tools\devtools\jdk8\windows-i586\jre\lib\rt.jar
    com.javafx.main (Ensemble.jar)
       -> java.applet
       -> java.awt
       -> java.awt.event
       -> java.io
       -> java.lang
       -> java.lang.reflect
       -> java.net
       -> java.security
       -> java.util
       -> java.util.jar
       -> javax.swing
       -> sun.misc                                 JDK internal API (rt.jar)

Statische sum(…)/max(…)/min(…) Methoden in numerischen Wrapper-Klassen

In den numerischen Wrapper-Klassen, also Byte, Short, Integer, Long, Float, Double und auch Character – obwohl Character nicht die Basisklasse Number erweitert – gibt es seit Java 8 je drei neue Methoden: sum(…)/max(…)/min(…), die genau das machen, was der Methodenname verspricht.

final class java.lang.Byte|Short|Integer|Long|Float|Double
extends Number
implements Comparable<Integer>

§ static Typ sum(Typ a, Typ b)
Bildet die Summe zweier Werte und liefert diese zurück. Es entspricht einem einfachen a + b. Die Angabe Typ steht dabei für den entsprechenden primitiven Typ byte short, int, long, float oder double, etwa in int sum(int a, int b).

§ static Typ min(Typ a, Typ b)
Liefert das Minimum der zwei Zahlen.

§ static Typ max(Typ a, Typ b)
Liefert das Maximum der zwei Zahlen.

final class java.lang.Character
implements Comparable<Character>, Serializable

§ static Typ sum(Typ a, Typ b)
Liefert (char)(a + b) zurück.

§ static Typ min(Typ a, Typ b)
Liefert das kleinere der beiden Zeichen bezüglich der Unicode-Position.

§ static Typ max(Typ a, Typ b)
Liefert das größere der beiden Zeichen.

Die Methoden sich für sich genommen nicht spannend. Für die Summe (Addition) tut es genauso gut der +-Operator – er steckt sowieso hinter den sum(…)-Methoden – und so wird keiner auf die Idee kommen i = Integer.sum(i, 1) statt i++ zu schreiben. Für das Maximum/Minimum bietet die Math-Klasse auch schon entsprechende Methoden min(a,b)/max(a,b). Der Grund für diese drei Methoden ist vielmehr, dass sie im Zusammenhang mit Lambda-Ausdrücken interessant sind – dazu später mehr.