4. Objektorientierung in Java

 
4.1 Klassen
4.1.1 Definition von Klassen
Seitenfuß  Eigenschaften 
Klassen  Seitenkopf 


Java ist streng objektorientiert. Darum wird verlangt, dass der gesamte Quellcode in Klassen steht. Aus diesem Grund kamen bereits im vorangegangenen Kapitel Klassendefinitionen vor, die jetzt näher erläutert werden sollen.
Eine Klassendefinition wird durch das Schlüsselwort class, gefolgt vom Namen der Klasse, eingeleitet. Darauf folgen geschweifte Klammern, in welchen der gesamte Quellcode der Klasse stehen muss:
Punkt1.java
1: //Punkt1
2: class Punkt1
3: {
4:   //Eigenschaften 
5:   //Konstruktor(en) 
6:   //Methoden
7: }
Wie zu erkennen ist, heißt die Klasse Punkt1 und hat noch keine Funktionalität.
Die angedeuteten Bestandteile eines Javaprogrammes werden in den nächsten Kapiteln nacheinander erläutert.


4.1.2 Eigenschaften
Seitenfuß  Konstruktoren 
Definition von Klassen  Seitenkopf 


allgemein
Eigenschaften sind alle Dinge die ein Objekt charakterisieren. Dementsprechend wären das beim Punkt ein x-Wert und ein y-Wert:
Punkt2.java
1: //Punkt2
2: class Punkt2
3: {
4:   private double x;
5:   private double y;
6:
7:   //Konstruktor(en) 
8:   //Methoden
9: }
x und y werden als gebrochene Zahlen deklariert, generell können jedoch für Eigenschaften alle Datentypen verwendet werden, also sowohl Zahlen und Zeichen als auch andere Objekte. Außerdem ist es möglich, Eigenschaften mit einem Standardwert zu initialisieren.
 
Modifier
Bei dem Schlüsselwort private spricht man von einem Modifier. Mit Modifiern können verschiedene Eigenschaften von Variablen festgelegt werden. Die zunächst wichtigste Möglichkeit ist es, die Sichtbarkeit mit Hilfe von Sichtbarkeitsmodifiern zu definieren. Unter Sichtbarkeit wird dabei verstanden von wem die Eigenschaft gesehen und verändert werden darf. Wichtig ist zunächst:
private-Variablen sind nur in der aktuellen Klasse sichtbar. Methoden der aktuellen Klasse dürfen auf sie zugreifen und Veränderungen vornehmen, Methoden fremder Klassen jedoch nicht. Vorteilhaft ist dies wenn Daten nicht von außerhalb geändert werden sollen, sie also sehr stark gekapselt sein müssen. Variablen mit dem Attribut private sind in abgeleiteten Klassen nicht sichtbar.
protected-Variablen haben die selben Eigenschaften wie private-Variablen, mit der Ausnahme, dass sie für abgeleitete Klassen sichtbar sind.
public-Variablen sind überall sichtbar. Jede Klasse darf auf sie zugreifen. Sinnvoll ist dies beispielsweise bei Konstanten. Diese müssen so nur in einer Klasse untergebracht werden, können aber von jeder Klasse gelesen werden.
Die Modifier können so auch auf Methoden angewendet werden.
Für den Punkt bedeutet dies nun Folgendes: Da seine Koordinaten mit private deklariert sind, kann nur er auf sie zugreifen und sie bei Bedarf verändern. Daraus folgt, dass er die Koordinaten mithilfe eines Konstruktors und der Methoden für andere Klassen nutzbar machen muss.


4.1.3 Konstruktoren
Seitenfuß  Methoden 
Eigenschaften  Seitenkopf 


allgemein
Die Membervariablen sollen bei der Objekterzeugung von Konstruktoren mit Startwerten belegt werden. Konstruktoren erhalten den Namen der Klasse, und sind meist als public deklariert, da sie von anderen Klassen aufgerufen werden sollen:
Punkt3.java
1: //Punkt3
2: class Punkt3
3: {
4:   //Eigenschaften
5:   private double x;
6:   private double y;
7:
8:   //Konstruktor
9:   public Punkt3(double x, double y)
10:   {
11:     this.x=x;
12:     this.y=y;
13:   }
14:
15:   //Methoden
16: }
 
