Title:

Java Programmierung

Home
deutsch
  
ISBN: 3897214482   ISBN: 3897214482   ISBN: 3897214482   ISBN: 3897214482 
 
|<< First     < Previous     Index     Next >     Last >>|
  Wir empfehlen:       
 


3.                  Programmierung für Vererbung

3.1.            Motivation

Die Motivation für Vererbung zu programmieren liegt in der bestmöglichsten Wiederverwendbarkeit des Source-Codes. Wie in Abschnitt 2.3 gesehen, bestehen zwei Möglichkeiten Code wiederzuverwenden. Diese beide Möglichkeiten sind leicht anzuwendende Verfahren, mit denen man erreicht, den "alten" Quellcode anzupassen und zu erweitern. Trotz der durch Java relativ geschützten Typsicherheit, erhöht sich jedoch die Komplexität des Programms. Um an dieser Stelle Fehler zu vermeiden muss der Entwickler oder Verwender sicherstellen, dass keine Seiteneffekte oder unerwünschte Zugriffe auf die Klassen, bzw. deren Mitglieder stattfinden. Deshalb ist es notwendig bereits im voraus zu planen. Das Programm sollte durch sinnvolle Klassen und Methoden strukturiert werden. Zunächst ist es sinnvoll, den Programmcode in kleine Methoden nach ihrer Funktionsweise aufzugliedern, um gezielt einzelne Bereiche anpassen zu können und sich nicht durch zu frühe Spezialisierung einzuschränken. Man erreicht eine wesentlich flexiblere Vererbung, jedoch erschwert dies auch die Leserlichkeit des Source-Codes. Ein weiterer Aspekt ist das Schachteln von Methoden, speziell das Aufrufen von Methoden innerhalb von Konstruktoren, was aufgrund der dynamischen Methodenbindung zu fatalen Seiteneffekten führen kann:

 class SuperKlasse {
       //Member-Variablen
      String a;
       //Standard-Konstruktor
      SuperKlasse() {
a = "SuperKlasse";
PrintMe();
}
//Member-Methoden
void printMe(){
      System.out.println("SuperKlasse Länge: " + a.length());
}
}//class SuperKlasse
 
class SubKlasse extends SuperKlasse{
       //Member-Variablen
      String b;
       //Standard-Konstruktor
      SubKlasse() {
b = "SubKlasse";
void PrintMe();
}
//Member-Methoden
printMe(){
      System.out.println("SubKlasse Länge: " + b.length());
}
}//class SuperKlasse

Beispiel dynbind.java

Wenn man nun eine Instanz von SubKlasse erzeugt, wird implizit der Konstruktor der SuperKlasse aufgerufen. Dort wird die SubKlassenVariable a initialisiert und die Methode printMe() aufgerufen. Durch die dynamische Bindung wird jedoch die SubKlassen-Methode aufgerufen, was zu einem Programmabbruch durch eine NullPointerException führt, da das Attribut b der SubKlasse noch nicht initialisiert wurde. Weitere Gefahren entstehen bei unzureichendem Zugriffschutz. Auf Zugriffschutz durch geeignete Anwendung von Kapselungstechniken wird im Abschnitt 2.4. genauer eingegangen.

 

3.2.      Subclassing / Abstrakte Klassen / Interfaces / Substituierbarkeit

 Im vorhergehenden Abschnitt wurde der Begriff Typsicherheit erwähnt. Dieser Begriff geht einher mit Vererbung und Subtyping. Für Subtyping ist grundsätzlich verlangt, dass Subtyp-Objekte alle Methoden vom Supertyp unterstützen und insbesondere auch besitzen. Weiterhin ist auch verlangt, dass die Methoden die gleichen Parameter, sowie gleichen Rückgabetyp haben. Diese syntaktische Bedingung ist auch bei den von Java unterstützten Interface zu finden (class SubTyp implements InterfaceA { ... }). Diese Bedingung ist ebenfalls durch die Vererbung in Java erfüllt. Bei den meisten OOP-Sprachen, insbesondere Java, fällt die Vererbungshierarchie (vor allem wegen der fehlenden Mehrfachvererbung, vgl. Abschnitt 3.3.) direkt mit der SubTyp-Hierarchie zusammen. Deshalb spricht man bei Vererbung wegen der Verbindung zur Subtypisierung von Subclassing. In dem Moment wo eine Klasse von einer anderen erbt besitzt sie ihren Typ. So ist es zum Beispiel in Java nicht möglich, dass eine Klasse durch mehrfache Vererbung mehrere Typen in sich vereinigt.

Um aber einen gemeinsamen Typen festzulegen kann man sich in Java der Interfaces und der abstrakten Klassen bedienen. Das Ziel besteht im Zusammenfassen gemeinsamer Implementierungsteile zukünftiger Subtypen an einer Stelle. Abstrakte Klassen sind ausschließlich vererbbar, d.h. man kann sie nur durch extends weitervererben und somit ihre SubKlasse mit dem Typ der abstrakten Klasse ausstatten und die SubKlasse muss alle in der abstrakten Klassen definierten Methoden deklarieren. Eine abstrakte Klasse kann auch Teilimplementierungen, sowie Konstanten enthalten. Interfaces sind gegenüber den abstrakten Klassen im Vergleich nicht vererbbar, sondern nur mit implements implementierbar. In Java dürfen jedoch beliebig viele Interfaces in einer Klasse implementiert werden, jedoch nur eine Klasse vererbt. Weiterhin besitzen Interfaces nur Operationssignaturen und keine Attribute. Abstrakte Klassen stellen wie die SuperKlassen, von denen man erben kann, einen Obertypen zu dem entstehenden Subtypen dar und es werden auf dem selben Pfad in der Vererbungshierarchie nach unten laufend alle direkten und indirekten Obertypen weitervererbt.

 abstract class SampleAbstract {
       //Member-Variablen
      int a;
      
//Member-Methoden
public void print(){
      System.out.println("abstract class nr " + a);
}
public abstract void move();
 
}//abstract class SampleAbstract

 Die abstrakte Klasse SampleAbstract stellt eine Methode und ein Attribut zur Verfügung, die vererbt werden können. In diesem Beispiel wird angenommen, dass die Methode in anderen Klassen gleich verwendet wird. Die abstrakte Methode move() enthält keine Gemeinsamkeiten in der Implementierung und muss deshalb für jede Klasse eigens implementiert werden:

interface SampleInterface {
      //Methodensignaturen
void copy();
void print();
}//interface SampleInterface

Das Interface SampleInterface stellt zwei Methodensignaturen zur Verfügung, die implementiert werden müssen, falls eine Klasse mit dem Interface erweitert wird. Die Klasse Sample implementiert das Interface SampleInterface und erbt von SampleAbstract:

class Sample extends SampleAbstract implements SampleInterface {
      //Implementierungen
public void copy() {
      System.out.println("copying ...");
}
public void move() {
      System.out.println("moving ...");
}
}//class Sample  

Beispiel abstrakt.java

Das auffällige an diesem Beispiel ist, dass die Methoden copy() und move() aufgrund der Implementierungspflicht implementiert wurden, jedoch die Methode print() nicht, da eine Implementierung bereits samt dem Attribut a geerbt wurde und somit nicht mehr zu implementieren ist. Ein Objekt der Klasse Sample hätte jetzt die Typen SampleInterface und SampleAbstract.

Ein weiterer Aspekt der aus SubClassing resultiert ist die Substituierbarkeit (Substitutability). Unter Substituierbarkeit versteht man das mögliche Einsetzen eines SubTyp an der Stelle wo eigentlich der SuperTyp des SubTyps erwartet wird. Es ist allerdings nicht möglich, dass man einen SuperTyp dort einsetzt (substituiert), wo ein SubTyp erwartet wird. Dies ist möglich, da durch die Vererbung an den SubTypen, der SubTyp genau die selben, eventuell verfeinerten, Methoden und Attribute besitzt. Allgemein ausgedrückt, ist eine Substituierung in Richtung der Generalisierung grundsätzlich möglich.

