Java 9: de puzzelstukjes vallen op hun plaats!

Onder deze release van Java, met ‘jigsaw’ als het hete hangijzer, gaat veel meer schuil dan alleen modularisatie. Oracle brengt ons op donderdag 21 september 2017 de nieuwste release van Java: Java 9. Iets meer dan drie jaar na Java 8. Er was het nodige politieke gesteggel, maar het resultaat is er!

Hedzer Westra

In dit artikel nu eens niet de focus op het controversiële en veelbesproken Java Platform Module System (JPMS), in de volksmond ‘jigsaw’. Hieronder lees je over de andere features die Java 9 brengt. Althans, een deel daarvan. De volgende versie van Java omvat meer dan 90 Java Enhancement Proposals (JEP’s). Te veel om hier te behandelen. De nuttigste en leukste komen aan bod in deze en de volgende editie van het Java Magazine.

In dit nummer: JShell, Milling Project Coin, Multi-Release JARs en Factory methods voor Collections.

In de volgende editie gaan we verder. Dan gaat het onder meer over de Process API, CompletableFuture, Project Verona, Security, Performance en Flow reactive streams.

 

Project Kulla: JShell

Java ontbeerde na al die jaren nog steeds een REPL (Read-Evaluate-Print-Loop). Elke zichzelf respecterende taal heeft er één. Java 9 levert nu ook een out of the box oplossing om via de command line bijvoorbeeld snel kleine stukjes code uit te proberen. Voorheen moest je uitwijken naar bijvoorbeeld BeanShell of je complete IDE opstarten, al dan niet in debug mode.

Om de shell te starten voer je het commando ‘jshell’ uit. Je kunt meteen java code beginnen te tikken. Een afsluitende puntkomma is niet nodig. Er is tab completion support. Enkele nuttige instructies zijn /help, /imports, /vars, /methods, /save, /open, /edit, /env en /exit.

 

Figuur 1: een eenvoudige sessie in jshell.

De herkomst van de naam Kulla is overigens wat schimmig. De link naar de Sumerische god van steen, of een draagstijl van tulbands, is niet eenduidig te leggen…

 

Milling Project Coin

In deze JEP worden “five small amendments” toegevoegd aan Java 7’s Project Coin. In dit artikel de eerste drie.

De kleinste is wel dat het underscore (‘_’) karakter als keyword wordt gezien – code die het als variabelenaam gebruikt zal niet meer compileren. In Java 8 leidde dit al tot een compiler warning.

Daarnaast is de diamond operator onder voorwaarden nu toegestaan in anonymous inner classes.

 

 

Java 8:

<T> Package<T> createPackage(T packageContent) {

        // type vereist: ‘Non-denotable type‘

        return new Package<T>(packageContent) { … };

}

Java 9:

<T> Package<T> createPackage(T packageContent) {

        // denotable type T wordt afgeleid!

        return new Package<>(packageContent) { … };

}

 

Package<?> createPackage(Object content) {

        List<?> innerList = Arrays.asList(content);

        // helaas geen diamond – List<?> is ‘non-denotable’:

        return new Package<List<?>>(innerList) { };

}

 

De derde is een kleine verbetering aan try-with-resources: het is niet meer nodig om een expliciete variabele aan te maken voor de AutoCloseable. Voorwaarde is wel dat de gebruikte variabele (effectief) final is.

Java 7 code:

BufferedReader reader1 = …

try (BufferedReader reader2 = reader1) { .. }

wordt:

BufferedReader reader1 = …

try (reader1) { .. }

 

Factory Methods voor collecties

Op het vlak van de JDK API is één klein juweeltje te vinden. Het gaat om Factory Methods voor immutable collecties. Hiermee zijn eenvoudig gevulde instanties van List, Set & Map te creeëren.

Enkele implementaties zijn overwogen, inclusief nieuwe syntax. Uiteindelijk is gekozen voor een pure library-oplossing, waarbij varargs en overloads uitgebreid ingezet zijn om met name het geheugengebruik zo efficiënt mogelijk te maken.

 

 

List<String> emptyList = List.of();

List<String> list = List.of(“foo”, “bar”);

