Warum Properties extends Hashtable<Object, Object> und nicht Hashtable<String, String>?

Von der Intuition her ist von der Vererbung class Properties extends Hashtable<String, String> naheliegend, doch hat Sun nicht gewollt. Der Grund hat gar nicht so sehr mit Properties selbst zu tun, sondern mit der Basisklasse Hashtable und gewählten Umsetzung der Generics und der gewünschten Kompatibilität. Von Java 1.0 bis Java 1.4 hat die Klasse Hashtable eine Methode put(Object, Object). Ab Java 5 wird Object durch generische Typparameter ersetzt, sodass es in Hashtable heißt: put(K, V). Wenn nun Properties extends Hashtable<String,String> ist, dann wird die Hinzufügemethode zu put(String, Sting). Das wäre nicht mehr kompatibel zu put(Object, Object) vor Java 5. Also haben die Sun-Entwickler Properties extends Hashtable<Object, Object> geschrieben, auch wenn die put(…)-Methode überhaupt nicht verwendet werden soll, sondern setProperty(…). Da Generics nur ein „Trick“ des Compilers sind, lässt sich über den Raw-Typ tricksen:

Properties props = new Properties();

@SuppressWarnings( {"rawtypes", "unchecked"} )

Map<String, String> map = (Map) props;

jdk-11-ea+2 freigegeben

Die größte Änderung ist, was angekündigt wurde:

These builds include JEP 320 (Remove the Java EE and CORBA Modules) [1],
so they're significantly smaller (nine fewer modules, 22 fewer megabytes
on Linux/x64).

- Mark

Weitere Infos und Download unter http://jdk.java.net/11/. Folgende Änderungen gibt es:

Changeset Bug ID Synopsis
d9fce53461a1 8197812 (ref) Data race in Finalizer
c38163717870 8198227 Fix COMPARE_BUILD after forest consolidation
329428e095b6 8198306 Add post custom extension hooks to two launchers
67cdc215ed70 8198228 Spec clarification: j.u.Locale.getDisplayName()
edaa989b4e67 8058965 Remove IPv6 support from TwoStacksPlainSocketImpl [win]
01237b276b8b 8198318 Make build comparisons clean again
7f9c3cd11e97 8170120 jimage throws IOException when the given file is not a jimage file
0a185d6fafa1 8198379 tools/jimage/JImageListTest.java failing
b417304c811b 8198380 tools/jimage/JImageExtractTest.java failing
42cec55157fa 8198417 Exclude tools/jimage/JImageExtractTest.java and tools/jimage/JImageListTest.java on Windows
37beaca49e63 8194922 jlink –exclude-resources should never exclude module-info.class
18debf414948 8198425 make/Main.gmk Add extra extension/override points to the make file
c7e84c0a51c3 8198328 Create devkit for Solaris with developer studio 12.6 and Solaris11.3
916690b5edc9 8194892 add compiler support for local-variable syntax for lambda parameters
576e024f10b6 8198418 Invoke LambdaMetafactory::metafactory exactly from the BootstrapMethodInvoker
906025796009 8198441 Replace native Runtime::runFinalization0 method with shared secrets
b75c9e2e3b1f 8198450 Make jdk.internal.vm.compiler/module-info.java.extra reproducable
1913e7fc6be9 8198301 jdk11+1 was built as ‚fcs‘ instead of ‚ea‘
b2bba53b079d 8198303 jdk11+1 was build with incorrect GA date as 2018-03-20
02404e27d356 8198479 JDK build is broken by 8194892
847a988152b8 8194154 System property user.dir should not be changed
cc30928a834e 8198385 Remove property sun.locale.formatasdefault
28d8fc8cd3cd 8190904 Incorrect currency instance returned by java.util.Currency.getInstance()
b1a5b4ad7427 8198523 Refactor BootstrapMethodInvoker to further avoid runtime type checks
b25eb74ec283 8197439 Crash with -XDfind=lambda for anonymous class in anonymous class.
9e3f2ec326ba 8198502 Exception at runtime due to lambda analyzer reattributes live AST
20358c9f72c1 8198563 Test langtools/tools/javac/analyzer/AnonymousInAnonymous.java failing after JDK-8198502
03ae177c26b0 8198512 compiler support for local-variable syntax for lambda parameters

Release Notes Since build 1  Integrations

core-libs/java.util.concurrent

 ThreadPoolExecutor should not specify a dependency on finalizationPrevious versions of ThreadPoolExecutor had a finalize method that shut down the thread pool, but in this version the finalize method does nothing. This should have no visible effect unless a subclass explicitly invokes the finalize method and relies on the executor being shutdown.

security-libs/java.security

 jarsigner should print when a timestamp will expireThe jarsigner tool now shows more information about the lifetime of a timestamped JAR. New warning and error messages are displayed when a timestamp has expired or is expiring within one year.

Callable oder Runnable mit FutureTask ummanteln

Die Klasse java.util.concurrent.FutureTask implementiert Future, Runnable und RunnableFuture und wird intern von der Java-Bibliothek verwendet, wenn wir mit submit(…) etwas beim ExecutorService absetzen. Auch wir können den Typ direkt als Wrapper um ein Callable oder Runnable nutzen, denn es gibt praktische Callback-Methoden, die wir überschreiben können, etwa done(), wenn eine Berechung fertig ist.

Dazu ein Beispiel: Ein Callable liefert den Namen des Benutzers. Ein FutureTask legt sich um dieses Callable und bekommt mit, wann das Callable fertig ist und modifiziert dann den Benutzernamen und gibt weiterhin eine Meldung aus.

Callable<String> username = () -> System.getProperty( "user.name" );

FutureTask<String> wrappedUsername = new FutureTask<>( username ) {

  @Override

  protected void done() {

    try {

      System.out.printf( "done: isDone=%s, isCancelled=%s%n", isDone(), isCancelled() );

      System.out.println( "done: get=" + get() );

    }

    catch ( InterruptedException | ExecutionException e ) { /* Ignore */ }

  }

  @Override

  protected void set( String v ) {

    System.out.println( "set: " + v );

    super.set( v.toUpperCase() );

  }

};

ExecutorService scheduler = Executors.newCachedThreadPool();

scheduler.submit( wrappedUsername );

System.out.println( "main: " + wrappedUsername.get() );

scheduler.shutdown();

Wichtig in der Nutzung ist, nicht die Rückgabe vom submit(…) auszuwerten, was wir normalerweise machen, sondern das übergebene FutureTask zu erfragen.

Die Ausgaben vom Programm sind oft ein wenig durcheinander:

set: Christian
done: isDone=true, isCancelled=false
done: get=CHRISTIAN
main: CHRISTIAN

Die Reihenfolge in den Aufrufen ist immer so: Der FutureTask stellt die Fertigstellung vom Callable fest und ruft set(…) auf. Anschließend done().

Tor auf mit CountDownLatch

Die Klasse CountDownLatch hilft Threads an einem gewissen Punkt zusammenzukommen und dann weiterzumachen. Das kann ein Thread sein, der auf das Fertigwerden von N anderen Threads wartet oder N Threads, die ein Signal zum Loslaufen bekommen. CountDownLatch ist mit einem Zähler assoziiert, der sich herunterzählen lässt. Auf der anderen Seite wartet die andere Partei darauf, dass der Zähler 0 wird, um fortzuführen. Mit diesem Wissen ergibt auch der Klassenname Sinn: „count down“ heißt auf deutsch „runterzählen“ und „latch“ ist das englische Wort für Falle, Riegel, Sperrklinke; nach dem erfolgreichen runterzählen entsperrt die Klinke.

Ein Beispiel. Wir wollen prüfen, ob Hosts im Internet erreichbar sind. Der Zähler vom CountDownLatch entspricht der Anzahl der zu überprüfenden Hosts. Für jeden Host starten wir einen Thread. Ist der Host erreichbar, wird CountDownLatch mit countDown() herunterzählt. Am Ende waren wir jedoch nicht mit await() ob wir bei 0 angekommen sind – Hosts können nicht erreichbar sein – sondern wir warten eine gewisse Zeit mit der überladenen Methode await(long timeout, TimeUnit unit). Ist der Zähler nach der Zeit nicht 0 gibt es keine Ausnahme, sondern wir erfragen den Zähler um herauszufinden, wie viele Hosts erreichbar waren.

List<String> hosts = List.of( "popel.nase", "tutego.de" );

CountDownLatch latch = new CountDownLatch( hosts.size() );



for ( String host : hosts ) {

  new Thread( () -> {

    try {

      if ( InetAddress.getByName( host ).isReachable( 900 /* ms */ ) )

        latch.countDown();

    }

    catch ( IOException e ) { /* ignore */ }

  } ).start();

}



try {

  if ( latch.await( 1, TimeUnit.SECONDS ) )

    System.out.println( "Alle Hosts erreicht" );



  System.out.printf( "%d von %d Hosts nicht erreicht", latch.getCount(), hosts.size() );

}

catch ( InterruptedException e ) {

  e.printStackTrace();

}

Der CountDownLatch hat nur eine Methode zum Herunterzählen, nicht zum wieder erhöhen. Damit ist klar, dass sich ein heruntergezähltes Objekt nicht mehr verwenden kann.

Inselupdate: switch hat ohne Unterbrechung Durchfall

Bisher haben wir in die letzte Zeile eine break-Anweisung gesetzt. Ohne ein break würden nach einer Übereinstimmung alle nachfolgenden Anweisungen ausgeführt. Sie laufen somit in einen neuen Abschnitt herein, bis ein break oder das Ende von switch erreicht ist. Da dies vergleichbar mit einem Spielzeug ist, bei dem Kugeln von oben nach unten durchfallen, nennt sich dieses auch Fall-through. Ein häufiger Programmierfehler ist, das break zu vergessen, und daher sollte ein beabsichtigter Fall-through immer als Kommentar angegeben werden.

