Blog

Java 7-Sprachfeatures in der Praxis

Endlich, nach all den Jahren! Java 7 ist tatsächlich veröffentlicht worden! Der extrem lange Release-Zyklus gestattet es, auch relativ spät nach dem Erscheinungstermin einen Praxistest der neuen Sprachfeatures anzugehen. Außerdem unterstützte Eclipse 3.7.0 das neue Java noch nicht; erst mit Java 3.7.1 kam die Unterstützung mit an Bord.

Meine Motivation für diesen Beitrag ist, dass ich kaum einen Artikel zu diesem Thema im Netz gefunden habe, der wirklich alle Neuerungen erläutert und betrachtet.

Die neuen Sprachfeatures – Übersicht

Um es kurz und schmerzlos zu gestalten, hier die neuen Features:

  • Der Diamond Operator
    Mit diesem werden redundante Typisierungen bei Konstruktoraufrufen überflüssig
  • Binäre Literale
    Neben dezimalen und hexadezimalen Literalen gibt es jetzt auch binäre Literale
  • Unterstriche in numerischen Literalen
    Sie sollen es dem Programmierer ermöglichen, Code lesbarer zu gestalten
  • Strings in switch-case
    Viele werden aufatmen: Jetzt werden Strings in switch-case-Anweisungen unterstützt
  • try-with-resources
    Ressourcen können nun automatisiert geschlossen werden
  • Multiple Catch
    Auch ist es nun möglich, mehrere Exceptions in einem Catch-Block zu fangen
  • More Precise Rethrow
    Der Java Compiler ist intelligenter und kann dem Programmierer ggf. Instanzprüfungen abnehmen
  • Varargs und Heap Pollution
    Ein Hinweis auf Risiken während der Benutzung von Varargs-Methoden

Ich habe mit meinen Kollegen über die einzelnen Neuerungen gesprochen und kann sagen, dass die Resonanz größtenteils positiv war. Aber mehr dazu jetzt in den Details…

Der Diamond Operator

Hierzu gibt es eigentlich nicht viel zu sagen: Im Grunde wird dem Programmierer einfach Tipparbeit abgenommen. Eine redundante Typisierung bei einer Instanziierung, wenn die Deklaration bekannt ist, entfällt vollständig.

// The diamond operator
List<List<List<List<List<List<List<String>>>>>>> diamondList = new ArrayList<>();

Bei Holisticon sind alle durchweg begeistert, diesen Splitter endlich gezogen bekommen zu haben.

Binäre Literale

Als Ergänzung zu dezimalen und hexadezimalen Literalen wurden jetzt binäre Literale eingeführt.

// Binary literals
int mask1 = 0b00010000;
int mask2 = 0b00010001;
int mask3 = 0b00100000;
int mask4 = 0b00100001;

Ob dieses neue Feature wirklich sinnvoll ist, wurde bei uns kontrovers diskutiert. Ich z.B. finde es gut, kann man doch jetzt endlich leicht lesbare binäre Masken abbilden. Ein berechtigter Kritikpunkt ist, dass die Nutzung solcher Masken schon lange nicht mehr State of the Art ist. Sie wurden verwendet, um Bandbreiteproblemen zu begegnen oder Speicherplatz zu sparen. Heutzutage ist es nicht sinnvoll, mit ihnen zu arbeiten, da sie eine Hauptursache für kryptischen und unleserlichen Quelltext sein können.

Unterstriche in numerischen Literalen

Zahlen konnten in Java bislang nicht durch ein Gruppierungszeichen leserlicher geschrieben werden. Mit Java 7 hat sich das geändert, denn mit Unterstrichen kann man jetzt numerische Literale in Gruppen unterteilen.

// Binary literals and underscores
int mask1 = 0b0001_0000;
int mask2 = 0b0001_0001;
int mask3 = 0b0010_0000;
int mask4 = 0b0010_0001;

Wie man sehen kann, ist die Lesbarkeit von Zahlen in Quelltexten deutlich erhöht, wenn man es nicht übertreibt. Bei uns wurde es intern als nice to have bewertet. Einen signifikanten Mehrwert gibt es bei der Programmierung nicht zwangsläufig.

