Klassen mit einer abstrakten Methode als funktionale Schnittstelle?

Als die Entwickler der Sprache Java die Lambda-Ausdrücke diskutierten, stand auch die Frage im Raum, ob abstrakte Klassen, die nur über eine abstrakte Methode verfügen, ebenfalls für Lambda-Ausdrücke genutzt werden können.[1] Sie entschieden sich dagegen, unter anderem deswegen, weil bei der Implementierung von Schnittstellen die JVM weitreichende Optimierungen vornehmen kann. Und bei Klassen wird das schwierig. Das liegt auch daran, dass ein Konstruktor umfangreiche Initialisierungen mit Seiteneffekten vornimmt (die Konstruktoren aller Oberklassen nicht zu vergessen), sowie Ausnahmen auslösen könnte. Gewünscht ist aber nur die Ausführung einer Implementierung der funktionalen Schnittstelle und kein anderer Code.

Es gibt nun im JDK einige abstrakte Klassen, die genau eine abstrakte Methode vorschreiben, etwa java.util.TimerTask. Solche Klassen können nicht über einen Lambda-Ausdruck realisiert werden; hier müssen Entwickler weiterhin zu Klassenimplementierungen greifen, und die kürzeste Lösung ist eine innere anonyme Klasse. Eigene Hilfsklassen können natürlich den Code etwas abkürzen, aber eben nur mit Hilfe einer eigenen Implementierung.

Wer abstrakte Methoden mit Lambda-Ausdrücken implementieren möchte, kann mit Hilfsklassen arbeiten. Denn wenn eine Hilfsklasse funktionale Schnittstellen einsetzt, so können Lambda-Ausdrücke wieder ins Spiel kommen, in dem die Implementierung der abstrakten Methode an den Lambda-Ausdruck weiterleitet. Nehmen wir das Beispiel für TimerTask und gehen zwei unterschiedliche Strategien der Implementierung durch. Mit Delegation sieht das so aus:

import java.util.*;

class TimerTaskLambda {

  public static TimerTask createTimerTask( Runnable runnable ) {
    return new TimerTask() {
        @Override public void run() { runnable.run(); }
    };
  }
 
  public static void main( String[] args ) {
    new Timer().schedule( createTimerTask( () -> System.out.println("Hi") ), 500 );
  }
}

Mit Vererbung erhalten wir:

public class LambdaTimerTask extends TimerTask {
  private final Runnable runnable;
    public LambdaTimerTask( Runnable runnable ) {
    this.runnable = runnable;
  }
   
  @Override public void run() { runnable.run(); }
}

Der Aufruf erfolgt dann statt createTimerTask(…) mit dem Konstruktor:

new Timer().schedule( new LambdaTimerTask( () -> System.out.println("Hi") ), 500 );

[1]              Früher wurde hier die Abkürzung SAM (Single Abstract Method) genutzt.

Ausnahmen in Lambda-Ausdrücken

Lambda-Ausdrücke sind Implementierung von funktionalen Schnittstellen, und bisher haben wir noch nicht die Frage betrachtet was passiert, wenn der Code-Block vom Lambda-Ausdruck eine Ausnahme auslöst und wer diese Auffangen muss.

Ausnahmen im Code-Block eines Lambda-Ausdrucks

In java.util.function gibt es eine funktionale Schnittstelle Predicate, dessen Deklaration im Kern wie folgt ist:

public interface Predicate<T> { boolean test( T t ); }

Ein Predicate führt einen Test durch und liefert wahr oder falsch als Ergebnis. Ein Lambda-Ausdruck kann diese Schnittstelle nun implementieren. Nehmen wir an, wir wollen Testen, ob eine Datei die Länge 0 hat, um etwa Datei-Leichen zu finden. In einer ersten Idee greifen wir auf die existierende Files-Klasse zurück, die size(…)anbietet:

Predicate<Path> isEmptyFile = path -> Files.size( path ) == 0;  // N Compilerfehler

Problem dabei ist, das Files.size(…) eine IOException auslöst, die behandelt werden muss und zwar nicht vom Block, in dem der Lambda-Ausdruck als Ganzes steht, sondern vom Code im Lambda-Ausdruck selbst. Das schreibt der Compiler so vor. Folgendes ist also keine Lösung:

try {
  Predicate<Path> isEmptyFile = path -> Files.size( path ) == 0; // Nee
} catch ( IOException e ) { … }

sondern nur:

Predicate<Path> isEmptyFile = path -> {
  try {
    return Files.size( path ) == 0;
  } catch ( IOException e ) { return false; }
};

Die Eigenschaft, die Java fehlt, nennt sich Exception-Transparenz, und hier ist deutlich der Unterschied zwischen geprüften und ungeprüften Ausnahmen zu sehen. Bei der Exception-Transparenz wäre kein Ausnahmebehandlung im Lambda-Ausdruck nötig und an einer übergeordneten Stelle möglich. Doch da diese Möglichkeit in Java fehlt, bleibt uns nur übrig, geprüfte Ausnahmen im Lambda-Ausdrücken direkt zu behandeln.

Funktionale Schnittstellen mit throws-Klausel

Ungeprüfte Ausnahmen können immer auftreten und führen (nicht abgefangen) wie üblich zum Abbruch des Threads. Eine throws-Klausel an den Methoden/Konstruktoren ist dafür nicht nötig. Doch können Funktionale Schnittstellen eine throws-Klausel mit geprüften Ausnahmen deklarieren, und die Implementierung einer funktionalen Schnittstelle kann logischerweise geprüfte Ausnahmen auslösen.

Eine Deklaration wie Callable aus dem Paket java.util.concurrent macht das deutlich. (Callable trägt kein @FunctionalInterface):

public interface Callable<V> {
  V call() throws Exception;
}

Das könnte durch folgenden Lambda-Ausdruck realisiert werden:

Callable<Integer> randomDice = () -> (int)(Math.random() * 6) + 1;

Der Aufruf von call() auf einem randomDice muss mit einer Ausnahmebehandlung einher gehen, da call() eine Exception auslöst, etwa so:

try {
  System.out.println( randomDice.call() );
  System.out.println( randomDice.call() );
}
catch ( Exception e ) { … }

