Galileo Computing < openbook >
Galileo Computing - Programming the Net
Galileo Computing - Programming the Net


Java ist auch eine Insel (2. Aufl.) von Christian Ullenboom
Programmieren für die Java 2-Plattform in der Version 1.4
Java ist auch eine Insel (2. Auflage)
gp Kapitel 6 Eigene Klassen schreiben
  gp 6.1 Eigene Klassen definieren
  gp 6.2 Assoziationen zwischen Objekten
  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.3.4 Zusammenfassung zur Sichtbarkeit
  gp 6.4 Statische Methoden und Variablen
    gp 6.4.1 Warum statische Eigenschaften sinnvoll sind
    gp 6.4.2 Statische Eigenschaften mit static
    gp 6.4.3 Statische Eigenschaften als Objekteigenschaften nutzen
    gp 6.4.4 Statische Eigenschaften und Objekteigenschaften
    gp 6.4.5 Statische Variablen zum Datenaustausch
    gp 6.4.6 Warum die Groß- und Kleinschreibung wichtig ist
    gp 6.4.7 Konstanten mit dem Schlüsselwort final bei Variablen
    gp 6.4.8 Typsicherere Konstanten
    gp 6.4.9 Statische Blöcke
  gp 6.5 Objekte anlegen und zerstören
    gp 6.5.1 Konstruktoren schreiben
    gp 6.5.2 Einen anderen Konstruktor der gleichen Klasse aufrufen
    gp 6.5.3 Initialisierung der Objekt- und Klassenvariablen
    gp 6.5.4 Finale Werte im Konstruktor setzen
    gp 6.5.5 Exemplarinitialisierer (Instanzinitialisierer)
    gp 6.5.6 Zerstörung eines Objekts durch den Müllaufsammler
    gp 6.5.7 Implizit erzeugte Stringobjekte
    gp 6.5.8 Zusammenfassung: Konstruktoren und Methoden
  gp 6.6 Veraltete (deprecated) Methoden/Konstruktoren
  gp 6.7 Vererbung
    gp 6.7.1 Vererbung in Java
    gp 6.7.2 Einfach- und Mehrfachvererbung
    gp 6.7.3 Kleidungsstücke modelliert
    gp 6.7.4 Sichtbarkeit
    gp 6.7.5 Das Substitutionsprinzip
    gp 6.7.6 Automatische und Explizite Typanpassung
    gp 6.7.7 Finale Klassen
    gp 6.7.8 Unterklassen prüfen mit dem Operator instanceof
  gp 6.8 Methoden überschreiben
    gp 6.8.1 super: Aufrufen einer Methode aus der Oberklasse
    gp 6.8.2 Nicht überschreibbare Funktionen
    gp 6.8.3 Fehlende kovariante Rückgabewerte
  gp 6.9 Die oberste aller Klassen: Object
    gp 6.9.1 Klassenobjekte
    gp 6.9.2 Hashcodes
    gp 6.9.3 Objektidentifikation mit toString()
    gp 6.9.4 Objektgleichheit mit equals() und Identität
    gp 6.9.5 Klonen eines Objekts mit clone()
    gp 6.9.6 Aufräumen mit finalize()
    gp 6.9.7 Synchronisation
  gp 6.10 Die Oberklasse gibt Funktionalität vor
    gp 6.10.1 Dynamisches Binden als Beispiel für Polymorphie
    gp 6.10.2 Keine Polymorphie bei privaten, statischen und finalen Methoden
    gp 6.10.3 Konstruktoren in der Vererbung
  gp 6.11 Abstrakte Klassen
    gp 6.11.1 Abstrakte Klassen
    gp 6.11.2 Abstrakte Methoden
    gp 6.11.3 Über abstract final
  gp 6.12 Schnittstellen
    gp 6.12.1 Die Mehrfachvererbung bei Schnittstellen
    gp 6.12.2 Erweitern von Interfaces – Subinterfaces
    gp 6.12.3 Vererbte Konstanten bei Schnittstellen
    gp 6.12.4 Vordefinierte Methoden einer Schnittstelle
    gp 6.12.5 CharSequence als Beispiel einer Schnittstelle
  gp 6.13 Innere Klassen
    gp 6.13.1 Geschachtelte Top-Level-Klassen und Schnittstellen
    gp 6.13.2 Mitglieds- oder Elementklassen
    gp 6.13.3 Lokale Klassen
    gp 6.13.4 Anonyme innere Klassen
    gp 6.13.5 Eine Sich-Selbst-Implementierung
    gp 6.13.6 this und Vererbung
    gp 6.13.7 Implementierung einer verketteten Liste
    gp 6.13.8 Funktionszeiger
  gp 6.14 Gegenseitige Abhängigkeiten von Klassen
  gp 6.15 Pakete


