Garbage Collection

Garbage Collection und Java Virtual Machine

Alle erzeugten Objekte werden im Heap-Speicher der JVM (Java Virtual Machine) abgelegt. Da aber dieser Speicherbereich nicht unendlich groß ist, müssen die nicht mehr benötigten Objekte wieder entfernt werden. Diese Aufgabe könnte uns als Entwickler obliegen. Doch wir haben Glück: Java kennt eine automatische Speicherbereinigung, die Garbage Collection (Übersetzt: Müllabfuhr). Alle nicht referenzierten Objekte werden aus dem Heap-Speicher entfernt.
Trotzdem kann es durch unachtsame Programmierung zu Speicherlecks (Memory Leaks) kommen. Wenn eine nicht mehr benötigte Referenz noch ein Objekt, dies kann auch eine sehr große Collection sein, referenziert, wird dieses Objekt eben nicht aus dem Speicher entfernt.

Beispiel

Anhand eines kleinen Beispiels möchte ich die Garbage Collection erläutern:

public class Person {

  private String name;

  private Person partner;

  public Person( String name ) { this.name = name; }

  public Person getPartner() { return partner;       }

  public void setPartner(Person partner) { this.partner = partner; }

  public static void main(String[] args) {

    Person [ ] personen = { new Person( "Margit"), new Person("Hans" ) };

  }

}

Anschließend wollen wir unserer Person Margit einen Partner zuweisen.

public static void main(String[] args) {

  Person [ ] personen = { new Person("Margit"), new Person("Hans") };

 

  personen[0].setPartner( personen[1] );

}

Was passiert aber, wenn wir das Array an der Stelle 0 mit null initialisieren?

public static void main(String[] args) {

  Person [ ] personen = { new Person("Margit"), new Person("Hans") };

 

  personen[0].setPartner( personen[1] );

           

  personen[0] = null;

}

Arbeitsweise

Der Garbage Collector wird ausgehend von unserer Klasse Person auf dem Stack die main-Methode untersuchen. Welche Objekte können von hier erreicht werden? Über die lokale Variable personen wird das Array erreicht. An der Stelle 1 im Array wird die Person Hans referenziert und somit werden weder das Array noch die Person Hans aus dem Heap-Speicher entfernt. Der Speicher der Person Margit ist ohne Referenzierung und wird somit freigegeben.

Wie sieht es im nächsten Fall aus?

public static void main(String[] args) {

  Person [ ] personen = { new Person("Margit"), new Person("Hans") };

  personen[0].setPartner( personen[1] );

  personen[1].setPartner( personen[0] );

}

Nun initialisieren wir die lokale Variable personen mit null.

public static void main(String[] args) {

  Person [ ] personen = { new Person("Margit"), new Person("Hans") };

  personen[0].setPartner( personen[1] );

  personen[1].setPartner( personen[0] );

            

  personen = null;

}

Jetzt sieht die Sache etwas anders aus. Der Garbage Collector beginnt in der main-Methode und findet die null-Referenz in der lokalen Variablen personen, Somit ist von hier kein Objekt erreichbar. Obwohl Margit und auch Hans jeweils referenziert sind, werden sie aus dem Heap-Speicher entfernt, da sie aus der main-Methode unerreichbar sind.

 

Im nächsten Beispiel sehen wir eine zusätzliche lokale Variable misterX. Wenn nun die lokale Variable personen mit null initialisiert wird, entfernt der Garbage Collector nur das Array, nicht aber die Personen Margit und Hans, da es noch eine Referenz misterX auf die Person Margit gibt.

In der Praxis sind dies oft nicht mehr benötigte oder vergessene Referenzen, so dass die Garbage Collection diese Objekte im Speicher belässt. Man spricht hier von Memory Leaks oder Speicherlecks. Dies könnte man hier durch Dereferenzieren der lokalen Variablen misterX leicht verhindern.

public static void main(String[] args) {

  Person [ ] personen = { new Person("Margit") , new Person("Hans") };

  personen[0].setPartner( personen[1] );

  personen[1].setPartner( personen[0] );

            

  Person misterX = personen[ 0 ].getPartner( );

            

  personen = null;

}

Bevor der Garbage Collector ein Objekt aus dem Speicher entfernt, wird immer noch die Methode finalize( ) durchlaufen. Diese ist in der Klasse Object implementiert und kann überschrieben werden. Da der Entwickler aber weder weiß wann die Garbage Collection läuft, noch ob sie überhaupt läuft, ist diese Methode mit Vorsicht zu genießen. Seit JAVA 9 ist sie deprecated gekennzeichnet, weil viele Programmierer sie fehlerhaft eingesetzt haben.