String in switch-case

Ein lang ersehntes Feature ist die Verwendung von Strings in switch-case-Blöcken, bei dem die JVM intern die Methode String.equals(Object) aufruft.

public static void main(String[] args) throws IOException {
	// Strings in switch
	BufferedReader r = new BufferedReader(new InputStreamReader(System.in));
	while (true)
		switch (r.readLine().toLowerCase()) {
			case "help":
				System.out.println("dir");
				System.out.println("exit");
				System.out.println("help");
				break;
			case "dir":
				System.out.println(Arrays.toString(new File(".").listFiles()));
				break;
			case "exit":
				System.exit(0);
			default:
				System.out.println("command not found");
		}
}

Sehr naheliegend ist der Anwendungsfall von kleinen „billigen“ Konsolenprogrammen. Aber natürlich gibt es unzählige weitere Fälle, die nun vereinfacht dargestellt werden können. Ich nenne hier nur mal das Stichwort PropertyChangeListener.

Neben der naheliegenden Argumentation, dass mit diesem praktischen Feature einmal mehr auf transparente Art und Weise Quelltext reduziert werden kann, kam auch der Punkt, dass es nur konsequent sei, auch mit Strings in switch-case-Blöcken arbeiten zu können (von Zahlen über Zeichen zu Zeichenketten). Aber zusätzlich wurde auch die ketzerische Frage in den Raum gestellt, warum bei Strings aufgehört wurde. Hätte man nicht, wie mit Interable, ein Interface entwerfen können, mit dem man alle Arten von Objekten in ein switch-case hätte geben können? Dieses hätte z.B. Switchable heißen können.

try-with-resources

Damit Ressourcen endlich vollautomatisch geschlossen werden können, wurden ein neues Interface AutoCloseable und das Sprachkonstrukt try-with-resources eingeführt.

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;

public class TryWithResources {

	public static void main(String[] args) {
		TryWithResources t = new TryWithResources();
		t.doSomethingSafe();
	}

	void doSomethingSafe() {
		try {
			doSomethingUnsafe();
		} catch (IOException e) {
			System.err.println("caught " + e);
			for (Throwable t : e.getSuppressed())
				System.err.println("suppressed " + t);
		}
	}

	void doSomethingUnsafe() throws IOException {
		try (InputStream i = new DefectInputStream(); OutputStream o = new DefectOutputStream()) {
			int b;
			while ((b = i.read()) != -1)
				o.write(b);
			o.flush();
		} // The try-with-resources doesn't need a catch or finally block
	}
}

Dieses Feature scheint auf Anhieb allen Anforderungen gerecht zu werden. Objekte, egal welcher Art, die das oben genannte Interface implementieren, können von der JVM automatisiert geschlossen werden. Arbeitet man mit einem try-with-resources-Block, können Ressourcen-Variablen auch endlich lokaler deklariert werden, nämlich innerhalb des Blocks. Ein catch-Block ist nicht notwendig, da man ggf. nur die Ressourcen schließen und eine evtl. geworfene Exception an anderer Stelle fangen möchte. Man kann sich generell einen impliziten finally-Block vorstellen, der dediziert für die Ressourcen-Behandlung ist. Tritt beim Schließen einer Ressource ein Fehler auf, d.h., wird eine Exception geworfen, wird diese an den Aufrufer weitergegeben. Ein interessanter Sonderfall ist, wenn eine Exception bereits im try-with-resources-Block geworfen wird und beim Schließen der Ressourcen ebenfalls ein Fehler auftritt. Für solche Fälle wurde die Klasse Throwable um die Methoden addSupressed(Throwable) und getSupressed erweitert. Fängt ein Aufrufer eine Exception aus einem try-with-resources-Block, kann man dort mit Throwable.getSupressed die Exceptions auswerten, die nachfolgend beim Schließen von Ressourcen aufgetreten sind.

Bis jetzt hat niemand bei uns Zweifel daran, dass dieses Konstrukt sehr nützlich sein wird.

Multiple Catch

