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

Inhaltsverzeichnis
Vorwort
1 Java ist auch eine Sprache
2 Sprachbeschreibung
3 Klassen und Objekte
4 Der Umgang mit Zeichenketten
5 Eigene Klassen schreiben
6 Exceptions
7 Generics<T>
8 Äußere.innere Klassen
9 Besondere Klassen der Java SE
10 Architektur, Design und angewandte Objektorientierung
11 Die Klassenbibliothek
12 Bits und Bytes und Mathematisches
13 Datenstrukturen und Algorithmen
14 Threads und nebenläufige Programmierung
15 Raum und Zeit
16 Dateien, Verzeichnisse und Dateizugriffe
17 Datenströme
18 Die eXtensible Markup Language (XML)
19 Grafische Oberflächen mit Swing
20 Grafikprogrammierung
21 Netzwerkprogrammierung
22 Verteilte Programmierung mit RMI
23 JavaServer Pages und Servlets
24 Datenbankmanagement mit JDBC
25 Reflection und Annotationen
26 Dienstprogramme für die Java-Umgebung
A Die Begleit-DVD
Stichwort
Ihre Meinung?

Spacer
 <<   zurück
Java ist auch eine Insel von Christian Ullenboom
Das umfassende Handbuch
Buch: Java ist auch eine Insel

Java ist auch eine Insel
geb., mit DVD
1482 S., 49,90 Euro
Rheinwerk Computing
ISBN 978-3-8362-1506-0
Pfeil 9 Besondere Klassen der Java SE
  Pfeil 9.1 Vergleichen von Objekten
    Pfeil 9.1.1 Natürlich geordnet oder nicht?
    Pfeil 9.1.2 Die Schnittstelle Comparable
    Pfeil 9.1.3 Die Schnittstelle Comparator
    Pfeil 9.1.4 Rückgabewerte kodieren die Ordnung
  Pfeil 9.2 Wrapper-Klassen und Autoboxing
    Pfeil 9.2.1 Wrapper-Objekte erzeugen
    Pfeil 9.2.2 Konvertierungen in eine String-Repräsentation
    Pfeil 9.2.3 Die Basisklasse Number für numerische Wrapper-Objekte
    Pfeil 9.2.4 Vergleiche durchführen mit »compare()«, »compareTo()« und »equals()«
    Pfeil 9.2.5 Die Klasse Integer
    Pfeil 9.2.6 Die Klassen Double und Float für Fließkommazahlen
    Pfeil 9.2.7 Die Boolean-Klasse
    Pfeil 9.2.8 Autoboxing: Boxing und Unboxing
  Pfeil 9.3 Object ist die Mutter aller Klassen
    Pfeil 9.3.1 Klassenobjekte
    Pfeil 9.3.2 Objektidentifikation mit »toString()«
    Pfeil 9.3.3 Objektgleichheit mit »equals()« und Identität
    Pfeil 9.3.4 Klonen eines Objekts mit »clone()« *
    Pfeil 9.3.5 Hashcodes über »hashCode()« liefern *
    Pfeil 9.3.6 Aufräumen mit »finalize()« *
    Pfeil 9.3.7 Synchronisation *
  Pfeil 9.4 Die Spezial-Oberklasse Enum
    Pfeil 9.4.1 Methoden auf Enum-Objekten
    Pfeil 9.4.2 »Enum« mit eigenen Konstruktoren und Methoden *
  Pfeil 9.5 Erweitertes »for« und »Iterable«
    Pfeil 9.5.1 Die Schnittstelle »Iterable«
    Pfeil 9.5.2 Einen eigenen Iterable implementieren *
  Pfeil 9.6 Zum Weiterlesen


Rheinwerk Computing - Zum Seitenanfang

9.3 Object ist die Mutter aller Klassen  Zur nächsten ÜberschriftZur vorigen Überschrift

java.lang.Object ist die oberste aller Klassen. Somit spielt diese Klasse eine ganz besondere Rolle, da alle anderen Klassen automatisch Unterklassen sind und die Methoden erben beziehungsweise überschreiben.


Rheinwerk Computing - Zum Seitenanfang

9.3.1 Klassenobjekte  Zur nächsten ÜberschriftZur vorigen Überschrift

Zwar ist jedes Objekt ein Exemplar einer Klasse – doch was ist eine Klasse? In einer Sprache wie C++ existieren Klassen nicht zur Laufzeit, und der Compiler übersetzt die Klassenstruktur in ein ausführbares Programm. Im absoluten Gegensatz dazu steht Smalltalk: Diese Laufzeitumgebung verwaltet Klassen selbst als Objekte. Diese Idee, Klassen als Objekte zu repräsentieren, übernimmt auch Java – Klassen sind Objekte vom Typ java.lang.Class.


class java.lang.Object

  • final Class<? extends Object> getClass() Liefert die Referenz auf das Klassenobjekt, die das Objekt konstruiert hat. Das Class-Objekt ist immer eindeutig in der JVM, sodass auch mehrere Anfragen an getClass() immer dasselbe Class-Objekt liefern.

Beispiel Die Objektmethode getName() eines Class-Objekts liefert den Namen der Klassen:

System.out.println( "Klaviklack".getClass().getName() ); // java.lang.String

Klassen-Literale