Zusammenfassung

Dies war eine kurze einfache Darstellung der Garbage Collection. Alle nicht mehr erreichbaren Objekte ausgehend von der Root-Klasse und der main werden automatisch in parallelen Threads aus dem Heap-Speicher entfernt. Der Zeitpunkt ist für uns Entwickler nicht vorhersehbar.

In unserem Seminar Java Erweiterungen II (03323) wird das Thema mit schwachen (Weak Reference) und starken Referenzen vertieft.

 

Seminar zum Thema

 

Bildnachweis: Photo by Melodi2 at Morguefile.com

Weiterlesen

Referenzen und Objekte

Objekt-orientierte Programmierung ist bekanntermaßen ein sehr verbreitetes Konzept zur Modellierung komplexer Anwendungen. Die Programmiersprache Java hat bereits seit ihrer Einführung 1995 dieses Konzept aufgenommen. Auch die Laufzeitumgebung, also die Java Virtual Machine (JVM) berücksichtigt dieses Konzept und implementiert deshalb ein Speichermodell, das auf Referenzen und Objekte ausgerichtet ist.

Stack und Heap

Der Speicher der Java Virtual Machine ist in einen Stack- und einen Heap-Bereich unterteilt.

Stack und Heap der JVM

Diese Unterteilung ist zwar etwas grob, genügt aber für die folgende Betrachtung völlig.

Jede Java-Methode bekommt ihren eigenen Stack-Bereich zugeordnet. Innerhalb des Stacks werden sämtliche lokalen Variablen abgelegt. Wie im Objekt-orientierten notwendig sind diese Variablen in Java stets Referenzen auf Objekte. Diese werden im Heap-Bereich abgelegt.

Dies sei am Beispiel der folgenden main-Methode aufgezeigt:

public static void main(String[] args){
String message = "Hello";
String[] names = {"John", "Fritz"};
}

Im Speicher der JVM ergibt sich das folgende Bild:

Die main-Methode mit den lokalen Variablen message und names

Methodenaufruf

Nun definieren wir eine zweite Methode, called, diesmal mit 2 Parametern

public static void called(String str, String[] list){

}

Die methoden called mit den beiden Parametern str und list

Diese Methode wird aus main heraus aufgerufen:

public static void main(String[] args){
String message = "Hello";
String[] names = {"John", "Fritz"};
called(message, names);
}

Wie erfolgt nun jedoch die Übergabe der lokalen Variablen message und names in die Parameter str und list?

Wie werden die Parameter belegt?

Dazu wird die JVM die Werte der Referenzen als Kopie übergeben, also ein “CallByValue”-Verhalten auf der Ebene der Referenzen:

Copy By Value mit Referenzen

Im Endeffekt zeigen die Referenzen auf die selben Objekte im Heap.

Zuweisung von Werten

Was passiert aber nun, wenn in der aufgerufenen Methode den Parametern neue Werte zugewiesen werden? Hier ergibt sich ein feiner aber unglaublich wichtiger Unterschied: Belegen wir den Parameter str einfach mit einem neuen Wert, z.B. mit

str = "Goodbye";

so wird die Variable im Stack einfach mit einer Referenz auf das neue Objekt im Heap überschrieben:

Zuweisung eines neuen Wertes an den Parameter str

Wichtig hier ist, dass in der aufrufenden main-Methode die Variable message davon völlig unbeeinflusst bleibt.

Was passiert aber, wenn wir die Referenz auf das Array benutzen, um beispielsweise den Wert des zweiten Elements mit Index 1 auf “Paul” zu setzen?

list[1] ="Paul";

Nun ergibt sich ein anderes Bild:

Zuweisung eines neuen Wertes an das zweite Element der Liste

Hier ist klar ersichtlich, dass diese Änderung des Array-Objekts innerhalb der Methode called auch direkt Auswirkungen auf die aufrufende main-Methode hat: Nachdem die Methode called terminiert ist referenziert auch die Variable message auf das Array, dessen zweites Element nun auf “Paul” zeigt.

Also innerhalb von main

public static void main(String[] args){
String message = "Hello";
String[] names = {"John", "Fritz"};
called(message, names);
System.out.println(names[1]); //-> Paul
}

Zusammenfassung und Ausblick

