Methoden definieren das Verhalten von Objekten. Sie werden innerhalb einer Klassendefinition angelegt und haben Zugriff auf alle Variablen des Objekts. Methoden sind das Pendant zu den Funktionen anderer Programmiersprachen, arbeiten aber immer mit den Variablen des aktuellen Objekts. Globale Funktionen, die vollkommen unabhängig von einem Objekt oder einer Klasse existieren, gibt es in Java ebensowenig wie globale Variablen. Wir werden später allerdings Klassenvariablen und -methoden kennenlernen, die nicht an eine konkrete Instanz gebunden sind.
Die Syntax der Methodendefinition in Java ähnelt der von C:
{Modifier}
Typ Name([Parameter])
{
{Anweisung;}
}
Nach einer Reihe von Modifiern (wir kommen weiter unten im Abschnitt "Attribute von Klassen, Methoden und Variablen" darauf zurück) folgt der Typ des Rückgabewerts der Funktion, ihr Name und eine optionale Parameterliste. In geschweiften Klammern folgt dann der Methodenrumpf, also die Liste der Anweisungen, die das Verhalten der Methode festlegen. Die Erweiterung unserer Beispielklasse um eine Methode zur Berechnung des Alters des Auto-Objekts würde beispielsweise so aussehen:
Beispiel
public class Auto
{
public String name;
public int erstzulassung;
public int leistung;
public int alter()
{
return 1996 - erstzulassung;
}
}
Hier wird eine Methode alter definiert, die einen ganzzahligen Wert zurückgibt, der sich aus der Differenz von 1996 und dem Jahr der Erstzulassung errechnet.
Der Aufruf einer Methode erfolgt ähnlich der Verwendung einer Instanzvariablen in Punktnotation. Zur Unterscheidung von einem Variablenzugriff müssen zusätzlich die Parameter der Methode in Klammern angegeben werden, selbst wenn die Liste leer ist. Das folgende Programm würde demnach die Zahl 6 auf dem Bildschirm ausgeben.
Auto golf1 = new Auto(); golf1.erstzulassung = 1990; System.out.println(golf1.alter());
|
|
Wie an der Definition von alter zu erkennen ist, darf eine Methode auf die Instanzvariablen ihrer Klasse zugreifen, ohne die Punktnotation zu verwenden. Das funktioniert deshalb, weil der Compiler alle nicht in Punktnotation verwendeten Variablen x, die nicht lokale Variablen sind, auf das Objekt this bezieht und damit als this.x interpretiert. |
Bei this handelt es sich um einen Zeiger, der beim Anlegen eines Objekts automatisch generiert wird. this ist eine Referenzvariable, die auf das aktuelle Objekt zeigt und dazu verwendet wird, die eigenen Methoden und Instanzvariablen anzusprechen. Der this-Zeiger ist auch explizit verfügbar und kann wie eine ganz normale Objektvariable verwendet werden. Er wird als versteckter Parameter an jede nicht-statische Methode übergeben. Die Methode alter hätte also auch so geschrieben werden können:
public int alter()
{
return 1996 - this.erstzulassung;
}
|
|
Es ist tatsächlich manchmal sinnvoll, this explizit zu verwenden, wenn hervorgehoben werden soll, daß es sich um den Zugriff auf eine Instanzvariable handelt. |
Eine Methode kann mit Parametern definiert werden. Dazu wird bei der Methodendefiniton eine Parameterliste innerhalb der Klammern angegeben. Jeder formale Parameter besteht aus einem Typnamen und dem Namen des Parameters. Soll mehr als ein Parameter definiert werden, so sind die einzelnen Definitionen durch Kommata zu trennen.
Alle Parameter werden in Java per call by value übergeben. Beim Aufruf einer Methode wird also der aktuelle Wert in die Parametervariable kopiert und an die Methode übergeben. Veränderungen der Parametervariablen innerhalb der Methode bleiben lokal und wirken sich nicht auf den Aufrufer aus. Das folgende Beispiel definiert eine Methode printAlter, die das Alter des Autos insgesamt wieoft mal auf dem Bildschirm ausgibt:
public void printAlter(int wieoft)
{
while (wieoft-- > 0) {
System.out.println("Alter = "+alter());
}
}
Obwohl der Parameter wieoft innerhalb der Methode verändert wird, merkt ein Aufrufer nichts von diesen Änderungen, da innerhalb der Methode mit einer Kopie gearbeitet wird. Das folgende Programm würde das Alter des Objekts auto daher insgesamt neunmal auf dem Bildschirm ausgeben:
... int a = 3; auto.printAlter(a); auto.printAlter(a); auto.printAlter(a); ...
Wie bereits erwähnt, sind Objektvariablen Referenzen, also Zeiger. Zwar werden auch sie bei der Übergabe an eine Methode per Wert übergeben, da innerhalb der Methode aber der Zeiger auf das Originalobjekt zur Verfügung steht (wenn auch in kopierter Form), sind Veränderungen an dem Objekt natürlich auch für den Aufrufer der Methode sichtbar. Wie in allen anderen Programmiersprachen entspricht die call by value-Übergabe eines Zeigers damit natürlich genau der Semantik von call by reference.
|
|
Die Übergabe von Objekten an Methoden hat damit zwei wichtige Konsequenzen: |
Sollen Objekte kopiert werden, so muß dies explizit durch Aufruf der Methode clone der Klasse Object erfolgen.
Jede Methode in Java ist typisiert. Der Typ einer Methode wird zum Zeitpunkt der Definition festgelegt und bestimmt den Typ des Rückgabewerts. Dieser kann von einem beliebigen primitiven Typ, einem Objekttyp oder vom Typ void sein. Die Methoden vom Typ void haben gar keinen Rückgabewert und dürfen nicht in Ausdrücken verwendet werden. Sie sind lediglich wegen ihrer Nebeneffekte von Interesse und dürfen daher nur als Ausdrucksanweisung verwendet werden.
Hat eine Methode einen Rückgabewert (ist also nicht vom Typ void), so kann sie mit Hilfe der return-Anweisung einen Wert an den Aufrufer zurückgeben. Die return-Anweisung hat folgende Syntax:
return Ausdruck;
Wenn diese Anweisung ausgeführt wird, führt dies zum Beenden der Methode, und der Wert des angegebenen Ausdrucks wird an den Aufrufer zurückgegeben. Der Ausdruck muß dabei zuweisungskompatibel zum Typ der Funktion sein. Die in Kapitel 5 erläuterte Datenflußanalyse sorgt dafür, daß hinter der return-Anweisung keine unerreichbaren Anweisungen stehen und daß jeder mögliche Ausgang einer Funktion mit einem return versehen ist. Der in C beliebte Fehler, die return-Anweisung einer Funktion zu vergessen und damit einen undefinierten Rückgabewert zu erzeugen, kann in Java nicht auftreten.
In Java ist es erlaubt, Methoden zu überladen, d.h. innerhalb einer Klasse zwei unterschiedliche Methoden mit demselben Namen zu definieren. Der Compiler unterscheidet die verschiedenen Varianten anhand der Anzahl und Typisierung der Parameter. Es ist also nicht erlaubt, zwei Methoden mit exakt demselben Namen und identischer Parameterliste zu definieren. Dabei werden auch zwei Methoden, die sich nur durch den Typ ihres Rückgabewertes unterscheiden, als gleich angesehen.
Das folgende Beispiel erweitert die Klasse Auto um eine weitere Methode alter, die das Alter des Autos nicht nur zurückgibt, sondern es auch mit einem als Parameter übergebenen Titel versieht und auf dem Bildschirm ausgibt:
public int alter(String titel)
{
int alter = alter();
System.out.println(titel+alter);
return alter;
}
Innerhalb dieser Methode wird der Name alter in drei verschiedenen Bedeutungen verwendet. Erstens ist alter der Name der Methode selbst. Zweitens wird die lokale Variable alter definiert, um drittens den Rückgabewert der parameterlosen alter-Methode aufzunehmen. Der Compiler kann die Namen in allen drei Fällen unterscheiden, denn er arbeitet mit der Signatur der Methode. Unter der Signatur einer Methode versteht man ihren internen Namen. Dieser setzt sich aus dem nach außen sichtbaren Namen plus codierter Information über die Reihenfolge und Typen der formalen Parameter zusammen. Die Signaturen zweier gleichnamiger Methoden sind also immer dann unterscheidbar, wenn sie sich wenigstens in einem Parameter voneinander unterscheiden.
In jeder objektorientierten Programmiersprache lassen sich spezielle Methoden definieren, die bei der Initialisierung eines Objekts aufgerufen werden: die Konstruktoren. In Java werden Konstruktoren als Methoden ohne Rückgabewert definiert, die den Namen der Klasse erhalten, zu der sie gehören. Konstruktoren dürfen eine beliebige Anzahl an Parametern haben und können überladen werden. Die Erweiterung unserer Auto-Klasse um einen Konstruktor, der den Namen des Auto-Objekts vorgibt, sieht beispielsweise so aus:
Beispiel
public class Auto
{
public String name;
public int erstzulassung;
public int leistung;
public Auto(String name)
{
this.name = name;
}
}
Soll ein Objekt unter Verwendung eines parametrisierten Konstruktors instanziert werden, so sind die Argumente wie bei einem Methodenaufruf in Klammern nach dem Namen des Konstruktors anzugeben:
Auto dasAuto = new Auto("Porsche 911");
System.out.println(dasAuto.name);
In diesem Fall wird zunächst Speicher für das Auto-Objekt beschafft und dann der Konstruktor aufgerufen. Dieser initialisiert seinerseits die Instanzvariable name mit dem übergebenen Argument "Porsche 911". Der nachfolgende Aufruf schreibt dann diesen Text auf den Bildschirm.
|
|
Explizite Konstruktoren werden immer dann eingesetzt, wenn zur Initialisierung eines Objektes besondere Aufgaben zu erledigen sind. Es ist dabei durchaus gebräuchlich, Konstruktoren zu überladen und mit unterschiedlichen Parameterlisten auszustatten. Beim Ausführen der new-Anweisung wählt der Compiler anhand der aktuellen Parameterliste den passenden Konstruktor und ruft ihn mit den angegebenen Argumenten auf. |
Wir wollen das vorige Beispiel um einen Konstruktor erweitern, der alle Instanzvariablen initialisiert:
public class Auto
{
public String name;
public int erstzulassung;
public int leistung;
public Auto(String name)
{
this.name = name;
}
public Auto(String name,
int erstzulassung,
int leistung)
{
this.name = name;
this.erstzulassung = erstzulassung;
this.leistung = leistung;
}
}
Falls eine Klasse überhaupt keinen expliziten Konstruktor besitzt, so wird beim Anlegen eines Objektes ein parameterloser default-Konstruktor aufgerufen. Der default-Konstruktor wird auch dann verwendet, wenn ein Objekt mit einem parameterlosen new-Aufruf erzeugt wird, in der Klasse aber neben parametrisierten Konstruktoren kein parameterloser Konstruktor definiert wurde.
|
|
Definiert die Klasse dagegen einen eigenen parameterlosen Konstruktor, überlagert dieser den default-Konstruktor und wird bei allen parameterlosen Instanzierungen dieses Objekts verwendet. |
Konstruktoren können in Java verkettet werden, d.h., sie können sich gegenseitig aufrufen. Der aufzurufende Konstruktor wird dabei als eine normale Methode angesehen, die über den Namen this aufgerufen werden kann. Die Unterscheidung zum bereits vorgestellten this-Pointer nimmt der Compiler anhand der runden Klammern vor, die dem Aufruf folgen. Der im vorigen Beispiel vorgestellte Konstruktor hätte damit auch so geschrieben werden können:
Beispiel
public Auto(String name,
int erstzulassung,
int leistung)
{
this(name);
this.erstzulassung = erstzulassung;
this.leistung = leistung;
}
Neben Konstruktoren, die während der Initialisierung eines Objekts aufgerufen werden, gibt es in Java auch Destruktoren. Sie werden unmittelbar vor dem Zerstören eines Objekts aufgerufen.
Ein Destruktor wird als parameterlose Methode mit dem Namen finalize definiert:
void finalize()
{
...
}
Tatsächlich garantiert die Sprachspezifikation nicht, daß ein Destruktor überhaupt aufgerufen wird. Wenn er aber aufgerufen wird, so erfolgt dies nicht, wenn die Lebensdauer des Objektes endet, sondern dann, wenn der Garbage Collector den für das Objekt reservierten Speicherplatz zurückgibt. Dies kann unter Umständen nicht nur viel später der Fall sein (der Garbage Collector läuft ja als asynchroner Hintergrundprozeß), sondern auch gar nicht. Wird nämlich das Programm beendet, bevor der Garbage Collector das nächste Mal aufgerufen wird, werden auch keine Destruktoren aufgerufen. Selbst wenn Destruktoren aufgerufen werden, ist die Reihenfolge oder der Zeitpunkt ihres Aufrufs undefiniert. Der Einsatz von Destruktoren in Java sollte also mit der nötigen Vorsicht erfolgen.