Ein Klassen-Literal (engl. class literal) ist ein Ausdruck der Form Datentyp.class, wobei Datentyp entweder eine Klasse, eine Schnittstelle, ein Feld oder ein primitiver Typ ist. Beispiele sind String.class, Integer.class oder int.class (was nicht mit Integer.class identisch ist). Der Ausdruck ist immer vom Typ Class. Bei primitiven Typen liefert die Schreibweise primitiverTyp.class das gleiche Ergebnis wie WrapperTyp.TYPE; es ist also Integer.TYPE identisch mit int.class. Class-Objekte spielen insbesondere bei dynamischen Abfragen über die so genannte Reflection eine Rolle. Zur Laufzeit können so beliebige Klassen geladen, Objekte erzeugt und Methoden aufgerufen werden.


Rheinwerk Computing - Zum Seitenanfang

9.3.2 Objektidentifikation mit »toString()«  Zur nächsten ÜberschriftZur vorigen Überschrift

Jedes Objekt sollte sich durch die Methode toString() mit einer Zeichenkette identifizieren und den Inhalt der interessanten Attribute als Zeichenkette liefern.


Beispiel Die Klasse Point implementiert toString() so, dass der Rückgabestring die Koordinaten enthält:

System.out.println( new java.awt.Point() );  // java.awt.Point[x=0,y=0]

Das Angenehme ist, dass toString() automatisch aufgerufen wird, wenn die Methoden print() oder println() mit einer Objektreferenz als Argument aufgerufen werden. Ähnliches gilt für den Zeichenkettenoperator + mit einer Objektreferenz als Operand:

Listing 9.8  com/tutego/insel/object/tostring/Player.java, Player

public class Player
{
  String name;
  int    age;

  @Override
  public String toString()
  {
    return getClass().getName() +
           "[name=" + name +
           ",age=" + age + "]";
  }
}

Die Ausgabe mit den Zeilen

Listing 9.9  com/tutego/insel/object/tostring/PlayerToStringDemo.java, main()

Player tinkerbelle = new Player();
tinkerbelle.name   = "Tinkerbelle";
tinkerbelle.age    = 32;
System.out.println( tinkerbelle.toString() );
System.out.println( tinkerbelle );

ist damit:

com.tutego.insel.object.tostring.Player[name=Tinkerbelle,age=32]
com.tutego.insel.object.tostring.Player[name=Tinkerbelle,age=32]

Bei einer eigenen Implementierung müssen wir darauf achten, dass die Sichtbarkeit public ist, da toString() in der Oberklasse Object öffentlich vorgegeben ist und wir in der Unterklasse die Sichtbarkeit nicht einschränken können. Zwar bringt die Spezifikation nicht deutlich zum Ausdruck, dass toString() nicht null als Rückgabe liefern darf, doch ist dann der Leerstring "" allemal besser. Die Annotation @Override macht das Überschreiben deutlich.

Standardimplementierung

Neue Klassen sollten toString() überschreiben. Ist dies nicht der Fall, gelangt das Programm zur Standardimplementierung in Object, wo lediglich der Klassenname und der wenig aussagekräftige Hash-Wert hexadezimal zusammengebunden werden.

public String toString()
{
  return getClass().getName() + "@" + Integer.toHexString(hashCode());
}

class java.lang.Object

  • String toString() Liefert eine String-Repräsentation des Objekts aus Klassenname und Hash-Wert.

Zwar sagt der Hash-Wert selbst wenig aus, allerdings ist er ein erstes Indiz dafür, dass bei Klassen, die keine toString()- und hashCode()-Methoden überschreiben, zwei Referenzen nicht identisch sind.


Beispiel Ein Objekt der class A {} wird gebildet, und toString() liefert die ID, die ausgegeben wird:

System.out.println( new A().toString() );  // A@923e30
System.out.println( new A().toString() );  // A@130c19b

»toString()« generieren lassen