Durch die Betrachtung des Speichermodells der Java Virtual Machine wird die Arbeitsweise eines Objekt-orientierten Programms unter Verwendung von Referenzen klar und verständlich. Das komplette Beispiel liegt als fertiges Eclipse-Projekt vor und enthält zusätzlich Konsolen-Ausgaben, die den Ablauf des Programms demonstrieren. Selbstverständlich kann aber auch der Eclipse-Debugger benutzt werden, um die Ausführung nachzuvollziehen.

Allerdings sind sicherlich auch noch einige Punkte nicht fundiert behandelt worden:

  • Das vorgestellte Speichermodell ist stark vereinfacht. Wie schaut es denn tatsächlich aus?
  • Was passiert eigentlich mit dem armen Objekt “Fritz”, das nun komplett losgelöst im Heap vorhanden ist?

Diese beiden Punkte werden in einem folgenden Artikel, der den Garbage Collector der JVM als Thema hat, behandelt.


Seminar zum Thema

Weiterlesen

Spring Boot

Was ist Spring Boot?

Mit Spring Boot hat die Spring Community eine echte Vereinfachung bei der Erstellung Spring-basierter Anwendung erreicht. Damit können mit wenigen Programmzeilen Prototypen selbst komplexer Anwendungen erstellt werden, die anschließend zum fertigen Produkt konfiguriert werden können. Der Funktionsumfang von Spring Boot umfasst:

  • Build-Management
    • Ein Spring Boot-Projekt kann mit einer simplen Build-Definition gebaut werden.
  • Projekt-Generator
    • Zur Neuanlage eines Projektes kann die Build-Konfiguration mit einem Online-Dienst der Spring-Community erstellt werden.
  • Convention over Configuration und Autoconfiguration
    • Für Rapid Prototyping sind keine Konfigurationsdateien oder Ähnliches vonnöten. Die Anwendung ist nach der Erstellung sofort lauffähig. Dies gilt auch dann, wenn beispielsweise für eine Web-Anwendung ein Server benötigt wird: Die Autoconfiguration startet einfach einen Tomcat auf Port 8080.
  • Externe Konfiguration
    • Für die ausgelieferte Programmversion muss selbstverständlich eine explizite Konfiguration erfolgen, die Autoconfiguration ist hierfür bestenfalls bedingt geeignet. Dazu bietet Spring Boot einen einfachen Mechanismus über Properties- oder YAML-Dateien. Eine Annotation-basierte Konfiguration ist natürlich auch möglich.
  • Monitoring der Runtime
    • Die in Produktion laufende Anwendung wird prnzipiell wie alle Java-Anwendungen über JMX überwacht. Mit dem Spring Actuator werden zusätzliche Metriken hinzugefügt, die via REST-Schnittstelle abgegriffen werden können.

Die einzelnen Funktionen werden im Folgenden detaillierter beschrieben.

Features

 Build Management

Der Build-Prozess einer Spring Boot-Anwendung wird über Gradle oder über Maven definiert. Die Grundideen sind in beiden Systemen die selben und werden hier an einem Maven-Projekt beschrieben:

Als erstes stellt Spring Boot eine Parent-POM zur Verfügung, in der alle (!) möglichen Spring-Dependencies einer Applikation abstrakt deklariert sind:

	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>2.0.3.RELEASE</version>
		<relativePath />
	</parent>

Ein konkretes POM eines Projekts definiert als Dependencies die Teile des Frameworks, die benutzt werden sollen durch “Starter”. So wird im folgenden Beispiel ein Projekt mit der Spring Core-Komponente sowie der Test-Umgebung definiert:

 

	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
		</dependency>
		<dependency>
			<groupId>junit</groupId>
			<artifactId>junit</artifactId>
			<scope>test</scope>
		</dependency>
	</dependencies>

Projekt Generator Spring Initializr

Mit Hilfe des online verfügbaren Projekt-Generators Spring Initializr wird ein POM für ein Projekt sowie eine simple Rahmenanwendung generiert. Die notwendigen Starter können durch eine einfache Oberfläche hinzugefügt werden. Das fertige Projekt wird als Archiv geladen werden.

Convention over Configuration und Autoconfiguration

Eine Spring Boot-Anwendung wird über eine zentrale Einstiegsklasse definiert, die mit @SpringBootApplication annotiert ist.  Damit sind die folgenden Features automatisch aktiviert:

  • Diese Klasse ist automatisch eine @Configuration
  • ComponentScan für alle Unterpakete, so dass automatisch alle @Components gefunden werden
  • Automatisches Laden der application.properties oder alternativ einer application.yaml zur Anwendungs-Konfiguration. Hierin werden Anwendungs-spezifische Konfigurationen definiert, aber auch die restliche Konfiguration (Logging, Ports, URLs…) werden hier zentral gepflegt.
  • Spring-Profile werden unterstützt. So werden Profile-abhängige Konfigurationen wie application-<profile>.properties automatisch erkannt und gemerged.

 

