5 Java-Hintergründe

Nachdem wir uns in ein paar ersten Beispielen an Java herangetastet haben, wollen wir nun zu einem etwas abstrakteren Kapitel kommen, das andererseits viel zum Grundverständnis von Java beiträgt. Dabei sind die ersten beiden Abschnitte über die Theorie der Objektorientierung bzw. die spezielle Objektorientierung von Java zwar recht trocken, aber für das Erlernen von Java von fundamentaler Bedeutung. Der dritte Abschnitt über die Plattformunabhängigkeit von Java und den Aufbau der JVM ist dagegen als technische Hintergrundinformation zu verstehen. Für einen ersten Einstieg in die Java-Welt sind diese Informationen nicht unbedingt nötig, Sie können bei Bedarf später darauf zurückkommen. Das soll aber natürlich nicht heißen, dass Sie diesen Abschnitt nicht auch jetzt schon durcharbeiten dürfen. Ich möchte nur vermeiden, dass Sie bereits zu Anfang von zu vielen technischen Details erschlagen werden.

5.1 Was ist OOP?

Ein zentraler Aspekt von Java ist seine Objektorientiertheit (Object Orientation - OO). Deshalb ist eine grundsätzliche Beschäftigung mit diesem Begriff zwingend erforderlich. Nicht zuletzt deshalb, um den Begriff zu entmystifizieren. Außerdem erleichtert ein elementares Verständnis des objektorientierten Konzeptes einen Einstieg in Java erheblich. Zwar kann man Java ebenfalls lernen, ohne sich um das objektorientierte Konzept große Gedanken zu machen. Jedoch werden dann viele wesentliche und unumgängliche Java-Elemente (Klassen, Methoden usw.) quasi vom Himmel fallen, während sie sich aus dem objektorientierten Konzept zwingend ergeben.

Dabei gilt es als Erstes festzuhalten, dass nicht die Eigenschaften einer Programmiersprache in der Regel Objektorientiertheit ausmachen, sondern der Denkansatz für die Lösung einer zu programmierenden Aufgabe.

Unter dieser abstrakten Aussage können Sie sich vielleicht nicht viel vorstellen, aber ich möchte es so verdeutlichen: Man kann mit jeder Programmiersprache objektorientiert - zumindest im weiteren Sinne - arbeiten, sogar mit solch überholten, prozeduralen Sprachen wie Basic, Cobol oder Fortran. Auch das sehr weit verbreitete Turbo Pascal wurde von seinem Hersteller Borland schon ab der Version 5.5 objektorientiert genannt. Allerdings erleichtern bestimmte Sprachen die objektorientierte Programmierung, andere (wohl auch die oben zitierten) machen es ziemlich schwierig. Weitere Programmiersprachen wiederum nennen sich objektorientiert und lassen den Programmierern so viele (prozedurale) Schlupflöcher, dass das OO-Konzept erfahrungsgemäß immer dann ausgehebelt wird, wenn ein nicht-objektorientierter Ansatz auf den ersten Blick sinnvoller und einfacher erscheint. Als Beispiele für solche Sprachen kann man C++ oder Delphi anführen. Die hier nicht zwingende und teilweise recht kompliziert aufgebaute Logik zur Arbeit mit Objekten hat viele Programmierer davon abgehalten, sich ein grundsätzliches Verständnis des objektorientierten Konzepts zu erarbeiten.

Und dann gibt es einen Typ von Programmiersprachen, der Programmierern keine andere Wahl als einen objektorientierten Ansatz lässt. Dazu zählen die schon recht alten Sprachen Lisp oder Small Talk. Und eben Java.

Der Hauptgrund, dass der Begriff »Objektorientierte Programmierung« (OOP) in der Programmiererwelt für eine große Unruhe und Verwirrung sorgte, ist wohl der, dass Programmierer - nahezu seit es den Beruf gibt - anders denken: prozedural. Aber wir haben immer noch nicht geklärt, was es mit dem Begriff Objektorientierung überhaupt auf sich hat.

Im Zentrum der althergebrachten, prozeduralen Programmierung steht die Umsetzung eines Problems in ein Programm durch eine Folge von Anweisungen, die in einer vorher festgelegten Reihenfolge auszuführen sind. Einzelne zusammengehörende Anweisungen werden dabei maximal in so genannte Funktionen oder Prozeduren zusammengefasst. Die Datenstruktur befindet sich in einer von den Anweisungen getrennten Hierarchieebene.

Versuchen wir einen Erklärungsansatz über die Entwicklung der EDV. Denn die Geschichte der Programmierung zeigt, dass sich die nicht-objektorientierte Denkweise lange Zeit bewährt hatte und vor allem historische Computerplattformen einfach nicht zu einer anderen Vorgehensweise in der Lage. Und so hatte sich das traditionelle Denkmodell mit der Zeit einfach in den Köpfen der Programmierer festgesetzt. Großer Nachteil dieser nicht-objektorientierten Philosophie ist die mangelnde Wartbarkeit, denn Änderungen in der Datenebene können Auswirkungen auf die unterschiedlichsten Programmsegmente haben. Außerdem entspricht ein solches Denkkonzept der Logik von Maschinen, nicht dem Abbild der realen Natur. Dies war natürlich zu Zeiten der allerersten Computersysteme besonders ausgeprägt. Ein erster Schritt hin zu einer mehr menschlichen Logik war das in den 70er-Jahren entstandene Denkmodell der »strukturierten Programmierung«. Darin wurde versucht, zusammengehörende Befehle als Einheit zu sehen und Programmfunktionen in Unterstrukturen bzw. Unterprogramme (Prozeduren und Funktionen) zusammenzufassen, die bereits über genau festgelegte Schnittstellen zu ihrer Umwelt verfügten. Bei Bedarf konnte man diese Unterstrukturen über ihren Namen aufrufen.

Dieser Ansatz stellt zwar eine Verbesserung gegenüber Abläufen dar, die jeden einzelnen Befehl bei Bedarf aufrufen - auch wenn mehrere Programmschritte mehrfach in der gleichen Version benötigt werden. Jedoch ist diese strukturierte Programmierung noch immer nicht die konsequente Umsetzung von menschlicher Denkweise. Insbesondere sind Anweisungen und Daten (Variablen) noch immer getrennt. Hier setzt die Philosophie der Objektorientiertheit als folgerichtige Weiterentwicklung der strukturierten Programmierung an.

5.2 Die Definition von Objektorientierung

Objektorientierte Programmierung lässt sich am treffendsten darüber definieren, dass die Trennung von Datenebene und Anweisungenebene aufgehoben wird. Zusammengehörende Anweisungen und Daten bilden eine zusammengehörende, abgeschlossene und eigenständige Einheit. Man nennt sie - Objekte!

Objekte bestehen also im Allgemeinen aus zwei Bestandteilen, den so genannten Objektdaten - das sind die Attribute bzw. Eigenschaften - und aus Objektmethoden. Man kann sagen, dass Attribute die Merkmale sind, durch die sich ein Objekt von einem anderen unterscheidet. Objektmethoden stellen die objektorientierte Ausdrucksweise für die bisherigen Elemente der Anweisungenebene (Funktionen und Prozeduren) dar.

Abbildung 5.1:  Schema eines Objekts

5.2.1 Objektmethoden

Objektmethoden (oder kurz Methoden genannt) sollen in unserer Vorstellung erst einmal irgendwelche Algorithmen (d.h. Berechnungsformeln) sein, mit denen die - im Prinzip abgeschlossenen und eigenständigen - Objekte miteinander kommunizieren und/oder ihre Objektdaten manipulieren können.

Als eine sinnvolle Definition von Methode kann im Allgemeinen die folgende Aussage gelten: Methoden realisieren die Funktionalität der Objekte, ihre Implementierung erfolgt in den Klassen. Das heißt, Methoden sind die programmtechnische Umsetzung der Objektfunktionalitäten.

Wesentlich an der objektorientierten Programmierung ist, dass die Methoden und die Daten (Attribute) gemeinsam einem Objekt zugeordnet sind. Nur über dieses Objekt kommt man an Methoden und die Daten des Objekts heran. Es gibt keine freien Eigenschaften und Funktionalitäten. Dies schließt bei strenger Objektorientierung als wichtige Konsequenz beispielsweise eine Existenz von globalen Variablen aus. Die Zuordnung bedeutet weiterhin (ebenfalls sehr wichtig für das grundlegende Konzept von Java), dass es Methoden ohne zugehörige Objekte nicht geben darf. Obwohl wir nun etwas vorgreifen müssen, soll eine wesentliche - und die einzige - Ausnahme erwähnt werden. Diese Ausnahme scheinen diejenigen Methoden darzustellen, die außerhalb der konkreten Objekte, jedoch innerhalb der Klassen liegen - die so genannten Klassenmethoden. In die gleiche Kerbe schlägt die Existenz von Klassenvariablen, die eine Art von globalen Variablen darstellen. Diese scheinbaren Ausnahmen passen aber dennoch in das objektorientierte Konzept und widerlegen nicht die oben getroffenen Aussagen, sofern man das so genannte Metaklassen-Konzept berücksichtigt, das etwas weiter unten noch erläutert wird.