Galileo Computing

6.12 Schnittstellen  downtop

Eine Schnittstelle enthält keine Implementierungen, sondern nur Namen und Signaturen der enthaltenen Methoden. Anders gesagt, sie sind vollständig abstrakte Klassen. Obwohl keine Funktionen ausprogrammiert werden und keine Objektvariablen deklariert werden dürfen, sind static final-Variablen (benannte Konstanten) in einer Schnittstelle erlaubt. Möchte eine Klasse eine Schnittstelle verwenden, so folgt hinter dem Klassennamen das Schlüsselwort implements. Eine Klasse kann mehrere Schnittstellen implementieren.

Abbildung

Obwohl Schnittstellen auf den ersten Blick nichts bringen – Programmierer wollen gerne etwas vererbt bekommen, damit sie Implementierungsarbeit sparen können – sind sie eine enorm wichtige Erfindung. Denn über Schnittstellen lässt sich eine Sicht auf ein Objekt beschreiben. Jede Schnittstelle definiert solch eine Sicht, eine Art Rolle. Implementiert eine Klasse diverse Schnittstellen, so können ihre Exemplare in verschiedenen Rollen auftreten. Hier wird erneut das Substitutionsprinzip wichtig, bei dem ein mächtigeres Objekt verwendet wird, auch wenn weniger erwartet wird.

Hinweis   Bei einer Implementierung einer Schnittstelle müssen die Methoden in den Unterklassen öffentlich implementiert werden, da die Methoden in Schnittstellen immer automatisch public sind.

Beispiel   Die Schnittstelle Material ist mit drei Konstanten gefüllt. Daneben gibt es eine nicht ausprogrammierte Funktion berechneGewicht(). Diese wird nicht als abstract gekennzeichnet, da alle Methoden in Schnittstellen automatisch abstrakt sind. Der Compiler akzeptiert natürlich redundante Angaben von abstract und public.

Listing 6.31   MaterialienDemo.java, Teil 1

interface Material
{ int HART = 0, WEICH = HART+1, FLAUSCHIG = WEICH+1; int berechneGewicht(); } class Wolle implements Material
{ public int berechneGewicht() { return 234; } } class Synthetik implements Material
{ public int berechneGewicht() { return 452; } } abstract class Stahl implements Material
{ boolean glänzend; }

Die Klassen Wolle und Synthetik implementieren Material und müssen daher nicht zwingend abstrakt sein, da es eine Implementierung der abstrakten Methode berechneGewicht() aus der Schnittstelle Material gibt. Wolle und Synthetik überschreiben die Methode und definieren die Funktion berechneGewicht() beide anders. Dies ist einleuchtend, denn alle Materialien haben ein unterschiedliches Gewicht. Eine Ausnahme bildet die Klasse Stahl, die die Methode nicht überschreibt. Sie erbt eine abstrakte Methode, ohne sie zu implementieren – deshalb muss Stahl auch abstrakt sein.

Hinweis zur Ausdrucksweise: Klassen werden vererbt und Schnittstellen implementiert.

Jetzt ist es kein Problem, ein konkretes Wolle- oder Synthetik-Objekt zu konstruieren. Da in der Vererbungskette etwa gilt, Wolle ist vom Typ Material (und auch vom Typ Object, wie alles), lässt sich schreiben:

Listing 6.32   MaterialienDemo.java, Teil 2

public class MaterialienDemo
{
 Material schaf = new Wolle();
 Material satinPolyester = new Synthetik();
}

Ein Polymorphie-Beispiel mit Schnittstellen

An dieser Stelle sei noch einmal an die Möglichkeit erinnert, Funktionen auf Objekten auszuführen, die eine gemeinsame Basis haben.

Beispiel   Anwendung der gemeinsamen Methodenbasis.

Unterschiedliche Klassen, die die Schnittstelle Atom implementieren, definieren unterschiedliche Protonenwerte. So verstehen alle Klassen die Methode protonen(), die die Schnittstelle vorschreibt. Die UML-Abbildung soll das verdeutlichen.

Abbildung

Listing 6.33   AtomInterfaceDemo.java, Teil 1

interface Atom
{
  int protonen();
}

class Wasserstoff implements Atom {
  public int protonen() { return 1; }
}

class Helium implements Atom {
  public int protonen() { return 2; }
}

class Lithium implements Atom {
  public int protonen() { return 3; }
}

class Sauerstoff implements Atom {
  public int protonen() { return 8; }
}

Im Beispiel implementieren verschiedene Stoffe das Interface Atom. Jeder Stoff informiert über die Methode protonen() über seine Anzahl von Protonen.

Das Testprogramm verbindet ein Kürzel mit einem Objekt. Als Typ reicht lediglich Atom aus. Die Methode protonen() landet dann dynamisch gebunden beim richtigen Objekt.

Listing 6.34   AtomInterfaceDemo.java, Teil 2

public class AtomInterfaceDemo
{
  public static void main( String[] args )
  {
    char atomKürzel = 'h'; // setze Kürzel mit irgendeinem Wert

    Atom atom = kürzelZuAtom( atomKürzel );

    System.out.println( atom.protonen() );
  }

  static Atom kürzelZuAtom( char kürzel )
  {
    switch ( kürzel )
    {
      case 'h': return new Helium();
      case 'o': return new Sauerstoff();
      case 'l': return new Lithium();
    }
    return new Wasserstoff();
  }
}

Im Zusammenhang mit Schnittstellen bleibt zusammenfassend zu sagen, dass hier dynamisches Binden pur auftaucht.


Galileo Computing

6.12.1 Die Mehrfachvererbung bei Schnittstellen  downtop

Bei Klassen gibt es die Einschränkung, dass nur von einer direkten Oberklasse abgeleitet werden darf. Wird hingegen eine Schnittstelle implementiert, dann werden nicht mehr aus verschiedenen Quellen unterschiedliche Implementierungen für dieselbe Methode angeboten, was zu Problemen führen kann. Der Unterschied zu Klassen ist jetzt, dass mehrere Schnittstellen von einer einzigen Klasse implementiert werden können. Dies wird gelegentlich als »Mehrfachvererbung in Java« bezeichnet.

Beispiel   Sollte Wasserstoff nicht nur die Methoden von Atom implementieren, sondern auch von java.lang.Comparable, damit ein Atom mit einem anderen verglichen werden kann, schreiben wir:
class Wasserstoff implements Atom, Comparable
{
  public int protonen() { return 1; }        // Aus Atom

  public int compareTo( Object o ) {/*...*/} // Aus Comparable
}

Durch diese Mehrfachvererbung bekommt Wasserstoff zwei Obertypen, sodass sich je nach Sichtweise schreiben lässt:

Object      w1 = new Wasserstoff();
Atom        w2 = new Wasserstoff();
Comparable  w3 = new Wasserstoff();
Wasserstoff w4 = new Wasserstoff();

Dabei ist für w2 die Methode compareTo() und für w3 die Methode protonen() nicht sichtbar. Für w4 sind dagegen alle Methoden sichtbar.