Zusätzlich wird eine Autokonfiguration angeboten, die einiges an Hintergrund-“Magie” durchführt. Dies sei an einem konkreten Beispiel demonstriert:

  • Die Anwendung benötigt eine Datenbank und deklariert somit in seinem POM den Starter für JDBC.
  • Zusätzlich wird ein Datenbanktreiber angegeben.
  • Jetzt startet die Autokonfiguration und erzeugt automatisch eine DataSource. Dazu wird untersucht, ob der angegebene  Datenbank-Treiber eine Embedded-Konfiguration ermöglicht. Hier sind aktuell H2, Derby und die HSQLDB unterstützt.

Diese Autokonfiguration kann aber beispielsweise auch einen kompletten Jetty-Server konfigurieren und starten. Dazu ist nur der Web-Starter zu inkludieren.

Externe Konfiguration

Die eben besprochene Autoconfiguration ist für Prototyping sehr praktisch. Für reale Anwendungen muss diese jedoch durch explizite Konfiguration ersetzt werden, also beispielsweise den Port des Jetty-Servers oder eine DataSource. Dazu bietet Spring Boot

  • die application.properties
    • spring.datasource.user=Hugo
  • Aufruf-Parameter der Anwendung
    • -Dspring.datasource.user=Hugo
  • Falls zumindest eine DataSource definiert ist wird die Autokonfiguration nicht durchgeführt.

Monitoring

Der Spring-Actuator stellt umfangreiche Informationen via JMX oder über eine REST-Schnittstelle zur Verfügung. Darin werden nicht nur die aktuellen Metriken des Java-Prozesses angezeigt sondern auch beispielsweise das Geflecht der Spring-Beans.

Das Hinzufügen des Actuators erfolgt wie bei Spring Boot üblich durch das Hinzufügen des Starters zum POM. Welche Endpoints aktiviert werden wird in der application.properties definiert.

Ebenso einfach können die Metriken über Jolokia abgegriffen werden.

Fazit

Spring Boot wird zwar als Ergänzung zum Spring-Framework angeboten, ist nach meiner Auffassung aber so überzeugend, dass selbst simple Projekte damit realisiert werden sollten. Selbst wenn “nur” der Build-Prozess mit Parent Startern benutzt wird, ist der Mehrwert den Aufwand der Einarbeitung in jedem Falle wert. Dies beschreibt ein anderer Artikel zu Spring Boot, darin wird ein RESTful Web Service mit Datenbank-Anbindung programmiert. Die Datenbank-Anbindung mit Spring Data ist Thema eines weiteren Artikels.


Seminar zum Thema

Weiterlesen

Einwickeln von Exceptions

Über das Einwickeln von Exceptions

Das Leben wäre schöner, gäbe es nur RuntimeExceptions – leider aber gibt’s auch Exceptions.

Eine übliche Lösung dieses Problems besteht bekanntlich darin, checked Exceptions in RuntimeExceptions einzuwickeln – und statt einer Exception dann diese RuntimeException zu werfen.

JPA z.B. wickelt alle SQLExceptions in JPA-Exceptions ein – und letztere sind RuntimeExceptions. Und auch das Springframework verwendet dieselbe Technik.

Dieses Einwickeln ist aber immer nervtötend. Betrachten wird folgendes Code-Fragment:

 

final Thread t = new Thread() {
    @Override 
    public void run() {
        System.out.println("Thread starts...");
        System.out.println("Simulating hard work...");
        try {
            Thread.sleep(1000);
        } 
        catch (final InterruptedException e) {
            throw new RuntimeException(e);
        }
        System.out.println("Thread terminates...");
    }
};
t.start();
System.out.println("waiting...");
try {
    t.join();
} 
catch (final InterruptedException e) {
    throw new RuntimeException(e);
}
System.out.println("go on...")

(Man beachte, dass die run-Methode der Thread-Klasse keine checked-Exception werfen darf – wir müssen also einwickeln.)

Das Einwickeln erfordert immer sechs Zeilen.

Für die folgende Diskussion verwenden wir zwei Demo-Methoden, die jeweils eine checked Exception werfen können – eine Methode foo, die nichts zurückliefert, und eine bar-Methode, die ein int–Resultat liefert:

static void foo(final int x) throws Exception {
    if (x <= 0)
        throw new Exception("argument must be positive");
    System.out.println(x);
}
static int bar(final int x) throws Exception {
    if (x >= 0) 
        throw new Exception("argument must be negative");
    return -x;
}

Wir benutzen diese Methoden in einer Runnable– und einer Supplier-Implementierung. Bekanntlich darf weder die run-Methode von Runnable noch die get-Methode von Supplier eine checked-Exception werfen – wir wickeln diese Exception also in eine RuntimeException ein:

final Runnable runnable = () -> {
    try {
        foo(42);
    }
    catch (final Exception e) {
        throw new RuntimeException(e);
    }
};
runnable.run();
final Supplier<Integer> supplier = () -> { 
    try { 
        return bar(-42); 
    } 
    catch (final Exception e) { 
        throw new RuntimeException(e); 
    } 
}; 
System.out.println(supplier.get());

Nervtötend…

Wir entwickeln eine kleine Utilitiy-Klasse:

package jn.util;

public class TryCatch {

    public interface XRunnable {
        void run() throws Throwable;
    }

    public interface XSupplier<T> {
        T get() throws Throwable;
    }

    public static void wrapException(final XRunnable runnable) {
        try {
            runnable.run();
        }
        catch (final Throwable t) {
            throw new RuntimeException(t);
        }
     }

     public static <T> T wrapException(final XSupplier<T> supplier) {
         try {
             return supplier.get();
         }
         catch (final Throwable t) {
             throw new RuntimeException(t);
         }
     }
}

Die Klasse definiert zunächst “Exception”-verträgliche Versionen der beiden Standard-Interfaces Runnable und Supplier: die Interfaces XRunnable und XSupplier.

Sie definiert dann zwei statische wrapException-Methoden. Der ersten Methode wird ein XRunnable übergeben, der zweiten ein XSupplier. Die erste Methode ruft die run-Methode des XRunnables auf und wickelt eine Exception, die von dieser Methode geworfen werden kann, in eine RuntimeException ein. Die zweite Methode ruft die get-Methode des XSuppliers auf und liefert deren Resultat als eigenes Resultat zurück (und wickelt ein…)

Eine Anwendung dieser Methode kann zunächst einen statischen Import definieren:

import static jn.util.TryCatch.wrapException;

Die Thread-Anwendung, die oben vorgestellt wurde, kann nun wesentlich konziser formuliert werden:

final Thread t = new Thread() {
    @Override
    public void run() {
        System.out.println("Thread starts...");
        System.out.println("Simulating hard work...");
        wrapException(() -> Thread.sleep(1000));
        System.out.println("Thread terminates...");
    }
};
t.start();
System.out.println("waiting...");
wrapException(() -> t.join());
System.out.println("go on...");

Dasselbe gilt auch für das Runnable– und Supplier-Beispiel:

final Runnable runnable =
    () -> wrapException(() -> foo(42));
runnable.run();

final Supplier<Integer> supplier =
    () -> wrapException(() -> bar(-42));
System.out.println(supplier.get());

Bevor wir unsere Überlegungen weiterführen, ist zunächst eine kleine Refaktorierung angesagt. Wir können die erste wrapException-Methode auf die zweite wrapException-Methode abbilden:

public class TryCatch {

    // ...

    public static void wrapException(final XRunnable runnable) {
        wrapException(
             (XSupplier<Void>) () -> { runnable.run(); return null; });
    }

    public static <T> T wrapException(final XSupplier<T> supplier) {
        try {
            return supplier.get();
        }
        catch (final Throwable t) {
            throw new RuntimeException(t);
        }
    }
}

Exceptions sollten auch in spezifische RuntimeExceptions eingewickelt werden können. Wir erweitern unsere TryCatch-Klasse um zwei weitere wrapException-Methoden, denen zusätzlich zu dem XRunnable resp. dem XSupplier jeweils eine Function übergeben wird – ein exceptionWrapper, welchem ein Throwable übergeben wird und welcher dann eine RuntimeException liefern muss:

import java.util.function.Function;

public class TryCatch {

    // ...

    public static void wrapException(
            final XRunnable runnable,
            final Function<Throwable, RuntimeException> exceptionWrapper) {
        wrapException((XSupplier<Void>) () -> {
            runnable.run(); return null; }, exceptionWrapper);
    }

    public static <T> T wrapException(
            final XSupplier<T> supplier,
            final Function<Throwable, RuntimeException> exceptionWrapper) {
       try {
           return supplier.get();
       }
       catch (final Throwable t) {
           throw exceptionWrapper.apply(t);
       }
    }
}