Dass der Aufrufer die Ausnahme behandeln muss ist klar. Die Deklaration des Lambda-Ausdrucks enthält keinen Hinweis auf die Ausnahme, das ist ein Unterschied zum vorangegangenen Abschnitt.

Design-Tipp

Ausnahmen in Methoden funktionaler Schnittstellen schränken den Nutzen stark ein, und daher löst keine der funktionalen Schnittstellen aus etwa java.util.function eine geprüfte Ausnahme aus. Der Grund ist einfach, denn jeder Methodenaufrufer müsste sonst entweder die Ausnahme weiterleiten oder behandeln.[1]

Um die Einschränkungen und Probleme mit einer throws-Klausel  noch etwas deutlicher zu machen stellen wir uns vor, dass die funktionale Schnittstelle Predicate ein throws Exception (vom Sinn der Typs Exception an sich einmal abgesehen) enthält:

interface Predicate<T> { boolean test( T t ) throws Exception; } // Was wäre wenn?

Die Konsequenz wäre, dass jeder Aurufer von test(…) nun seinerseits die Exception in die Hände bekommt und sie auffangen oder weiterleiten muss. Leitet der test(….)-Aufrufer mit throws Exception die Ausnahme weiter nach oben, bekommen wir plötzlich an allen Stellen ein throws Exception in die Methodensignatur, was auf keinen Fall gewünscht ist. So enthält jetzt etwa ArrayList eine Deklaration von removeIf(Predicate filter); hier müsste dann removeIf(…) – was letztendlich filter.test(…) aufruft – sich mit der Test-Ausnahme rumärgern und removeIf(Predicate filter) throws Exception ist keine gute Sache.

Von geprüft nach ungeprüft

Geprüfte Ausnahmen sind in Lamba-Ausdrücken nicht schön. Eine Lösung ist, Code, der geprüfte Ausnahmen auslöst, zu verpacken und die geprüfte Ausnahme in einer ungeprüften zu manteln. Das kann etwa so aussehen:

public class PredicateWithException {

 @FunctionalInterface
 public interface ExceptionalPredicate<T, E extends Exception> {
   boolean test( T t ) throws E;
 }

 public static <T> Predicate<T> asUncheckedPredicate( ExceptionalPredicate<T, Exception> predicate ) {
  return t -> {
   try {
    return predicate.test( t );
   }
   catch ( Exception e ) {
    throw new RuntimeException( e.getMessage(), e );
   }
  };
 }
 public static void main( String[] args ) {
  Predicate<Path> isEmptyFile = asUncheckedPredicate( path -> Files.size( path ) == 0 );
  System.out.println( isEmptyFile.test( Paths.get( "c:/" ) ) );
 }
}

Die Schnittstelle ExceptionalPredicate ist ein Prädikat mit optionaler Ausnahme. In der eigenen Hilfsmethode asUncheckedPredicate(ExceptionalPredicate) nehmen wir so ein ExceptionalPredicate an und packen es in ein Predicate, was die Methode zurückgibt. Geprüfte Ausnahmen werden in eine ungeprüfte Ausnahme vom Typ RuntimeException gesetzt. Somit muss Predicate keine geprüfte Ausnahme weiterleiten, was es ja laut Deklaration auch nicht kann.


[1]       Von Callable gibt es zwar Nutzer, die mit Nebenläufigkeit (daher das Paket java.util.concurrent) in Zusammenhang stehen, aber keine weiteren Verwendungen in der Java-Bibliothek, von zwei Beispielen aus javax.tools abgesehen. Mit java.util.function.Supplier existiert eine entsprechende Alternative ohne throws-Klausel.

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, sondern nur neue und vielleicht überdeckte Variablen durch die Parameter. Das ist einer der grundlegenden Unterschiede zwischen Lambda-Ausdrücken und inneren Klassen. Somit ist auch die Bedeutung von this und super bei Lambda-Ausdrücken und inneren Klassen unterschiedlich.

Zugriff auf finale, lokale Variablen/Parametervariablen

Lambda-Ausdrücke können problemlos auf Objektvariablen und Klassenvariablen lesend und schreibend zugreifen. Auch auf lokale Variablen und Parameter hat ein Lambda-Ausdruck Zugriff. Doch greift ein Lambda-Ausdruck auf lokale Variablen bzw. Parametervariablen zu, müssen diese final sein. Dass eine Variable final ist, muss nicht extra mit einem Modifizierer geschrieben werden, aber sie muss effektiv final (engl. effectively final) sein. Effektiv final ist eine Variable, wenn sie nach der Initialisierung nicht mehr beschrieben wird.

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

Neu eingeschobene Lambda-Ausdrücke, die auf lokale Variablen bzw. Parametervariablen zugreifen, können also im Nachhinein zu Compilerfehlern führen. Folgendes Segment ist ohne Lambda-Ausdruck korrekt:

/*1*/ boolean compareIgnoreCase = new Scanner( System.in ).nextBoolean();
/*2*/ …
/*3*/ compareIgnoreCase = true;

Schiebt sich zwischen Zeile 1 und 3 nachträglich ein Lambda-Ausdruck rein, der auf compareIgnoreCase zugreift, gibt es anschließend einen Compilerfehler. Allerdings liegt der Fehler nicht in Zeile 3, sondern beim Lambda-Ausdruck. Denn die Variable compareIgnoreCase ist nach der Änderung nicht mehr effektiv final, was sie aber sein müsste, um in dem Lambda-Ausdruck verwendet zu werden.

Tipp

Lambda-Ausdrücke verhalten sich genauso wie innere anonyme Klassen, die auch nur auf finale Variablen zugreifen können. Mit Behältern, wie einem Feld oder den speziellen AtomicXXX-Klassen aus dem java.util.concurrent.atomic-Paket, lässt sich das Problem im Prinzip lösen. Denn greift ein Lambda-Ausdruck etwa auf das Feld boolean[] compareIgnoreCase = new boolean[1]; zu, so ist die Variable compareIgnoreCase selbst final, aber compareIgnoreCase[0] = true; ist erlaubt und ein Schreibzugriff auf das Feld, nicht der Variablen. Je nach Code besteht jedoch die Gefahr, dass Lambda-Ausdrücke parallel ausgeführt werden. Wird etwa ein Lambda-Ausdruck mit Veränderung auf diesem Feldinhalt parallel ausgeführt, so ist der Zugriff nicht synchronisiert und das Ergebnis kann „kaputt“ sein, denn paralleler Zugriff auf Variablen muss immer koordiniert vorgenommen werden.

