1.3 Eigenschaften von Java 

Java ist eine objektorientierte Programmiersprache, die sich durch einige zentrale Eigenschaften auszeichnet. Diese machen sie universell einsetzbar und für die Industrie als robuste Programmiersprache interessant. Da Java objektorientiertes Programmieren ermöglicht, können Entwickler moderne und wiederverwertbare Softwarekomponenten programmieren.
1.3.1 Bytecode und die virtuelle Maschine 

Zunächst ist Java eine Programmiersprache wie jede andere. Doch im Gegensatz zu herkömmlichen Übersetzern einer Programmiersprache, die Maschinencode für eine spezielle Plattform und einen bestimmten Prozessor generieren, erzeugt der Java-Compiler Programmcode, den so genannten Bytecode, für eine virtuelle Maschine. Bytecode ist vergleichbar mit Mikroprozessorcode für einen erdachten Prozessor, der Anweisungen wie arithmetische Operationen, Sprünge und Weiteres kennt. Der Java-Compiler von Sun und der Java-Compiler der Entwicklungsumgebung Eclipse ist selbst in Java implementiert und generiert diesen Bytecode. (Es gibt aber auch Java-Compiler in C++, wie der Jikes-Compiler unter http://tutego.com/go/jikes).
Damit der Programmcode des virtuellen Prozessors ausgeführt werden kann, führt nach der Übersetzungsphase die Laufzeitumgebung (auch Run-Time-Interpreter genannt), die Java Virtual Machine (JVM), den Bytecode aus. [Die Idee des Bytecodes (FrameMaker schlägt hier als Korrekturvorschlag »Bote Gottes« vor) ist schon alt. Die Firma Datapoint schuf um 1970 die Programmiersprache PL/B, die Programme auf Bytecode abbildet. Auch verwendet die Originalimplementierung von UCSD-Pascal, die etwa Anfang 1980 entstand, einen Zwischencode – kurz p-code. ] Somit ist Java eine compilierte, aber auch interpretierte Programmiersprache – von der Hardwaremethode einmal abgesehen.
Das pure Interpretieren bereitet natürlich Geschwindigkeitsprobleme, da das Erkennen, Dekodieren und Ausführen der Befehle Zeit kostet. Java-Programme der ersten Stunde waren deutlich langsamer als übersetzte C(++)-Programme. Die Technik der Just-In-Time-(JIT-)Compiler [Diese Idee ist auch schon alt: HP hatte um 1970 JIT-Compiler für BASIC-Maschinen. ] löste das Problem. Ein JIT-Compiler beschleunigt die Ausführung der Programme, indem er zur Laufzeit die Programmanweisungen der virtuellen Maschine in Maschinencode der jeweiligen Plattform übersetzt. Anschließend steht ein an die Architektur angepasstes Programm im Speicher, das der physikalische Prozessor ohne Interpretation schnell ausführt. Mit dieser Technik entspricht die Geschwindigkeit der anderer übersetzter Sprachen.
Java on a chip
Neben einer Laufzeitumgebung, die den Java-Bytecode interpretiert und in den Maschinencode eines Wirtssystems übersetzt, wurde auch ein Prozessor in Silizium gegossen, der in der Hardware Bytecode ausführt. Die Entwicklung ging verstärkt von Sun aus, und einer der ersten Prozessoren war PicoJava. Bei der Entwicklung des Prozessors standen nicht die maximale Geschwindigkeit im Vordergrund, sondern die Kosten pro Chip, um ihn in jedes Haushaltsgerät einbauen zu können. Das Interesse an Java auf einem Chip zieht nach einer Flaute wieder an, denn viele mobile Endgeräte wollen mit schnellen Ausführungseinheiten versorgt werden.
Die ARM-Technologie des Unternehmens ARM Limited erlaubt über Jazelle DBX eine sehr schnelle Ausführung von Java-Bytecode. Mit dem Prozessor S5L8900 hat Samsung die ARM-Technologie ARM1176JZ(F)-S zusammen mit Speicherschnittstellen und Teile für Connectivity, Peripherie und Multimedia-Möglichkeiten in Silizium gegossen, und als 667 MHz-Prozessor sitzt er nun in Apples iPhone. Ironie des Schicksals ist jedoch, dass Apple im iPhone bisher keine Java-Unterstützung vorsieht.
Der aJ-100 von aJile Systems Inc. ist ein weiterer Prozessor. Und wenn wir den Pressemitteilungen von Azul Systems (http://www.azulsystems.com/) glauben können, gibt es auch bald einen 64-Bit-Prozessor mit 48 Kernen, der Java- und auch .NET-Bytecode ausführt. Ein Doppelherz tut auch Java gut.
1.3.2 Objektorientierung in Java 

Die Sprache Java ist nicht bis zur letzten Konsequenz objektorientiert, so wie Smalltalk es vorbildlich demonstriert. Primitive Datentypen wie Ganzzahlen oder Fließkommazahlen werden nicht als Objekte verwaltet. Der Design-Grund war vermutlich, dass der Compiler und die Laufzeitumgebung mit der Trennung besser in der Lage waren, die Programme zu optimieren. Allerdings zeigt die virtuelle Maschine von Microsoft für die .NET-Plattform deutlich, dass auch ohne die Trennung eine gute Performance möglich ist.
Java ist als Sprache entworfen worden, die es einfach machen soll, fehlerfreie Software zu schreiben. In C-Programmen erwartet uns statistisch gesehen alle 55 Programmzeilen ein Fehler. Selbst in großen Softwarepaketen (ab einer Million Codezeilen) findet sich, unabhängig von der zugrunde liegenden Programmiersprache, im Schnitt alle 200 Programmzeilen ein Fehler. Selbstverständlich gilt es, diese Fehler zu beheben, obwohl bis heute noch keine umfassende Strategie für die Softwareentwicklung im Großen gefunden wurde. Viele Arbeiten der Informatik beschäftigen sich mit der Frage, wie Tausende Programmierer über Jahrzehnte miteinander arbeiten und Software entwerfen können. Dieses Problem ist nicht einfach zu lösen und wurde im Zuge der Softwarekrise in den 1960er Jahren heftig diskutiert.
1.3.3 Java-Security-Modell 

Das Java-Security-Modell gewährleistet den sicheren Programmablauf auf den verschiedensten Ebenen. Der Verifier liest Code und überprüft die strukturelle Korrektheit und Typsicherheit. Der Klassenlader (engl. class loader) lädt Dateien entweder von einem externen Medium wie der Festplatte oder auch einem Netzwerk und überträgt die Java-Binärdaten zum Interpreter. Dort überwacht ein Security-Manager Zugriffe auf das Dateisystem, die Netzwerk-Ports, externe Prozesse und die Systemressourcen. Treten Sicherheitsprobleme auf, werden diese durch Exceptions zur Laufzeit gemeldet. Das Sicherheitsmodell ist vom Programmierer erweiterbar.
1.3.4 Zeiger und Referenzen 

In Java gibt es keine Zeiger (engl. pointer), wie sie aus anderen Programmiersprachen bekannt und gefürchtet sind. Da eine objektorientierte Programmiersprache ohne Verweise aber nicht funktioniert, werden Referenzen eingeführt. Eine Referenz repräsentiert ein Objekt, und eine Variable speichert diese Referenz. Die Referenz hat einen Typ, der sich nicht ändern kann. Ein Auto bleibt ein Auto und kann nicht als Laminiersystem angesprochen werden. Eine Referenz unter Java ist nicht als Zeiger auf Speicherbereiche zu verstehen.
#include <string.h> #include <iostream.h> class Ganz_unsicher { public: Ganz_unsicher() { strcpy(passwort, "geheim"); } private: char passwort[100]; }; |
void main() { Ganz_unsicher gleich_passierts; char *boesewicht = (char*)&gleich_passierts; cout << "Passwort: " << boesewicht << endl; } Dieses Beispiel demonstriert, wie problematisch der Einsatz von Zeigern sein kann. Der zunächst als Referenz auf die Klasse Ganz_unsicher gedachte Zeiger mutiert durch die explizite Typumwandlung zu einem Char-Pointer boesewicht. Problemlos können über diesen die Zeichen byteweise aus dem Speicher ausgelesen werden. Dies erlaubt auch einen indirekten Zugriff auf die privaten Daten. |
In Java ist es nicht möglich, auf beliebige Teile des Speichers zuzugreifen. Auch sind private Variablen erst einmal sicher. [Ganz stimmt das allerdings nicht. Mit Reflection lässt sich da schon etwas machen, wenn die Sicherheitseinstellungen das nicht verhindern. ] Der Compiler bricht mit einer Fehlermeldung ab – beziehungsweise das Laufzeitsystem löst eine Ausnahme (Exception) aus –, wenn das Programm einen Zugriff auf eine private Variable versucht.
1.3.5 Bring den Müll raus, Garbage-Collector! 

In Programmiersprachen wie C++ lässt sich etwa die Hälfte der Fehler auf falsche Speicher-Allokation zurückführen. Mit Objekten zu arbeiten, bedeutet unweigerlich: Anlegen und Löschen. Die Java-Laufzeitumgebung kümmert sich jedoch selbstständig um die Verwaltung dieser Objekte – die Konsequenz: Sie müssen nicht freigegeben werden, ein Garbage-Collector (kurz GC) entfernt sie. Der GC ist Teil des Laufzeitsystems von Java. Das Generieren eines Objekts in einem Block mit anschließender Operation zieht eine Aufräumaktion des GCs nach sich. Nach Verlassen des Wirkungsbereichs erkennt das System das nicht mehr referenzierte Objekt. Ein weiterer Vorteil des GCs: Bei der Benutzung von Unterprogrammen werden oft Objekte zurückgegeben, und in herkömmlichen Programmiersprachen beginnt dann wieder die Diskussion, welcher Programmteil das Objekt jetzt löschen muss oder ob es nur eine Referenz ist. In Java ist das egal, auch wenn ein Objekt nur der Rückgabewert einer Methode ist (anonymes Objekt).
Der GC ist ein nebenläufiger Thread im Hintergrund, der nicht referenzierte Objekte markiert und von Zeit zu Zeit entfernt. Damit macht der Garbage-Collector die Funktionen free() aus C oder delete() aus C++ überflüssig. Wir können uns über diese Technik freuen, da viele Probleme damit verschwunden sind. Nicht freigegebene Speicherbereiche gibt es in jedem größeren Programm, und falsche Destruktoren sind vielfach dafür verantwortlich. An dieser Stelle sollte nicht verschwiegen werden, dass es auch ähnliche Techniken für C(++) gibt. [Ein bekannter Garbage-Collector stammt von Hans-J. Boehm, Alan J. Demers und Mark Weiser. Er ist unter http://tutego.com/go/boehmgc zu finden. Der Algorithmus arbeitet jedoch konservativ, das heißt, er findet nicht garantiert alle unerreichbaren Speicherbereiche, sondern nur einige. Eingesetzt wird der Boehm-Demers-Weiser-GC unter anderem in der X11-Bibliothek. Dort sind die malloc()- und free()-Funktionen einfach durch neue Methoden ausgetauscht. ]
1.3.6 Ausnahmebehandlung 

Java unterstützt ein modernes System, um mit Laufzeitfehlern umzugehen. In der Programmiersprache wurden Exceptions eingeführt: Objekte, die zur Laufzeit generiert werden und einen Fehler anzeigen. Diese Problemstellen können durch Programmkonstrukte gekapselt werden. Die Lösung ist in vielen Fällen sauberer als die mit Rückgabewerten und unleserlichen Ausdrücken im Programmfluss. In C++ gibt es ebenso Exceptions, die aber nicht so intensiv wie in Java benutzt werden.
Aus Geschwindigkeitsgründen überprüft C(++) [In C++ ließe sich eine Variante mit einem überladenen Operator lösen. ] die Array-Grenzen (engl. range checking) standardmäßig nicht, was ein Grund für viele Sicherheitsprobleme ist. Ein fehlerhafter Zugriff auf das Element n + 1 eines Felds der Größe n kann zweierlei bewirken: ein Zugriffsfehler tritt auf oder, viel schlimmer, andere Daten werden beim Schreibzugriff überschrieben, und der Fehler ist nicht nachvollziehbar.
Das Laufzeitsystem von Java überprüft automatisch die Grenzen eines Arrays. Diese Überwachungen können auch nicht, wie es Compiler anderer Programmiersprachen mitunter erlauben, abgeschaltet werden. Eine clevere Laufzeitumgebung findet heraus, ob keine Überschreitung möglich ist, und optimiert diese Abfrage dann weg; Feldüberprüfungen kosten daher nicht mehr die Welt und machen sich nicht automatisch in einer schlechteren Performance bemerkbar.
1.3.7 Kein Präprozessor für Textersetzungen 

Viele C(++)-Programme enthalten Präprozessor-Direktiven wie #define, #include oder #if zum Einbinden von Prototyp-Definitionen oder zur bedingten Compilierung. Einen solchen Präprozessor gibt es in Java aus unterschiedlichen Gründen nicht:
- Header-Dateien sind in Java nicht nötig, da der Compiler die benötigten Informationen wie Funktionssignaturen direkt aus den Klassendateien liest.
- Da in Java die Datentypen eine feste, immer gleiche Länge haben, entfällt die Notwendigkeit, abhängig von der Plattform unterschiedliche Längen zu definieren.
- Pragma-Steuerungen sind im Programmcode unnötig, da die virtuelle Maschine ohne äußere Steuerung Programmoptimierungen vornimmt.
Ohne den Präprozessor sind schmutzige Tricks wie #define private public oder Makros, die Fehler durch doppelte Auswertung erzeugen, von vornherein ausgeschlossen. (Im Übrigen findet sich der Private/Public-Hack im Quellcode von Suns StarOffice. Die obere Definition ersetzt jedes Auftreten von private durch public – mit der Konsequenz, dass der Zugriffsschutz ausgehebelt ist.)
Ohne Präprozessor ist auch die bedingte Compilierung mit #ifdef nicht mehr möglich. Innerhalb von Anweisungsblöcken können wir uns in Java damit behelfen, Bedingungen der Art if (true) oder if (false) zu formulieren; über den Schalter -D auf der Kommandozeile lassen sich Variablen einführen, die dann eine if-Anweisung über System.getProperty() zur Laufzeit prüfen kann.
Da aber besonders bei mobilen Endgeräten Präprozessor-Anweisungen für unterschiedliche Geräte praktisch sind, gibt es Hersteller-Erweiterungen wie die von NetBeans (http://tutego.com/go/nbpreprocessor).
1.3.8 Keine überladenen Operatoren 

Wenn wir Operatoren wie das Plus- oder das Minuszeichen verwenden und damit Ausdrücke zusammenfügen, tun wir dies meistens mit bekannten Rechengrößen. So fügt ein Plus zwei Ganzzahlen, aber auch zwei Fließkommazahlen (Gleitkommazahlen) zusammen. Einige Programmiersprachen – meistens Skriptsprachen – erlauben auch das »Rechnen« mit Zeichenketten. Mit einem Plus können diese beispielsweise aneinandergehängt werden. Die meisten Programmiersprachen erlauben es jedoch nicht, die Operatoren mit neuer Bedeutung zu versehen und damit Objekte zu verknüpfen. In C++ ist jedoch das Überladen von Operatoren möglich, sodass etwa das Pluszeichen dafür genutzt werden kann, zum Beispiel geometrische Punktobjekte zu addieren. Dies ist bei umfangreicheren Rechnungen mit Objekten praktisch, da dort umständliche Verbindungen nicht über die Methoden geschaffen werden, sondern angenehm kurze über ein Operatorzeichen. Obwohl sie zuweilen ganz praktisch ist – das Standardbeispiel sind Objekte für komplexe Zahlen und Brüche –, verführt die Möglichkeit, Operatoren durch den Programmierer zu überladen, oft zu unsinnigem Gebrauch. In Java ist daher das Überladen der Operatoren bisher nicht möglich. Es kann aber sein, dass sich dies in Zukunft ändert.
Grundrechenarten wie Plus, Minus, Mal, Geteilt sind für Ganzzahlen und Gleitkommazahlen ebenso überladen wie die Operatoren Oder, Und oder Xor für Ganzzahlen und Boolesche Werte. Der einzige auffällige überladene Operator in Java für Objekte ist das Pluszeichen bei Strings. Zeichenketten können damit leicht zusammengesetzt werden. Informatiker verwenden in diesem Zusammenhang auch gern das Wort Konkatenation (selten Katenation). Bei einem String »Hallo« und »du da« ist »Hallo du da« die Konkatenation der Zeichenketten.
1.3.9 Java als Sprache, Laufzeitumgebung und Bibliothek 

Java ist nicht nur eine Programmiersprache, sondern ebenso ein Laufzeitsystem, was Sun durch den Begriff »Java Platform« klarstellen will. So gibt es neben der Programmiersprache Java durchaus andere Sprachen, die eine Java-Laufzeitumgebung ausführt, etwa diverse Skriptsprachen oder der Visual Basic 6 Clone Semplice. Skriptsprachen wie Groovy oder Jython werden immer populärer; sie etablieren eine anderen Syntax, nutzen aber die JVM und die Bibliotheken.
Zu der Programmiersprache und JVM kommt ein Satz von Bibliotheken für grafische Oberflächen, Ein-/Ausgabe, Netzwerkoperationen. Integraler Bestandteil der Standard-Bibliothek seit Java 1.0 sind Threads. Sie sind leicht zu erzeugende Ausführungsstränge, die unabhängig voneinander arbeiten können. Mittlerweile unterstützen alle populären Betriebssysteme diese »leichtgewichtigen Prozesse« von Haus aus, sodass die JVM diese parallelen Programmteile nicht nachbilden muss, sondern auf das Betriebssystem verweisen kann. Bei den neuen Multi-Core-Prozessoren sorgt das Betriebssystem für eine optimale Ausnutzung der Rechenleistung, da Threads wirklich nebenläufig arbeiten können.
1.3.10 Wofür sich Java nicht eignet 

Java-Fanatiker sehen oft nicht, dass Java zwar eine Programmiersprache ist, die sich für große Anwendungsgebiete eignen (general purpose language), doch nicht für alle. Jede Programmiersprache hat ihren Platz – ja, auch Perl und PHP.
In erster Linie kann ein Projekt mit Java nicht durchgeführt werden, wenn Eigenschaften gefordert werden, die Java nicht bietet. Java ist plattformunabhängig entworfen worden, sodass alle Funktionen auf allen Systemen lauffähig sind. Benutzerrechte etwa können von Java nicht erfragt oder modifiziert werden, da schon die Rechteverwaltungen von Unix und Windows völlig anders aussehen. Besonders systemnahe Eigenschaften wie die Taktfrequenz sind nicht sichtbar, und sicherheitsproblematische Manipulationen wie der Zugriff auf bestimmte Speicherzellen (das PEEK und POKE) sind ebenso untersagt. Weitere Beschränkungen:
- CD auswerfen, Verknüpfungen folgen
- Bildschirm auf der Textkonsole löschen, Cursor positionieren und Farben setzen
- auf niedrige Netzwerk-Protokolle wie ICMP zugreifen
- nicht-rechteckige Fenster
- Microsoft Office fernsteuern über die COM-Schnittstelle
- Zugriff auf USB [Eigentlich sollte es Unterstützung für den Universal Serial Bus geben, doch Sun hat hier – wie leider auch an anderer Stelle – das Projekt JSR 80: Java USB API nicht weiter verfolgt. ] oder Firewire
Aus den genannten Nachteilen, dass Java nicht auf die Hardware zugreifen kann, folgt, dass die Sprache nicht so ohne weiteres für die Systemprogrammierung eingesetzt werden kann. Treibersoftware, die Grafik-, Sound- oder Netzwerkkarten anspricht, lässt sich in Java nicht realisieren. Genau das Gleiche gilt für den Zugriff auf die allgemeinen Funktionen des Betriebssystems, zum Beispiel die Funktion, die Windows, Linux oder ein anderes System bereitstellt. Typische System-Programmiersprachen sind C oder C++.
Aus diesen Beschränkungen ergibt sich, dass Java eine hardwarenahe Sprache wie C(++) nicht ersetzen kann. Doch das muss die Sprache auch nicht! Jede Sprache hat ihr bevorzugtes Terrain, und Java ist eine allgemeine Applikationsprogrammiersprache; C(++) darf immer noch für virtuelle Java-Maschinen herhalten. Soll ein Java-Programm trotzdem systemnahe Eigenschaften nutzen – und das kann es mit entsprechenden Bibliotheken ohne Probleme –, bietet sich zum Beispiel der native Aufruf einer Systemfunktion an. Native Funktionen sind Funktionen, die nicht in Java implementiert werden, sondern in einer anderen Programmiersprache, häufig C(++). In manchen Fällen lässt sich auch ein externes Programm aufrufen und so etwa die Windows-Registry manipulieren oder Dateirechte setzen. Es läuft aber immer darauf hinaus, dass für jede Plattform die Lösung immer neu implementiert werden muss.
1.3.11 Java im Vergleich zu anderen Sprachen 

Beschäftigen sich Entwickler mit dem Design von Programmiersprachen, werden häufig existierende Spracheigenschaften auf ihre Tauglichkeit überprüft und dann in das Konzept aufgenommen. Auch Java ist eine sich entwickelnde Sprache, die Merkmale anderer Sprachen aufweist.
Syntax von C(++)
Java basiert syntaktisch stark auf C(++), etwa bei den Datentypen, Operatoren oder Klammern, hat aber nicht alle Eigenschaften übernommen. In der geschichtlichen Kette wird Java gern als Nachfolger von C++ (und als Vorgänger von C#) angesehen, doch die Programmiersprache verzichtet bewusst auf problematische Konstrukte wie Zeiger.
Das Klassenkonzept – und damit der objektorientierte Ansatz – wurde nicht unwesentlich durch SIMULA und Smalltalk inspiriert. Die Schnittstellen (engl. interfaces), die eine elegante Möglichkeit der Klassenorganisation bieten, sind an Objective-C angelehnt – dort heißen sie Protocols. Während Smalltalk alle Objekte dynamisch verwaltet und in C++ der Compiler statisch Klassen zu einem Programm kombiniert, mischt Java in sehr eleganter Form dynamisches und statisches Binden. Alle Klassen – optional auch von einem anderen Rechner über das Netzwerk – lädt die JVM zur Laufzeit. Selbst Methodenaufrufe sind über das Netz möglich. [Diese Möglichkeit ist unter dem Namen »RMI« (Remote Method Invocation) bekannt. Bestimmte Objekte können über das Netz miteinander kommunizieren. ] In der Summe lässt sich sagen, dass Java bekannte und bewährte Konzepte übernimmt, aber die Sprache sicherlich keine Revolution darstellt; moderne Skriptsprachen sind da weiter und übernehmen auch Konzepte aus funktionalen Programmiersprachen.
Java und JavaScript
Obacht ist beim Gebrauch des Namens »Java« geboten. Nicht alles, was bei Java im Wortstamm auftaucht, hat tatsächlich mit Java zu tun; JavaScript hat keinen Bezug zu Java. Die Programmiersprache wurde von Netscape entwickelt. Dazu ein Zitat aus dem Buch »The Java Developer’s Resource«: »Java and JavaScript are about as closely related as the Trump Taj Mahal in Atlantic City is to the Taj Mahal in India. In other words Java and Java-Script both have the word Java in their names. JavaScript is a programming language from Netscape which is incorporated in their browsers. It is superficially similar to Java in the same way C is similar to Java but differs in all important respects.«
1.3.12 Java ist Open Source 

Sehr lange gab es in der Software-Welt Forderungen an Sun, Java unter eine Open-Source-Lizenz zu stellen. Zwar war der Quellcode der Java-Bibliotheken schon immer Bestandteil des JDKs, und auch Suns virtuelle Maschine war verfügbar, doch eine bekanntere Lizenzform wie GPL oder BSD gab es 10 Jahre lang nicht. Dabei hat Jonathan Schwartz in San Francisco bei der JavaOne Konferenz 2006 schon angedeutet: »It's not a question of whether we'll open source Java, now the question is how.« War die Frage also statt des »Ob« ein »Wie«, kündigte bei der Eröffnungsrede der JavaOne Konferenz im Mai 2007 Rich Green die endgültige Freigabe Java als OpenJDK unter der Open Source-Lizenz GPL 2 an. Dem ging Ende 2006 die Freigabe von Compiler und der virtuellen Maschine voraus.
Das OpenJDK (https://openjdk.dev.java.net/) bildet die Basis von Java 7, und jeder Entwickler kann sein eigenes Java zusammenzustellen und beliebige Erweiterungen veröffentlichen. Damit ist der Schritt vollzogen, dass auch Java auf Linux-Distributionen Platz finden darf, die Java vorher aus Lizenzgründen nicht integrieren wollten.