Ein kleines Beispiel soll die Kopplung von Methoden und Eigenschaften an ein Objekt deutlich machen. Wenn man prozedural einen Bauernhof abbilden wollte, könnte man beispielsweise einen Hahn, ein Schwein und eine Kuh programmieren. Zusätzlich erstellt man Funktionen oder Prozeduren, die das Krähen, das Grunzen und das Muhen realisieren. Da diese jedoch nicht an das jeweils sinnvolle Objekt gekoppelt sind, könnte eine Kuh plötzlich krähen, ein Schwein muhen und ein Hahn grunzen. Im objektorientierten Ansatz ist so etwas nicht möglich, denn die Realisierung der Objekte Hahn, Kuh und Schwein beinhaltet bereits deren Funktionalitäten in Form von Methoden, die auch nur über das jeweilige Objekt anzusprechen sind.

Auf der anderen Seite ist ein Objekt nach außen nur durch seine Methoden und Attribute definiert, es ist gekapselt, versteckt seine innere Struktur vollständig vor andern Objekten. Man nennt dies Information Hiding oder Datenkapselung.

Der ganz entscheidende Vorteil von diesem Verfahren ist, dass ein Objekt sich im Inneren, d.h. bezüglich seiner Objektdaten und seiner inneren Attribute, vollständig verändern kann. Solange es sich nur nach außen unverändert zeigt, wird das veränderte Objekt problemlos in ein System integriert, wo es in seiner alten Form funktioniert hatte. Ob nun - um auf unseren Bauernhof zurückzukommen - die Methode »Krähen« über die Wiedergabe einer lokalen MP3-Datei oder einer MP3-Datei aus einem (hinreichend schnellen) Netzwerk realisiert wird, ändert nichts an der Funktionalität des Objekts. Ein Objekt ist also eine Art »Black Box«.

Diese gesamte objektorientierte Philosophie entspricht viel mehr der realen Natur als der prozedurale Denkansatz, der von der Struktur des Computers definiert wird. Ein Objekt ist im Sinne der objektorientierten Philosophie eine Abstraktion eines in sich geschlossenen Elements der realen Welt. Dabei spricht man von Abstraktion, weil zur Lösung eines Problems normalerweise weder sämtliche Aspekte eines realen Elements benötigt werden, noch überhaupt darstellbar sind. Irgendwo muss immer abstrahiert werden - und wenn es erst auf der Ebene der Atome ist, jedoch dann wird es philosophisch.

Ein Mensch bedient sich eines Objekts, um eine Aufgabe zu erledigen. Man weiß in der Regel nicht genau, wie das Objekt im Inneren funktioniert, aber man kann es bedienen (weiß also um die Methoden, um es verwenden zu können) und weiß um die Eigenschaften des Objekts (seine Attribute). Die Reihe von Beispielen kann man beliebig lang ausdehnen, wir wollen es beim Auto, der Waschmaschine oder der Kaffeemaschine (es geht ja in dem Buch um Java) belassen.

Im Detail sind bei der Erklärung der Objektorientierung jetzt noch einige Festlegungen notwendig.

5.2.2 Who's calling?

Besonders wichtig ist der Begriff der Botschaften. Damit Objekte aktiv werden können, tauschen sie so genannte Botschaften (oder Nachrichten bzw. Messages) aus, die ausschließlich für die Kommunikation von Objekten verwendet werden. Andere Kommunikationswege - etwa die in anderen Programmiersprachen oft genutzten globalen Variablen - gibt es nicht (dies wären ja Attribute, die außerhalb von Objekten existieren würden und das ist explizit nicht erlaubt). Das sendende Objekt schickt dem Zielobjekt eine Aufforderung, eine bestimmte Methode auszuführen. Das Zielobjekt versteht (hoffentlich) diese Aufforderung und reagiert mit der zugehörigen Methode. Dieser Vorgang wird von einigen OO-Gurus als eine Art Bitte bezeichnet, mit der Objekte andere Objekte höflich zur Ausführung diverser Wünsche veranlassen. Die genaue formale Schreibweise solcher Botschaften in OO-Programmiersprachen ist natürlich im Detail verschieden, jedoch meistens wird das Schema

Empfänger

Methodenname

Argument

verwendet. Punkt und Klammer trennen dabei meist die drei Bestandteile der Botschaft (die so genannte Punkt-Notation). Eine Botschaft sieht also meist so aus:

Empfänger.Methodenname(Argument)

Dies kann aber von Programmiersprache zu Programmiersprache differieren. Das Argument stellt in dem Botschafts-Ausdruck einen Übergabeparameter für die Methode dar.

Eine ähnliche Form wird auch für die Verwendung von Objektattributen gewählt. In der Regel sieht das dann so aus:

Empfänger.Attributname

Ein fundamentaler Unterschied zwischen der prozeduralen und der objektorientierten Denkweise kann so umschrieben werden:

Prozedural bedeutet typengestützt, objektorientiert heißt kommunikationsgestützt.

Konkret drückt dies aus, dass eine Prozedur oder Funktion im herkömmlichen Sinn nur Variablen fest vorgegebener Datentypen verwenden kann. Falls eine bestimmte Operation für eine Menge von verschiedenen Datentypen benötigt wird (ein gutes Beispiel ist die Multiplikation), muss für jeden möglichen Typ eine Prozedur geschrieben werden, zumeist noch mit verschiedenen Namen. Die Folge ist in der Regel Fallunterscheidung nach dem Datentyp. Ein neuer Datentyp zieht eine weitere Prozedur/Funktion und - noch schlimmer - an diversen Stellen im Programm Änderungen nach sich. Um das Beispiel der Multiplikation zu nehmen, kann man die mathematischen Datentypen »Reelle Zahlen« und »Komplexe Zahlen« heranziehen und die zugehörigen - mathematisch abweichenden - Multiplikationsvorschriften in zwei Prozeduren abbilden.

Die Prozedur für die Multiplikation der reellen Zahlen sollte in etwa so aussehen (das Listing ist nur als Schema zu verstehen):

procedure multiplikation(double a,double b) {
double ergebnis;
ergebnis= a* b;
return (ergebnis);
}

Für komplexe Zahlen muss eine vollständig andere Prozedur definiert werden, etwa folgende (dabei seien ergebnis_real und ergebnis_imagiär global definierte Variablen):

procedure multi_komplex(double realteil_1, double realteil_2, double imaginaer_teil_1, double 
imaginaer_teil_2) {
ergebnis_real = 
  (realteil_1* realteil_2) - 
  (imaginaer_teil_1* imaginaer_teil_2);
ergebnis_imaginaer = 
  (realteil_1* imaginaer_teil_2) + 
  (realteil_2 * imaginaer_teil_1);
}

Man sieht, dass sowohl die Zahl der Übergabeparameter abweicht als auch der eigentliche Berechungsalgorithmus. Zwei - in der Regel unterschiedlich benannte - Prozeduren sind die Folge.

In der objektorientierten Sichtweise hingegen steht das Objekt im Mittelpunkt. Die Operation ist in der Klasse implementiert, zu der das Objekt gehört. Die Ausführung der Multiplikation übernehmen die Objektmethoden, der Anwender muss sich darum nicht kümmern. Soll nun eine bestehende Operation (in unserem Beispiel sei das die Multiplikation der reellen Zahlen) um eine neue Funktionalität erweitert werden (Berechnungsvorschrift für komplexe Zahlen), so werden die Veränderungen in der Klasse vorgenommen und die zugehörigen Methoden dort geschrieben. Die neue Form der Multiplikation wird einfach als Erweiterung innerhalb der Klasse hinzugefügt. Nach außen erscheint das Objekt wie schon erwähnt unverändert und lässt sich wie gehabt unter einem Namen ansprechen. Weitergehende Änderungen im Programm sind nicht notwendig.

5.3 Klassen und Instanzen

Der Begriff der Klasse ist von elementarer Wichtigkeit, wenn man in Java oder einer anderen OO-Sprache programmieren möchte. In der OOP werden ähnliche Objekte zu Gruppierungen zusammengefasst, was eine leichtere Klassifizierung der Objekte ermöglicht. Die Eigenschaften der Objekte werden also in den Gruppierungen gesammelt und für eine spätere Erzeugung von realen Objekten verwendet. Diese Beschreibungen oder Baupläne für konkrete Objekte nennt man Klassen, die Objekte selbst sind im OO-Sprachgebrauch Instanzen dieser Klassen. Oft werden Klassen mit Schablonen verglichen. Man könnte sich Klassen auch als Backformen vorstellen, mit denen Plätzchen (Instanzen) aus einem (Computer-)Teig gestochen werden.

Abbildung 5.2:  Klassen-Instanzen-Beziehungen