Namensräume

Deklariert eine innere anonyme Klasse Variablen innerhalb der Methode, so sind diese immer „neu“, das heißt, die neuen Variablen überlagern 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 Fehlermeldung des Compilers: „variable compareIgnoreCase ist already defined“.

boolean compareIgnoreCase = true;
Comparator<String> c = (s1, s2) -> {
  boolean compareIgnoreCase = false;  // N Compilerfehler
  return …
}

this-Referenz

Ein Lambda-Ausdruck unterscheidet sich von einer inneren (anonymen) Klasse auch in dem, worauf die this-Referenz verweist:

  • Beim Lambda-Ausdruck zeigt this immer auf das Objekt, in dem der Lambda-Ausdruck eingebettet ist.
  • Bei einer inneren Klasse referenziert this die innere Klasse, und die ist ein komplett neuer Typ.

Folgendes Beispiel macht das deutlich:

class InnerVsLambdaThis {
  InnerVsLambdaThis() {
    Runnable lambdaRun = () -> System.out.println( this.getClass().getName() );
    Runnable innerRun  = new Runnable() {
      @Override public void run() { System.out.println( this.getClass().getName()); }
    };
    lambdaRun.run();      // InnerVsLambdaThis
    innerRun.run();       // InnerVsLambdaThis$1
  }
 
  public static void main( String[] args ) {
    new InnerVsLambdaThis();
  }
}

Als erstes nutzen wir this in einen Lambda-Ausdruck im Konstruktor der Klasse InnerVsLambdaThis. Damit bezieht sich this auf jedes gebaute InnerVsLambdaThis-Objekt. Bei der inneren Klasse referenziert this ein anderes Exemplar und zwar vom Typ Runnable. Da es bei anonymen Kassen keinen Namen hat, trägt es lediglich die Kennung InnerVsLambdaThis$1.

Rekursive Lambda-Ausdrücke

Lambda-Ausdrücke können auf sich selbst verweisen. Da aber ein this zur Selbstreferenz nicht funktioniert, ist ein kleiner Umweg nötig. Erst muss eine Objekt- oder eine Klassenvariable deklariert werden, dann muss dieser Variablen ein Lambda-Ausdruck zugewiesen werden, und dann kann der 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 aus dem Paket java.util.function mit einer Operation T apply(int i). 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 hier nicht. Zumal jetzt auch der Begriff „anonyme Methode“ nicht mehr so richtig passt, da der Lambda-Ausdruck ja doch einen Namen hat, nämlich fact. Und weil der Lambda-Ausdruck einer Variablen zugewiesen wurde, kann er in dieser Form natürlich auch nicht mehr als Implementierung an eine Methode oder einen Konstruktor übergeben werden, sondern nur als Methoden/Konstruktor-Referenz, dazu später mehr.

Syntax für Lambda-Ausdrücke

Lambda-Ausdrücke haben wie Methoden mögliche Parameter- und Rückgabe-Werte. Die Java-Grammatik für die Schreibweise von Lambda-Ausdrücken sieht ein paar nützliche syntaktische Abkürzungen vor.

Ausführliche Schreibweise

Lambda-Ausdrücke lassen sich auf unterschiedliche Arten und Weisen schreiben, da es für diverse Konstruktionen Abkürzungen gibt. Eine Form, die jedoch immer gilt ist:

‚(‚ LambdaParameter ‚)‘ ‚->‘ ‚{‚ Anweisungen ‚}‘

Der Lambda-Parameter besteht (voll ausgeschrieben) wie ein Methodenparameter aus a) dem Typ, b) dem Namen und c) optionalen Modifizieren.

Der Parametername öffnet einen neuen Gültigkeitsbereich für eine Variable, wobei der Parametername keine anderen Namen von lokalen Variablen überlagern darf. Hier verhält sich die Lambda-Parametervariable wie eine neue Variable aus einem inneren Block und nicht wie eine Variable aus einer inneren Klasse, wo die Sichtbarkeit anders ist.

Beispiel

Folgendes gibt einen Compilerfehler im Lambda-Ausdruck, weil var schon deklariert ist, die Parametervariable vom Lambda-Ausdruck muss also „frisch“ sein:

String var = "";
var.chars().forEach( var -> { System.out.println( var ); } );  //  Compilerfehler

Abkürzung 1: Typinferenz

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

Sind für den Compiler genug Typ-Informationen verfügbar, dann erlaubt der Compiler bei Lambda-Ausdrücken eine Abkürzung. Bei

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

ist Typ-Inferenz einfach (Comparator<String> sagt alles aus), daher funktioniert die folgende  Abkürzung:

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

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

Wenn der Compiler die Typen ablesen kann, sind die Parametertypen optional. Aber Typ-Inferenz ist nicht immer möglich, weshalb die Abkürzung nicht immer möglich ist. Außerdem hilft die explizite Schreibweise auch der Lesbarkeit: kurze Ausdrücke sind nicht unbedingt die verständlichsten.

Hinweis

Der Compiler liest aus den Typen ab, ob alle Eigenschaften vorhanden sind. Die Typen sind dabei entweder explizit oder implizit gegeben.

Comparator<String> sc = (a, b) -> { return Integer.compare( a.length(), b.length() ); };
Comparator<BitSet> bc = (a, b) -> { return Integer.compare( a.length(), b.length() ); };

Die Klassen String und BitSet besitzen beide die Methode length(), daher ist der Lambda-Ausdruck korrekt. Der gleiche Lambda-Code lässt sich für zwei völlig verschiedene Klassen einsetzen, die überhaupt keine Gemeinsamkeiten haben, nur das sie zufällig beide eine Methode namens length() besitzen.

Abkürzung 2: 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

( LambdaParameter ) -> { return Ausdruck; }

heißt es dann

( LambdaParameter ) -> Ausdruck

Lambda-Ausdrücke mit einer return–Anweisung im Rumpf kommen häufig vor, da dies den typischen Funktionen entspricht. Somit 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.

Hier sind 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()

Ausführliche und abgekürzte Schreibweise

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. Das heißt, wenn Lambda-Ausdrücke mit der kurzen Ausdrucks-Syntax eingesetzt werden, können diese Ausdrücke etwas zurückgeben, müssen aber nicht.