Über dieses Durchfallen ist es möglich, bei unterschiedlichen Werten immer die gleiche Anweisung ausführen zu lassen. Das nutzt auch das nächste Beispiel, das einen kleinen Parser für einfache Datumswerte realisiert. Der Parser soll mit drei unterschiedlichen Datumsangeben umgehen können, je ein Beispiel:

  • „18 12“: Jahr in Kurzform, Monat
  • „2018 12“: Jahr, Monat
  • „12“: Nur Monat, es soll das aktuelle Jahr implizit gelten
public class SimpleYearMonthParser {

@SuppressWarnings( "resource" )

public static void main( String[] args ) {

String date = "17 12";

int month = 0, year = 0;

java.util.Scanner scanner = new java.util.Scanner( date );

switch ( date.length() ) {

case 5:  // YY MM

year = 2000;

// Fall-through

case 7: // YYYY MM

year += scanner.nextInt();

// Fall-through

case 2: // MM

month = scanner.nextInt();

if ( year == 0 )

year = java.time.Year.now().getValue();

break;

default :

System.err.println( "Falsches Format" );

}

System.out.println( "Monat=" + month + ", Jahr=" + year );

}

}

In dem Beispiel bestimmt eine case-Anweisung über die Länge, wie der Aufbau ist. Ist die Länge 5, so ist die Angabe des Jahres verkürzt und wir initialisieren das Jahr mit 2000, um im folgenden Schritt mit Hilfe vom Scanner das Jahr einzulesen. Zu diesem Schritt wären wir auch direkt gekommen, wenn die Länge der Eingabe 7 gewesen wäre, also das Jahr vierstellig gewesen wäre. Damit ist der Jahresanteil geklärt, es bleibt die Monate zu parsen. Kommen wir direkt über einen String der Länge 2, ist vorher kein Jahr gesetzt, wir bekommen über java.time.Year.now().getValue() das aktuelle Jahr, andernfalls überschreiben wir die Variable nicht.

Was sollte der Leser von diesem Beispiel mitnehmen? Eigentlich nur Kopfschütteln für eine schwer zu verstehende Lösung. Das Durchfallen ist eigentlich nur zur Zusammenfassung mehrerer case-Blöcke sinnvoll zu verwenden.

Sprachvergleich *

Obwohl ein fehlendes break zu lästigen Programmierfehlern führt, haben die Entwickler von Java dieses Verhalten vom syntaktischen Vorgänger C übernommen. Eine interessante andere Lösung wäre gewesen, das Verhalten genau umzudrehen und das Durchfallen explizit einzufordern, zum Beispiel mit einem Schlüsselwort. Dazu gibt es eine interessante Entwicklung: Java »erbt« diese Eigenschaft von C(++), die wiederum erbt sie von der Programmiersprache B. Einer der »Erfinder« von B ist Ken Thompson, der heute bei Google arbeitet und an der neuen Programmiersprache Go beteiligt ist. In Go müssen Entwickler ausdrücklich die fallthrough-Anweisung verwenden, wenn ein case-Block zum nächsten weiterleiten soll. Das gleiche gilt für die neue Programmiersprache Swift; auch hier gibt es die Anweisung fallthrough. Selbst in C++ gibt es seit dem Standard C++17 das Standard-Attribut [[fallthrough]][1] – Attribute sind mit Java-Annotationen vergleichbar ist; es steuert den Compiler, bei einem Durchfallen keine Warnung anzuzeigen.

Stack-Case-Labels

Stehen mehrere case-Blöcke untereinander, um damit Bereiche abzubilden, nennt sich das auch Stack-Case-Labels. Nehmen wir an, eine Variable hour steht für eine Stunde am Tag und wir wollen herausfinden, ob Mittagsruhe, Nachtruhe oder Arbeitszeit ist:

int hour = 12;

switch ( hour ) {

// Nachtruhe von 22 Uhr bis 6 Uhr

  case 22:

  case 23:

  case 24: case 0:

  case 1: 

  case 2: 

  case 3: 

  case 4:

  case 5: 

System.out.println( "Nachtruhe" );

break;

// Mittagsruhe von 13 bis 15 Uhr

  case 13:

  case 14:  

System.out.println( "Mittagsruhe" );

break;

default :

System.out.println( "Arbeiten" );

break;

}

[1]      http://en.cppreference.com/w/cpp/language/attributes

 

Herunterladen der JDK 10 Early-Access Builds

Das ist unter http://jdk.java.net/10/ möglich. Die Javadoc liegt unter https://download.java.net/java/jdk10/docs/api/overview-summary.html. Release Notes unter http://jdk.java.net/10/release-notes

Interessanter ist:

tools/javadoc(tool)

 Provide a new comment tag to specify the summary of an API description.

By default, the summary of an API description is inferred from the first sentence, which is determined by using either a simple algorithm or java.text.BreakIterator. But, the heuristics are not always correct, and can lead to incorrect determination of the end of the first sentence. A new inline tag {@summary ...} is now available, to explicitly specify the text to be used as the summary of the API description. Please refer to Documentation Comment Specification for the Standard Doclet.

security-libs/java.security

 Remove policytool

The policytool security tool has been removed from the JDK.

Auch Eclipse kann mittlerweile schon zumindest Java 10 im Dialog einstellen, einige Updates wurden gemacht, siehe https://bugs.eclipse.org/bugs/show_bug.cgi?id=529875.

Feature-Release vs. zeitorientiertes Release (ab Java 10)

20 Jahre lang bestimmten Features die Freigabe von neuen Java-Versionen; die Entwickler setzten bestimmte Neuerungen auf die Wunschliste, und wenn alle Features realisiert und getestet waren, erfolgte die allgemeine Verfügbarkeit (eng. general availability, kurz GA). Hauptproblem dieses Feature-basierten Vorgehensmodells waren die Verzögerungen, die mit Problemen bei der Implementierung einhergingen. Vieldiskutiert war das Java 9-Release, weil es unbedingt ein Modulsystem enthalten sollte.

Die Antwort auf diese Probleme, und der Wusch der Java-Community nach häufigeren Releases, beantwortet Oracle mit der „JEP 322: Time-Based Release Versioning“[1]. Vier Releases sind im Jahr geplant:

  • Im März und September erscheinen Haupt-Releases, wie Java 10, Java 11.
  • Updates erscheinen einen Monat nach einem Haupt-Release und dann im Abstand von drei Monaten. Im April und Juli erscheinen folglich Updates 10.0.1 und 10.0.2. Für Java 11 wären das Oktober 2018 und Januar 2019.

Anders gesagt: Im Halbjahresrhythmus gibt es Updates, die es Oracle erlauben, in der schnelllebigen IT-Zeit die Bibliotheken weiter zu entwickeln und neue Spracheigenschaften zu integrieren. Kommt es zu Verzögerungen, hält das nicht gleich das ganze Release auf. Java 10 erscheint als erste Version im März 2018 nach diesem Modell.[2]

[1]      http://openjdk.java.net/jeps/322

[2] http://openjdk.java.net/projects/jdk/10/

Warum „können“ Arrays so wenig?

Arrays haben ein Attribut length und eine Methode clone() sowie die von Object geerbten Methoden. Das ist nicht viel. Für Arrays gibt es keine Klassendeklaration, also auch keine .class-Datei, die Methoden deklariert. Es gibt auch keinen Java-Code wie bei anderen Klassen wie String, Arrays oder System, die mit Javadoc dokumentiert sind und folglich in der API-Dokumentation auftauchen. Da es unendliche viele Array-Typen gibt – hinter jedem beliebigen Typ kann [] gesetzt werden – würde das auch unendlich viele Klassendeklarationen nach sich ziehen.

Das, was Arrays können, ist in der Java Sprachdefinition (JLS) festgeschrieben. Bei jeder neuen Methode oder Änderung müsste die JLS angepasst werden, was unpraktisch ist. Natürlich wären Methoden wie sort(…), indexOf(…) praktisch, aber die Beschreibung will Oracle aus der JLS heraushalten und sind in eine Extraklasse Arrays gewandert.

GWT 2.8.2 erschienen

http://www.gwtproject.org/release-notes.html#Release_Notes_2_8_2
Highlights
  • Supports running in Java 9. Note that this does not yet mean that GWT can compile Java 9 sources, or support the Java 9 JRE changes, but that a Java 9 JRE can be used to compile a GWT project. Do note that the new --module-path flag is not supported, but-classpath must still be used as in the past.

  • Chrome 61 removed functionality that had been used for reading the absolute top/left values. The internal implementation has been updated to reflect modern standards.

  • Uncaught exception handler will now receive all errors on the page, as handled bywindow.onerror. This may potentially be a breaking change if there were misbehaving scripts on the page. To disable this functionality, set the propertygwt.uncaughtexceptionhandler.windowonerror to IGNORE:

    <set-property name="gwt.uncaughtexceptionhandler.windowonerror" value="IGNORE"/>

For more details, see com.google.gwt.core.Core.

Bug fixes
  • LookupMethodCreator creates too large method
  • NativeRegExp should use iframe instance, fixing Edge JIT bug
  • JsProperty getter/setter sometimes were reported as incompatible
  • Instantiating native JsTypes from JSNI results in InternalCompilerException
  • Remove the SUBSIZED characteristic from filtered streams
  • Internal compiler exception when using native JsType varargs in a JsMethod
  • Regression in String.toLowerCase and toUpperCase for some locales, specifically for Turkish
  • Missing bounds check in String.charAt
  • Fix AIOOBE when compiling method references involving varargs.
  • Apply HtmlUnit workaround ensuring that window.onerror is called correctly