Zentrale Bedeutung hat dabei die hierarchische Struktur der Gruppierungen von allgemein bis fein (zumindest bei der Einfachvererbung, auf deren genaue Bedeutung wir noch zu sprechen kommen).

Beispiel: Ein Objekt Saxophon als konkrete Instanz gehört zu der Klasse der Holzblasinstrumente1. Diese Holzblasinstrumente wiederum gehören zu einer höheren Klasse, den Musikinstrumenten, und diese sind allgemein der Klasse der »Musik erzeugende Dinge« zugehörig.

Abbildung 5.3:  Hierarchie-Beziehungen

Gemeinsame Erscheinungsbilder sollten also in der objektorientierten Philosophie in einer möglichst hohen Klasse zusammengefasst werden. Erst wenn Unterscheidungen möglich bzw. notwendig sind, die nicht für alle Mitglieder einer Klasse gelten, werden Untergruppierungen, also untergeordnete Klassen, gebildet.

Jede (gewöhnliche) Klasse kann eine Vielzahl von Unterklassen und konkreten Instanzen (Objekten) haben.

Abbildung 5.4:  Eine Klassenhierarchie

5.3.1 Superklasse und Subklasse

Damit wir jetzt schon in der späteren Java-Sprache sprechen (außerdem ist es gültiger OO-Dialekt), führen wir für eine übergeordnete Klasse den Begriff Superklasse, eine untergeordnete Klasse den Begriff Subklasse ein. Die ineinander geschachtelten Klasse bilden einen so genannten Klassenbaum. Dieser kann im Prinzip beliebig tief werden. Eben so tief, wie es notwendig ist, um eine Problemstellung detailliert zu beschreiben.

Jede Subklasse erbt alle Eigenschaften und Methoden ihrer Superklasse. Sie beinhaltet also immer mindestens die gleichen Eigenschaften und Methoden wie die Superklasse. Daher sollte dort sinnvollerweise mindestens eine Eigenschaft oder Methode hinzugefügt werden, die die Superklasse nicht beinhaltet (sonst sind Sub- und Superklasse ja identisch). Mit dieser Erklärung haben wir zum ersten Mal den Begriff der Vererbung herangezogen, der in der OOP von fundamentaler Bedeutung ist. Gleich mehr zur Vererbung, doch vorher klären wir noch zwei Begriffe aus dem Klassenbereich.

5.3.2 Metaklassen

In Java sind alle Klassen Ableitungen einer einzigen obersten Klasse - der Klasse Object. Andererseits soll Java ein konsequent realisiertes objektorientiertes System sein. In einem solchen System darf es nur Objekte geben, daher müssen selbst Klassen ebenfalls Objekte sein. Das Problem ist jetzt, dass damit auch die oberste Klasse selbst die Instanz einer Klasse sein muss. Jedes Objekt ist nach Definition die Instanz einer Klasse, also muss gleichfalls jede Klasse - sogar die oberste Klasse - Instanz einer anderen Klasse sein. Eigentlich ist dies für die oberste Klasse ein Widerspruch. Die Frage ähnelt ein wenig der Problematik, was sich außerhalb des Universums befindet2. Im Grunde befindet sich keine Klasse mehr außerhalb dieses Objektuniversums. Was also tun, um in dem Modell konsistent zu bleiben? Man definiert zur Einhaltung des streng objektorientierten Konzeptes deshalb so genannte Metaklassen. Die einzigen Objekte von Metaklassen sind Klassen, und sie vererben die Klassenmethoden und Klassenvariablen. Diese werden nicht an das Objekt selbst, sondern an die Objektklasse vererbt (Methoden zum Erzeugen von Objekten oder zur Initialisierung von Klassenvariablen).

Neben der Konsistenz des Konzepts ist ein weiterer Vorteil des Metaklassenkonzepts, dass die Klassenmethoden in den Klassen überladen werden können. Gibt es für alle Klassen nur eine einzige Metaklasse, dann gibt es für alle Klassen dieselben Klassenmethoden.

Dieses Metaklassenkonzept ist recht ungewöhnlich. Viele andere objektorientierte Sprachen verzichten darauf. C++ verfügt beispielsweise über keine Klassenmethoden und unterstützt das Metaklassenkonzept nicht. An ihre Stelle treten Systemfunktionen, wie etwa ein Operator mit Namen new, um die Instanz einer Klasse zu erzeugen. Diesen new-Operator gibt es in Java ebenfalls. Sogar mit der gleichen Aufgabe. Er ist nur bedeutend schlüssiger in das logische Konzept integriert.

5.3.3 Abstrakte Klassen

Als letzten Klassentyp besprechen wir an dieser Stelle die abstrakten Klassen. Unter einer abstrakten Klasse versteht man eine Klasse, von der nie eine direkte Instanz benötigt wird und auch keine Instanz direkt erstellt werden kann. Eine solche abstrakte Klasse dient zu einem oder mehreren allgemeinen Verweis(en). Signifikante Eigenschaft ist, dass abstrakte Klassen keine Implementierung einer Methode enthalten dürfen und damit auch nicht instanzierbar sind. Java charakterisiert abstrakte Klassen mit dem Schlüsselwort abstract.

Abstrakte Klassen werden im Allgemeinen zur Strukturierung eines Klassenbaums verwendet. Eine weitere Aufgabe von ihnen ist die Vereinbarung von Methodenunterschriften, auch wenn die Methoden selbst erst zu einem späteren Zeitpunkt implementiert werden. Die Namen der Methoden und deren Parameter sind dann trotzdem in den Unterklassen schon bekannt. Abstrakte Klassen dienen außerdem dazu, zwei oder mehrere Klassen zusammenzufassen, die zwar einige Gemeinsamkeiten besitzen, aber nicht unabhängig voneinander herzuleiten sind und wo es ist nicht offensichtlich ist, welche die Superklasse der anderen ist. In einer abstrakten Oberklasse können die Gemeinsamkeiten zusammengefasst werden.

5.4 Vererbung

Die Beziehung der ursprünglichen Klasse, der Superklasse, zur abgeleiteten, der Subklasse, ist immer streng hierarchisch und heißt Vererbung. Abgeleitete Klassen übernehmen die Eigenschaften und Methoden aller übergeordneter Klassen, wobei Übernehmen nicht bedeutet, dass eine Subklasse die Befehle und Eigenschaften der Superklasse in ihre eigene Deklaration kopiert. Statt dessen gibt es nur eine formale Verknüpfung zwischen den Klassen (eine Art von Zeiger). Mit anderen Worten: Die abgeleitete Klasse verwendet bei Bedarf die Methoden oder Eigenschaften der Superklasse. Den Mechanismus kann man sich analog dem Verwenden von Bibliotheken und dort implementierten Funktionalitäten vorstellen. Die Methoden- bzw. Eigenschaftenauswahl in einer Klassenhierarchie muss natürlich geregelt sein. Sie erfolgt nach einer einfachen Regel. Ist der Nachrichtenselektor (der Methodenname einer Botschaft) in der Objektklasse des Empfängers nicht vorhanden, so wird die gewünschte Methode oder Eigenschaft in der nächst höheren Superklasse des Nachrichtenselektors gesucht. Ist sie dort nicht vorhanden, erfolgt die Suche in der nächst höheren Klasse, bis die oberste Superklasse der Klassenbaums erreicht ist (bei Java die Klasse Object). Die Ausführung der Methode bzw. der Zugriff auf die Eigenschaft erfolgt also in der ersten Klasse, in der sie gefunden wird (von der aktuellen Klasse in der Hierarchie aufwärts gesehen). Gibt es im Klassenbaum keine Methode bzw. Eigenschaft des spezifizierten Namens, so kommt es zu einer Fehlermeldung. Klassen auf derselben Ebene oder in anderen Zweigen werden nicht durchsucht (siehe Abbildung 5.5).

Subklassen werden ihre Erbschaft in der Regeln nicht unverändert lassen. Sie können mit ihren ererbten Eigenschaften und Methoden Einiges anstellen, wie beispielsweise:

Abbildung 5.5:  Vererbung über mehrere Ebenen

5.4.1 Mehrfachvererbung

Der Begriff Mehrfachvererbung gehört zu einer Betrachtung der OOP einfach dazu, obwohl Java explizit auf Mehrfachvererbung verzichtet. Aus gutem Grund, wie man bei solchen Sprachen sieht, die Mehrfachvererbung realisiert haben (etwa C++).

Bei der oben beschriebenen Vererbung gilt für die Klassenhierarchie in einer baumartigen Struktur die Voraussetzung, dass eine Subklasse immer nur genau eine Superklasse hat. Man nennt eine solche Struktur Einfachvererbung (Single Inheritance).

Einige objektorientierte Programmiersprachen (beispielsweise C++) bieten jedoch die Möglichkeit, eine Klasse mit mehreren (bzw. beliebig vielen) Superklassen durch die Vererbung zu verknüpfen. Dies nennt man Mehrfachvererbung (Multiple Inheritance). Objekte der Subklasse erben Eigenschaften aus verschiedenen Superklassen.