Angenommen nun, die Anwendung definiert folgende RuntimeException-Klasse:

class MyException extends RuntimeException {
    private static final long serialVersionUID = 1L;

    public MyException(final String msg, final Throwable t) {
        super(msg, t);
    }

    @Override
    public String toString() {
         return this.getClass().getName() +
             " [" + this.getMessage() + ", " + this.getCause() + "]";
    }
}

Dann können die beiden neuen wrapException-Klassen wie folgt genutzt werden:

final Runnable runnable = () -> wrapException(
    () -> foo(42),
    e -> new MyException("Hello", e));
runnable.run();

final Supplier<Integer> supplier = () -> wrapException(
    () -> bar(42),
    e -> new MyException("World", e));
System.out.println(supplier.get());

Auch hier ist ein wenig Refaktorierung angesagt. Wir können eine der beiden alten wrapException-Methode auf die neue abbilden:

public static <T> T wrapException(final XSupplier<T> supplier) {
    return wrapException(supplier, e -> new RuntimeException(e));
}

public static <T> T wrapException(
        final XSupplier<T> supplier,
        final Function<Throwable, RuntimeException> exceptionWrapper) {
    try {
        return supplier.get();
    }
    catch (final Throwable t) {
        throw exceptionWrapper.apply(t);
    }
}

Hier noch einmal der komplette Quellcode von TryCatch:

package jn.util;

import java.util.function.Function;

public class TryCatch {

    public interface XRunnable {
        void run() throws Throwable;
    }

    public interface XSupplier<T> {
        T get() throws Throwable;
    }

    public static void wrapException(final XRunnable runnable) {
        wrapException(
            (XSupplier<Void>) () -> { runnable.run(); return null; });
    }

    public static void wrapException(
           final XRunnable runnable,
           final Function<Throwable, RuntimeException> exceptionWrapper) {
       wrapException(
           (XSupplier<Void>) () -> { runnable.run(); return null; },
           exceptionWrapper);
    }

    public static <T> T wrapException(final XSupplier<T> supplier) {
        return wrapException(supplier, e -> new RuntimeException(e));
    }

    public static <T> T wrapException(
            final XSupplier<T> supplier,
            final Function<Throwable, RuntimeException> exceptionWrapper) {
        try {
            return supplier.get();
        }
        catch (final Throwable t) {
            throw exceptionWrapper.apply(t);
        }
    }
}

 

Seminare zum Thema:

Clean Code – Professionelle Codeerstellung und Wartung

Java 8 / JDK8 Update

Java Erweiterungen II – Vertiefung

Weiterlesen

Java 9 und Jigsaw

Neue Tools in Java 9

Java 9 Modul-Konzept

Die wichtigste Neuerung von Java 9 ist zweifellos das neue Modul-Konzept – auch als “jigsaw” bezeichnet (JEPs 200, 201, 220, 260, 261, 281).

Das Modulkonzept verfolgt die Ziele “reliable configuration” und “strong encapsultion”. Was ist damit gemeint?

Im “alten” Java werden Klassen Pakete zugeordnet. Nur die als “public” gekennzeichneten Klassen eines Pakets können von Klassen anderer Pakete genutzt werden. Nicht-öffentliche Klassen können nur in anderen Klassen desselben Pakets verwendet werden.

Das Paket war bislang die höchste Organisations-Einheit, die Java kannte. Natürlich konnten class-Dateien zu jar-Dateien zusammengefasst werden – eine jar-Datei war bislang aber eben nichts anderes als ein gezipptes Verzeichnis mit class-Dateien. Eine jar-Datei hatte also kein dem Compiler oder der VM bekannte Java-Beschreibung. Das “Interface” einer jar war identisch mit ihrem Inhalt.