Miscellanous
  • Migrated lang/jre emulation JSNI to JsInterop to share with J2CL
  • Added ErrorProne to gwt builds
  • Improved compliance with CSP
  • Added emulation for java.io.Externalizable
  • Added emulation for java.lang.reflect.Array
  • JSO.equals/hashcode will delegate to the JS object if it has methods with those names
  • Removed outdated or unused parts of project
  • Migrate guava JRE emulation to GWT
  • HtmlUnit tests are now run in batch mode

For more detail, see the issue tracker and the commit log.

Module entwickeln und einbinden

Das JPMS (Java Platform Module System), auch unter dem Projektnamen Jigsaw bekannt, ist eine der größten Neuerungen in Java 9. Im Mittelpunkt steht die starke Kapselung: Implementierungsdetails kann ein Modul geheim gehalten. Selbst Hilfscode innerhalb des Moduls, auch wenn er öffentlich ist, darf nicht nach außen dringen. Zweitens kommt eine Abstraktion von Verhalten über Schnittstellen hinzu, die interne Klassen aus dem Modul implementieren können, wobei dem Nutzer die konkreten Klassen nicht bekannt sind. Als dritten Punkt machen explizite Abhängigkeiten die Interaktion mit anderen Modulen klar. Eine grafische Darstellung hilft auch bei großen Architekturen, die Übersicht über Nutzungsbeziehungen zu behalten.

Wer sieht wen

Klassen, Pakete und Module lassen sich als Container mit unterschiedlichen Sichtbarkeiten sehen:

· Ein Typ, sei es Klasse oder Schnittstelle, enthält Attribute und Methoden

· Ein Paket enthält Typen.

· Ein Modul enthält Pakete.

· Private Eigenschaften in einem Typ sind nicht in anderen Typen sichtbar.

· Nicht öffentliche Typen sind in anderen Paketen nicht sichtbar.

· Nicht exportierte Pakete sind außerhalb eines Moduls nicht sichtbar.

Ein Modul ist definiert

1. durch einem Namen,

2. durch die Angabe, was es exportiert möchte und

3. welches Modul es zur Arbeit selbst benötigt.

Interessant ist der zweite Aspekt, also dass ein Modul etwas exportiert. Wenn nichts exportiert wird, ist auch nichts sichtbar nach außen. Alles, was Außenstehende sehen sollen, muss in der Modulbeschreibung aufgeführt sein – nicht alle öffentlichen Typen des Moduls sind standardmäßig öffentlich, dann wäre das kein Fortschritt zu JAR-Dateien. Mit dem neuen Modulsystem haben wir also eine ganz andere Sichtbarkeit. Aus der Viererbande public, private, paketsichtbar, protected bekommt public eine viel feinere Abstufung. Denn was public ist bestimmt das Modul, und das sind

– Typen, die das Modul für alle exportiert,

– Typen für explizit aufgezählte Module,

– alle Typen im gleichen Modul.

Der Compiler und die JVM achten auf die Einhaltung der Sichtbarkeit, und auch Tricks mit Reflection sind nicht mehr möglich, wenn ein Modul keine Freigabe erteilt hat.

Modultypen

Wir wollen uns in dem Abschnitt intensiver mit drei Modultypen beschäftigen. Wenn wir neue Module schreiben, dann sind das benannte Module. Daneben gibt es aus Kompatibilitätsgründen automatische Module und unbenannte Module, mit denen wir vorhandene JAR-Dateien einbringen können. Die Bibliothek der Java SE ist selbst in Module unterteilt, wir nennen sie Plattform-Module.

Die Laufzeitumgebung zeigt mit einem Schalter –list-modules alle Plattform-Module an.

Beispiel: Liste die ca. 70 Module auf:

$ java –list-modules

java.activation@9

java.base@9

java.compiler@9

oracle.desktop@9

oracle.net@9

Im Ordner C:\Program Files\Java\jdk-9\jmods liegen JMOD-Dateien.

Plattform-Module und JMOD-Beispiel

Das Kommandozeilenwerkzeug jmod zeigt an, was ein Modul exportiert und benötigt. Nehmen wir die JDBC-API für Datenbankverbindungen als Beispiel; die Typen sind in einem eigenen Modul mit den Namen java.sql.

C:\Program Files\Java\jdk-9\bin>jmod describe ..\jmods\java.sql.jmod

java.sql@9

exports java.sql

exports javax.sql

exports javax.transaction.xa

requires java.base mandated

requires java.logging transitive

requires java.xml transitive

uses java.sql.Driver

platform windows-amd64

Wir können ablesen:

· den Namen

· die Pakete, die das Modul exportiert: java.sql, javax.sql und javax.transation.xa

· die Module, die java.sql benötigt: java.base ist hier immer drin, dazu kommen java.logging und java.xml

Die Meldung mit „uses“ steht im Zusammenhang mit dem Service-Locator – wir können das vorerst ignorieren. Die Kennung über die Plattform (windows-amd64) schreibt jmod mit hinein, es ist die Belegung der System-Property os.arch auf dem Build-Server.

Verbotene Plattformeigenschaften nutzen, –add-exports

Als Sun von vielen Jahren mit der Entwicklung der Java-Bibliotheken begann, kamen viele interne Hilfsklassen mit in die Bibliothek. Viele beginnen mit den Paketpräfixen com.sun und sun. Die Typen wurden immer als interne Typen kommuniziert, doch bei einigen Entwicklern war die Neugierde und das Interesse so groß, dass die Warnungen von Sun/Oracle ignoriert wurden. In Java 9 kommt der große Knall, da public nicht mehr automatisch public für alle Klassen außerhalb des Moduls ist; die internen Klassen werden nicht mehr exportiert, sind also nicht mehr benutzbar. Es kommt zu einem Compilerfehler, wie in folgendem Beispiel:

public class ShowRuntimeArguments {

public static void main( String[] args ) throws Exception {

System.out.println( java.util.Arrays.toString( jdk.internal.misc.VM.getRuntimeArguments() ) );

}

}

Unser Programm greift auf die VM-Klasse zurück, um die eigentliche Belegung der Kommandozeile zu erfragen. In der main(String[] args)-Methode sind in args keine VM-Argumente enthalten.

Übersetzen wir das Programm, gibt es einen Compilerfehler (nicht bei einem Java 8-Compiler):

$ javac ShowRuntimeArguments.java

ShowRuntimeArguments.java:3: error: package jdk.internal.misc is not visible

System.out.println( java.util.Arrays.toString( jdk.internal.misc.VM.getRuntimeArguments() ) );

^

(package jdk.internal.misc is declared in module java.base, which does not export it to the unnamed module)

1 error

The module java.base does not export the package jdk.internal.misc., so the type jdk.internal.misc.Unsafe is not accessible – as a consequence compilation fails.

Das Problem dokumentiert der Compiler. Es ist dadurch zu lösen, indem mit dem Schalter –add-exports aus dem Modul java.base das Paket jdk.internal.misc unserer Klasse bereitgestellt wird. Die Angabe ist für den Compiler und für die Laufzeitumgebung zu setzen:

$ javac –add-exports java.base/jdk.internal.misc=ALL-UNNAMED ShowRuntimeArguments.java

$ java ShowRuntimeArguments

Exception in thread "main" java.lang.IllegalAccessError: class ShowRuntimeArguments (in unnamed module @0x77afea7d) cannot access class jdk.internal.misc.VM (in module java.base) because module java.base does not export jdk.internal.misc to unnamed module @0x77afea7d

at ShowRuntimeArguments.main(ShowRuntimeArguments.java:3)

$ java –add-exports java.base/jdk.internal.misc=ALL-UNNAMED ShowRuntimeArguments

[–add-exports=java.base/jdk.internal.misc=ALL-UNNAMED]

Wir sehen die Ausgabe, das Programm funktioniert.

Eine Angabe wie java.base/jdk.internal.misc, bei der vorne das Modul steht und hinter dem / der Paketname, ist oft ab Java 9 anzutreffen. Hinter dem Gleichheitszeichen steht entweder unser Paket, welches die Typen in jdk.internal.misc sehen kann, oder – wie in unserem Fall – ALL-UNNAMED.

jdeps

Hätten wir das Programm schon erfolgreich unter Java 8 übersetzt, würde es zur Laufzeit ebenfalls knallen. Da es nun sehr viel Programmcode gibt, haben die Java-Entwickler bei Oracle das Kommandozeilenprogramm jdeps entwickelt. Es meldet, wenn interne Typen im Programm vorkommen:

$ jdeps ShowRuntimeArguments.class

ShowRuntimeArguments.class -> java.base

<unnamed> -> java.io java.base

<unnamed> -> java.lang java.base

<unnamed> -> java.util java.base

<unnamed> -> jdk.internal.misc JDK internal API (java.base)

Die Meldung „JDK internal API“ bereitet uns darauf vor, dass es gleich Ärger geben wird.

So kann relativ leicht eine große Codebasis untersucht werden und Entwicker können proaktiv den Stellen auf den Grund gehen, die problematische Abhängigkeiten haben.

Plattformmodule einbinden, –add-modules und –add-opens

Jedes Java SE-Projekt basiert auf dem Modul java.se, was diverse Modulabhängigkeiten nach sich zieht.

<pic: java.se-graph.png, „Modulabhängigkeiten von java.se“>

Diverse Module sind nicht Teil vom Modul Java SE, unter anderem sind das das Java Activation Framework, CORBA, Transaction-API, JAXB, Web-Services und interne Module, die mit jdk beginnen.

