Rückgabetypen und Typ-Inferenz bei Lambda-Deklarationen

Typinferenz spielt bei Lambda-Ausdrücken eine große Rolle – das gilt insbesondere für die Rückgabetypen, die überhaupt nicht in der Deklaration auftauchen. Bei 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, aber int taucht 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 = () -> 2; // 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 = () -> 2L;

oder

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

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

public static Long two() { return 2; } // Compilerfehler

wird vom Compiler angemeckert. Doch weil Generics bei funktionalen Schnittstellen viel häufiger sind, treten diese Besonderheiten öfters zu Tage auf als bei Methodendeklarationen.

Rekursive Lambda-Ausdrücke

Lambda-Ausdrücke können auf sich selbst verweisen, doch da ein this zur Selbstreferenz nicht möglich ist, ist ein kleiner Umweg nötig. Erstes muss eine Objekt- oder Klassenvariable deklariert werden, zweitens dann dieser Variablen ein Lambda-Ausdruck zugewiesen werden und dann kann drittens dieser 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 mit den der Operation T apply(int i), und 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 nicht. Zumal jetzt auch der Begriff „anonyme Funktion“ nicht mehr so richtig schlüssig ist, da der Lambda-Ausdruck ja doch einen Namen hat, nämlich fact.

Annotation @FunctionalInterface

Zwar eignet sich jede Schnittstelle mit einer abstrakten Methode als funktionale Schnittstelle und damit für einen Lambda-Ausdruck, doch nicht jede Schnittstelle, die im Moment nur eine abstrakte Methode deklariert, soll auch für Lambda-Ausdrücke verwendet werden. Der Compiler kann das jedoch nicht wissen, ob sich vielleicht Schnittstellen weiterentwickeln, und daher gibt es zur Dokumentation die Annotation FunctionalInterface im java.lang-Paket.

Beispiel: Eine eigene funktionale Schnittstelle soll FunctionalInterface markieren:

@FunctionalInterface

interface MyFunctionalInterface {

void foo();

}

Der Compiler prüft, ob die Schnittstelle mit einer solchen Annotation tatsächlich nur exakt eine abstrakte Methode enthält und löst einen Fehler aus, wenn dem nicht so ist. Aus Kompatibilitätsgründen erzwingt der Compiler nicht diese Annotation bei funktionalen Schnittstellen, um inneren Klassen von alten Schnittstellen einfach in Lambda-Ausdrücke umzuschreiben zu können. Die Annotation ist also keine Voraussetzung für die Nutzung in einem Lambda-Ausdruck und dient bisher nur der Dokumentation.

Die Umgebung der Lambda-Ausdrücke

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.

Namensräume

Deklariert eine innere anonyme Klasse in der Methode Variablen, so ist der Satz immer „neu“, beziehungsweise die neuen Variablen überschatten 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 Compilermeldung „variable compareIgnoreCase ist already defined“.

boolean compareIgnoreCase = true;

Comparator<String> c = (s1, s2) -> {

boolean compareIgnoreCase = false; // Compilerfehler

return …

}

this-Referenz

Lambda-Ausdrücke unterscheiden sich von inneren (anonymen) Klassen auch in dem, worauf die this-Referenz verweist: Bei Lambda-Ausdrücke zeigt this auf das Objekt, in dem der Lambda-Ausdruck eingebettet ist, bei inneren Klassen referenziert this die inneren Klasse.

class Application {

Application() {

Runnable run1 = () -> { System.out.println( this.getClass().getName() ); };

Runnable run2 = new Runnable() {

@Override public void run() { System.out.println( this.getClass().getName()); } };

run1.run(); // app.Application

run2.run(); // app.Application$1

}

public static void main( String[] args ) {

new Application();

}

}

Das Programm nutzt this einmal im Lambda-Ausdruck und einmal in der inneren Klasse. Im Fall vom Lambda-Ausdruck bezieht sich ausdrücklich auf das Application-Exemplar, was sich im Klassenamen niederschlägt. Bei der inneren Klasse bekommen wir den Anhang $1, weil es sich um ein anderes Exemplar handelt.