Die Methode eignet sich gut zum Debugging, doch ist das manuelle Tippen der Methoden lästig. Zwei Lösungen vereinfachen das Implementieren der Methode toString():

  • Eclipse und NetBeans können standardmäßig über das Kontextmenü eine toString()-Methode anhand ausgewählter Attribute generieren.
  • Die Zustände werden automatisch über Reflection ausgelesen. Hier führt Apache Commons Lang (http://jakarta.apache.org/commons/lang/) auf den richtigen Weg.

Rheinwerk Computing - Zum Seitenanfang

9.3.3 Objektgleichheit mit »equals()« und Identität  Zur nächsten ÜberschriftZur vorigen Überschrift

Ob zwei Referenzen dasselbe Objekt repräsentieren, stellt der Vergleichsoperator == fest. Er testet die Identität, nicht jedoch automatisch die inhaltliche Gleichheit. Am Beispiel mit Zeichenketten ist das gut zu erkennen: Ein Vergleich mit firstname == "Christian" hat im Allgemeinen einen falschen, unbeabsichtigten Effekt, obwohl er syntaktisch korrekt ist. An dieser Stelle sollte der inhaltliche Vergleich stattfinden: Stimmen alle Zeichen der Zeichenkette überein?

Eine equals()-Methode sollte Objekte auf Gleichheit prüfen. So besitzt das String-Objekt eine Implementierung, die jedes Zeichen vergleicht:

String firstname = "Christian";
if ( firstname.equals( "Christian" ) )
  ...

class java.lang.Object

  • boolean equals( Object o ) Testet, ob das andere Objekt mit dem eigenen gleich ist. Die Gleichheit definiert jede Klasse für sich anders, doch die Basisklasse vergleicht nur die Referenzen o == this.

equals()-Implementierung aus Object und Unterklassen

Die Standardimplementierung aus der absoluten Oberklasse Object kann über die Gleichheit von speziellen Objekten nichts wissen und testet lediglich die Referenzen:

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

Überschreibt eine Klasse equals() nicht, ist das Ergebnis von o1.equals(o2) gleichwertig mit o1 == o2. Unterklassen überschreiben diese Methode, um einen inhaltlichen Vergleich mit ihren Zuständen vorzunehmen. Die Methode ist in Unterklassen gut aufgehoben, denn jede Klasse benötigt eine unterschiedliche Logik, um festzulegen, wann ein Objekt mit einem anderen Objekt gleich ist.

Nicht jede Klasse implementiert eine eigene equals()-Methode, sodass die Laufzeitumgebung unter Umständen ungewollt bei Object und seinem Referenzenvergleich landet. Dies hat ungeahnte Folgen, und diese Fehleinschätzung kommt leider bei Exemplaren der Klassen StringBuffer und StringBuilder vor, die kein eigenes equals() implementieren. Wir haben dies bereits in Kapitel 4, »Der Umgang mit Zeichenketten«, erläutert.

Ein eigenes »equals()«

Bei selbst deklarierten Methoden ist Vorsicht geboten, da wir genau auf die Signatur achten müssen. Die Methode muss ein Object akzeptieren und boolean zurückgeben. Wird diese Signatur falsch verwendet, kommt es statt zu einer Überschreibung der Methode zu einer Überladung und bei einer Rückgabe ungleich boolean zu einer zweiten Methode mit gleicher Signatur, was Java nicht zulässt (Java erlaubt bisher keine kovarianten Parametertypen). Um das Problem zu minimieren, sollte die Annotation @Override an equals() angeheftet sein.

Die equals()-Methode stellt einige Anforderungen:

  • Heißt der Vergleich equals(null), so ist das Ergebnis immer false.
  • Kommt ein this hinein, lässt sich eine Abkürzung nehmen und true zurückliefern.
  • Das Argument ist zwar vom Typ Object, aber dennoch vergleichen wir immer konkrete Typen. Eine equals()-Methode einer Klasse X wird sich daher nur mit Objekten vom Typ X vergleichen lassen. Eine spannende Frage ist, ob equals() auch Unterklassen von X beachten soll.
  • Eine Implementierung von equals() sollte immer eine Implementierung von hashCode() bedeuten, denn wenn zwei Objekte equals()-gleich sind, müssen auch die Hashwerte gleich sein. Bei einer geerbten hashCode()-Methode aus Object ist das aber nicht in jedem Fall erfüllt.

Die beiden ersten Punkte sind leicht erfüllbar, und ein Beispiel für einen Club mit den Attributen numberOfPersons und sm ist schnell implementiert:

@Override
public boolean equals( Object o )
{
  if ( o == null )
    return false;

  if ( o == this )
    return true;

  Club that = (Club) o;

  return    this.numberOfPersons == that.numberOfPersons
         && this.sm == that.sm;
}

Diese Lösung erscheint offensichtlich, führt aber spätestens bei einem Nicht-Club-Objekt zu einer ClassCastException. Das Problem scheint schnell behoben:

if ( ! o instanceof Club )
  return false;

Jetzt sind wir auf der sicheren Seite, aber das Ziel ist noch nicht ganz erreicht.


Hinweis Die equals()-Methode sollte bei nicht passenden Typen immer false zurückgeben und keine Ausnahme auslösen.


Das Problem der Symmetrie *

Zwar funktioniert die aufgeführte Implementierung bei finalen Klassen schön, doch bei Unterklassen ist die Symmetrie gebrochen. Warum? Ganz einfach: instanceof testet Typen in der Hierarchie, liefert also auch dann true, wenn das an equals() übergebene Argument eine Unterklasse von Club ist. Diese Unterklasse wird wie die Oberklasse die gleichen Attribute haben, sodass – aus der Sicht von Club – alles in Ordnung ist. Nehmen wir einmal die Varia-blen club und superClub an, die die Typen Club und SuperClub – die fiktive Unterklasse von Club – besitzen. Sind beide Objekte gleich, so ergibt club.equals(superClub) das Ergebnis true. Drehen wir den Spieß um, und fragen wir, was superClub.equals(club) ergibt. Zwar haben wir SuperClub nicht implementiert, nehmen aber an, dass dort eine equals()-Methode steckt, die nach dem gleichen instanceof-Schema implementiert wurde wie Club. Dann wird dort bei einem Test Folgendes ausgeführt: club instanceof superClub – und das ist false. Damit wird aber die Fallunterscheidung mit return false beendet. Fassen wir zusammen:

club.equals( superClub ) == true
superClub.equals( club ) == false

Das darf nicht sein, und zur Lösung dürfen wir nicht instanceof verwenden, sondern müssen fragen, ob der Typ exakt ist. Das geht mit getClass(). Korrekt ist daher Folgendes:

Listing 9.10  com/tutego/insel/object/equals/Club.java, Club

public class Club
{
  int numberOfPersons;
  int sm;

  @Override
  public boolean equals( Object o )
  {
    if ( o == null )
      return false;

    if ( o == this )
      return true;

    if ( ! o.getClass().equals(getClass()) )
      return false;

    Club that = (Club) o;

    return    this.numberOfPersons == that.numberOfPersons
           && this.sm   == that.sm;
  }

  @Override
  public int hashCode()
  {
    return (31 + numberOfPersons) * 31 + sm;
  }
}

Die hashCode()-Methode besprechen wir in Abschnitt 9.3.5, »Hashcodes über hashCode() liefern« – sie steht nur der Vollständigkeit halber hier, da equals() und hashCode() immer Hand in Hand gehen sollten.

Es ist günstig, bei erweiterten Klassen ein neues equals() anzugeben, sodass auch die neuen Attribute in den Test einbezogen werden. Bei hashCode()-Methoden müssen wir eine ähnliche Strategie anwenden, was wir hier nicht zeigen wollen.

Einmal gleich, immer gleich *

Ein weiterer Aspekt von equals() [Eine korrekte Implementierung der Methode equals() bildet eine Äquivalenzrelation. Lassen wir die null-Referenz außen vor, ist sie reflexiv, symmetrisch und transitiv. ] ist der folgende: Das Ergebnis muss während der gesamten Lebensdauer eines Objekts gleich bleiben. Ein kleines Problem steckt dabei in equals() der Klasse URL, die vergleicht, ob zwei URL-Adressen auf die gleiche Ressource zeigen. In der Dokumentation heißt es:

»Two URL objects are equal if they have the same protocol, reference equivalent hosts, have the same port number on the host, and the same file and fragment of the file.«

Hostnamen gelten als gleich, wenn entweder beide auf dieselbe IP-Adresse zeigen oder – falls eine nicht auflösbar ist – beide Hostnamen gleich (ohne Groß-/Kleinschreibung) oder null sind. Da hinter den URLs http://tutego.de/ und http://java-tutor.com/ aber letztendlich http://www.tutego.com/ steckt, liefert equals() die Rückgabe true:

Listing 9.11  com/tutego/insel/object/equals/UrlEquals.java, main()

URL url1 = new URL( "http://tutego.com/" );
URL url2 = new URL( "http://www.tutego.com/" );
System.out.println( url1.equals(url2) );               // true

Die dynamische Abbildung der Hostnamen auf die IP-Adresse des Rechners kann aus mehreren Gründen problematisch sein:

  • Der (menschliche) Leser erwartet intuitiv etwas anderes.
  • Wenn keine Netzwerkverbindung besteht, wird keine Namensauflösung durchgeführt, und der Vergleich liefert false. Die Rückgabe sollte jedoch nicht davon abhängig sein, ob eine Netzwerkverbindung besteht.
  • Dass die beiden URLs auf den gleichen Server zeigen, könnte sich zur Laufzeit ändern.

Rheinwerk Computing - Zum Seitenanfang

9.3.4 Klonen eines Objekts mit »clone()« *  Zur nächsten ÜberschriftZur vorigen Überschrift

Zum Replizieren eines Objekts gibt es oft zwei Möglichkeiten:

  • einen Konstruktor (auch Copy-Constructor genannt), der ein vorhandenes Objekt als Vorlage nimmt, ein neues Objekt anlegt und die Zustände kopiert
  • eine öffentliche clone()-Methode

Was eine Klasse nun anbietet, ist in der API-Dokumentation zu erfahren.


Beispiel Erzeuge ein Punkt-Objekt, und klone es:

Listing 9.12  com/tutego/insel/object/clone/ClonePoint.java, main()

java.awt.Point p = new java.awt.Point( 12, 23 );
java.awt.Point q = (java.awt.Point) p.clone();
System.out.println( q );                       // java.awt.Point[x=12,y=23]

Mehr als 300 Klassen der Java-Bibliothek unterstützen ein clone(), das ein neues Exemplar mit dem gleichen Zustand zurückgibt. Eine überschriebene Methode kann den Typ der Rückgabe dank kovarianter Rückgabetypen anpassen. Die clone()-Methode bei java.awt.Point bleibt allerdings bei Object.


Felder erlauben standardmäßig clone(). Speichern die Arrays jedoch nicht-primitive Werte, liefert clone() nur eine flache Kopie, was bedeutet, dass das neue Feldobjekt, der Klon, die exakt gleichen Objekte wie das Original referenziert und die Einträge selbst nicht klont.

»clone()« aus »java.lang.Object«

Da clone() nicht automatisch unterstützt wird, stellt sich die Frage, wie wir clone() für unsere Klassen mit geringstem Aufwand umsetzen können. Erster Vorschlag:

Object o = new Object();
o.clone();                   // Fehler Compilerfehler, da clone() nicht sichtbar ist.

class java.lang.Object

  • protected Object clone() throws CloneNotSupportedException Liefert eine Kopie des Objekts.

Eine eigene clone()-Methode

Eigene Klassen überschreiben die protected-Methode clone() aus der Oberklasse java .lang.Object und machen sie public. Für die Implementierung kommen zwei Möglichkeiten in Betracht:

  • Von Hand ein neues Objekt anlegen, alle Attribute kopieren und die Referenz auf das neue Objekt zurückgeben.
  • Das Laufzeitsystem soll selbst eine Kopie anlegen, und diese geben wir zurück. Lösung zwei verkürzt die Entwicklungszeit und ist auch spannender.

Um das System zum Klonen zu bewegen, müssen zwei Dinge getan werden:

  • Der Aufruf super.clone() stößt die Methode clone() aus Object an und veranlasst so die Laufzeitumgebung, ein neues Objekt zu bilden und die nicht-statischen Attribute zu kopieren. Die Methode kopiert elementweise die Daten des aktuellen Objekts in das neue. Die Methode ist in der Oberklasse protected, aber das ist der Trick: Nur Unterklassen können clone() aufrufen, keiner sonst.
  • Die Klasse implementiert die Markierungsschnittstelle Cloneable. Falls von außen ein clone() auf einem Objekt aufgerufen wird, dessen Klasse nicht Cloneable implementiert, ist das Ergebnis eine CloneNotSupportedException. Natürlich implementiert java .lang.Object die Schnittstelle Cloneable nicht selbst, denn sonst hätten ja Klassen schon automatisch diesen Typ, was sinnlos wäre.

clone() gibt eine Referenz auf das neue Objekt zurück, und wenn es keinen freien Speicher mehr gibt, folgt ein OutOfMemoryError.

Nehmen wir an, für ein Spiel sollen Player geklont werden:

Listing 9.13  com/tutego/insel/object/clone/Player.java

package com.tutego.insel.object.clone;

public class Player implements Cloneable
{
  public String name;
  public int    age;

  @Override
  public Player clone()
  {
    try
    {
      return (Player) super.clone();
    }
    catch ( CloneNotSupportedException e ) {
      // Kann eigentlich nicht passieren, da Cloneable
      throw new InternalError();
    }
  }
}

Da es seit Java 5 kovariante Rückgabetypen gibt, gibt clone() nicht lediglich Object, sondern den Untertyp Player zurück.

Testen wir die Klasse etwa so:

Listing 9.14  com/tutego/insel/object/clone/PlayerCloneDemo.java, main()

Player susi = new Player();
susi.age  = 29;
susi.name = "Susi";
Player dolly = susi.clone();
System.out.println( dolly.name + " ist " + dolly.age ); // Susi ist 29

»clone()« und »equals()«

Die Methode clone() und die Methode equals() hängen, wie auch equals() und hashCode(), miteinander zusammen. Wenn die clone()-Methode überschrieben wird, sollte auch equals() angepasst werden, denn ohne ein überschriebenes equals() bleibt Folgendes in Object stehen:

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

Das bedeutet aber, dass ein geklontes Objekt – das ja im Allgemeinen ein neues Objekt ist – durch seine neue Objektidentität nicht mehr equals()-gleich zu seinem Erzeuger ist. Formal heißt das: o.clone().equals(o) == false. Diese Semantik dürfte nicht erwünscht sein.

Flach oder tief?

Das clone() vom System erzeugt standardmäßig eine flache Kopie (engl. shallow copy). Bei untergeordneten Objekten werden nur die Referenzen kopiert, und das Originalobjekt sowie die Kopie verweisen anschließend auf dieselben untergeordneten Objekte (sie verwenden diese gemeinsam). Wenn zum Beispiel die Bedienung ein Attribut für einen Arbeitgeber besitzt und eine Kopie der Bedienung erzeugt wird, wird der Klon auf den gleichen Arbeitgeber zeigen. Bei einem Arbeitgeber mag das noch stimmig sein, aber bei Datenstrukturen ist mitunter eine tiefe Kopie (engl. deep copy) erwünscht. Bei dieser Variante werden rekursiv alle Unterobjekte ebenfalls geklont. Die Bibliotheksimplementierung hinter Object kann das nicht.

Keine Klone bitte!

Wenn wir weder flach noch tief kopieren wollen, aber zum Beispiel aus der Oberklasse clone() erben, können wir mit einer CloneNotSupportedException anzeigen, dass wir nicht geklont werden wollen.


Hinweis Erben wir von einer Klasse mit implementierter clone()-Methode, die ihrerseits mit super.clone() arbeitet, bekommen wir von oben gleich auch die eigenen Zustände kopiert.



Rheinwerk Computing - Zum Seitenanfang

9.3.5 Hashcodes über »hashCode()« liefern *  Zur nächsten ÜberschriftZur vorigen Überschrift

Die Methode hashCode() soll zu jedem Objekt eine möglichst eindeutige Integerzahl (sowohl positiv als auch negativ) liefern, die das Objekt identifiziert. Die Ganzzahl heißt Hashcode beziehungsweise Hash-Wert, und hashCode() ist die Implementierung einer Hash-Funktion. Nötig sind Hashcodes, wenn die Objekte in speziellen Datenstrukturen untergebracht werden, die nach dem Hashing-Verfahren arbeiten. Datenstrukturen mit Hashing-Algorithmen bieten einen effizienten Zugriff auf ihre Elemente. Die Klasse java.util.HashMap implementiert eine solche Datenstruktur.


class java.lang.Object

  • int hashCode() Liefert den Hash-Wert eines Objekts. Die Basisklasse Object implementiert die Methode nativ.

Spieler mit Hash-Funktion

Im folgenden Beispiel soll die Klasse Player die Methode hashCode() aus Object überschreiben. Um die Objekte erfolgreich in einem Assoziativspeicher abzulegen, ist gleichfalls equals() nötig, was die Klasse Player ebenfalls implementiert:

Listing 9.15  com/tutego/insel/object/hashcode/Player.java

package com.tutego.insel.object.hashcode;

public class Player
{
  String name;
  int    age;
  double weight;

  /**
   * Returns a hash code value for this {@code Player} object.
   *
   * @return A hash code value for this object.
   *
   * @see java.lang.Object#equals(java.lang.Object)
   * @see java.util.HashMap
   */
  @Override public int hashCode()
  {
    int result = 31 + age;
    result = 31 * result + ((name == null) ? 0 : name.hashCode());
    long temp = Double.doubleToLongBits( weight );
    result = 31 * result + (int) (temp ^ (temp >>> 32));

    return result;
  }

  /**
   * Determines whether or not two players are equal. Two instances of
   * {@code Player} are equal if the values of their {@code name}, {@code age}
   * and {@code weight} member fields are the same.
   *
   * @param that an object to be compared with this {@code Player}
   *
   * @return {@code true} if the object to be compared is an instance of
   *         {@code Player} and has the same values; {@code false} otherwise.
   */
  @Override public boolean equals( Object that )
  {
    if ( this == that )
      return true;

    if ( that == null )
      return false;

    if ( getClass() != that.getClass() )
      return false;

    if ( age != ((Player)that).age )
      return false;

    if ( name == null )
      if ( ((Player)that).name != null )
        return false;
    else if ( !name.equals( ((Player)that).name ) )
      return false;

    return !( Double.doubleToLongBits( weight ) != Umbruch
              Double.doubleToLongBits( ((Player)that).weight ) );
  }
}

Testen können wir die Klasse etwa mit den folgenden Zeilen:

Listing 9.16  com/tutego/insel/object/hashcode/PlayerHashcodeDemo.java, main()

Player bruceWants = new Player();
bruceWants.name = "Bruce Wants";
bruceWants.age = 32;
bruceWants.weight = 70.3;

Player bruceLii = new Player();
bruceLii.name = "Bruce Lii";
bruceLii.age = 32;
bruceLii.weight = 70.3;;

System.out.println( bruceWants.hashCode() );                 // –340931147
System.out.println( bruceLii.hashCode() );                   // 301931244
System.out.println( System.identityHashCode( bruceWants ) ); // 1671711
System.out.println( System.identityHashCode( bruceLii ) );   // 11394033
System.out.println( bruceLii.equals( bruceWants ) );         // false

bruceWants.name = "Bruce Lii";
System.out.println( bruceWants.hashCode() );                 // 301931244
System.out.println( bruceLii.equals( bruceWants ) );         // true

Die statische Methode System.identityHashCode() liefert für ein Objekt den Hashcode, wie es die Standard-Implementierung von Object liefern würde, wenn wir sie nicht überschrieben hätten.


Hinweis Da der Hashcode negativ sein kann, muss Obacht gegeben werden bei Ausdrücken wie array[o.hashCode() % array.length()]. Ist o.hashCode() negativ, ist auch das Ergebnis vom Restwert negativ, und die Folge ist eine ArrayIndexOutOfBoundsException.


Eclipse
Eclipse kann die Methoden hashCode() und equals() automatisch generieren, wenn wir im Kontextmenü unter SourceGenerate Hashcode and equals() auswählen.

Tiefe oder flache Vergleiche/Hash-Werte

Referenziert ein Objekt Unterobjekte (etwa eine Person ein String-Objekt für den Namen – keine Primitiven –, so geben die Methoden equals() und hashCode()den Vergleich beziehungsweise die Berechnung des Hashcodes an das referenzierte Unterobjekt weiter (wenn es denn nicht null ist). Ablesen können wir das an folgendem Ausschnitt unserer equals()-Methode:

Listing 9.17  com/tutego/insel/object/hashcode/Player.java, equals() Ausschnitt

if ( name == null )
  if ( ((Player)that).name != null )
    return false;
else if ( !name.equals( ((Player)that).name ) )
  return false;

Es ist demnach die Aufgabe der String-Klasse (name ist vom Typ String), den Gleichheitstest vorzunehmen. Das heißt, dass zwei Personen problemlos equals()-gleich sein können, auch wenn sie zwei nicht-identische, aber equals()-gleiche String-Objekte referenzieren.

Auch bei hashCode() ist diese Delegation an das referenzierte Unterobjekt abzulesen:

Listing 9.18  com/tutego/insel/object/hashcode/Player.java, hashCode() Ausschnitt

result = 31 * result + ((name == null) ? 0 : name.hashCode());

Dass eine equals()-Methode beziehungsweise hashCode() einer Klasse den Vergleich beziehungsweise die Hashcode-Berechnung nicht an die Unterobjekte delegiert, sondern selbst umsetzt, ist unüblich.

»equals()«- und »hashCode()«-Berechnung bei (mehrdimensionalen) Arrays

Einen gewissen Sonderfall bei equals()/hashCode() nehmen mehrdimensionale Arrays ein. Mehrdimensionale Arrays sind nichts anderes als Arrays von Arrays. Das erste Array für die erste Dimension referenziert jeweils auf Unterarrays für die zweite Dimension. Wichtig wird diese Realisierung bei der Frage, wie diese Verweise der ersten Dimension nun bei equals() betrachtet werden sollen. Denn hier stellt sich die Frage, ob die Unterarrays von zwei zu testenden Arrays nur identisch oder auch gleich sein dürfen. Diese Frage hatten wir schon im Abschnitt »Felder vergleichen mit Arrays.equals() und Arrays.deepEquals()« in Abschnitt 3.7.15 angesprochen.

Enthält unsere Klasse ein Array und soll es in einem equals() mit berücksichtigt werden, so sind prinzipiell drei Varianten zum Umgang mit diesem Array möglich. Felder selbst einfach mit == wie primitive Werte zu vergleichen ist keine gute Lösung, da Arrays Objekte sind, die wie Strings nicht einfach mit == zu vergleichen sind. Während allerdings Objekte ein equals() haben, bieten Arrays keine eigene equals()-Methode, sondern diese ist in die Utility-Klasse Arrays gewandert. Hier gibt es jedoch zwei Methoden, die in Frage kämen. Arrays.equals(Object[] a, Object[] a2) geht jedes Element von a, also bei mehrdimensionalen Arrays jede Referenz auf ein Unterarray durch, und testet, ob es identisch mit einem zweiten Feld a2 ist. Wenn also zwei gleiche, aber nicht-identische Hauptarrays identische Unterarrays besitzen, liefert Arrays.equals() die Rückgabe true, aber nicht, wenn die Unterarrays zwar gleich, aber nicht identisch sind. Spielt das eine Rolle, so ist Arrays.deep-Equals() die passende Methode, denn sie fragt immer mit equals() die Unterarrays ab.

Bei der Berechnung des Hash-Werts gibt es eine vergleichbare Frage. Die Arrays-Klasse bietet zur Berechnung des Hash-Werts eines ganzen Arrays die Methoden Arrays.hashCode() und Arrays.deepHashCode(). Die erste Methode fragt jedes Unterelement über die von Object angebotene Methode hashCode() nach dem Hash-Wert. Nehmen wir ein mehrdimensionales Array an. Dann ist das Unterelement ebenfalls ein Feld. Arrays.hashCode() wird dann wie erwähnt nur die hashCode()-Methode auf dem Feld-Objekt aufrufen, während Arrays.deep-HashCode() auch in das Unterarray hinabsteigt und so lange Arrays.deepHashCode() auf allen Unterfeldern aufruft, bis ein equals()-Vergleich auf einem Nicht-Feld möglich ist.

Was heißt das nun für unsere equals()-/hashCode()-Methode? Üblich ist der Einsatz von Arrays.equals() und nicht von Arrays.deepEquals(), genauso wie Arrays.hashCode() üblicher als Arrays.deepHashCode() ist.

Das folgende Beispiel zeigt das in der Anwendung. Die Methoden wurden von Eclipse generiert und etwas kompakter geschrieben:

Listing 9.19  com/tutego/insel/object/hashcode/Chess.java, Chess

char[][] chessboard = new char[8][8];

@Override public int hashCode()
{
  return 31 + Arrays.hashCode( chessboard );
}

@Override public boolean equals( Object obj )
{
  if ( this == obj )
    return true;
  if ( obj == null )
    return false;
  if ( getClass() != obj.getClass() )
    return false;
  if ( ! Arrays.equals( chessboard, ((Chess) obj).chessboard ) )
    return false;
  return true;
}

Fließkommazahlen im Hashcode

Abhängig von den Datentypen sehen die Berechnungen immer etwas unterschiedlich aus. Während Ganzzahlen direkt in einen Ganzzahlausdruck für den Hashcode eingebracht werden können, sind im Fall von double die statischen Konvertierungsmethoden Double.doubleToLongBits() beziehungsweise Float.floatToIntBits() im Einsatz.

Die Datentypen double und float haben eine weitere Spezialität, da NaN und das Vorzeichen der 0 zu beachten sind, wie Kapitel 12, »Bits und Bytes und Mathematisches«, näher ausführt. Fazit: Sind x = +0.0 und y = –0.0, gilt x == y, aber Double.doubleToLongBits(x) != Double.doubleToLongBits(y). Sind x = y = Double.NaN, gilt x != y, aber Double.doubleToLongBits(x) == Double.doubleToLongBits(y). Wollen wir die beiden Nullen nicht unterschiedlich behandeln, sondern als gleich werten, ist Folgendes ein übliches Idiom:

x == 0.0 ? 0L : Double.doubleToLongBits( x )

Double.doubleToLongBits(0.0) liefert die Rückgabe 0, aber der Aufruf Double.doubleToLongBits(-0.0) gibt –9223372036854775808 zurück.

Equals, die Null und das Hashen

Inhaltlich gleiche Objekte (gemäß der Methode equals()) müssen denselben Wert bekommen.

Die beiden Methoden hashCode() und equals() hängen miteinander zusammen, sodass in der Regel bei der Implementierung einer Methode auch eine Implementierung der anderen notwendig wird. Denn es gilt, dass bei Gleichheit natürlich auch die Hash-Werte übereinstimmen müssen. Formal gesehen heißt das:

x.equals( y ) =>   x.hashCode() == y.hashCode()

So berechnet sich der Hashcode bei Point-Objekten aus den Koordinaten. Zwei Punkt-Objekte, die inhaltlich gleich sind, haben die gleichen Koordinaten und damit auch den gleichen Hashcode. Wenn Objekte den gleichen Hashcode aufweisen, aber nicht gleich sind, handelt es sich um eine Kollision und den Fall, dass in der Gleichung nicht die Äquivalenz gilt.


Rheinwerk Computing - Zum Seitenanfang

9.3.6 Aufräumen mit »finalize()« *  Zur nächsten ÜberschriftZur vorigen Überschrift

Einen Destruktor, der wie in C++ am Ende eines Gültigkeitsbereichs einer Variable aufgerufen wird, gibt es in Java nicht. Wohl ist es möglich, eine Methode finalize() für Aufräumarbeiten zu überschreiben, die Finalizer genannt wird (ein Finalizer hat nichts mit dem finally-Block einer Exception-Behandlung zu tun). Der Garbage-Collector ruft die Methode immer dann auf, wenn er ein Objekt entfernen möchte. Es kann allerdings sein, dass finalize() überhaupt nicht aufgerufen wird, und zwar dann, wenn die virtuelle Maschine Fantastillionen Megabyte an Speicher hat und dann beendet wird – in dem Fall gibt sie den Heap-Speicher als Ganzes dem Betriebssystem zurück. Ohne Garbage-Collector (GC) als Grabträger gibt es auch kein finalize()! Und wann der Garbage-Collector in Aktion tritt, ist auch nicht vorhersehbar, sodass im Gegensatz zu C++ in Java keine Aussage über den Zeitpunkt möglich ist, zu dem das Laufzeitsystem finalize() aufruft. Es ist von der Implementierung des GC abhängig. Üblicherweise werden aber Objekte mit finalize() von einem extra GC behandelt, und der arbeitet langsamer als der normale GC.


class java.lang.Object

  • protected void finalize() throws Throwable Wird vom GC aufgerufen, wenn es auf dieses Objekt keinen Verweis mehr gibt. Die Methode ist geschützt, weil sie von uns nicht aufgerufen wird. Auch wenn wir die Methode überschreiben, sollten wir die Sichtbarkeit nicht erhöhen, also public setzen.

Hinweis Klassen sollten finalize() überschreiben, um wichtige Ressourcen zur Not freizugeben, etwa File-Handles via close() oder Grafik-Kontexte des Betriebssystems, wenn der Entwickler das vergessen hat. Alle diese Freigaben müssten eigentlich vom Entwickler angestoßen werden, und finalize() ist nur ein Helfer, der rettend eingreifen kann. Da der GC finalize() nur dann aufruft, wenn er tote Objekte freigeben möchte, dürfen wir uns nicht auf die Ausführung verlassen. Gehen zum Beispiel die File-Handles aus, wird der GC nicht aktiv; es erfolgen keine finalize()-Aufrufe, und nicht mehr erreichbare, aber noch nicht weggeräumte Objekte belegen weiter die knappen File-Handles.


Einmal Finalizer, vielleicht mehrmals der GC

Objekte von Klassen, die eine finalize()-Methode besitzen, kann Oracles JVM nicht so schnell erzeugen und entfernen wie Klassen ohne finalize(). Das liegt auch daran, dass der GC vielleicht mehrmals laufen muss, um das Objekt zu löschen. Es gilt zwar, dass der GC aus dem Grund finalize() aufruft, weil das Objekt nicht mehr benötigt wird, es kann aber sein, dass aus der finalize()-Methode die this-Referenz nach außen gegeben wurde, sodass das Objekt wegen einer bestehenden Referenz nicht gelöscht werden kann. Das Objekt wird zwar irgendwann entfernt, aber der Finalizer läuft nur einmal und nicht immer pro GC-Versuch. Einige Hintergründe erfährt der Leser unter http://www.iecc.com/gclist/GC-lang.html#Finalization.

Löst eine Anweisung in finalize() eine Ausnahme aus, so wird diese ignoriert. Das bedeutet aber, dass die Finalisierung des Objekts stehen bleibt. Den GC beeinflusst das in seiner Arbeit aber nicht.

super.finalize()

Überschreiben wir in einer Unterklasse finalize(), dann müssen wir auch gewährleisten, dass die Methode finalize() der Oberklasse aufgerufen wird. So besitzt zum Beispiel die Klasse Font ein finalize(), das durch eine eigene Implementierung nicht verschwinden darf. Wir müssen daher in unserer Implementierung super.finalize() aufrufen (es wäre gut, wenn der Compiler das wie beim Konstruktoraufruf immer automatisch machen würde …). Leere finalize()-Methoden ergeben im Allgemeinen keinen Sinn, es sei denn, das finalize() der Oberklasse soll explizit übergangen werden:

Listing 9.20  com/tutego/insel/object/finalize/SuperFont.java, finalize()

@Override protected void finalize() throws Throwable
{
  try {
    // ...
  }
  finally {
    super.finalize();
  }
}

Der Block vom finally wird immer ausgeführt, auch wenn es im oberen Teil eine Ausnahme gab.

Die Methode von Hand aufzurufen, ist ebenfalls keine gute Idee, denn das kann zu Problemen führen, wenn der GC-Thread die Methode auch gerade aufruft. Um das Aufrufen von außen einzuschränken, sollte die Sichtbarkeit von protected bleiben und nicht erhöht werden.


Hinweis Da beim Programmende vielleicht nicht alle finalize()-Methoden abgearbeitet wurden, haben die Entwickler schon früh einen Methodenaufruf System.runFinalizersOn-Exit(true); vorgesehen. Mittlerweile ist die Methode veraltet und sollte auf keinen Fall aufgerufen werden. Die API-Dokumentation erklärt:

»It may result in finalizers being called on live objects while other threads are concurrently manipulating those objects, resulting in erratic behavior or deadlock.«

Dazu auch Joshua Bloch, Autor des ausgezeichneten Buchs »Effective Java Programming Language Guide«:

»Never call System.runFinalizersOnExit or Runtime.runFinalizersOnExit for any reason: they are among the most dangerous methods in the Java libraries.«



Rheinwerk Computing - Zum Seitenanfang

9.3.7 Synchronisation *  topZur vorigen Überschrift

Threads können miteinander kommunizieren und dabei Daten teilen. Sie können außerdem auf das Eintreten bestimmter Bedingungen warten, zum Beispiel auf neue Eingabedaten. Die Klasse Object deklariert insgesamt fünf Versionen der Methoden wait(), notify() und notifyAll() zur Beendigungssynchronisation von Threads. Kapitel 14, »Threads und nebenläufige Programmierung«, geht näher auf die Programmierung von Threads ein.



Ihr Kommentar

Wie hat Ihnen das <openbook> gefallen? Wir freuen uns immer über Ihre freundlichen und kritischen Rückmeldungen. >> Zum Feedback-Formular
 <<   zurück
 Ihre Meinung?
Wie hat Ihnen das <openbook> gefallen?
Ihre Meinung

 Buchempfehlungen
Zum Katalog: Java ist auch eine Insel






 Java ist auch
 eine Insel


Zum Katalog: Java SE Bibliotheken






 Java SE Bibliotheken


Zum Katalog: Professionell entwickeln mit Java EE 7






 Professionell
 entwickeln mit
 Java EE 7


Zum Katalog: Einstieg in Eclipse






 Einstieg in
 Eclipse


Zum Katalog: Einstieg in Java






 Einstieg in
 Java


 Shopping
Versandkostenfrei bestellen in Deutschland und Österreich
InfoInfo




Copyright © Rheinwerk Verlag GmbH 2011
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.


Nutzungsbestimmungen | Datenschutz | Impressum

Rheinwerk Verlag GmbH, Rheinwerkallee 4, 53227 Bonn, Tel.: 0228.42150.0, Fax 0228.42150.77, service@rheinwerk-verlag.de

Cookie-Einstellungen ändern