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

Eine Schnittstelle (interface) enthält keine Implementierungen, sondern nur Namen und Signaturen der enthaltenen Methoden.


Beispiel   Definiere die Schnittstelle Unterhaltend mit einer Funktion unterhaltungs-wert()

Listing 6.59   vd/Unterhaltend.java

package vd;
  interface   Unterhaltend
{
  int unterhaltungswert();
}

Die Definition einer Schnittstelle erinnert an eine abstrakte Klasse mit abstrakten Methoden, nur an Stelle von class steht das Schlüsselwort interface und bei den abstrakten Methoden muss der Modifizierer abstract nicht zwingend geschrieben werden. Da alle Methoden in Schnittstellen automatisch abstrakt und öffentlich sind akzeptiert der Compiler das redundante abstract und public, doch die Modifizierer sollten nicht geschrieben werden. Die von den Schnittstellen definierten Funktionen sind – wie auch bei abstrakten Methoden – mit einem Semikolon abgeschlossen und haben niemals eine Implementierung.


Hinweis   Der Name einer Schnittstelle endet oft mit –able und beginnt üblicherweise nicht mit einem Zeichen wie »I«, obwohl die Eclipse-Entwickler diese Namenskonvention nutzen.

Eine Schnittstelle darf keinen Konstruktor definieren. Das ist auch klar, da Exemplare von Schnittstellen nicht erzeugt werden können.

Obwohl in einer Schnittstelle keine Funktionen ausprogrammiert werden und keine Objektvariablen deklariert werden dürfen, sind static final-Variablen (benannte Konstanten) in einer Schnittstelle erlaubt. Statische Funktionen sind jedoch nicht erlaubt.

Eclipse

Existiert eine Klasse, in der Methoden in einer neuen Schnittstelle definiert werden sollen, lässt sich Refactor, Extract Interface… einsetzen. Es folgt ein Dialog, der uns Methoden auswählen lässt, die später in einer neuen Schnittstelle definiert werden. Eclipse legt die Schnittstelle automatisch an und lässt die Klasse die Schnittstelle implementieren. Dort, wo es möglich ist, erlaubt Eclipse, dass die konkrete Klasse durch die Schnittstelle ersetzt wird.

Implementieren von Schnittstellen

Möchte eine Klasse eine Schnittstelle verwenden, so folgt hinter dem Klassennamen das Schlüsselwort implements und dann der Name der Schnittstelle. Die Ausdrucksweise ist dann: »Klassen werden vererbt und Schnittstellen implementiert.«

Die Klasse Kirche soll die Schnittstelle Unterhaltend implementieren und verpflichtet sich damit, alle beschriebenen Methoden zu implementieren. Unsere Klasse Kirche soll ein Gebaude und Unterhaltend sein. Damit wird sie eine Methode unterhaltungswert() besitzen.

Listing 6.60   vd/Kirche.java

package vd;
class Kirche extends Gebaeude   implements Unterhaltend
  {
  int anzahlBeichtstühle;
  int verfügbarerMesswein;
  /**
   * Liefert den Unterhaltungswert der Kirche.
   *
   * @return  Unterhaltungswert.
   */
    public int unterhaltungswert()
  {
    return anzahlBeichtstühle * verfügbarerMesswein * verfügbarerMesswein;
  }
  }

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

Abbildung 6.9   Kirche und Disko sind Gebäude und implementieren Unterhaltend


Hinweis   Bei der Implementierung einer Schnittstelle müssen die Methoden in den Unterklassen öffentlich implementiert werden, da die Methoden in Schnittstellen immer automatisch public sind. Außerdem darf die Annotation @Override nicht verwendet werden.

Implementiert eine Schnittstelle nicht alle Funktionen aus den Schnittstellen, so erbt sie damit eine abstrakte Funktion und muss selbst wieder als abstrakt gekennzeichnet werden.

Eclipse

