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 objektorientiert ist, spiegelt es den Wunsch der Entwickler wider, moderne und wieder verwertbare Softwarekomponenten zu programmieren.
1.3.1 Bytecode und die virtuelle Maschine
 
Zunächst ist Java eine Programmiersprache wie jede andere. Nur im Gegensatz zu herkömmlichen Übersetzern einer Programmiersprache, die Maschinencode für eine spezielle Plattform generieren, erzeugt der Java-Compiler Programmcode für eine virtuelle Maschine, den so genannten Bytecode. Bytecode ist vergleichbar mit Mikroprozessorcode für einen erdachten Prozessor, der Anweisungen wie arithmetische Operationen, Sprünge und Weiteres kennt. Ein Java-Compiler, etwa der von Sun, der selbst in Java implementiert ist, generiert diesen Bytecode.
Damit aber der Programmcode des virtuellen Prozessors ausgeführt werden kann, führt nach der Übersetzungsphase die Laufzeitumgebung (auch Run-Time-Interpreter genannt), die Java Virtuelle Maschine, den Bytecode aus.1
Somit ist Java eine compilierte, aber auch interpretierte Programmiersprache – von der Hardwaremethode einmal abgesehen.
Das Interpretieren bereitet noch Geschwindigkeitsprobleme, da das Erkennen, Dekodieren und Ausführen der Befehle Zeit kostet. Im Schnitt sind Java-Programme drei bis zehn Mal langsamer als C(++)-Programme. Die Technik der Just-In-Time(JIT)-Compiler2
mildert das Problem. Ein JIT-Compiler beschleunigt die Ausführung der Programme, indem die Programmanweisungen der virtuellen Maschine in Maschinencode der jeweiligen Plattform zur Laufzeit übersetzt wird. Es steht anschließend ein auf die Architektur angepasstes Programm im Speicher, das ohne Interpretation schnell ausgeführt wird. Mit dieser Technik liegt die Geschwindigkeit nahe an C(++).
 Hier klicken, um das Bild zu Vergrößern
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 Hardware Bytecode ausführt. Die Entwicklung ging verstärkt von Sun aus, und einer der ersten Prozessoren war PicoJava. Bei der Entwicklung des Prozessors stand 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; der aJ-100 von aJile Systems Inc. ist ein derartiger Prozessor.
1.3.2 Kein Präprozessor für Textersetzungen
 
Viele C(++)-Programme enthalten Präprozessor-Direktiven wie #define, #include oder #if zum Einbilden von Prototyp-Definitonen 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 Funktions-Signaturen direkt aus den Klassendateien liest. |
|
Da in Java die Datentypen eine feste, immergleiche 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. Mit der oberen Definition wird jedes Auftreten von private durch public ersetzt, 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 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 über Sytem.getProperty() in einem if zur Laufzeit geprüft werden können.
1.3.3 Keine überladenen Operatoren
 
Wenn wir Operatoren wie das Plus- oder das Minuszeichen verwenden und damit Ausdrücke zusammenfügen, machen 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 aneinander gehä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 praktisch bei umfangreicheren Rechnungen mit Objekten, da dort umständliche Verbindungen nicht über die Methoden geschaffen werden, sondern angenehm kurze über ein Operatorzeichen. Obwohl zuweilen ganz praktisch – 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.
Die Grundrechenarten sind für Ganzzahlen und Gleitkommazahlen überladen und ebenso ein einfaches 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 gerne das Wort Konkatenation (selten Katenation). Bei einem String »Hallo« und »du da« ist »Hallo du da« die Konkatenation der Zeichenketten.
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 aber ohne Verweise 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 sehen.
Beispiel Dass das Pfuschen in C++ leicht möglich ist und wir Zugriff auf private Elemente über eine Zeigerarithmetik bekommen können, zeigt das folgende Programm. Für uns Java-Programmierer ist dies ein abschreckendes Beispiel.
#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 aber auch nicht. Mit Reflection lässt sich da schon etwas machen, wenn die Sicherheitseinstellungen das nicht verhindern.) Aber zumindest würde der Compiler eine Fehlermeldung geben oder das Laufzeitsystem eine Ausnahme (Exception) auslösen, wenn ein Attributzugriff versucht wird.
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. Arbeiten mit Objekten heißt unweigerlich: Anlegen und Löschen. Die Java-Laufzeitumgebung sorgt 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 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 Rückgabewert einer Methode ist (anonymes Objekt).
Der GC ist ein spezieller Thread-Prozess, der Objekte markiert, auf die nicht mehr verwiesen wird. Dann entfernt er sie von Zeit zu Zeit. Damit macht der Garbage-Collector die Funktionen free() aus C oder delete() aus C++ überflüssig. Wir können uns über diese Technik freuen, denn viele Probleme sind damit verschwunden. 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.3
1.3.6 Ausnahmenbehandlung
 
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, diese werden aber nicht so intensiv wie in Java benutzt.
Aus Geschwindigkeitsgründen wird die Überwachung von Array-Grenzen (engl. rangechecking) in C(++)4
nicht durchgeführt. Und der fehlerhafte 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. Schon in PASCAL wurde eine Grenzüberwachung mit compiliert. Das Laufzeitsystem von Java überprüft automatisch die Grenzen eines Arrays. Diese Überwachungen können nicht, wie es diverse PASCAL-Compiler erlauben, abgeschaltet werden, sondern sind immer eingebaut. 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 Performanz bemerkbar.
1.3.7 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 es auch ohne die Trennung mit einer guten Performanz geht.
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 Sechzigerjahren heftig diskutiert.
1.3.8 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 Festplatte oder auch 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.9 Wofür sich Java nicht eignet
 
Java-Fanatiker sehen oft nicht, dass Java zwar eine Programmiersprache ist, die für große Anwendungsgebiete geeignet ist (general purpose language), doch nicht für alle. Jede Programmiersprache hat ihren Platz – ja, auch Perl.
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 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; |
|
grafische Applikationen mit einem Tray-Icon ausstatten; |
|
auf niedrige Netzwerk-Protokolle wie ICMP zugreifen; |
|
Zugriff auf USB 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 etwa Grafikkarten oder Soundkarten ansprechen, lassen 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(++).
Aus diesen Beschränkungen ergibt sich, dass Java auch C(++) nicht ersetzen kann. Doch das muss die Sprache auch nicht! Jede Sprache hat ihr bevorzugtes Terrain, und Java ist eine allgemeine Applikations-Programmiersprache; C(++) darf immer noch für virtuelle Java-Maschinen herhalten. Soll ein Java-Programm trotzdem systemnahe Eigenschaften nutzen, 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 mit System.exec() aufrufen und so etwa die Windows Registry manipulieren und Dateirechte setzen. Es läuft aber immer darauf hinaus, dass für jede Plattform die Lösung immer neu implementiert werden muss.
1 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, etwa Anfang 1980, einen Zwischencode – kurz p-code.
2 Diese Idee ist auch schon alt: HP hatte um 1970 JIT-Compiler für BASIC-Maschinen.
3 Ein bekannter Garbage-Collector stammt von Hans-J. Boehm, Alan J. Demers und Mark Weiser. Er ist unter http://reality.sgi.com/boehm_mti/gc.html 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.
4 In C++ ließe sich eine Variante mit einem überladenen Operator lösen.
|