Abbildung 5.6:  Mehrfachvererbung

Es gibt unbestritten einige gute Gründe für die Mehrfachvererbung. Der entscheidende Vorteil der mehrfachen Vererbung liegt in der Möglichkeit, die Probleme der realen Welt einfacher beschreiben zu können, denn auch dort hat ein Objekt Eigenschaften und Fähigkeiten aus verschiedenen übergeordneten logischen Bereichen.

Ein Saxophon gehört nicht nur zu der Klasse der Holzblasinstrumente und diese wiederum zur Klasse der »Musik erzeugende Dinge«. Ein Saxophon kann man ebenso zur Superklasse der »Nervenden Krachmacher« - für Nachbarn :-) - und zusätzlich in gleicher Weise zur Superklasse der »Stress erzeugenden Ereignisse« - ebenfalls für Nachbarn und vor allem für Saxophonlehrer bei entsprechend begabten Schülern ;-) - zählen. Es erbt seine Eigenschaften aus den diversen Superklassen. Damit lässt sich das Beziehungsgeflecht des realen Objekts durch die Sammlung der Superklassen (relativ) vollständig beschreiben. Ein weiterer Vorteil ist, dass man leicht Dinge ergänzen kann, die man bei der ersten Realisierung einer Klasse vergessen hat. Wenn bestimmte Eigenschaften und Methoden in einer Subklasse vergessen wurden, nimmt man einfach eine weitere Superklasse hinzu, die die fehlenden Elemente vererben kann.

Diese Vorteile einer relativ vollständigen und einfachen Abbildung der Natur stehen aber in keinem Verhältnis zu den damit eingekauften Nachteilen. Die schlimmsten Nachteile sind sicher die kaum nachvollziehbaren Beziehungsgeflechte in komplexeren Programmen. C++-Programmierer (vor allem diejenigen, die ein Programm übernehmen mussten) wissen davon ein Lied zu singen. Wartbarkeit wird bei exzessivem Einsatz der Mehrfachvererbung zum Fremdwort. Java arbeitet deshalb ohne Mehrfachvererbung. Dafür bietet Java einen ähnlich leistungsfähigen Mechanismus, der dennoch die Wartbarkeit erhält - das Konzept der Schnittstellen. Diese gehören aber weniger in die Abhandlung über die allgemeinene Begriff der OOP, sondern werden später an geeigneten Stellen ausführlich diskutiert.

5.5 Überschreiben

Eine weitere fundamentale Möglichkeit der OOP ist das Überschreiben von bereits im Klassenbaum vorhandenen Methoden, um damit eine Spezialisierung von Objekten zu erreichen.

Überschreiben (Overriding) bedeutet, dass bei Namensgleichheit von Methoden in Superklasse und abgeleiteter Klasse die Methoden der abgeleiteten Klasse die der Superklasse überdecken.

Überschreiben heißt im Englischen eigentlich Overwriting. Overriding ist jedoch kein Schreibfehler, sondern es bedeutet im Grunde Überdefinieren, wird jedoch im Deutschen zum besseren (?) Verständnis als Überschreiben übersetzt.

Aus der Regel für die Methodenauswahl in einer Klassenhierarchie (ist der Nachrichtenselektor in der Objektklasse des Empfängers nicht vorhanden, so wird die gewünschte Methode in der nächst höheren Superklasse des Nachrichtenselektor gesucht usw., also von unten nach oben) folgt ein solches Überschreiben-Konzept im Prinzip zwingend, denn eine Methode wird auf der Ebene ausgeführt, wo sie zuerst gefunden wird.

5.6 Polymorphismus

Wir haben bereits gesehen, dass Operationen in der OOP denselben Namen haben können, sogar, wenn sie auf dasselbe Objekte angewandt werden können, aber auch, wenn sie zu verschiedenen Objekte gehören. Dies hätte in der prozeduralen Welt mehrere unterschiedliche (auch namentlich) Operationen (Funktionen oder Prozeduren) zur Folge. Das Beispiel der Multiplikation machte es deutlich. Dabei ist es in der OOP nicht unbedingt notwendig, dass die verschiedenen Objekte, auf die die Operation angewandt wird, zur selben Klasse gehören. Auch auf Objekte, die zu unterschiedlichen Klassen gehören, lässt sich dieselbe Operation definieren. Immer wenn ein und dieselbe Operation sich verschieden auswirken kann und die konkrete Erzeugungsklasse irrelevant ist, wird dies Polymorphismus genannt. Wichtig ist, dass es dabei vollkommen unerheblich ist, ob diese Objekte durch Vererbung in Beziehung zueinander stehen oder nicht. Java ist polymorph.

5.7 Binden

Es ist bereits angesprochen worden, dass Objekte gegenseitig per Botschaften in Kontakt treten. Die gerade angesprochenen Möglichkeiten des Polymorphismus und der Vererbung generieren aber ein gewisses Problem. Wie soll eine Nachricht die physikalische Adresse eines Objekts im Speicher finden? Durch Polymorphismus und Vererbung ist der Methodennamen (oft Selektor genannt) ja meist nur eine Verknüpfung zur physikalischen Implementierung (dem Programmcode) der Methode und nicht die Adresse des eigentlichen Speicherbereichs. Damit eine Nachricht die Ausführung einer Methode bewirken kann, muss allerdings irgendwann einmal (vor der Ausführung der Methode zur Laufzeit) eine Zuordnung zwischen dem Selektor und dem tatsächlichen Programmcode der Methode stattfinden. Diesen Vorgang der Zuordnung nennt man Binden oder Linken. Dabei unterscheidet man in der OOP zwei Formen des Bindens, die von dem Bindezeitpunkt abhängig sind: das frühe Binden und das späte Binden.

5.7.1 Frühes Binden

Frühes Binden (Early Binding oder oft statisches Binden genannt) bedeutet, dass ein Compiler schon zum Zeitpunkt der Übersetzung des Programms die tatsächliche physikalische Adresse der Methode dem Methodenaufruf zuordnet. Grundsätzlicher Vorteil ist, dass schon zur Zeit der Kompilierung fehlerhafte Angaben vom Compiler gefunden werden können. Außerdem sind solche früh gelinkten Programme schnell, da die physikalische Zieladresse eines Methodenaufrufs zur Programmlaufzeit schon festliegt und nicht immer wieder neu berechnet werden muss.

Größter Nachteil des frühen Bindens ist die mangelnde Flexibilität bei interaktiven Aktionen, also wenn durch Aktionen - etwa durch einen Anwender - neue Objekte in einem Programm erzeugt und verwaltet werden müssen. Das klingt vielleicht abstrakt, ist jedoch in vielen Programmen notwendig. Denken Sie beispielsweise an Grafikprogramme, wo durch die diversen Zeichenoperationen permanent neue Instanzen von Objekten erzeugt werden können. Ein objektorientiertes Grafikprogramm kann unter anderem aus einer Anzahl von Klassen in einem Grafiksystem bestehen: Linie, Kreis, leeres Rechteck, gefülltes Rechteck usw. Jede dieser Klassen beinhaltet nun beispielsweise eine Methode zur Ausgabe der jeweiligen Figur auf dem Bildschirm. Gemeinsamkeiten dieser Zeichnen-Methoden sind sinnvollerweise in einer Superklasse definiert. Die konkreten Figuren befinden sich in Subklassen und erben die gemeinsamen Merkmale.

Für die Verwaltung aller verwendeten Grafikobjekte werden oft so genannte Listen verwendet. Diese Listen könnten nun so aufgebaut sein, dass dabei Elemente einer Liste Verweise aufeinander enthalten. Die Struktur ähnelt einer Kette. Jedes Element hat zusätzlich einen Verweis auf die konkrete Instanz eines Objekts. In einem Grafikprogramm erstellt der Benutzer nun interaktiv am Bildschirm seine Figuren. Beim Zeichnen eines neuen Objekts durch einen Anwender wird das Listenelement adressiert, eine Verknüpfung mit der Liste hergestellt und die Datenadresse der Instanz eingetragen. Das heißt, der Typ des Listenelements ist ein Verweis auf die Superklasse. Zur Laufzeit wird hier jedoch eine Referenz auf die Subklasse - das konkrete Zeichenobjekt - vermerkt.

Zum Zeitpunkt der Kompilierung kann der konkrete Listenplatz von einem Objekt (etwa einem gefüllten Reckeck) und seinen Methoden nicht bekannt sein, da es ja noch nicht existiert. Also muss beim frühen Linken eine andere, umständliche Variante der Objektverwaltung verwendet werden.

5.7.2 Spätes Binden

Wenn das Linken nun später erfolgen würde, wäre das System bedeutend flexibler. Man nennt diesen Vorgang dann spätes Binden (Late Binding oder dynamisches Binden genannt). Der Begriff ist bereits bei den Eigenschaften von Java aufgetaucht, denn Java verwendet diesen Mechanismus. Hier wird erst zur Laufzeit eines Programms die tatsächliche Verknüpfung zwischen dem Selektor und dem Code hergestellt. Die richtige Verbindung übernimmt das Laufzeitsystem der Programmiersprache. Ein solches System ist viel leichter erweiterbar. Es muss nur bei einer Interaktion eine weitere Subklasse von der grafischen Superklasse und die zugehörige Zeichnen-Methode definiert werden. Die Implementierung ändert sich nicht, da erst zur Laufzeit die Verbindung erfolgt. Damit nimmt man allerdings eine schlechtere Performance in Kauf. Außerdem steigt bei ungesicherten Entwicklungsumgebungen mit spätem Binden die Gefahr von Fehlern, da fehlende oder falsche Methodenaufrufe erst zur Laufzeit des Programms bemerkt werden. Für Java besteht allerdings keine Gefahr, denn dort wird explizit mit diversen Sicherheitsmechanismen beim Kompilieren und Linken dieses Problem beseitigt.

5.8 Die Grundprinzipien der objektorientierten Programmierung

Aus den bisherigen Ausführungen können wir jetzt die Grundprinzipien der OOP herleiten.

5.9 Objektorientierte Analyse und objektorientiertes Design

Im Rahmen der objektorientierten Philosophie tauchen immer wieder zwei weitere Begriffe auf, die eng daran gekoppelt sind und zur Vorbereitung der OOP dienen - die objektorientierte Analyse (OOA) und das objektorientierte Design (OOD). Zwar sprengt eine ausführliche Darstellung dieser beiden vorbereitenden Techniken den Rahmen des Buchs, der Versuch einer kurzen Beschreibung ist der Vollständigkeit halber aber sicher sinnvoll.

5.9.1 Die objektorientierte Analyse

Unter einer Analyse versteht man eine gründliche Untersuchung eines Problems, bevor man versucht, das Problem zu lösen. Vereinfacht gesagt: Nachdenken, bevor man vollendete Tatsachen schafft, die ein Problem nicht optimal (im günstigsten Fall) oder gar nicht lösen.

Die OOA ist nun eine - vielfach als sehr abgehoben und theoretisch bezeichnete - Technik, mit der eine solche Problemanalyse in der objektorientierten Welt in ein strukturiertes Konzept gefasst werden soll. Sie soll nur ganz kurz angerissen werden.

Die OOA umfasst fünf Grundvorgänge:

Es gibt sogar ein aus vier Ebenen bestehendes, sehr theoretisches Schichtenmodell für die OOA, das sich aus den genannten Grundvorgängen ableitet.

Das OOA-Schichtenmodell
1 Klassen- und Objektschicht
2 Strukturschicht
3 Attributschicht
4 Serviceschicht

Tabelle 5.1:   Das OOA-Schichtenmodell

Die OOA steht in der Theorie der Softwareentwicklung immer am Anfang.

5.9.2 Das objektorientierte Design

Das OOD ist der mittlere Zyklus der Softwareentwicklung und wird nach der OOA durchgeführt. In der OOA wird das Problem analysiert und so aufbereitet, dass es nun im OOD für bestimmte Hard- und Softwareplattformen umgesetzt werden kann.

Im OOD gibt es vier Grundvorgänge:

Unter der Berufssparte der OO-Designer werden wahrscheinlich die wenigsten Java-Freunde anzutreffen sein. Java ist durch die Plattformneutralität ein potenter Jobkiller für die Tätigkeit dieser hochspezialisierten OO-Designer. Oder unter Kostenaspekten positiv ausgedrückt: Java reduziert den Aufwand und damit die Kosten für den Mittelbau in der Softwareerstellung erheblich. Statt für diverse Plattformen zu designen, muss unter Java jeder OOD-Vorgang nur einmal durchgeführt werden.

Wie dem auch sei, am Ende dieser beiden Prozesse steht dann die OOP.

5.10 Die Objektorientierung von Java

Java ist eine rein objektorientierte Sprache. Sie ist sogar eine äußerst strenge objektorientierte Sprache. Hier sollen kurz, d.h. ohne allzu ausführliche Diskussion der konkreten Java-Syntax, die wesentlichen Java-Konzepte erläutert werden.

5.10.1 Java-Klassen

Jedes Java-Programm besteht aus einer Sammlung von Klassen. Der gesamte Code, der bei Java verwendet wird, wird in Klassen eingeteilt. Jede Klasse, abstrakt oder nicht, definiert das Verhalten eines Objekts durch verschiedene Methoden. Verhalten und Eigenschaften können von der einen Klasse zur nächsten weitervererbt werden. Alle Klassen in Java haben eine gemeinsame Oberklasse, die Klasse Object. Diese wiederum verfügt nur über eine Metaklasse, aber damit bleibt das objektorientierte Konzept voll konsistent.

Die Einteilung des Codes in Klassen bedarf einer näheren Erläuterung. Um Java möglichst einfach zu halten, gibt es eine Ausnahme, die jedoch geschickt in das Konzept eingepasst wurde. Boolesche Operatoren, Zahlen und andere einfache Typen sind in Java erst einmal keine Objekte. Aber Java hat für alle einfachen Typen so genannte Wrapper-Objekte implementiert. Ein Wrapper-Objekt ist eine spezielle Klasse, die eine Objektschnittstelle für die in Java vorhandenen primitiven Typen darstellt. Diese Wrapper-Objekte erlauben es, dass alle einfachen Typen wie Klassen verwendet werden können.

5.10.2 Polymorphismus und Binden

Java unterstützt die OO-Grundprinzipien Polymorphismus und das Konzept des Late Binding. Gerade das Late Binding trägt zur Sicherheit und Stabilität von Java erheblich bei.

5.10.3 Schnittstellen und Pakete statt Mehrfachvererbung

Obwohl Java streng objektorientiert ist, heißt das nicht, dass in Java alles, was in der OO-Theorie erlaubt ist, tatsächlich realisiert wurde. So unterstützt Java keine Mehrfachvererbung. Das erscheint zunächst als eine große Einschränkung, macht Java-Programme allerdings stabil und leicht wartbar. Der Verzicht auf Mehrfachvererbung erzwingt eine gründlichere Vorbereitung, bevor man losprogrammiert (oder feiner ausgedrückt - eine saubere objektorientierte Analyse). Hier bietet sich dann auch ein neues Beschäftigungsfeld für einige der OO-Designer, die Java um ihren Job gebracht hat. Es ist kaum zu zählen, wie viele Programme nur deshalb so schlecht wartbar und instabil sind, weil man sich am Anfang zu wenig Gedanken gemacht hat und bei fehlenden Funktionalitäten zu stricken angefangen hat. Unter Basic gab es dazu den berüchtigten GOTO-Befehl, andere Sprachen ließen die Implementierung fehlender Daten an beliebigen Stellen zu und bei C++ konnte mehr schlecht als recht einfach noch eine Superklasse hinzugefügt und dort die fehlenden Funktionalitäten untergebracht werden.

Statt der Mehrfachvererbung gibt es unter Java einen anderen Mechanismus, der fast genauso flexibel, jedoch nicht so gefährlich ist. Es wird ein Schnittstellenkonzept verfolgt. Klassen können nicht nur von einer Superklasse erben, sondern auch eine beliebige Anzahl an Schnittstellen implementieren. Java-Schnittstellen sind wie die IDL-(Interface Description Languag>)-Schnittstellen - ein Schnittstellenstandard zum Informationsaustauch verschiedener Programmiersprachen - aufgebaut.

Eine Schnittstelle ist in der Java-Sprache eine Sammlung von Methodennamen ohne konkrete Definition. Obwohl eine einzelne Java-Klasse nur genau eine Superklasse haben kann (und genau eine haben muss!), können in einer Klasse mehrere Schnittstellen implementiert werden.

5.10.4 Überladen, Überschreiben und Überschatten

Bei diesen drei Begriffen handelt es sich um Grundfunktionalitäten der OOP, die für das Verständnis von Java absolut grundlegend sind.

Überladen von Methoden bedeutet, dass in einer Klasse mehrere Methoden mit identischem Namen definiert werden, die sich nur durch verschiedene Parameterlisten unterscheiden. Welche Methode dann vom Programm bei Aufruf angesprochen wird, hängt von der dort verwendeten Anzahl und dem Typ der Parameter ab. Java erlaubt zwar ein solches Überladen von Methoden, jedoch kein Überladen von Operatoren. Die Theorie der Objektorientierung erlaubt ein Überladen von Operatoren im Prinzip analog, jedoch unterstützt Java diesen Mechanismus nicht.

Obwohl wir später noch auf das Überladen von Methoden zurückkommen, soll das nachfolgende kleine Programmbeispiel zwei innerhalb einer Klasse definierte Methoden mit gleichem Namen zeigen. Beide unterscheiden sich nur durch die Anzahl der Parameter.

public class MeineKlasse() {
public int meinemethode(int arg1) {
int berechnung;
berechnung = arg1 + 1;
return berechung;
}
public int meinemethode(int arg1; int arg2) {
int berechnung;
berechnung = arg1 + arg2;
return berechung;
}
}

Überladen kann sich auch über Superklassen erstrecken. Wenn sich die verschiedenen Methoden mit gleichem Namen und unterschiedlichen Parameterlisten in verschiedenen, über Vererbung verbundenen Klassen befinden, funktioniert die Geschichte ebenso. Dies liegt im Wesentlichen daran, dass der Aufruf einer Methode in der Subklasse mit der Parameterliste der Methode aus der Superklasse dort keinen Erfolg hat und die Methode dann in der nächst höheren Ebene (der Superklasse) gesucht wird.

Methoden, die mit dem Schlüsselwort static deklariert werden (Klassenmethoden), können nicht überladen werden.

Der Mechanismus des Überladens von Methoden sollte nicht mit dem bereits beschriebenen Überschreiben (Overriding) verwechselt werden, obwohl der Vorgang sehr ähnlich ist. Auch Überschreiben ist in Java möglich und erluabt eine Spezialisierung von Objekten. Es bedeutet ebenfalls, dass Methoden einer Subklasse die Methoden der Elternklasse komplett überschreiben.

Der Unterschied zu Überladen ist mehr formeller als inhaltlicher Natur. Eine Methode zu überschreiben bedeutet, dass die Methode in der Subklasse den identischen Deklarationskopf (also neben der Namensgleichheit auch vollkommen identische Argumente) wie die Methoden der Superklasse erhält (Überschreiben funktioniert also nur zwischen einer Super- und einer Subklasse), während beim Überladen der Methoden zwar die Namen in Super- und Subklasse, aber nicht die Argumente identisch sind. Auch darf beim Überschreiben der Rückgabewert einer Methode nicht geändert werden. Das nachfolgende kleine Beispiel soll den Vorgang skizzieren:

public class Superklasse() {
public int supermethode(int Superarg1) {
int berechnung;
berechnung = Superarg1 + 1;
return berechnung;
}  }
public class Subklasse() extends Superklasse {
public int supermethode(int Superarg1) {
int berechnung;
berechnung = Superarg1 + 2;
return berechnung;
}  }

Die verdeckten Methoden einer Superklasse sind übrigens trotzdem immer noch zu verwenden. In Java macht dies das Schlüsselwort super möglich, also z.B. mit super.supermethode(3);.

Die letzte der drei Überlagerungstechniken in Java ist das Überschatten. Überschattet werden Variablen. Wenn eine Variable in einer Superklasse mit einem bestimmten Typ deklariert wird, so kann in der Subklasse durchaus eine Variable gleichen Namens (mit einem identischen oder auch abweichenden Typ) deklariert werden, die dann dort die Variable der Superklasse überlagert. Das nachfolgende Beispiel skizziert das Verfahren:

class Superklasse() {
int berechnung;
}
class Subklasse extends Superklasse {
double berechnung;
}

Der Zugriff auf die Variable der Superklasse kann unter anderem wieder mit dem Schlüsselwort super erreicht werden.

5.10.5 Globale Vereinbarungen

Als letzte gravierende (positive) Einschränkung verfügt Java nicht über die Möglichkeit, globale Konstanten, Variablen oder Funktionen zu definieren, was jedoch bei strenger Auslegung der OO-Theorie zwingend ist. Bezüglich der Konstanten gilt die Aussage aber nicht ganz so streng, wie sie im ersten Moment erscheint. Über Schnittstellen kann man Konstanten so festlegen, dass sie insofern als global verfügbar gelten, als dass jede Klasse diese Schnittstelle implementiert.

5.11 Zusammenfassung

Java ist explizit objektorientiert, ohne jedoch sämtliche Möglichkeiten des objektorientierten Konzeptes zu realisieren (etwa Verzicht auf Mehrfachvererbung und Überladen von Operatoren). Dieses Fehlen von einzelnen Bestandteilen steht jedoch in keinerlei Widerspruch zu den objektorientierten Paradigmen.

5.12 Plattformunabhängigkeit von Java und die JVM

Java ist sowohl eine Programmiersprache als auch Bestandteil eines modernen EDV-Systems, dessen wichtigster Bestandteil ein neues, vollkommen plattformunabhängiges Betriebssystem für ein nahezu unbeschränktes Einsatzfeld ist. Statt plattformunabhängig wird gerne der Begriff architekturneutral genommen, was jedoch dasselbe bedeutet. Eine erheblich größere Fehlertoleranz, eine leichtere Bedienbarkeit und eine bedeutend bessere Stabilität als bei bisherigen Betriebssystemen sind Charakteristika der Java-Umgebung.

Java-Applikationen können auf Rechnern unterschiedlichster Konfiguration laufen, sogar auf sämtlichen elektrischen Geräten, die irgendwelche Daten verarbeiten sollen. Denken Sie nur an Videorecorder, die bereits seit geraumer Zeit (mehr schlecht als recht) zu programmieren sind. Java erlaubt es, die Zahl und Funktionalität der Geräte auszudehnen. Vorstellbar sind Kaffeemaschinen, die von unterwegs aus dem Auto heraus per Internet gestartet werden, sodass der Kaffee in dem Moment fertig ist, wenn man zur Tür hereinkommt. Es gibt die verschiedensten Anwendungsmöglichkeiten für Java. Der Zukunftsmarkt ist auf Grund der Plattformunabhängigkeit gigantisch.

Wie realisiert nun Java diese Plattformunabhängigkeit? Java ist sowohl auf der so genannten Quellebene, als auch der Binärebene plattformunabhängig. Beschäftigen wir uns zunächst mit der binären Plattformunabhängigkeit.

5.12.1 Die binäre Plattformunabhängigkeit von Java

Bevor eine Java-Anwendung oder ein Java-Applet ausgeführt werden kann, muss der Java-Quellcode in so genannten Bytecode kompiliert werden. Dies erledigt der Java-Compiler, wie wir bereits mehrfach gesehen und angewendet haben. Der daraus resultierende Bytecode ist binär, aber immer noch architekturunabhängig. Der Preis dafür ist, dass dieser Bytecode nicht vollständig und noch nicht lauffähig ist. Bytecode kann man sich als eine Reihe von binären Anweisungen vorstellen, die zwar wie Maschinencode aussieht, jedoch im Gegensatz dazu nicht spezifisch an den Befehlssatz eines bestimmten Prozessors gebunden ist. Diese konkreten Anweisungen fehlen noch. Das daraus resultierende Problem ist dann natürlich, dass Java-Bytecode keine expliziten Prozessorbefehle enthalten kann. Java-Bytecode wird deshalb gelegentlich als ein architekturneutrales Object-Codeformat bezeichnet.

Schauen wir uns den Fall an, wenn beim Kompilieren von Quellcode bereits auf einem Prozessor lauffähiger Code entsteht. Beim Kompilieren eines in einer Sprache wie PASCAL oder C/C++ geschriebenen Quelltextes entsteht ein solcher maschinenabhängiger Code, dessen Anweisungen vom jeweiligen Zielprozessor direkt interpretiert werden können. Den Unterschied zu Java-Bytecode kann man sich über die Mengenlehre verdeutlichen. Lauffähiger Maschinencode ist eine nicht deckungsgleiche Obermenge von Bytecode. Der noch fehlende Teil muss bei Bytecode also noch irgendwie hinzugefügt werden. Dies übernimmt die virtuelle Maschine.

Da der Java-Bytecode wie gesagt noch nicht direkt den Befehlssatz eines Prozessors steuern kann, benötigt er einen prozessorabhängigen Container, eine Java-Laufzeitumgebung.

Aus diesem Grund verfügt Java immer über einen Java-Interpreter. Der Bytecode ist innerhalb dieser Java-Laufzeitumgebung lauffähig und wird von dieser interpretiert. Erst hier werden die plattformabhängigen Befehle hinzugebunden. Die jeweilige Laufzeitumgebung ist plattformspezifisch und kennt den Befehlssatz des jeweiligen Prozessors, also arbeitet das endgültige Produkt auf dieser spezifischen Plattform. Java-fähige Browser sind beispielsweise solche plattformspezifischen Java-Laufzeitumgebungen, die erst die Verwendung von Java-Applets auf den unterschiedlichsten Plattformen des WWW ermöglichen.

5.12.2 Die JVM-Architektur

Die Interpretation des Java-Bytecodes basiert, wie bereits angedeutet, auf der Definition einer so genannten virtuellen Maschine (Java Virtual Machine, abgekürzt JVM), einem virtuellen Computer, der nur im Speicher eines Rechners zur Laufzeit resistent ist und der vom Bytecode gesteuert wird. Man nennt einen solchen Prozess oft eine Emulation.