Eclipse zeigt bei der Tastenkombination (Ctrl)+(T) eine Typhierarchie an, also Oberklassen stehen oben und Unterklassen unten. Wird in dieser Ansicht erneut (Ctrl)+(T) gedrückt, wird die Ansicht umgedreht, dann stehen die Obertypen unten mit dem Vorteil, dass auch die implementierte Schnittstelle unter den Obertypen ist.

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

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

Markierungsschnittstellen

Auch Schnittstellen ohne Methoden sind möglich. Diese leeren Schnittstellen werden Markierungsschnittstellen (engl. marker interface) genannt. Sie sind nützlich, da mit instanceof leicht überprüft werden kann, ob sie einen gewollten Typ einnehmen.


Beispiel   Die Sun-Bibliothek bringt einige Markierungsschnittstellen schon mit, etwa: java.util.RandomAccess, java.rmi.Remote oder java.io.Serializable.
interface Serializable
{
}

Was bringen Schnittstellen?

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 eine neue Sicht, eine Art Rolle. Implementiert eine Klasse diverse Schnittstellen, 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.


Galileo Computing

6.11.1 Ein Polymorphie-Beispiel mit Schnittstellen  downtop

An dieser Stelle sei noch einmal an die Möglichkeit erinnert, Funktionen auf Objekte auszuführen, die eine gemeinsame Basis haben. Wir haben Disko und Kirche als zwei Klassen, die Unterhaltend implementieren und somit über die Funktion unterhaltungswert() verfügen. Diese Unterhaltungs-Klassen wollen wir in eine Container-Klasse aufnehmen, das Unterhaltungsprogramm. Die Klasse soll neue unterhaltende »Dinge« aufnehmen können. Dabei ist es der Klasse ziemlich egal, ob die Elemente Diskotheken, Kirchen oder Unterwäsche sind – Hauptsache, die Elemente implementieren die Schnittstelle Unterhaltend. Die interessante Funktion ist daher:

void fügeHinzu( Unterhaltend u )

Intern sollen die Unterhaltend-Objekte in einer dynamischen Datenstruktur vom Typ java.util.ArrayList gehalten werden. Das Beispiel nutzt Java Generics und präzisiert den Typ der Datenstruktur mit Unterhaltend.

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

Listing 6.61   vd/Unterhaltungsprogramm.java

package vd;
import java.util.ArrayList;
public class Unterhaltungsprogramm
{
  /** Eine dynamische Datenstruktur nimmt Unterhaltend-Objekt auf. */
  private ArrayList<Unterhaltend> programm = new ArrayList<Unterhaltend>();
  /**
   * Fügt ein neues Unterhaltungs-Objekt in das Unterhaltungsprogramm ein.
   *
   * @param u Hinzuzufügendes Unterhaltungs-Objekt.
   */
  public void fügeHinzu( Unterhaltend u )
  {
    programm.add( u );
  }
  /**
   * Liefert den Gesamtunterhaltungswert aller Unterhaltungs-Objekte.
   *
   * @return Gesamtunterhaltungswert.
   */
  public int unterhaltungswert()
  {
    int uw = 0;
    for ( Unterhaltend u : programm )
      uw += u.unterhaltungswert();
    return uw;
  }
}

Die Polymorphie tritt in der Funktion unterhaltungswert() auf. Dort rufen wir auf jedem Objekt, das Unterhaltend implementiert, die Funktion unterhaltungswert() auf. Durch die Summation der unterschiedlichen Unterhaltungswerte bekommen wir den Unterhaltungswert des gesamten Unterhaltungsprogramms. Auch die Klasse Unterhaltungsprogramm könnte wiederum Unterhaltend implementieren. So implementiert sich schnell das so genannte Composite-Pattern.

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


Galileo Computing

6.11.2 Die Mehrfachvererbung bei Schnittstelledowntop

Bei Klassen gibt es die Einschränkung, dass nur von einer direkten Oberklasse abgeleitet werden darf – egal, ob abstrakt oder nicht. 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. Ohne Schwierigkeiten kann eine Klasse mehrere Schnittstellen implementieren. Dies wird gelegentlich als »Mehrfachvererbung in Java« bezeichnet. Auf diese Weise besitzt die Klasse ganz unterschiedliche Typen, da sie nun instanceof der Oberklasse – beziehungsweise der indirekten Oberklassen – sowie der Schnittstellen ist.

