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 auch. 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 aus1. 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 für die physikalische übersetzt werden. Es steht anschließend ein auf die Architektur angepasstes Programm im Speicher, das ohne Interpretation schnell ausgeführt wird. Auch Netscape übernahm im Windows-Communicator3 4.0 einen JIT (ein Produkt von ehemals Symantec), um an Geschwindigkeit zuzulegen - obwohl diese Variante noch nicht den gesamten 1.1 Standard beherrschte. (Erst in der Version 4.06 von Netscape kam die volle Unterstützung für Java 1.1.) Mit dieser Technik liegt die Geschwindigkeit zwar in vielen Fällen immer noch unter der von C, aber der Abstand ist geringer.
Hier klicken, um das Bild zu Vergrößern
Java on a chip
Dieser virtuelle Prozessor wurde mittlerweile auch in Silizium gegossen - eine Entwicklung, die verstärkt von Sun beziehungsweise Lizenznehmern verfolgt wird. Der Prototyp dieses Prozessors (genannt PicoJava) ist verfügbar und findet bald Einzug in so genannte Network-Computer. Das sind Computer ohne bewegliche Peripherie, wie Festplatten, die als Terminal am Netz hängen. 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 einem Java -on -a Chip ist inzwischen stark zurückgegangen.
Noch mehr Programmiersprachen, die Bytecode erstellen
Doch nicht nur aus der Programmiersprache Java lässt sich Bytecode erzeugen. Zurzeit gibt es bei verschiedenen Herstellern Entwicklungen von ADA-Compilern und sogar C-Compilern, die Bytecode erstellen. Auch die OOP-Programmiersprache EIFFEL (SmartEiffel)4 - unter der Leitung von Bertrand Meyer entwickelt - generiert Java-Bytecode. Ebenso gibt es eine Scheme11-Umgebung, die komplett in Java programmiert ist. Der Compiler erstellt für den LISP-Dialekt ebenfalls Java-Bytecode. Eine Webseite, die Programmiersprachen für die JVM aufzeigt, ist http://flp.cs.tu-berlin.de/~tolk/vmlanguages.html.
Mittlerweile ist Java nicht nur interpretierte Sprache, sondern zugleich auch interpretierende Sprache. Das zeigen unterschiedliche Computer- und Prozessor-Emulationsprogramme.5
1.3.2 Kein Präprozessor
In C(++) ersetzt ein Präprozessor Makros etwa für bedingte Compilierung oder Header-Dateien. Einen Präprozessor gibt es in Java nicht und entsprechend keine Header-Dateien. Diese sind in Java nicht nötig, da der Compiler die benötigten Informationen über die Softwareschnittstellen von Klassen direkt aus den Klassendateien liest. Ein schmutziger Trick wie
#define private public
#include "allesMoegliche"
oder Makros, die Fehler durch doppelte Auswertung erzeugen, sind damit 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.
Leider ist damit auch eine bedingte Compilierung mit #ifdef nicht mehr möglich. Dies führt vereinzelt dazu, dass ein externer Präprozessor benutzt werden muss, um den Quellcode entsprechend zu bearbeiten.
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 komplexen 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 dem 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;
}
|
Diese sehr gepfuschte Art demonstriert, wie problematisch der Einsatz von Zeigern sein kann. Der Zeiger, der zunächst als Referenz auf die Klasse Ganz_unsicher gedacht war, 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 dies nicht möglich, die Implementierung ist sicher, es gibt keinen Zugriff auf private Daten einer Klasse. Zunächst einmal würde der Compiler eine Fehlermeldung geben oder das Laufzeitsystem eine Ausnahme (Exception) auslösen, wenn beispielsweise eine Klasse über das Netz geladen 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 ist: 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.6
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. Range-Checking) in C(++)7 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 mitcompiliert. 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 kann herausfinden, ob keine Überschreitung möglich ist, und diese Abfrage dann wegoptimieren.
1.3.7 Objektorientierung in Java
Die Sprache Java ist nicht bis zur letzten Konsequenz objektorientiert, so wie Smalltalk es vorbildlich zeigt. Primitive Datentypen (beispielsweise Ganzzahlen oder Fließkommazahlen) werden nicht als Objekte verwaltet. Der Grund ist vermutlich in der Performance zu sehen. Der Compiler ist somit besser in der Lage, die Programme zu optimieren.
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, die erst ab einer Million Codezeilen anfangen, 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-Model
Das Java-Security-Model 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, so werden diese durch Exceptions zur Laufzeit gemeldet. Das Sicherheitsmodell ist vom Programmierer erweiterbar.
Hier klicken, um das Bild zu Vergrößern
1.3.9 Wofür Java nicht geeignet ist
Java-Fanatiker sehen oft nicht, dass Java zwar eine Programmiersprache ist, die für große Anwendungsgebiete geeignet ist, aber auch 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 in Java nicht machbar sind. 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 total 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 sind:
|
Anzahl freier Bytes im Dateisystem, Laufwerkstyp erkennen (CDRom), 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 |
Für diese Probleme bietet Java keine Bordmittel an. Abhilfe schafft ein nativer Aufruf einer Systemfunktion. 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 das Problem 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 Netscape hört es gar nicht gerne, wenn der Web-Browser als Navigator bezeichnet wird. Hier im Tutorial verwenden wir dies allerdings synonym. Die Firma versteht den Communicator als Web-Lösung, die nicht nur aus einem Web-Browser besteht. Es wird gemunkelt, dass Mitarbeiter aus der Firma rausfliegen, wenn sie das Wort »Navigator« nur in den Mund nehmen ...
4 http://smarteiffel.loria.fr/.
5 Dies beweist Hob, ein portabler ZX-Spectrum-Emulator, der komplett in Java geschrieben ist. Auf der Web-Seite http://www.emuunlim.com/hob/ gibt es noch viele Spiele dazu, die als Applet ausprobiert werden können. Ebenfalls gibt es den C=64-Emulator-Versuch jc64 unter http://sourceforge.net/projects/jc64.
6 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.
7 In C++ ließe sich eine Variante mit einem überladenen Operator lösen.
|