Wieder muss ich das in diesem Artikel bereits abgenutzte Wort „endlich“ verwenden, denn nun ist es möglich, mit einem catch-Block multiple Exceptions zu fangen.

public class MultiCatch {

	public static void main(String[] args) {
		try {
			MultiCatch.class.newInstance();
		} catch (InstantiationException | IllegalAccessException e) {
			// Multi catch
			System.out.println("Didn't work, damn.");
		}
	}

	public MultiCatch() {
		throw new UnsupportedOperationException("No one may instantiate me!");
	}
}

Als Separator wurde die Pipe (|) herangezogen und nicht etwa das Komma, wie man aus Sicht einer Variablendeklaration evtl. vermuten könnte. Der Grund liegt eigentlich auf der Hand: Es kann immer nur eine Exception gleichzeitig geworfen werden und von daher kann man immer nur eine der deklarierten Exceptions fangen. Also ist es keine Variablendeklaration im althergebrachten Sinn, sondern vielmehr eine Veroderung des Deklarationstyps. Entweder man fängt Exception A oder B oder C. Und wie wir alle wissen, ist die Pipe ein Oder in Java (| binär, || logisch), wobei ohne genauere Typenprüfung und ohne Type Cast nur Felder und Methoden der nächsten gemeinsamen Superklasse referenziert werden können.

Wir freuen uns zumindest über dieses Feature, denn man kann so Exceptions logisch gruppieren, auch wenn eine schlechte Vererbungshierarchie vorhanden ist. Businness Exceptions können für ein Logging klar von kritischeren Application Exceptions getrennt werden, um nur mal ein Beispiel zu nennen.

More Precise Rethrow

Ein Augenbrauenhochzieher ist die neue Fähigkeit des Java-Compilers, gefangene Exceptions ohne präzise Typeninformation als konkrete Ausprägung weiterwerfen zu können.

public class MorePreciseRethrow {

	static abstract class AbstractMegaException extends Exception {}

	static class NotCoolException extends AbstractMegaException {}

	static class TooCoolException extends AbstractMegaException {}

	void doSomething() throws NotCoolException, TooCoolException {
		try {
			if ((int) Math.random() % 11 == 5)
				throw new NotCoolException();
			throw new TooCoolException();
		} catch (Exception e) {
			throw e;
		}
	}
}

Dem aufmerksamen Leser wird aufgefallen sein, dass im catch-Block nur eine Superklasse des Typs gefangen wird, der in der throws-clause der Methode angegeben ist. Wie das funktioniert? Der Java-Compiler erkennt, welche Exception-Typen innerhalb eines try-Blocks potentiell auftreten können. Können nur Exceptions auftreten, die auch in throws-clause der Methode genannt sind, so kann eine gefangene Exception auch als Superklasse weitergeworfen werden, ohne dass es zu einem Compiler-Fehler kommt.

Dieses Features wurde bei uns tendenziell eher negativ betrachtet, denn es kann zu schwer verständlichem Quelltext führen, da nicht alles offensichtlich ist. Vielleicht muss man sich aber auch erst damit vertraut machen. In jedem Fall reduziert es den Code, denn egal, wie man eine Exception für einen Weiterwurf fängt (ob mit mehreren catch-Blöcken oder einem Multi Catch), es reduziert den Quelltext auf ein Minimum.

Varargs und Heap Pollution

Wenn man eine Methode mit Varargs schreibt, kann man in Java 7 Compiler-Warnungen zentral behandeln. Ruft man z.B. eine Varargs-Methode mit typisierten Objekten auf, so erhält der Aufrufer die Warnung, dass für den Aufruf aus den typisierten Objekten ein generisches Array erstellt wird.

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

public class Varargs2 {

	public static void main(String[] args) {
		Varargs2 v = new Varargs2();
		v.doSomething();
	}

	void doSomething() {
		List l = new ArrayList<>();
		l.add("Hello");
		l.add("World");

		doIt(Collections.checkedList(l, String.class));

		for (String s : l)
			System.out.println(s);
	}