Starten wir ein Programm mit Bezug zu einem dieser Bibliotheken gibt es einen Fehler. Zunächst zum Programm, das ein Objekt automatisch in XML konvertieren soll:

public class Person {

public String name = "Chris";

public static void main( String[] args ) {

javax.xml.bind.JAXB.marshal( new Person(), System.out );

}

}

Ausgeführt auf der Kommandozeile folgt ein Fehler:

$ java Person

Exception in thread "main" java.lang.NoClassDefFoundError: javax/xml/bind/JAXB

at Person.main(Person.java:6)

Caused by: java.lang.ClassNotFoundException: javax.xml.bind.JAXB

at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(Unknown Source)

at java.base/jdk.internal.loader.ClassLoaders$AppClassLoader.loadClass(Unknown Source)

at java.base/java.lang.ClassLoader.loadClass(Unknown Source)

… 1 more

Wir müssen das Modul java.xml.bind (oder auch das „Über“-Modul java.se.ee) mit angeben; dafür dient der Schalter –add-modules.

$ java –add-modules java.xml.bind Person

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>

<person>

<name>Chris</name>

</person>

Öffnen für Reflection

Jetzt gibt es allerdings ein anderes Problem, was auffällt, wenn wir einen anderen Typ in XML umwandeln wollen:

public class Today {

public static void main( String[] args ) {

javax.xml.bind.JAXB.marshal( new java.util.Date(), System.out );

}

}

Auf der Kommandozeile zeigt sich:

$ java –add-modules java.xml.bind Today

Exception in thread "main" javax.xml.bind.DataBindingException: javax.xml.bind.JAXBException: Package java.util with JAXB class java.util.Date defined in a module java.base must be open to at least java.xml.bind module.

at java.xml.bind@9/javax.xml.bind.JAXB._marshal(Unknown Source)

at java.xml.bind@9/javax.xml.bind.JAXB.marshal(Unknown Source)

at Today.main(Today.java:4)

Caused by: javax.xml.bind.JAXBException: Package java.util with JAXB class java.util.Date defined in a module java.base must be open to at least java.xml.bind module.

at java.xml.bind@9/javax.xml.bind.ModuleUtil.delegateAddOpensToImplModule(Unknown Source)

at java.xml.bind@9/javax.xml.bind.ContextFinder.newInstance(Unknown Source)

at java.xml.bind@9/javax.xml.bind.ContextFinder.newInstance(Unknown Source)

Die zentrale Information ist „Package java.util with JAXB class java.util.Date defined in a module java.base must be open to at least java.xml.bind module“. Wir müssen etwas öffnen; dazu verwenden wir den Schalter –add-opens:

$ java –add-modules java.xml.bind –add-opens java.base/java.util=java.xml.bind Box

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>

<date>2017-09-02T23:20:52.170+02:00</date>

Die Option öffnet für Reflection das Paket java.util aus dem Modul java.base für java.xml.bind. Neben –add-opens gibt es das ähnliche –add-exports, was alle öffentlichen Typen und Eigenschaften zur Übersetzungs-/Laufzeit öffnet; –add-opens geht für Reflection einen Schritt weiter.

Projektabhängigkeiten in Eclipse

Um Module praktisch umzusetzen wollen wir in Eclipse zwei neue Java-Projekte aufbauen: „com.tutego.greeter“ und „com.tutego.main“. Wir legen in das Projekt „com.tutego.greeter“ eine Klasse com.tutego.insel.greeter.Greeter an und in „com.tutego.main“ die Klasse com.tutego.insel.main.Main.

Jetzt ist eine wichtige Vorbereitung in Eclipse nötig: Wir müssen einstellen, dass „com.tutego.main“ das Java-Projekt „com.tutego.greeter“ benötigt. Dazu gehen wir auf das Projekt „com.tutego.main“ und rufen im Kontextmenü Project auf; alternativ im Menüpunkt Project > Properties oder über die Tastenkombination Alt + Return. Im Dialog navigiere links auf Java Build Path und aktiviere den Reiter Projects. Wähle Add… und im Dialog wähle aus der Liste com.tutego.greeter. Ok schließt den kleinen Dialog und unter Required projects in build path taucht eine Abhängigkeit auf.

Wir können jetzt zwei einfache Klassen implementieren. Zunächst für das Projekt „com.tutego.greeter“:

com/tutego/insel/greeter/Greeter.java

package com.tutego.insel.greeter;

public class Greeter {

private Greeter() { }

public static Greeter instance() {

return new Greeter();

}

public void greet( String name ) {

System.out.println( "Hey "+ name );

}

}

Und die Hauptklasse im Projekt „com.tutego.main“:

com/tutego/insel/main/Main

package com.tutego.insel.main;

import com.tutego.insel.greeter.Greeter;

public class Main {

public static void main( String[] args ) {

Greeter.instance().greet( "Chris" );

}

}

Da wir in Eclipse vorher die Abhängigkeit gesetzt haben, gibt es keinen Compilerfehler.

Benannte Module und module-info.java

Die Modulinformationen werden über eine Datei module-info.java (kurz Modulinfodatei) deklariert, Annotationen kommen nicht zum Einsatz. Diese zentrale Datei ist der Hauptunterschied zwischen einem Modul und einer einfachen JAR-Datei. In dem Moment, in dem die spezielle Klassendatei module-info.class im Modulpfad ist, beginnt die Laufzeitumgung das Projekt als Modul zu interpretieren.

Testen wir das, indem wir in unsere Projekte „com.tutego.greeter“ und „com.tutego.main“ eine Modulinfodatei anlegen. Das kann Eclipse über das Kontextmenü Configue > Create module-info.java für uns machen.

Für das erste Modul com.tutego.greeter entsteht:

module-info.java

/**

*

*/

/**

* @author Christian

*

*/

module com.tutego.greeter {

exports com.tutego.insel.greeter;

requires java.base;

}

Und für die zweite Modulinfodatei – Kommentare ausgeblendet:

module-info.java

module com.tutego.main {

exports com.tutego.insel.main;

requires com.tutego.greeter;

requires java.base;

}

Hinter dem Schlüsselwort module steht der Name des Moduls, den Eclipse automatisch so wählt wie das Eclipse-Projekt heißt.[1] Es folgt ein Block in geschweiften Klammern.

Zwei Schlüsselworte fallen ins Auge, die wir schon vorher bemerkt haben: exports und requires.

· Das Projekt/Modul com.tutego.greeter exportiert das Paket com.tutego.insel.greeter. Andere Pakete nicht. Es benötigt (requires) java.base, wobei das Modul Standard ist, und die Zeile gelöscht werden kann.

· Das Projekt/Modul com.tutego.main exportiert das Paket com.tutego.insel.main und es benötigt com.tutego.greeter – diese Information nimmt sich Eclipse selbstständig aus den Projektabhängigkeiten.

Info: Ein Modul required ein anderes Modul aber exports ein Paket.

Beginnen wir mit den Experimenten in den beiden module-info.java-Dateien:

Modul

Aktion

Ergebnis

com.tutego.greeter

com.tutego.main

// requires java.base;

Auskommentieren führt zu keiner Änderung, da java.base immer required wird

com.tutego.greeter

// exports com.tutego.insel.greeter;

Compilerfehler im main-Modul “The type com.tutego.insel.greeter.Greeter is not accessible”

com.tutego.greeter

exports com.tutego.insel.greeter to god;

Nur das Modul god bekommt Zugriff auf com.tutego.insel.greeter. Das main-Modul meldet “The type com.tutego.insel.greeter.Greeter is not accessible”

com.tutego.greeter

exports com.tutego.insel.closer;

Hinzufügen führt zum Compilerfehler “The package com.tutego.insel.closer does not exist or is empty”

com.tutego.main

// requires com.tutego.greeter;

Compilerfehler “The import com.tutego.insel.greeter cannot be resolved”

com.tutego.main

// exports com.tutego.insel.main;

Keins, denn c.t.i.m wird von keinem Modul required

Die Zeile mit exports com.tutego.insel.greeter to god zeigt einen qualifizierten Export.

Übersetzen und Packen von der Kommandozeile

Setzen wir in der Wurzelverzeichnis vom Modul com.tutego.greeter ein Batch-Skript compile.bat:

compile.bat

set PATH=%PATH%;C:\Program Files\Java\jdk-9\bin

rmdir /s /q lib

mkdir lib

javac -d bin src\module-info.java src\com\tutego\insel\greeter\Greeter.java

jar –create –file=lib/com.tutego.greeter@1.0.jar –module-version=1.0 -C bin .

jar –describe-module –file=lib/com.tutego.greeter@1.0.jar

Folgende Schritte führt das Skript aus:

1. Setzen der PATH-Variable für die JDK-Tools

2. Löschen eines vielleicht schon angelegten lib-Ordners

3. Anlegen eines neuen lib-Orders für die JAR-Datei

4. Übersetzen der zwei Java-Dateien in den Zielordner bin

5. Anlegen einer JAR-Datei. –create (abkürzbar zu –c) instruiert das Werkzeug eine neue JAR-Datei anzulegen. –file (oder kurz –f) bestimmt den Zielnamen. –module-version unsere Versionsnummer und –C wechselt das Verzeichnis und beginnt ab dort die Dateien einzusammeln. Die Kommandozeilensyntax beschreibt Oracle auf der Webseite https://docs.oracle.com/javase/9/tools/jar.htm.

6. Die Option –describe-module (oder kurz –d) zeigt die Modulinformation und führt zu folgender (vereinfachten) Ausgabe: com.tutego.greeter@1.0 jar:file:///C:/…/com.tutego.greeter/lib/com.tutego.greeter@1.0.jar/!module-info.class exports com.tutego.insel.greeter requires java.base.