Parameter
Es ist zu erkennen, dass die Modifier, wie bei den Variablen, vor dem Namen des Konstruktors stehen. Nach dem Namen folgen jedoch Klammern in denen Parameter stehen können. Mit Parametern sollen dem Konstruktor Werte übergeben werden, die er zur Erledigung seiner Aufgabe benötigt.
Ein Parameter besteht jeweils aus dem Typ des Wertes, der übergeben werden soll, und einem Namen. Somit kann man sich Parameter als Variablendeklarationen vorstellen. Die Sichtbarkeit der Variablen ist dabei festgelegt, und auf den Konstruktor beschränkt.
Bei Aufruf des Konstruktors werden diese Variablen dann mit den übergebenen Werten belegt und im Anschluss der Quellcode innerhalb der geschweiften Klammern ausgeführt.
 
this
Da beim Punkt sowohl die Parameter des Konstruktors, als auch die Membervariablen vom selben Typ sind, können die per Parameter übergebenen Werte den Membervariablen zugewiesen werden. Allerdings gibt es ein Problem: Wie später bei den Methoden, können die im Konstruktor deklarierten Variablen die Membervariablen überdecken. Dies ist der Fall, wenn beide den selben Namen besitzen. Bei einer Zuweisung wird dann immer die Variable verwendet, die die geringere Sichtbarkeit besitzt. Somit würde in diesem Fall mit einer Zuweisung an x, nicht die Eigenschaft x geändert werden, sondern die Variable x des Konstruktors, da diese ja die geringere Sichtbarkeit besitzt.
Um nun auf die Eigenschaft x zuzugreifen, muss das Schlüsselwort this mit anschliessendem Punkt verwendet werden. Das Schlüsselwort this zeigt immer auf das aktuelle Objekt, hier also auf eine Instanz des Punktes.
 
Punktoperator
Mit dem Punktoperator kann auf Eigenschaften und Methoden, des vor dem Punkt stehenden Objektes, zugegriffen werden. Besitzt ein Objekt A eine Eigenschaft b so kann mittels A.b auf die Eigenschaft zugegriffen werden. Demnach bedeutet this.x, dass auf die Variable x des Objektes this zugegriffen werden soll. Da nun aber this immer auf das aktuelle Objekt zeigt, wird auf die Membervariable x zugegriffen.


4.1.4 Methoden
Seitenfuß  Erzeugung von Objekten 
Konstruktoren  Seitenkopf 


allgemein
Da Konstruktoren im Grunde nur spezielle, der Erzeugung von Objekten dienende Methoden sind, trifft ein Großteil der zu Konstruktoren gemachten Aussagen auch auf Methoden zu. So werden Parameter genauso übergeben, und das Schlüsselwort this funktioniert in der selben Weise.
Methoden haben jedoch noch weitergehende Aufgaben. Sie sollen der Klasse Funktionalität und Mittel zur Kommunikation mit anderen Klassen verleihen, was für den Punkt nun Folgendes bedeutet: Es muss die Möglichkeit vorhanden sein, durch Methoden die Koordinaten zu ändern oder einfach nur abzufragen:
Punkt.java
1: //Punkt
2: class Punkt
3: {
4:   //Eigenschaften
5:   private double x;
6:   private double y;
7:
8:   //Konstruktor
9:   public Punkt(double x, double y)
10:   {
11:     this.x=x;
12:     this.y=y;
13:   }
14:
15:   //Methoden
16:   //setter
17:   public void setX(double x)
18:   {
19:     this.x=x;
20:   }
21:
22:   public void setY(double y)
23:   {
24:     this.y=y;
25:   }
26:
27:   //getter
28:   public double getX()
29:   {
30:     return x;
31:   }
32:
33:   public double getY()
34:   {
35:     return y;
36:   }
37: }
 
Rückgabewerte
Wie bei den Konstruktoren wird auch bei Methoden die Definition durch einen Sichtbarkeitsmodifier eingeleitet. Danach folgt jedoch eine Angabe über den Typ des Rückgabewertes. Dabei sind zwei grundlegende Möglichkeiten zu unterscheiden: Die Methode hat keinen Rückgabewert, oder sie hat einen Rückgabewert. Mehrere Rückgabewerte sind, wie in den meisten Sprachen, auch in Java nicht möglich.
Soll die Methode keinen Rückgabewert haben, so wird das Schlüsselwort void verwendet. Dies ist meist der Fall, wenn durch die Methode innerhalb des Objektes eine Eigenschaft geändert wird. Für Methoden die nur einer bestimmten Eigenschaft einen übergebenen Parameter zuweisen, wird meistens der Name "setEigenschaft(Parameter)" verwendet.
Soll die Methode einen Rückgabewert haben, so wird nach dem einleitenden Sichtbarkeitsmodifier, der Typ des Rückgabewertes notiert. Weiterhin muss am Ende der Methode das Schlüsselwort return, gefolgt vom zurückzugebenden Wert stehen. Methoden die eine bestimmte Membervariable zurückgeben sollen, erhalten meist die Bezeichnung "getEigenschaft()".