Die JVM wird gerne als Herz von Java gesehen und muss auf jedem Rechner vorhanden sein, der Java-Anwendungen ausführen möchte. Die virtuelle Maschine stellt eine Abstraktionsschicht zwischen dem kompilierten Programm, der zugrunde liegenden Hardwareplattform und dem Betriebssystem dar und realisiert die gerade beschriebene Umsetzung des Bytecodes in ausführbare Prozessorbefehle. Die virtuelle Maschine kümmert sich anderseits auch um grundlegende Java-Operationen wie die Objekterstellung und die Müllbeseitigung (Garbage Collection, sprich die Speicherfreigabe mit einer Papierkorbfunktion).

Wir beziehen uns hier im Wesentlichen auf die JVM von Sun bzw. JavaSoft. Es gibt aber noch andere JVMs von weiteren Herstellern, die nicht unbedingt voll kompatibel sein müssen und sich vom Aufbau her unterscheiden können. Die wichtigsten Punkte werden jedoch übereinstimmen.

Die JVM ist sehr kompakt und verbraucht wenig RAM-Speicherplatz. Sie soll ja in beliebige elektronische Geräte passen. Konkret läuft der Prozess vom Java-Sourcecode zum lauffähigen und architekturneutralen Java-Programm wie folgt ab:

1. Eine Java-Sourcedatei wird erstellt und in der Regel unter dem Namen ihrer öffentlichen Klasse mit der Erweiterung .java gespeichert.
2. Der Java-Compiler liest die Dateien mit der Erweiterung .java ein und konvertiert den Java-Sourcecode in Bytecode. Die resultierende Bytecode-Datei bekommt den gleichen Namensstamm, jedoch die Erweiterung .class.
3. Die JVM liest den Bytecode-Datenstrom aus der .class-Datei als Folge von Anweisungen (der so genannte Bytecode-Strom). Dieser Strom von Anweisungen hat immer die gleiche Struktur. Jede Anweisung beginnt mit einem opcode (Operationscode). Dieser ist ein Bit lang und ein spezifischer und wieder erkennbarer Befehl. Auf Grund der Länge kann er nur null (keine weiteren Operanden) oder ungleich null (mehrere Operanden, mit denen der opcode vervollständigt werden muss, folgen) sein. Der opcode instruiert die virtuelle Maschine, was genau zu tun ist. Wenn die JVM mehr als nur den opcode für die Ausführung einer Aktion benötigt, ist dieser wie gesagt ungleich null und es folgt dem opcode ein oder mehrere Operand(en).

Die JVM besteht aus vier Teilen:

1. Stack
2. Register
3. Garbage Collection Heap
4. Methodenbereich

Gelegentlich wird die JVM auch in fünf Basisteile untergliedert:

1. Bytecode-Anweisungen
2. Stack
3. Register
4. Heap
5. Methodenbereich

Manche Quellen nehmen also die Bytecode-Anweisungen direkt in die Gliederung mit auf, was jedoch keinen wesentlichen Unterschied ausmacht.

Schauen wir uns die Details genauer an. Zuerst soll der Java-Stack betrachtet werden. Die virtuelle Maschine basiert im Wesentlichen auf diesen Stacks. Unter einem Stack versteht man einen Befehlsstapel. Der Rahmen eines Java-Stacks ist mit dem Stack in herkömmlichen Programmiersprachen durchaus vergleichbar. Er enthält den Zustand eines Methodenaufrufs und den Zustand von verschachtelten Methodenaufrufen. Da die JVM primär auf Stacks basiert, werden die Parameter in der virtuellen Maschine dort gespeichert. Das Weitergeben und Empfangen von Argumenten läuft in der JVM immer über den Stack. Ein Stack in Java dient also der Bereitstellung von Parametern für Bytecodes und Methoden zum Aufnehmen der Ereignisse.

Die Größe einer Adresse in der JVM beträgt immer 4 Byte, also 32 Bit. Deshalb können bis zu 4 Gigabyte Speicher adressiert werden (2 hoch 32, also 4.294.967.296 Bit). Die oben genannten Komponenten der JVM (der Stack, der Garbage Collection Heap und der Methodenbereich) befinden sich innerhalb dieser 4 Gigabyte. Die Java-Methoden sind auf 32 Kilobyte als Größe für jede einzelne Methode beschränkt.

Alle Prozessoren benötigen für ihre Funktionalität so genannte Register. Register beinhalten einen Zustand eines Computers, beeinflussen den Betrieb und werden nach jeder Ausführung von Befehlen aktualisiert. Die virtuelle Maschine von Java verwaltet den System-Stack mit den folgenden Registern:

  • pc (Program Counter): Ein Programmzähler, der verfolgt, wo genau das Programm sich in der Ausführung befindet, d.h. welcher Bytecode gerade ausgeführt wird. Weiter enthält der Programmzähler die Adresse des Bytecodes, der als nächstes ausgeführt werden soll. Zusätzlich wird das pc-Register dafür verwendet, wenn Ausnahmen und catch-Klauseln abgearbeitet werden.
  • optop: Ein Zeiger oder Pointer, der auf die Spitze des Operanden-Stack zeigt. Er dient im Wesentlichen zur Auswertung arithmetischer Ausdrücke.
  • frame: Ein Pointer, der auf die aktuelle Ausführungsumgebung der gerade ausgeführten Methode zeigt. Dieser Pointer enthält einen Aktivierungsdatensatz für den Aufruf dieser Methode und diverse Debugging-Informationen.
  • vars: Ein weiterer Pointer, der auf die erste lokale Variable der aktuell ausgeführten Methode zeigt.

Weitere Register verwendet Java nicht. Die JVM gebraucht dementsprechend nur vier Register mit der Länge eines Bytes, simuliert also einen Prozessor mit einem 32-Bit-Register. Aus diesem Grund benötigen die primitiven Datentypen double und long durch ihre Datenbreite von 64 Bit jeweils zwei Positionen auf dem Operanden-Stack (jeweils 32 Bit groß) zur Darstellung.

Von dem Java-Programm wird der Bytecode an die JVM weitergegeben und erzeugt einen Stack-Frame für jede Methode. Jeder Frame enthält drei Arten von Informationen - die lokalen Variablen für den Methodenaufruf, die Ausführungsumgebung und den Operanden. Diese jeweiligen Datenmengen können unter Umständen auch leer sein. Die Größe der ersten beiden Elemente steht zu Beginn eines Methodenaufrufs fest, während die Größe des Operanden bei der Ausführung des Bytecodes der Methode variieren kann.

  • Die lokalen Variablen: Sie werden in einem Array von 32-Bit-Variablen gespeichert, auf die von dem vars-Register gezeigt wird (im Word-Offset).
  • Die Ausführungsumgebung: In der Ausführungsumgebung (Execution Environment) eines Stacks wird die Methode ausgeführt. Das frame-Register zeigt auf die Ausführungsumgebung. In der Ausführungsumgebung ist ein Pointer auf den vorherigen Stack, ein Pointer auf die lokale Variable des aktuellen Methodenaufrufs und je ein Pointer auf das momentane obere und untere Ende des Stacks im Speicher enthalten. Diese Informationen können neben einigen weiteren Hinweisen gut zum Debugging genutzt werden. Besonders für das dynamische Linken ist die Ausführungsumgebung wichtig, denn erst durch die Referenzen auf die Symboltabellen des Interpreters für die aktuelle Methode und die aktuelle Klasse wird dieses erst ermöglicht.
  • Der Operanden-Stack: Er arbeitet nach dem FIFO-(First-in, First-out)- Prinzip und ist 32 Bit groß. Verwendet wird er zum Speichern der Parameter und Rückgabewerte der meisten Bytecode-Anweisungen und er enthält noch weitere Argumente, die für den opcode notwendig sind. Jeder primitive Java-Datentyp hat eindeutige Anweisungen, wie er sich beim Herausziehen, Verwenden und Zurückschieben der Operanden des jeweiligen Typs zu verhalten hat. Die Typen in einem Stack und deren Anweisungen müssen kompatibel sein, was unter Java natürlich sicher gestellt ist. Die Spitze des Operanden-Stack wird von dem optop-Register indexiert und ist in der Regel mit der Spitze des gesamten Java-Stacks identisch. >

Der Garbage Collection Heap oder auch nur Heap (Haufen) ist derjenige Teil des Hauptspeichers, dem neu erstellte Klasseninstanzen (Objekte) zugewiesen werden. Jedesmal wenn ein neues Objekt erstellt wird, kommt dieser Speicher aus dem Heap. In Java hat der Heap meist eine feste, voreingestellte Größe, sobald das Java-Laufzeitsystem gestartet wird. Bei Computersystemen, wo die Technik des virtuellen Speichers (also Auslagerung von Hauptspeicher auf die Festplatte bei Bedarf) unterstützt wird (etwa Windows), kann die Größe des Heap entsprechend ausgedehnt werden. Die Freigabe des Heap, wenn ein Objekt nicht mehr benötigt wird, übernimmt ein automatischer Prozess, der Garbage Collection (automatische Speicherbereinigung) genannt wird.

