zu Kapitel 1 zum Inhalt

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.


zu Kapitel 3 Kapitelanfang zum Inhalt