Base64-Kodierung (unter Java 8)

Für die Übertragung von Binärdaten hat sich im Internet die Base64-Kodierung durchgesetzt, die zum Beispiel bei E-Mail-Anhängen und SOAP-Nachrichten zu finden ist. Auch bei der HTTP-Authentifizierung Basic Authentication kommt Base64 zum Tragen, denn die Konkatenation von Benutzername + „:“ + Passwort wird über Base64 codiert und so zum Server gesendet – der Sicherheitsgewinn ist natürlich null.

Die Base64-Kodierung wird im RFC 4648[1] beschriebene. Drei Bytes (24 Bit) werden in vier Base64-kodierte Zeichen (vier Zeichen mit jeweils sechs repräsentativen Bits) umgesetzt. Die Konsequenz dieser Umformung ist, dass Binärdaten rund 33  % größer werden. Die Base64-Zeichen bestehen aus den Buchstaben des lateinischen Alphabets, den Ziffern 0 bis 9 sowie (im Normalfall) »+«, »/« und »=«.

Das JDK liefert seit Java 8 direkte Unterstützung für diese Base64-Umsetzung mit der Klasse java.util.BASE64 aus. Zwei inneren Klassen Base64.Decoder bzw. Base64.Encoder kümmern sich um die Umwandlung. Zur Erzeugung der Exemplare gibt es statische Methoden in BASE64, und zwar nicht nur zwei, sieben. Der Grund ist, dass es neben der Standard-Konvertierung „Base“ noch MIME und URL/Dateiname-sicher gibt:

· getEncoder() und getDecoder() liefern Exemplare vom Typ Base64.Encoder und Base64.Decoder bzw. für den normalen Basic-Typ.

· getEncoder(int lineLength, byte[] lineSeparator), getMimeEncoder() und getMimeDecoder() liefern Encoder/Decoder für MIME-Nachrichten, bei der Zeilen mit einem „\r“ getrennt sind.

· getUrlEncoder() und getUrlDecoder() nutzt zur Kodierung nur Zeichen, die für URL und Dateinamen gültig sind, und ersetzt „+“ durch „-“ und „/“ durch „_“.

Beispiel

Das folgende Beispiel erzeugt zuerst ein Byte-Feld mit Zufallszahlen. Die Base64-Klasse kodiert das Byte-Feld in einen String, der auf dem Bildschirm ausgegeben wird. Nachdem der String wieder zurückkodiert wurde, werden die Byte-Felder verglichen und liefern natürlich true:

byte[] bytes1 = SecureRandom.getSeed( 20 );

// byte[] -> String

String s = Base64.getEncoder().encodeToString( bytes1 );

System.out.println( s ); // z.B. TVST9v+JMk/vVUOSENmIcriXFLo=

// String -> byte[]

byte[] bytes2 = Base64.getDecoder().decode( s );

System.out.println( Arrays.equals(bytes1, bytes2) ); // true

Wer nicht mit Java 8 arbeiten kann, aber mit älteren Versionen vom Oracle JDK, der kann BASE64Encoder/BASE64Decoder aus dem nicht-öffentlichen Paket sun.misc nutzen.[2] Wem das nicht ganz geheuer ist, der kann javax.mail.internet.MimeUtility von der JavaMail-API nutzen[3] oder unter http://jakarta.apache.org/commons/codec/ die Commons Codec-Bibliothek beziehen.


[1] http://tools.ietf.org/html/rfc4648

[2] Siehe dazu http://java.sun.com/products/jdk/faq/faq-sun-packages.html. Bisher existieren sie aber seit über zehn Jahren, und wer Oracles Philosophie kennt, der weiß, dass die Abwärtskompatibilität oberste Priorität hat.

[3] http://www.rgagnon.com/javadetails/java-0598.html gibt ein Beispiel. Die JavaMail-API ist Teil von Java EE 5 und muss sonst für das Java SE als Bibliothek hinzugenommen werden.

