§1 Einführung in die objektorientierte Programmierung

«


« Zeichnen im Fenster

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:

« Daten in Feldern

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.

« Daten in einer eigenen Klasse

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.

« Konstruktoren

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.

« Über Klassen und Objekte

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

« Die Klasse "Rechteck"

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:

« Die "paint"-Methode

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 );

                }

        }

« Vererbung

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: