Titel   Inhalt   Suchen   Index   API  Go To Java 2, Zweite Auflage, Handbuch der Java-Programmierung
 <<    <     >    >>  Kapitel 7 - OOP I: Grundlagen

7.3 Methoden



7.3.1 Definition

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/C++:

{Modifier}
Typ Name([Parameter])
{
  {Anweisung;}
}

Nach einer Reihe von Modifiern (wir kommen weiter in Abschnitt 8.2 darauf zurück) folgen 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:

001 public class Auto
002 {
003   public String name;
004   public int    erstzulassung;
005   public int    leistung;
006 
007   public int alter()
008   {
009     return 2000 - erstzulassung;
010   }
011 }
Listing 7.6: Eine einfache Methode zur Altersberechnung

Hier wird eine Methode alter definiert, die einen ganzzahligen Wert zurückgibt, der sich aus der Differenz von 2000 und dem Jahr der Erstzulassung errechnet.

7.3.2 Aufruf

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 9 auf dem Bildschirm ausgeben.

001 Auto golf1 = new Auto();
002 golf1.erstzulassung = 1990;
003 System.out.println(golf1.alter());
Listing 7.7: Aufruf einer Methode

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.

 Tip 

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:

001 public int alter()
002 {
003   return 2000 - this.erstzulassung;
004 }
Listing 7.8: Verwendung von this

Manchmal ist es sinnvoll, this explizit zu verwenden, auch wenn es nicht unbedingt erforderlich ist. Dadurch wird hervorgehoben, daß es sich um den Zugriff auf eine Instanzvariable, und nicht eine lokale Variable, handelt.

 Tip 

7.3.3 Parameter

Eine Methode kann mit Parametern definiert werden. Dazu wird bei der Methodendefinition 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:

001 public void printAlter(int wieoft)
002 {
003   while (wieoft-- > 0) {
004     System.out.println("Alter = " + alter());
005   }
006 }
Listing 7.9: Eine Methode zur Ausgabe des Alters

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:

001 ...
002 int a = 3;
003 
004 auto.printAlter(a);
005 auto.printAlter(a);
006 auto.printAlter(a);
007 ...
Listing 7.10: Wiederholter Aufruf der Methode zur Ausgabe des Alters

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), wirken sich Veränderungen an dem Objekt natürlich direkt auf das Originalobjekt aus und sind somit 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:

  • Die Methode erhält keine Kopie, sondern arbeitet mit dem Originalobjekt.
  • Die Übergabe von Objekten ist performant, gleichgültig wie groß sie sind.
 Hinweis 

Sollen Objekte kopiert werden, so muß dies explizit durch Aufruf der Methode clone der Klasse Object erfolgen.

Die Übergabe von Objekten und Arrays per Referenz kann leicht zu verdeckten Fehlern führen. Da die aufgerufene Methode mit dem Originalobjekt arbeitet, kann sie deren Membervariablen bzw. Elemente verändern, ohne daß der Aufrufer es merkt. Auch der final-Modifier (siehe Abschnitt 8.2) bietet dagegen keinen Schutz. Das unbeabsichtigte Ändern einer modifizierbaren Referenzvariable bei der Übergabe an eine Methode kann nur durch vorheriges Kopieren verhindert werden.

 Warnung 

7.3.4 Rückgabewert

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 (also einer Klasse) 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, einen Funktionsausgang ohne return-Anweisung zu erzeugen (und damit einen undefinierten Rückgabewert zu erzeugen), kann in Java also nicht passieren.

7.3.5 Überladen von Methoden

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 der Typisierung ihrer Parameter. Haben zwei Methoden denselben Namen, aber unterschiedliche Parameterlisten, werden sie als verschieden angesehen. Es ist dagegen nicht erlaubt, zwei Methoden mit exakt demselben Namen und identischer Parameterliste zu definieren.

Der Rückgabetyp einer Methode trägt nicht zu ihrer Unterscheidung bei. Zwei Methoden, die sich nur durch den Typ ihres Rückgabewertes unterscheiden, werden also als gleich angesehen. Da Methoden auch ohne die Verwendung ihres Rückgabewerts aufgerufen werden können (was typischerweise wegen ihrer Nebeneffekte geschieht), hätte weder der Compiler noch der menschliche Leser in diesem Fall die Möglichkeit, festzustellen, welche der überladenen Varianten tatsächlich aufgerufen werden soll.

Das Überladen von Methoden ist dann sinnvoll, wenn die gleichnamigen Methoden auch eine vergleichbare Funktionalität haben. Eine typische Anwendung von überladenen Methoden besteht in der Simulation von variablen Parameterlisten (die als Feature direkt in Java nicht zur Verfügung stehen). Auch, um eine Funktion, die bereits an vielen verschiedenen Stellen im Programm aufgerufen wird, um einen weiteren Parameter zu erweitern, ist es nützlich, diese Funktion zu überladen, um nicht alle Aufrufstellen anpassen zu müssen.

 Tip 

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:

001 public int alter(String titel)
002 {
003   int alter = alter();
004   System.out.println(titel+alter);
005   return alter;
006 }
Listing 7.11: Überladen einer Methode

Die Signatur einer Methode

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.

7.3.6 Konstruktoren

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:

001 public class Auto
002 {
003   public String name;
004   public int    erstzulassung;
005   public int    leistung;
006 
007   public Auto(String name)
008   {
009     this.name = name;
010   }
011 }
Listing 7.12: Definition eines parametrisierten Konstruktors

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:

001 Auto dasAuto = new Auto("Porsche 911");
002 System.out.println(dasAuto.name);
Listing 7.13: Aufruf eines parametrisierten Konstruktors

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.

 Tip 

Wir wollen das vorige Beispiel um einen Konstruktor erweitern, der alle Instanzvariablen initialisiert:

001 public class Auto
002 {
003   public String name;
004   public int    erstzulassung;
005   public int    leistung;
006 
007   public Auto(String name)
008   {
009     this.name = name;
010   }
011 
012   public Auto(String name,
013               int    erstzulassung,
014               int    leistung)
015   {
016     this.name = name;
017     this.erstzulassung = erstzulassung;
018     this.leistung = leistung;
019   }
020 }
Listing 7.14: Eine Klasse mit mehreren Konstruktoren

Default-Konstruktoren

Falls eine Klasse überhaupt keinen expliziten Konstruktor besitzt, wird vom Compiler automatisch ein parameterloser default-Konstruktor generiert. Seine einzige Aufgabe besteht darin, den parameterlosen Konstruktor der Superklasse aufzurufen. Enthält eine Klassendeklaration dagegen nur parametrisierte Konstruktoren, wird kein default-Konstruktor erzeugt, und die Klassendatei besitzt überhaupt keinen parameterlosen Konstruktor.

Verkettung von Konstruktoren

Unterschiedliche Konstruktoren einer Klasse 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:

001 public Auto(String name,
002             int    erstzulassung,
003             int    leistung)
004 {
005   this(name);
006   this.erstzulassung = erstzulassung;
007   this.leistung = leistung;
008 }
Listing 7.15: Verkettung von Konstruktoren

Der Vorteil der Konstruktorenverkettung besteht darin, daß vorhandener Code wiederverwendet wird. Führt ein parameterloser Konstruktor eine Reihe von nichttrivialen Aktionen durch, so ist es natürlich sinnvoller, diesen in einem spezialisierteren Konstruktor durch Aufruf wiederzuverwenden, als den Code zu duplizieren.

 Hinweis 

Wird ein Konstruktor in einem anderen Konstruktor derselben Klasse explizit aufgerufen, muß dies als erste Anweisung innerhalb der Methode geschehen. Steht der Aufruf nicht an erster Stelle, gibt es einen Compiler-Fehler.

 Warnung 

Es gibt noch eine zweite Form der Konstruktorenverkettung. Sie findet automatisch statt und dient dazu, abgeleitete Klassen während der Instanzierung korrekt zu initialisieren. In Abschnitt 8.1.4 werden wir auf die Details dieses Mechanismus eingehen.

Initialisierungsreihenfolge

Beim Instanzieren eines neuen Objekts werden die Initialisierungschritte in einer genau festgelegten Reihenfolge ausgeführt:

Wir wollen dies an einem Beispiel veranschaulichen:

001 /* Listing0716.java */
002 
003 public class Listing0716
004 {
005   public static String getAndPrint(String s)
006   {
007     System.out.println(s);
008     return s;
009   }
010 
011   public static void main(String[] args)
012   {
013     Son son = new Son();
014   }
015 }
016 
017 class Father
018 {
019   private String s1 = Listing0716.getAndPrint("Father.s1");
020 
021   public Father()
022   {
023     Listing0716.getAndPrint("Father.<init>");
024   }
025 }
026 
027 class Son
028 extends Father
029 {
030   private String s1 = Listing0716.getAndPrint("Son.s1");
031 
032   public Son()
033   {
034     Listing0716.getAndPrint("Son.<init>");
035   }
036 }
Listing0716.java
Listing 7.16: Initialisierungsreihenfolge

Im Hauptprogramm wird eine neue Instanz der Klasse Son angelegt. Durch die Konstruktorenverkettung wird zunächst zur Vaterklasse Father verzweigt. Darin wird zunächst die Membervariable s1 initialisiert, und anschließend wird der Rumpf des Konstruktors ausgeführt. Erst danach führt Son dieselben Schritte für sich selbst durch. Die Ausgabe des Programms ist demnach:

Father.s1
Father.<init>
Son.s1
Son.<init>

7.3.7 Destruktoren

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 geschützte (protected) parameterlose Methode mit dem Namen finalize definiert:

001 protected void finalize()
002 {
003   ...
004 }
Listing 7.17: Die finalize-Methode

Da Java über ein automatisches Speichermanagement verfügt, kommt den Destruktoren hier eine viel geringere Bedeutung zu als in anderen objektorientierten Sprachen. Anders als etwa in C++ muß sich der Entwickler ja nicht um die Rückgabe von belegtem Speicher kümmern; und das ist sicher eine der Hauptaufgaben von Destruktoren in C++.

 Hinweis 

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.


 Titel   Inhalt   Suchen   Index   API  Go To Java 2, Zweite Auflage, Addison Wesley, Version 2.0
 <<    <     >    >>  © 2000 Guido Krüger, http://www.gkrueger.com