Schneller programmieren mit Java 5.0

22.10.2004
Im Zentrum des jetzt von Sun freigegebenen Java-Release 5.0 stehen erstmals seit langem keine Performance-Verbesserungen, sondern Spracherweiterungen zur vereinfachten Entwicklung.

Von Bernhard Steppan*

Die unter ihrem Codenamen "Tiger" bekannt gewordene neue Java 2 Standard Edition (J2SE) 5.0 ist eine Zäsur. Sie ist die seit der Java-Version 1.1 umfassendste Erneuerung der Sprache und bringt für den Programmierer neben einigen Performance-Vorteilen primär eine Fülle von sinnvollen und teilweise überraschenden Spracherweiterungen.

Die wichtigste Erweiterung des Tiger-Release dürften die so genannten generischen Klassen (Generics) sein. Hinter diesem Begriff verbirgt sich ein neuer Typus von Klassen, die C++-Templates ähneln. Mit ihnen ist es nun auch in Java möglich, Klassen zu entwickeln, die beliebige Objekte ohne Verrenkungen aufnehmen können.

Klassenschablonen

Diese neuen Klassen sind wie Schablonen allgemeingültig (generisch) verwendbar, wie folgendes Beispiel zeigt. Will man eine Klasse zur Verfügung stellen, die beliebige Wertsachen aufnehmen kann, so könnte man sie beispielsweise auf folgende Art definieren (Listing 01):

000 // Listing 01

001 public class Safe<T> {

002 private T valueable;

003 public void setValueable (T valueable) {

004 this.valueable = valueable;

005 }

006 public T getValueable() {

007 return valueable;

008 }

009 } // Safe

Der Typ der Wertsache muss hierbei nicht genau spezifiziert werden. Das bedeutet, dass der Programmierer ihn erst dann festlegen muss, wenn er den Behälter erzeugt. Bei der Defini- tion der Klasse setzt der Programmierer einfach den Typ T als Platzhalter für Typen beliebiger Art ein (Zeile 002). Über die Methode setValueable() (Zeile 003) legt er fest, welche Wertsachen hinzugefügt werden, über getValueable() (Zeile 006) kann er sie wieder abfragen.

Die neue Behälterklasse "Safe" lässt sich für die Aufbewahrung von Geld oder für andere Wertsachen wie Schmuck verwenden. Folgende Anwendung (Listing 02) zeigt ihren Einsatz:

000 // Listing 02

001 Safe<Money> moneyBox = new Safe<Money>();

002 moneyBox.setValueable (new Money("400,53 Euro"));

003 Safe<Jewelry> jewelCase = new Safe<Jewelry>();

004 jewelCase.setValueable (new Jewelry ("12 Ringe"));

005 System.out.println("In der Spardose sind: "+

006 moneyBox.getValueable ().getValue());

007 System.out.println("Im Schmuckkasten sind: "+

008 jewelCase.getValueable ().getValue());

Zu Beginn des Programms (Zeile 001) wird ein neues Objekt namens moneyBox erzeugt, das vom Typ Safe ist. An dieser Stelle teilt der Programmierer dem Compiler mit, dass er die weitere Verwendung dieses Typs prüfen soll. Danach füllt das Programm diese neue Spardose mit einem bestimmten Geldbetrag (Zeile 002). Die nächste Anweisung zeigt, dass die generische Klasse Safe auch anders eingesetzt werden kann. In Zeile 003 nutzt sie das Programm, um Schmuck aufzubewahren. Dazu erzeugt es eine neue Schmuckdose und füllt sie mit Ringen (Zeile 004).

Programmierer werden jetzt einwenden, dass solche allgemein verwendbaren Container-Klassen in Java ein alter Hut sind. Auf den ersten Blick ist das auch richtig. Die universelle Verwendbarkeit der traditionellen Container-Klassen wurde aber dadurch erreicht, dass die Klassenschnittstellen auf dem allgemeinen Klassentyp Object basieren. Erst durch die Typwandlung (Cast) der Rückgabewerte in den speziellen Objekttyp ist die Verwendung der Klassen möglich.