Daraus ergaben sich Probleme:

  • Angenommen, eine Anwendung enthält Klassen der Pakete jj.mod.pub und jj.mod.pri. In jj.mod.pub existiert die Klasse Foo, in jj.mod.pri existiert die Klasse Bar – eine Helper-Klasse, die von Foo genutzt wird. Bar muss nun public sein – ansonsten könnte sie von Foo nicht genutzt werden. Damit ist Bar natürlich auch für alle anderen Klassen nutzbar – obwohl sie nur als Helper-Klasse für Foo gedacht war. Wobei es völlig unerheblich ist, wie die class-Dateien zu jars zusammengefasst werden.
  • Klassen, die ein und demselben Paket zugeordnet waren, konnten auf verschiedene jar-Dateien aufgeteilt werden. Das trug nicht unbedingt zur Übersichtlichkeit bei.
  • Die Klassen, die zur Compilationszeit und zur Laufzeit herangezogen wurden, wurden über den CLASSPATH ermittelt. Enthielt der CLASSPATH zwei gleichnamige Klassen, so wurde die erste dieser Klassen herangezogen (die Änderung der Reihenfolge der Elemente des CLASSPATH konnte also eine sehr überraschende Wirkung haben).
  • Einer jar-Datei konnte man nicht ansehen, von welchen anderen jars sie abhängig war. Im Grunde war es immer Glücksache, alle benötigten jar-Dateien aufgefunden und bereitgestellt zu haben. Dieses Problem kann zwar durch die Benutzung von Build-Werkzeugen wie Apache Maven oder auch dem OSGi-Framework gelöst werden, aber eben nicht im Rahmen des Java-Standards.

Solche und ähnliche Probleme können vermieden werden, wenn das neue Modul-Konzept verwendet wird.

Ein Modul ist jar-Datei, die in ihrem Wurzelverzeichnis eine module-info.class enthält. Diese Datei wird vom Compiler aufgrund einer module-Info.java-Datei. Diese Datei enthält die Selbstbeschreibung des Moduls. Zu dieser Beschreibung gehören u.a. zwei wichtige Elemente: welche Pakete des Moduls sind von anderen Modulen nutzbar? – und von welchen anderen Modulen ist das Modul abhängig?

Wir greifen auf das obige Szenario zurück. Wir bauen ein Modul mit den Klassen Foo und Bar:

package jj.mod.pub;
 import jj.mod.pri;
 public class Foo {
 new Bar();
 }

package jj.mod.pri;
 public class Bar {
 }

Diese beiden Klasse sollen ein Modul namens jj.mod bilden. Wir schreiben folgende module-info.java:

module jj.mod {
 exports jj.mod.pub;
 }

Das Modul hat einen eigenständigen Java-Namen:  jj.mod. Solche Modul-Namen werden nach demselben Schema gebildet wie Paket-Namen. Der Modul-Name, die Namen der Pakete, die das Modul zusammenfasst, sind technisch völlig unabhängig voneinander – dennoch sollte natürlich ein gewisser Zusammenhang erkennbar sein.

Eine Modul jj.appl, welches dieses Modul nutzt, wird folgende Beschreibungsdatei enthalten:

module jj.appl {
 requires jj.mod;
 }

Die Klassen des jj.appl-Module können nun die Klassen aller Pakete des Moduls jj.mod nutzen, welche von diesem Modul exportiert werden – hier also nur die Klasse Foo:

package jj.appl;

public class Applicaiton {
 new jj.mod.pub.Foo();
 // new jj.mod.pri.Bar(); // illegal
 }

Module können zudem transitiv exportiert werden; und sie können ihre Pakete nur einer begrenzten Menge anderer Module zur Nutzung zur Verfügung stellen.

Mit diesem neuen Modul-Konzept wird der Modul-Pfad eingeführt, der den CLASSPATH ersetzen kann. Wird statt des CLASSPATH der Modul-Pfad verwendet, wird zudem verhindert, dass Pakete auf mehrere Module verteilt werden können.

Natürlich muss gewährleistet sein, dass auch “alte” jar-Dateien weiterhin genutzt werden können: sofern eine jar-Datei keine module-info.class enthält, exportiert sie implizit all ihre Pakete – und kann auf alle anderen jar-Dateien (auch auf modulare) zugreifen.

Und umgekehrt kann auch eine alte VM die neuen modularen jars nutzen – alte VMs ignorieren eine Datei namens module-info.class (“illegaler” Klassenname wegen des Bindestrichs).

Auch die rt.jar ist übrigens in mehrere Module zerlegt worden (eine Anwendung muss nur diejenigen Java-SE-Module an Bord haben, die sie tatsächlich benötigt).

Insbesondere bei der Erstellung großer Programmsysteme und bei der Erstellung von Bibliotheken ist das neue Modul-Konzept natürlich von grundlegender Bedeutung.

Publisher-Subscriber