	@SafeVarargs
	final  void doIt(T... values) {
		// This method is still causing heap pollution
		for (T t : values)
			if (t instanceof List) {
				@SuppressWarnings("unchecked")
				List l = (List) t;
				l.add(Integer.valueOf(l.size()));
			}
	}
}

Nun stellt sich erst einmal die Frage, was ist ein generisches Array? Generische Arrays gibt es nur in Verbindung mit typisierbaren Objekten. Aufgrund von Type Erasure (die Typisierungsinformationen sind zur Laufzeit nicht verfügbar) kann es keine typisierten Arrays geben. Zwar kann ein generisches Array typisierte Objekte enthalten, aber eine konkrete Typisierung von Arrays ist nicht möglich. Und was ist jetzt ein generisches Array? Ein Array wie jedes andere auch – nur mit dem Unterschied des Typs (typisierbar oder nicht).

Class[] genericArray = new Class[0];
// Class<Integer>[] specificArray = new Class<Integer>[0]; // impossible due to type erasure

Mit der Annotation @SafeVarargs wird die Compiler-Warnung des Aufrufers einer solchen Methode unterdrückt. Zusätzlich wird aber mit dieser Annotation eine weitere, neue Compiler-Warnung unterdrückt: „Potential heap pollution[…]“ Und auch hier soll zuerst geklärt werden, was eigentlich heap pollution ist. Meiner Meinung nach ist die Java Sprachspezifikation an dieser Stelle nicht ausführlich genug. Unter heap pollution versteht man im Grunde die Verwendung von typisierten Objekten mit unterschiedlichen Typeninformationen. Schauen wir uns folgendes Beispiel an:

List l = new ArrayList();

List<Integer> integerList = l;
List<String> stringList = l;

integerList.add(Integer.valueOf(1));
stringList.add("1");

integerList.get(1); // ClassCastException
stringList.get(0); // ClassCastException

Hier wird dasselbe Listenobjekt einmal mit Integer und einmal mit String verwendet. Fügt man der Liste keine Objekte hinzu, ist das Szenario vollkommen harmlos. Fügt man allerdings der integerList ein Integer– und der stringList ein String-Objekt hinzu, so sind in derselben Liste unterschiedliche Objekttypen. Auch das ist noch nicht problematisch, man bezeichnet dies aber schon als heap pollution. Kritisch wird es, wenn man die Objekte der Liste auswertet, denn hier kann es zu ClassCastExceptions kommen.

Annotiert man eine Varargs-Methode nun mit @SafeVarargs, wird, wie bereits erwähnt, die „Possible heap pollution[…]“-Warnung unterdrückt. Trotzdem sollte man sich vorsehen, denn die implementierte Logik kann nach wie vor fehlerbehaftet sein.

Wir bei Holisticon sind der Meinung, dass die Annotation durchaus sinnvoll ist, insbesondere wenn es um Legacy Code geht, da die Warnung am Methodenaufruf dadurch unterdrückt wird. Die neue Warnung hingegen hätte man sich komplett sparen können, denn jeder Programmierer sollte sich darüber im Klaren sein, dass es zu ClassCastExceptions kommen kann, wenn man die Logik nicht typensicher gestaltet. Wann immer der Typ einer Eingabe nicht bekannt ist, sollte man besser eine generische Lösung finden, um sie zu verarbeiten.

Noch ein Hinweis: Die @SafeVarargs-Annotation kann nur auf static– oder final-Methoden angewendet werden.

Fazit

Nicht alle Neuerungen sollten euphorisch absorbiert werden. Mitunter ist man voll großer Dankbarkeit, dass man nun endlich so verfahren kann, wie man es schon früher gerne gewollt hätte, auch wenn teilweise nicht zu Ende gedacht wurde. Manche der neuen Sprachfeatures können aber auch mit „ganz nett“ oder „unnötig“ tituliert werden.

Zusätzlich muss man sich wahrscheinlich noch einige Zeit gedulden, bis man mit Java 7 für Konzerne und Unternehmen entwickeln kann, denn gegenwärtig wird dort noch auf Java 5 und 6 gesetzt.

Holisticon AG — Teile diesen Artikel

Über den Autor

Antwort hinterlassen