Diese Speicherbereinigung mit der Garbage Collection erleichtert die Arbeit mit Objekten gegenüber vielen anderen objektorientierten Sprache erheblich. Da in Java Objekte einer automatischen Müllbereinigung unterliegen, braucht sich ein Programmierer normalerweise nicht darum zu kümmern und den Speicher nicht mehr - wie etwa unter C/C++ oder Delphi - manuell freigeben. Eine manuelle Speicherfreigabe ist unter Java sogar gar nicht vorgesehen, Sie können höchstens den Garbage-Collection-Prozess direkt aufrufen. Jedoch ist dies in den meisten Fällen nicht notwendig und auch nicht zu empfehlen. Die Laufzeitumgebung verfolgt alle Referenzen auf ein Objekt in dem Heap und gibt automatisch den Speicher frei, der von Objekten belegt wird, die nicht mehr länger referenziert werden, sobald die CPU dafür Kapazität frei hat.

Auf Java-Objekte wird zur Laufzeit nur indirekt über so genannte Handles - eine Art Pointer auf den Heap - zugegriffen. Deshalb können Bereinigungsprozesse parallel ablaufen. Diese laufen als eigener Thread ab und können nach eigenem Gutdünken (der JVM) Objekte löschen oder verschieben. Speicherbereinigungen laufen also in der Regel als ein permanenter Hintergrundthread ab.

Die JVM verfügt neben den bereits diskutierten Speicherbereichen über zwei weitere Speicherbereiche, die als Methodenbereich von Java bezeichnet werden:

  • Der eigentliche Methodenbereich
  • Der Constant Pool (Konstantenpool)

Wie bei konventionell kompilierten Programmen speichert der eigentliche Methodenbereich bei Java die Java-Bytecodes. Zusätzlich werden dort die zum dynamischen Verknüpfen benötigten Symboltabellen und andere Informationen (etwa Debug-Informationen) gespeichert.

Der Constant Pool existiert in Java als ein an jede Klasse angehängter Heap. Diese normalerweise vom Java-Compiler erzeugten Konstanten codieren alle Namen (von Methoden, Variablen usw.), die von einer Methode der jeweiligen Klasse verwendet werden. Die Größe des Constant Pool differiert je nach Anzahl der zu kodierenden Namen für jede Klasse. Die Klasse »weiß« um die Menge der Konstanten und spezifiziert mit einem Symbol, wo in der Klassenbeschreibung das Konstanten-Array beginnt. Die Konstanten werden mit speziell codierten Bytes typisiert und haben innerhalb der .class-Datei ein genau definiertes Format.

Es gibt in Java keine Begrenzung, ab wo und bis wohin diese beiden Speicherbereiche existieren müssen. Damit wird die JVM portabler und sicherer.

5.12.3 Die Datentypen der JVM

Java-Datentypen unterscheiden sich in einigen Details von gewohnten Datentypen aus vielen anderen Programmiersprachen. Im Wesentlichen unterscheiden sie sich in der Länge (besonders auffällig ist der char-Typ, der nicht wie sonst meist üblich aus einem Byte besteht), aber auch in ein paar weiteren Einzelheiten. Die JVM verarbeitet im Detail die folgenden Typen primitiver Daten:

Typ Länge Kurzbeschreibung
byte 8 Bit Kleinster Wertebereich mit Vorzeichen. Wird zum Darstellen von Ganzzahlwerten von (- 2 hoch 7 = -128) bis (+ 2 hoch 7 - 1 = 127) verwendet.
short 16 Bit Wertebereich mit Vorzeichen. Kurze Darstellung von Ganzzahlwerten von (- 2 hoch 15 = - 32768) bis (+ 2 hoch 15 - 1 = 32767).
int 32 Bit Standardwertebereich mit Vorzeichen zur Darstellung von Ganzzahlwerten. Bereich von (- 2 hoch 31 =
- 2147483648) bis (+ 2 hoch 31 - 1 = 2147483647).
long 64 Bit Größter Wertebereich mit Vorzeichen zur Darstellung von Ganzzahlwerten. Bereich von (- 2 hoch 63 =
- 9,223372036855e+18) bis (+ 2 hoch 63 - 1 = 9,223372036855e+18 - 1).
float 32 Bit Kürzester Wertebereich mit Vorzeichen zur Darstellung von Gleitkommazahlwerten. Es existiert ein Literal zur Darstellung von plus/minus unendlich, sowie der Wert NaN (Not a Number) zur Darstellung von nicht definierten Ergebnissen.
double 64 Bit Größter Wertebereich mit Vorzeichen zur Darstellung von Gleitkommazahlwerten. Es existiert wie beim Typ float ein Literal zur Darstellung von plus/minus Unendlich, sowie der Wert NaN (Not a Number) zur Darstellung von nicht definierten Ergebnissen.
char 16 Bit Vorzeichenlose Darstellung eines Zeichens des Unicode-Zeichensatzes.
boolean 1 Bit Kann nur die Werte true (wahr) oder false (falsch) annehmen.

Tabelle 5.2:   Primitive Datentypen der JVM   von Sun

Falls Sie bereits Kenntnisse in anderen Programmiersprachen haben, fällt Ihnen vielleicht auf, dass in der Liste der primitiven Datentypen keine Arrays vorhanden sind. Dies liegt daran, dass Arrays in Java als Objekte gesehen werden und keine eigene Darstellung besitzen. Dies ist übrigens ein nicht zu unterschätzender Sicherheitsaspekt von Java.

Es gibt - wie schon angedeutet - virtuelle Maschinen, die nicht von Sun stammen (etwa von Microsoft). Einige dieser anderen virtuellen Maschinen besitzen noch zwei weitere Datentypen:

Typ Länge Kurzbeschreibung
object 32 Bit Referenz auf eine Java-Instanz.
returnAddress 32 Bit Wert mit Anweisungen für die Befehle
jsr/ret/jsr_w/ret_w.

Tabelle 5.3:   Optionale primitive Datentypen einiger JVM

5.12.4 Die Java-Plattformunabhängigkeit auf Quellebene

Plattformunabhängigkeit auf Quellebene bedeutet, dass die primitiven Datentypen einer Programmiersprache konsistente Größen auf allen Entwicklungsplattformen haben.

Wenn ein primitiver Datentyp wie beispielsweise ein Integer-Wert unter der einen Plattform die Größe von einem Byte hätte, unter einer anderen Plattform die Größe von zwei Byte, wäre die Plattformunabhängigkeit auf Quellebene nicht mehr gegeben. Bei fast allen anderen Programmiersprachen gibt es diese Probleme. Java hingegen enthält umfangreiche Klassenbibliotheken, um Standards für Datentypen festzulegen und damit das Schreiben von Code für verschiedene Plattformen überflüssig zu machen. In Java wird die Größe jedes einfachen Datentyps in den umfangreichen Klassenbibliotheken genau spezifiziert. Genauso wichtig ist jedoch auch die Arithmetik, d.h. wie sich diese einfachen Datentypen gegenüber Berechnungsvorschriften verhalten. Auch hier legt Java in den Klassenbibliotheken genaue Vorschriften fest. Fast alle wichtigen Prozessoren unterstützen diese Java-Spezifikation und außerdem enthalten die Java-Bibliotheken portierbare Schnittstellen für die wichtigsten Plattformen in der Computerwelt. Damit wird - bis auf ganz wenige Ausnahmen - eine nahezu perfekte Plattformunabhängigkeit auf Quellebene gewährleistet.

5.13 Zusammenfassung

In diesem Kapitel haben wir für das Verständnis von Java unabdingbare Hintergründe besprochen, insbesondere das Konzept der objektorientierten Programmierung. Objektorientierung hebt die Trennung von Daten und Anweisungen auf. Dabei tauschen Objekte Nachrichten aus und reagieren darauf. Klassen und daraus erzeugte Objekte erben Fähigkeiten und Eigenschaften der zugehörigen Superklasse, und Subklassen können geerbte Fähigkeiten und Eigenschaften erweitern und verändern. Dabei erhalten die Objekte ihre Fähigkeiten und Eigenschaften durch Zugehörigkeit zu der Klasse, aus der sie als Instanz erstellt werden. Wenn Objekte zu verschiedenen Klassen gehören, werden sie auf die gleiche Nachricht unterschiedlich reagieren (Polymorphismus).

Grundsätzlich ist Java eine äußerst strenge objektorientierte Sprache, schränkt aber das denkbare Maximum in den Bereichen ein, wo es sinnvoll ist.

1

Um Lesermails vorzubeugen - das ist in der Tat so: Saxophone zählen zur Familie der Klarinetten. Sie sind zwar aus Metall, besitzen aber ein Holzblättchen zur Tonerzeugung und fallen deshalb unter die Klasse der Holzblasinstrumente.

2

Oder, was zuerst da war: die Henne oder das Ei?


© Copyright Markt+Technik Verlag, ein Imprint der Pearson Education Deutschland GmbH
Elektronische Fassung des Titels: Java 2 Kompendium, ISBN: 3-8272-6039-6 Kapitel: 5 Java-Hintergründe