Java 9 führt ein Publisher-Subscriber-Framework ein, welches der Reactive Streams Specification entspricht (java.utilconcurrent.Flow). Reactive Streams sind “asynchronous streams of data with non-blocking back pressure”. Ein reactive stream lässt sich grob als eine Kombination des Iterator- und des Observer-Patterns. Ein Iterator folgt dem Pull-Modell; ein Oberser folgt dem Push-Modell. Beim Flow-API fordert zunächst der Subscriber eine bestimmte Anzahl von zu verarbeitenden Objekten an (pull); dann stellt der Publisher diese Objekte dem Supplier in der von ihm gewünschten Anzahl zu (push). Das von Java 9 eingeführte Framework spezifiziert allerdings ausschließlich Interfaces – Implementierungen dieser Interfaces werden bereitgestellt z.B. von RxJava. (JEP 266)

jshell

Java 9 enthält nun eine jshell, ein interaktives Tool zur Auswertung von Deklarationen, Expressions und Anweisungen. Das Tool implementiert das REPL-Muster (Read-Eval-Print-Loop). Zwei kleine Beispiele:

jshell> Math.sqrt(2)

$1 ==> 1.4142135623730951

 

jshell> {     

   …> int a = 25;     

   …> int b = 15;     

   …> while (a != b) {     

   …> if (a > b) a -= b; else b -= a;     

   …> }     

   …> System.out.println(a);     

   …> }

5

Man kann nun also Java-Fragmente ausprobieren, ohne zunächst den Code komplett übersetzen und zu seiner Ausführung eine VM starten zu müssen (JEP 222).

Collection-Factory-Methods

Java 9 führt einige neue Factory-Methoden ein, mittels derer auf bequeme Weise immutable Collections erzeugt werden können (List.of, Set.of, Map.of, Map.ofEntries).

Erweiterungen der Klasse Process

Die Klasse Process erlaubt nun eine wesentlich erweiterte Kontrolle und Monitoring nativer Prozesse (diese Möglichkeiten waren bislang recht begrenzt). Neu sind in diesem Kontext u.a. die Interfaces ProcessHandle und ProcessHandle.Info.

Stream-API

Das Stream-API wurde erweitert. Neu sind hier insbesondere die Methoden dropWhile und takeWhile.

StackWalker

Informationen über den aktuellen Stack mussten bislang mit der Throwable-Methode getStackTrace abgefragt werden. Diese Methode liefert den kompletten Stack zurück. Mittels der in Java-9 eingeführten Klasse StackWalker können solche Informationen nun lazy ermittelt werden. Somit kann man z.B. erhebliche Kosten sparen, sofern man nur an den obersten Elementen des Stacks interessiert ist (JEP 259)

HTTP/2

Java 9 führt ein neues HTTP client API ein, das HTTP/2 und WebSocket implementiert und ersetzt somit das alte HttpURLConnection-API. Das neue API wird als Incubator-Modul ausgeliefert (es ist also noch offiziell Bestandteil der SE) (JEP 110)

Multi-release jars

Das jar-Dateiformat erlaubt nun die Koexistenz mehrerer Java-Release-spezifischer Versionen von class-Dateien (“multi-realease jar-files”). Dieses neue Feature verlangte natürlich auch die Erweiterung des jar-Tools (JEP 238)

GC

Als Garbage-Collector wird nun standardmäßig der G1 verwendet (dieser Collector eignet sich für große Heaps und mimimiert die Häufigkeit von Pausen). Er ersetzt somit den bislang verwendeten Parallel GC (JEP 248).

Compact Strings

Der Speicherverbrauch eines Programms wird maßgeblich von der Speicherung von Strings bestimmt. Strings wurden bislang mittels Arrays von chars implementiert – und ein char umfasst bekanntlich 2 Byte. In den meisten Fällen reicht aber ein einziges Byte zur Speicherung eines Zeichens aus. In Java 9 werden Strings nun – sofern möglich – platzsparender gespeichert (“compact strings”) (JEP 254).

Parser-API für JavaScript

Der Java-Script-Interpreter Nashorn wurde um ein Parser-API erweitert. Das API erlaubt die Visitor-basierte Inspektion eines JavaScript-AST-Baumes (JEP 236)

Diamond-Operator

Bislang war die Benutzung des “Diamond-Operators” beschränkt: bei der Definition von anonymen Klassen konnte er nicht verwendet werden. Diese Beschränkung ist nun aufgehoben worden (auch Kleinigkeiten sind wichtig…).

Für wen hat sich das Warten gelohnt? Für alle!

Was sich Entwickler jetzt noch wünschen? Das Modul-Konzept wird von einigen als zu statisch kritisiert. Kritisiert wird zuweilen auch, dass Module nicht geschachtelt werden können.

 

Seminare zu Java 9:

https://www.integrata-cegos.de/seminarangebot/anwendungsentwicklung/neuerungen-java-9-und-jigsaw/

Weiterlesen