Map<String, String> map = Map.of(

  “key1”, “value1”,

  “key2”, “value2”);

Map<String, String> mapOfEntries =

   Map.ofEntries( Map.entry(“key1”, “value1”),

                  Map.entry(“key2”, “value2”) );

 

Concurrency

Op het vlak van concurrency is Java altijd behoorlijk sterk geweest. Sinds Java 5 is er in elke release iets fundamenteels toegevoegd. Java 9 bouwt voort op die lijn. Naast enkele verfijningen van bestaande classes, is er nu ook een Reactive Streams implementatie.

 

Optional

Optional heeft drie nieuwe methods: stream() (levert een Stream met 0 of 1 elementen), ifPresentOrElse() (accepteert twee lambda’s – één voor elk van de twee Optional toestanden; is vergelijkbaar met de ternaire operator) en or()(levert de eigen waarde, of de door de parameter geleverde wanneer de Optional geen waarde bevat).

 

myStreamOfOptionals().flatMap(Optional::stream)

Optional.of(42).ifPresentOrElse(

   i -> System.out.println(“number is ” + i),

  () -> System.out.println(“nothing present”))

someOptional.or(“fallback value”)

 

Collectors

Ook Collectors is uitgebreid. Er zijn twee nieuwe methods: filtering en flatMapping. Dit zijn varianten van filter respectievelijk mapping om samen met groupingBy te gebruiken. flatMapping is een efficiënte combinatie van mapping en flatMap.

 

 

List<Integer> numbers = List.of(1, 2, 3, 5, 5);

result = numbers.stream().collect(

  Collectors.groupingBy(key -> key,

    Collectors.filtering(key -> key > 3,

      Collectors.counting())));