Galileo Computing

6.12.2 Erweitern von Interfaces – Subinterfaces  downtop

Ein Subinterface ist die Erweiterung eines anderen Interfaces. Diese Erweiterung erfolgt – wie bei der Vererbung – durch das Schlüsselwort extends.

Beispiel   Eine Schnittstelle erbt von einer anderen Schnittstelle.
interface SchönesAtom extends Atom
{ double atomgewicht(); }

Eine Klasse, die nun SchönesAtom implementiert, muss die Methoden von beiden Schnittstellen implementieren, demnach die Methode atomgewicht() aus SchönesAtom sowie die Methode protonen(), die in Atom angegeben wurde.


Galileo Computing

6.12.3 Vererbte Konstanten bei Schnittstellen  downtop

Schnittstellen können Variablen besitzen, die jedoch, wie wir gesehen haben, immer automatisch statisch und final, also Konstanten, sind. Diese Konstanten können einer anderen Schnittstelle vererbt werden. Es gibt dabei einige kleine Einschränkungen.

Wir wollen an einem Beispiel sehen, wie sich die Vererbung auswirkt, wenn gleiche Bezeichner in den Unter-Schnittstellen erneut verwendet werden. Das nachfolgende Programm implementiert folgendes UML-Diagramm:

Abbildung

Listing 6.35   VererbteSchnittstellen.java

interface Grundfarben
{
  int ROT     = 1;
  int GRÜN    = 2;
  int BLAU    = 3;
}

interface Sockenfarben extends Grundfarben
{
  int SCHWARZ = 10;
  int LILA    = 11;
}
interface Hosenfarben extends Grundfarben
{
  int LILA    = 11;
  int SCHWARZ = 20;
  int BRAUN   = 21;
}

interface Allefarben extends Sockenfarben, Hosenfarben
{
  int BRAUN  = 30;
}

public class VererbteSchnittstellen
{
  public static void main( String args[] )
  {
    System.out.println( Sockenfarben.ROT );     // 1
    System.out.println( Allefarben.ROT );       // 1
    System.out.println( Hosenfarben.SCHWARZ );  // 20

//    System.out.println( Allefarben.SCHWARZ );

    // The field name "SCHWARZ" is an ambiguous name
    //found in the types "Sockenfarben" and "Hosenfarben".

 //    System.out.println( Allefarben.LILA );
  }
}

Die Definitionen der Schnittstellen können ohne Fehler übersetzt werden. Das Programm zeigt im Wesentlichen vier Eigenschaften:

1. Schnittstellen vererben ihre Eigenschaften an die Unter-Schnittstellen. Es erbt Sockenfarben das Attribut ROT aus Grundfarben.
2. Erbt eine Schnittstelle von mehreren Oberklassen, die jeweils ein bestimmtes Attribut von einer gemeinsamen Oberklasse beziehen, so ist dies kein Fehler. Es erbt etwa Allefarben von Sockenfarben und Hosenfarben das Attribut ROT aus Grundfarben.
3. Konstanten dürfen überschrieben werden. Es überschreibt Hosenfarben die Farbe SCHWARZ aus Grundfarben mit dem Wert 20. Auch LILA wird überschrieben. Obwohl die Konstante mit dem gleichen Wert belegt ist, handelt es sich um ein Überschreiben. Wird jetzt der Wert Hosenfarben.SCHWARZ verlangt, liefert die Umgebung den Wert 20.
4. Unter-Schnittstellen können aus zwei Ober-Schnittstellen die Attribute gleichen Namens übernehmen, auch wenn sie den gleichen Wert haben. Dann muss nur bei der Benutzung eine qualifizierter Name verwendet werden, der deutlich macht, welches Attribut gemeint ist, also zum Beispiel Sockenfarben.SCHWARZ. Das zeigt sich an den beiden Beispielen Allefarben.SCHWARZ und Allefarben.LILA. Die schwarze Farbe ist in den Ober-Schnittstellen Sockenfarben und Hosenfarben unterschiedlich initialisiert. Ähnliches gilt für die Farbe LILA. Obwohl LILA in beiden Fällen den Wert 11 trägt, ist das nicht erlaubt. Das ist ein guter Schutz gegen Fehler, denn wenn der Compiler dies durchlassen würde, könnte sich im Nachhinein die Belegung von LILA in Sockenfarben oder Hosenfarben ohne Neuübersetzung aller Klassen ändern und zu Schwierigkeiten führen. Diesen Fehler - die Ober-Schnittstellen haben für eine Konstante unterschiedliche Werte - müsste die Laufzeitumgebung erkennen. Zudem kann und sollte der Compiler für alle Konstanten die Werte direkt einsetzen.

