From 33613a85afc4b1481367fbe92a17ee59c240250b Mon Sep 17 00:00:00 2001
From: Sven Eisenhauer
+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++:
+
+
+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:
+
+
+
+
+
+
+ Titel
+ Inhalt
+ Suchen
+ Index
+ DOC
+ Handbuch der Java-Programmierung, 5. Auflage
+
+ <<
+ <
+ >
+ >>
+ API
+ Kapitel 7 - OOP I: Grundlagen
+
+
+
+
+
+7.3 Methoden
+
+
+
+
+
+
+
+
+7.3.1 Definition
+
+
+
+
+
+
+
+
+
+
+{Modifier}
+Typ Name([Parameter])
+{
+ {Anweisung;}
+}
+
+
+
+
+
+Listing 7.6: Eine einfache Methode zur Altersberechnung
+
+
+
+
+
+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 }
+
+
+Hier wird eine Methode alter +definiert, die einen ganzzahligen Wert zurückgibt, der sich aus +der Differenz des Jahres 2000 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 10 auf dem Bildschirm ausgeben. + + +
+
+
++001 Auto golf1 = new Auto(); +002 golf1.erstzulassung = 1990; +003 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: + + +
+
+
+
+001 public int alter()
+002 {
+003 return 2000 - this.erstzulassung;
+004 }
+
+ |
+
+
![]() |
+![]() |
+
+
+ +Manchmal ist es sinnvoll, this +explizit zu verwenden, auch wenn es nicht unbedingt erforderlich ist. +Dadurch wird hervorgehoben, dass es sich um den Zugriff auf eine Instanzvariable, +und nicht eine lokale Variable, handelt. |
+
+
|
+![]() |
+
+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 }
+
+ |
+
+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 ...+ + |
+
+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: +
|
+
+
|
+![]() |
+
+Sollen Objekte kopiert werden, so muss 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 dass 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. |
+
+
|
+![]() |
+
+
+Technisch entspricht die Deklaration der eines Arrays-Parameters,
+und so wird auch auf die Elemente zugegriffen. Die Vereinfachung wird
+sichtbar, wenn man sich den Aufruf der Methode ansieht. An
+dieser Stelle darf nämlich nicht nur ein einzelnes Array übergeben
+werden, sondern die einzelnen Elemente können auch separat angegeben
+werden. Dabei erzeugt das Laufzeitsystem automatisch ein Array, in
+das diese Werte übertragen werden. Die beiden folgenden Aufrufe
+sind also gleichwertig:
+
+
+printArgs(new String[]{"so", "wird", "es", "gemacht"});
+
+printArgs("so", "wird", "es", "gemacht");
+
+
+
+
+Nun wird auch deutlich, warum lediglich der letzte Parameter variabel +sein darf. Andernfalls könnte der Compiler unter Umständen +nicht mehr unterscheiden, welches aktuelle Argument zu welchem formalen +Parameter gehört. + +
+Praktischen Nutzen haben die variablen Parameterlisten bei Anwendungen, +in denen nicht von vorneherein klar ist, wieviele Argumente benötigt +werden. Tatsächlich wurde ihre Entwicklung durch den Wunsch motiviert, +flexible Ausgabemethoden definieren zu können, wie sie etwa in +C/C++ mit der printf-Familie zur Verfügung stehen (und seit der +J2SE 5.0 mit der Klasse java.util.Formatter, +die in Abschnitt 11.6 +beschrieben wird). Sie können dann die in diesem Fall vielfach +verwendeten überladenen Methoden ersetzen (siehe Abschnitt 7.3.6). +Natürlich benötigt nicht jede Methode variable Parameterlisten, +sondern ihre Anwendung sollte auf Spezialfälle beschränkt +bleiben. + +
+Wird eine Methode mit einem Parameter vom Typ Object... +deklariert, entstehen in Zusammenhang mit dem ebenfalls seit der J2SE +5.0 verfügbaren Mechanismus des Autoboxings (siehe Abschnitt 10.2.3) +Methoden, bei denen praktisch alle Typprüfungen des Compilers +ausgehebelt werden. Da ein Element des Typs Object +zu allen anderen Referenztypen kompatibel ist und primitive Typen +dank des Autoboxings automatisch in passende Wrapper-Objekte konvertiert +werden, kann an einen Parameter des Typs Object... +eine beliebige Anzahl beliebiger Argumente übergeben werden. + +
+Das folgende Listing zeigt eine Methode, die numerische Argumente +jeweils solange summiert, bis ein nichtnumerischer Wert übergeben +wird. Dieser wird dann in einen String konvertiert und zusammen mit +der Zwischensumme ausgegeben. Am Ende wird zusätzlich die Gesamtsumme +ausgegeben. Nicht unbedingt eine typische Anwendung, und erst recht +kein empfehlenswerter Programmierstil, aber das Listing demonstriert, +wie weitreichend die Möglichkeiten dieses neuen Konzepts sind: + + +
+
+
+
+001 /* Listing0712.java */
+002
+003 public class Listing0712
+004 {
+005 public static void registrierKasse(Object... args)
+006 {
+007 double zwischensumme = 0;
+008 double gesamtsumme = 0;
+009 for (int i = 0; i < args.length; ++i) {
+010 if (args[i] instanceof Number) {
+011 zwischensumme += ((Number)args[i]).doubleValue();
+012 } else {
+013 System.out.println(args[i] + ": " + zwischensumme);
+014 gesamtsumme += zwischensumme;
+015 zwischensumme = 0;
+016 }
+017 }
+018 System.out.println("Gesamtsumme: " + gesamtsumme);
+019 }
+020
+021 public static void main(String[] args)
+022 {
+023 registrierKasse(
+024 1.45, 0.79, 19.90, "Ware",
+025 -3.00, 1.50, "Pfand",
+026 -10, "Gutschein"
+027 );
+028 }
+029 }
+
+ |
++Listing0712.java | +
+Die Ausgabe des Programms ist:
+
+
+Ware: 22.14
+Pfand: -1.5
+Gutschein: -10.0
+Gesamtsumme: 10.64
+
+
+
+
+
+
+
+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 muss dabei zuweisungskompatibel zum +Typ der Funktion sein. Die in Kapitel 5 +erläuterte Datenflussanalyse sorgt dafür, dass hinter der +return-Anweisung +keine unerreichbaren Anweisungen stehen und dass 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. + + + + +
+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 erst seit der Version +5.0 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. |
+
+
|
+![]() |
+
+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 }
+
+ |
+
+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: + + +
+
+
+
+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 }
+
+ |
+
+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);
+
+ |
+
+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: + + +
+
+
+
+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 }
+
+ |
+
+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. + + + + +
+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 }
+
+ |
+
+
![]() |
+
+
+ +Der Vorteil der Konstruktorenverkettung besteht darin, dass 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. |
+
+
|
+![]() |
+
+
![]() |
+![]() |
+
+
+ +Wird ein Konstruktor in einem anderen Konstruktor derselben Klasse +explizit aufgerufen, muss dies als erste Anweisung innerhalb der Methode +geschehen. Steht der Aufruf nicht an erster Stelle, gibt es +einen Compiler-Fehler. |
+
+
|
+![]() |
+
+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. + + + + +
+Beim Instanzieren eines neuen Objekts werden die Initialisierungschritte +in einer genau festgelegten Reihenfolge ausgeführt: +
+Wir wollen dies an einem Beispiel veranschaulichen: + + +
+
+
+
+001 /* Listing0718.java */
+002
+003 public class Listing0718
+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 = Listing0718.getAndPrint("Father.s1");
+020
+021 public Father()
+022 {
+023 Listing0718.getAndPrint("Father.<init>");
+024 }
+025 }
+026
+027 class Son
+028 extends Father
+029 {
+030 private String s1 = Listing0718.getAndPrint("Son.s1");
+031
+032 public Son()
+033 {
+034 Listing0718.getAndPrint("Son.<init>");
+035 }
+036 }
+
+ |
++Listing0718.java | +
+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>
+
+
+
+
+
+
+
+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 }
+
+ |
+
+
![]() |
+
+
+ +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++ muss 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++. |
+
+
|
+![]() |
+
+Tatsächlich garantiert die Sprachspezifikation nicht, dass 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 Hintergrundprozess), 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 + | DOC + | Handbuch der Java-Programmierung, 5. Auflage, Addison +Wesley, Version 5.0.1 + |
| << + | < + | > + | >> + | API + | © 1998, 2007 Guido Krüger & Thomas +Stark, http://www.javabuch.de + |