Dieser Cast kann vom Compiler nicht überprüft werden. Er ist also unsicherer und mit einem zusätzlichen Arbeitsaufwand verbunden. Im Ergebnis lässt sich sagen, dass generische Klassen flexibler einzusetzen sind als die klassischen Container-Klassen und zudem den Vorteil der Typsicherheit mitbringen. Wichtig ist übrigens noch, dass der neue JDK-Compiler aus diesen und allen anderen Spracherweiterungen klassischen Bytecode erzeugt. Dadurch, dass der Bytecode zu den Vorgängerversionen kompatibel ist, laufen Programme, die mit dem neuen Compiler übersetzt wurden, interessanterweise auch unter älteren virtuellen Maschinen (siehe "Bytecode-Kompatibilität").

Insider wissen, dass das aus der Programmiersprache C bekannte Schlüsselwort "enum" in Java reserviert ist, aber seltsamerweise nicht verwendet werden durfte. Das Fehlen eines Auszählungstyps gehört zu den meistdiskutierten Themen in der Java-Community. Um den dringend benötigten Aufzählungstyp zu schaffen, behalf man sich mit Konstanten und bestimmten Entwurfsmustern. Diese Entwurfsmuster sind nunmehr sozusagen Bestandteil der Sprache und erleichtern die Programmierung ganz erheblich, wie folgendes Beispiel zeigt:

000 // Listing 03

001 private enum DaysOfThe Week {

002 Montag, Dienstag, Mittwoch,

003 Donnerstag, Freitag, Samstag, Sonntag }

Durch diese Anweisung lassen sich die Tage der Woche ohne irgendwelche Tricks festlegen. Sie können durch eine weitere Spracherweiterung, die neue for-Schleife, ausgegeben werden. Listing 04 zeigt, wie einfach die neue Schreibweise aussieht.

000 // Listing 04

