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

 << zurück
Java ist auch eine Insel von Christian Ullenboom
Programmieren für die Java 2-Plattform in der Version 5
Java ist auch eine Insel

Java ist auch eine Insel
5., akt. und erw. Auflage
1454 S., mit CD, 49,90 Euro
Galileo Computing
ISBN 3-89842-747-1
gp Kapitel 6 Eigene Klassen schreiben
  gp 6.1 Eigene Klassen definieren
    gp 6.1.1 Methodenaufrufe und Nebeneffekte
    gp 6.1.2 Argumentübergabe mit Referenzen
    gp 6.1.3 Die this-Referenz
    gp 6.1.4 Überdeckte Objektvariablen nutzen
  gp 6.2 Assoziationen zwischen Objekten
    gp 6.2.1 Gegenseitige Abhängigkeiten von Klassen
  gp 6.3 Privatsphäre und Sichtbarkeit
    gp 6.3.1 Wieso nicht freie Methoden und Variablen für alle?
    gp 6.3.2 Privat ist nicht ganz privat: Es kommt darauf an, wer’s sieht
    gp 6.3.3 Zugriffsmethoden für Attribute definieren
  gp 6.4 Statische Methoden und statische Attribute
    gp 6.4.1 Warum statische Eigenschaften sinnvoll sind
    gp 6.4.2 Statische Eigenschaften mit static
    gp 6.4.3 Statische Eigenschaften über Referenzen nutzen?
    gp 6.4.4 Warum die Groß- und Kleinschreibung wichtig ist
    gp 6.4.5 Statische Eigenschaften und Objekteigenschaften
    gp 6.4.6 Statische Variablen zum Datenaustausch
    gp 6.4.7 Statische Blöcke als Klasseninitialisierer
  gp 6.5 Konstanten und Aufzählungen
    gp 6.5.1 Konstanten über öffentliche statische final-Variablen
    gp 6.5.2 Problem mit finalen Klassenvariablen
    gp 6.5.3 Typsicherere Konstanten
    gp 6.5.4 Aufzählungen mit enum in Java 5
    gp 6.5.5 enum-Konstanten in switch
  gp 6.6 Objekte anlegen und zerstören
    gp 6.6.1 Konstruktoren schreiben
    gp 6.6.2 Einen anderen Konstruktor der gleichen Klasse aufrufen
    gp 6.6.3 Initialisierung der Objekt- und Klassenvariablen
    gp 6.6.4 Finale Werte im Konstruktor und in statischen Blöcken setzen
    gp 6.6.5 Exemplarinitialisierer (Instanzinitialisierer)
    gp 6.6.6 Zerstörung eines Objekts durch den Müllaufsammler
    gp 6.6.7 Implizit erzeugte String-Objekte
    gp 6.6.8 Private Konstruktoren, Utility-Klassen, Singleton und Fabriken
  gp 6.7 Vererbung
    gp 6.7.1 Vererbung in Java
    gp 6.7.2 Einfach- und Mehrfachvererbung
    gp 6.7.3 Gebäude modelliert
    gp 6.7.4 Konstruktoren in der Vererbung
    gp 6.7.5 Sichtbarkeit protected
    gp 6.7.6 Das Substitutionsprinzip
    gp 6.7.7 Automatische und explizite Typanpassung
    gp 6.7.8 Typen testen mit dem binären Operator instanceof
    gp 6.7.9 Array-Typen und Kovarianz
    gp 6.7.10 Methoden überschreiben
    gp 6.7.11 Mit super eine Methode der Oberklasse aufrufen
    gp 6.7.12 Kovariante Rückgabetypen
    gp 6.7.13 Finale Klassen
    gp 6.7.14 Nicht überschreibbare Funktionen
    gp 6.7.15 Zusammenfassung zur Sichtbarkeit
    gp 6.7.16 Sichtbarkeit in der UML
    gp 6.7.17 Zusammenfassung: Konstruktoren und Methoden
  gp 6.8 Object ist die Mutter aller Oberklassen
    gp 6.8.1 Klassenobjekte
    gp 6.8.2 Objektidentifikation mit toString()
    gp 6.8.3 Objektgleichheit mit equals() und Identität
    gp 6.8.4 Klonen eines Objekts mit clone()
    gp 6.8.5 Hashcodes
    gp 6.8.6 Aufräumen mit finalize()
    gp 6.8.7 Synchronisation
  gp 6.9 Die Oberklasse gibt Funktionalität vor
    gp 6.9.1 Dynamisches Binden als Beispiel für Polymorphie
    gp 6.9.2 Keine Polymorphie bei privaten, statischen und finalen Methoden
    gp 6.9.3 Polymorphie bei Konstruktoraufrufen
  gp 6.10 Abstrakte Klassen und abstrakte Methoden
    gp 6.10.1 Abstrakte Klassen
    gp 6.10.2 Abstrakte Methoden
  gp 6.11 Schnittstellen
    gp 6.11.1 Ein Polymorphie-Beispiel mit Schnittstellen
    gp 6.11.2 Die Mehrfachvererbung bei Schnittstellen
    gp 6.11.3 Erweitern von Interfaces – Subinterfaces
    gp 6.11.4 Vererbte Konstanten bei Schnittstellen
    gp 6.11.5 Vordefinierte Methoden einer Schnittstelle
    gp 6.11.6 Abstrakte Klassen und Schnittstellen im Vergleich
    gp 6.11.7 CharSequence als Beispiel einer Schnittstelle
    gp 6.11.8 Die Schnittstelle Iterable
  gp 6.12 Innere Klassen
    gp 6.12.1 Statische innere Klassen und Schnittstellen
    gp 6.12.2 Mitglieds- oder Elementklassen
    gp 6.12.3 Lokale Klassen
    gp 6.12.4 Anonyme innere Klassen
    gp 6.12.5 this und Vererbung
    gp 6.12.6 Implementierung einer verketteten Liste
    gp 6.12.7 Funktionszeiger
  gp 6.13 Generische Datentypen
    gp 6.13.1 Einfache Klassenschablonen
    gp 6.13.2 Einfache Methodenschablonen
    gp 6.13.3 Umsetzen der Generics, Typlöschung und Raw-Types
    gp 6.13.4 Einschränken der Typen
    gp 6.13.5 Generics und Vererbung, Invarianz
    gp 6.13.6 Wildcards
  gp 6.14 Die Spezial-Oberklasse Enum
    gp 6.14.1 Methoden auf Enum-Objekten
    gp 6.14.2 enum mit eigenen Konstruktoren und Methoden
  gp 6.15 Dokumentationskommentare mit javaDoc
    gp 6.15.1 Einen Dokumentationskommentar setzen
    gp 6.15.2 Mit javadoc eine Dokumentation erstellen
    gp 6.15.3 HTML-Tags in Dokumentationskommentaren
    gp 6.15.4 Generierte Dateien
    gp 6.15.5 Weitere Dokumentationskommentare
    gp 6.15.6 javaDoc und Doclets
    gp 6.15.7 Veraltete (deprecated) Klassen, Konstruktoren und Methoden


