Die ersten (Programmier-)Schritte

Bekanntlich ist aller Anfang schwer. Deshalb soll hier eine kleine Hilfe gegeben werden, die den Start mit der Entwicklung erleichtern soll.

Einführung in die objektorientierte Programmierung

"Wenn man heute einen Computer kauft, ist er morgen schon veraltet". Kaum ein anderes Gerücht hält sich so stark in der Informatik, wie das über die Kurzlebigkeit. Jedoch gibt es gerade im Bereich der Softwareentwicklung Programmiertechniken und -konzepte, die seit Jahrzehnten nichts von ihrer Aktualität eingebüßt haben. Ein Beispiel dafür ist die Objektorientierung. Sie wurde bereits Mitte der 80er Jahre entwickelt und ist auch heute noch das Grundkonzept moderner Programmiersprachen.

Idee der Objektorientierung

Die Idee der Objektorientierung ist, die Daten und die Funktionen, die auf diese Daten zugreifen, in einer Komponente zu bündeln. Auf die Daten kann man nur über die entsprechenden Funktionen (die man Methoden nennt) zugreifen.

Vorgehensweise ohne Objektorientierung

Möchte man zum Beispiel ein Konto ohne Objektorientierung schreiben, so braucht man eine Integer-Variable int kontostand = 0;, die Auskunft über den verfügbaren Saldo (z.B. in Cent) gibt. Ein- und Auszahlungen lassen sich durch direkte Wertzuweisungen vornehmen: kontostand = kontostand + 30;. Soweit sogut, aber wie werden Auszahlungen gehandhabt? Eine Auszahlung soll nur möglich sein, wenn das Konto ausreichend gedeckt ist:

// Beispiel um 40 Geldeinheiten abzuheben
if (kontostand >= 40) {
    kontostand = kontostand - 40;
}

Jedes Mal, wenn Geld abgehoben werden soll, muss erst überprüft werden, ob das Konto ausreichend gedeckt ist. Sollte man die if-Abfrage nur an einer Stelle vergessen, läuft man Gefahr, dass der Saldo negativ wird, was nicht möglich sein soll.

Es gibt aber noch weitere Probleme: Durch kontostand = kontostand + (-25); kann man quasi durch Einzahlung eines negativen Betrages den Saldo ins Negative treiben. Dies ist in zweifacher Weise kritisch, da zum einen der Kontostand negativ werden kann und es zum anderen keine negativen Einzahlungen geben soll. Also müssen auch Einzahlungen auf ihre Gültigkeit überprüft werden.

Ein weiteres Problem könnte z.B. entstehen, wenn man dem Kontoinhaber einen Dispokredit einräumen will. Dann muss man in jeder if-Abfrage den Dispobetrag hinzufügen. Sollte das an einer Stelle vergessen werden, kann man unter Umständen nichts abheben, obwohl der Dispo noch nicht voll genutzt ist.

Objektorientiert arbeiten

Möchte man ein Konto objektorientiert darstellen, schreibt man zunächst eine Klasse, die eine Art Bauplan ist:

public class Konto {
    private int kontostand;

    public Konto() {
        kontostand = 0;
    }

    public void einzahlen(int betrag) {
        if (betrag > 0) {
            kontostand = kontostand + betrag;
        }
    }

    public boolean abheben(int betrag) {
        if (kontostand >= betrag) {
            kontostand = kontostand - betrag;
            return true;
        }
        return false;
    }

    public int getKontostand() {
        return kontostand;
    }
}

Das Wort private vor der Variablen für den Kontostand bedeutet, dass der Zugriff darauf nur innerhalb der Klasse gestattet ist, während public Zugriff von überall erlaubt. Möchte man also Geld einzahlen oder abheben, muss man die entsprechenden Methoden nehmen.

Mit dem Schlüsselwort new kann man aus dem Bauplan (also der Klasse) ein Objekt erzeugen:

Konto konto1 = new Konto(); // Erzeugt ein Konto-Objekt und speichert es in der Variablen konto1
Konto konto2 = new Konto(); // Erzeugt noch ein Konto und speichert es in einer anderen Variablen

Das new führt den Konstruktor aus (public Konto() { kontostand = 0; }). Also hat jedes neu erstellte Konto zunächst Kontostand von 0 Geldeinheiten. Ein Konstruktor muss immer den Klassennamen tragen und darf keinen Rückgabewert (nicht einmal void) haben. Analog zu Methoden kann man auch einem Konstruktor Argumente übergeben.