Aus dem obigen Beispiel hat sich ergeben, dass ein Sample-Objekt zwei Typen hat. Dies führt direkt auf die Frage Mehrfachvererbung.

 

3.3.      Mehrfachvererbung

Wie bereits mehrfach erwähnt besitzt Java nicht die Möglichkeit direkt mehrere Klassen an eine Klasse zu vererben. Jedoch kann Mehrfachvererbung (multiple inheritance), genauer gesagt nur die Mehrgestaltigkeit (Polymorphismus), durch Einfachvererbung (single inheritance) und mehrfachen Subtyping über Schnittstellen erzeugt werden, wie man es im obigen Beispiel sehen kann. In der Programmiersprache C++ ist es jedoch möglich mehrfach zu vererben:

class Reptil{
      Reptil();
      ~Reptil();
      void move();
};//class Reptil
 
class Fisch{
      Fisch()
      ~Fisch();
      void move();
};//class Fisch  
class Amphibium : public Reptil, public Fisch{
      Amphibium()
      ~Amphibium();
};//class Amphibium

Die Klasse Amphibium erbt von den Klassen Reptil und Fisch mit ihren Attributen, Methoden und Implementierungen. Durch Mehrfachvererbung entstehen jedoch Nachteile entstehend aus der resultierenden Unübersichtlichkeit und der durch Seiteneffekte entstehenden Fehlerträchtigkeit. Da die Methoden dynamisch zur Laufzeit gebunden werden, kann man von vorne herein nicht sagen, ob Fisch.move() oder Reptil.move() gebunden wird. Dies muss vorher festgelegt werden. Jedoch bleibt auch unklar wie oft ein Attribut vererbt wurde und welches man verwenden sollte.

An diesem Punkt stellt sich jetzt die Frage, ob und wenn ja wie man bestimmte Implementierungen, speziell bei der Einfachvererbung in Java, sperren bzw. verstecken kann.

 

3.4.            Kapselung

Die vorherigen Abschnitte haben erläutert in welcher Weise man vererben kann und welche Schwierigkeiten sich dadurch ergeben können. Das Ziel was man aber ins Auge fasst ist die optimale Wiederverwendbarkeit. Aufgrund dessen sollte der Anwender von Klassen nicht die Möglichkeit haben sie zu verändern, bzw. auch nicht die Möglichkeit haben sie einzusehen, da ein Anwender meist nicht mit Details der Implementierung vertraut ist und so mögliche falsche Rückschlüsse ziehen kann und den Programm-Code verändert oder durch Unwissenheit falsch benutzt. Um dies zu vermeiden und die Möglichkeit zu haben das Klassenmodell transparent zu gestalten verfügt Java, ebenso wie C++ und andere OOP-Sprachen, über Kapselungskonstrukte, die es ermöglichen bestimmte Teile einer Klasse zu verstecken bzw. zu sperren. Die allgemeinen Zugriffmodifikatoren (access modifier) der Kapselung über Methoden, Attribute und Konstruktoren, nämlich private, für den privaten Zugriff auf Methoden und Attribute nur innerhalb der Klasse, package (default-Einstellung), der nur innerhalb eines Pakets den Zugriff erlaubt und public, der einen öffentlichen Zugriff gestattet, werden für die Vererbung in Java durch weitere Zugriffmodifikatoren erweitert. Durch die dynamische Methodenbindung wird die Prüfung der Zugriffregeln erschwert, da zur Übersetzungszeit nicht bekannt ist, welche Methode aufgerufen wird. Um die Zugriffbeschränkungen trotzdem statisch prüfen zu können, müssen überschreibende, bzw. implementierende Methoden (die der SubKlasse) die selben Zugriffrechte besitzen, wie die Überschriebene, Verfeinerte (also die der SuperKlasse).