Hinweis

Die Schreibweise mit den geschweiften Klammern und den Rückgabe-Ausdrücken kann nicht gemischt werden. Entweder gibt es ein Block geschweifter Klammern und return oder keine Klammern und kein return-Schlüsselwort. Fehler ergeben also diese falschen Mischungen:

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

Würden wir in (1) ein explizites return nutzen wäre alles in Ordnung, würde bei (2) das return wegfallen wäre die Zeile auch compilierbar.

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

  • void-kompatibel: Der Lambda-Rumpf gibt kein Ergebnis zurück. Entweder weil der Block kein return enthält, oder ein return ohne Rückgabe, oder weil ein void-Ausdruck in der verkürzten Schreibweise eingesetzt wird. Der Lambda-Ausdruck () -> System.out.println() ist also void-kompatibel, genauso wie () -> {}.
  • Wert-kompatibel: Der Rumpf beendet den Lambda-Ausdruck mit einer return-Anweisung, die einen Wert zurückgibt oder besteht aus der kompakten Schreibenweise mit einer Rückgabe ungleich void.

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

Abkürzung 3: Einzelner Identifizierer statt Parameterliste und Klammern

Besteht die Parameterliste

a) nur aus einem einzelnen Identifizierer und

b) ist der Typ durch Typ-Inferenz klar,

können die runden Klammern wegfallen.

Lange Schreibweise Typen inferred Vollständig abgekürzt
(String s) -> s.length() (s) -> s.length() s -> s.length()
(int i) -> Math.abs( i ) (i) -> Math.abs( i ) i -> Math.abs( i )

Unterschiedlicher Grad von Abkürzungen

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

Syntax-Hinweis

Nur bei genau einem Lambda-Parameter können die Klammern weggelassen werden, da es sonst Mehrdeutigkeiten gibt, für die es sonst wieder komplexe Regeln zur Auflösung geben müsste. Heißt es etwa foo( k, v -> { … } ) ist es unklar, ob foo zwei Parameter deklariert. Ist das zweite Argument ein Lambda-Ausdruck, oder handelt es sich um nur genau einen Parameter, wobei dann ein Lambda-Ausdruck übergeben wird, der selbst zwei Parameter deklariert. Um Probleme wie diesen aus dem Weg zu gehen, können Entwickler auf den ersten Blick sehen, dass foo( k, v -> { … } ) eindeutig für Parameter steht, und foo( (k, v) -> { … } ) nur einen Parameter besitzt.

Java 8 Syntax-Migration mit Eclipse

Seit der Version Eclipse 4.4 (bzw. Eclipse 4.3 mit Java 8-Erweiterung) integriert die IDE einen Code-Transformator, der Klassen-Implementierung von funktionalen Schnittellen in Lambda-Ausdrücke verkürzen kann. Diese Transformationen ist über die Quick-Fix/Quick-Assist möglich.

Eclipse_ConvertToLambda

Leider kann diese Umsetzung (bisher) nicht für ein ganzes Projekt erfolgen, und keine umfangreiche Code-Basis komplett und automatisch auf die neuen Lambda-Ausdrücke gebracht werden.

Java 8 Syntax-Migration mit NetBeans

Die aktuelle NetBeans IDE bringt einen Code-Transformer mit, der elegant die Code-Basis durchsucht und Transformationen automatisch durchführt. Das Menü Refactor bietet den Unterpunkt Inspect and Transform…, der zu einem Dialog führt:

NB_InspectAndTransform

Unter Manage… öffnet sich ein weiter Dialog, bei dem JDK Migration Support aktiviert ist und einige Änderungen voreingestellt sind, neu für Java 8 ist Covert to Lambda or Method References. Auch die Transformationen in die neue Java 7-Syntax sind voreingestellt, aktiviert werden kann: Can use Diamond, Convert to try-with-resources, Join catch sections using mulitcatch, Use specific catch und Use switch over Strings when possible.

NB_InspectAndTransform2

Wer neu Programmcode mit NetBeans schreibt wird gleich auf die neue Syntax hingewiesen. Vervollständigungen nutzen ganz natürlich zum Beispiel den Diamond-Operator.

Metadata space in JDK 8 HotSpot JVM

Wenn es um Java geht, müssen wir immer unterscheiden, ob es um die Sprache selbst geht, um die Bibliotheken, um die JVM oder um die Implementierung von Oracle, die das JDK darstellt. Eine zentrale Neuerung in der Version 8 der HotSpot JVM ist der Umgang bei der internen Repräsentation von Klassen und Metadaten; die Daten landen nicht mehr auf dem Java-Heap, sondern im nativen Speicher, der Metaspace heißt. Vorher gehörte ein java.lang.OutOfMemoryError: PermGen space zu bei großen Anwendungen zum Alltag[1], das Problem ist (im Prinzip) Vergangenheit, es gibt kein Perm-Space mehr in der JDK 8 JVM; die Oracle JRockit und JVM von IBM hatten übrigens immer Metaspace. Der Metaspace-Speicherbedarf kann mit dem Schalter MaxMetaspaceSize begrenzt werden, ein Übertritt führt zur java.lang.OutOfMemoryError: Metadata space. Eine Begrenzung ist nicht verkehrt, das Ergebnis unter Java 7 und Java 8 aber das gleiche, ein OutOfMemoryError.


[1] http://stackoverflow.com/questions/88235/dealing-with-java-lang-outofmemoryerror-permgen-space-error

Java 8, es ist geschafft

Heute hat Oracle Java 8 freigegeben, es ist vollbracht. Nach langer Verzögerung können wir uns auf Lambda-Ausdrücke, einer neuen Date-Time-API und einer chicen Stream-API freuen. Auch NetBeans 8 ist mit am Start.

Die Java Insel wird in den beiden Bänden umfassend auf Java 8 eingehen, die Bücher werden in ca. 1,5 Monaten in den Buchhandlungen ausliegen und als eBook verfügbar sein. Mit den Ergänzungen bin ich komplett durch, auch das Kapitel über die Date-Time-API war nicht so übel wie gedacht, wobei mich die Typen und Hierarchien schwindlig machen machen wir auch bei den FX-Properties. Insgesamt habe ich beim Schreiben und Ausprobieren einen tiefen Eindruck in Java 8 bekommen und meine interne tutego Schulungssoftware auf Java 8 gebracht. Das passt soweit und ist rund. Ätzend ist, dass Optional nicht serialisierbar ist und bei GWT eine Sonderbehandlung braucht. Auch die Date-Time-Typen sind noch zu neu, als dass sie bei GWT, XStream, JPA berücksichtigt werden.