Um Geld einzuzahlen oder abzuheben ruft man die Methoden mit dem sog. Punktoperator auf:

Konto konto1 = new Konto();            // konto: 0 Geldeinheiten (GE)
Konto konto2 = new Konto();            // konto2: 0 GE

konto1.einzahlen(100);                 // konto1: 100 GE, konto2: 0  GE
konto2.einzahlen(50);                  // konto1: 100 GE, konto2: 50 GE

boolean erfolg = konto1.auszahlen(30); // konto1: 70 GE, konto2: 50 GE, erfolg: true
erfolg = konto2.auszahlen(90);         // konto1: 70 GE, konto2: 50 GE, erfolg: false

Die Erweiterung der Klasse um den Dispokredit erweist sich auch als sehr einfach, da nur noch Änderungen in der Klasse notwendig sind:

class Konto {
     private int kontostand;
     private int dispo; (1)

     public Konto() {
         kontostand = 0;
         dispo = 500; (2)
     }

     public void einzahlen(int betrag) {
         if (betrag > 0) {
             kontostand = kontostand + betrag;
         }
     }

     public boolean abheben(int betrag) {
         if ((kontostand + dispo) >= betrag) { (3)
             kontostand = kontostand - betrag;
             return true;
         }
         return false;
     }

     public int getKontostand() {
         return kontostand;
     }
}
1 Neue private Variable um den Kreditrahmen zu speichern.
2 Kreditrahmen im Konstruktor initialisieren.
3 Kreditrahmen beim Abheben mit berücksichtigen.
Referenzierung von Objekten

Wenn man mit Konto konto3 = new Konto() ein neues Objekt erzeugt, wird dieses im Arbeitsspeicher abgelegt und die Variable konto3 enthält die Speicheradresse zum entsprechenden Objekt. Mit dem Befehl Konto konto4 = konto3 wird der Variablen konto4 die Speicheradresse von konto3 zugewiesen. Beide zeigen also auf dieselbe Speicheradresse und somit auf das gleiche Objekt. Somit verändert konto3.einzahlen(40) auch den Kontostand von konto4, weil beide auf dasselbe Objekt zeigen. Statt zeigen sagt man oft auch referenzieren.

Merkregel: Neue Objekte erzeugt man nur mit dem Schlüsselwort new!

Vererbung

Die Vererbung ist eine Technik, mit der man eine Klasse, durch hinzufügen von Methoden und Variablen, einen neuen Bauplan (Klasse) erzeugt.

Möchte man zum Beispiel zusätzlich auch noch ein Premiumkonto anbieten, auf dem der Kontostand verzinst wird, kann man die bestehende Klasse nehmen und entsprechend erweitern:

public class PremiumKonto extends Konto {
     private double zinsbetrag;

     public PremiumKonto() {
         super();
         zinsbetrag = 2.5d; // 2.5% Zinsen
     }

     public void zinsenGutschreiben() {
         int saldo = getKontostand();
         if (saldo > 0) {
             einzahlen(saldo * zinsbetrag / 100);
         }
     }
}

Die Methoden zum Ein- und Auszahlen brauchen nicht neu geschrieben werden, da diese von der Klasse Konto "kopiert" werden. Man kann eine Methode aus einer Oberklasse neu schreiben. Dann wird immer die geänderte Version genommen. Das Schlüsselwort super() ruft den Konstruktor aus der Kontoklasse auf. In Java wird immer der leere Konstruktor der Oberklasse aufgerufen, so dass diese Zeile auch weggelassen werden darf.

Ein neues Objekt erzeugt man auf die gleiche Weise, wie bei einem normalen Konto:

PremiumKonto premium = new PremiumKonto();

premium.einzahlen(50); //geerbte Methode
premium.zinsenGutschreiben();
Casting von Objekten

Da ein Premiumkonto auch ein normales Konto ist, ist der folgende Aufruf legal:

Konto konto5 = new PremiumKonto();

Weil konto5 vom Typ Konto ist, dürfen auch nur die Methoden aus dieser Klasse verwendet werden. Möchte man auch Zinsen gutschreiben können, so muss aus dem Konto ein Premiumkonto gemacht werden:

PremiumKonto konto6 = (PremiumKonto) konto5;

Dieser Cast gelingt jedoch nur, wenn das Konto auch ein Premiumkonto ist! Sonst wird eine Fehlermeldung geworfen. Mit dem Schlüsselwort instanceof kann man abfragen, ob ein Objekt zu einer gewissen Klasse gehört:

Konto konto7 = new PremiumKonto();

if (konto6 instanceof PremiumKonto) {
    Premiumkonto premium2 = (PremiumKonto) konto7;
    premium2.zinsenGutschreiben();
}

Wichtig: Es werden nur Methoden vererbt, jedoch keine Variablen! Deshalb wird auf den kontostand nur über die entsprechenden Methoden der Oberklasse zugegriffen.

Statische Variablen und Methoden

Gibt es Methoden oder Variablen, die für alle Objekte gültig sind, so werden diese als statisch (static) deklariert. Statische Variablen und Klassen werden von allen Objekten geteilt.

Soll zum Beispiel der Zinssatz beim Premiumkonto für alle Konten gleich sein, kann man diesen als statisch deklarieren:

public class PremiumKonto extends Konto {
     private static double zinsbetrag = 2.5d; // 2.5% Zinsen

     ...

     public static double getZinsbetrag() {
         return zinsbetrag;
     }

     public static void setZinsbetrag(double wert) {
         zinsbetrag = wert;
     }
 }

Von außen kommt man an den Zinsbetrag über die Methode setZinsbetrag(double wert), die man entweder über das Objekt oder über den Klassennamen aufrufen darf.

PremiumKonto.setZinsbetrag(3d); // Zinsen auf 3% erhöhen

PremiumKonto premium3 = new PremiumKonto();
premium3.setZinsbetrag(3d);

Tipp: Damit man besser erkennen kann, dass es sich um statische Variablen oder Methoden handelt, sollte man auf diese immer über den Klassennamen zugreifen.

Weitere Aspekte

Die Objektorientierung bietet noch viele weitere Aspekte, wie zum Beispiel die Polymorphie. Da es sich hier nur um eine Einführung handelt, wurden solche fortgeschrittenen Themen allerdings nicht behandelt.

Der saubere Programmierstil

Eventuell hat manche(r) schon erlebt, dass man von einer/m Bekannten ein Stück Programmcode bekommen hat, den man gar nicht versteht. Oft versteht man sogar nach einiger Zeit seine eigenen Codezeilen nicht mehr. Meistens liegt das gar nicht an den komplizierten Algorithmen, sondern an einen schlechten Programmierstil. Deshalb gibt es für alle Programmiersprachen sog. Style Guides, also Regeln für den Aufbau von Quelltexten.

Mit den Java Code Conventions stammt der bekannteste Style Guide für Java von seinen Entwicklern. Dieser soll hier ein wenig näher gebracht werden.

Allgemeiner Dokumentaufbau

  • Keine Zeile sollte länger als 80 Zeichen sein. Gerade in Zeiten großer Breitbildschirme ist das wohl eine der schwierigsten Regeln überhaupt. Man muss aber davon ausgehen, das nicht jeder, der den Code lesen will, auch einen ähnlich breiten Bildschirm hat. Außerdem ist meistens die Schriftgröße zum Drucken so eingestellt, dass höchstens 80 Zeichen in eine Zeile passen

  • In einer Klasse sollten immer als erstes die globalen Variablen, dann die Konstruktoren und als letztes die Methoden auftauchen

  • Klassennamen sollten im CamelCase geschrieben werden, also jedes Teilwort wird mit einem Großbuchstaben geschrieben (z.B. GameHandler)

  • Für Variablen- und Methodennamen wird der lower CamelCase verwendet, bei dem nur das erste Teilwort mit einem kleinen Buchstaben beginnt (z.B. eigenerSpieler)

  • Für Konstantenbezeichner werden ausschließlich große Buchstaben benutzt (z.B. int ANZAHL_SPIELER = 2;)

  • Jede Zeile sollte nur eine Anweisung enthalten.

Klammerungsregeln

Grundsätzlich sollte jeder Schleifenrumpf und jede if-Anweisung geklammert werden, auch wenn nur ein Befehl darin steht. Die öffnende Klammer wird dabei ans Ende der Zeile geschrieben (Kernighan&Ritchie-Stil, bzw. K&R-Style). Der Code im Rumpf wird dabei eingerückt. Die schließende Klammer befindet sich in der ersten Zeile, die nicht mehr eingerückt ist.

for (int i = 0; i < 110; i++) {
     x = x + 2;
     ...
}