Folgendes Beispiel einer Queue mit einem Iterator dient zur Veranschaulichung des protected-Modifiers und als Beispiel für die Kapselung von Objektgeflechten in Abschnitt 4:

class Queue {
       //Member-Variablen
       protected Node head, tail;
       //Standard-Konstruktor
Queue() { ... }
//Member-Methoden
public Object pop() { ... }
public void push (Object o) { ... }
 
//store elements of type Object
static class Node {
      Object element;
      Node next;
      Node(Object o) { ... }
}//inner class Node

//QueueIterator - inner class
public class QueueIterator {
      //Member-Variablen
      protected Node next;
      //Member-Methoden
      public boolean hasNext() { ... }
      public Object next() { ... }
}//inner class QueueIterator
 
//Return Iterator
public QueueIterator iterate() { ... }
}//class Queue

 

protected - geschützter Zugriff

 Es bestehen grundsätzlich zwei Möglichkeiten die obige Implementierung einer Queue zu verwenden. Wenn die Queue der reinen Anwendung dient, dann hätte es ausgereicht, alle derzeit mit protected geschützten mit private zu deklarieren. So hätte der Anwender z. B. keinen Zugriff auf das head-Element der Queue gehabt, um die Datenstruktur zu zerstören. Möchte der Anwender oder Programmierer jedoch die Queue ausbauen, sprich er erweitert seine Klasse mit der bestehenden Queue, so hätte er mit einem reinen private-Zugriffschutzmechanismus keine Möglichkeit mehr in der erbenden Klasse auf diese Elemente zugreifen zu können. Im Falle der Vererbungsnutzung stellt Java den Zugriffmodifikator protected zur Verfügung, der den klasseninternen Zugriff auf diese geschützten Elemente auch noch in der erbenden Klasse zulässt, aber auch dort nach außen die Elemente nicht sichtbar macht. Die Elemente der SubKlasse haben somit private-Zugriff auf die geerbten Programmteile.

 

final - gesperrte Vererbung

 class calculate{
protected int a =1;
protected void setA( int i ){ if(i>0) a=i; }
public void divide(){ float f = 7 / a; }
}//class calculate
 
//Erweiterte Klasse mit überschriebener setA()-Methode
class recalculate extends calculate {
public void setA(int i){ a=i; }
}//class recalculate  

Beispiel calc.java

In der Klasse calculate befindet sich die Methode setA(int i), welche die Membervariable a den Wert des Parameters i zuweist, falls dieser größer 0 ist. Durch überschreiben von Methoden beim Vererben ist es jedoch möglich, dass man die Eigenschaft einer Klasse völlig verändert, insbesondere auch außerhalb des Pakets, in dem der Klassentyp deklariert wurde, falls ein Paket importiert wurde. Durch das Überschreiben kann man eine SubKlasse erzeugen, die die Funktionsweise der SuperKlasse nicht mehr erfüllt, wie im obigen Beispiel zu sehen ist. Dort wurde die Methode setA(int i) durch eine ähnliche Methode ersetzt, die aber im Gegensatz zur Überschriebenen keine Prüfung vornimmt, ob der übergebene int-Wert 0 ist. Dies scheint auf den ersten Blick den Programmablauf nicht zu beeinflussen. Ruft man jedoch die Methode divide() auf, und die Member-Variable a wurde von ihrem default-Wert 1, durch die überschreibende Methode auf den Wert 0 gesetzt führt dies zu einer arithmetischen Ausnahmebehandlung und das Programm terminiert mit einem Fehler. In derartigen Situationen ist es von Nöten, den Vererbungsmechanismus noch weiter einzuschränken. Java bietet hierfür anstelle des protected-Modifiers den final-Modifier, welcher Methoden und Attribute als unveränderlich, nicht überschreibbar, deklariert. Von unveränderlichen Klassen können keine SubKlassen gebildet werden! (vgl. class String) Solche Klassen, Methoden und Attribute heißen "value object" oder "immutable objects".

 