Für das zweite Projekt ist die compile.bat sehr ähnlich, dazu kommt ein Aufruf der JVM, um das Programm zu starten.

compile.bat

set PATH=%PATH%;C:\Program Files\Java\jdk-9\bin

rmdir /s /q lib

mkdir lib

javac -d bin –module-path ..\com.tutego.greeter\lib src\module-info.java src\com\tutego\insel\main\Main.java

jar -c -f=lib/com.tutego.main@1.0.jar –main-class=com.tutego.insel.main.Main –module-version=1.0 -C bin .

java -p lib;..\com.tutego.greeter\lib -m com.tutego.main

Änderungen gegenüber dem ersten Skript sind:

1. Beim Compilieren müssen wir den Modulpfad mit –module-path (oder kürzer mit -p) angeben, weil ja das Modul com.tutego.greeter required ist.

2. Beim Anlegen der JAR-Datei geben wir über –main-class die Klasse mit der main(…)-Methode an.

3. Startet die JVM das Programm, lädt sie das Hauptmodul und alle abhängigen Module. Wir geben beide lib-Order mit den JAR-Dateien an und mit –m das sogenannte initiale Modul für die Hauptklasse.

Automatische Module

JAR-Dateien spielen seit 20 Jahren eine zentrale Rolle im Java-System; sie vom einen zum anderen Tag abzuschaffen würde große Probleme bereiten. Ein Blick auf https://mvnrepository.com/repos offenbart über 7,7 Millionen Artefakte; es gehen auch Dokumentationen und andere Dateien in die Statistik ein, doch es gibt eine Größenordnung, wie viele JAR-Dateien im Umlauf sind.

Damit JAR-Dateien unter Java 9 eingebracht werden können gibt es zwei Lösungen: das JAR in den Klassenpfad oder in den neuen Modulpfad zu setzen. Kommt ein JAR in den Modulpfad und hat es keine Modulinfodatei entsteht ein automatisches Modul. Bis auf eine kleine Einschränkung funktioniert das für die meisten existierenden Java-Bibliotheken.

Ein automatisches Modul hat gewisse Eigenschaften für den Modulnamen und Konsequenzen in den Abhängigkeiten:

· Ohne Modulinfo haben die automatischen Module keinen selbstgewählten Namen, sondern sie bekommen vom System einen Namen zugewiesen, der sich aus dem Dateinamen ergibt.[2] Vereinfacht gesagt: Angehängte Versionsnummern und die Dateiendung werden entfernt und alle nicht-alphanummerischen Zeichen durch Punkte ersetzt, jedoch nicht zwei Punkte hintereinander.[3] Die Version wird erkannt. Die Dokumentation gibt das Beispiel foo-bar-1.2.3-SNAPSHOT.jar an, was zum Modulnamen foo.bar und der Version 1.2.3-SNAPSHOT führt.

· Automatische Module exportieren immer alle ihre Pakete. Wenn es also eine Abhängigkeit zu diesem automatischen Modul gibt kann der Bezieher alle sichtbare Typen und Eigenschaften verwenden.

· Automatische Module können alle anderen Module lesen, auch die unbenannten.

Auf den ersten Blick scheint eine Migration in Richtung Java 9 einfach: Alle JARs auf den Modulpfad und nacheinander Modulinfodateien anlegen. Allerdings gibt es JAR-Dateien, die von der JVM als automatisches Modul abgelehnt werden, wenn sie nämlich Typen eines Paketes enthalten, und dieses Paket sich schon in einem anderen aufgenommen Modul befindet. Module dürfen keine „split packages“ enthalten, also das gleiche Paket noch einmal enthalten. Die Migration erfordert dann entweder a) das Zusammenlegen der Pakete zu einem Modul, b) die Verschiebung in unterschiedliche Pakete oder c) die Nutzung des Klassenpfades

Unbenanntes Modul

Eine Migration auf eine neue Java-Version sieht in der Regel so aus, dass zuerst die JVM gewechselt und geprüft wird, ob die vorhandene Software weiterhin funktioniert. Laufen die Testfälle durch und gibt es keine Auffälligkeiten im Testbetrieb kann der Produktivbetrieb unter der neuen Version erfolgen. Gibt es keine Probleme, können nach einiger Zeit die neuen Sprachmittel und Bibliotheken verwendet werden.

Übertragen wir das auf den Wechsel von Java 8 auf Java 9: Eine vorhandene Java-Software muss inklusive aller Einstellungen und Einträge im Klassenpfad weiterhin laufen. Das heißt, eine Java 9 Laufzeitumgebung kann den Klassenpfad nicht ignorieren. Da es intern nur einen Modulpfad gibt, müssen auch diese JAR-Dateien zu Modulen werden. Die Lösung ist das unbenannte Modul (eng. unnamed module): jedes JAR im Klassenpfad – dabei spielt es keine Rolle, ob es eine modul-info.class enthält – kommt in das unbenannte Modul. Davon gibt es nur eines, wir sprechen also im Singular, nicht Plural.

„Unbenannt“ sagt schon, dass das Modul keinen Namen hat, und folglich auch keine Abhängigkeit zu den JAR-Dateien im unbenannten Modul existieren kann; das ist der Unterschied zu einem automatischen Modul. Ein unbenanntes Modul hat die gleiche Eigenschaft wie ein automatisches Modul, dass es alle Pakete exportiert. Und weil es zur Migration gehört, hat ein unbenanntes Modul auch Zugriff auf alle anderen Module.

Lesbarkeit und Zugreifbarkeit

Die Laufzeitumgebung sortiert Module in einem Graphen ein. Die Abhängigkeit der Module führt dabei zu sogenannten Lesbarkeit (engl. readibility): Benötigt Modul A das Modul B, so ist liest A das Modul B und B wird von A gelesen. Für die Funktionsweise vom Modulsystem ist dies elementar, denn so werden zur Übersetzungszeit schon Fehler ausgeschlossen, wie Zkylen, oder gleiche Pakete in unterschiedlichen Modulen. Die Lesbarkeit ist zentral für eine zuverlässige Konfiguration, engl. reliable configuration.

Ein Schritt weiter geht der Begriff der Erreichbarkeit/Zugänglichkeit (engl. accessibility). Wenn ein Modul ein anderes Modul grundsätzlich lesen kann, bedeutet es noch nicht, dass es an alle Pakete und Typen kommt, denn nur die Typen sind sichtbar, die exportiert worden. Lesbare und erreichbare Typen nennen sich erreichbar.

Die nächste Frage ist, welcher Modultyp auf welchen anderen Modultyp Zugriff hat. Eine Tabelle fasst die Lesbarkeit am Besten zusammen:

Modultyp

Ursprung

Exportiert Pakete

Hat Zugriff auf

Plattformmodul

JDK

explizit

 

Benannte Module

Container mit Modulinfo im Modulpfad

explizit

Plattformmodule, andere benannte Module, automatische Module

Automatische Module

Container ohne Modulinfo im Modulpfad

alle

Plattformmodule, andere benannte Module, automatische Module, unbenanntes Modul

Unbenanntes Modul

Klassendateien und JARs im Klassenpfad

alle

Plattformmodule, benannte Module, automatische Module

Lesbarkeit der Module

Der Modulinfodatei kommt dabei die größte Bedeutung zu, denn sie macht aus einer JAR ein modular-JAR; fehlt die Modulinfomation bleibt es eine normale JAR, wie sie Java-Entwickler seit 20 Jahren kennen. Die JAR-Datei kann neu in den Modulpfad kommen oder in den bekannten Klassenpfad. Das ergibt vier Kombinationen:

 

Modulpfad

Klassenpfad

JAR mit Modulinfomation

wird benanntes Module

wird ubenanntes Modul

JAR ohne Modulinfomation

wird automatisches Modul

wird unbenanntes Modul

JARs im Pfad

JAR-Archive im Klassenpfad sind das bekannte Verhalten, weswegen auch ein Wechsel von Java 8 auf Java 9 möglich sein sollte.

Modul-Migration

Nehmen wir an, unsere monolithische Applikation hat keine Abhängigkeiten zu externen Bibliotheken und soll modularisiert werden. Dann besteht der erste Schritt darin, die gesamte Applikation in ein großes benanntes Modul zu setzen. Als nächstes müssen die einzelnen Bereiche identifiziert werden, damit nach und nach die Bausteine in einzelne Module wandern. Das ist nicht immer einfach, zumal zyklische Abhängigkeiten nicht unwahrscheinlich sind. Bei der Modularisierung des JDK hatten die Oracle-Entwickler viel Mühe.

Das Problem mit automatischen Modulen

Traditionell generieren Build-Werkzeuge wie Maven oder Gradle JAR-Dateien und ein Dateiname hat sich irgendwie ergeben. Werden jedoch diese JAR-Dateien zu automatischen Modulen spielt der Dateiname plötzlich eine große Rolle. Doch bewusst wurde der Dateiname vermutlich nie gewählt. Referenziert ein benanntes Modul ein automatisches Modul bringt das zwei Probleme mit sich: Ändert sich der Dateiname, lassen wir die Versionsnummer einmal außen vor, heißt auch das automatische Module anders, und die Abhängigkeit kann nicht mehr aufgelöst werden. Das zweite Problem ist größer. Viele Java-Bibliotheken haben noch keine Modulinformationen, und folglich werden Entwickler eine Abhängigkeit zu diesem automatischen Modul über den abgeleiteten Namen ausdrücken. Nehmen wir z. B. die beliebte Open-Source Bibliothek Google Guava. Die JAR-Datei hat den Dateinamen guava-23.0.jar – guava heißt folglich das automatische Modul. Ein benanntes Modul (nennen wir es M1) kann über required guava eine Abhängigkeit ausdrücken. Konvertiert Google die Bibliothek in ein echtes Java 9-Modul, dann wird sich der Name ändern – geplant ist com.google.guava. Und ändert sich der Name, führen alle Referenzierungen in Projekten zu einem Compilerfehler; ein Alias wäre eine tolle Idee, das gibt es jedoch nicht. Und das Problem besteht ja nicht nur im eigenen Code der Guava referenziert; Referenziert das eigene Modul M1 ein Modul M2, das wiederum Guava referenziert, so gibt es das gleiche Problem – wir sprechen von einer transitiven Abhängigkeit. Die Änderung vom Modulnamen von Guava wird zum Problem, denn wir müssen warten, bis M2 den Namen korrigiert, damit M1 wieder gültig ist.

