Titel | Inhalt | Suchen | Index | DOC | Handbuch der Java-Programmierung, 7. Auflage |
<< | < | > | >> | API | Kapitel 10 - OOP III: Interfaces |
In den vorigen Abschnitten wurde gezeigt, dass es viele Gemeinsamkeiten zwischen (abstrakten) Klassen und Interfaces gibt. Während der Designphase eines komplexen Software-Systems ist es daher häufig schwierig, sich für eine von beiden Varianten zu entscheiden. Für die Verwendung des Interface spricht die größere Flexibilität durch die Möglichkeit, in unterschiedlichen Klassenhierarchien verwendet zu werden. Für die Verwendung einer Klasse spricht die Möglichkeit, bereits ausformulierbare Teile der Implementation zu realisieren, und die Fähigkeit, statische Bestandteile und Konstruktoren unterzubringen.
Wir wollen in diesem Abschnitt zeigen, wie sich beide Ansätze vereinen lassen. Dabei wird zunächst jeweils ein Interface zur Verfügung gestellt und seine Anwendung dann unter Verwendung einer Hilfsklasse vereinfacht.
Wird ein Interface erstellt, das voraussichtlich häufig implementiert werden muss und/oder viele Methoden definiert, ist es sinnvoll, eine Default-Implementierung zur Verfügung zu stellen. Damit ist eine Basisklasse gemeint, die das Interface und alle sinnvoll realisierbaren Methoden implementiert. Besitzt eine Klasse, die das Interface implementieren muss, keine andere Vaterklasse, so kann sie von der Default-Implementierung abgeleitet werden und erbt so bereits einen Teil der sonst manuell zu implementierenden Funktionalität.
Als Beispiel soll ein Interface SimpleTreeNode definiert werden, das zur Konstruktion eines Baums verwendet werden kann, dessen Knoten von beliebigem Typ sind und jeweils beliebig viele Kinder haben:
001 /* SimpleTreeNode.java */ 002 003 public interface SimpleTreeNode 004 { 005 public void addChild(SimpleTreeNode child); 006 public int getChildCnt(); 007 public SimpleTreeNode getChild(int pos); 008 } |
SimpleTreeNode.java |
Die Default-Implementierung könnte wie folgt realisiert werden:
001 /* DefaultTreeNode.java */ 002 003 public class DefaultTreeNode 004 implements SimpleTreeNode 005 { 006 private int CAPACITY; 007 private String name; 008 private SimpleTreeNode[] childs; 009 private int childcnt; 010 011 public DefaultTreeNode(String name) 012 { 013 this.CAPACITY = 5; 014 this.name = name; 015 this.childs = new SimpleTreeNode[CAPACITY]; 016 this.childcnt = 0; 017 } 018 019 public void addChild(SimpleTreeNode child) 020 { 021 if (childcnt >= CAPACITY) { 022 CAPACITY *= 2; 023 SimpleTreeNode[] newchilds = new SimpleTreeNode[CAPACITY]; 024 for (int i = 0; i < childcnt; ++i) { 025 newchilds[i] = childs[i]; 026 } 027 childs = newchilds; 028 } 029 childs[childcnt++] = child; 030 } 031 032 public int getChildCnt() 033 { 034 return childcnt; 035 } 036 037 public SimpleTreeNode getChild(int pos) 038 { 039 return childs[pos]; 040 } 041 042 public String toString() 043 { 044 return name; 045 } 046 } |
DefaultTreeNode.java |
Der Vorteil ist hier noch nicht sehr offensichtlich, weil die Implementierung nicht sehr aufwändig ist. Bei komplexeren Interfaces zahlt es sich in der Praxis meistens aus, wenn eine Default-Implementierung zur Verfügung gestellt wird. Neben der dadurch erzielten Arbeitsersparnis dient sie auch zur Dokumentation und stellt eine Referenzimplementierung des Interface dar. |
|
Lässt sich eine potenzielle SimpleTreeNode-Klasse nicht von DefaultTreeNode ableiten, muss sie das Interface selbst implementieren. Besitzt die Default-Implementierung bereits nennenswerte Funktionalitäten, wäre es schlechter Stil (und auch sehr fehlerträchtig), diese ein zweites Mal zu implementieren. Stattdessen ist es eventuell möglich, die Implementierung an die bereits vorhandene DefaultTreeNode zu delegieren.
Dazu muss die zu implementierende Klasse eine Membervariable vom Typ DefaultTreeNode anlegen und alle Aufrufe der Interface-Methoden an dieses Objekt weiterleiten. Soll beispielsweise die Klasse Auto aus Listing 8.1 in eine SimpleTreeNode verwandelt werden, könnte die Implementierung durch Delegation wie folgt vereinfacht werden:
001 /* Auto5.java */ 002 003 public class Auto5 004 implements SimpleTreeNode 005 { 006 public String name; 007 public int erstzulassung; 008 public int leistung; 009 010 private SimpleTreeNode treenode = new DefaultTreeNode(""); 011 012 public void addChild(SimpleTreeNode child) 013 { 014 treenode.addChild(child); 015 } 016 017 public int getChildCnt() 018 { 019 return treenode.getChildCnt(); 020 } 021 022 public SimpleTreeNode getChild(int pos) 023 { 024 return treenode.getChild(pos); 025 } 026 027 public String toString() 028 { 029 return name; 030 } 031 } |
Auto5.java |
Hier nutzt die Klasse Auto5 die Funktionalitäten der Membervariable DefaultTreeNode zur Verwaltung von Unterknoten und leitet alle entsprechenden Methodenaufrufe an sie weiter. Die Verwaltung des Knotennamens erfolgt dagegen mit Hilfe der eigenen Membervariable name.
Mitunter wird ein Interface entworfen, bei dem nicht immer alle definierten Methoden benötigt werden. In der Java-Klassenbibliothek gibt es dafür einige Beispiele, etwa bei Listenern (siehe Kapitel 28) oder im Collection-API (siehe Kapitel 16). Da bei der Implementierung aber immer alle definierten Methoden implementiert werden müssen, wenn die Klasse nicht abstrakt bleiben soll, kann es nützlich sein, eine leere Standard-Implementierung zur Verfügung zu stellen. Implementierende Klassen können sich dann gegebenenfalls von dieser ableiten und brauchen nur noch die Methoden zu realisieren, die tatsächlich benötigt werden.
Titel | Inhalt | Suchen | Index | DOC | Handbuch der Java-Programmierung, 7. Auflage, Addison Wesley, Version 7.0 |
<< | < | > | >> | API | © 1998, 2011 Guido Krüger & Heiko Hansen, http://www.javabuch.de |