Galileo Computing

6.8 Object ist die Mutter aller Oberklassedowntop

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.

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


Galileo Computing

6.8.1 Klassenobjekte  downtop

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  

gp  final Class<? extends Object> getClass() Liefert die Referenz auf das Klassenobjekt, die das Objekt konstruiert hat.

Beispiel   Die Objektmethode der Klasse Class.getName() fragt nach dem Namen der Objektklasse.
System.out.println( "Klaviklack".getClass().getName() ); // java.lang.String

Klassen-Literale

Ein Klassen-Literal (eng. class literal) ist ein Ausdruck der Form Datentyp.class, wobei Datentyp entweder eine Klasse, eine Schnittstelle, ein Feld oder ein primitiver Typ ist. Der Ausdruck ist immer vom Typ Class. Beispiele sind Object.class oder int.class.


Galileo Computing

6.8.2 Objektidentifikation mit toString(downtop

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 definiert toString() so, dass die Koordinaten angegeben werden.
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 6.39   DiskoAusgabe.java

public class DiskoAusgabe
{
  int anzahlPersonen;
  int quadratmeter;
  @Override
  public String toString()
  {
    return getClass().getName() +
           "[anzahlPersonen=" + anzahlPersonen +
           ",quadratmeter=" + quadratmeter + "]";
  }
  public static void main( String[] args )
  {
    DiskoAusgabe d = new DiskoAusgabe();
    d.anzahlPersonen = 1223;
    d.quadratmeter   = 633;
    System.out.println( d.toString() );
    System.out.println( ":" + d + ":");   // toString() wird automatisch aufgerufen
  }
}

Die Ausgabe ist damit:

DiskoAusgabe[anzahlPersonen=1223,quadratmeter=633]
:DiskoAusgabe[anzahlPersonen=1223,quadratmeter=633]:

Bei einer eigenen Implementierung müssen wir darauf achten, dass die Sichtbarkeit public ist, da sich toString() in der Oberklasse public befindet und wir die Sichtbarkeit nicht einschränken können.

Standardimplementierung

Neue Klassen sollten toString() überschreiben. Wenn das nicht der Fall ist, 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  

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

Zwar sagt der Hash-Wert selbst wenig, allerdings ist er ein erstes Indiz dafür, dass bei Klassen, die keine toString()- und hashCode()-Methode ü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 ist ganz praktisch zum Debugging, doch das manuelle Tippen der Methoden ist lästig. Zwei Lösungen vereinfachen das Implementieren der Methode toString():

gp  Ein Eclipse-Plugin erzeugt selbstständig die Methode auf Grund ausgewählter Attribute. Zu nennen sind http://java-tutor.com/go/prysme/ und das Plugin http://codesugar.sourceforge.net/.
gp  Die Zustände werden automatisch über Reflection ausgelesen. Hier führt Apache Commons Lang auf den richtigen Weg.

Galileo Computing

6.8.3 Objektgleichheit mit equals() und Identität  downtop

Ob zwei Referenzen das gleiche Objekt repräsentieren, lässt sich durch den Vergleichsoperator == feststellen. Damit wird aber lediglich die Identität, nicht jedoch automatisch die inhaltliche Gleichheit getestet. So wird der Vergleich

if ( meinName == "Ulli" )

gewiss einen falschen, unbeabsichtigten Effekt haben, obwohl er syntaktisch korrekt ist. An dieser Stelle sollte der inhaltliche Vergleich stattfinden: Stimmen alle Zeichen der Zeichenkette überein?

Mit der Methode equals() aus Object lassen sich Objekte auf Gleichheit prüfen. Unterklassen überschreiben diese Methode, um einen inhaltlichen Vergleich vorzunehmen. Die Methode ist in jeder Klasse gut aufgehoben, denn nur ein Objekt weiß, wann es einem anderen gleich ist. So besitzt das String-Objekt eine Implementierung, die jedes Zeichen vergleicht:

String meinName = "Ulli";
if ( meinName.equals( "Ulli" ) )
  // gefunden

Leider implementiert nicht jede Klasse eine eigene equals()-Methode, sodass die Laufzeitumgebung unbeabsichtigt bei Object und seinem Referenzenvergleich landet.

Dies hat ungeahnte Folgen, denn dann wird einfach die Standardimplementierung aufgerufen. Diese kann über die Gleichheit von Objekten nichts wissen und testet lediglich die Referenzen.

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

Diese Fehleinschätzung kommt leider bei Exemplaren der Klasse StringBuffer/StringBuilder vor, die kein eigenes equals() implementiert. Wir haben die Diskussion darüber schon geführt.


class java.lang.  Object  

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

Ein eigenes equals()

Bei selbstdefinierten Methoden ist Vorsicht geboten, denn wir müssen genau auf die Signatur achten. Die Methode muss ein Object akzeptieren und boolean zurückgeben. Wird diese Signatur falsch verwendet, kommt es statt einer Überschreibung der Funktion zu einer Überladung und bei einer Rückgabe ungleich boolean zu einer zweiten Funktion mit gleicher Signatur, was Java nicht zulässt. (Wir erinnern uns: Der Rückgabetyp gehört nicht zur Signatur.) Um das Problem zu minimieren, sollte die Annotation @Override an equals() angeheftet sein.

Die equals()-Funktion stellt einige Anforderungen:

gp  Heißt der Vergleich equals(null), so ist das Ergebnis immer false.
gp  Kommt ein this rein, lässt sich eine Abkürzung nehmen und true zurückliefern.
gp  Das Argument ist zwar vom Typ Object, aber dennoch vergleichen wir immer konkrete Typen. Eine equals()-Funktion 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.

Die beiden ersten Punkte sind leicht erfüllbar, und ein Beispiel DiskoEquals ist schnell implementiert:

Listing 6.40   DiskoEquals.java, erster Versuch

public class DiskoEquals
{
  int anzahlPersonen;
  int quadratmeter;
  @Override
  public boolean equals( Object o )
  {
    if ( o == null )
      return false;
    if ( o == this )
      return true;
    DiskoEquals that = (DiskoEquals) o;
    return    this.anzahlPersonen == that.anzahlPersonen
           && this.quadratmeter   == that.quadratmeter;
  }
}

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

if ( ! o instanceof DiskoEquals )
  return false;

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

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 DiskoEquals ist. Diese Unterklasse wird wie die Oberklasse die gleichen Attribute haben, sodass – aus der Sicht von DiskoEquals – alles in Ordnung ist. Nehmen wir einmal die Variablen disko und superDisko an, die die Typen DiskoEquals und SuperDiskoEquals – die Unterklasse von DiskoEquals – besitzen. Sind beide Objekte gleich, so ergibt disko.equals(superDisko) das Ergebnis true. Drehen wir den Spieß um und fragen, was superDisko.equals(disko) ergibt. Zwar haben wir SuperDiskoEquals nicht implementiert, nehmen aber an, dass dort eine equals()-Funktion steckt, die nach dem gleichen instanceof-Schema implementiert wurde wie DiskoEquals. Dann wird dort bei einem Test ausgeführt: disko instanceof superDisko und das ist false. Damit wird aber die Fallunterscheidung mit return false beendet. Fassen wir zusammen:

disko.equals( superDisko ) == true
superDisko.equals( disko ) == 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 6.41   DiskoEquals.java, Korrektur

public class DiskoEquals
{
  int anzahlPersonen;
  int quadratmeter;
  @Override
  public boolean equals( Object o )
  {
    if ( o == null )
      return false;
    if ( o == this )
      return true;
      if ( ! o.getClass().equals(getClass()) )
      return false;
      DiskoEquals that = (DiskoEquals) o;
    return    this.anzahlPersonen == that.anzahlPersonen
           && this.quadratmeter   == that.quadratmeter;
  }
}

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() 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. Die Dokumentation schreibt: »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 bei den Urls http://java-tutor.com/ und http://javatutor.de/ aber letztendlich http://java-tutor.com/ steckt, liefert equals() die Rückgabe true.

Listing 6.42   UrlEquals.java, main()

URL url1 = new URL( "http://java-tutor.com/" );
URL url2 = new URL( "http://javatutor.de/" );
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:

gp  Der menschliche Leser erwartet intuitiv etwas anderes.
gp  Wenn keine Netzwerkverbindung besteht, wird keine Namensauflösung durchgeführt und der Vergleich liefert false, doch sollte er für immer gleich sein.
gp  Dass die beiden URLs auf den gleichen Server zeigen, kann sich später ändern.

Galileo Computing

6.8.4 Klonen eines Objekts mit clone(downtop

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

gp  Einen Konstruktor, der ein vorhandenes Objekt als Vorlage nimmt, ein neues Objekt anlegt und die Zustände kopiert oder
gp  eine clone()-Methode.

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


Beispiel   Erzeuge ein Punkt-Objekt und klone es.

Listing 6.43   ClonePoint.java, Ausschnitt

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]

Viele der Standard-Klassen unterstützen ein clone(), sodass ein neues Exemplar mit dem gleichen Zustand zurückgegeben wird. Für uns als Programmierer stellt sich die Frage, wie ein clone() unserer Klassen mit geringem Aufwand umgesetzt wird, denn clone() wird nicht automatisch unterstützt, sodass möglich ist:

Object o = new Object();
o.clone();                   // Das geht nicht, da clone() nicht sichtbar ist.

Es beginnt bei eigenen Klassen immer damit, dass die Funktion public Object clone() aus der Oberklasse java.lang.Object überschrieben wird. Für die Implementierung kommen zwei Möglichkeiten in Betracht:

gp  Von Hand ein neues Objekt anlegen, alle Attribute kopieren und die Referenz auf das neue Objekt zurückgeben.
gp  Das Laufzeitsystem soll selbst eine Kopie anlegen und dieses geben wir zurück.

Lösung zwei verkürzt die Entwicklungszeit und ist auch spannender.


class java.lang.  Object  

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

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

gp  Der Aufruf super.clone() stößt die Funktion 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.
gp  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.

clone() gibt eine Referenz auf das neue Objekt zurück, und wenn es keinen freien Speicher mehr gibt, folgt ein OutOfMemoryError. Da es seit Java 5 kovariante Rückgabetypen gibt, muss clone() nicht lediglich Object liefern, sondern kann auch einen Untertyp geben.


Beispiel   In einer Disko arbeitet eine Thekenbedienung. Wir wünschen uns, die Mitarbeiter leicht klonen zu können.

Listing 6.44   Bedienung.java

public class Bedienung implements Cloneable
{
  String name;
  int alter;
  @Override
  public Bedienung clone()
  {
    try
    {
      return (Bedienung) super.clone();
    }
    catch ( CloneNotSupportedException e ) {
      // this shouldn't happen, since we are Cloneable
      throw new InternalError();
    }
  }
}


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

Testen wir die Klasse etwa so:

Listing 6.45   BedienungCloneDemo.java, main()

Bedienung susi = new Bedienung();
susi.alter = 29;
susi.name  = "Susi";
Bedienung dolly = susi.clone();
System.out.println( dolly.name + " ist " + dolly.alter ); // 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 in Object stehen:

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

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

Flach oder tief?

clone() erzeugt standardmäßig nur flache Kopien. Bei untergeordneten Objekten werden nur die Referenzen kopiert und Originalobjekt sowie 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 sind mitunter tiefe Kopien gewünscht.

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


Galileo Computing

6.8.5 Hashcodes  downtop

Die Methode hashCode() soll zu jedem Objekt eine möglichst eindeutige Integerzahl (sowohl positiv als auch negativ) liefern, die das Objekt identifiziert. Inhaltlich gleiche Objekte (gemäß der Methode equals()) müssen denselben Wert bekommen. Eine spezielle Funktion berechnet diesen Wert, der Hashcode oder Hash-Wert genannt wird. Die Funktionen, die solche Werte berechnen, werden Hash-Funktionen genannt. Hashcodes werden verwendet, um Elemente in Hash-Tabellen zu speichern. Diese sind Datenstrukturen, die einen effizienten Zugriff auf ihre Elemente erlauben. Die Klassen java.util. HashMap oder java.util.Hashtable implementieren eine solche Datenstruktur.

Die Klasse, die Bauplan für Objekte ist, die in einem Assoziativspeicher abgelegt werden sollen, müssen die Methode hashCode() aus Object überschreiben.


class java.lang.  Object  

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

Listing 6.46   DiskoHashing.java

public class DiskoHashing
{
  int anzahlPersonen;
  int quadratmeter;
  /**
   * Liefert den Hashcode für das aktuelle <code>DiskoHashing</code>-Objekt.
   *
   * @return Hashcode.
   */
  @Override
  public int hashCode()
  {
    return anzahlPersonen ^ ( quadratmeter >> 16 );
  }
  public static void main( String[] args )
  {
    DiskoHashing d = new DiskoHashing();
    d.anzahlPersonen = 1223;
    d.quadratmeter   = 633;
    System.out.println( d.hashCode() );   // 1726
  }
}

Equals, die Null und Hashen

Die beiden Methoden hashCode() und equals() hängen zusammen, sodass in der Regel bei der Implementierung einer Funktion 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 haben, aber nicht gleich sind, handelt es sich um eine Kollision und den Fall, dass in der Gleichung nicht die Äquivalenz gilt.


Galileo Computing

6.8.6 Aufräumen mit finalize()  downtop

Einen Destruktor, so wie in C++, gibt es in Java nicht. Wohl ist es möglich, eine Funktion finalize() auszuprogrammieren, in der Aufräumarbeiten erledigt werden können. Sie wird vom Laufzeitsystem immer dann aufgerufen, wenn der GC ein Objekt entfernen möchte. An dieser Stelle ist sehr wichtig zu verstehen, dass finalize() nur und nur dann aufgerufen wird, wenn der GC das Objekt zu Grabe trägt.

Im Gegensatz zu C++ mit einer manuellen Freigabe ist in Java keine Aussage über den Zeitpunkt möglich, zu dem die Routine aufgerufen wird – dies ist von der Implementierung des GCs abhängig. Es kann sein, dass finalize() überhaupt nicht aufgerufen wird, und zwar dann, wenn die VM Fantastillionen Megabyte Speicher hat und dann beendet wird; sie wird den Heap-Speicher als Ganzes dem Betriebssystem zurückgeben.


class java.lang.  Object  

gp  protected void finalize() throws Throwable 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 (public).

Klassen sollten finalize() überschreiben, um wichtige Ressourcen zur Not freizugeben, etwa File-Handles oder Grafik-Kontexte des Betriebssystems, wenn der Entwickler das vergessen hat. Alle diese Freigaben müssen vom Entwickler angestoßen werden können, und finalize() ist nur ein Helfer, der rettend eingreifen kann. Da finalize() nur bei knappem Speicher aufgerufen wird und wenn tote Objekte freigegeben werden müssen, dürfen wir uns nicht auf die Ausführung verlassen. Gehen zum Beispiel nur die File-Handles aus, wird der GC nicht aktiv; es erfolgen keine finalize()-Aufrufe, und tote Objekte belegen weiter die knappen File-Handles.

super.finalize()

Überschreiben wir in einer Unterklasse finalize(), dann müssen wir auch gewährleisten, dass finalize() der Oberklasse aufgerufen wird. Das erreichen wir mit der Referenz super. (Es wäre gut, wenn der Compiler das automatisch machen würde …)


Beispiel   Wir bilden eine Unterklasse von Font, um unsere eigenen Zeichensätze zu verwalten. Die Klasse Font definiert eine finalize()-Methode, und unsere Klasse soll auch finalize() implementieren:
class MehrAlsFontKann extends Font
{
  MehrAlsFontKann ()  // Font hat keinen Standard-Konstruktor
  {
    super( null, PLAIN, 10 );
  }
  protected void   finalize()   throws Throwable
  {
    try
    {
      /*
         MehrAlsFontKann Dinge freigeben
         ...
       */
    }
    finally {
      super.finalize();
    }
  }
}
Der Block vom finally wird immer ausgeführt, auch wenn es im oberen Teil eine Ausnahme gab.


Galileo Computing

6.8.7 Synchronisation  toptop

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 definiert insgesamt fünf Versionen der Methoden wait(), notify() und notifyAll() zur Beendigungssynchronisation von Threads. Ein Sonderkapitel geht näher auf die Programmierung von Threads ein.




1  Die Mathematiker werden sich freuen, denn die Methode equals() bildet eine Äquivalenzrelation. Sie ist, wenn wir die null-Referenz außen vor lassen, reflexiv, symmetrisch und transitiv.

 << zurück




Copyright © Galileo Press GmbH 2005
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, Rheinwerkallee 4, 53227 Bonn, Tel.: 0228.42150.0, Fax 0228.42150.77, info@galileo-press.de