Eine Lösung mildert das Problem ab: In der JAR-Manifest-Datei kann ein Eintrag Automatic-Module-Name gesetzt werden – das „überschreibt“ den automatischen Modulnamen.

Beispiel: Apache Commons setzt den Namen so:

Automatic-Module-Name: org.apache.commons.lang3

Benannte Module, die Abhängigkeiten auf automatische Module besitzen, sind also ein Problem. Es ist zu hoffen, dass die zentralen Java-Bibliotheken, auf die sich so viele Lösungen stützen, schnell Modulinformationen einführen. Das wäre eine Lösung von unten nach oben, englisch Bottom-Up. Das ist das einzige, was erfolgversprechend ist, aber wohl auch eine lange Zeit benötigen wird. Im Monat vom Java 9-Release hat noch keine wichtige Java-Bibliothek eine Modulinformation, Automatic-Module-Name kommt häufiger vor.


[1] Zur Benennung von Modulen gibt es Empfehlungen in dem englischsprachigen Beitrag http://mail.openjdk.java.net/pipermail/jpms-spec-experts/2017-May/000687.html

[2] Automatic-Module-Name in die META-INF-Datei zu setzen ist eine Alternative, dazu später mehr.

[3] Für Details siehe http://download.java.net/java/jigsaw/docs/api/java/lang/module/ModuleFinder.html#of-java.nio.file.Path…-

JMOD-Dateien und JAR-Dateien

Der Klassenlader bezieht .class-Dateien nicht nur aus Verzeichnissen, sondern in der Regel aus Containern. So müssen keine Verzeichnisse ausgetauscht werden, sondern nur einzelne Dateien. Als Container-Formate finden wir JMOD (neu in Java 9) und JAR. Wenn Java-Software ausgeliefert wird bieten sich JAR- oder JMOD-Dateien an, denn es ist einfacher und platzsparender, nur ein komprimiertes Archiv weiterzugehen als einen großen Dateibaum.

JAR-Dateien

Sammlungen von Java-Klassendateien und Ressourcen werden in der Regel in Java-Archiven, kurz JAR-Dateien, zusammengefasst. Diese Dateien sind im Grunde ganz normale ZIP-Archive mit einem besonderen Verzeichnis META-INF für Metadateien. Das JDK bringt im bin-Verzeichnis das Werkzeug jar zum Aufbau und Extrahieren von JAR-Dateien mit.

JAR-Dateien behandelt die Laufzeitumgebung wie Verzeichnisse von Klassendateien und Ressourcen. Zudem haben Java-Archive den Vorteil, dass sie signiert werden können und illegale Änderungen auffallen. JAR-Dateien können Modulinformationen beinhalten, dann heißen sie engl. modular JAR.

JMOD-Dateien

Das Format JMOD ist speziell für Module und neu in Java 9 – es organisiert Typen und Ressourcen. Zum Auslesen und Packen gibt es im bin-Verzeichnis des JDK das Werkzeug jmod.

Hinweis

Die JVM greift selbst nicht auf diese Module zurück. Achten wir auf die Ausgabe vom letzten Programmm, dann steht in der ersten Zeile:

[0.015s][info][class,load] opened: C:\Program Files\Java\jdk-9\lib\modules

Die Datei module ist ca. 170 MiB groß und in einem proprietären Dateiformat.

JAR vs. JMOD

Module können in JMOD- und JAR-Conatainer gepackt werden. Wenn ein JAR kein modular-JAR ist, also keine Modulinformationen enthält, so fehlen zentrale Informationen, wie Abhängigkeiten oder eine Version; ein JMOD ist immer ein benanntes Modul.

JMOD-Dateien sind nicht so flexibel wie JAR-Dateien, denn sie können nur zur Übersetzungszeit und zum Linken eines Runtime-Images – dafür gibt es das Kommandozeilenwerkzeug jlink – genutzt werden. JMOD-Dateien können nicht wie JAR-Dateien zur Laufzeit verwendet werden. Das Dateiformat ist proprietär und kann sich jederzeit ändern, es ist nichts Genaues spezifiziert.[1] Einziger Vorteil von JMOD: Native Bibliotheken lassen sich standardisiert einbinden.


[1] Die http://openjdk.java.net/jeps/261 macht die Aussage, dass es ein ZIP ist.

java.util.Optional ist keine Nullnummer

Java hat eine besondere Referenz, die Entwicklern die Haare zu Berge stehen lässt und die Grund für lange Debug-Stunden ist: die null-Referenz. Eigentlich sagt null nur aus: „nicht initialisiert“. Doch was null so problematisch macht, ist die NullPointerException, die durch referenzierte null-Ausdrücke ausgelöst wird.

Beispiel: Entwickler haben vergessen, das Attribut location mit einem Objekt zu initialisieren, sodass setLocation(…) fehlschlagen wird:

class Place {
   private Point2D location;
   public void setLocation( double longitude, double latitude ) {
     location.setLocation( longitude, latitude );  // BANG! NullPointerException
   }
 }

Einsatz von null

Fehler dieser Art sind durch Tests relativ leicht aufzuspüren. Aber hier liegt nicht das Problem. Das eigentliche Problem ist, dass Entwickler allzu gerne die typenlose null[1] als magischen Sonderwert sehen, sodass sie neben „nicht initialisiert“ noch etwas anderes bedeutet:

  • Erlaubt die API in Argumenten für Methoden/Konstruktoren null, heißt das meistens „nutze einen Default-Wert“ oder „nichts gegeben, ignorieren“.
  • In Rückgaben von Methoden steht null oftmals für „nichts gemacht“ oder „keine Rückgabe“. Im Gegensatz dazu kodieren andere Methoden wiederum mit der Rückgabe null, dass eine Operation erfolgreich durchlaufen wurde, und würden sonst zum Beispiel Fehlerobjekte zurückgegeben.[2]

Beispiel 1

Die mit Javadoc dokumentiere Methode getTask(out, fileManager, diagnosticListener, options, classes, compilationUnits) in der Schnittstelle JavaCompiler ist so ein Beispiel:

  • out: „a writer for additional output from the compiler; use system.err if null“
  • fileManager: „a file manager; if null use the compiler’s standard filemanager“
  • diagnosticListener: „a diagnostic listener; if null use the compiler’s default method for reporting diagnostics“
  • options: „compiler options, null means no options“
  • classes: „names of classes to be processed by annotation processing, null means no class names“
  • compilationUnits: „the compilation units to compile, null means no compilation units“

Alle Argumente können null sein, getTask(null, null, null, null, null, null) ist ein korrekter Aufruf. Schön ist die API nicht, und besser wäre sie wohl mit einem Builder-Pattern gelöst.

Beispiel 2

Der BufferedReader erlaubt das zeilenweise Einlesen aus Datenquellen, und readLine() liefert null, wenn es keine Zeile mehr zu lesen gibt.

Beispiel 3

Viel Irritation gibt es mit der API vom Assoziativspeicher. Eine gewöhnliche HashMap kann als assoziierten Wert null bekommen, doch get(key) liefert auch dann null, wenn es keinen assoziierten Wert gibt. Das führt zu einer Mehrdeutigkeit, da die Rückgabe von get(…) nicht verrät, ob es eine Abbildung auf null gibt oder ob der Schlüssel nicht vorhanden ist.

Map<Integer,String> map = new HashMap<>();
 map.put( 0, null );
 System.out.println( map.containsKey( 0 ) );      // true
 System.out.println( map.containsValue( null ) ); // true
 System.out.println( map.get( 0 ) );              // null
 System.out.println( map.get( 1 ) );              // null

Kann die Map null-Werte enthalten, muss es immer ein Paar der Art if(map.containsKey(key)), gefolgt von map.get(key) geben. Am besten verzichten Entwickler auf null in Datenstrukturen.

Da null so viele Einsatzfälle hat und das Lesen der API-Dokumentation gerne übersprungen wird, sollte es zu einigen null-Einsätzen Alternativen geben. Manches Mal ist das einfach, etwa wenn die Rückgabe Sammlungen sind. Dann gibt es mit einer leeren Sammlung eine gute Alternative zu null. Das ist ein Spezialfall des so genannten Null-Object-Patterns.

Fehler, die aufgrund einer NullPointerException entstehen, ließen sich natürlich komplett vermeiden, wenn immer ordentlich auf null-Referenzen getestet würde. Aber gerade die null-Prüfungen werden von Entwicklern gerne vergessen, da ihnen nicht bewusst ist oder sie nicht erwarten, dass eine Rückgabe null sein kann. Gewünscht ist ein Programmkonstrukt, bei dem explizit wird, dass ein Wert nicht vorhanden sein kann, sodass nicht null diese Rolle übernehmen muss. Wenn im Code lesbar ist, dass ein Wert optional ist, also vorhanden sein kann oder nicht, reduziert das Fehler.

