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