2 Eigenschaften von Java
2.1 Spracheigenschaften
In Kapitel1 wurde bereits eine ganze Liste von Schlagwörtern präsentiert,
welche die Eigenschaften von Java beschreiben sollen. Diese Eigenschaften
repräsentieren vielleicht das Beste, was andere Sprachen hervorgebracht haben. SUN
hat damit also nicht versucht, die Programmierung neu zu erfinden, sondern
baut auf bekannte (und erprobte!) Technologien auf. Java ist ein Kompromiß aus diesen verschiedenen Technologien. Hier wird kurz auf die einzelnen Punkte eingegangen
und untersucht, ob diese Ziele verwirklicht werden konnten.
Java ist einfach:
Was ist damit gemeint? Entwurfsziel war unter anderem, eine Sprache zu
entwickeln, welche so schnell wie möglich erlernt werden kann und die
den Programmierern `bekannt vorkommen` sollte. Ersteres wird durch eine
vergleichsweise geringe Zahl von Sprachkonstrukten verwirklicht, letzteres
durch die
Anlehnung dieser Konstrukte an bekannte C++-Syntax. Um die Sprache
einfach zu halten, wurde auf verschiedene nicht objektorientierte Features
verzichtet, was zu einem homogenen und leichten Sprachdesign führte.
Prinzipiell eliminiert wurden Mehrfachvererbung und implizite Typumwandlungen
ebenso wie zum Beispiel der goto-Befehl, der prähistorische
C-Präprozessor, Header-Dateien entfallen ebenso wie struct und
union, aber am wichtigsten: Java benutzt keine Zeiger. Jedem C/C++-Programmierer dürfte sich bei
diesem Gedanken `der Magen drehen`, aber für Zeiger besteht in Java einfach
kein Bedarf - Felder und Zeichenketten sind Objekte. Weiter erleichtert wird
die Programmierung durch die dynamische Speicherverwaltung und ihre Garbage
Collection. Die GC entbindet den Programmierer von der Aufgabe, sich um die
Freigabe von dynamisch allokiertem Speicher zu kümmern. Das System erkennt
selbst, wenn Speicherbereiche nicht mehr benötigt werden, und stellt diese
wieder dem Betriebssystem zur Verfügung. Weiterhin ist die GC beim
dynamischen Einbinden von Libraries oder auch der Thread-Programmierung ein
großer Vorteil.
Java ist objektorientiert:
Mit Ausnahme einiger einfacher Typen, wie Zahlen und Booleans, sind die meisten Typen in Java Objekte. Das Objektverhalten von Java unterscheidet sich
von C/C++, was man bei fast gleicher Syntax nicht vermuten würde. Java
wurde von vornherein konsequent objektorientiert entworfen, und nicht wie
C/C++, im Laufe eines Vierteljahrhunderts erweitert und immer weiter ver(schlimm)bessert. Genauer wird auf die Objektorientierung im Kapitel3 eingegangen.
Java ist verteilt:
Java wurde entworfen, um Applikationen auf Netzwerken zu unterstützen. Es
ist damit eine verteilte Sprache. Java unterstützt Netzverbindungen auf
verschiedenen Ebenen durch Klassen im java.net Paket. Zum Beispiel erlaubt
die url-Klasse, mit Java-Applikationen entfernte Objekte im Internet zu
öffnen und auf sie zuzugreifen. Mit Java ist es genauso einfach, eine
entfernte Datei zu öffnen wie eine lokale. Java unterstützt außerdem
zuverlässige Stream-Netzwerkverbindungen mit der socket-Klasse, so daß
verteilte Client/Server-Implementierungen programmiert werden können.
Java ist robust:
Java ist eine stark typisierte Sprache, was eine strenge Typüberprüfung
während der Übersetzung ermöglicht. Die beim Übersetzen gewonnenen
Typinformationen werden an den Linker und von dort an das Laufzeitsystem
weitergegeben, so daß dieses zur Laufzeit weitere Typprüfungen
(z. B. ob sich Felder und Zeichenketten noch innerhalb ihrer Grenzen
befinden), vornehmen kann. Ein weiteres herausragendes Feature von Java ist
die Ausnahmebehandlung. Dem Programmierer steht es frei, wie er auf Fehler
reagiert. In Kapitel3 wird auf diese Befehle
(try/catch) weiter eingegangen.
Die hohe Robustheit erlaubt ein `fast and fearless prototyping` (= schneller und angstfreier Prototypenentwurf) und der Programmierer braucht sich nur auf
seine Anwendungsproblematik zu konzentrieren. Die Semantik von Java eliminiert ganze Klassen von Fehlermöglichkeiten. Perfekt ist Java aber auch nicht
- logische Fehler können natürlich nicht erkannt werden.
Java ist architekturunabhängig und portabel:
Java Programme arbeiten auf jedem System, auf welchem die bereits erwähnte
Java Virtual Machine verfügbar ist; dies war eine der Grundvoraussetzungen
der Sprache. Der Java-Sourcecode, welchen der Programmierer erstellt, ist
somit für alle verschiedenen Systeme gleich. Der Java-Compiler produziert
keine maschinenspezifischen Instruktionen, sondern einen neutralen Bytecode,
der dann vom Java-Interpreter (eben der JVM) ausgeführt wird. Diese
Portabilität wird auch erreicht in dem Java z. B. explizit vorschreibt, wie
`groß` ein Datentyp und sein arithmetisches Verhalten ist. Eine int-Variable
ist z. B. in Java immer 32-Bit breit. Die ganze Software des JDK, der
Compiler, Debugger usw. ist bis auf winzige Teile tatsächlich schon in
Java geschrieben, einzig und allein die Laufzeitumgebung wurde in ANSI-C
entwickelt (also einem festen, bekannten Standard!). Als Objektbasis dient
das ebenfalls standardisierte POSIX-Format, womit alle Vorraussetzungen für
eine einfache Portierung des Laufzeitsystems auf andere Plattformen gegeben
sind. Das Java-Paket (JDK) selbst ist gut auf plattformübergreifende
Entwicklung vorbereitet - selbst das Bildschirmlayout ist durch die Klasse
java.awt (:= abstract window toolkit) auf verschiedenen Systemen identisch.
Java ist hochperformant:
Java ist eine interpretierte Sprache. Deswegen wird sie niemals so schnell
wie eine übersetzte Sprache sein. Java ist z. B. etwa 20 mal langsamer als C
[2] [2].
Diese Geschwindigkeit ist jedoch ausreichend für interaktive,
grafische oder netzbasierte Anwendungen, welche oft nur darauf warten, daß
der Anwender endlich etwas eintippt, oder Daten vom Netz eintreffen. Sollte
Geschwindigkeit eine Rolle spielen (und es gibt natürlich solche
Anwendungen), so sind inzwischen sogenannte `Just in Time`-Compiler entwickelt
worden, welche den Bytecode während der Laufzeit compilieren. Tatsächlich
war man sich der Geschwindigkeitsnachteile schon bewusst, bevor es solche
JIT-Compiler gab und hat den Bytecode entsprechend auf diese Anwendung vorbereitet. Der JIT-Code ist schon fast so schnell wie `echter` C/C++-Code und
hier ist sicherlich noch eine Verbesserung zu erwarten. Wenn es um Leistung
geht, muß betrachtet werden wo Java positioniert ist. Ein Ende der
Leistungsskala wird von Sprachen wie TCL, UNIX-Shells oder allgemein Scriptsprachen gestellt, welche bestens zum Entwurf geeignet, und portabel
aber langsam sind. Am anderen Ende der Skala sind die schnellen, betriebssystemnahen, übersetzten Sprachen wie eben C/C++. Dafür sind diese Sprachen
weniger portabel und verläßlich. Java liegt in der Mitte dieses Spektrums
und bildet in allen Belangen einen Kompromiß der beiden `Enden`.
Schneller als Scriptsprachen, dafür aber genauso portabel und stabil
- Optionen welche man gerne gegen einen geringen Geschwindigkeitsverlust
eintauscht - besonders wenn die Software den Anforderungen des Internet genügen soll.
Java ist multithread-fähig und dynamisch:
Die meisten neueren Betriebssysteme unterstützen
Multitasking. Dieses Multitasking wird von Java direkt unterstützt.
Ein Java-Programm kann gleichzeitig mehr als einen Thread ausführen,
ist also multithread-fähig. Das Programmieren in Multithread-Umgebungen
gestaltet sich eher schwierig, da viele Dinge zur gleichen Zeit
oder in einer unbestimmbaren Abfolge ablaufen können. In
Java kann der Programmierer auf Befehle zurückgreifen, die
auf den Paradigmen von C. A. R. Hoare beruhen
[3] [3].
Das java.lang Paket
enthält dazu eine Klasse thread welche z. B. die synchronized-Anweisung
enthält, die dafür sorgt, daß Methoden einer Klasse
nicht gleichzeitig ablaufen. Wie bereits erwähnt arbeitet
Java mit einem Speicherverwaltungssystem das unter anderem eine
Garbage Collection durchführt. Dieses System kann aber noch
mehr, so erlaubt es das dynamische Einbinden von Klassen z. B.
über das Internet. Durch die Kapselung und die klare Schnittstellenspezi-
fikation weiß der Java-Interpreter, zu welcher Klasse ein Objekt gehört,
indem er die Laufzeitinformationen überprüft. Java definiert
für diesen Zweck eine Klasse `class`. Für jede im Programm
verwendete Klasse existiert eine Instanz von `class`, die alle
Laufzeitinformationen über diese Klasse hält.
Zum Abschluß dieses Abschnitts einen kurzen Überblick über das, was Java von
anderen Sprachen übernommen hat
[4] [4]:
Feature Java vergleichbar mit
objektorientiert Single Inheritance, OO-Sprachen allgemein
Interfaces (à la
Smalltalk) für
Mehrfachvererbung,
Klassen, Methoden
einfach erlernbare Syntax C-Syntax ohne Zeiger und C++
Möglichkeiten zur Kalku-
lation von Speicher-
adressen, voreichenbehaf-
tete Integralwerte, 16 Bit
Unicode für Druckzeichen,
Strings sind ein
elementarer Typ
strenge bounds-Ckecking, Pascal, Purify
Laufzeitüberprüfungen type-checking zur
durch Verifier Laufzeit
interpretative Ausführung Java-Programme werden in P-Code (UCSD-Pascal),
Bytecode übersetzt, der Smalltalk 80
durch Interpreter zur
Ausführung gebracht wird
Klassen-Bibliotheken auf Diverse Browser HTML
WWW zugeschnitten beinhalten Interpreter
für Java
dynamische Bibliotheken late-binding zur shared-libraries (Unix:
Laufzeit, jede Klasse ist *.lib.so, Windows: *.dll)
in einer separaten Datei
abgespeichert
multithreaded Threads sind Bestandteil OS/2, Solaris, WindowsNT
jeder
Java-Laufzeitumgebung
Synchronisation Monitore sind Mesa
Sprachbestandteil
(Schlüsselwort
synchronized markiert
kritische Bereiche)
2.2 Interpretierte Sprache
Beim lesen dieser Überschrift wird eingefleischten C-Hackern wieder
ein Schauer über den Rücken laufen. Tatsächlich
ist Java nicht im klassischen Sinne - wie z. B. BASIC oder Scheme -
interpretiert. Der Java-Compiler erzeugt Bytecode statt direkten
Maschinencode. Während dieser Erzeugung wird bereits überprüft,
ob die Typen für Variablenzugriffe übereinstimmen, und
ob Variablen beim Lesen überhaupt initialisiert sind. Typkonvertierungen
sind ja nur in einem begrenzten Rahmen erlaubt, es kann z. B.
nicht zwischen einem fundamentalen Datentyp und Klassen konvertiert
werden. Wird ein Programm später auf der Virtual Machine
gestartet, können solche Überprüfungen blitzschnell
bearbeitet werden, weil das Laufzeitsystem schon das Programm
`kennt`; hierdurch werden die Programme erheblich schneller. Klassische
Interpretation macht das alles erst während der Laufzeit.
In einer interpretierten Umgebung verschwindet die übliche Linkphase
der Programmentwicklung. Wenn Java überhaupt eine Linkphase
hat, so ist es nur der Prozeß zum Laden neuer Klassen in
die Umgebung, was leicht und schnell vonstatten geht. Im direkten
Gegensatz zu traditionellen Methoden (Übersetzen, Linken
und Testen) erlaubt dies ein sehr schnelles Prototyping und einfaches
Experimentieren/Testen der Programme.
2.3 Sicherheit
Sicherheit war bei der Entwicklung von Java der vielleicht wichtigste
Aspekt. Die Problematik liegt auf der Hand. Das Internet hat in
den letzten Jahren eine rasante Entwicklung erfahren. Die Sicherheit von
Übertragungen konnte da nicht Schritt halten. Es exisitieren zwar
Ansätze, wie z. B. Verschlüsselungsverfahren, aber letzen Endes muß
sich der Programmierer selbst darum kümmern. Da Java im Netz
laufen soll (und nicht mit dem Netz) ist es davon besonders betroffen.
Wie aber schützt man sich vor Viren? Wie verhindert man Manipulationen
der Laufzeitumgebung? Eine 100%ige Sicherheit wird es nie geben,
das ist klar. Dennoch besteht die Möglichkeit, Manipulationen
so schwierig wie möglich zu machen, und genau das tut Java.
Wie bereits weiter oben angesprochen, erlaubt Java keine Zeigerzugriffe;
weiterhin ist es dem Programmierer unmöglich genaues über
die Speicheraufteilung in Erfahrung zu bringen - welche sowieso
erst während der Laufzeit festgelegt wird. Der Compiler kann
also kaum zum Schreiben zweideutigen Codes mißbraucht werden.
Was aber tun, wenn nicht bekannt ist, wer den Bytecode entwickelt
hat? Vielleicht wurde ein Compiler nachgebildet und die Sicherheitsüberprüfungen
ausgeschaltet? Oder während des Übertragens hat sich
ein anderes Programm dazwischengehängt und das Original verändert?
Das Laufzeitsystem von Java überprüft, ob alle aus dem bereits
vorhandenen Bytecode angesprochenen Elemente des neuen Codes auch
wirklich dort vorkommen. Gleichzeitig werden eine Reihe von Tests
durchgeführt, die den Ablauf des Programms erheblich beschleunigen.
Sind diese Tests durchgeführt, weiß das Laufzeitsystem
darüber Bescheid, daß
- keine Stacküber- oder unterläufe vorkommen,
- alle Registerzugriffe und -veränderungen korrekt sind,
- alle Parameter des Bytecodes korrekt sind,
- keine illegale Datenkonvertierung vorkommt.
Es kann jeder Bytecode getested werden, der der Spezifikation der virtuellen Maschine genügt, egal
ob er von dem Java-Compiler oder einem anderen Werkzeug erzeugt
wurde. Damit ist es möglich, Quelltexte anderer Sprachen
in Java-Bytecode umzuwandeln und in ein Programm einzubinden. Diese
Überprüfung findet in vier Schritten statt:
Schritt1: Formatüberprüfungen
- Am Anfang einer Definition steht eine spezielle, konstante Zahl
(magic number).
- Alle Attribute müssen die korrekte Länge besitzen.
- Die Datei darf weder zu kurz noch zu lang sein.
- In der Tabelle der Konstanten dürfen keine unreferenzierten
Daten vorkommen.
Schritt2: Genauere Formatüberprüfung, noch immer ohne zu interpretieren
- Ist sichergestellt, daß nur dafür vorgesehene Klassen abgeleitet,
und daß keine geschützten Methoden überschrieben werden?
- Jede Klasse muß eine Superklasse besitzen.
- Alle Variablen- und Methodenreferenzen müssen erlaubte Namen be-
sitzen, zu erlaubten Klassen gehören und eine `magic number`
haben.
Schritt 3: Analyse des Bytecodes und Datenflußanalyse (längster Schritt
mit Typüberprüfung)
- Die Größe des Stacks muß konstant bleiben und immer
korrekte Objekttypen enthalten.
- Kein Prozessorregister wird verwendet, wenn nicht bekannt ist, daß
das Register eine Variable vom entsprechenden Typ referenziert.
- Die Parameter der aufgerufenen Methoden stimmen mit der Definition
überein.
- Variablen dürfen nur mit Werten des entsprechenden Typs versehen
werden.
- Register und Stack enthalten immer nur Werte der dafür vorge-
sehenen Typen.
Schritt 4: Tests während der Laufzeit
Beim ersten referenzieren einer Klasse:
- Laden der Klassendefinition, falls noch nicht geschehen.
- Überprüfen, ob der Bytecode die Klasse im aktuellen
Kontext ansprechen darf.
Beim ersten Aufruf einer Methode oder dem Lesen/Schreiben von
Variablen:
- Existiert die Methode/Variable in der aktuellen bzw.
referenzierten Klasse?
- Ist die Signatur der Methode/Variable korrekt?
- Darf im aktuellen Kontext auf die Methode/Variable zuge-
griffen werden?
Beim Arbeiten im Netzwerk können Zugriffsrechte vergeben werden,
welche dort die Sicherheit erhöhen:
- Überhaupt kein Netzzugriff erlaubt.
- Nur Zugriff auf Knoten, von denen Bytecode importiert
wurde.
- Nur Zugriff auf Knoten innerhalb eines Firewalls
(eine Einrichtung die mehrere Rechner zu einer Sicher-
heitszone zusammenfaßt), wenn der Bytecode von dort
geladen wurde.
- Zugriff auf das gesamte Netz.
Sicherheit ist natürlich keine Entweder-Oder-Angelegenheit. Niemand
garantiert, daß ein Java-Programm nicht doch gefährlichen Code
enthält. Java sieht die meisten klassischen Techniken voraus, welche
benutzt werden, um Software zu manipulieren; und SUN kann behaupten, es
zumindest sehr schwierig zu machen, solche Software zu schreiben.
In der neuen Version des JDK (V1.1) ist ein Programm enthalten,
welches eine JVM mit 5000 Einzeltests auf ihre Zuverlässigkeit
und Sicherheit überprüft. SUN schaltet damit eine weitere
eventuelle Sicherheitslücke aus und vermeidet, daß
Fremdfirmen inkompatible und unsichere JVMs entwickeln können. Es gab
schon derartige Bestrebungen - von Microsoft. Die komplette Veröffentlichung
aller Sources (Laufzeitumgebung, Compiler, Bibliotheken) sorgt
zudem dafür, daß niemand behaupten kann Java habe eine
Sicherheitslücke. Trotzdem ist das Netz eine gefährliche
Umgebung. Java bietet einen guten Mittelweg dazwischen, die Sicherheitsproblematik
zu ignorieren und davon gelähmt zu werden.
2.4 Java und Java-Script
Es würde den Rahmen dieses Proseminars sprengen die genauen Unterschiede zwischen Java und Java-Script zu erläutern oder Java-Script genau
vorzustellen. Um zu vermeiden, daß mit Kanonen (Java) auf Spatzen
(z. B. Dokumentlayouts) geschossen wird, entwickelte SUN zusammen mit der
Firma Netscape Java-Script. Java-Script ist eine reine Interpretersprache,
welche nicht in Bytecode übersetzt wird. Folgende Tabelle vergleicht an
wichtigen Punkten die beiden Sprachen:
Java-Script: Java:
vom Client interpretiert. Übersetzt, bevor der Bytecode auf dem
Client interpretiert wird.
Objektbasiert: ein Objekt kann Objektorientiert: Applets bestehen
funktional erweitert werden, es vollständig aus Klassen, die
existieren aber keine Klassen und hierarchisch angeordnet werden
keine Vererbung. können.
Quelltext steht direkt in der Bytecode wird gesondert übertragen
HTML-Seite und wird mit dem Dokument (evtl. von einer ganz anderen
übertragen. Adresse).
Variable Datentypen müssen nicht Variable Datentypen müssen deklariert
deklariert werden, Typüberprüfungen werden, strenge Typtests.
sind sehr locker.
Sicher: kein Zugriff auf Weniger sicher: Zugriff auf
Festplatte. Festplatte möglich.
Java-Script ist als Bindeglied zwischen HTML-Dokumenten und Java-Applets
gedacht und entsprechend stark gegenüber Java eingeschränkt.
Java-Script vereinfacht die Parameterübergabe an Java-Applets,
oder ermöglicht kleine Laufschriften usw. Java-Script bietet somit hauptsächlich
Features zur Gestaltung einer HTML-Seite, die Interaktion zwischen
Client und Server wird davon kaum beeinflußt.
2.5 Unterschied zwischen C++ und Java
Obwohl SUN Java stark an C++ angelehnt hat, ist der Umstieg von C++ nach
Java nicht so leicht wie es zunächst scheint. Java hat keine OO-feindlichen Hintertüren. C++-Programmierer finden sich in der Syntax sofort
`zu Hause`. class, public und main sind bekannte Schlüsselwörter.
Aber letztere ist z. B. keine eigenständige Funktion (außerhalb
des OO-Kontexts) wie in C++, sondern eine statische Methode innerhalb einer
Klasse. Der Grund hierfür ist, das Java nur Klassenmethoden und
Klassenvariablen kennt. Es gibt weder externe Funktionen, noch globale
Variablen. Ein Java-Programm enthält genau eine Klasse main vom typ
void. Sie braucht und kann also keinen Wert zurückgeben.
Vermissen wird der C++-Programmierer auch den Präprozessor, was
zu lesbareren Quelltexten und gesteigerter Wiederverwendbarkeit
der Programme führt.
In Java existieren keine Strukturen und keine `unions`. Alle Daten werden in Klassen zusammengefaßt,
egal ob noch zusätzliche Funktionalität in Form von
Methoden hinzukommt oder nicht. In Java gibt es keine Funktionen;
jegliche Funktionalität kann in Klassen gekapselt werden
und wird in OO-Sprachen allgemein nicht benötigt. Tatsächlich
wird das oft als Grund angegeben, warum C++ nicht als OO-Sprache
akzeptiert werden kann. Wie bereits erwähnt, gibt es in Java
keine automatische Typumwandlung (muß explizit angezeigt
werden), keine Zeiger(-arithmetik), kein Überladen von Operatoren.
Das Überladen von Operatoren wird oft mißbraucht um
z. B. Klassen zu addieren, was sinnvoll gar nicht addiert werden
könnte - die Java-Entwickler verzichteten deshalb darauf.
Mit dem Fehlen des goto-Befehls kann sich wohl die Mehrzahl der
Programmierer abfinden, ein weitere Sprungmöglichkeit im
Wust der HTML-Referenzen/Links etc. ist auch nicht notwendig.
Mehrfachvererbung ist zwar ein interessanter Ansatz, aber in der Realität
durch den extrem anwachsenden Komplexitätsgrad nicht sinnvoll einsetzbar.
Java implementiert eine andere Art der Mehrfachvererbung durch
den Einsatz sogenannter Interfaces. Ein Interface definiert keine
Klasse, sondern nur die Funktionen, die eine Klasse als Schnittstelle
zur Verfügung stellt. Eine Klassendefinition kann dabei durchaus
mehrere Schnittstellen implementieren. Pascal-Programmierer kommen
in Java wahrscheinlich sogar etwas besser weg. So sind die syntaktischen
Unterschiede größer, aber der Umgang mit Variablen
(es gibt Boolean, man kann Strings addieren) und deren Initialisierung
mit null (zeigt die `Abwesenheit einer Referenz` an) läßt
jeden Pascal-Programmierer auf sicherem Boden landen (im gegensatz zu
C). null ist nicht konstant zu 0 definiert sondern ein reserviertes
Schlüsselwort/Wert und stellt somit eine Ausnahme der strengen
Typisierungsregeln von Java dar; es kann jeder Variablen, die
eine Klasse, eine Schnittstelle oder ein Array als Typ hat, zugewiesen
werden.
Auf weitere syntaktische Unterschiede wird noch genauer
in den folgenden Kapiteln eingegangen.