Statische ausprogrammierte Methoden in Schnittstellen

In der Regel deklariert eine Schnittstelle Operationen, also abstrakte Objektmethoden, die eine Klasse später implementieren muss. Die in Klassen implementiere Schnittstellenmethode kann später wieder überschrieben werden, nimmt also ganz normal an der dynamischen Bindung teil. Einen Objektzustand kann die Schnittstelle nicht deklarieren, denn Objektvariablen sind in Schnittstellen tabu – jede deklarierte Variable ist automatisch statisch, also eine Klassenvariable.

Ab Java 8 lassen sich in Schnittstellen statische Methoden unterbringen und als Utility-Methoden neben Konstanten stellen. Als statische Methoden werden sie nicht dynamisch gebunden und der Aufruf ist ausschließlich über den Namen der Schnittstelle möglich. (Bei statischen Methoden von Klassen ist im Prinzip auch der Zugriff über eine Referenz erlaubt, wenn auch unerwünscht.)

Beispiel:

interface Buyable {
int MAX_PRICE = 10_000_000;

static boolean isValidPrice( double price ) { return price >= 0 && price < MAX_PRICE; }
double price();
}

Von außen ist dann der Aufruf Buyable.isValidPrice(123) möglich.

Alle deklarieren Eigenschaften sind immer implizit public, sodass dieser Sichtbarkeitsmodifizierer redundant ist.

Fassen wir die erlaubten Eigenschaften einer Schnittstelle zusammen:

 

Attribut

Methode

Objekt-

Nein, nicht erlaubt

Ja, üblicherweise abstrakt

Statische(s)

Ja, als Konstante

Ja, immer mit Implementierung

Erlaubte Eigenschaften einer Schnittstelle

 

Design: Eine Schnittstelle mit nur statischen Methoden ist ein Zeichen für ein Designproblem und sollte durch eine finale Klasse mit privaten Konstruktor ersetzt werden. Schnittstellen sind immer als Vorgaben zum Implementieren gedacht und wenn nur statische Methoden vorgekommen, erfüllt die Schnittstelle nicht ihren Zweck, dass sie Vorgaben macht, die unterschiedlich umgesetzt werden können.

Kalender-Exemplare bauen über den Calendar.Builder

Java 8 führte in Calendar die neue statische innere Klasse Builder ein, mit der sich leicht Calendar-Exemplare mit gesetzten Feldern aufbauen lassen. Die allgemeine Schreibweise ist wie folgt:

Calendar cal = new Calendar.Builder().setXXX( … ).setXXX( … ).setXXX( … ).build();

Zum Setzen von Feldern gibt es setXXX(…)-Methoden, am Ende folgt ein Aufruf von build(), der ein fertiges Calendar-Objekt liefert.

static class java.util.Calendar.Builder

§ Calendar.Builder setDate(int year, int month, int dayOfMonth)

§ Calendar.Builder set(int field, int value)

§ Calendar.Builder setFields(int… fieldValuePairs)

§ Calendar.Builder setInstant(Date instant)

§ Calendar.Builder setInstant(long instant)

§ Calendar.Builder setTimeOfDay(int hourOfDay, int minute, int second)

§ Calendar.Builder setTimeOfDay(int hourOfDay, int minute, int second, int millis)

§ Calendar.Builder setWeekDate(int weekYear, int weekOfYear, int dayOfWeek)

§ Calendar.Builder setTimeZone(TimeZone zone)

Etwas weniger gebräuchliche Mehtoden sind weiterhin setCalendarType(String type) – was Rückgaben von Calendar.getAvailableCalendarTypes() erlaubt und alternativ zu „gregory“ auch “gregorian“ bzw. “ iso8601“ –, setLenient(boolean lenient), setLocale(Locale locale) und setWeekDefinition(int firstDayOfWeek, int minimalDaysInFirstWeek).