Kommandozeilenprogramme jrunscript und jjs

Java bringt zwei Werkzeuge im bin-Verzeichnis zum Ausführen von Skripte mit:

· jrunscript: Führt ein Skript mit einer JSR-223-kompatiblen Skript-Einige aus. Standardmäßig ist das JavaScript, weil keine anderen Skript-Engine vorinstalliert ist. Der Schalter -I bestimmt alternative Skript-Sprachen. Zur weiteren Dokumentation der Optionen siehe http://download.java.net/jdk8/docs/technotes/tools/windows/jrunscript.html.

· jjs: Führt immer JavaScript-Programme mit Nashorn aus. Siehe auch http://download.java.net/jdk8/docs/technotes/tools/windows/jjs.html für weitere Optionen. Interessant für JavaFX-Anwendungen ist der Schalter –fx, um verkürzte JavaFX-Anwendungen in JavaScript schreiben zu können.

Während also jrunscript generisch für alle Skriptsprachen sind, ist jjs exklusiv für Nashorn und bietet Angabe eines Log-Level, die Möglichkeit den abstrakten Syntaxbaum anzuzeigen, und vieles mehr.

Hinweis: Ohne Argumente geben beide in einen interaktiven Modus:

$ jjs

jjs> print(„Hallo Nashhorn“)

Hallo Nashhorn

jjs> 12+3

15

jjs>

java.lang.reflect.Parameter in Java 8

Ein Parameter repräsentiert einen Parameter einer Methode oder eines Konstruktors. Zu den neuen Methoden zählen:

final class java.lang.reflect.Parameter

implements AnnotatedElement

§ String getName()

§ int getModifiers()

§ boolean isNamePresent()

§ boolean isImplicit()

§ boolean isSynthetic()

§ boolean isVarArgs()

§ Type getParameterizedType()

§ Executable getDeclaringExecutable()

§ Class<?> getType()

§ AnnotatedType getAnnotatedType()

Die in Java 8 eingeführte finale Klasse Parameter implementiert AnnotatedElement, da seit Java 8 auch Parametervariablen annotiert sein können; die Methoden sind in der oberen Aufzählung nicht noch einmal aufgezählt.

Um an einen Parameter zu gelangen nutzen wir getParameters() auf einem Executable, also konkret Constructor, Method.

abstract class java.lang.reflect.Executable<T>
extends AccessibleObject

implements Member, GenericDeclaration

§ public Parameter[] getParameters()

Falls etwas beim Erfragen schief geht, gibt es eine MalformedParametersException, eine ungeprüfte Ausnahme.

Beispiel:

Constructor<?>[] constructors = Point.class.getDeclaredConstructors();

for ( Constructor<?> constructor : constructors ) {

System.out.println( constructor );

for ( Parameter param : constructor.getParameters() )

System.out.printf( “ %s %s%n“, param.getType(), param.getName() );

}

Mit der Ausgabe (an der abzusehen ist, dass die Parameternamen für die JVM nicht bekannt sind):

public java.awt.Point(int,int)

int arg0

int arg1

public java.awt.Point(java.awt.Point)

class java.awt.Point arg0

public java.awt.Point()

Ausgaben mit Calendar-Methoden getDisplayName(…)

Die Calendar-Klasse liefert mit getDisplayName(int field, int style, Locale locale) einen String für einen Feldwert zurück. Der style ist eine Konstante und deklariert sind folgende Konstanten: SHORT_FORMAT (SHORT), SHORT_STANDALONE, LONG_FORMAT (LONG), LONG_STANDALONE, NARROW_FORMAT, NARROW_STANDALONE, einige neu seit Java 8. Eine weiter Methoden getDisplayNames(int field, int style, Locale locale) liefert ein Map<String,Integer> mit allen String-Repräsentationen für ein Feld:

Beispiel

Calendar cal = Calendar.getInstance();

System.out.println( cal.getDisplayName( Calendar.MONTH, Calendar.LONG_FORMAT, Locale.GERMANY ) ); // Februar

System.out.println( cal.getDisplayNames( Calendar.MONTH, Calendar.LONG_FORMAT, Locale.GERMANY ) ); // {Oktober=9, Mai=4, …

SplittableRandom in Java 8

Die Klasse SplittableRandom aus dem java.util-Paket ist neu in Java 8 und hat die Aufgabe Folgen guter Zufallszahlen zu liefern. (Auch wenn die Klasse SplittableRandom heißt, hat sie mit einem java.util.Spliterator nichts zu tun.) Während bei Random eher die einzelne Zufallszahl im Mittelpunkt steht, rückt SplittableRandom Folgen von Zufallszahlen in den Mittelpunkt, die insbesondere den Dieharder-Test[1] bestehen.

Die Methoden von SplittableRandom drehen sich daher auch um Ströme von Zufallszahlen, die als IntStream, LongStream und DoubleStream geliefert werden. Zudem gibt es auch die auf Random bekannten nextXXX()-Methoden und eine Methode split(), die ein neues SplittableRandom liefert, sodass zwei parallele Threads weiterhin unabhängig gute Zufallszahlen bekommen.


[1] http://www.phy.duke.edu/~rgb/General/dieharder.php

Wunschzeit und wo die Inseln für Java 8 stehen

Jetzt wo Java 8 auf der Zielgeraden ist, und ich mit keinen API-Änderungen mehr rechnen muss, gehen auch die beiden Insel in die Endphase. Die Neuerungen sind ja im Grunde sehr große Bausteine wie Lambda-Ausdrücke und Stream-API und dann duzend verteilte Änderungen.

Nervig für mich als Autor war die Dynamik, die die API noch in der letzten Phase entwickelte; besonders trat das bei der Stream-API auf. Doch komplett fertig sind nun die großen Kapitel über

  • Lambda-Ausdrücke (ca. 30 Word-Seiten)
  • java.util.function und funktionale Programmierung (ca. 20 Word-Seiten)
  • Stream-API (ca. 30 Word-Seiten)

