Galileo Computing < openbook > Galileo Computing - Professionelle Bücher. Auch für Einsteiger.
Professionelle Bücher. Auch für Einsteiger

Java ist auch eine Insel von Christian Ullenboom
Programmieren für die Java 2-Plattform in der Version 5 (Tiger-Release)
Buch: Java ist auch eine Insel
gp Kapitel 3 Klassen und Objekte
  gp 3.1 Objektorientierte Programmierung
    gp 3.1.1 Warum überhaupt OOP?
    gp 3.1.2 Modularität und Wiederverwertbarkeit
  gp 3.2 Klassen benutzen
    gp 3.2.1 Die Klasse Point
    gp 3.2.2 Etwas über die UML
    gp 3.2.3 Anlegen eines Exemplars einer Klasse mit new
    gp 3.2.4 Zugriff auf Variablen und Methoden mit dem ».«
    gp 3.2.5 Konstruktoren
  gp 3.3 Import und Pakete
  gp 3.4 Die API-Dokumentation
  gp 3.5 Mit Referenzen arbeiten
    gp 3.5.1 Die null-Referenz
    gp 3.5.2 Zuweisungen bei Referenzen
    gp 3.5.3 Funktionen mit nichtprimitiven Parametern
    gp 3.5.4 Gleichheit von Objekten und die Methode equals()
  gp 3.6 Arrays
    gp 3.6.1 Deklaration von Arrays
    gp 3.6.2 Arrays mit Inhalt
    gp 3.6.3 Die Länge eines Arrays über das Attribut length
    gp 3.6.4 Zugriff auf die Elemente
    gp 3.6.5 Array-Objekte erzeugen
    gp 3.6.6 Fehler bei Arrays
    gp 3.6.7 Arrays mit nicht-primitiven Elementen
    gp 3.6.8 Initialisierte Array-Objekte
    gp 3.6.9 Die erweiterte for-Schleife
    gp 3.6.10 Mehrdimensionale Arrays
    gp 3.6.11 Die Wahrheit über die Array-Initialisierung
    gp 3.6.12 Mehrere Rückgabewerte
    gp 3.6.13 Argument per Referenz übergeben
    gp 3.6.14 Arrays klonen
    gp 3.6.15 Feldinhalte kopieren
    gp 3.6.16 Die Klasse Arrays zum Vergleichen, Füllen, Suchen
    gp 3.6.17 Methode mit variabler Argumentanzahl (vararg)
  gp 3.7 Der Einstiegspunkt für das Laufzeitsystem main()
    gp 3.7.1 Kommandozeilen-Parameter ausgeben
    gp 3.7.2 Der Rückgabewert von main() und System.exit()
    gp 3.7.3 Parser der Kommandozeilenargumente Apache CLI
  gp 3.8 Eigene Pakete schnüren
    gp 3.8.1 Die package-Anweisung
    gp 3.8.2 Importieren von Klassen mit import
    gp 3.8.3 Paketnamen
    gp 3.8.4 Hierarchische Strukturen und das Default-Package
    gp 3.8.5 Klassen mit gleiche Namen in unterschiedlichen Paketen
    gp 3.8.6 Statische Imports
    gp 3.8.7 Eine Verzeichnisstruktur für eigene Projekte


Galileo Computing

3.5 Mit Referenzen arbeitedowntop


Galileo Computing

3.5.1 Die null-Referenz  downtop

In Java gibt es das spezielle Literal null, das anzeigt, dass eine Referenzvariable auf kein Objekt verweist. Der Wert ist nur für Referenzen vorgesehen und kann nicht in einen primitiven Typ wie die Ganzzahl 0 umgewandelt werden. Die null-Referenz ist typenlos, das heißt, sie kann jedem Objekt zugewiesen werden 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.

Mit null lässt sich eine ganze Menge machen. Der Haupteinsatz sieht vor, damit uninitialisierte Referenzvariablen zu kennzeichnen. In Listen oder Bäumen kennzeichnet null aber auch keinen gültigen Nachfolger; 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, weiß aber erst zur Laufzeit, was referenziert wird. Wird versucht, über die null-Referenz auf eine Eigenschaft eines Objekts zuzugreifen, dann wird eine NullPointerException ausgelöst.


Beispiel   Wir beobachten eine NullPointerException. Die Zeilen sind durch Kommentare deutlich gemacht.

Listing 3.2   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 */}

Das Programm bricht bei p.setLocation() 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. Es scheint auf den ersten Blick nicht viel auszumachen, ob alle Teilausdrücke ausgewertet werden oder nicht. Es ist aber in einigen Ausdrücken wichtig, wie das folgende Beispiel zeigt:


