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:
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 viertenabstract 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
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):
Achten Sie darauf, dass die Methodeabstract 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
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:
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: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
Diese kleine Erweiterung ermöglicht, mittels des Konstruktors eine Funktion der Funktionenscharclass 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
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.