4.2 Erzeugung von Objekten
Seitenfuß  Interfaces 
Methoden  Seitenkopf 


Die Klasse Punkt ist jetzt vollständig. Sie besitzt die beiden Attribute x und y, die beim Erstellen eines Punktes durch den Konstruktor gesetzt werden. Außerdem sind Methoden zur Änderung der Koordinaten und zur Abfrage der Koordinaten vorhanden. Bevor die Klasse verwendet werden kann, muss sie noch getestet werden. Das Testen kann entweder durch Hinzufügen von Quellcode in die Klasse Punkt, oder durch das Erstellen einer eigenen Klasse zum Testen erfolgen. Hier ist es günstiger den Testcode in eine eigene Klasse zu verlagern, da die Klasse Punkt dann nicht mehr neu kompiliert werden muss. Der Testcode könnte beispielsweise folgende Form haben:
Test1.java
1: class Test1
2: {
3:   public static void main(String[] args)
4:   {
5:     //erstellen der Punkte
6:     Punkt p;
7:     p=new Punkt(10.3,12.4);
8:     Punkt p1=new Punkt(-1.3,3.4);
9:     //verändern der Werte
10:     p1.setY(-3.3);
11:     p.setX(p1.getY());
12:     //Ausgabe der Koordinaten
13:     String tmp=" p("+p.getX()+"|"+p.getY()+")";
14:     System.out.println(tmp);
15:     tmp="p1("+p1.getX()+"|"+p1.getY()+")";
16:     System.out.println(tmp);
17:   }
18: }
 
main
Soll in Java ein Programm ausgeführt werden, so sucht Java nach einer Methode mit der Signatur "public static void main(String[] args)". Sofern diese vorhanden ist, wird sie aufgerufen und ausgeführt.
Da die Testklasse ausgeführt werden soll, enthält sie eine Methode mit der angegebenen Signatur.
 
Objekterzeugung
In dieser werden Objekte der Klasse Punkt erstellt. Dazu werden zunächst Variablen des entsprechenden Typs deklariert. Danach werden diesen Variablen Objekte dieses Typs, die mit dem new-Operator erstellt wurden, zugewiesen. Wie bei den einfachen Datentypen können auch hier Deklaration und Initialisierung zusammen gelegt werden.
Der new-Operator ruft den Konstruktor der angegebenen Klasse auf. Ist der Konstruktor parametrisiert, so müssen dem Namen der Klasse runde Klammern mit den Parametern folgen. Da der Punktkonstruktor zwei double Werte als Paramter erwartet, müssen diese übergeben werden.
Nach der Objekterzeugung können auf die Objekte der Klasse Punkt die Methoden ausgeführt werden.
Die Test- und die Punktklasse sollten nun kompiliert und anschließend ausgeführt werden. Erfolgt beides ohne Fehler, ist das Testen abgeschlossen.


4.3 Interfaces
Seitenfuß  Vererbung 
Erzeugung von Objekten  Seitenkopf 


