lokale und globale Variable, Zeigervariable, Call by value, call by reference
Methoden können keinen, einen oder mehrere Parameter
besitzen. Die Datentypen der Parameter müssen angegeben
werden. Der Methodenblock selbst kann neue (lokale) Variable
deklarieren und initialisieren. In ihm kann aber auch auf
(globale) Variable der jeweiligen Klasse zurückgegriffen
werden. Es ist zum Verständnis insbesondere von Programmen mit
OOP ganz wesentlich zu verstehen, was intern mit diesen Variablen
geschieht.
Man betrachte die folgende Methode mit einem double-Feld
als Parameter und einer double-Zahl als Rückgabewert.
static double mittelwert(double[] x){
int i, n;
n=x.length;
double w=0;
for (i=0;i<n;i++) w+=x[i];
w/=n;
return w;
}//Ende mittelwert()
i, n, w, aber auch der
Parameter x sind lokale Variable dieser Methode.
Für sie wird erst Platz geschaffen, wenn die Methode aufgerufen
wird. Vorher und nachher sind diese Variablen bzw. ihre Namen unbekannt.
(Globale) Variable der Applikationsklasse (diese müssen
als static deklariert werden), die diese Methode enthält,
können ohne weiteres dieselben Bezeichner als die
lokalen Variablen haben (auch wenn dies nicht immer sinnvoll ist):
class MW{
static double v, w;
static double mittelwert(double[] x){
int i, n;
n=x.length;
double w=0;
for (i=0;i<n;i++) w+=x[i];
w/=n;
return w;
//w=berechneter Mittelwert
}//Ende mittelwert()
public static void main(String[] args){
double[] y={1, 2.1, 3};
w=y[0];
//hat nichts mit dem Mittelwert zu tun
v=mittelwert(y);
//Hier wird der Mittelwert mit v bezeichnet
System.out.println("w="+w+" Mittelwert="+v);
//Ausgabe: w=1.0 Mittelwert=2.033333333333333
}//Ende main()
}//Ende class MW
Wir verwenden hier unnötiger Weise eine globale und eine lokale Variable
gleichen Namens w, die unterschiedliche Werte haben. Ausgegeben wird im
main()-Block die globale Variable. Die lokalen Variablen der Methode
mittelwert() sind im main()-Block unbekannt. Wollte man auf
sie (z.B. auf n) im main()-Block zugreifen, so gäbe es eine Syntax-Fehlermeldung
Undefined variable: n.
Globale Variable können aber sehr wohl in einem Methodenblock
verändert werden, wenn es keine lokalen Variablen gleichen Namens
gibt.
Denselben Zweck wie das letzte Programm
erfüllt
class MW2{
static double w;//global
static void mittelwert(double[] x){
int i, n;
n=x.length;
for (i=0;i<n;i++) w+=x[i];
w/=n;
}//Ende mittelwert()
public static void main(String[] args){
double[] y={1, 2.1, 3};
mittelwert(y);
System.out.println("Mittelwert="+w);
//Ausgabe: Mittelwert=2.033333333333333
}//Ende main()
}//Ende class MW2
Dieses Programm ist sogar kürzer, aber weniger gut
strukturiert. Der Mittelwert wird jetzt implizit berechnet, die
Methode mittelwert() hat keinen Rückgabewert.
Übrigens hätte die lokale Feldvariable double[]
y der Methode main() durchaus den gleichen Namen wie die Parametervariable
double[] x der Methode mittelwert() haben können, ohne
dass hierdurch Speicherplatz gespart worden wäre!
Man wiederhole zunächst Deklaration und Initialisierung.
Wir starten mit dem folgenden Programm:
class lokal{
static void lift(double x){
x+=5;
}
public static void main(String[] args){
double y=1;
lift(y);
System.out.println(y);
//Ergebis: 1, nicht 6
}
}
Jetzt machen wir Gleiches mit Zeichenketten und Feldern:
class lokal{
static void lift(double x){
x+=5;
}
static void lift(String s){
s+=" !!!!";
}
static void lift(double[] x){
for (int i=0;i<x.length;i++)
x[i]+=5;
}
public static void main(String[] args){
double y=1;
lift(y);
System.out.println(y);
String s="Java";
lift(s);
System.out.println(s);
double[] z={1,2,3};
lift(z);
for(int i=0;i<3;i++)
System.out.println(z[i]);
}
}
Nur bei dem double- Feld wirkt sich die Methode lift() aus!
(Übrigens: Es gibt hier drei verschiedene Methoden gleichen
Namens: man spricht von Überladung!). Der Grund
hierfür soll jetzt erarbeitet werden:
x ist eine lokale double-Variable der Methode
lift(double x), während y eine lokale double-Variable
der Methode main() ist. Bei Aufruf von lift(y); wird y
(inkl. Wert) kopiert, die Kopie wird mit x
bezeichnet und deren Wert (aber nicht der von y!) um 5
erhöht. Dies bleibt ohne Folgen nach Beendigung des
Methodenaufrufs! Aus dem gleichen Grund bleibt auch die Stringvariable
s unbeeindruckt von lift(s), hier wird die
(Zeiger-)Variable samt Inhalt kopiert. Nur das
komponentenweise Liften des double-Feldes z hat
geklappt. Warum? Wieder wird bei Aufruf von lift(z) eine
Kopie von z namens x angelegt - aber nur von
der Zeigervariablen, nicht von dem Inhalt, auf den sie zeigt.
Beide Variable - das Original und die Kopie - zeigen auf denselben
Inhalt, nämlich dem
von z. Im Methodenblock von lift(double[] x)
wird nun der Inhalt von x verändert und damit
gleichzeitig der von z.
Jetzt wenden wir uns den Parametern einer Methode genauer zu wie dem
Parameter double[] x in der oberen Methode mittelwert(). Wir wollen
eine Methode diskutieren, die das Feld vom Kopf auf die
Füße stellt:
class KopfFuss{
static double[] umkehren(double[] x){
int i, n; //lokale Variablen von umkehren()
n=x.length;
double[] w=new double[n];
for (i=0;i<n;i++) w[i]=x[n-i-1];
return w;
}//Ende umkehren()
public static void main(String[] args){
double[] y={1, 2.1, 3};
y=umkehren(y);
//y={3, 2.1, 1}
for (int i=0;i<3;i++)
System.out.println(y[i]);
}//Ende main()
}//Ende class KopfFuss
Der invertierte Vektor ist hier der Rückgabewert der Methode
umkehren(). Die Zeile y=umkehren(y);
überschreibt das Feld y durch das
invertierte. Alles klappt wie es soll.
Alternativ kann man auch versuchen, keinen
Rückgabewert vorzusehen und den Parameter double[] x
der Methode zu überschreiben. Das sähe so aus:
class KopfFuss2{
static void umkehren(double[] x){
int i, n;
n=x.length;
double[] w=new double[n];
// x und w sind lokale Variable dieser Methode
for (i=0;i<n;i++) w[i]=x[n-i-1];
x=w;
//bleibt ohne Wirkung nach außen!
}//Ende umkehren()
public static void main(String[] args){
double[] y={1, 2.1, 3};
umkehren(y);
//tut nicht, was es soll!
for (int i=0;i<3;i++)
System.out.println(y[i]);
}//Ende main()
}//Ende class KopfFuss2
Dies klappt in dem Sinne nicht, dass die Anweisung umkehren(y);
gar keine Umkehr bewirkt! Der Inhalt der lokalen double[]-Variable w des
Methodenblocks von umkehren() hat zwar die gewünschte Gestalt und
die letzte Zeile der Methode (x=w;) bewirkt, dass die lokale
double[]-Variable x (Parameter von umkehren() auf den
"richtigen" Inhalt zeigt, aber von dem double[]-Parameter y (einer
Zeigervariable!) beim Aufruf von umkehren(y); wird nur eine Kopie
angelegt, die nach Abarbeit der Methode gelöscht wird: y zeigt immer
noch auf den alten Inhalt.
Um die Verwirrung zu vergrößern: Das Programm
class KopfFuss3{
static void umkehren(double[] x){
int i, n;
n=x.length;
double[] w=new double[n];
for (i=0;i<n;i++) w[i]=x[n-i-1];
for (i=0; i<n;i++) x[i]=w[i];
}//Ende umkehren()
static void main(String[] args){
double[] y={1, 2.1, 3};
umkehren(y);
for (int i=0;i<3;i++)
System.out.println(y[i]);
}//Ende main()
}//Ende class KopfFuss3
leistet das, was es soll! Offensichtlich sind die beiden Zeilen
for (i=0; i<n;i++) x[i]=w[i]; und
x=w; nicht gleichwertig. Um dies zu
verstehen, muss verstanden werden, was call by value und
Zeigervariable bedeuten:
Grundsätzlich werden Methodenparameter beim Aufruf einer Methode
stets kopiert, sie werden zu einem
lokalen Parameter der Methode. Nach
Beendigung der Methode wird die Kopie wieder gelöscht. Bei
Feldern (wie auch bei anderen Objekten) sind die zugehörigen
Variablen jedoch Zeigervariable. Ich
wiederhole (s. auch Kap.4.3): Das
muss man sich so vorstellen, dass eine solche Variable ihren
Bezeichner abspeichert und als Information den Ort des Speicherplatzes
enthält, wo das Objekt (hier das gesamte Feld, sein
Inhalt) abgespeichert wird. Die Variable
zeigt also nur auf den Ort des eigentlichen Inhalts. Die
Zeigervariable beansprucht vergleichsweise wenig Speicher im Gegensatz
zu dem (vielleicht sehr langen) Feld.
Beim Aufruf von umkehren(y); in der Klasse
KopfFuss2 wird zunächst Platz gemacht für eine lokale
Zeigervariable namens x und mit der Zeigervariablen
y des main()-Blocks belegt - x
ist eine Kopie von
y. Beide zeigen jetzt auf denselben Speicher, wo das double-Feld der
Länge drei gespeichert ist. Sodann wird innerhalb der Methode
eine Zeigervariable namens w deklariert, die nach ihrer
Initialisierung ebenfalls auf ein double-Feld der
Länge drei zeigt. Dieser Speicherbereich wird danach durch den
invertierten Vektor besetzt (nach der Initialisierung mit Hilfe des
new-Operators haben alle Komponenten
den Wert Null). Die Zeile x=w; verändert jetzt nur
die Zeigervariable x: sie zeigt jetzt auf den Inhalt von
w, der den gewünschten invertierten Vektor
enthält, und nicht mehr auf den Inhalt von y. Nach
Beendigung der Methode jedoch stehen die Zeigervariablen x,
w sowie der Inhalt von w
nicht mehr zur Verfügung, sie wurden "gelöscht", die
Information ging verloren.
Anders in der Klasse KopfFuss3.java. Hier wird durch
for (i=0; i<n;i++) x[i]=w[i]; der Inhalt
von x und damit auch der von y in der
gewünschten Weise verändert. Nach Beendigung der Methode bleibt es bei diesem
invertierten Inhalt von y.
Das Anlegen einer Kopie der beim Aufruf der Methode verwendeten
Methodenparameter wird mit call by
value bezeichnet, während ein call by reference es ermöglicht, eine
Veränderung des Parameters innerhalb des Methodenblocks zu
bewirken, die auch nach Beendigung der Methode wirksam bleibt. Sind
Parameter einer Methode Zeigervariable, so ist es trotz des call by
value-Prinzips möglich, die Inhalte der Speicherbereiche, auf
die der Zeiger zeigt, zu verändern. Die Wirkung der Methode ist
dann - was den Inhalt betrifft - identisch mit einer Call by
reference-Wirkung. Man nennt daher Zeigervariable auch Referenzvariable.
Im Gegensatz zu C++ "verschleiert" Java die Tatsache, dass alle
Objektvariable (Sie kennen bisher nur Felder!) Zeigervariable
sind. Dadurch bleibt der unerfahrenen ProgrammiererIn manche Wirkung
kryptisch, z.B. die Fehlermeldung java.lang.NullpointerException.
Variable vom Grunddatentyp sind
keine Zeigervariable, sie sind unauflöslich mit ihrem
Inhalt verbunden. Treten
sie als Parameter einer Methode auf, wird stets eine Kopie angelegt
und alle Änderungen betreffen nur die Kopie, niemals die
Variable, die an die Methode übergeben wurde. So bleibt die
folgende Methode, obwohl syntaktisch richtig, wirkungslos:
//Bsp Vertausche
static void vertausche(double x, double y){
double h=x;
x=y;
y=h;
}//Ende vertausche()
Es gibt im Gegensatz anderer Programmiersprachen wie Pascal und C
keine einfache Möglichkeit, Variable eines Grunddatentyps als
Referenzvariable zu behandeln. Vor einem Jahr hatte ich einen Preis
für ein einfaches Vertausche-Programm ausgesetzt - ohne Ergebnis.
Im Gegensatz zu Variablen vom Grunddatentyp können die Inhalte
von Objektvariablen leicht vertauscht werden:
class VertauscheFelder{
static void vertausche(double[] x, double[] y){
int n=x.length;
for (int i=0;i<n;i++){
double h=x[i]; x[i]=y[i]; y[i]=h;
}
}//Ende vertausche()
public static void main(String[] args){
double[] x={1,2};
double[] y={-1,-2};
vertausche(x,y);
System.out.println("x(0]="+x[0]+" x[1]="+x[1]);
}//Ende main()
}//Ende class
Einen letzten Satz zu String-Variablen. Man kann durch sie eine
NullPointerException hervorrufen, was auf den Zeigercharakter
hinweist. Was jedoch ihre Verwendung als Paramter von Methoden
betrifft, ähneln sie eher den Grunddatentypen: es wird eine Kopie
auch des Inhalts angelegt, d.h. (Zeiger-)Variable und Inhalt sind
nicht zu trennen (anders bei der Klasse StringBuffer.
Dieser Abschnitt ist für ProgrammieranfängerInnen nicht
einfach. Also nicht verzagen, wenn Sie ihn nicht auf Anhieb
verstehen. Es wird noch viele Gelegenheiten geben, auf das
Phänomen Zeiger- oder Referenzvariable hinzuweisen.
5.3 Überladen von Methoden
Java erlaubt es verschiedene Methoden gleichen Namens zu
verwenden, sofern sie sich im Typ des Rückgabewertes nicht
unterscheiden. Sie unterscheiden sich nur in ihren Parametern (Anzahl
oder Typ).
Nehmen wir an, wir wollen mit Hilfe einer Methode die p-te Wurzel aus einer Zahl
x ziehen. Die Parameter der Methode wären dann
x und p. Wir erwarten jedoch, dass der
Nutzer dieser Methode in den meisten Fällen eine Quadratwurzel
(p=2) ziehen will. Für diesen Fall stellen wir eine
Methode gleichen Namens mit nur einem Parameter zur Verfügung:
//Wurzel
static double wurzel(double x){
return Math.sqrt(x);
}//Ende 1
static double wurzel(double x, int p){
return Math.pow(x,(double) 1/p);//Casting!!
}//Ende 2
Jetzt kann die Methode wurzel() mit einem oder auch mit zwei
Parametern aufgerufen werden! Man sagt, die Methode 2 hat die erste
überladen - allerdings behält die erste Methode ihre
Gültigkeit, beide können gleichberechtigt eingesetzt werden.
|