result.forEach((key, val) ->

  System.out.println(key + “: #” + val));

// uitvoer: 1: #0, 2: #0, 3: #0, 5: #2

 

Stream

Stream is aangevuld met vier nieuwe methods: takeWhile, dropWhile, iterate en ofNullable.

iterate is feitelijk een Stream-en-lambda versie van de aloude for-loop! Er bestond al een infinite versie: Stream.iterate(1, i->i+2)

 

 

Stream.of(1, 4, 2, -8, 6, 3).takeWhile(i -> i > 0)

  .forEach(System.out::println)                  // 1, 4, 2

Stream.of(1, 4, 2, -8, 6, 3).dropWhile(i -> i > 0)

  .forEach(System.out::println)                  // -8, 6, 3

Stream.iterate(1, i -> i < 20, i -> i + 2)

  .forEach(System.out::println)                 // 1, 3, 5, …, 19

Stream<String> comments = Stream.ofNullable(post)

  .flatMap(bpost -> bpost.getComments().stream())

 

HTTP/2

In Java Magazine #04-2016 schreef ik over HTTP/2: “In JSE 9 zitten twee JEP’s die de JDK HTTP/2-compatible gaan maken.” Het heeft niet zo mogen zijn: JEP-110 is incubated hetgeen zoveel betekent als dat de HTTP/2 client (met instabiele API) alleen via een omweg beschikbaar is. Tip: beter niet gebruiken in productiecode.

 

MRJAR

Een nieuw concept is de Multi-Release JAR, ook wel MRJAR. Dit maakt backward & forward compatibiliteit eenvoudiger. Met name voor libraries & frameworks is dit een interessante uitbreiding.

Het idee is als volgt: in de standaard JAR directorystructuur staan class files in de laagste te supporten java versie. Daarnaast past de JVM vanaf Java 9 alle eventuele class files uit directory /versions/<JVM versie> toe als overlay. Zie figuur 2. Classes onder bijvoorbeeld versions/9 overschrijven simpelweg de standaard directorystructuur, als deze JAR geladen wordt op een Java 9 VM. Om dit mechanisme te activeren moet een extra entry in de MANIFEST.MF staan: Multi-Release: true.

 

 

my-jar.jar/

  A.class               # wordt gebruikt door Java 8 VM en ouder

  B.class

  C.class

  META-INF

  versions/

    9/                  # verwijst naar java.version

       A.class          # vervangt ‘base’ A.class in Java 9 VM

    10/

       B.class          # vervangt ‘base’ B.class in Java 10 VM

 

Het is niet mogelijk om classes toe te voegen in directory versions; alleen overschrijven kan. Daarnaast werkt dit natuurlijk alleen vanaf Java 9 – releases daarvoor negeren de versions directory.

Hiermee wordt het mogelijk om gebruik te maken van nieuwe taalfeatures en JDK classes in de overlay classes. Voorheen moest dit met Reflection worden opgelost.

Voor libraries geldt dat momenteel ze een minimum JDK versie vereisen, die alleen in documentatie terug te vinden is. Als voorbeeld: Guava 1.0-11.0 eist Java 5+, 12.0-20.0 Java 6+ en 21.0 Java 8+. Ook worden er wel op verschillende artifactIds meerdere builds gepubliceerd. Voorbeelden zijn tika vs. tika-java7 en google-oauth-client-java6 vs. google-oauth-client-java7. Dergelijke gedwongen upgrades en truuks zijn met een Multi-Release JAR niet meer nodig.

Hoe deze feature in de praktijk opgepikt gaat worden is nog even de vraag. Zo zijn er uitdagingen qua regressietesten en security. Daarnaast is nog onduidelijk hoe IDE’s en build tools hiermee omgaan. De crux is: welke source file structuur ‘past’ hierop, en hoe combineer je die met de configuratie van de java release versie(s) van je projectmodule?

 

Migratie

Om je alvast op weg te helpen om je projecten te migreren, volgen hier enkele aanknopingspunten.

Qua IDE support zit je bij IntelliJ (2017.1.3+) goed. Eclipse Oxygen, en NetBeans 8 nightly builds bieden ondersteuning na installatie van extra plugins.

Gebruik je Maven, dan is versie 3.0 (uit 2010!) in principe al genoeg. Overigens is versie 3.3.x wel aan te raden. Daarnaast heb je een upgrade nodig van o.a. de compiler & war plugins. Wil je cross-compilen om tijdelijk naar zowel Java 8 als 9 te compilen, kijk dan naar de toolchain-plugin, animal-sniffer-plugin of maven.compiler.release property.

Update zo nodig dependencies & tools, zoals mocking frameworks, aspect weavers en code generators. Deze zijn namelijk extra gevoelig voor het nieuwe module system. Ze leunen hevig op reflectie en code generatie, waarbij vaak interne JDK classes betrokken zijn. Dat is na de modularisatie van de JDK niet altijd meer mogelijk.

Ga je modularisatie in je eigen project toepassen, maak dan geen module-info.java in src/test/java; alleen voor src/main/java.

Commando mvn dependency:list helpt bij het vaststellen van de module names die je nodig hebt voor module-info.java. Commando mvn jdeps:jdkinternals toont eventuele (illegale!) afhankelijkheden op JDK internals.

Laatste tip: voeg de enforcer plugin toe met extra enforcer rule banDuplicateClasses. Deze detecteert classes die in meerdere artifacts voorkomen – iets waar het module systeem niet van gediend is.

Meer informatie vind je in OpenJDK 9 Outreach referentie.

 

Upgraden?

Enkele argumenten om jou – of je baas, danwel opdrachtgever – over de streep te helpen om te upgraden:

  1. JShell: live coding!
  2. Verbeterde performance & security.
  3. Bijblijven met latest-and-greatest: Java 7 werd end-of-life in april 2015, Java 8 al per september 2018.
  4. Nieuwe API features, o.a. Collections Factory methods en de Process API.
  5. Nog betere ondersteuning voor concurrency & reactive apps.
  6. Mocht je JavaFx of JavaScript gebruiken: ook daar is veel in verbeterd.

 

Conclusie

Met Java 9 is er weer een mooie update gekomen van onze favoriete programmeertaal. Enkele highlights zijn hierboven uitgelicht; er is nog meer moois te vinden. In de volgende editie meer hierover! Bij de referenties vind je uitgebreidere informatie en documentatie.

 

Referenties

 

Bronvermelding

Enkele codevoorbeelden zijn afkomstig van Ordina België JWorks’ Java 9 workshop van Yannick De Turck.