Universität Hamburg - Fachbereiche - Fachbereich Mathematik

Java-Kurs (5)

WiSe 01/02

Bodo Werner

Zurück zum Inhaltsverzeichnis.

5. Methoden (Teil 3)

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.

5.1 Lokale und globale Variable

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()
  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 haben 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()
  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!

5.2 Zeigervariable, call by value, call by reference, Referenzvariable

Man wiederhole zunächst Deklaration und Initialisierung. Wir starten mit dem folgenden Programm:

class lokal{
    static void lift(double x){
	x+=5;
    }
    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;
    }
    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()
  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()
  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()
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.

Weiter mit Klassen und Objekte (Teil 1).