Eine Disko soll nicht nur die Methode unterhaltungswert() von Unterhaltend implementieren, sondern sich auch mit anderen Diskotheken vergleichen können. Dazu gibt es schon eine passende Schnittstelle von Sun: java.lang.Comparable. Wir fordern, dass unsere Disko auch die Methode compareTo(Object) implementiert. (Von der Implementierung mit Generics mit Comparable<Disko> sehen wir in diesem Beispiel ab.)

Listing 6.62   ve/Disko.java

package ve;
public class Disko implements UnterhaltendComparable
{
  int anzahlPersonen;
  int quadratmeter;
  public int unterhaltungswert()
  {
    return (int) (anzahlPersonen * Math.sqrt( quadratmeter ));
  }
  /**
   * Vergleicht ein anderes Disko-Objekt mit unserem auf Grund des
   * Unterhaltungswerts.
   *
   * @param   other  Die <code>Disko</code>, die sich mit uns vergleicht.
   *
   * @return  Einen Wert kleiner Null, wenn wir einen kleinen Unterhaltungswert haben
   *          als die andere Disko. 0, wenn beide gleich unterhaltend sind. Einen
   *          Wert größer Null, wenn unsere Disko unterhaltender ist.
   *
   * @throws  <code>IllegalArgumentException</code> wenn das Argument keine Disko ist.
   *
   * @see     java.lang.Comparable
   */
  public int compareTo( Object other )
  {
    if ( other instanceof Disko )
      return this.unterhaltungswert() – ((Disko) other).unterhaltungswert();
    throw new IllegalArgumentException( "Kann mich nur mit Diskos vergleichen" );
  }
}

Durch diese »Mehrfachvererbung« bekommt Disko mehrere Obertypen, sodass sich je nach Sichtweise schreiben lässt:

Object       d1 = new Disko();
Unterhaltend d2 = new Disko();
Comparable   d3 = new Disko();
Disko        d4 = new Disko();

Dabei ist über d2 die Methode compareTo() und für d3 die Methode unterhaltungswert() nicht definiert. Für d4 sind dagegen alle Methoden sichtbar.

Ein kleines Beispiel zeigt abschließend die Anwendung der Funktion compareTo().

Listing 6.63   ve/Disko.java

package ve;
public class DiskoKritiker
{
  public static void main( String[] args )
  {
    Disko d1 = new Disko();
    d1.quadratmeter   = 1230; d1.anzahlPersonen =  891;
    Disko d2 = new Disko();
    d2.quadratmeter   = 2390; d2.anzahlPersonen = 1091;
    System.out.println( d1.unterhaltungswert() );  //  31248
    System.out.println( d2.unterhaltungswert() );  //  53336
    System.out.println( d1.compareTo(d2) );        // –22088
  }
}

Keine Kollisionen bei Mehrfachvererbung

Das Dilemma bei der Mehrfachvererbung von Klassen wäre, dass zwei Oberklassen die gleiche Funktion mit zwei unterschiedlichen Implementierungen vererben könnte. Die Unterklasse wüsste dann nicht, welche Logik sie erbt. Bei den Schnittstellen gibt es das Problem nicht, denn auch wenn zwei implementierende Schnittstellen die gleiche Funktion vorschreiben würden, gäbe es keine zwei verschiedenen Implementierungen von Anwendungslogik. Die implementierende Klasse bekommt sozusagen zweimal die Aufforderung, die Operation zu implementieren.


Beispiel   Zwei Schnittstellen schreiben die gleiche Funktion vor. Eine Klasse implementiert beide Schnittstellen.

Listing 6.64   Saddam.java

interface Politisch
{
    void geldHer();
  }
interface Verbrecherisch
{
    void geldHer();
  }
public class Saddam   implements Politisch, Verbrecherisch
  {
    public void geldHer()
    {
  }
}


Galileo Computing