Galileo Computing

6.12.4 Vordefinierte Methoden einer Schnittstelle  downtop

Der Typ eines Objekts bei der Deklaration einer Referenz kann entweder ein Objekt oder eine Schnittstelle sein. Ist die Referenz vom Typ einer Schnittstelle, dann ist es bemerkenswert, dass der Compiler erlaubt, Methoden der Klasse Object für diese Referenz aufzurufen.

Beispiel   Wir definieren eine Schnittstelle ohne besondere Methoden und eine Klasse, die keine besonderen Methoden hinzufügt.
interface I
{
}

class C implements I
{
}

Erzeugen wir ein Objekt vom Typ C, so kennt C automatisch alle Methoden aus C (keine) und zusätzlich, aufgrund des impliziten extends Object, auch die Methoden aus Object. Daher lässt sich schreiben:

C ref = new C();
ref.toString();

Die Schnittstelle spielt hier noch keine Rolle. Was jedoch auch funktioniert ist Folgendes:

I ref = new C();
ref.toString();

Es ist zu erwarten, dass ref nur Methoden aus I nutzen kann und das sind keine! Da allerdings jede Klasse automatisch von Object erbt und damit die Methoden besitzt, erlaubt der Compiler Zugriff auf diese Eigenschaften. So lässt sich vereinfacht sagen, dass alle Methoden von Object erlaubt sind, auch wenn ein Interface selbst diese Methoden nicht besitzt. Jede Schnittstelle ist somit eine indirekte Erweiterung von Object.

Schnittstellenmethoden, die nicht implementiert werden müssen

Bis auf eine Ausnahme muss eine Klasse, zu der Exemplare erzeugt werden sollen, alle Methoden der Schnittstellen implementieren. Eine Ausnahme ergibt sich wieder aus der Tatsache, dass jede Schnittstelle die Methoden von Object annimmt. Sehen wir uns den Programmcode der Schnittstelle Comparator an, die im Paket java.util definiert ist:

package java.util;

public interface Comparator
{
  int compare( Object o1, Object o2 );

  boolean equals( Object obj );
}

Wir entdecken, dass dort die equals()-Methode vorgeschrieben wird. Der erste Gedanke ist, nun eine Klasse zu schreiben, die compare() und equals() implementieren muss. Dies ist hier allerdings nicht nötig. Denn equals() ist schon eine Methode, die jedes Objekt besitzt. Daraus ergibt sich, dass nicht alle Methoden ausprogrammiert werden müssen. (Eventuell überschreiben wir equals(), wenn uns die Semantik von equals() in Object nicht gefällt.) Weiter lässt sich eine Schnittstelle angeben, die die Methoden von Object auflistet. Auch dann müsste keine Methode implementiert werden. Bleibt die Frage, warum denn Comparator eine equals()-Methode vorschreibt, wenn diese doch nicht implementiert zu werden braucht. Um uns zu verwirren? Nein. Der Sinn besteht einfach in der genauen Angabe der Funktionsweise in der Dokumentation. Eine Java-Dokumentation kann nur generiert werden, wenn auch eine Funktion im Quellcode vorhanden ist. Die Entwickler wollten bei equals() in der Schnittstelle Comparator noch einmal bewusst auf die Funktion hinweisen, dass equals() zwei Comparator-Objekte daraufhin vergleicht, ob beide die gleiche Sortierfolge verwenden, und nicht, wie wir annehmen könnten, zwei Objekte auf Gleichheit testet.