if ( p != null && p.x >= 10 )

Die Bedingung testet, ob p überhaupt auf ein Objekt verweist, und ob die x-Koordinate mindestens 10 ist. Diese Schreibweise tritt häufig auf, und der Und-Operator zur Verknüpfung muss ein Kurzschlussoperator sein, denn in diesem Fall kommt es ausdrücklich darauf an, dass die Koordinate nur dann bestimmt wird, wenn die Variable p überhaupt auf ein Point-Objekt verweist. Andernfalls bekämen wir eine NullPointerException, wenn jeder Teilausdruck ausgewertet würde und p gleich null ist.


Galileo Computing

3.5.2 Zuweisungen bei Referenzen  downtop

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 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.

Abbildung
Hier klicken, um das Bild zu Vergrößern


Beispiel   Das hat die Konsequenz, dass bei einer Änderung des Punkt-Objekts über die in der Variablen p gespeicherte Referenz die Änderung auch bei Zugriff über die Variable q beobachtet werden kann.

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.


Vergleich mit der Kopie von primitiven Werten

Primitive Variablen werden immer per Wert kopiert. Betrachten wir folgende Zeilen, dann ist leicht zu sehen, 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. 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. Ändert sich nun das 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. Wir könnten also sagen: »Kopierer haben keine Geschichte«.


j = i;
System.out.println( j );

i = 3;
System.out.println( j );

Galileo Computing

3.5.3 Funktionen mit nichtprimitiven Parametern  downtop

Dass sich das gleiche Objekt unter zwei Namen (über zwei verschiedene Variablen) ansprechen lässt, werden wir auch bei Methoden beobachten können. Eine Funktion, die als Argument eine Objektreferenz erhält, bezieht sich genau auf das übergebene Objekt. Das heißt, die Funktion kann dieses Objekt mit den angebotenen Methoden ändern oder auf die Attribute zugreifen.

Listing 3.3   PointFunktion.java


import java.awt.Point;

public class PointFunktion
{
  static void foo( Point p )
  {
    p.x = 10;
  }

  public static void main( String args[] )
  {
    Point q = new Point( 0,0 );
    foo( q );
    System.out.println( q.x );  // 10
  }
}

Wieder ein alternatives Beispiel mit Zeichenketten

Dieses Beispiel ist insofern bedeutend, da es bewusst macht, dass es in Java keine Referenzsemantik für Objekte wie in C++ gibt. Die Referenzen werden wie primitive Werte kopiert. Daher hat auch die folgende Funktion keine Nebenwirkungen:


static void buuh( 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 nicht nach außen sichtbar, das heißt, der Aufrufer wird plötzlich kein neues Objekt unter sich haben.

Eine Argumentübergabe per Referenz (call by reference), wie sie in C++ verwendet wird, gibt es in Java nicht. In C++ ließe sich das von Java benutzte Verfahren nur mittelbar nachbilden, indem überall explizit Zeiger auf Objekte (anstelle der Objekte selbst) verwendet würden. Wir werden gleich noch ein zweites Beispiel für die Auswirkungen der in Java allgegenwärtigen Objektreferenzen kennen lernen.


Galileo Computing

3.5.4 Gleichheit von Objekten und die Methode equals()  toptop

Die Zuweisung mit dem Gleichheitszeichen schafft eine zusätzliche Kopie einer Referenz auf ein bereits existierendes Objekt. Der Vergleichsoperator == ist für alle Datentypen so definiert, dass er die vollständige Übereinstimmung zweier Werte testet. Bei primitiven Datentypen ist das einfach einzusehen, und bei Referenztypen ist dies im Prinzip genauso. Der Operator == testet bei Referenzen, ob diese übereinstimmen, also auf das gleiche Objekt verweisen. Demnach sagt der Test etwas über die Identität der referenzierten Objekte aus, aber nicht, ob zwei verschiedene Objekte möglicherweise den gleichen Inhalt haben.


Beispiel   Drei unterschiedliche Punktvariablen und die Bedeutung von ==

Point p = new Point();
p.x = 12;

Point q = p;

Point r = new Point();
r.x = 12;

if ( p == q )    // ist wahr, da p und q dasselbe Objekt referenzieren
  ...

if ( p == r )    // ist falsch, da p und r zwei Verschiedene
Punkt-Objekte
  ...            // referenzieren, die zufällig dieselben Koordinaten haben

Da p und q auf dasselbe Objekt verweisen, ergibt der Vergleich true. p und r referenzieren unterschiedliche Objekte, die aber zufälligerweise den gleichen Inhalt haben. Doch woher soll der Compiler wissen, wann zwei Punkt-Objekte inhaltlich gleich sind? Weil ein Punkt sich durch die Attribute x und y auszeichnet? Die Laufzeitumgebung könnte voreilig die Belegung jeder Objektvariablen vergleichen, doch das entspricht nicht immer einem korrekten Vergleich, so wie wir ihn uns wünschen. Ein Punkt-Objekt könnte etwa zusätzlich die Anzahl der Zugriffe zählen, die jedoch für einen Vergleich, der auf der Lage zweier Punkte basiert, nicht berücksichtigt werden darf.

Die Methode equals()

Die allgemein gültige Lösung ist, die Klasse festlegen zu lassen, wann Objekte gleich sind. Da Klassen Eigenschaften definieren, können nur sie Attribute für einen Gleichheitstest heranziehen. Dazu kann jede Klasse eine Methode equals() implementieren, die Exemplare dieser Klasse mit beliebigen anderen Objekten vergleichen kann, und true liefern, wenn die gewünschten Zustände (Objektvariablen) übereinstimmen.


Beispiel   Zwei inhaltlich gleiche Punkt-Objekte verglichen mit == und equals().

Point a = new Point( 10, 10 );
Point b = new Point( 10, 10 );

if ( a == b )         // false
  ...

if ( a.equals(b) )    // true
  ...

Nur equals() testet in diesem Fall die inhaltliche Gleichheit.


Aus den unterschiedlichen Bedeutungen müssen wir demnach die Begriffe »Identität« und »Gleichheit« von Objekten sorgfältig unterscheiden. Daher noch einmal eine Zusammenfassung:
  Getestet mit Implementierung
Identität == Nichts zu tun
Gleichheit equals() Aus der Klasse Object geerbte Methode muss geeignet überschrieben werden.

Es gibt immer ein equals()

Glücklicherweise müssen wir als Programmierer nicht lange darüber nachdenken, ob eine Klasse eine equals()-Methode anbieten soll oder nicht. Jede Klasse besitzt sie, da die universelle Oberklasse Object sie vererbt. Eine Unterklasse, wie etwa Point, kann nun eine eigene Implementierung angeben. Sie muss es aber nicht.

Werfen wir einen Blick auf die equals()-Methode aus Point, um eine Vorstellung von der Arbeitsweise zu bekommen:


public boolean equals( Object obj )
{
  if ( obj instanceof Point ) {
    Point pt = (Point)obj;
    return (x == pt.x) && (y == pt.y);   // (*)
  }
  return super.equals(obj);
}

Obwohl bei diesem Beispiel für uns einiges neu ist, erkennen wir den Vergleich in der Zeile (*). Hier vergleicht das Point-Objekt seine eigenen Attribute mit den Attributen des Objekts, das als Argument an equals() übergeben wurde.

Die Oberklasse Object und ihr equals()

Wenn eine Klasse keine equals()-Methode angibt, dann erbt sie eine Implementierung aus der Klasse Object, die wie folgt aussieht:


public boolean equals( Object obj )
{
  return ( this == obj );
}

Wir erkennen, dass hier die Gleichheit auf die Gleichheit der Referenzen abgebildet wird. Ein inhaltlicher Vergleich findet nicht statt.


Hinweis   Der Datentyp für den Parameter in der equals()-Funktion ist immer Object und niemals etwas anderes, da sonst die equals()-Funktion nicht überschrieben, sondern überladen wird. Folgendes für eine Klasse K ist also falsch:

public class K
{
  private int v;
  public boolean equals(   K   that ) { return this.v == that.v; }
}






1   Hier unterscheiden sich C(++) und Java.

2   Der Name zeigt das Überbleibsel von Zeigern. Zwar haben wir es in Java nicht mir 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.





Copyright © Galileo Press GmbH 2004
Für Ihren privaten Gebrauch dürfen Sie die Online-Version natürlich ausdrucken. Ansonsten unterliegt das <openbook> denselben Bestimmungen, wie die gebundene Ausgabe: Das Werk einschließlich aller seiner Teile ist urheberrechtlich geschützt. Alle Rechte vorbehalten einschließlich der Vervielfältigung, Übersetzung, Mikroverfilmung sowie Einspeicherung und Verarbeitung in elektronischen Systemen.


[Galileo Computing]

Galileo Press GmbH, Gartenstraße 24, 53229 Bonn, Tel.: 0228.42150.0, Fax 0228.42150.77, info@galileo-press.de