Geschichte

Tony Hoare gilt als „Erfinder“ der null-Referenz. Heute bereut er es und nennt die Entscheidung „my billion-dollar mistake“.[3]

Optional-Typ

Die Java-Bibliothek bietet eine Art Container, der ein Element enthalten kann oder nicht. Wenn der Container ein Element enthält, ist es nie null. Dieser Container kann befragt werden, ob er ein Element enthält oder nicht. Eine null als Kennung ist somit überflüssig.

Beispiel

Optional<String> opt1 = Optional.of( "Aitazaz Hassan Bangash" );
 System.out.println( opt1.isPresent() );   // true
 System.out.println( opt1.get() );         // Aitazaz Hassan Bangash
 Optional<String> opt2 = Optional.empty();
 System.out.println( opt2.isPresent() );   // false
 // opt2.get() -> java.util.NoSuchElementException: No value present
 Optional<String> opt3 = Optional.ofNullable( "Malala" );
 System.out.println( opt3.isPresent() );   // true
 System.out.println( opt3.get() );         // Malala
 Optional<String> opt4 = Optional.ofNullable( null );
 System.out.println( opt4.isPresent() );   // false
 // opt4.get() -> java.util.NoSuchElementException: No value present

final class java.util.Optional<T>

  • static<T>Optional<T>empty()
    Liefert ein leeres Optional-Objekt.
  • booleanisPresent()
    Liefert wahr, wenn dieses Optional einen Wert hat, sonst ist wie im Fall von empty() die Rückgabe false.
  • static<T>Optional<T>of(Tvalue)
    Baut ein neues Optional mit einem Wert auf, der nicht null sein darf; andernfalls gibt es eine NullPointerException. null in das Optional hineinzubekommen, geht also nicht.
  • static<T>Optional<T>ofNullable(Tvalue)
    Liefert ein Optional mit dem Wert, wenn dieser ungleich null ist, bei null ist die Rückgabe ein empty().
  • Tget()
    Liefert den Wert. Enthält das Optional keinen Wert, weil kein Wert isPresent() ist, folgt eine NoSuchElementException.
  • TorElse(Tother)
    Ist ein Wert isPresent(), liefere den Wert. Ist das Optional leer, liefere other.
  • Stream<T> stream()
    Konvertiere das Optional in den Datentyp Stream. Neu in Java 9.

Des Weiteren überschreibt Optional die Methoden equals(…), toString() und hashCode() – 0, wenn kein Wert gegeben ist, sonst Hashcode vom Element – und ein paar weitere Methoden, die wir uns später anschauen.

Hinweis: Intern null zu verwenden hat zum Beispiel den Vorteil, dass die Objekte serialisiert werden können. Optional implementiert Serializable nicht, daher sind Optional-Attribute nicht serialisierbar, können also etwa nicht im Fall von Remote-Aufrufen mit RMI übertragen werden. Auch die Abbildung auf XML oder auf Datenbanken ist umständlicher, wenn nicht JavaBean-Properties herangezogen werden, sondern die internen Attribute.

Ehepartner oder nicht?

Optional wird also dazu verwendet, im Code explizit auszudrücken, ob ein Wert vorhanden ist oder nicht. Das gilt auf beiden Seiten: Der Erzeuger muss explizit ofXXX(…) aufrufen und der Nutzer explizit isPresent() oder get(). Beide Seiten sind sich bewusst, dass sie es mit einem Wert zu tun haben, der optional ist, also existieren kann oder nicht. Wir wollen das in einem Beispiel nutzen, und zwar für eine Person, die einen Ehepartner haben kann:

public class Person {
   private Person spouse;
   public void setSpouse( Person spouse ) {
     this.spouse = Objects.requireNonNull( spouse );
   }
   public void removeSpouse() {
     spouse = null;
   }
   public Optional<Person> getSpouse() {
     return Optional.ofNullable( spouse );
   }
 }

In diesem Beispiel ist null für die interne Referenz auf den Partner möglich; diese Kodierung soll aber nicht nach außen gelangen. Daher liefert getSpouse() nicht direkt die Referenz, sondern es kommt Optional zum Einsatz und drückt aus, ob eine Person einen Ehepartner hat oder nicht. Auch bei setSpouse(…) akzeptieren wir kein null, denn null-Argumente sollten so weit wie möglich vermieden werden. Ein Optional ist hier nicht angemessen, weil es ein Fehler ist, null zu übergeben. Zusätzlich sollte natürlich die Javadoc an setSpouse(…) dokumentieren, dass ein null-Argument zu einer NullPointerException führt. Daher passt Optional als Parametertyp nicht.

Person heinz = new Person();
 System.out.println( heinz.getSpouse().isPresent() ); // false
 Person eva = new Person();
 heinz.setSpouse( eva );
 System.out.println( heinz.getSpouse().isPresent() ); // true
 System.out.println( heinz.getSpouse().get() ); // com/…/Person
 heinz.removeSpouse();
 System.out.println( heinz.getSpouse().isPresent() ); // false

Primitive optionale Typen

Während Referenzen null sein können und auf diese Weise das Nichtvorhandensein anzeigen, ist das bei primitiven Datentypen nicht so einfach. Wenn eine Methode ein boolean zurückgibt, bleibt neben true und false nicht viel übrig, und ein „nicht zugewiesen“ wird dann doch gerne wieder über einen Boolean verpackt und auf null getestet. Gerade bei Ganzzahlen gibt es immer wieder Rückgaben wie –1.[4] Das ist bei den folgenden Beispielen der Fall:

  • Wenn bei InputStreams read(…) keine Eingaben mehr kommen, wird -1 zurückgegeben.
  • indexOf(Object) von List liefert -1, wenn das gesuchte Objekt nicht in der Liste ist und folglich auch keine Position vorhanden ist.
  • Bei einer unbekannten Bytelänge einer MIDI-Datei (Typ MidiFileFormat) hat getByteLength() als Rückgabe -1.

Diese magischen Werte sollten vermieden werden, und daher kann auch der optionale Typ wieder erscheinen.

Als generischer Typ kann Optional beliebige Typen kapseln, und primitive Werte könnten in Wrapper verpackt werden. Allerdings bietet Java für drei primitive Typen spezielle Optional-Typen an: OptionalInt, OptionalLong, OptionalDouble:

Optional<T> OptionalInt OptionalLong OptionalDouble
static <T>
Optional<T>
empty()
static
OptionalInt
empty()
static
OptionalLong
empty()
static
Optional-Double
empty()
T get() int getAsInt() long getAsLong() double getAsDouble()
boolean isPresent()
static <T>
Optional<T>
of(T value)
static OptionalInt
of(int value)
static OptionalLong
of(long value)
static OptionalDouble
of(double value)
static <T>
Optional<T>
ofNullable(T
value)
nicht übertragbar
T orElse(T other) int orElse(int other) long orElse(long other) double orElse(
double other)
boolean equals(Object obj)
int hashCode()
String toString()

Tabelle: Methodenvergleich zwischen den vier OptionalXXX-Klassen

Die Optional-Methode ofNullable(…) fällt in den primitiven Optional-Klassen natürlich raus. Die optionalen Typen für die drei primitiven Typen haben insgesamt weniger Methoden, und die obere Tabelle ist nicht ganz vollständig. Wir kommen im Rahmen der funktionalen Programmierung in Java noch auf die verbleibenden Methoden wie isPresent(…) zurück.

Best Practice: OptionalXXX-Typen eignen sich hervorragend als Rückgabetyp, sind als Parametertyp denkbar, doch wenig attraktiv für interne Attribute. Intern ist null eine akzeptable Wahl, der „Typ“ ist schnell und speicherschonend.

Erstmal funktional mit Optional

Neben den vorgestellten Methoden wie ofXXX(…) und isPresent() gibt es weitere, die auf funktionale Schnittstellen zurückgreifen:

final class java.lang.Optional<T>

  • voidifPresent(Consumer<?superT>consumer)
    Repräsentiert das Optional einen Wert, rufe den Consumer mit diesem Wert auf, andernfalls mache nichts.
  • void ifPresentOrElse(Consumer<? super T> action, Runnable emptyAction)
    Repräsentiert das Optional einen Wert, rufe den Consumer mit diesem Wert auf, andernfalls führe emptyAction Das Runnable muss hier als Typ aus java.lang herhalten, weil es im java.util.function-Paket keine Schnittstelle gibt, die keine Parameter hat und auch keine Rückgabe liefert. Neu in Java 9.
  • Optional<T>filter(Predicate<?superT>predicate)
    Enthält das Optional einen Wert und ist das Prädikat predicate auf dem Wert wahr, ist die Rückgabe das eigene Optional (also this), sonst ist die Rückgabe empty().
  • <U>Optional<U>map(Function<?superT,?extendsU>mapper)
    Repräsentiert das Optional einen Wert, dann wende die Funktion an und verpacke das Ergebnis (wenn es ungleich null ist) wieder in ein Optional. Ist das Optional ohne Wert, dann ist die Rückgabe empty(), genauso, wenn die Funktion null liefert.
  • <U>Optional<U>flatMap(Function<?superT,Optional<U>>mapper)
    Wie map(…), nur dass die Funktion ein Optional statt eines direkten Werts gibt. Liefert die Funktion mapper ein leeres Optional, so ist das Ergebnis von flatMap(…) auch empty().
  • Optional<T> or(Supplier<? extends Optional<? extends T>> supplier)
    Repräsentiert das Optional einen Wert, so liefere ihn. Ist das Optional leer, beziehe den Wert aus dem anderen Optional. Neu in Java 9.
  • TorElseGet(Supplier<?extendsT>other)
    Repräsentiert das Optional einen Wert, so liefere ihn; ist das Optional leer, so beziehe den Alternativwert aus dem Supplier.
  • <XextendsThrowable>TorElseThrow(Supplier<?extendsX>exceptionSupplier)
    Repräsentiert das Optional einen Wert, so liefere ihn, andernfalls hole mit Supplier das Ausnahme-Objekt, und löse es aus.