Inselraus: Kalender-Typen getCalendarType()/getAvailableCalendarTypes()

Welcher Kalendertyp ein konkreter Kalender repräsentiert, ermittelt die Calendar-Objektmethode getCalendarType(); die Rückgabe ist ein String und lautet bei dem typischen Gregorianischen Kalender „gregory“, könnte aber auch „buddhist“ oder „japanese“ heißen. Welche Kalendertypen Java unterstützt, liefert die statische Methode Calendar.getAvailableCalendarTypes() als Set<String>. Im Moment sind es genau die drei genannten.

Würde geloggt werden?

Das Logging-Framwork versucht so schnell wie möglich zu entscheiden, ob eine Nachricht bei einem eingestellten Log-Levels geloggt werden soll oder nicht. Ist die Stufe in der Produktion zum Beispiel auf WARNING, sind INFO-Meldungen zu ignorieren. Problematisch aus Performance-Sicht sind zum Beispiel aufwändig aufgebaute Log-Nachrichten, die dann sowieso nicht geloggt werden. Der Plus-Operator bei Strings gehört nicht zu den beachtlichen Zeitfressern, doch ein

log.info( "Open file: " + filename );

führt zur Laufzeit immer zu einer String-Konkatenation, egal, ob die erzeugte Nachricht später geloggt wird oder nicht.

JUL bietet zur Umgehung des Problems zwei Lösungen. Als erstes bietet die Logger-Klasse eine Testmethode boolean isLoggable(Level level), über die ein schneller Test durchgeführt werden kann:

if ( log.isLoggable(Level.INFO) )

  log.info( "Open file: " + filename );

Natürlich kann info(…) nicht wissen, dass es auf jeden Fall loggen soll, daher findet der Test noch einmal statt. Eine allgemeine Überprüfung für alle Logging-Ausgaben bietet sich daher nicht an, sondern nur dann, wenn eine aufwändige Operation im Logging-Fall ausgeführt werden soll.

Die zweite Möglichkeit ist neu in Java 8. Sie nutzt Objekte vom Typ Supplier, die eine Implementierung enthalten, also etwa die Konkatenation. Im Prinzip hätte Oracle das auch schon vor Java 8 integrieren können, doch erst Lambda-Ausdrücke führen zu einer kompakten Schreibweise. Das sieht zum Beispiel so aus:

log.info( () -> { "Open file: " + filename } );

Die Default-Falle

Insbesondere bei Kodierungen und zeitgebundenen Eigenschaften müssen sich Entwickler zu jeder Zeit bewusst sein, welche Einstellung gerade verwendet wird. Neulinge greifen oft auf Default-Einstellungen zurück und String-Parsing mit Scanner und Ausgaben mit Formatter funktionieren in der Entwicklung, doch spätestens wenn die Software halb um den Globus wandert, läuft nichts mehr, weil die Default-Werte plötzlich anders sind.

Wenn Konstruktoren oder Methoden es nicht explizit verlangen, greift das JDK auf Standardwerte unter anderen für

· Zeilenendezeichen

· Zeichenkodierung

· Sprache (Locale)

· Zeitzone (TimeZone)

zurück.

Ein Beispiel: Der Konstruktor Scanner(File) öffnet eine Datei zum Lesen und konvertiert die Bytes in Unicodes mit einem Konverter, den die Default-Zeichenkodierung bestimmt. Wird aus dem Scanner eine Zahl gelesen, etwa mit nextDouble(), greift die voreingestellte Default-Locale, die dem Scanner sagt, ob Dezimalzahlen mit „,“ oder „.“ interpretiert werden muss. Verarbeitet ein Java-Programm die gleiche Textdatei einmal in den USA und Deutschland, ist das Ergebnis unterschiedlich und in der Regel sollte das nicht so sein.