Alle kleineren Sprachänderungen wie Default-Methoden und statische Schnittstellenmethoden sind ebenfalls komplett fertig und verteilt.

Fast alle kleineren API-Änderungen sind ebenfalls eingeflossen und im Grunde sind damit die beiden neuen Bände fertig. Was noch fehlt — und daran arbeite ich gerade als letztes — ist die neue Date-Time-API. Große Freude bereitet mir das nicht … Vielleicht baue ich das später in der nächsten Auflage weiter aus, wenn der andere Datums-Teil (Date, Calender, …) komprimiert wird. Ebenfalls von meiner Lust abhängig wird sein, wie weit ich JFC (Swing, Java 2D) kürze und die GUI-Themen nach JavaFX bringe.

Die Insel Band 2 greift nicht alle Bibliotheken in hundertprozentiger Tiefe auf, da es Bereiche gibt, die schon sehr speziell sind, und dafür fehlt auch der Platz. Vermutlich werden ich zu folgenden (extrem spannenden Themen) nicht mehr als einen Satz bringen:

  • Neuer Typ java.util.concurrent.locks.StampedLock
  • Neuer Typ java.util.concurrent.CountedCompleter
  • Neuer Typ java.util.concurrent.ConcurrentHashMap.KeySetView
  • Neuer Typ java.util.concurrent.CompletionStage
  • Neuer Typ java.util.concurrent.CompletionException
  • Neuer Typ java.util.concurrent.CompletableFuture
  • Neuer Typ java.util.concurrent.CompletableFuture.AsynchronousCompletionTask
  • Änderungen am ForkJoinPool

Bis auf ein paar weiteren Kleinigkeiten (Implementierung vom eigenen Spliterator, Reflection-Kram, Locale-Update, …) ist sonst die Abdeckung der neuen Themen bei 100%.

Kummer macht mir die Java 8 Unterstützung der Eclipse IDE, da es bisher keine Eclipse-Version mit integriertem Java 8 Compiler gibt, nur über ein Update. Luna soll im Juni fertig sein, das wäre eigentlich zu spät. Bei NetBeans haben wir das Problem natürlich nicht.

Haben meine Leser noch spezielle Wünsche an die nächste Auflage?

LongAccumulator und DoubleAccumulator

Die beiden XXXAdder-Klassen haben eine ganz spezielle Aufgabe und das ist Werte zu addieren und aufzusummieren. Allerdings gibt es noch viele weitere Aufgaben, die ähnlich wie die Summation auf ein Endergebnis gebracht werden. Für diesen Anwendungsfall deklariert das Paket java.util.concurrent.atomic weiterhin LongAccumulator und DoubleAccumulator. Im Konstruktor nehmen die Klassen einen XXXBinaryOperator an und die Identität, die beim binären Operator auf nur einem Ergebnis genau das Ergebnis gibt.

· LongAccumulator(LongBinaryOperator accumulatorFunction, long identity)

· DoubleAccumulator(DoubleBinaryOperator accumulatorFunction, double identity)

Die Methoden heißen dann accumulate(long x) bzw. accumulate(double x) und get() verdichtet das Ergebnis zum Schluss. Die Addition der Klassen LongAdder und DoubleAdder lässt sich dann alternativ ausdrücken durch new XXXXAccumulator((x, y) -> x + y, 0).

java.util.concurrent.atomic.LongAdder und DoubleAdder in Java 8

Die AtomicXXX-Klassen sind gut, wenn es nicht zu viele parallel Threads gibt, die gleichzeitig die AtomicXXX-Exemplare verändern. Der Grund ist einfach: jeder Thread warten muss, bis ein anderer Thread die Veränderung am AtomicXXX vorgenommen hat. Stehen also 100 Threads in der Schlage den AtomicXXX zu verändern, werden sie erst nacheinander abgearbeitet – das geht zwar an schnell, dennoch führt die sequenzielle Verarbeitung zu Wartesituationen.

Wenn es wirkliche viel nebenläufige Threads gibt, sind die AtomicXXX-Klassen nicht optimal und Java biete ab Java 8 zwei neue Klassen LongAdder und DoubleAdder. Ein XXXAdder sieht nach außen wie ein long/double aus (die Klassen erweitern auch Number), doch intern sind sie vielmehr eine Liste von Werten, auf die dann unterschiedliche Thread zugreifen können, ohne zu warten. Um sich das vorzustellen zu können ein Beispiel: Nehmen wir an, mehrere Threads teilen sich einen LongAdder. Ruft ein Thread add(1) auf, so führt das intern zu einem Element in einer Liste[1]. Kommt gleichzeitig add(2) am LongAdder an, muss der Thread nicht auf das Ende vom ersten add(…) warten, sondern fügt einen neuen Knoten an. Kommt später ein dritter und vierter Thread über den Weg und führt add(3) und add(4) aus, können diese ohne Warten an den ersten und zweiten existieren Knoten gehen und die Werte addieren, in den beiden internen Knoten stehen also 4 und 6. Es sind also nur so viele Knoten intern nötig, wie wirklich parallele Threads auftauchen. Eine Summation am Ende mit sum() läuft dann über die internen Knoten und summiert sie auf zu 10, was 1+2+3+4 ist.


[1] Genau genommen beim ersten Element noch in einer Variablen, die Liste beginnt erst beim zweiten Element, also beim zweiten parallelen Thread.

map(…) Methoden von Optional für funktionale Programmierung

Java 8 bekommt eine Optional Klasse und die beiden XXXmap(…)-Methoden sind besonders interessant. Sie ermöglichen einen ganz neuen Programmierstil. Warum soll ein Beispiel zeigen.

Der folgende Zweizeiler gibt auf meinem System „MICROSOFT KERNELDEBUGGER-NETZWERKADAPTER“ aus:

String s = NetworkInterface.getByIndex( 2 ).getDisplayName().toUpperCase();

System.out.println( s );

Allerdings ist der Programmcode alles andere als gut, denn NetworkInterface.getByIndex(int) kann null zurückgeben und getDisplayName() auch. Um ohne eine NullPointerException um die Klippen zu schiffen müssen wir schreiben:

NetworkInterface networkInterface = NetworkInterface.getByIndex( 2 );

