3.6 Mit Referenzen arbeiten 

3.6.1 Die null-Referenz 

In Java gibt es drei spezielle Referenzen: null, this und super. (Wir verschieben this und super auf Kapitel 6.) Das spezielle Literal null lässt sich zur Initialisierung von Referenzvariablen verwenden. Die null-Referenz ist typenlos, kann also jeder Referenzvariable zugewiesen und jeder Funktion übergeben werden, die ein Objekt erwartet. Daher ist Folgendes gültig:
Point p = null; String s = null; System.out.println( null );
Da es nur ein null gibt, ist zum Beispiel (Point) null == (String) null. Der Wert ist ausschließlich für Referenzen vorgesehen und kann in keinen primitiven Typ wie die Ganzzahl 0 umgewandelt werden. [Hier unterscheiden sich C(++) und Java. ]
Mit null lässt sich eine ganze Menge machen. Der Haupteinsatz sieht vor, damit uninitialisierte Referenzvariablen zu kennzeichnen, also auszudrücken, dass eine Referenzvariable auf kein Objekt verweist. In Listen oder Bäumen kennzeichnet null aber auch das Fehlen eines gültigen Nachfolgers; null ist dann ein gültiger Indikator und kein Fehlerfall.
Die NullPointerException
Da sich hinter null kein Objekt verbirgt, ist es auch nicht möglich, eine Methode aufzurufen. Der Compiler kennt zwar den Typ jedes Objekts, aber erst die Laufzeitumgebung (JVM) weiß, was referenziert wird. Wird versucht, über die null-Referenz auf eine Eigenschaft eines Objekts zuzugreifen, löst eine JVM eine NullPointerException [Der Name zeigt das Überbleibsel von Zeigern. Zwar haben wir es in Java nicht mit Zeigern zu tun, sondern mit Referenzen, doch heißt es NullPointerException und nicht NullReferenceException. Das erinnert daran, dass eine Referenz ein Objekt identifiziert und eine Referenz auf ein Objekt ein Pointer ist. ] aus.
Listing 3.3 NullPointer.java
/* 1 */import java.awt.Point;
/* 2 */
/* 3 */public class NullPointer
/* 4 */{
/* 5 */ public static void main( String[] args )
/* 6 */ {
/* 7 */ Point p = null;
/* 8 */ String s = null;
/* 9 */
/* 10 */ p.setLocation( 1, 2 );
/* 11 */ s.length();
/* 12 */ }
/* 13 */}
Wir beobachten eine NullPointerException, denn bei p.setLocation() bricht das Programm mit folgender Ausgabe ab:
java.lang.NullPointerException
at NullPointer.main(NullPointer.java:10)
Exception in thread "main"
Die Laufzeitumgebung teilt uns in der Fehlermeldung mit, dass sich der Fehler, die NullPointerException, in Zeile 10 befindet.
null-Referenzen testen
Wir wollen an dieser Stelle noch einmal auf die logischen Kurzschlussoperatoren und normalen logischen Operatoren zu sprechen kommen. Letztere werten Operanden nur so lange von links nach rechts aus, bis der Wert der Operation feststeht. Auf den ersten Blick scheint es nicht viel auszumachen, ob alle Teilausdrücke ausgewertet werden oder nicht, in einigen Ausdrücken ist es aber wichtig, wie das folgende Beispiel für die Variable s vom Typ String zeigt:
if ( s != null && s.length() > 0 ) ...
Die Bedingung testet, ob s überhaupt auf ein Objekt verweist und ob die Länge echt größer 0 ist. Diese Schreibweise tritt häufig auf, und der Und-Operator zur Verknüpfung muss ein Kurzschlussoperator sein, da es in diesem Fall ausdrücklich darauf ankommt, dass die Länge nur dann bestimmt wird, wenn die Variable s überhaupt auf ein String-Objekt verweist und nicht null ist. Andernfalls bekämen wir bei s.length() eine NullPointerException, wenn jeder Teilausdruck ausgewertet würde und s gleich null ist.
3.6.2 Zuweisungen bei Referenzen 