Default-Werte sind eine gute Sache, allerdings sollten Entwickler sich bewusst sein, an welchen Stellen das JDK auf sie zurückgreift, um keine Überraschungen zu erleben. Es lohnt sich, immer konkrete Belegungen anzugeben, auch wenn als Argument zum Beispiel Locale.getDefault() steht. Das dokumentiert das gewollte Nutzen der Default-Werte.

Hashwerte von Wrapper-Objekten mit neuen Methoden ab Java 8

Der Hashwert eines Objekts bildet den Zustand auf eine kompakte Ganzzahl ab. Haben zwei Objekte ungleiche Hashwerte, so müssen auch die Objekte ungleich sein (mindest, wenn die Berechnung korrekt ist). Zur Bestimmung des Hashwertes deklariert jede Klasse über die Oberklasse java.lang.Object die Methode int hashCode(). Alle Wrapper-Klassen überschreiben diese Methode. Zudem kommen in Java 8 statische Methoden hinzu, sodass sich leicht der Hashwert berechnen lässt, ohne extra ein Wrapper-Objekte zu bilden.

Klasse

Klassenmethode

Objektmethode

Boolean

static int hashCode(boolean value)

int hashCode()

Byte

static int hashCode(byte value)

int hashCode()

Short

static int hashCode(short value)

int hashCode()

Integer

static int hashCode(int value)

int hashCode()

Long

static int hashCode(long value)

int hashCode()

Float

static int hashCode(float value)

int hashCode()

Double

static int hashCode(double value)

int hashCode()

Character

static int hashCode(char value)

int hashCode()

Abbildung 4 Statische Mehtoden hashCode(…) und Objektmethoden im Vergleich

 

Um den Hashwert eines ganzen Objekts zu errechnen, müssen folglich alle einzelnen Hashwerte berechnet werden und diese dann zu einer Ganzzahl verknüpft werden. Schematisch sieht das so aus:

int h1 = WrapperClass.hashCode( value1 );

int h2 = WrapperClass.hashCode( value2 );

int h3 = WrapperClass.hashCode( value3 );

Eclipse nutzt zur Verknüpfung der Hashwerte folgendes Muster, welches eine guter Ausgangspunkt ist:

int result = h1;

result = 31 * result + h2;

result = 31 * result + h3;

LinkedHashMap und LRU-Implementierungen

Da die Reihenfolge der eingefügten Elemente bei einem Assoziatspeicher verloren geht, gibt es mit LinkedHashMap eine Mischung, also ein schneller Assoziativspeicher mit gleichzeitiger Speicherung der Reihenfolge der Objekte. Die Bauart vom Klassename LinkedHashMap macht schon deutlich, dass es eine Map ist, und die Reihenfolge der Objekte liefert ein Iterator; es gibt keine listenähnliche Schnittstelle mit get(int). LinkedHashMap ist für Assoziativspeicher das, was LinkedHashSet für HashSet ist.

Im Gegensatz zur normalen HashMap ruft LinkedHashMap immer genau dann die besondere Methode boolean removeEldestEntry(Map.Entry<K,V> eldest) auf, wenn intern ein Element der Sammlung hinzugenommen wird. Die Standardimplementierung dieser Methode liefert immer false, was bedeutet, dass das älteste Element nicht gelöscht werden soll, wen ein neues hinzukommt. Doch bietet das JDK die Methode aus Absicht protected an, denn sie kann von uns überschrieben werden, um eine Datenstruktur aufzubauen, die eine maximal Anzahl Elemente hat. So sieht das aus:

package com.tutego.insel.util.map;

import java.util.*;

public class LRUMap<K,V> extends LinkedHashMap<K, V> {
  private final int capacity;

  public LRUMap( int capacity ) {
    super( capacity, 0.75f, true );
    this.capacity = capacity;
  }

  @Override
  protected boolean removeEldestEntry( Map.Entry<K, V> eldest ) {
    return size() > capacity;
  }
}

LinkedHashSet bietet eine vergleichbare Methode removeEldestEntry(…) nicht. Wer dies benötigt, muss eine eigene Mengenklasse auf der Basis von LinkedHashMap realisieren.

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.