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.

Doch erst mal keine privaten Interface-Methoden

So schreibt Brian Goetz:

> We would like to pull back two small features from the JSR-335 feature plan:
>
>  - private methods in interfaces
>  - "package modifier" for package-private visibility
>
> The primary reason is resourcing; cutting some small and inessential
> features made room for deeper work on more important things like type
> inference (on which we've made some big improvements lately!)  Private
> methods are also an incomplete feature; we'd like the full set of
> visibilities, and limiting to public/private was already a compromise based
> on what we thought we could get done in the timeframe we had.  But it would
> still be a rough edge that protected/package were missing.
>
> The second feature, while trivial (though nothing is really trivial), loses
> a lot of justification without at least a move towards the full set of
> accessibilities.  As it stands, it is pretty far afield of lambda, nothing
> else depends on it, and not doing it now does not preclude doing it later.
> (The only remaining connection to lambda is accelerating the death of the
> phrase "default visibility" to avoid confusion with default methods.)
>

Die nächsten beiden Tage werden für Java 8 spannend, denn …

… am 31.01.2013 muss das JDK 8 http://openjdk.java.net/projects/jdk8/milestones#Feature_Complete sein. Date & Time hat es noch geschafft. Dann müssen wir den M6 bekommen, das laut Vorgaben enthält:

101 Generalized Target-Type Inference

103 Parallel Array Sorting

104 Annotations on Java Types

109 Enhance Core Libraries with Lambda

115 AEAD CipherSuites

118 Access to Parameter Names at Runtime

119 javax.lang.model Implementation Backed by Core Reflection

120 Repeating Annotations

126 Lambda Expressions & Virtual Extension Methods

135 Base64 Encoding & Decoding

138 Autoconf-Based Build System

139 Enhance javac to Improve Build Speed

140 Limited doPrivileged

142 Reduce Cache Contention on Specified Fields

143 Improve Contended Locking

147 Reduce Class Metadata Footprint

148 Small VM

149 Reduce Core-Library Memory Usage

150 Date & Time API

160 Lambda-Form Representation for Method Handles

161 Compact Profiles

162 Prepare for Modularization

164 Leverage CPU Instructions for AES Cryptography

165 Compiler Control

166 Overhaul JKS-JCEKS-PKCS12 Keystores

170 JDBC 4.2

172 DocLint

173 Retire Some Rarely-Used GC Combinations

So wie ich das überblicke, sind die meisten Punkte realisiert.

Java 8 und JSR-308, was ist Stand der Dinge?

Die Idee bei JSR-308: Annotationen an allem möglichen Typen dranmachen (daher auch der Name “Type Annotations”). Z.B. so:

Map<@NonNull String, @NonEmpty List<@Readonly Document>> files;

Mehr auch unter http://jcp.org/aboutJava/communityprocess/ec-public/materials/2012-01-1011/jsr308-201201.pdf oder in der Spec http://types.cs.washington.edu/jsr308/specification/java-annotation-design.html.

Mit dem aktuellen JDK 8 können zwar Annotationen deklariert werden, die für neue “Orte” stehen (http://download.java.net/jdk8/docs/api/java/lang/annotation/ElementType.html hat seit 1.8 TYPE_PARAMETER und TYPE_USE), aber sonst ist mit Standardcompiler nicht viel los. Testet man oberes Beispiel, gibt es nur Fehler:

@Target(value= ElementType.TYPE_USE)
@interface NonNull { }

@Target(value= ElementType.TYPE_USE)
@interface NonEmpty { }

@Target(value= ElementType.TYPE_USE)
@interface Readonly { }

class Document {}

class Main {
    Map<@NonNull String, @NonEmpty List<@Readonly Document>> files;
}

Dann rappelt es nur:
C:\Users\Christian\Documents\NetBeansProjects\App\src\app\Main.java:53: error: illegal start of type
    Map<@NonNull String, @NonEmpty List<@Readonly Document>> files;
C:\Users\Christian\Documents\NetBeansProjects\App\src\app\Main.java:53: error: ‚;‘ expected
    Map<@NonNull String, @NonEmpty List<@Readonly Document>> files;
C:\Users\Christian\Documents\NetBeansProjects\App\src\app\Main.java:53: error: <identifier> expected
    Map<@NonNull String, @NonEmpty List<@Readonly Document>> files;
C:\Users\Christian\Documents\NetBeansProjects\App\src\app\Main.java:53: error: <identifier> expected
    Map<@NonNull String, @NonEmpty List<@Readonly Document>> files;
C:\Users\Christian\Documents\NetBeansProjects\App\src\app\Main.java:53: error: ‚;‘ expected
    Map<@NonNull String, @NonEmpty List<@Readonly Document>> files;
C:\Users\Christian\Documents\NetBeansProjects\App\src\app\Main.java:53: error: <identifier> expected
    Map<@NonNull String, @NonEmpty List<@Readonly Document>> files;
C:\Users\Christian\Documents\NetBeansProjects\App\src\app\Main.java:53: error: ‚(‚ expected
    Map<@NonNull String, @NonEmpty List<@Readonly Document>> files;
C:\Users\Christian\Documents\NetBeansProjects\App\src\app\Main.java:53: error: <identifier> expected
    Map<@NonNull String, @NonEmpty List<@Readonly Document>> files;

Wenn man JSR-308 wirklich nutzen möchte, greift man zur experimentelle Version http://openjdk.java.net/projects/type-annotations/. Wenn Oracle hier alles für rund hält, wandert das in das normale OpenJDK 8 Projekt. Das ist wie mit Lambda und dem OpenJDK 8.

Wenn man dann einen funktionierenden Compiler und eine Unterstützung hat, die Annotationen auszulesen, kann ein Checker diverse Sachen testen. Interessant sind @NonNull-Dinger oder auch Test für Immutibility. Hier ist http://types.cs.washington.edu/jsr308/ interessant, ein Checker-Framework, das in den Compiler integriert wird.

JSR-308 ist schon ewig im Gespräch, 2007 (!) hatte ich das schon im Blog: http://www.tutego.de/blog/javainsel/2007/05/erste-implementierung-fur-jsr-308/

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.

Mit welchen Java-Open-Source Libs Java besser lernen?

Die Apache Commons Lang Lib finde ich für den Einstieg gut geeignet. (Guava ist echt hart für jmd. der gerade seine Java-Gewässer erkundet; ist was für nach dem ersten Hügel.) Nicht zu vergessen die Sourcen zu den Java-Libs selbst. Oder vorbereiten auf Java 8, in etwa mit einem Blick auf die Date-Time-API JSR-310 (SourceForge.net: threeten) wirft.
Was sonst noch?
* log4j bwz. diverse Nachfolger.
* FreeMarker
* Apache Commons CLI oder Alternativen
Dann noch spezielle Libs, wenn man in diversen Technologien einsteigt
* Code aus SwingX (sehr gut, wenn man Swing-Entwicklung macht)
* Tag-Libs
* JavaFX-Extentions
* Eclipse-Plugins
Sonst kann man noch die Java Certification Exams durchgehen für ein besseres Verständnis der Sprache an sich.
Und meine RTFLib jrtf hat noch ein paar Issues offen

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