Doch wozu können Punkte verwendet werden? Jedes geometrische Objekt besteht aus Punkten. Mithilfe der Klasse Punkt könnten nun verschiedene Flächen definiert werden. Ein Dreieck könnte so durch drei Objekte des Typs Punkt dargestellt werden. Ein Kreis durch einen Punkt und eine Längenangabe. Diese geometrischen Objekte könnten dann grafisch dargestellt werden. Weiterhin könnte man an ihnen verschiedene Berechnungen durchführen.
Um Berechnungen für alle diese Objekte gleichermaßen vornehmen zu können, müsste überlegt werden, was alle (ebenen) geometrischen Objekte gemeinsam haben. Was haben also ein Kreis und ein Dreieck gemeinsam? Es gibt zumindest zwei Dinge: Beide haben einen bestimmten Flächeninhalt und einen Umfang. Diese errechnen sich jedoch jeweils verschieden. Es wäre jedoch möglich in zwei Klassen, Kreis und Dreieck, die Methoden zum Zurückgeben des Umfangs und des Flächeninhalts, unterzubringen. Dasselbe gilt für alle weiteren Flächen.
Bei solchen, doch recht verschiedenen Klassen, die Methoden für den selben Zweck besitzen, diesen jedoch unterschiedlich erreichen, können in Java Interfaces benutzt werden. Das Wort Interface kommt aus dem Englischen und bedeutet soviel wie Schnittstelle. Das ist es dann auch: Ein Interface definiert die Methoden, die verschiedene Klassen gemeinsam haben, also quasi die Schnittmenge der Methoden verschiedener Klassen.
 
Definition
Eine Interfacedefinition könnte nun folgendermaßen aussehen:
1: //GeometrischesObjekt
2: interface GeometrischesObjekt
3: {
4:   public double getUmfang();
5:   public double getFlaeche();
6: }
Das Schlüsselwort class wird durch interface ersetzt, was zeigt, dass es sich um eine Interfacedefinition handelt.
Innerhalb der Interfacedefinition stehen jeweils nur die Methodennamen mit Parametern und Rückgabewerten. Der Anweisungsblock wird durch ein Semikolon ersetzt. Bei solchen Methodenköpfen ohne Methodenrumpf spricht man von abstrakten Methoden.
Um das Interface zu kompilieren, wird genauso verfahren wie bei normalen Klassen.
 
Implementierung
Klassen, die dieses Interface verwenden wollen, müssen alle im Interface enthaltenen Methoden definieren, und die Methodenrümpfe ausfüllen. Dies wird als implementieren der Methoden bezeichnet. Um zu zeigen, dass eine Klasse ein Interface verwendet, wird hinter den Namen der Klasse das Schlüsselwort implements mit dem Namen des verwendeten Interfaces angehängt.
Für Dreiecke könnte dies so aussehen:
Dreieck.java
1: //Dreieck
2: class Dreieck
3: implements GeometrischesObjekt
4: {
5:   private Punkt a;
6:   private Punkt b;
7:   private Punkt c;
8:
9:   public Dreieck(Punkt a, Punkt b, Punkt c)
10:   {
11:     this.a=a;
12:     this.b=b;
13:     this.c=c;
14:   }
15:
16:   public double getFlaeche()
17:   {
18:     //Fläche eines Dreieckes
19:     double tmp=a.getX()*b.getY()
20:                +b.getX()*c.getY()
21:                +c.getX()*a.getY()
22:               -(a.getY()*b.getX()
23:                +b.getY()*c.getX()
24:                +c.getY()*a.getX());
25:     return 0.5*tmp;
26:   }
27:
28:   public double getUmfang()
29:   {
30:     //Umfang eines Dreieckes
31:     return Math.sqrt(
32:              Math.pow(a.getX()-b.getX(),2.0)
33:              +Math.pow(a.getY()-b.getY(),2.0))
34:            +(Math.sqrt(
35:              Math.pow(b.getX()-c.getX(),2.0)
36:              +Math.pow(b.getY()-c.getY(),2.0)))
37:            +(Math.sqrt(
38:              Math.pow(c.getX()-a.getX(),2.0)
39:              +Math.pow(c.getY()-a.getY(),2.0)));
40:   }
41: }
Der Flächeninhalt und der Umfang werden nach den üblichen Formeln für das Dreieck(bei drei gegebenen Punkten) ausgerechnet.
Für Kreise würden die Methoden dagegen vollkommen anders implementiert werden:
Kreis.java
1: //Kreis
2: class Kreis
3: implements GeometrischesObjekt
4: {
5:   Punkt m;
6:   double r;
7:
8:   public Kreis(Punkt m, double r)
9:   {
10:     this.m=m;
11:     this.r=r;
12:   }
13:
14:   public double getFlaeche()
15:   {
16:     //Fläche eines Kreises
17:     return Math.PI*r*r;
18:   }
19:
20:   public double getUmfang()
21:   {
22:     //Umfang eines Kreises
23:     return 2*Math.PI*r;
24:   }
25: }
 
