Universität Hamburg - Fachbereiche - Fachbereich Mathematik

Java-Kurs (7)

WiSe 01/02

Bodo Werner

Zurück zum Inhaltsverzeichnis.

7. Klassen und Objekte (Teil 2)

Unterklassen, Vererbung, abstrakte Klassen

In der Mathematik (auch in der Numerischen Mathematik) hat man es mit Funktionen zu tun, die reelle Argumente und reelle Werte haben. Beispiele hierfür sind quadratische Funktionen f(x)=a0+a1*x+a2*x^2, aber auch sin(x), ln(x) und rationale Funktionen. Solche Funktionen wollen wir in Java relisieren, u.a. mit dem Ziel, ihre Graphen (Funktionsbilder) zu zeichnen.

Zwei verschiedene Funktionen unterscheiden sich im wesentlichen in ihrer Abbildungsvorschrift, die in Java in naheliegender Weise durch eine Methode (die wir getY() nennen werden) mit einem double-Parameter und einem double-Rückgabewert realisiert wird. Wollte man zwei verschiedene Funktionen als zwei Objekte derselben Klasse (etwa namens Funktion) realisieren, müssten diese die Methoden wie ihre Daten wechseln können, was nicht ohne weiteres möglich ist. Daher werden wir hier von der mächtigen Möglichkeit der Bildung von Unterklassen Gebrauch machen. Wir werden eine abstrakte Klasse namens Funktion mit einer abstrakten Methode double getY(double x) einführen, deren Unterklassen konkret gegebene Funktionen sein werden, in denen die konkrete Abbildungsvorschrift durch den Methodenblock von getY() definiert wird.

Die Bildung von Unterklassen mit der Vererbung von Daten und Methoden ist ein ganz wesentlicher Aspekt von OOP.

7.1 Die abstrakte Klasse Funktion

extends

Abstrakt heißt eine Klasse, wenn man keine Instanzen von ihr selbst, sondern nur von ihren Unterklassen bilden will. Eine Methode einer abstrakten Klasse heißt abstrakt, wenn ihre genaue Realisierung bis zur Bildung einer Unterklasse aufgeschoben wird.

Wir beginnen mit einer ganz einfachen ersten Realisierung einer abstrakten Klasse Funktion1, ihrer Unterklasse Sinus1 und einer Applikationsklasse AppF1, die ein Objekt der Klasse Sinus1 konstruiert:


abstract class Funktion1{
   abstract  double getY(double x);
}//Ende class Funktion1


class Sinus1 extends  Funktion1{
   double getY(double x){
      return Math.sin(x);
   }//Ende getY()
}//Ende class Sinus1

class AppF1{
   static void main(String[] args){
      Sinus1 sinus = new Sinus1();
      System.out.println(sinus.getY(Math.PI/2));
   }//Ende main()
}//Ende AppF1
Das hätten wir ohne das Getrickse mit Unterklassen auch einfacher haben können: Die Applikationsklasse hätte genügt, wenn man die dritte Zeile weggelassen und in der vierten sinus.getY durch Math.sinus ersetzt hätte. Bevor wir die Klasse Funktion1 so erweitern, dass wir doch von dem Unterklassenkonzept profitieren können, eine Bemerkung: Hier wurden keine Konstruktoren definiert, obwohl in Zeile 3 der Klasse AppF1 ein Default-Konstruktor Sinus1() zur Konstruktion eines Objektes namens sinus der Unterklasse Sinus1 von Funktion1 verwendet wird. Ein solcher Default-Konstruktor macht nichts anderes als ein Objekt zu initialisieren (allokieren), ohne dass weitere Informationen über den Inhalt gegeben werden.

Wichtig ist auch das Schlüsselwort extends zur Deklaration der Klasse als Unterklasse einer anderen Klasse.

Gewöhnungsbedürftig ist die Unterscheidung zwischen der Klasse namens Sinus1 und dem Objekt namens sinus dieser Klasse. Besonders deswegen, weil es gar nicht zwei verschiedene Objekte der Klasse Sinus1 geben kann. Das wird anders, wenn wir durch Einführung von Daten dieser Klasse und eines Konstruktors, der diese Daten übergibt, verschiedene Objekte konstruieren können (s.u.).