if ( networkInterface != null ) {

String displayName = networkInterface.getDisplayName();

if ( displayName != null )

System.out.println( displayName.toUpperCase() );

}

Von der Eleganz des Zweizeilers ist nicht mehr viel geblieben. Integrieren wir Optional (was ja eigentlich ein toller Rückgabetyp für getByIndex() und getDisplayName():

Optional<NetworkInterface> networkInterface = Optional.ofNullable( NetworkInterface.getByIndex( 2 ) );

if ( networkInterface.isPresent() ) {

Optional<String> name = Optional.ofNullable( networkInterface.get().getDisplayName() );

if ( name.isPresent() )

System.out.println( name.get().toUpperCase() );

}

Mit Optional wird es nicht sofort besser, doch statt if können wir ein Lambda-Ausdruck einsetzen und bei ifPresent(…) einsetzen:

Optional<NetworkInterface> networkInterface = Optional.ofNullable( NetworkInterface.getByIndex( 2 ) );

networkInterface.ifPresent( ni -> {

Optional<String> displayName = Optional.ofNullable( ni.getDisplayName() );

displayName.ifPresent( name -> {

System.out.println( name.get().toUpperCase() );

} );

} );

Wenn wir nun die lokale Variablen entfernen, kommen wir aus bei:

Optional.ofNullable( NetworkInterface.getByIndex( 2 ) ).ifPresent( ni -> {

Optional.ofNullable( ni.getDisplayName() ).ifPresent( name -> {

System.out.println( name.get().toUpperCase() );

} );

} );

Von der Struktur ist das mit der if-Afrage identisch und über die Einrückungen auch zu erkennen. Fallunterscheidungen mit Optional und ifPresent(…) umzuschreiben bringt also keinen Vorteil.

In Fallunterscheidungen zu denken hilft hier nicht weiter. Was wir uns bei NetworkInterface.getByIndex( 2 ).getDisplayName().toUpperCase() vor Augen halten müssen ist eine Kette von Abbildungen. NetworkInterface.getByIndex(int) bildet auf NetworkInterface ab, getDisplayName() von NetworkInterface bildet auf String ab, und toUpperCase()bildet von einem String auf einen anderen String ab. Wir verketten also drei Abbildungen und müssten ausdrücken können: Wenn eine Abbildung fehlschlägt, dann höre mit der Abbildung auf. Und genau hier kommt Optional und map(…) ins Spiel. In Code:

Optional<String> s = Optional.ofNullable( NetworkInterface.getByIndex( 2 ) )

. map( ni -> ni.getDisplayName() )

. map( name -> name.toUpperCase() );

s.ifPresent( System.out::println );

Die Klasse Optional hilft uns bei zwei Dingen: Erstes wird map(…) beim Empfangen einer null-Referenz auf ein Optional.empty() abbilden. Und zweitens ist das Verketten von leeren Optionals kein Problem, es passiert einfach nichts – Optional.empty().map(…) führt nichts aus und die Rückgabe ist einfach nur ein leeres Optional.

Umgeschrieben mit Methoden-Referenzen und weiter verkürzt ist das Code sehr gut lesbar.

Optional.ofNullable( NetworkInterface.getByIndex( 2 ) )

. map( NetworkInterface::getDisplayName )

. map( String::toUpperCase )

.ifPresent( System.out::println );

Die Logik kommt ohne externe Fallunterscheidungen aus und arbeitet nur mit optionalen Abbildungen. Das ist ein schönes Beispiel für funktionale Programmierung.

Compact-Profile in Java 8

Nicht jedes Java-Programm braucht den vollen Satz von 4000 Typen, sondern oftmals reicht eine kleine Teilmenge. Eine Teilmenge der Java-Bibliothek wiederum ermöglicht es, kleinere Laufzeitumgebung zu bauen, was es erlaubt, Java auch für Geräte mit weniger Ressourcen einzusetzen. Eine kleine Java-Version für Embedded-Systeme braucht kein CORBA, AWT oder JavaFX und kann so viel kleiner als das Standard-JRE sein.

In Java 8[1] wurde die Bibliothek in 4 Gruppen eingeteilt, auf der einen Seite stehen drei kompakte aufsteigende Teilmengen der Standardbibliothek, genannt Compact-Profile, und auf der anderen Seite das vollständige System. Das Profil contact1 ist das kleinste, compact2 enthält contact1, contact3 enthält compact2 und das Gesamt-JRE compact3.

Welche Typen zu welchem Profil gehören dokumentiert die Java-API auf der Startseite sowie gibt jeder Typ in der Javadoc sein Profil an; die grobe Einteilung ist:

Profil

Größe

Pakete

compact1

10 MiB

java.io, java.math, java.net, java.nio, java.security, java.time, java.util (inklusive Stream-API), javax.crypto, javax.script, javax.security

compact2

17 MiB

java.sql, java.rmi, javax.rmi, javax.transaction, javax.xml, org.xml, org.w3c

compact3

24 MiB

java.lang.instrument, java.lang.management, java.util.prefs, java.lang.model, javax.management, javax.naming, javax.sql.rowset, javax.tools, javax.xml.crypto, org.ieft.jgss, Kerberos, SASL

JRE/JDK

140 MiB

Alles weitere: java.beans, AWT, Swing, JavaFX, CORBA, java.awt.print, Sound, SOAP, Web-Service, …

Inhalt der verschiedenen Profile mit Speicherbedarf[2]

Weiterhin ist die Anzahl verschiedener Provider minimiert worden, es gilt zum Beispiel nur Englisch als verpflichtende Sprache.

Werkzeug-Unterstützung für Profile

Die Werkzeuge javac, javadoc und jdeps aus dem JDK sind für die Profile aktualisiert worden, etwa dass sie prüfen können, ob ein Typ/Eigenschaft zum Profil gehört oder nicht. Der Schalter –profile gibt dabei das gewünschte Profil an.

Beispiel

Versuche die Klasse T mit der Deklaration class T extends java.awt.Point {} mit dem Profile compact3 zu übersetzen:

$ javac -profile compact3 T.java

T.java:2: error: Point is not available in profile ‚compact3‘

class T extends java.awt.Point { }

^

1 error

Obwohl Point eine nützliche Klasse ist, und keine plattformspezifischen Eigenschaften hat, ist das gesamte Paket java.awt gesperrt und kein Teil vom Compact-Profile.


[1] Beschrieben erstmalig unter http://openjdk.java.net/jeps/161

[2] Zahlen von JavaOne Konferenz 2013.

Annotation jdk.Exported

Im Endeffekt haben Entwickler es zu tun mit

1. der offiziellen Java-API,

2. der API aus JSR-Erweiterungen, wie der Java Enterprise API und

3. nicht-offiziellen Bibliotheken, wie quelloffenen Lösungen etwa zum Zugriff auf PDF-Dateien oder Bankautomaten.

Allerdings gibt es noch weitere Typen, die nicht im java bzw. javax-Paket liegen, die von jeder Java SE-Implementierung unterstützt werden müssen. Dazu zählen

· HTTP Server API (com.sun.net.httpserver)

· Java Debug Interface (com.sun.jdi)

· Attach API (com.sun.tools.attach)

· SCTP API (com.sun.nio.sctp)

· Management Extensions (com.sun.management)

· JConsole Plugin API (com.sun.tools.jconsole)

und ein paar Typen aus dem Sicherheits-Paket, com.sun.security.auth und com.sun.security.jgss.

Um zugängliche öffentliche bzw. protected Typen und Eigenschaften zu markieren, tragen sie eine spezielle Annotation @jdk.Exported – am dem Paket jdk lässt sich schon ablesen, dass die Annotation selbst schon sehr speziell ist, und auch nicht zur Standard-Bibliothek gehört. Alternative Java SE-Implementierungen müssen diese Typen also bereitstellen, da jedoch Oracle mit dem JDK (beziehungsweise das OpenJDK) so präsent sind, ist diese Markierung eher etwas für die Plattformentwickler, weniger für die normalen Entwickler.

Kollisionen und Hash-Funktionen

Die Wahl der richtigen Hash-Funktion ist wichtig für die Performance. Denn eine »dumme« Hash-Funktion, die beispielsweise alle Schlüssel nur auf einen konstanten Wert abbildet, erreicht keine Verteilung, sondern lediglich eine lange Liste von Schlüssel-Werte-Paaren; diese Anhäufung an einer Stelle nennt sich Clustering. Doch auch bei der besten Verteilung über N Buckets ist nach dem Einfügen des Elements N + 1 irgendwo eine Liste mit mindestens zwei Elementen aufgebaut, daher vergrößert die Bibliothek standardmäßig das Feld. Ist aber die Hash-Funktion aber so schlecht, dass alles auf das gleiche Bucket abgebildet wird, hilft dieses Re-Hashing auch nicht.

Je länger die Datenstruktur der miteinander kollidierenden Einträge wird, desto langsamer wird der Zugriff der auf Hashing basierenden Datenstruktur insgesamt. Java basiert beim Hashing einzig auf der hashCode()-Methode, und es liegt in unserer Hand sie gut zu implementieren. Die Klasse String hat eine relativ einfache (dafür schnelle) Implementierung von hashCode(), die in der Vergangenheit zu Denial of Service Attacken führte gerade weil es zu Kollisionen kam.[1]


[1] In Java 7 wurde dafür in String eine zusätzliche Hash-Methode realisiert, doch in Java 8 diese Implementierung wieder entfernt und die Implementierung für das Hashing insgesamt verändert, zumindest für die aktuellen Klassen, das ältere java.util.Hashtable blieb unverändert.

Schnittstelle Map.Entry und Updates in Java 8

Während keySet() nur die eindeutigen Schlüssel in einer Menge liefert und die assoziierten Elemente in einem zweiten Schritt geholt werden müssten, gibt entrySet() ein Set von Elementen typisiert mit Map.Entry zurück. Entry ist eine innere Schnittstelle von Map, die eine API zum Zugriff auf Schlüssel-Werte-Paare deklariert. Die wichtigen Operationen dieser Schnittstelle sind getKey(), getValue() und setValue(), wobei die letzte Methode optional ist, aber etwa von HashMap angeboten wird. Neben diesen Methoden überschreibt Entry auch hashCode() und equals(…).

Beispiel: Laufe die Elemente HashMap als Menge von Map.Entry-Objekten ab:

for ( Map.Entry<String, String> e : h.entrySet() )
System.out.println( e.getKey() + „=“ + e.getValue() );

Map.Entry ist eher ein Interna und die Objekte dienen nicht der langfristen Speicherung. Ein entrySet() ist eine Momentaufnahme und das Ergebnis sollte nicht referenziert werden, denn ändert sich der darunterliegende Assoziativspeicher, ändern sich auch die Entry-Objekt und das Set<Map.Entry> als ganzes ist vielleicht nicht mehr gültig. Entry-Objekt sind nur gültig im Moment der Iteration, was den Nutzen eingeschränkt. Daher ist die Rückgabe von entrySet() mit Set<Map.Entry<K,V>> auch relativ unspezifisch, um was für eine Art von Set es sich genau handelt; ob HashSet oder vielleicht NavigableSet spielt keine Rolle.

Auch wenn die Map.Entry-Objekte nicht für die Speicherung gedacht sind, können Sie in Java 8 in einem Strom von Daten verarbeitet und in einer zustandsbehafteten Operation sortiert werden. Der Bonus der Entry-Objekte im Strom ist einfach, dass es von Vorteil ist, Schüssel und Wert in einem Objekte gekapselt zu sehen. Aber was ist, wenn jetzt der Strom sortiert werden soll, etwa nach dem Schlüssel, oder dem Wert? Hier kommen neue Methoden von Java 8 ins Spiel, die den nötigen Comparator liefern.

Beispiel: Erfrage eine Menge von Entry-Objekten und sortiere sie nach dem assoziierten Wert:

map.entrySet()

.stream()

.sorted( Map.Entry.<String, String>comparingByValue() )

.forEach( System.out::println );

 

static interface Map.Entry<K,V>

§ static <K extends Comparable<? super K>,V> Comparator<Map.Entry<K,V>> comparingByKey()

§ static <K,V extends Comparable<? super V>> Comparator<Map.Entry<K,V>> comparingByValue()

§ static <K,V> Comparator<Map.Entry<K,V>> comparingByKey(Comparator<? super K> cmp)

§ static <K,V> Comparator<Map.Entry<K,V>> comparingByValue(Comparator<? super V> cmp)