Eine Referenz erlaubt den Zugriff auf das referenzierte Objekt. Es kann durchaus mehrere Kopien dieser Referenz geben, die in Variablen mit unterschiedlichen Namen abgelegt sind – so wie eine Person (ein Personen-Objekt) von den Mitarbeitern als »Chefin« angesprochen wird, aber von ihrem Mann als »Schnuckiputzi«.
Wir wollen uns dies an einem Punkt-Objekt näher ansehen, das wir unter einem alternativen Variablennamen ansprechen können:
Point p = new Point(); Point q = p;
Ein Punkt-Objekt wird erzeugt und mit der Variablen p referenziert. Die zweite Zeile speichert nun dieselbe Referenz in der Variablen q. Danach verweisen p und q auf dasselbe Objekt.
Point p = new Point(); Point q = p; p.x = 10; System.out.println( q.x ); // 10 q.y = 5; System.out.println( p.y ); // 5 Was wir über die Variable p ändern, kann über die andere Variable q erfragt werden und umgekehrt. Es ist ja das identische Objekt! |
3.6.3 Funktionen mit nicht-primitiven Parametern 

Dass sich das gleiche Objekt unter zwei Namen (über zwei verschiedene Variablen) ansprechen lässt, können wir bei Methoden beobachten. Eine Funktion, die im Parameter eine Objektreferenz erhält, bezieht sich genau auf das übergebene Objekt. Das bedeutet: Die Funktion kann dieses Objekt mit den angebotenen Methoden ändern oder auf die Attribute zugreifen.
Listing 3.4 PointFunktion.java
import java.awt.*; public class InitPoint { static void clear( Point p ) { p.setLocation( 0, 0 ); } public static void main( String[] args ) { Point q = new Point( 47, 11 ); // Coordinates (x=47,y=11) clear( q ); System.out.println( q.x ); // 0 } }
Im dem Moment, in dem main() die Funktion clear() aufruft, gibt es sozusagen die Namen q und p für das Objekt, wobei nur clear() das Objekt unter p kennt und main() nicht, und clear() von dem Namen q keine Idee hat.
|
Wertübergabe Call by Value
Primitive Variablen werden immer per Wert kopiert (engl. call by value). Wenn wir folgende Zeilen betrachten, ist leicht zu erkennen, wie sich die Daten verändern. Zunächst deklarieren wir zwei Variablen:
int i = 2; int j;
Anschließend weisen wir j den Wert von i zu:
j = i;
An dieser Stelle wird der Wert aus i ausgelesen und in j kopiert. Dabei ist es der Zuweisung ziemlich egal, woher der Wert kommt (er könnte beispielsweise auch die Rückgabe einer Funktion sein). Die Ausgabe gibt demnach 2 aus:
System.out.println( j ); // 2
Ändert sich die Variable i und geben wir j aus, so ist die Ausgabe natürlich immer noch 2, da eine Änderung von j keine Änderung von i nach sich zieht.
i = 3; System.out.println( j ); // 2
Referenzübergabe Call by Value
Die Referenzen werden wie primitive Werte kopiert. Daher hat auch die folgende Funktion keine Nebenwirkungen:
static void clear( Point p ) { p = new Point(); }
Nach der Zuweisung referenziert die Variable p ein anderes Punkt-Objekt, und das übergebene Argument an die Funktion geht verloren. Diese Änderung wird nach außen hin nicht sichtbar, was bedeutet, dass der Aufrufer kein neues Objekt unter sich hat.
Call by Reference gibt es in Java nicht – ein Blick auf C und C++
In C++ gibt es eine weitere Argumentübergabe, die sich call by reference nennt. Würde eine Funktion wie clear() mit Referenzsemantik deklariert, stellt die Variable p ein Synonym, also einen anderen Namen für eine Variable – in unserem Fall q – dar. Damit würde die Zuweisung im Rumpf den Zeiger auf ein neues Objekt legen. Die swap()-Funktion ist ein gutes Beispiel für die Nützlichkeit von call by reference:
void swap( int& a, int& b ) { int tmp = a; a = b; b = tmp; }
Zeiger und Referenzen sind in C++ etwas anderes, was Spracheinsteiger leicht irritiert. Denn in C++ und auch C hätte eine vergleichbare swap()-Funktion auch mit Zeigern implementiert werden können:
void swap( int *a, int *b ) { int tmp = *a; *a = *b; *b = tmp; }
Die Implementierung gibt in C(++) einen Verweis auf das Argument.