Der folgende Quelltext zeigt einen einfachen Ansatz, drei Ostereier in einem Fenster zu zeichnen.
import java.awt.*;
import java.awt.event.*;
class Oo01 extends Frame {
public Oo01() {
addWindowListener(new WindowAdapter() {
public void windowClosing(WindowEvent e) {
dispose();
System.exit(0);
}
});
}
public static void main(String args[]) {
System.out.println("Starting Oo01...");
Oo01 mainFrame = new Oo01();
mainFrame.setSize(400, 400);
mainFrame.setTitle("Oo01");
mainFrame.setVisible(true);
}
public void paint( Graphics g ){
g.setColor( Color.red );
g.fillOval( 100, 100, 20, 30 );
g.setColor( Color.blue );
g.fillOval( 100, 120, 40, 60 );
g.setColor( Color.yellow );
g.fillOval( 200, 100, 20, 30 );
}
Erklärung des Quelltextes:
public Oo01() {
addWindowListener(new WindowAdapter() {
public void windowClosing(WindowEvent e) {
dispose();
System.exit(0);
}
});
}
Dieser Teil dient dazu, das das Programm beendet wird, wenn im Grafikfenser auf das "Kreuz" geklickt wird. Die Einzelheiten lassen sich erst im weiteren Verlauf des Kurses klären.
public static void main(String args[]) {
System.out.println("Starting Oo01..."); // Ausgabe im Textfenster
Oo01 mainFrame = new Oo01(); // neues Grafikfenster
mainFrame.setSize(400, 400); // Größe auf 400, 400
mainFrame.setTitle("Oo01"); // Titel auf Oo01
mainFrame.setVisible(true); // sichtbar machen
}
Diese Zeilen (also die Routine main) wird als erstes vom Java-Interpreter aufgerufen. Deshalb erfolgt zuerst eine Ausgabe in das Textfenster und anschließend wird ein Grafikfenster geöffnet und sichtbar gemacht.
public void paint( Graphics g ){
g.setColor( Color.red ); // Stiftfarbe auf rot setzen
g.fillOval( 100, 100, 20, 30 ); // Osterei zeichnen
g.setColor( Color.blue ); // Stiftfarbe auf blau setzen
g.fillOval( 100, 120, 20, 30 ); // Osterei zeichnen
g.setColor( Color.yellow ); // Stiftfarbe auf gelb setzen
g.fillOval( 200, 100, 20, 30 ); // Osterei zeichnen
}
Dieser Code wird aufgerufen, wenn das Fenster gezeichnet werden soll. Hier werden die Ostereier gezeichnet.
Aus Sicht des Informatikers hat diese Codierung der "Ostereierzeichnung" folgende Nachteile:
- Redundanz: Um ein neues "Ei" zu zeichnen, müssen jeweils die zwei Befehle g.setColor und g.fillOval angefügt werden. Hart kodierte Wiederholungen im Quelltext sind aber zu vermeiden: Sie nehmen unnötig Platz im Speicher weg und sind unübersichtlich in der Programmstruktur.
- Schlechte Erweiterbarkeit: Um weitere Eier zu zeichnen, muss der Code geändert werden. Da es sich im wesentlichen aber um ein Feld von "Eiern" handelt, sollte die Erweiterung allein auf der Datenebene möglich sein. Dazu wäre dann nur noch das Ändern einer Datei nötig.
Ein weiterer Ansatz wäre also die folgende Variante für die Routine paint:
public void paint( Graphics g ){
Color col[] = { Color.red, Color.blue, Color.yellow }; // Feld mit den Farben für die drei Eier
int x[] = { 100, 100, 200 }; // Feld mit den x-Werten
int y[] = { 100, 120, 100 }; // Feld mit den y-Werten
int w[] = { 20, 40, 20 }; // Feld mit den Breiten
int h[] = { 30, 60, 30 }; // Feld mit den Höhen
for (int i=0; i < col.length; i++){ // Schleife über alle drei Eier
g.setColor( col[i] ); // jeweilige Farbe setzen
g.fillOval( x[i], y[i], w[i], h[i] ); // jeweiliges Ei zeichnen
}
}
Jetzt tauchen die Befehle g.setColor und g.fillOval nur noch einmal auf, die Struktur basiert auf der Länge der Daten in den einzelnen Feldern und deshalb auch leichter erweiterbar.
Aber auch dieser Ansatz ist nicht perfekt: Um ein neues Element hinzuzufügen, müssen sie jedes der einzelnen Felder col, x, y, w, und h um eine weitere Zahl ergänzen.
Bei einer größeren Zahl von "Eiern" vergißt man leicht mal einen x-Wert oder eine Farbe, denn ihre Zugehörigkeit wird nur aus der Position im Feld klar. Es fehlt eine Zusammenfassung der zusammengehörigen Daten in eine eigene Einheit:
public void paint( Graphics g ){
class Oval{
int x, y, w, h;
Color col;
};
Oval o[] = new Oval[3];
o[0] = new Oval();
o[0].x = 100; o[0].y = 100; o[0].w = 20; o[0].h = 30; o[0].col = Color.red;
o[1] = new Oval();
o[1].x = 100; o[1].y = 120; o[1].w = 40; o[1].h = 60; o[1].col = Color.blue;
o[2] = new Oval();
o[2].x = 200; o[2].y = 100; o[2].w = 20; o[2].h = 30; o[2].col = Color.yellow;
for (int i=0; i < o.length; i++){
g.setColor( o[i].col );
g.fillOval( o[i].x, o[i].y, o[i].w, o[i].h );
}
}
In diesem Fall wird eine eigene Datenstruktur durch eine sogenannte "Klasse" definiert. In ihr sind alle Daten zusammengefasst, die ein Ei ausmachen. Um sie zu zeichnen wird ein Feld mit drei Elementen dieser Klasse erzeugt.
Zu beachten ist, dass jedes Objekt der Klasse Oval erst mit new erzeugt werden muss. Dabei wird jeweils in einem bestimmten Teil des Speichers (Heap) Platz reserviert, um die Variablen x, y, w, h und col aufzunehmen. Dies ist einer der Vorteile des objektorientierten Programmierens: Speicher wird nur dann reserviert, wenn er auch benötigt wird. Die Java-Laufzeitumgebung enthält übrigens einen sogenannten "garbage collector", der selbst erkennt, wann ein solches Objekt vom Programm nicht mehr benötigt wird, er gibt dann von sich aus den Speicher wieder frei.
Diese Lösung enthält keine Redundanz mehr im Zeichnen (alle Objekte werden in einer for-Schleife gezeichnet), sondern hauptsächlich noch in der Syntax der Initialisierung: Die Zuweisung an die einzelnen Variablen der Klasse müssen jedesmal einzeln ausgeführt werden.
Deshalb kann eine Klasse zusätzlich auch noch sogenannte Konstruktoren enthalten, das sind Unterprogramme, die beim Erzeugen der Klasse aufgerufen werden.
public void paint( Graphics g ){
class Oval{
int x, y, w, h;
Color col;
public Oval( int x, int y, int w, int h, Color c ){
this.col = c; this.x = x; this.y = y; this.w = w; this.h = h;
}
};
Oval o[] = new Oval[3];
o[0] = new Oval( 100, 100, 20, 30, Color.red );
o[1] = new Oval( 100, 120, 20, 30, Color.blue );
o[2] = new Oval( 200, 100, 20, 30, Color.yellow );
for (int i=0; i < o.length; i++){
g.setColor( o[i].col );
g.fillOval( o[i].x, o[i].y, o[i].w, o[i].h );
}
}
Mit der Unterprogrammdefinition:
public Oval( int x, int y, int w, int h, Color c ){
this.col = c; this.x = x; this.y = y; this.w = w; this.h = h;
}
Wird festgelegt, dass im new-Befehl (also während der Erzeugung des Objektes) bereits die Startwerte für die einzelnen Variablen der Klasse eingtragen werden. Diese Konvention macht den Quelltext nochmals ein ganzes Stück übersichtlicher.
An dieser Stelle ist es vielleicht ganz gut, einen Stopp zu machen und sich die neue Datenstruktur etwas genauer anzuschauen. Eine Klasse ist also eine Datenstruktur, die
- Attribute (Variablen)
- und Methoden (Unterprogramme)
enthalten kann. Wird ein Objekt dieser Klasse erzeugt, so lässt sich durch Angabe von
<Objektname>.<Eigenschaft> auf ein Attribut(Variable) oder eine Methode (Unterprogramm) zugreifen.
Beispiel:
color mycolor = o[2].col;
Hier wird der Variablen mycolor die Farbe des 2. Ostereis zugeordnet.
Um einen wichtigen Vorteil des objektorientierten Programmierens kennenzulernen werden wir jetzt die möglichen Figuren im Fenster um Rechtecke erweitern. Wir müssen also eine neue Klasse Rechteck definieren und können dann sowohl Ostereier als auch bunte Kisten auf das Fenster zeichnen.
Hier ein erster Ansatz:
class Oval{
int x, y, w, h;
Color col;
public Oval( int x, int y, int w, int h, Color c ){
this.col = c; this.x = x; this.y = y; this.w = w; this.h = h;
}
};
class Rechteck{
int x, y, w, h;
Color col;
public Rechteck( int x, int y, int w, int h, Color c ){
this.col = c; this.x = x; this.y = y; this.w = w; this.h = h;
}
}
Oval o[] = new Oval[3];
o[0] = new Oval( 100, 100, 20, 30, Color.red );
o[1] = new Oval( 100, 120, 20, 30, Color.blue );
o[2] = new Oval( 200, 100, 20, 30, Color.yellow );
Rechteck r[] = new Rechteck[3];
r[0] = new Rechteck( 50, 20, 20, 30, Color.orange );
r[1] = new Rechteck( 50, 80, 20, 30, Color.green );
r[2] = new Rechteck( 100, 300, 20, 30, Color.gray );
for (int i=0; i < o.length; i++){
g.setColor( o[i].col );
g.fillOval( o[i].x, o[i].y, o[i].w, o[i].h );
}
for (int i=0; i < r.length; i++){
g.setColor( r[i].col );
g.fillRect( r[i].x, r[i].y, r[i].w, r[i].h );
}
So gut, wie es zu sein scheint, neue Klassen hinzufügen zu können, so fallen doch einige Schwächen diesr Version auf:
- Wie ein Objekt gezeichnet werden soll wird immer noch in der Schleife angegeben, ein grafisches Objekt weiss also noch nicht selbst, wie es sich zeichnen soll.
- Obwohl Rechtecke als auch Ovale grafische Figuren sind, die vieles gemeinsam haben (x, y, w, h und Farbe; müssen gezeichnet werden können...) wird das an dieser Stelle im Quelltext noch nicht klar.
Zumindest der erste Punkt lässt sich leicht dadurch ändern, dass das Unterprogramm zum Zeichnen mit in die Klassendefinition hineingepackt wird:
public void paint( Graphics g ){
class Oval{
int x, y, w, h;
Color col;
public Oval( int x, int y, int w, int h, Color c ){
this.col = c; this.x = x; this.y = y; this.w = w; this.h = h;
}
public void paint( Graphics g ){
g.setColor( col );
g.fillOval( x, y, w, h );
}
};
class Rechteck{
int x, y, w, h;
Color col;
public Rechteck( int x, int y, int w, int h, Color c ){
this.col = c; this.x = x; this.y = y; this.w = w; this.h = h;
}
public void paint( Graphics g ){
g.setColor( col );
g.fillRect( x, y, w, h );
}
}
Oval o[] = new Oval[3];
o[0] = new Oval( 100, 100, 20, 30, Color.red );
o[1] = new Oval( 100, 120, 20, 30, Color.blue );
o[2] = new Oval( 200, 100, 20, 30, Color.yellow );
Rechteck r[] = new Rechteck[3];
r[0] = new Rechteck( 50, 20, 20, 30, Color.orange );
r[1] = new Rechteck( 50, 80, 20, 30, Color.green );
r[2] = new Rechteck( 100, 300, 20, 30, Color.gray );
for (int i=0; i < o.length; i++){
o[i].paint( g );
}
for (int i=0; i < r.length; i++){
r[i].paint( g );
}
}
Der neue Quelltext beseitigt immer noch nicht die Redundanzen der beiden Klassen "Oval" und "Rechteck". Daher gibt es in objektorientierten Programmiersprachen das Mittel der Vererbung. In unserem Fall definieren wir eine neue Klasse "Figur", die alle Eigenschaften hat, die ein grafisches Objekt wie "Oval" oder "Rechteck" braucht.
Nun werden "Oval" und "Rechteck" von abstrakt abgeleitet, das heißt so viel wie: "Ich habe hier eine Klasse Oval, die ist wie eine Klasse "Figur", nur mit folgenden Zusätzen...". Schauen wir uns den Quelltext dazu an:
abstract class Figur{
int x, y, w, h;
Color col;
public abstract void paint( Graphics g );
}
class Oval extends Figur{
public Oval( int x, int y, int w, int h, Color c ){
this.col = c; this.x = x; this.y = y; this.w = w; this.h = h;
}
public void paint( Graphics g ){
g.setColor( col );
g.fillOval( x, y, w, h );
}
};
class Rechteck extends Figur{
public Rechteck( int x, int y, int w, int h, Color c ){
this.col = c; this.x = x; this.y = y; this.w = w; this.h = h;
}
public void paint( Graphics g ){
g.setColor( col );
g.fillRect( x, y, w, h );
}
}
Hier passiert bemerkenswertes:
- In der Klassendefinition von "Oval" und "Rechteck" steht jetzt extends Figur, was bedeutet, dass diese Klassen jetzt von der Klasse "Figur" abgeleitet wurden (geerbt haben).
- In den beiden Klassen "Oval" und "Rechteck" sind die Attribute verschwunden. Dass sie nach wie vor Position, Ausmaße und Farbe besitzen, das steht ja im Vorfahren "Figur". Abgeleitete Klassen übernehmen also die Attribute ihrer Vorfahren.
- In "Figur" wird ein Unterprogramm "paint" definiert, allerdings enthält es keine Anweisungen und ist deshalb als abstrakt deklariert. Das ergibt sich daher, dass die Klasse Figur ja alle Eigenschaften einer grafischen Figur vorgeben muss, auch solche, die in den Nachfahren unterschiedlich implementiert sind. Deshalb muss die Methode "paint" in "Oval" und "Rechteck" auch weiterhin (mit Anweisungen!) definiert sein. Abstrakte Klassen "zwingen" ihren vererbten Klassen sozusagen bestimmte Eigenschaften auf. (Das sind genau die Eigenschaften, die diese abstrakte Struktur ausmachen: Eine grafische Figur die nicht gezeichnet werden kann, wird ihrem Namen wohl nicht ganz gerecht.)