Bei einem if schreibt man das zugehörige else direkt hinter die schließende Klammer:

if (bedingung) {
     ...
} else {
     ...
}

Mehrere geschachtelte if-Anweisungen werden zusammen geschmolzen:

if (bedingung) {
     ...
} else if (begingung2) {
     ...
} else if (bedingung3) {
     ...
} else {
     ...
}

Die Switch-Anweisung

Bei einem switch sollte die folgende Form gewahrt werden:

switch (variable) {
case 1 :
    ...
    /* fällt durch */
case 2 :
    ...
    break;
case 3 :
    ...
    break;
default :
    ...
    break;
}

Wenn nach einem case-Fall nicht aus dem switch herausgesprungen wird, soll das durch einen Kommentar gekennzeichnet werden. Wenn ein default benutzt wird, wird dieses als letzte Klausel geschrieben.

Weiterführende Informationen

Eine Idee implementieren

Man hat einige Spiele absolviert und sich eine gute Strategie ausgedacht. Damit hat man zwar schon einen wichtigen Teil der Arbeit geleistet, aber irgendwie muss dem Computerspieler noch beigebracht werden, nach dieser Strategie zu spielen.

Anhand einer kleinen Aufgabe soll gezeigt werden, wie man eine Idee formal beschreiben und in ein Programm überführen kann. Dabei nehmen wir an, dass wir einen Stapel mit Karten haben, der sortiert werden soll.

Vorraussetzungen

  • eine beliebige Anzahl an Spielkarten

  • eine Reihenfolge, in der die Spielkarten sortiert werden sollen

Idee formalisieren

Als erstes muss die Idee formal beschrieben werden. Oftmals kann man zunächst beschreiben, wie man als Mensch vorgehen würde.

  1. Gehe den Stapel durch und merke die Position, an der sich die kleinste Karte befindet.

  2. Tausche die Position der kleinsten Karte mit der untersten Karte im Stapel.

  3. Die kleinste Karte ist jetzt an der richtigen Position.

  4. Führe die Schritte nochmal für den Reststapel (ohne die sortierten Karten) aus.

Idee implementieren

Nachdem man seine Idee formal niedergeschrieben hat, kann sie ganz leicht in ein Programm überführt werden:

/**
  * Das Array a[] symbolisiert den Stapel der unsortierten Karten. Dabei steht
  * eine Zahl immer für eine spezielle Karte. Eine kleinere Zahl bedeutet,
  * dass es sich um eine kleinere Karte handelt.
  *
  * start gibt die Position an, wo der Reststapel beginnt (am Anfang: start = 0)
  */
 public static void sortiere(int[] a, int start) {
     //Position der kleinsten Karte
     int pos = start;

     // Gehe Array durch und merke die Position der kleinsten Karte (1)
     for (int i = start+1; i < a.length; i++) {
         // Wenn eine kleinere Karte gefunden wurde...
         if (a[i] < a[pos]) {

             ... neue Position merken
             pos = i;
         }
     }

     // kleinste Karte mit erster Karte des Reststapels tauschen  (2) (3)
     int temp = a[start]; // erste Karte merken
     a[start] = a[pos];   // kleinste Karte nach vorne bringen
     a[pos] = temp;       // gemerkte Karte in die Mitte des Stapels schreiben

     // Wenn es noch einen Reststapel gibt, soll dieser weitersortiert werden (4)
     if (start < a.length) {
          sortiere(a, start+1);
     }
 }
1 Gehe den Stapel durch und merke die Position, an der sich die kleinste Karte befindet.
2 Tausche die Position der kleinsten Karte mit der untersten Karte im Stapel.
3 Die kleinste Karte ist jetzt an der richtigen Position.
4 Führe die Schritte nochmal für den Reststapel (ohne die sortierten Karten) aus.

Weiterführende Literatur

Zu den meisten Programmiersprachen existieren umfassende Dokumentationen. Einige Empfehlenswerte werden hier aufgeführt. Der geneigte Leser ist eingeladen, in dem entsprechenden Bereich eigene Empfehlungen hinzuzufügen.

Java

Die umfassende (englischsprachige) offizielle Dokumentation zu Java befindet sich hier. Als deutschsprachiges Referenzwerk ist das Buch "Java ist auch eine Insel" empfehlenswert, welches man bei Galileo Computing als Open-Book kostenlos online betrachten oder auch herunterladen kann. Man kann es auf der selben Seite auch als gedruckte Ausgabe bestellen.