6.11.3 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 Telefonierend   extends Unterhaltend
  {
  double gesprächsdauer();
}

Eine Klasse, die nun Telefonierend implementiert, muss die Methoden von beiden Schnittstellen implementieren, demnach die Methode unterhaltungswert() aus Unter-haltend sowie die Methode gesprächsdauer(), die in Telefonierend selbst angegeben wurde.


Galileo Computing

6.11.4 Vererbte Konstanten bei Schnittstelledowntop

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 Unterschnittstellen erneut verwendet werden. Das nachfolgende Programm implementiert das folgende UML-Diagramm:

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

Listing 6.65   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 SockenfarbenHosenfarben
{
  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 Unterschnittstellen. 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 Sockenfarben 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. Unterschnittstellen können aus zwei Oberschnittstellen die Attribute gleichen Namens übernehmen, auch wenn sie den gleichen Wert haben. Dann muss nur bei der Benutzung ein 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 Oberschnittstellen 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 Oberschnittstellen 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.11.5 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.

Listing 6.66   InterfaceObjectReference.java

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<T>
{
  int compare( T o1T 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.) Außerdem 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.11.6 Abstrakte Klassen und Schnittstellen im Vergleich  downtop

Eine abstrakte Klasse und eine Schnittstelle sind sich sehr ähnlich: Beide schreiben den Unterklassen beziehungsweise den implementierten Klassen Operationen vor, die sie im-plementieren müssen. Ein wichtiger Unterschied ist jedoch der, dass beliebig viele Schnittstellen implementiert, jedoch nur eine Klasse – sei sie abstrakt oder nicht – erweitert werden kann. Des Weiteren bieten sich abstrakte Klassen meist im Refactoring oder in der Design-Phase an, wenn Gemeinsamkeiten in einer Oberklasse ausgelagert werden sollen. Abstrakte Klassen können zusätzlichen Programmcode enthalten, was Schnittstellen nicht können. Auch nachträgliche Änderungen an Schnittstellen sind nicht einfach: einer abstrakten Klasse kann eine konkrete Methode mitgegeben werden, was zu keiner Quellcodeanpassung für Unterklassen führt.

Ein Beispiel: Ist eine Schnittstelle oder eine abstrakte Klasse besser, um folgende Operation zu definieren?

abstract class Timer                          interface Timer
{                                             {
  abstract long getTimeInMillis();              long getTimeInMillis();
}                                             }

Eine abstrakte Klasse hätte den Vorteil, dass später einfacher eine Methode wie getTimeInSeconds() eingeführt werden kann, die konkret sein darf. Würde diese angenehme Hilfs-Operation in einer Schnittstelle vorgeschrieben, so müssten alle Unterklassen diese Implementierung immer neu einführen, wobei sie doch schon in der abstrakten Oberklasse einfach programmiert werden könnte:

abstract class Timer
{
  abstract long getTimeInMillis();
  long getTimeInSeconds()
  {
    return getTimeInMillis() * 1000;
  }
}

Galileo Computing

6.11.7 CharSequence als Beispiel einer Schnittstelldowntop

Bisher kennen wir die Klassen String, StringBuffer und StringBuilder, um Zeichenketten zu speichern und weiterzugeben. Ein String ist ein Wertobjekt und ein wichtiges Hilfsmittel in Programmen, da durch diesen unveränderliche Zeichenkettenwerte repräsentiert werden, während StringBuffer und StringBuilder veränderliche Zeichenfolgen umfassen. Aber wie sieht es aus, wenn eine Teilzeichenkette gefordert ist, bei der es egal sein soll, ob das Original als String-, StringBuffer oder StringBuilder-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 der Version 1.4 die Schnittstelle CharSequence eingefügt, die eine unveränderliche, nur lesbare Sequenz von Zeichen realisiert. Die Schnittstelle wird von String sowie StringBuffer und StringBuilder implementiert, sodass sich alle Zeichenketten dieser Klassen als CharSequence auszeichnen. Funktionen müssen sich also nicht mehr für konkrete Klassen entscheiden, sondern können einfach ein CharSequence-Objekt als Argument akzeptieren oder als Rückgabe weitergeben. Ein String und ein StringBuffer/StringBuilder-Objekt können zwar mehr, als CharSequence vorschreibt, beide lassen sich aber als CharSequence einsetzen, wenn das »Mehr« an Funktionalität nicht benötigt wird.

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


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/StringBuilder 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, … class java.lang.StringBuffer implements CharSequence, … class java.lang.StringBuilder implements CharSequence, …


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.


Galileo Computing

6.11.8 Die Schnittstelle Iterable  toptop

Dass Felder durch die erweiterte for-Schleife durchlaufen werden können, ist nur ein Teil. Als Typ rechts vom Doppelpunkt kann alles stehen, dessen Klassen die Schnittstelle Iterable implementieren. Sie schreibt nur die Existenz einer Funktion iterator() vor, die einen java.util.Iterator liefert.


interface java.lang.  Iterable<T>  

gp  Iterator<T> iterator() Liefert einen Iterator, der über alle Elemente vom Typ T iteriert.

Viele Klasse implementieren schon diese Schnittstelle, sodass mit dem erweiterten for durch Ergebnismengen iteriert werden kann. In erster Linie handelt es sich um Datenstrukturen. Dazu kommt noch das Feld, welches zwar nicht direkt als Klasse sichtbar ist, aber Iterable passend implementiert.

Einen eigenen Iterable

Möchten wir selbst rechts neben dem Doppelpunkt vom erweiterten for stehen, müssen wir ein Objekt angeben, dessen Klasse Iterable implementiert und somit eine iterator()-Funktion besitzt. iterator() muss dann einen passenden Iterator zurückgeben. Der wiederum muss die Methoden hasNext() und next() implementieren, um das nächste Element in der Aufzählung zu geben und das Ende anzuzeigen. Zwar schreibt der Iterator auch remove() vor, doch das wird leer implementiert.

Unser Beispiel soll einen praktischen Iterable implementieren, um über Worte eines Satzes zu gehen. Als grundlegende Implementierung dient der StringTokenizer, der über hasToken()die nächsten Teilfolgen und über hasMoreTokens() meldet, ob weitere Tokens ausgelesen werden können.

Beginnen wir mit dem ersten Teil, der Klasse WordIterable, die erst einmal Iterable implementieren muss, um auf der rechten Seite vom Punkt stehen zu können. Dann muss dieses Exemplar über iterator() einen Iterator zurückgeben, der über alle Wörter läuft. Dieser Iterator kann als eigene Klasse implementiert werden, doch wir implementieren die Klasse WordIterable so, dass sie Iterable und Iterator gleichzeitig verkörpert; daher ist nur ein Exemplar nötig.

Listing 6.67   WordIterableDemo.java

import java.util.Iterator;
import java.util.StringTokenizer;
class WordIterable implements Iterable<String>Iterator<String>
{
  private StringTokenizer st;
  public WordIterable( String s )
  {
    st = new StringTokenizer( s );
  }
  // Methode vom Iterable
  public Iterator<String> iterator()
  {
    return this;
  }
  // Methoden vom Iterator
  public boolean hasNext()
  {
    return st.hasMoreTokens();
  }
  public String next()
  {
    return st.nextToken();
  }
  public void remove()
  {
    // No remove.
  }
}

Im Beispiel:

public class WordIterableDemo
{
  public static void main( String[] args )
  {
    String s = "Am Anfang war das Wort – am Ende die Phrase. (Stanislaw Jerzy Lec)";
    for ( String word : new WordIterable(s) )
      System.out.println( word );
  }
}

Die Konstruktion

for ( Object word : new WordIterator(s) )
  System.out.println( words );

wird vom (Eclipse-)Compiler umgebaut zu:

Object word;
WordIterable worditerable;
for ( Iterator iterator = (worditerable = new WordIterable(s)).iterator();
iterator.hasNext(); )
{
  word = iterator.next();
  System.out.println( word );
}
 << 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