Galileo Computing

6.12.5 CharSequence als Beispiel einer Schnittstelle  toptop

Bisher kennen wir die Klassen String und StringBuffer, um Zeichenketten zu speichern und weiterzugeben. Ein String ist ein Wertobjekt und ein wichtiges Hilfsmittel in Programmen, da durch dieses unveränderliche Zeichenkettenwerte repräsentiert werden, während StringBuffer veränderliche Zeichenfolgen umfasst. Aber wie sieht es aus, wenn eine Teilzeichenkette gefordert ist, bei der es egal sein soll, ob das Original als String- oder StringBuffer-Objekt vorliegt?

Eine Lösung ist, alles in ein String-Objekt zu konvertieren. Möchte ein Programm eine Teilfolge liefern, auf die jemand lesend zugreifen möchte, die er aber nicht verändern können soll, ist ein String zu träge. Aus den beliebigen Zeichenfolgen müsste zuerst ein String-Objekt konstruiert werden. Daher haben die Entwickler seit dem SDK 1.4 die Schnittstelle CharSequence eingefügt, die eine unveränderliche, nur lesbare Sequenz von Zeichen realisiert. Die Schnittstelle wird von StringBuffer und String implementiert, sodass sich alle Zeichenketten dieser Klassen als CharSequence auszeichnen. Funktionen müssen sich also nicht mehr für String oder StringBuffer entscheiden, sondern können einfach ein CharSequence-Objekt als Parameter akzeptieren. Ein String und ein StringBuffer-Objekt können zwar mehr als CharSequence vorschreibt, aber beide lassen sich als CharSequence einsetzen, wenn das »Mehr« an Funktionalität nicht benötigt wird.

Abbildung

interface java.lang.CharSequence

gp  char charAt( int index )
Liefert das Zeichen an der Stelle index.
gp  int length()
Gibt die Länge der Zeichensequenz zurück.
gp  CharSequence subSequence( int start, int end )
Liefert eine neue CharSequence von start bis end.
gp  String toString()
Gibt einen String der Sequenz zurück. Die Länge des toString()-Strings entspricht genau der Länge der Sequenz.
Beispiel   Soll eine Methode eine Zeichenkette bekommen und die Herkunft ist egal, so schreiben wir etwa:
void giveMeAText( CharSequence s )
{
  ...
}

anstatt

void giveMeAText( String s )
{
  ...
}
void giveMeAText( StringBuffer s )
{
  void giveMeAText( new String(s) );  // oder Ähnliches
}

Anwendung von CharSequence in String

In den Klassen String und StringBuffer existiert eine Methode subSequence(), die ein CharSequence-Objekt liefert. Die Signatur ist in beiden Fällen die gleiche. Die Funktion macht im Prinzip nichts anderes als ein substring(begin, end).

class java.lang.String implements CharSequence, Serializable

class java.lang.StringBuffer implements CharSequence, Serializable

gp  CharSequence subSequence( int beginIndex, int endIndex )
Liefert eine neue Zeichensequenz von String beziehungsweise StringBuffer.

Die Implementierung sieht so aus, dass mit substring() ein neuer Teilstring zurückgeliefert wird. Das ist eine einfache Lösung, aber nicht unbedingt die schnellste. Für String-Objekte ist das Erzeugen von Substrings ziemlich schnell, da die Methode speziell optimiert ist. Da Strings unveränderlich sind, wird einfach das gleiche char-Feld wie im Original-String verwendet, nur eine Verschiebung und ein Längenwert werden angepasst.






1   Äquivalent zu denen in Objective-C.





Copyright © Galileo Press GmbH 2003
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