![]() |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Ziel dieses Teilkapitels ist es, dem Leser die Grundfunktionalität der Programmiersprache zu erläutern. Nach Lektüre dieses Teils des Buches sollte der Leser daher in der Lage sein, Java-Programme in einer beliebigen Komplexität zu entwickeln. Dies umfasst nicht die Grafikprogrammierung sowie erweiterte Funktionen, wie bspw. Sicherheitsroutinen oder Netzwerkprogrammierung. Insofern stellt dieser Teil des Buches eine Zusammenfassung der Funktionen bereit, die von den meisten prozeduralen Programmiersprachen, bspw. auch C oder Pascal, angeboten werden. Bezeichner Die Namen von Variablen, Methoden, Klassen, Packages und Interfaces werden im Folgenden Bezeichner genannt. Bezeichner bestehen in Java aus frei wählbaren Folgen von Unicode-Buchstaben und Unicode-Ziffern, die aber kein Schlüsselwort sein dürfen. Schlüsselworte sind alle die Namen, die bspw. Programmanweisungen (bspw. if, then oder else) sind, die also in der Programmiersprache Java eine besondere Bedeutung haben. Unicode ist ein Zwei-byte-Code, mit dem eine Vielzahl an länderspezifischen Zeichen repräsentiert werden können (bspw. auch griechische Zeichen). Die ersten 128 Unicode-Zeichen (0x0000 bis 0x007F) entsprechen genau den ASCII-Zeichen. Datentypen und Variablen Um in Java programmieren zu können, müssen zunächst Variablen und deren Datentypen definiert werden. Variablen sind abstrakte Platzhalter für konkrete Inhalte, die im Programmablauf variieren können. Ein Beispiel hierfür ist die Variable adresse, in der der Programmierer bspw. Namen und Anschriften seiner Bekannten speichern kann. An diesem Beispiel wird klar, dass Variablen verschiedene Typen haben können. Während Namen und Anschriften meist vom Typ Zeichenkette sind, sind auch numerische Variablen denkbar. Weiterhin kann man zwischen Basistypen und komplexen Typen unterscheiden. Zeichenkette ist ein Beispiel für einen Basistyp, während der komplexe Typ adresse_typ ein Konglomerat von Basistypen darstellt. Die in Java verfügbaren Basistypen sind in Tab. 3-3 definiert. |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Tab. 3.3: Basistypen in Java Aus der Tabelle wird ersichtlich, dass eine Reihe von Datentypen zur Verarbeitung von Zahlen (byte, short, int, long, float und double) zur Verfügung stehen. Es ist zu beachten, dass diese Datentypen nur in der Kleinschreibung verwendbar sind. Die Verwendung eines Datentyps Short führt daher zu einem Fehler. Alle numerischen Datentypen werden vom System mit dem Wert 0 initialisiert, wenn der Programmierer keine gesonderte Zuweisung vornimmt. Der boole'sche Datentyp boolean wird dazu benutzt, Aussagen auf ihren Wahrheitsgehalt zu überprüfen (true oder false). Die korrekte Antwort auf die Überprüfung (1 == 2) wäre daher false. Zusammengesetzte Typen und Variablen Oftmals ist es wünschenswert, Typen zu verwenden, die erheblich komplexer sind, als die einfachen Basistypen. Ein Beispiel hierfür ist ein Typ, mit dem die Koordinaten eines Punktes im dreidimensionalen Raum verwaltet werden können. Es ist einleuchtend, dass die getrennte Verwaltung der x-, y- und der z-Koordinaten einen unnötigen Aufwand verursachen würde. Bedenkt man aber, dass Java eine objektorientierte Sprache ist, so bietet sich die Modellierung von komplexen Typen in Objekten an, sog. Klassen. Klassen können daher nicht nur wie bisher erläutert, eine Menge von Variablen- und Methodendefinitionen enthalten, sondern auch lediglich eine Sammlung von Variablen darstellen. Eine Klasse ist daher allgemeiner als eine Vorlage anzusehen, mit der verschiedene Objekte mit ähnlichen Eigenschaften geeignet modelliert werden können. An dieser Stelle muss deutlich darauf hingewiesen werden, dass sich hier die Definition von Typ und Variable nicht mehr trennen lässt. Während die Koordinaten eines Punktes vom Typ Integer sind, sind die Bezeichnungen x, y und z bereits Variablen. Die Klassendefinition selber stellt allerdings wieder eine Typbezeichnung dar. Die Definition des Typs Punkt könnte dann wie folgt aussehen:
class Punkt { int x,y,z; } Um die Definition von Typ und Variable klarer abzugrenzen, ist folgende Notation hilfreich: unter einem Typ versteht man ein eigenständiges Element eines Computerprogramms, das eine Gruppe von Eigenschaften und Funktionen repräsentiert, die zueinander in einer inhaltlichen Beziehung stehen. Eine Variable bezeichnet dann eine Instanz eines Typs, die mit konkreten Werten belegt werden kann und deren Name vom Anwender frei gewählt werden kann. Als Beispiel betrachte man den Typ Punkt. Punkt kann beliebige Werte der drei Koordinaten annehmen und auch mehrfach verwendet werden, da bspw. die drei Variablen punkt 1, punkt 2 und punkt 3 den Typ Punkt verwenden. In punkt 1, punkt 2 und punkt 3 können den Variablen x, y und z jeweils unterschiedliche Werte zugewiesen werden. Bevor eine Klasse bzw. ein Basistyp verwendet werden kann, muss sie zunächst definiert werden. Dies erfolgt immer in der Notation
Typname Variablenname; Variablendefinitionen stehen in Java immer zu Beginn einer Klassen- bzw. Methodendefinition. Die in manchen Programmiersprachen vorzufindende Unsitte, Variablen dann zu definieren, wenn sie benötigt werden, ist hierbei (zu Recht) unzulässig. Es ist einleuchtend, dass ein Durchsuchen eines Code-Stücks nach Variablen nicht besonders zweckmäßig ist. Eine Ausnahme hierzu sind allerdings die später erläuterten lokalen Variablen, die nur in einem Programmblock gültig sind. Betrachtet man nun die bereits eingeführten Java-Applications, so findet man in diesen immer eine main-Methode vor, die in eine Klassendefinition eingebettet ist. Eine Beispielklasse mit Variablendefinitionen ist in Abb. 3-6 dargestellt. Die Funktion dieser Klasse wurde bereits im Kontext von Abb. 3-4 erläutert.
Abb. 3.6: Beispiel-Application Neu ist nun allerdings, dass einer Variablen ein Wert zugewiesen wurde, in diesem Fall der Variablen zahlen der Wert 1234. Allgemein können Variablen immer dann Werte zugewiesen werden, wenn diese bereits mit einer Typangabe definiert sind. Selbsterklärende Beispiele hierfür sind:
int zahlen = 4; Man erkennt, dass die Zuweisung auch direkt nach der Definition der Variablen erfolgen kann, um Platz zu sparen. Nach der Notation des Typs können auch mehrere Variablennamen folgen. Auch die Zuweisung von Klassen zu Variablennamen erfolgt analog zur Zuweisung zu Basistypen, was man am Beispiel der Klasse Punkt erkennt. Bei der Initialisierung der Variablen sind die folgenden Besonderheiten zu beachten:
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Tab. 3.4: Sonderzeichen in Zeichenketten Ausdrücke und Operatoren Unter einem Ausdruck versteht man eine Anweisung, die zu einer Auswertung führt. Einige Beispiele für Ausdrücke wurden bereits vorgestellt, bspw.
int a = 1; Das letzte Beispiel stellt eine Multiplikationsausdruck dar, da sich der Wert der Variablen c durch eine Multiplikation der Variablen a und b (die vorab definiert werden müssen) ergibt. Ausdrücke resultieren aus Wertezuweisungen. Neben der bereits angegebenen Form der Zuweisung werden in Java die folgenden speziellen Arten der Wertezuweisung verwendet:
In Java werden die in Tab. 3-5 angegebenen Operatoren verwendet. |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Tab. 3.5: Operatoren in Java Es ist zu beachten, dass einige der Operatoren typabhängig verwendet werden. Das Ergebnis des Ausdrucks 3/2 ist daher dann 1 (und nicht 1.5), wenn das Ergebnis einer Variablen vom Typ int zugewiesen wird. Wird in einem Ausdruck mehr als ein Operator verwendet, so muss man die Auswertungsreihenfolge der Operatoren kennen. Grundsätzlich können die Operatoren abgestuft nach ihrer Wichtigkeit wie folgt eingeordnet werden:
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Tab. 3.6: Auswertungsreihenfolge von Operatoren Wertet man mit Hilfe der Tabelle den Ausdruck a = 1 + 2 / 2 aus, so muss zuerst die Division und anschließend die Addition durchgeführt werden, wodurch man als Resultat 2 erhält. Arbeiten mit Klassen Klassendefinitionen Wie bereits in Abb. 3-4 gezeigt, ist eine Klasse ein Container für Variablen und Methoden, die in einem inhaltlichen Bezug zueinander stehen. Zunächst ist zu unterscheiden, ob eine Klasse als Basisklasse definiert wird, oder ob sie von einer weiteren Klasse abgeleitet wird. Basisklassen werden immer nach der folgenden Syntax
class Basisklasse { // Inhalt der Basisklasse } definiert. Hierbei können durch die vorangestellten beiden Schrägstriche Kommentare eingebracht werden, die die Funktion der Klasse erläutern. Eine ausführliche Kommentierung ist in jedem Fall ratsam, um die angedachte Funktion der Klasse ausreichend zu dokumentieren. Abgeleitete Klassen gehorchen der Syntax
class Subklasse extends Elternklasse { // Inhalt der Subklasse } Eine wichtige abgeleitete Klasse hat der Leser bereits im Zusammenhang mit Applets kennengelernt, die grundsätzlich von der Klasse Applet abgeleitet werden. Nach der Definition des Klassenrumpfes folgt meist eine Liste der verwendeten Variablen. Dies hat in der Art und Weise zu erfolgen, wie bereits oben beschrieben wurde. Eine Besonderheit stellt aber die Definition von Konstanten dar. Konstanten sind eine besondere Art von Variablen, die im Programmablauf niemals ihren Wert ändern. Ein Beispiel hierfür ist die Zahl Pi. Die Definition einer Konstanten erfolgt mittels des Schlüsselworts final, das der Variablendefinition vorangestellt wird. Definiert man also die Zahl Pi, so ist bspw. die Syntax
final float pi = 3.141592; zu verwenden. Eine weitere Besonderheit der Variablendeklaration ist, dass zwischen lokalen und globalen Variablen unterschieden wird. Globale Klassenvariablen werden zur Kommunikation zwischen verschiedenen Objekten der Klasse verwendet bzw., um Informationen zu verwalten, die für die gesamte Klasse Gültigkeit besitzen. Im Gegensatz dazu erfüllen lokale Variable eingegrenzte Aufgaben, wie z. B. Zählfunktionen. Lokale Variablen bezeichnet man häufig auch als Instanzvariablen, da sie nur einen eingeschränkten Gültigkeitsbereich besitzen. Globale Variablen, die man auch als Klassenvariablen bezeichnet, werden generiert, indem der Definition das Schlüsselwort static vorangestellt wird. Ein Beispiel hierfür ist eine Summenvariable, die folgendermaßen definiert wird:
static int summe; Nachdem der Klassenrumpf und die Konstanten bzw. Variablen spezifiziert sind, müssen Methoden entwickelt werden, die die eigentliche Funktionalität der Klasse darstellen. Eine Methodendefinition besteht immer aus den folgenden vier Teilen:
Rückgabetyp Methodenname (Typ_1 Argument_1,..., Typ_n Argument_n) { // Rumpf der Methode } Ein Sonderfall tritt immer dann ein, wenn kein Rückgabewert erforderlich ist, bspw. wenn in einer Methode lediglich eine Ausgabe getätigt werden soll. In diesem Fall wird das Schlüsselwort void verwendet, das dann die Angabe des Rückgabetyps ersetzt. Eine mögliche Methodendefinition zur Ausgabe einer Zeichenkette sieht dann wie folgt aus:
void ausgabe () { System.out.println("Vorsicht, in Ihrem Programm ist ein Fehler!!! \n"); } Bei der Entwicklung von Methoden muss besonderer Wert auf die Überprüfung von Gültigkeitsbereichen von Variablen gelegt werden. Eine Variable hat immer einen begrenzten Gültigkeitsbereich. Wird in Java auf eine Variable zugegriffen, so prüft Java zuerst, ob die Variable in dem Funktionsblock (bspw. in einer Zählschleife) gültig ist, in der sie aufgerufen wird. Ist dies nicht der Fall, so wird der nächstgrößere Funktionsblock überprüft (bspw. eine Methode), anschließend die Klasse und deren Superklassen. Unter einer Superklasse versteht man in diesem Zusammenhang eine Klasse, die ihre Eigenschaften auf die momentan verwendete Klasse vererbt hat. In diesem Vorgehen liegt aber eine große Gefahr: Das Verbergen von Variablen. Als ein Beispiel dient bspw. das folgende Programmsegment:
class gueltigkeit { int g = 1; int g = 2; } } Man erkennt, dass in der Methode test_g die Instanzvariable g durch die lokale Variable g maskiert wird, wodurch in der Ausgabe eine 2 erscheint. Dies kann jedoch unbeabsichtigt erfolgt sein. Eine Möglichkeit, dieses Problem zu vermeiden, ist die Verwendung des Schlüsselworts this. Das this-Schlüsselwort bezieht sich immer auf das derzeit aktuelle Objekt, also dasjenige, aus der die Methode aufgerufen wurde. Um auf die Instanzvariable g zuzugreifen, verwendet man also besser den Ausdruck this.g, wohingegen die lokale Variable g auch weiterhin nur mit g bezeichnet wird. Problematisch ist nun aber weiterhin, dass eine Variablendeklaration des gleichen Namens in einer Subklasse und in der Elternklasse zu merkwürdigen und schwer erkennbaren Fehlern führt. Dieses Problem ist nur dann zu umgehen, wenn auf eine saubere Namensgebung bei Variablen geachtet wird. Grundsätzlich sollten in einem Programm niemals Variablen gleichen Namens in einer Klassenhierarchie vorkommen, es sei denn, sie haben ausschließlich lokale Bedeutung (bspw. Zählervariablen). Übergibt man Argumente an eine Methode, so ist zu beachten, dass diese entweder by-Value oder by-Reference übergeben werden können. Ein sog. Call-by-Value bedeutet, dass Werte an eine Methode übergeben werden, mit denen diese rechnet. Die Originalbelegungen der Variablen an der aufrufenden Stelle des Programms bleiben aber unverändert. Anders ist dies bei einem Call-by-Reference. Hier werden komplexe Objekte übergeben (bspw. Listen). Die Methode arbeitet in diesem Fall nicht mit lokalen Kopien der Daten, sondern mit den Originaldaten. Dies kann dazu führen, dass unbeabsichtigt Originale verändert werden. Bei Call-by-Reference ist immer Vorsicht geboten, da Java alle die Argumentübergaben an Methoden als Call-by-Reference auffasst, die keine Basisdatentypen sind. Wie auch bei Variablen unterscheidet man bei Methoden zwischen Klassen- und Instanzmethoden. Klassenmethoden, die durch das vorangestellte Schlüsselwort static gekennzeichnet werden, sind für jede Instanz einer Klasse verfügbar bzw. auch für andere Klassen, die diese Methoden verwenden wollen. Zusätzlich muss keine Instanz einer Klasse zur Verfügung stehen, um mit einer derartigen Methode arbeiten zu können. Es wird nun auch ersichtlich, warum die bereits häufig verwendete Methode main als static deklariert sein muss: Wäre dies nicht der Fall, so müsste die Klasse, die die main-Methode enthält, initiiert werden, bevor main aufgerufen wird. Dies ist aber nicht möglich, da main die erste auszuführende Methode ist. Fehlt das Schlüsselwort static, so ist eine Methode lediglich eine Instanz, die auf einem speziellen Objekt operiert, anstatt auf einer Klasse von Objekten. Eine Besonderheit des Methodenaufrufs in Java ist die Möglichkeit, dieselbe Methode mit verschiedenen Parametern aufzurufen. Hierbei kann sowohl die Anzahl der Parameter variieren, als auch der Datentyp jedes Arguments. Diese Möglichkeit verwendet zwar jeweils denselben Methodennamen, aber verschiedene Signaturen und wird als Überladen bezeichnet. Ein Überladen einer Methode ist immer dann sinnvoll, wenn Methoden benötigt werden, die eine unterschiedliche Parameterzahl und dementsprechende Typen benötigen. Überladene Methoden realisieren also in Abhängigkeit von den Argumenten, die sie als Eingabe erhalten, eine bestimmte Funktionalität. Um eine Methode zu überladen, entwickelt man Methoden, die jeweils denselben Namen haben, die sich aber in der Anzahl und im Typ der Argumente unterscheiden können. Hierbei ist zu beachten, dass zwar die Argumentliste der überladenen Methode variieren kann (einschließlich der Namen der Argumente), nicht aber der Rückgabewert der Methode. Um die Funktionsweise des Überladens zu verdeutlichen, sollen als Beispiel die Koordinaten eines Quadrats programmiert werden. Hierzu kann die folgende Klassendefinition verwendet werden:
class Quadrat { int x1 = 0; } Um nun die korrekten Koordinaten des Quadrats einzurichten, kann die folgende Methode verwendet werden:
Quadrat initialisiereQuadrat (int x1, int x2, int y1, int y2) { this.x1 = x1; } Hierbei ist zu beachten, dass this verwendet werden muss, um auf die entsprechenden Koordinaten der Klasse zugreifen zu können. Diese werden mit einem Punkt an this angehängt. Eine weitere und sicherlich elegantere Möglichkeit, ein Quadrat zu programmieren, ist die Verwendung des Point-Objekts, das in Java bereits standardmäßig zur Verfügung steht. Hierzu muss nun die Methode initialisiereQuadrat folgendermaßen überladen werden:
Quadrat initialisiereQuadrat (Point obenlinks, Point untenrechts) { x1 = obenlinks.x; } Hierbei ist zu beachten, dass das Paket java.awt.Point importiert werden muss, das das Point-Objekt enthält. Nur so erhält Java Kenntnis über den Aufbau des Objekts. Neben der Möglichkeit, die linke obere und die rechte untere Ecke des Quadrats anzugeben, kann auch nur die linke obere Ecke und die Seitenlänge des Quadrats spezifiziert werden. Hierzu muss die Methode initialisiereQuadrat folgendermaßen überladen werden:
Quadrat initialisiereQuadrat (Point obenlinks, int laenge) { x1 = obenlinks.x; } Zur Ausgabe der Koordinaten wird die folgende Methode verwendet:
void ausgabeQuadrat () { System.out.print("<" + x1 + ", " + x2 + ", " + y1 + ", " + y2 + ">"); } Die folgende main-Methode wird zum Aufbau und zur Ausgabe des Quadrats eingesetzt. Hierbei wird der new-Operator verwendet, der anschließend erklärt wird.
public static void main (String arguments []) { Quadrat q = new Quadrat(); } Das gesamte Programm sieht dann wie folgt aus (Datei):
import java.awt.Point; class Quadrat { int x1 = 0; this.x1 = x1; } x1 = obenlinks.x; } x1 = obenlinks.x; } System.out.print("<" + x1 + ", " + x2 + ", " + y1 + ", " + y2 + ">"); } Quadrat q = new Quadrat(); } } Nachdem das Programm übersetzt und ausgeführt wurde, ergibt sich die folgende Ausgabe: <0, 0, 10, 10> Klasseninitialisierung Neben dem allgemeinen Aufbau einer Klasse aus Signatur, Variablen und Methoden muss der Java-Entwickler ebenfalls wissen, wie eine Klasse initialisiert wird und wie er auf die in der Klasse enthaltenen Objekte zugreifen kann. Zum Anlegen einer Klasse dient der new-Operator, der wie folgt verwendet wird:
Random zufallszahl = new Random (); In diesem Beispiel wurde eine Zufallszahl erzeugt, die der Klassendefinition der Klasse Random genügt. Random ist eine in Java bereits vordefinierte Funktionalität. Es ist darauf zu achten, dass die Klammern hinter dem Klassennamen nicht vergessen werden, selbst wenn sie leer sind. In den Klammern werden üblicherweise Argumente übergeben, mit denen eine Klasse initialisiert wird. Im Falle der Klasse Random sind keine Argumente nötig, weshalb lediglich die beiden Klammern angegeben werden. Betrachtet man die bereits vorgestellte Definition der Klasse Punkt
class Punkt { int x,y,z; } so könnte die Initialisierung dieser Klasse mittels der Anweisung
Punkt pt = new Punkt (0,0,0); erfolgen, wozu allerdings die Klasse erweitert werden muss. Offensichtlich muss eine Methode vorhanden sein, in der die Initialisierung stattfindet. Diese wird auch als Konstruktor der Klasse bezeichnet und muss ebenso heißen wie die Klasse selbst. Weiterhin darf eine Konstruktormethode niemals einen Rückgabewert erzeugen. Wird eine Konstruktormethode nicht selbstständig aufgerufen, so ruft Java den Konstruktor automatisch auf, wenn eine Klasse angelegt wird. Bei der Benutzung des new-Operators werden die folgenden drei Aufgaben erfüllt:
class Punkt { int x,y,z; x = a; } public static void main () { punkt p; } } Hier wird in der Methode main eine Instanz der Klasse Punkt erzeugt, die mit dem Tripel (0,0,0) initialisiert wird. Speicherverwaltung Anders als in anderen Programmiersprachen muss sich der Entwickler in Java weder darum kümmern, wie viel Speicherplatz für ein Objekt belegt wird noch, wie dieser wieder freigegeben wird. Dies ist äußerst angenehm, da eine häufig vorzufindende Fehlerquelle in Programmiersprachen, wie C, C++ oder Pascal, darin besteht, dass vergessen wird, Speicherplatz wieder freizugeben. Dies kann sich im Programmablauf akkumulieren, was nach einiger Zeit zum Programmabsturz führt. Die Speicherverwaltung in Java ist hingegen dynamisch und automatisch. Wenn ein neues Objekt mittels new angelegt wird, belegt Java automatisch einen ausreichenden Speicherplatz für dieses Objekt. Durch die Garbage Collection gewährleistet Java weiterhin, dass nicht mehr verwendete Objekte wieder freigegeben werden. An dieser Stelle soll die Erklärung der Garbage Collection nicht weiter vertieft werden, um den Rahmen des Buchs nicht zu sprengen. Der interessierte Leser sei hierzu auf die Fachliteratur verwiesen. Überladen und Überschreiben von Methoden Wie andere Methoden können auch Konstruktoren überladen werden. Ein gutes Beispiel, das das Überladen von Konstruktoren veranschaulichen kann, ist das Quadratprogramm, das bereits beschrieben wurde. Verwendet man die Methode initialisiereQuadrat, die mehrfach überladen wurde, (nach einer Umbenennung in Quadrat) als Konstruktor, so kann je nach Anforderung des Benutzers ein Quadrat auf verschiedene Art und Weise initialisiert werden. Eine Funktionalität von Java, die in enger Beziehung zum Überladen von Methoden steht, ist das Überschreiben von Methoden. Wird in einer Klasse eine Methode aufgerufen, so prüft Java, ob die Methode in der Klasse definiert ist. Ist dies nicht der Fall, so wird der Methodenaufruf an die nächsthöhere Stufe der Klassenhierarchie (also an die nächste Superklasse) übergeben und die Prüfung wiederholt. Dieser Vorgang wird fortgesetzt, bis die Methodendefinition lokalisiert ist. Hierdurch wird garantiert, dass Objekt-Code nicht in jeder Subklasse wiederholt definiert werden muss. Es kann nun erforderlich sein, dass in einer Subklasse zwar dieselbe Methodendefinition verwendet wird, dass aber die Funktionalität der Methode geändert werden soll. Hierzu wird eine Methode mit derselben Signatur wie der der Superklasse erzeugt, die aber eine andere Funktionalität hat. Dieser Vorgang wird als Überschreiben einer Methode bezeichnet. Wenn nun die Methode aufgerufen wird, wird zuerst die Subklasse überprüft und so die überschriebene Methode ausgeführt. Das Überschreiben von Methoden kann aus folgenden Gründen sinnvoll sein:
Die Ersetzung der Originaldefinition wurde bereits erläutert – hier findet eine Definition der Methode mit der gleichen Signatur, aber mit einer neuen Funktionalität statt. Der zweite Fall, also die Erweiterung der Funktionalität der Methode, ist aufwendiger. Wenn die ursprüngliche Funktion der Methode erweitert werden soll, muss eine Möglichkeit existieren, auf die ursprüngliche Definition der Methode zuzugreifen und zugleich in der Subklasse mittels desselben Namens auf die überschriebene Methode zurückgreifen zu können. Zum Zugriff auf eine Methodendefinition, die außerhalb einer Klasse liegt, wird in Java das Schlüsselwort super verwendet. Hierdurch wird der Aufruf der mit super gekennzeichneten Methode in der Klassenhierarchie in Aufwärtsrichtung weitergereicht. In einer allgemeinen Notation sieht die Erweiterung einer überschriebenen Methode dann wie folgt aus:
void neueMethode (Typ 1 Variable 1, ..., Typ n Variable n) { // Neue Funktionalität } Hierbei sei die Originaldefinition der überschriebenen Methode mit neueMethode bezeichnet. super ist in seiner Funktion daher ähnlich wie das Schlüsselwort this und daher ein Platzhalter für den Namen der Superklasse einer Klasse. In Analogie zur Erläuterung des Überladens von Konstruktoren muss in diesem Kontext auch betrachtet werden, wie Konstruktoren von Klassen überschrieben werden können. Aus technischer Sicht ist dies zunächst nicht möglich. Da Konstruktoren immer denselben Namen wie eine Klasse haben, werden Konstruktormethoden immer neu generiert und nicht von Elternklassen geerbt. Dieses Vorgehen ist meist das, was der Programmierer auch beabsichtigt: Wenn eine Konstruktormethode aufgerufen wird, erfolgt dies mit derselben Signatur für alle Superklassen. Die Initialisierung wird daher für alle Teile einer Klassenhierarchie ausgeführt, die vererbend zur jeweiligen Klasse beitragen. Werden jedoch Konstruktormethoden für eigene Klassen entwickelt, so ist es wünschenswert, nicht nur die Initialisierung neuer Variablen zu beeinflussen, die zur Elternklasse hinzugefügt werden, sondern auch die Inhalte der Variablen zu verändern, die aufgrund der Vererbungsstruktur durch die Elternklasse bereits vorgegeben sind. Es wurde bereits erklärt, dass für den Aufruf einer Methode einer Superklasse die Syntax super.methodenName (Argumente) verwendet werden muss. Da Konstruktormethoden keinen eigenen Namen haben, wird die Form super (Argumente) verwendet. In Java ist zu beachten, dass der Aufruf von super immer die erste Anweisung in einer Konstruktormethode sein muss. Wird dieser Aufruf nicht vom Entwickler angegeben, so wird er von Java implizit durch den Aufruf super() ausgeführt. Ähnlich wie auch bei this ruft super(Argumente) den Konstruktor der Elternklasse auf, die in der Hierarchie unmittelbar oberhalb der Klasse steht, aus der der Aufruf erfolgt. Es ist offensichtlich, dass ein Konstruktor mit einer entsprechenden Signatur auch in der Elternklasse existieren muss. Dies wird durch den Java-Compiler aber bereits zur Übersetzungszeit überprüft. Als Beispiel soll das Objekt Point, das in Java standardmäßig bereitsteht und das zweidimensionale Koordinaten erfassen kann, auf dreidimensionale Werte erweitert werden. Dies erfolgt mit folgendem Programmausschnitt:
import java.awt.Point; int z; super (x,y); } } 3Dpunkt erweitert also das Objekt Point um eine dritte Dimension. Die Initialisierung der Elternklasse erfolgt hier mittels der Anweisung super(x,y). Trotz der Garbage Collection existiert auch in Java eine Möglichkeit, das Löschen einmal belegter Objekte zu optimieren – die finalizer-Methode. In der durch Java vorgegebenen Object-Klasse wird eine Standardmethode vorgegeben, die allerdings keine Funktion hat. Zur Verwendung muss diese Methode folgendermaßen überschrieben werden:
protected void finalize () throws Throwable { super.finalize (); } throws Throwable löst im Fehlerfall eine Ausnahmebehandlung aus. Ausnahmebehandlungen werden in Kapitel 3.4 noch detailliert erklärt. In der überschriebenen Methode können nun jegliche Objektfreigaben enthalten sein (auch der Aufruf super.finalize()), die die Superklassen ermächtigen, das Objekt zu beenden, falls nötig. Der Aufruf von finalize kann nun jederzeit erfolgen, führt aber nicht zu einer unmittelbaren Garbage Collection, da ein Objekt erst dann zum Löschen freigegeben wird, wenn alle Verbindungen zu diesem gelöst worden sind. In den meisten Fällen ist die Verwendung dieser Methode daher überflüssig. Casting Gegenstand der vorangegangenen Abschnitte waren im Wesentlichen Variablen und Datentypen. Typen können entweder einfach (bspw. Ganzzahl) oder komplex (bspw. Klasse) sein. Werden Argumente an eine Methode übergeben oder Variablen in Ausdrücken verwendet, so ist darauf zu achten, dass die entsprechenden Datentypen verwendet werden. Wenn eine Methode als Argument eine Ganzzahl vom Typ int verlangt, resultiert bspw. aus der Übergabe einer Fließkommazahl vom Typ float ein Fehler. Ebenso müssen zwei Variablen, die gleichgesetzt werden, vom selben Typ sein. Eine Ausnahme hierzu wurde bereits mehrfach verwendet: die Zuweisung zu Zeichenketten, in denen sowohl Zeichenketten als auch Zahlvariablen mittels einer String-Verkettung miteinander verbunden werden können. Unter dem Begriff Casting versteht man die Umwandlung eines Datentyps in einen anderen. Dies ist immer dann nötig, wenn eine Variable zugewiesen werden soll, die vor dem Casting einen anderen Datentyp hat als die Zielvariable. Bei Casting verändert sich der Wert einer Variablen, im Gegensatz zum Datentyp, nicht. Der Wert einer Variablen eines Datentyps wird lediglich einer weiteren Variablen eines anderen Typs zugewiesen. Man unterscheidet in diesem Zusammenhang drei Möglichkeiten des Castings:
Beim Casting einfacher Datentypen wird der Wert einer Variablen eines einfachen Typs einer anderen Variablen eines anderen Typs zugewiesen. Dies ist für alle einfachen Typen mit Ausnahme des Typs boolean möglich. Boole'sche Werte können entweder true oder false sein und sind vom Casting ausgenommen. Oft findet ein Casting dann statt, wenn der Datentyp, auf den gecastet wird, größere Werte aufnehmen kann als der Originaldatentyp. Ein Beispiel hierfür ist ein Casting vom Datentyp byte zum Datentyp int, wenn man feststellt, dass Zahlenwerte außerhalb des Intervalls von -128 bis 127 liegen, da der Datentyp int weit größere Werte speichern kann. Im Regelfall findet also ein Casting immer auf Datentypen statt, die mehr Information speichern können, als der Originaldatentyp dies vermag. Hieraus resultiert folglich kein Informationsverlust. Eine Ausnahme findet sich beim Casting von int oder long auf float bzw. von long nach double. Während diese Form des Castings bereits dann implizit stattfindet, wenn eine entsprechende Variable einer anderen zugewiesen wird, muss ein Casting von einem Datentyp, der mehr Information speichert als der Zieldatentyp, stets explizit in der Form
(Typname) Wert erfolgen. Ein Beispiel hierfür ist der Ausdruck (int) (a/b), wobei a und b Ganzzahlen seien. Im Regelfall ist das Ergebnis dieser Division nicht ganzzahlig. Eine Zuweisung zu einer Variablen vom Typ int, die hier durch das Casting erzielt wird, verursacht daher fast immer einen Präzisionsverlust. Betrachtet man zudem erneut die Tabelle, in der die Reihenfolge der Operatoren angegeben ist (Tabelle 3-6 ), so stellt man fest, dass das Casting eine größere Wichtigkeit besitzt, als die Division. Die Klammerung des arithmetischen Ausdrucks ist daher an dieser Stelle zwingend erforderlich, um dem Ergebnis den Typ int zuzuweisen. Der zweite Anwendungsfall des Castings ist die Umwandlung einer Klasseninstanz in eine andere. Hierbei gilt die Einschränkung, dass Original- und Zielklasse zueinander in einem Vererbungsverhältnis stehen müssen, eine Klasse muss also eine Subklasse einer anderen sein. Wie auch bei einfachen Datentypen kann ein solches Casting implizit erfolgen. Da Subklassen mindestens die Information beinhalten, die auch die Superklasse hat, kann immer eine Instanz einer Subklasse verwendet werden, wenn normalerweise eine Superklasse eingesetzt worden wäre. Analog zu einfachen Typen ist der umgekehrte Weg problematischer. Da Subklassen normalerweise eine größere Funktionalität haben als Superklassen, kann die Zuweisung einer Subklasseninstanz zu einer Superklasseninstanz dazu führen, dass bspw. Methodenaufrufe verwendet werden, die in der Superklasse nicht enthalten sind. Dies führt zu Fehlern und ist daher im Regelfall nicht ratsam. Um deutlich zu machen, dass die Zuweisung beabsichtigt ist und nicht lediglich aus Unachtsamkeit resultiert, muss das Casting explizit erfolgen. Hierzu wird die Syntax
(Klassenname) Objekt verwendet, wobei Klassenname den Namen der Zielklasse und Objekt eine Referenz auf das Quellobjekt angibt. Gibt man also den Namen einer Superklasse an, wenn normalerweise eine Subklasse verwendet worden wäre, so verliert man keine Information, sondern gewinnt die Methoden und Variablen, um die die Subklasse die Superklasse erweitert. Die dritte Möglichkeit des Castings ist die Umwandlung eines einfachen Typs in ein Objekt und umgekehrt. Hierzu verfügt Java neben den einfachen Datentypen (bspw. int, float oder boolean) über Objekte (die dann in diesem Fall mit Integer, Float und Boolean bezeichnet sind), die Teil der Klasse java.lang.package sind. Den Unterschied zwischen einfachem Typ und Objekt verdeutlicht hierbei der Großbuchstabe zu Beginn der Objektnamen bzw. der Kleinbuchstabe bei einfachen Datentypen. Die Umwandlung von einfachem Typ zu Objekt erfolgt mittels der Methoden, die Teil der Objekte sind. Hat bspw. eine ganzzahlige Variable a den Wert 20, so wandelt die Anweisung
Integer a_obj = new Integer (a); die Variable in ein entsprechendes Integer-Objekt um. Die Umwandlung von Objekt zu einfachem Datentyp erfolgt wiederum mit einer Methode, die Teil des Objekts ist, im Falle dieses Beispiels mit
int b = a_obj.intValue(); Ein weiteres Beispiel, das häufig verwendet wird, ist die Umwandlung von Zeichenketten in Zahlen. Da Benutzereingaben häufig als Zeichenketten abgefragt werden, Berechnungen aber nur mit Zahlen funktionieren, findet sich hier ein weites Anwendungsfeld. Das folgende Beispiel verdeutlicht die Umwandlung, die mit Hilfe der Methode parseInt (Argument) arbeitet.
String postleitzahl = "68163"; Objekte und Klassen Java stellt einige Methoden zur Verfügung, die die Arbeit mit Klassen und Objekten erheblich erleichtern. Hierzu zählen der Operator instanceof und die Methoden getClass() bzw. getName(). instanceof stellt fest, ob ein Objekt Instanz einer Klasse oder einer ihrer Subklassen ist. Die Syntax von instanceof verdeutlicht das folgende Beispiel:
"Beispiel" instanceof String //true Während die Zeichenkette "Beispiel" eine Instanz des Typs String ist und die Antwort daher true lautet, ist dies für die Zahl 4 nicht der Fall (Antwortwert false). Mittels der Methoden getClass() und getName(), die in der Klasse Object definiert sind und daher Teil jedes Objekts sind, kann festgestellt werden, zu welcher Klasse ein Objekt gehört. Während getClass() ein Klassenobjekt als Antwortwert zurückgibt, repräsentiert getName() den Namen der Klasse als Zeichenkette. Folgendes Beispiel illustriert die Verwendung von getName() und getClass(). Die Ausgabe erzeugt hier den String Point:
Point punkt = new Point (0,0); Reflection Eine der wesentlichen Neuerungen, die ab Java 1.1 zur Verfügung stand, war die Einführung der Reflection, die in Java auch als Introspektion bezeichnet wird. Mittels Introspection kann eine Klasse Details über andere Klassen in Erfahrung bringen, indem eine vorab unbekannte Klasse geladen wird und im Anschluss daran deren Variablen, Methoden und Konstruktoren strukturiert werden. Hierzu wird die Klassengruppe java.lang.reflect.* verwendet, mit deren Hilfe Informationen über die Attribute, Methoden und über den Konstruktor der unbekannten Klasse gesammelt werden. Die folgende Application verdeutlicht die Funktionsweise der Introspection, die Ausgabe ist in Abb. 3-7 angegeben:
import java.lang.reflect.*; public static void main(String[] arguments) { Point punkt = new Point(0,0); System.out.println("Methode: " + methoden[i]); } } } In dieser Application werden zwei Konstrukte verwendet, die bisher noch nicht eingeführt wurden: Listen und Zählschleifen. Der Term Methods[] erzeugt eine Liste, in der die anzuzeigenden Methoden gespeichert werden können. Mit der Anweisung for (i=0; i < methoden.length; i++) wird diese Liste durchlaufen und deren Elemente mit der bereits bekannten Methode System.out.println() ausgegeben. Zählschleifen und Listen werden in der Folge dieses Kapitels noch detailliert betrachtet. Die Ausgabe, die die Methode pruefeMethoden erzeugt, zeigt die folgenden Informationen an:
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Tab. 3.7: Klassen in java.lang.reflect Das Konzept der Reflection wird meist in Klassen-Browsern und Debuggern eingesetzt, um vorab unbekannte Klassenstrukturen in Erfahrung zu bringen. Ein weiteres wichtiges Einsatzgebiet sind neben der Objektserialisierung JavaBeans, die in Kapitel 9 behandelt werden. In JavaBeans spielt es eine große Rolle, dass ein Objekt sich bei einem weiteren nach dessen Fähigkeiten erkundigen kann. Dies kann nur mit dem Konzept der Reflection realisiert werden.
Abb. 3.7: Ausgabe der Introspection-Application |
|
|