001 for (DaysOfTheWeek day : DaysOfTheWeek.values()) {

002 System.out.println(day);

Um den Grund dieser Spracherweiterung zu klären, muss man zunächst einen Blick auf den häufigsten Einsatz der konventionellen for-Schleife werfen (Listing 05). Sie dient in vielen Fällen ganz einfach dazu, alle Elemente eines Feldes vollständig zu durchlaufen. Somit spielt der Index eigentlich eine Nebenrolle und könnte entfallen.

Das ist genau der Ansatzpunkt der neuen for-Schleife. Um diese immer wiederkehrende Schleifenform zu vereinfachen, gibt es in der aktuellen Java-Version eine auf diesen Einsatzzweck zugeschnittene Form der for-Schleife, bei der der Index gar nicht auftaucht. Möchte man zum Beispiel alle Elemente eines Feldobjekts namens array auf die Konsole ausgeben, so hätte man dies bislang auf folgende Weise gelöst:

000 // Listing 05

001 String array [];

002 for (int i = 0; i < array.length, i++) {

003 System.out.println(array[i]);

004 }

Mit der neuen Kurzform der for-Schleife ergibt sich eine deutliche Vereinfachung:

000 // Listing 06

001 String array [];

002 for (String element: array) {

003 System.out.println(element);

004 }

Die komprimierte Schreibweise ist auf den ersten Blick aufgrund des fehlenden Indexes etwas erklärungsbedürftig. Sie bedeutet aufgelöst, dass alle Strings namens element des Felds array (Zeile 002) über die Methode println() (Zeile 003) ausgegeben werden sollen. Auch hier wird wie bei den generischen Klassen der Code intern auf die klassische Weise umgesetzt, so dass das kompilierte Programm prinzipiell auch unter virtuellen Maschinen älterer Bauart einwandfrei funktioniert. Die neue for-Schleife lässt sich übrigens elegant mit generischen Klassen kombinieren. Das sieht dann beispielsweise so aus:

000 // Listing 07

001 ArrayList<String> array;

002 for (String element: array) {

003 System.out.println(element);

004 }

In Anlehnung an prozedurale Programmiersprachen verfügt auch Java über eine Reihe primitiver Datentypen wie int oder float, die keine Klassen sind, aber auch nicht deren Overhead mitschleppen. Sie eignen sich ausgezeichnet für leichtgewichtige Kalkulationen, wirken aber in der ansonsten rein objektorientierten Programmiersprache Java oftmals wie ein Fremdkörper.

Autoboxing

Was dieser Bruch bedeutet, merkt man besonders dann, wenn es aus irgendwelchen Gründen erforderlich ist, "richtige" Objekte einzusetzen. Das ist zum Beispiel dann der Fall, wenn man Methoden verwenden möchte, die ein Objekt und keinen reinen Zahlenwert als Übergabeparameter benötigen. Um diesen Bruch zu kitten, haben die Sprachdesigner für alle primitiven Datentypen passende Wrapper-Klassen als Pendant entwickelt.

Aufgrund der Koexistenz derart unterschiedlicher Datentypen besteht eine Routinetätigkeit des Java-Programmierers in der häufigen Konvertierung zwischen Wrapper-Klassen und primitiven Datentypen. Diesen Vorgang empfinden viele Java-Programmieren als ausgesprochen lästig. Seine Änderung steht unter den Verbesserungswünschen ganz weit oben.

Tatsächlich kommt im Tiger-Release nun Abhilfe durch eine automatische Konvertierung, das so genannte Autoboxing. Der folgende Vergleich zeigt auch hier wieder eine deutliche Verminderung des Arbeitsaufwands. Eine Konvertierung eines int-Werts sah bislang so aus:

000 // Listing 08

001 ArrayList array;

002 array.add(new Integer(2004);

Die Verpackung des primitiven int-Wertes durch ein Integer-Objekt war notwendig, weil die Methode add() der Container-Klasse ArrayList ein solches Objekt erwartete. Diese explizite Verpackungsarbeit ist nicht mehr erforderlich, wenn generische Klassen verwendet werden:

000 // Listing 09

001 ArrayList<Integer> array;

002 array.add(2004);

Hierbei handelt es sich wieder um eine vereinfachte Schreibweise. Sie bedeutet keineswegs, dass die Wrapper-Klassen dadurch überflüssig wären.

Während die bisher genannten Erweiterungen alle mit generischen Klassen zusammenhängen, ist die nächste interessante Neuerung völlig losgelöst davon: Statische Importe bieten die Möglichkeit, Methoden zu importieren. Hinter dieser Erweiterung steht weniger der Wunsch, die Tipparbeit zu reduzieren, als eine bessere Lesbarkeit des Quelltextes zu erzielen. Angenommen, man will die Wurzelfunktion des math-Packages verwenden, sähe das etwa so aus:

000 // Listing 10

001 double radicant = 2004;

002 double result = Math.sqrt(radicant);

003 System.out.println("Die Quadratwurzel aus "+

004 radicant + "= "+ result);

Da die Klasse Math aus dem java.lang-Package stammt, müsste man die Funktion zwar nicht einmal explizit importieren, schleppt aber die zur Funktion gehörende Klasse ständig im Quelltext mit. Dabei ist die Herkunft der Funktion an dieser Stelle völlig unerheblich. Abhilfe schafft ein statischer Import:

000 // Listing 11

001 import static java.lang. Math.sqrt;

002 double radicant = 2004;

003 double result = sqrt (radicant);

004 System.out.println("Die Quadratwurzel aus "+

005 radicant +" = "+ result);

Durch den statischen Import der Methode erreicht das Programm die Knappheit einer prozeduralen Sprache ohne deren Nachteile in Kauf nehmen zu müs-sen. Dass über eine Wildcard auch alle Funktionen einer Klasse importiert werden können, erhöht den Reiz dieser Spracherweiterung zusätzlich ganz erheblich. (ue)

Bytecode-Kompatibilität

Ohne entsprechende Einstellungen erzeugt der neue Java-Compiler Bytecode, der nur unter einer Laufzeitumgebung ab Version 5.0 ausgeführt werden kann. Setzt man den undokumentierten Schalter -target jsr14, so erzeugt der Compiler Bytecode, der prinzipiell auch unter einer Laufzeitumgebung der Version 1.4 funktioniert. Der einzige Haken dabei sind die neuen Klassen der Version 5.0, die der alten Version natürlich nicht bekannt sind. Hier helfen momentan nur Tricks, wenn man das Programm unter einer älteren Laufzeitumgebung starten möchte. Zum Beispiel lassen sich alle neuen verwendeten Klassen mit dem oben genannten Schalter kompilieren und beim Laden des Programms angeben (Schalter -Xbootclasspath).

Pro und Kontra

+ Viele sinnvolle Spracherweiterungen;

+ generische Klassen;

+ vereinfachte for-Schleife;

+ automatische Typwandlung zwischen Wrapper-Klassen und primitiven Typen;

+ Compiler erzeugt abwärtskompatiblen Bytecode.

- Neuerungen kommen reichlich spät.