Ein rechnerisches Gegenstück zu einem Funktionsgraphen ist eine Wertetabelle einer Funktion. Hier betrachten wir solche, die eine Funktion an gleichabständigen, aufsteigenden Stellen auswertet. Es genügt hierzu den ersten, den letzten und die Anzahl (N+1) der Stellen zu kennen. Die folgende Klasse Funktion2 unterscheidet sich von Funktion1 darin, dass sie die Methode getWertetabelle() enthält, die auf alle Unterklassen vererbt wird und auf die Methode getY() zugreift (auch wenn diese erst in der jeweiligen Unterklasse definiert wird):


abstract class Funktion2{
  abstract  double getY(double x);
  double[] getWertetabelle(double a, double b, int N) {
     double[] y=new double[N+1];
     for (int i=0;i<N+1;i++) y[i]=getY(a+i*(b-a)/N);
     return y;
  }//Ende getWertetabelle()
}//Ende class Funktion2

class Sinus2 extends  Funktion2{
  double getY(double x){
    return Math.sin(x);
  }//Ende getY()
}//Ende class Sinus2

class AppF2{
  static void main(String[] args){
    Sinus2 sinus = new Sinus2();
    int N=10;
    double[] y=sinus.getWertetabelle(0,1,N);
    for(int i=0;i<N+1;i++) 
      System.out.println(
        "x["+i+"]="(double) i/N+"y["+i+"]="+y[i]);
  }//Ende main()
}//Ende AppF2
Achten Sie darauf, dass die Methode getWertetabelle() der Klasse Funktion2 nicht abstrakt ist. Sie kann von jeder Unterklasse übernommen werden, weil diese ja die aufgeschobene Methode getY() realisieren muss. Die Zeile double[] y=sinus.getWertetabelle(0,1,N); in AppF2 ist syntaktisch richtig, weil das Objekt namens sinus von der Oberklasse die Methode getWertetabelle() geerbt hat.

7.2 Die Überlagerung von Konstruktoren und Methoden in Unterklassen

super()

Bisher sind wir in den Funktion-Klassen ohne Konstruktoren ausgekommen. Zum Zeichnen von Funktionsgraphen, aber auch zur Erstellung einer Wertetabelle muss man von einem Intervall [a,b] als Definitionsbereich der Funktion ausgehen. Statt diese Parameter an die Methoden zu übergeben, können wir die Endpunkte auch alternativ als Daten der abstrakten Klasse Funktion vorsehen (wir nennen sie xMin, xMax ) und sie einem Konstruktor übergeben:
abstract class Funktion3{
    //Daten:
    double xMin, xMax;
    //Konstruktor:
    Funktion3(double xMin, double xMax){
	this.xMin=xMin; this.xMax=xMax;
    }
    abstract double getY(double x);
    //vergleiche mit Funktion2.java
    double[] getWertetabelle(int N){
      double[] y=new double[N+1];
      for (int i=0;i<N+1;i++) 
        y[i]=getY(xMin+i*(xMax-xMin)/N);
      return y;
    }//Ende Wertetabelle()
}//Ende class Funktion3


class Sinus3 extends  Funktion3{
    //Konstruktor:
    Sinus3(double a, double b){
	super(a,b);
    }
   double getY(double x){
      return Math.sin(x);
   }//Ende getY()
}//Ende class Sinus3

class AppF3{
  static void main(String[] args){
    Sinus3 sinus = new Sinus3(0,5);
    int N=10;
    double[] y=sinus.getWertetabelle(N);
    double[] x=new double[N+1];
    for (int i=0;i<N+1;i++) {
      x[i]=sinus.xMin+(sinus.xMax-sinus.xMin)*i/N;
      System.out.println(
        "x["+i+"]="+x[i]+" y["+i+"]="+y[i]);
  }
  }//Ende main()
}//Ende AppF3
Sieht die Oberklasse einen Konstruktor vor, muss die Unterklasse ebenfalls einen Konstruktor haben, und dieser muss als erstes mit dem super()-Befehl den Konstruktor der Oberklasse aufrufen. Danch kann der Konstruktor der Unterklasse noch weitere Details festlegen, z.B. zur Auswahl einer Funktion aus einer Funktionenschar:

class Sinus4 extends  Funktion3{
    int n;//Scharparameter
    Sinus4(double a, double b,int n){
	super(a,b);
	this.n=n;
    }
   double getY(double x){
      return Math.sin(n*x);
   }//Ende getY()
}//Ende class Sinus4


class AppF4{
  static void main(String[] args){
    int n=3;
    Sinus4 sinus = new Sinus4(0,5,n);
    int N=10;
    double[] y=sinus.getWertetabelle(N);
    double[] x=new double[N+1];

    for (int i=0;i<N+1;i++) {
      x[i]=sinus.xMin+(sinus.xMax-sinus.xMin)*i/N;
      System.out.println(
        "x["+i+"]="+x[i]+" y["+i+"]="+y[i]);
    }//Ende for
  }//Ende main()
}//Ende AppF4
Diese kleine Erweiterung ermöglicht, mittels des Konstruktors eine Funktion der Funktionenschar f(x)=sin(n*x) auszuwählen. Man sagt, dass der Konstruktor der Oberklasse in der Unterklasseüberlagert wird.

Entsprechend kann man auch Methoden der Oberklasse in einer Unterklasse überlagern (nicht zu verwechseln mit dem Begriff überladen innerhalb einer Klasse): Wir nehmen hierzu in die abstrakte Oberklasse Funktion eine neue Methode getAbleitung() auf, die die Ableitung mit Hilfe eines Differenzenquotienten annähert. Wenn in einem konkreten Fall einer Funktion die Ableitung analytisch zur Verfügung steht, kann man diese in der Unterklasse mit einer gleichlautenden Methode die der Oberklasse überlagern und sie hiermit "auszuschalten":


abstract class Funktion5{
    double xMin, xMax;
    Funktion5(double xMin, double xMax){
	this.xMin=xMin; this.xMax=xMax;
    }
    abstract double getY(double x);
    double[] getWertetabelle(int N){
      double[] y=new double[N+1];
      for (int i=0;i<N+1;i++) 
        y[i]=getY(xMin+i*(xMax-xMin)/N);
      return y;
   }//Ende getWertetabelle()
    double getAbleitung(double x){
	double h=0.0001;
	return (getY(x+h)-getY(x))/h;//Differenzenquotient
    }//Ende getAbleitung()
}//Ende class Funktion5

class Sinus5a extends Funktion5{
    //getAbleitung() wird nicht überlagert
    int n;
    Sinus5a(double a, double b,int n){
	super(a,b);
	this.n=n;
    }
   double getY(double x){
      return Math.sin(n*x);
   }//Ende getY()
}//Ende class Sinus5a

class Sinus5b extends  Funktion5{
    int n;
    Sinus5b(double a, double b,int n){
	super(a,b);
	this.n=n;
    }
   double getY(double x){
      return Math.sin(n*x);
   }//Ende getY()
  //Überlagerung:
   double getAbleitung(double x){
	return n*Math.cos(n*x);
    }
}//Ende class Sinus5b

class AppF5{
  static void main(String[] args){
    //getAbleitung() wird nicht überlagert:
    Sinus5a sinus5a = new Sinus5a(0,5,3);
    //getAbleitung() wird  überlagert:
    Sinus5b sinus5b = new Sinus5b(0,5,3);
    System.out.println(
      "Näherung: "+ sinus5a.getAbleitung(1.1));
    System.out.println(
      "exakt: "+ sinus5b.getAbleitung(1.1));
  }//Ende main()
}//Ende AppF5

7.3 Zugriffsrechte (Sichtbarkeit) Teil 2

protected

In allen Beispielklassen dieses Abschnitts werden keine Zugriffsrechte festgelegt, d.h. es gilt grundsätzlich Sichtbarkeit aller Daten und Methoden. Dies gilt als programmiertechnisch unfein. Bevorzugt wird, dass alle Daten nicht öffentlich sind, durch einen öffentlichen Konstruktor übergeben werden und höchstens durch eine öffentliche Methode (z.B. setDaten()) neu verändert werden können. Das Lesen von Daten könnte ebenfalls durch eine Methode (z.B. getDaten()) ermöglicht werden.

Werden die Daten nun als private vereinbart, kann auf diese auch nicht in Unterklassen zugegriffen werden. Hier schafft der Zusatz protected Abhilfe. Dieser Zusatz erlaubt den Zugriff auch in allen Klassen desselben Verzeichnisses.

Richtig "bezahlt" macht sich die Verwendung unterschiedlicher Zugriffsrechte erst, wenn ganze Pakete (wie z.B. Javas Grafikpakete) geschrieben werden. Der Benutzer braucht sich dann nur um die öffentlichen Klassen, Daten und Methoden kümmern. Ein gutes Beispiel sind die am Fachbereich Mathematik geschriebenen Black-Box-Programme, die Sie zum Zeichnen von Funktionsgraphen verwenden werden. Hier finden Sie eine Übersicht über alle öffentlichen Klassen, Methoden und Daten dieser Black Box.


Weiter mit 8. Zeichnen von (mehreren) Funktionsgraphen