Beispiel: Wenn das Optional keinen Wert hat, soll eine NullPointerException statt der NoSuchElementException ausgelöst werden.

String s = optionalString.orElseThrow( NullPointerException::new );

Beispiel für NullPointerException-sichere Kaskadierung von Aufrufen mit Optional

Die beiden XXXmap(…)-Methoden sind besonders interessant und 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() wäre):

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 einen Lambda-Ausdruck nehmen 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.toUpperCase() );        
   } );
 } );

Wenn wir die lokalen Variablen networkInterface und displayName entfernen, landen wir bei:

Optional.ofNullable( NetworkInterface.getByIndex( 2 ) ).ifPresent( ni -> {
   Optional.ofNullable( ni.getDisplayName() ).ifPresent( name -> {
     System.out.println( name.toUpperCase() );
   } );
 } );

Von der Struktur her ist das mit der if-Abfrage identisch und über die Einrückungen auch zu erkennen. Fallunterscheidungen mit Optional und ifPresent(…) umzuschreiben bringt 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 drei Abbildungen und müssten ausdrücken können: Wenn eine Abbildung fehlschlägt, dann höre mit der Abbildung auf. Und genau hier kommen 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: Erstens 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. Am Ende der Kette steht nicht mehr String (wie am Anfang des Beispiels), sondern Optional<String>.

Umgeschrieben mit Methodenreferenzen und weiter verkürzt ist der Code sehr gut lesbar und Null-Pointer-Exception-sicher:

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.

Primitiv-Optionales mit

Die eigentliche Optional-Klasse ist generisch und kapselt jeden Referenztyp. Auch für die primitiven Typen int, long und double gibt es in drei speziellen Klassen OptionalInt, OptionalLong, OptionalDouble Methoden zur funktionalen Programmierung. Stellen wir die Methoden der vier OptionalXXX-Klassen gegenüber:

Optional<T> OptionalInt OptionalLong OptionalDouble
static <T>
Optional<T>
empty()
static
OptionalInt
empty()
static
OptionalLong
empty()
static
OptionalDouble
empty()
T get() int getAsInt() long getAsLong() double
getAsDouble()
boolean isPresent()
static <T>
Optional<T> of(T value)
static OptionalInt
of(int value)
static OptionalLong
of(long value)
static Optional
Double
of(double value)
static <T>
Optional<T>
ofNullable(T value)
nicht übertragbar
T orElse(T other) int orElse(int other) long orElse(long other) double orElse(double other)
Stream<T> stream() IntStream stream() LongStream stream() DoubleStream stream()
boolean equals(Object obj)
int hashCode()
String toString()
void ifPresent(
Consumer<? super T> consumer)
void ifPresent(
IntConsumer
consumer)
void ifPresent(
LongConsumer
consumer)
void ifPresent(
DoubleConsumer
consumer)
void ifPresentOrElse( Consumer<? super T> action, Runnable emptyAction) void ifPresentOrElse( IntConsumer action, Runnable emptyAction) void ifPresentOrElse( LongConsumer action, Runnable emptyAction) void ifPresentOrElse( DoubleConsumer action, Runnable emptyAction)
T orElseGet( Supplier<? extends T> other) int orElseGet( IntSupplier other) long orElseGet( LongSupplier other) double orElseGet( DoubleSupplier other)
<X extends Throwable> T orElseThrow( Supplier<? extends X> exceptionSupplier) <X extends Throwable> int orElseThrow( Supplier<? extends X> exceptionSupplier) <X extends Throwable> long orElseThrow( Supplier<? extends X> exceptionSupplier) <X extends Throwable> double
orElseThrow( Supplier<? extends X>
exceptionSupplier)
Optional<T> filter(Predicate<? super T> predicate) nicht vorhanden
<U> Optional<U>
flatMap(Function
<? super T,Optional<U>> mapper)
<U> Optional<U> map(Function<? super T,? extends U> mapper)

Tabelle 1.13: Vergleich von Optional mit den primitiven OptionalXXX-Klassen

[1]      null instanceof Typ ist immer false .

[2] Zum Glück wird null selten als Fehler-Identifikator genutzt, die Zeiten sind vorbei. Hier sind Ausnahmen die bessere Wahl, denn Fehler sind Ausnahmen im Programm.

[3]      Er sagt dazu: „It was the invention of the null reference in 1965. At that time, I was designing the first comprehensive type system for references in an object oriented language (ALGOL W). My goal was to ensure that all use of references should be absolutely safe, with checking performed automatically by the compiler. But I couldn’t resist the temptation to put in a null reference, simply because it was so easy to implement. This has led to innumerable errors, vulnerabilities, and system crashes, which have probably caused a billion dollars of pain and damage in the last forty years.“ Unter http://www.infoq.com/presentations/Null-References-The-Billion-Dollar-Mistake-Tony-Hoare gibt es ein Video mit ihm und Erklärungen.

[4] Unter http://docs.oracle.com/javase/8/docs/api/constant-values.html lassen sich alle Konstantendeklarationen einsehen.

Konstruktorreferenzen

Um ein Objekt aufzubauen, nutzen wir das Schlüsselwort new. Das führt zum Aufruf eines Konstruktors, dem sich optional Argumente übergeben lassen. Die Java-API deklariert aber auch Typen, von denen sich keine direkten Exemplare mit new aufbauen lassen. Stattdessen gibt es Erzeuger, deren Aufgabe es ist, Objekte aufzubauen. Die Erzeuger können statische oder auch nichtstatische Methoden sein:

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

Tabelle 1.4: Beispiele für Konstruktoren und Erzeuger-Methoden

Beide, Konstruktoren und Erzeuger, lassen sich als spezielle Funktionen sehen, die von einem Typ in einen 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 Methodenreferenz 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 Methodenreferenzen statische Methoden und Objektmethoden angeben können, bieten Konstruktorreferenzen die Möglichkeit, Konstruktoren anzugeben, sodass diese als Erzeuger an anderer Stelle übergeben werden können. Damit lassen sich elegant Konstruktoren als Erzeuger angeben, und zwar auch von einer Klasse, die nicht über Erzeugermethoden verfügt. Wie auch bei Methodenreferenzen spielt eine funktionale Schnittstelle eine entscheidende Rolle, doch dieses Mal ist es die Methode der funktionalen Schnittstelle, die mit ihrem Aufruf zum Konstruktoraufruf führt. Wo syntaktisch bei Methodenreferenzen rechts vom Doppelpunkt ein Methodenname steht, ist dies bei Konstruktorreferenzen ein new.[1] Also ergibt sich alternativ zu

      .map( Integer::parseInt )       // Methode Integer.parseInt(String)

in unserem Beispiel das Ergebnis mittels:

      .map( Integer::new )            // Konstruktor Integer(String)

Mit der Konstruktorreferenz gibt es vier Möglichkeiten, funktionale Schnittstellen zu implementieren; die drei verbleibenden Varianten sind Lambda-Ausdrücke, Methodenreferenzen und klassische Implementierung über eine Klasse.

Beispiel: Die funktionale Schnittstelle sei:

interface DateFactory { Date create(); }

Die folgende Konstruktorreferenz bindet den Konstruktor an die Methode create() der funktionalen Schnittstelle:

DateFactory factory = Date::new;
 System.out.print( factory.create() ); // zum Beispiel Sat Dec 29 09:56:35 CET 2012

Beziehungsweise 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 zusammenpassen. Das gilt für den Typ DateFactory aus unserem Beispiel. 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 Typangabe dazu verwendet werden, dass über den Constructor der Class mit der Methode newInstance() Exemplare gebilder werden. Der Einsatz von Class lässt sich durch eine funktionale Schnittstelle ersetzen, und Konstruktorreferenzen lassen sich an Stelle 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);
}
Konstruktorreferenz DateFactory factory =
Date::new;
DateFactory factory =
Date::new;
Aufruf factory.create(); factory.create(1);

Tabelle 1.5: Standard- und parametrisierter Konstruktor mit korrespondierenden funktionalen Schnittstellen

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

Nützliche vordefinierte Schnittstellen für Konstruktorreferenzen

Die für einen Standard-Konstruktor passende funktionale Schnittstelle muss eine Rückgabe besitzen und keinen Parameter annehmen; die funktionale Schnittstelle für einen 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 ist es vorteilhaft, dass für diese beiden Fälle die Java-API zwei praktische (generisch 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 einfacher parametrisierter Konstruktor

Tabelle 1.6: Vorhandene funktionale Schnittstellen als Erzeuger

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 *

Besonders interessant werden die Konstruktorreferenzen mit den neuen Bibliotheksmethoden der Stream-API. Nehmen wir eine Liste vom Typ Zeitstempel an. Der Konstruktor Date(long) nimmt einen solchen Zeitstempel entgegen, 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

Die Konstruktorreferenz Date::new hilft dabei, das long mit dem Zeitstempel in ein Date-Objekt zu konvertieren.

Denksportaufgabe: Ein Konstruktor kann als Supplier oder Function gelten. Problematisch sind mal wieder geprüfte Ausnahmen. Der Leser soll überlegen, ob der Konstruktor URI(String str) throws URISyntaxException über URI::new angesprochen werden kann.

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