Welcher Kalendertyp ein konkreter Kalender repräsentiert, ermittelt die Calendar-Objektmethode getCalendarType(); die Rückgabe ist ein String und lautet bei dem typischen Gregorianischen Kalender „gregory“, könnte aber auch „buddhist“ oder „japanese“ heißen. Welche Kalendertypen Java unterstützt, liefert die statische Methode Calendar.getAvailableCalendarTypes() als Set<String>. Im Moment sind es genau die drei genannten.
Autor: Christian Ullenboom
Würde geloggt werden?
Das Logging-Framwork versucht so schnell wie möglich zu entscheiden, ob eine Nachricht bei einem eingestellten Log-Levels geloggt werden soll oder nicht. Ist die Stufe in der Produktion zum Beispiel auf WARNING, sind INFO-Meldungen zu ignorieren. Problematisch aus Performance-Sicht sind zum Beispiel aufwändig aufgebaute Log-Nachrichten, die dann sowieso nicht geloggt werden. Der Plus-Operator bei Strings gehört nicht zu den beachtlichen Zeitfressern, doch ein
log.info( "Open file: " + filename );
führt zur Laufzeit immer zu einer String-Konkatenation, egal, ob die erzeugte Nachricht später geloggt wird oder nicht.
JUL bietet zur Umgehung des Problems zwei Lösungen. Als erstes bietet die Logger-Klasse eine Testmethode boolean isLoggable(Level level), über die ein schneller Test durchgeführt werden kann:
if ( log.isLoggable(Level.INFO) )
log.info( "Open file: " + filename );
Natürlich kann info(…) nicht wissen, dass es auf jeden Fall loggen soll, daher findet der Test noch einmal statt. Eine allgemeine Überprüfung für alle Logging-Ausgaben bietet sich daher nicht an, sondern nur dann, wenn eine aufwändige Operation im Logging-Fall ausgeführt werden soll.
Die zweite Möglichkeit ist neu in Java 8. Sie nutzt Objekte vom Typ Supplier, die eine Implementierung enthalten, also etwa die Konkatenation. Im Prinzip hätte Oracle das auch schon vor Java 8 integrieren können, doch erst Lambda-Ausdrücke führen zu einer kompakten Schreibweise. Das sieht zum Beispiel so aus:
log.info( () -> { "Open file: " + filename } );
Die Default-Falle
Insbesondere bei Kodierungen und zeitgebundenen Eigenschaften müssen sich Entwickler zu jeder Zeit bewusst sein, welche Einstellung gerade verwendet wird. Neulinge greifen oft auf Default-Einstellungen zurück und String-Parsing mit Scanner und Ausgaben mit Formatter funktionieren in der Entwicklung, doch spätestens wenn die Software halb um den Globus wandert, läuft nichts mehr, weil die Default-Werte plötzlich anders sind.
Wenn Konstruktoren oder Methoden es nicht explizit verlangen, greift das JDK auf Standardwerte unter anderen für
· Zeilenendezeichen
· Zeichenkodierung
· Sprache (Locale)
· Zeitzone (TimeZone)
zurück.
Ein Beispiel: Der Konstruktor Scanner(File) öffnet eine Datei zum Lesen und konvertiert die Bytes in Unicodes mit einem Konverter, den die Default-Zeichenkodierung bestimmt. Wird aus dem Scanner eine Zahl gelesen, etwa mit nextDouble(), greift die voreingestellte Default-Locale, die dem Scanner sagt, ob Dezimalzahlen mit „,“ oder „.“ interpretiert werden muss. Verarbeitet ein Java-Programm die gleiche Textdatei einmal in den USA und Deutschland, ist das Ergebnis unterschiedlich und in der Regel sollte das nicht so sein.
Default-Werte sind eine gute Sache, allerdings sollten Entwickler sich bewusst sein, an welchen Stellen das JDK auf sie zurückgreift, um keine Überraschungen zu erleben. Es lohnt sich, immer konkrete Belegungen anzugeben, auch wenn als Argument zum Beispiel Locale.getDefault() steht. Das dokumentiert das gewollte Nutzen der Default-Werte.
Hashwerte von Wrapper-Objekten mit neuen Methoden ab Java 8
Der Hashwert eines Objekts bildet den Zustand auf eine kompakte Ganzzahl ab. Haben zwei Objekte ungleiche Hashwerte, so müssen auch die Objekte ungleich sein (mindest, wenn die Berechnung korrekt ist). Zur Bestimmung des Hashwertes deklariert jede Klasse über die Oberklasse java.lang.Object die Methode int hashCode(). Alle Wrapper-Klassen überschreiben diese Methode. Zudem kommen in Java 8 statische Methoden hinzu, sodass sich leicht der Hashwert berechnen lässt, ohne extra ein Wrapper-Objekte zu bilden.
Klasse |
Klassenmethode |
Objektmethode |
Boolean |
static int hashCode(boolean value) |
int hashCode() |
Byte |
static int hashCode(byte value) |
int hashCode() |
Short |
static int hashCode(short value) |
int hashCode() |
Integer |
static int hashCode(int value) |
int hashCode() |
Long |
static int hashCode(long value) |
int hashCode() |
Float |
static int hashCode(float value) |
int hashCode() |
Double |
static int hashCode(double value) |
int hashCode() |
Character |
static int hashCode(char value) |
int hashCode() |
Abbildung 4 Statische Mehtoden hashCode(…) und Objektmethoden im Vergleich
Um den Hashwert eines ganzen Objekts zu errechnen, müssen folglich alle einzelnen Hashwerte berechnet werden und diese dann zu einer Ganzzahl verknüpft werden. Schematisch sieht das so aus:
int h1 = WrapperClass.hashCode( value1 );
int h2 = WrapperClass.hashCode( value2 );
int h3 = WrapperClass.hashCode( value3 );
…
Eclipse nutzt zur Verknüpfung der Hashwerte folgendes Muster, welches eine guter Ausgangspunkt ist:
int result = h1;
result = 31 * result + h2;
result = 31 * result + h3;
…
LinkedHashMap und LRU-Implementierungen
Da die Reihenfolge der eingefügten Elemente bei einem Assoziatspeicher verloren geht, gibt es mit LinkedHashMap eine Mischung, also ein schneller Assoziativspeicher mit gleichzeitiger Speicherung der Reihenfolge der Objekte. Die Bauart vom Klassename LinkedHashMap macht schon deutlich, dass es eine Map ist, und die Reihenfolge der Objekte liefert ein Iterator; es gibt keine listenähnliche Schnittstelle mit get(int). LinkedHashMap ist für Assoziativspeicher das, was LinkedHashSet für HashSet ist.
Im Gegensatz zur normalen HashMap ruft LinkedHashMap immer genau dann die besondere Methode boolean removeEldestEntry(Map.Entry<K,V> eldest) auf, wenn intern ein Element der Sammlung hinzugenommen wird. Die Standardimplementierung dieser Methode liefert immer false, was bedeutet, dass das älteste Element nicht gelöscht werden soll, wen ein neues hinzukommt. Doch bietet das JDK die Methode aus Absicht protected an, denn sie kann von uns überschrieben werden, um eine Datenstruktur aufzubauen, die eine maximal Anzahl Elemente hat. So sieht das aus:
package com.tutego.insel.util.map; import java.util.*; public class LRUMap<K,V> extends LinkedHashMap<K, V> { private final int capacity; public LRUMap( int capacity ) { super( capacity, 0.75f, true ); this.capacity = capacity; } @Override protected boolean removeEldestEntry( Map.Entry<K, V> eldest ) { return size() > capacity; } }
LinkedHashSet bietet eine vergleichbare Methode removeEldestEntry(…) nicht. Wer dies benötigt, muss eine eigene Mengenklasse auf der Basis von LinkedHashMap realisieren.
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
109 Enhance Core Libraries with Lambda
118 Access to Parameter Names at Runtime
119 javax.lang.model Implementation Backed by Core Reflection
126 Lambda Expressions & Virtual Extension Methods
135 Base64 Encoding & Decoding
138 Autoconf-Based Build System
139 Enhance javac to Improve Build Speed
142 Reduce Cache Contention on Specified Fields
147 Reduce Class Metadata Footprint
149 Reduce Core-Library Memory Usage
160 Lambda-Form Representation for Method Handles
162 Prepare for Modularization
164 Leverage CPU Instructions for AES Cryptography
166 Overhaul JKS-JCEKS-PKCS12 Keystores
173 Retire Some Rarely-Used GC Combinations
So wie ich das überblicke, sind die meisten Punkte realisiert.
Funktionale Schnittstellen mit @FunctionalInterface dokumentierten
In Java 8 kommt dafür eine Markierung hinzu:
package java.lang; import java.lang.annotation.*;
@Documented @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) public @interface FunctionalInterface {}
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 RTF–Lib jrtf hat noch ein paar Issues offen
Wohl doch keine neue HTTP Async API in Java 8
http://mail.openjdk.java.net/pipermail/jdk8-dev/2012-December/001804.html
Macht nix, gibt genug Alternativen.
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.