3.5.      Zusammenspiel Kapselung  und Vererbung

 Ein noch ungeklärter Punkt sind als private deklarierte Methoden bei der Vererbung. Zuvor wurde festgestellt, dass private-Methoden in der erbenden Klasse nicht zur Verfügung stehen. Es stellt sich die Frage, was ist aber mit Methoden, die vererbt wurden und für ein Funktionieren diese private-Methoden benötigen? Mit private deklarierte Methoden und Attribute sind in SubKlassen nicht zugreifbar, sie stehen aber den vererbten Methoden zur Verfügung, um korrekt funktionieren zu können. Wird in der SubKlasse ein private Methode (mit gleicher Signatur) überschrieben, dann werden die Methoden ("geerbte" Methode und "überschriebene" Methode) getrennt behandelt, als hätten sie unterschiedliche Namen oder Signatur, d.h. es findet bei vererbten private-Methoden keine dynamische Methodenauswahl statt. Aufgrund dieser fehlenden dynamischen Bindung kann eine Kapselung, bei unachtsamer Anwendung, Auswirkung auf die Laufzeitsemantik des Programms haben:

 //class represents pair of int
class IntPair {
private int a;
private int b;
IntPair(int ai, int bi) { a = ai; b = bi; }
int sum() { return this.add(); }               (*)
private int add() { return a+b; }
}//class IntPair
 
//class represents triple of int
class IntTriple extends IntPair {
private int a;
IntTriple (int ai, int bi, int ci) { super(ai, bi); a = ci;}
int sum() { return this.add(); }               (x)
private int add() { return super.sum() + a; }  (+)
}//class IntTriple
 
//class PrivateTest
public class PrivateTest {
public static void main(String argv[]){
            IntPair ip = new IntPair(3,9);
            IntTriple it = new IntTriple(1,2,27);
            System.out.println(ip.sum() + " " + it.sum());
}
}//class PrivateTest  

Beispiel PrivateTest.java

In der mit (x) markierten Zeile wird die Methode sum() neu geschrieben. Wäre dies nicht der Fall, dann würde das Objekt it nur die ersten beiden Werte addieren, da die ererbte Methode sum() aus der SuperKlasse aufgerufen und dort dynamisch an die add()-Methode der SuperKlasse gebunden wird, wegen dem this.add()-Aufruf.

Eine weitere Gefahr stellt, vor allem beim Zusammenspiel mit Vererbung und Kapselung, der super.sum()-Aufruf der mit (+) markierten Zeile dar, welcher einen Aufruf der Methode sum() der IntPair-Klasse (*) nach sich zieht und dort einen this.add()-Aufruf auswertet. Dabei referenziert der this-Parameter ein IntTriple-Objekt, aber es wird trotzdem die Methode der IntPair-Klasse ausgeführt, da die add()-Methode aufgrund der private-Deklaration, wie oben erklärt, nicht überschrieben wurde, insbesondere nicht von der add()-Methode aus der SubKlasse, trotz der identischen Signatur. Hätte man die add-Methoden nicht private deklariert, dann wäre der add()-Aufruf in Zeile (*) dynamisch gebunden worden und hätte auf ein IntTriple-Objekt referenziert, was zu einer nicht terminierenden Rekursion geführt hätte.




  
Java von Kopf bis Fuß
von Kathy Sierra,
Bert Bates,
Lars Schulten,
Elke Buchholz
Siehe auch:
Java - kurz & gut
Entwurfsmuster
von Kopf bis Fuß
SQL
von Kopf bis Fuß
Softwareentwicklung
von Kopf bis Fuß: Ein Buch zum...
Java 6 Das Übungsbuch. 200 Aufgaben mit vollst...
PHP & MySQL
von Kopf bis Fuß
 
   
 
     
|<< First     < Previous     Index     Next >     Last >>| 

Back to the topic site:
StudyPaper.com/Startseite/Computer/Informatik/Programmieren/Java

External Links to this site are permitted without prior consent.
   
  Home  |  deutsch  |  Set bookmark  |  Send a friend a link  |  Copyright ©  |  Impressum