Sinn und Zweck
Nun können Dreiecke und Kreise (und alle weiteren Klassen die das Interface GeometrischesObjekt implementieren) in Bezug auf Fläche und Umfang gleich behandelt werden. Durch das Interface wird von der eigentlichen Flächeninhalts- bzw. Umfangsberechnung abstrahiert.
Angewendet werden kann dies in folgender Weise:
Test2.java
1: //Test2
2: class Test2
3: {
4:   public static void main(String[] args)
5:   {
6:     GeometrischesObjekt d,k;
7:     d=new Dreieck(new Punkt(2.0,1.0),
8:                   new Punkt(6.0,3.0),
9:                   new Punkt(4.0,7.0));
10:     k=new Kreis(new Punkt(0.0,0.0), 3);
11:     GeometrischesObjekt[] go={d,k};
12:     for(int i=0; i<go.length; i++)
13:     {
14:       System.out.println((i+1)+".Objekt");
15:       System.out.println("  Flaeche: "
16:                          +go[i].getFlaeche());
17:       System.out.println("  Umfang:  "
18:                          +go[i].getUmfang());
19:     }
20:   }
21: }
Zunächst werden zwei Variablen des Typs GeometrischesObjekt deklariert. Der einen wird ein Dreieck zugewiesen, der anderen ein Kreis. Dies ist möglich, weil Kreis und Dreieck das Interface GeometrischesObjekt implementieren. Dann werden beide in ein Array eingefügt und nacheinander durchlaufen, wobei die Methoden "getFlaeche()" und "getUmfang()" aufgerufen werden. Obwohl jeweils die selben Methoden aufgerufen werden, wird der Flächeninhalt und Umfang einmal für das Dreieck und einmal für den Kreis berechnet.
Diese Abstraktion entspricht dem Sinn von Schnittstellen.


4.4 Vererbung
Seitenfuß 
Vererbung
Interfaces  Seitenkopf 


Diesem System können beliebige ebene Objekte hinzugefügt werden. Doch was geschieht, wenn es auf dreidimensionale Objekte erweitert werden soll? Mit zweidimensionalen Punkten lassen sich schlecht dreidimensionlae Objekte kreieren. Nun gibt es zwei Möglichkeiten: Entweder man schreibt eine neue Klasse für 3D-Punkte oder man erweitert die bestehenede Klasse Punkt um eine Dimension. Die Wahl fällt einfach, warum sollte man noch einmal von vorn anfangen?
Die zweidimensionalen Punkte sollen demnach erweitert werden. In Java können solche Erweiterungen leicht durch Vererbung realisiert werden. Die Ursprungsklasse wird als Superklasse bezeichnet, und gibt ihre gesamten Eigenschaften und Methoden an die erbende Klasse weiter.
Soll eine Klasse von einer anderen erben, so schreibt man hinter ihren Namen das Schlüsselwort extends und den Namen der Superklasse.
Da der 3D-Punkt von Punkt erbt, sieht das dann so aus:
DreiDPunkt.java
1: //DreiDPunkt
2: class DreiDPunkt
3: extends Punkt
4: {
5:   double z;
6:
7:   public DreiDPunkt(double x, double y, double z)
8:   {
9:     super(x,y);
10:     this.z=z;
11:   }
12:
13:   public void setZ(double z)
14:   {
15:     this.z=z;
16:   }
17:
18:   public double getZ()
19:   {
20:     return z;
21:   }
22: }
In der 1. Zeile des Konstruktors der abgeleiteten Klasse besteht die Möglichkeit mit "super(Parameter)" den Konstruktor der Superklasse aufzurufen. Nachdem dieser abgearbeitet wurde, wird der restliche Konstruktor der abgeleiteten Klasse ausgeführt.
Da x und y schon im Punktkonstruktor initialisiert werden, braucht der DreiDPunktkonstruktor nur noch z initialisieren. Das Gleiche gilt für die entsprechenden Methoden: Da die getter und setter für x und y vererbt wurden, brauchen sie nicht neu definiert werden.


Für den 3D-Punkt können nun, genauso wie für den einfachen Punkt, geometrische Objekte definiert werden. Jetzt jedoch in 3D. Für diese können dann beispielsweise Oberflächeninhalt und Volumen berechnet werden. Doch diese Aufgabe soll dem Leser überlassen bleiben!


Viel Erfolg, und vor allen Dingen Spaß!



©2002 by Th. Zeume