Zum Inhalt

1. Java-Grundlagen

Das vorliegende Kapitel fast wesentliche Java Grundlagen zusammen, die im Laufe der Einführungsphase behandelt werden. Einige ausgewählte Themen wie Zeichenketten oder Felder werden im Rahmen der beiden Kapitel Suchen und Sortieren sowie Kryptologie vertiefend aufgegriffen.

Kontrollstrukturen

Kontrollstrukturen beeinflussen des Verlauf eines Programms. Die nachfolgenden Beispiele sind an das Greenfoot-Szenario "Mars-Rover" angelehnt, können aber auch ohne Kenntnis des Szenarios nachvollzogen werden.

Bedingte Anweisung

Die bedingte Anweisung überprüft den Wahrheitswert einer Bedingung. Ist die Bedingung erfüllt, so wird der erste Anweisungsbock ausgeführt, ansonsten der zweite mit else gekennzeichnete Anweisungsblock.

Zweiseitige Auswahl

Werden beide Fälle aufgeführt, so sprechen wir von zweiseitiger Auswahl.

if (huegelVorhanden("vorne")) {
  drehe("rechts");
}
else {
  fahre();
}

Einseitige Auswahl

Verzichten wir auf den else-Fall, so sprechen wir von einseitiger Auswahl.

if (huegelVorhanden("vorne")) {
  drehe("rechts");
  fahre();
}

Mehrseitige Auswahl

Gibt es mehr als zwei gleichwertige Alternativen, so kommt entsprechend die mehrseitige Auswahl zur Anwendung.

if (huegelVorhanden("vorne")) {
  drehe("rechts");
}
else if (huegelVorhanden("rechts")) {
  drehe("links");
}
else {
  fahre();
}

switch-Anweisung

Kann der weitere Verlauf vom Wert eines primitiven Datentyps oder vom Wert einer Zeichenkette abhängig gemacht werden, so steht als übersichtliche Alternative zur Mehrfachauswahl auch die switch-Anweisung bereit.

Die break-Anweisungen sorgen dafür, dass der Programmablauf nach der Abarbeitung eines Falls nach der switch-Anweisung fortgesetzt wird. Der optionale default-Fall tritt ein, wenn keiner der vorherigen case-Fälle zutrifft.

switch (anzahlMeinerSchritte) {
  case 0: { fahre();
            break; 
          }
  case 1: { drehe("links");
            break;
          }
  default: { drehe("rechts");
             break;
           }
}

Schleifen

Zur wiederholten Ausführung einer oder mehrerer Anweisungen werden Schleifen verwendet.

while-Schleife

Die while-Schleife wiederholt eine oder mehrere Anweisungen so lange, bis eine gegebene Bedingung nicht mehr erfüllt ist. Da die Bedingung vor dem Anweisungsblock überprüft wird ("vorprüfende Schleife"), ist es möglich, dass der Anweisungsblock gar nicht ausgeführt wird.

while (!huegelVorhanden("vorne")) {
  fahre();
}

do-while-Schleife

Die do-while-Schleife wiederholt eine oder mehrere Anweisungen so lange, bis eine gegebene Bedingung nicht mehr erfüllt ist. Da die Bedingung hier erst nach dem Anweisungsblock überprüft wird ("nachprüfende Schleife"), wird der Anweisungsblock mindestens einmal ausgeführt.

do {
  setzeMarke();
  fahre();
}
while (!huegelVorhanden("vorne"));

Zählschleife

In der Zählschleife wird eine Zählvariable mit einem Anfangswert belegt. Der zugehörige Anweisungsblock wird solange ausgeführt wie eine Schleifenbedingung erfüllt ist. In der Regel wird diese Bedingung von der Zählvariable selbst abhängen. Zusätzlich wird durch die dritte Komponente des Schleifenkopfes die Zählvariable nach jedem Durchlauf verändert, d.h. meist um eins erhöht oder erniedrigt.

Das folgende Beispiel sorgt dafür, dass die fahre()-Anweisung genau vier Mal ausgeführt wird:

for (int i=0; i<4; i++) {
  fahre();
}

Primitive Datentypen

Im Unterricht verwenden wir die primitiven Datentypen int, double, char und boolean, die wir vereinfachend als ganze Zahlen, Kommazahlen, Buchstaben und Wahrheitswerte beschreiben.

Datentyp Wertebereich Beispiele
int -2147483648 bis + 2147483647 -1, 1
double 4.9E-324 bis 1.8E308 -3.14, 0, 6.28
char alle Zeichen 'A', 'Z'
boolean true, false true, false

Deklaration und Initialisierung

Soll eine Variable eines primitiven Datentyps verwendet werden, so ist sie zunächst zu deklarieren, anschließend zu initialisieren, d.h. mit einem Anfangswert zu belegen.

int zahl; // Deklaration
zahl = 0; // Initialisierung

Die beiden Schritte können auch kompakt zu einem zusammengefasst werden.

int zahl = 0; // Deklaration + Initialisierung

Vergleichsoperatoren

Variablen des gleichen primitiven Datentyps lassen sich mit den Operatoren <, >, <=, >=, == und != vergleichen. Man beachte den Vergleichsoperator ==, der zur besseren Abgrenzung gegenüber dem Zuweisungssymbol = mit zwei Gleichheitszeichen geschrieben wird.

int zahl1 = 1;
int zahl2 = 11;

if (zahl1 < zahl2) {
  zahl1 = zahl1 + 10;
}
if (zahl1 == zahl2) {
  zahl1 = zahl1 + 100;
}

Konvertieren von Datentypen

Primitive Datentypen können ineinander konvertiert werden. In den meisten Fällen erfolgt die Konvertierung recht intuitiv - so wird eine double-Zahl in eine int-Zahl einfach durch Abschneiden der Nachkommastellen verwandelt. Die Verwandlung selbst wird durch den sogenannten type cast-Operator angezeigt, der den gewünschten Datentypen in Klammern vor die zu konvertierende Variable setzt:

double zahl = 1.47484;
int zahl2 = (int) zahl;

Interessant ist die Verwandlung von Buchstaben (char) in ganze Zahlen (int) und umgekehrt. Dieser Transfer richtet sich nach der Nummerierung aller bekannten Zeichen gemäß der genormten ASCII-Tabelle (vgl. Wikipedia-Artikel). Er weist beispielsweise dem Großbuchstaben A eine 65, B eine 66 usw. zu (Bildausschnitt aus WikiPedia).

Diese Festlegung kann geschickt für Veränderungen von Buchstaben genutzt werden. Das folgende Beispiel etwa verschiebt den gegebenen Buchstaben H um vier Stellen im Alphabet.

char c = 'H';
int zahl = (int) c;
zahl = zahl + 4;
c = (char) zahl;

Klassen und Objekte

Java-Klassengerüst

Klassen werden in Java mit dem Schlüsselwort class eingeleitet. Bei der Modellierung einer Klasse wird festgehalten, welche Attribute (Eigenschaften) und Methoden (Anfragen oder Aufträge) für die Klasse sinnvoll sind.

Im folgenden Beispiel wird für eine Klasse Schueler festgelegt, dass die Attribute Alter und Name verwendet werden. Beide bekommen durch den Konstruktor - eine besondere Methode mit dem gleichen Namen wie die Klasse - bei der Erzeugung eines Objekts der Klasse bereits saubere Anfangswerte. Zudem sind beide Attribute durch die beiden set-Methoden jederzeit veränderbar und die beiden get-Methoden jederzeit abfragbar.

public class Schueler {

    private String name;
    private int alter;

    public Schueler(String name, int alter) {
        this.name = name;
        this.alter = alter;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }

    public void setAlter(int alter) {
        this.alter = alter;
    }

    public int getAlter() {
        return alter;
    }

}

Erzeugen von Objekten

Die Syntax zum Erzeugen eines Objekts sieht die Verwendung des Schlüsselworts new vor. Greifen wir das obige Beispiel der Klasse Schueler wieder auf, so müssen bei der Erzeugung eines Objekts der Klasse Schueler bereits Werte für die beiden Attribute übergeben werden.

Schueler s = new Schueler("Otto", 15); // Objekt erzeugen

String z = s.getName(); // Namen abfragen 

int a = s.getAlter(); // Alter abfragen
s.setAlter(a+1); // und neu besetzen

Vererbung

Bei der Vererbung stehen der Unterklasse alle Attribute und Methoden der Oberklasse zur Verfügung. Auch der geerbte Konstruktor kann dabei zur Verwendung kommen.

Im folgenden Beispiel wird einem Oberstufenschüler gegenüber einem Schüler zusätzlich das Attribut Tutor zugeordnet. Dieses Attribut kann hier zwar abgefragt, aber nach der Erzeugung nicht mehr verändert werden.

Zusätzlich demonstriert diese Klasse mit der Methode setName(), wie eine Methode überschrieben werden kann, ohne die bestehende vererbte Methoden komplett neu gestalten zu müssen. Auch die Methode erhoeheAlter() ist geschickt gelöst, da das Alter erhöht wird, ohne direkt auf das Attribut alter zugreifen zu müssen. (Bemerkung: Eine Unterklasse kann nur dann direkt auf ein Attribut einer Oberklasse zugreifen, wenn die Sichtbarkeit des Attributs statt private auf protected gesetzt ist.)

public class Oberstufenschueler extends Schueler {

    private String tutor;

    public Oberstufenschueler(String name, int alter, String tutor) {
        super(name, alter);
    }

    public String getTutor() {
        return tutor;
    }

    public void setName(String name) {
        if (name.length()==0) {
            super.setName("unbekannt");
        }
        else {
            super.setName(name);
        }
    }

    public void erhoeheAlter() {
        int alter = getAlter();
        alter++;
        setAlter(alter);
    }

}

Zeichenketten

Zeichenketten sind eine Aneinanderreihung von Zeichen, deren Anzahl beliebig ist. Eine Zeichenkette kann jederzeit verändert werden. In Java wird eine Zeichenkette durch ein Objekt der Klasse String repräsentiert.

Durchlaufen einer Zeichenkette

Um eine Zeichenkette mit einer Zählschleife zu durchlaufen, wird zunächst die Länge der Zeichenkette mit der Methode length() abgefragt. Die Zählschleife durchläuft nun die Positionen 0, 1, 2, ..., Länge-1 der Zeichenkette und greift per charAt() auf den i-ten Buchstaben zu. Die Nummerierung mit 0 beginnend ist anfangs ungewohnt, sie wird aber in Java konsequent auch in anderen Bereichen (z.B. bei Feldern) durchgeführt.

String eingabe = "TEST";

for (int i=0; i<eingabe.length(); i++) {
    char c = eingabe.charAt(i);
    System.out.println("Das Zeichen Nr. "+i+"ist: "+c);
}

Im Beispiel ergeben sich demnach folgende Ausgaben auf dem Bildschirm:

Das Zeichen Nr. 0 ist: T
Das Zeichen Nr. 1 ist: E
Das Zeichen Nr. 2 ist: S
Das Zeichen Nr. 3 ist: T

Durchlaufen und Verändern einer Zeichenkette

Im folgenden Beispiel wird die Zeichenkette TEST als Eingabe von vorn nach hinten durchlaufen und für die Ausgabe jeder Buchstabe doppelt angehängt. Als Ausgabe ergibt sich somit: TTEESSTT

String eingabe = "TEST";
String ausgabe = "";

for (int i=0; i<eingabe.length(); i++) {
    char c = eingabe.charAt(i);
    ausgabe = ausgabe + c + c;
}

Durchlaufen und Verändern einer Zeichenkette (Switch)

Das nächste Beispiel zeigt sehr schön, wie mithilfe einer Switch-Struktur übersichtlich verschiedene Fälle bearbeitet werden können. Der i-te Buchstabe wird hier in der Variablen c vom Typ char (also ein Buchstabe) zwischengespeichert. Abhängig vom Buchstaben wird ein anderer Buchstabe oder der ursprüngliche Buchstabe (default-Fall) angehängt.

Das Beispiel demonstriert insgesamt die Normalisierung einer Zeichenkette, in der Umlaute in eine adäquate Darstellung (z.B. Ä in AE) gebracht werden. Eine solche Normalisierung findet z.B. bei Suchmaschinen Anwendung, die Sonderzeichen in ihrer internen Speicherung möglichst vermeiden möchten.

String eingabe = "HÖHENÄRGER";
String ausgabe = "";

for (int i=0; i<eingabe.length(); i++) {
    char c = eingabe.charAt(i);

    switch (c) {
        case 'Ä': { ausgabe = ausgabe + "AE"; break; }    
        case 'Ö': { ausgabe = ausgabe + "OE"; break; }    
        case 'Ü': { ausgabe = ausgabe + "UE"; break; }    
        default:  { ausgabe = ausgabe + c; break; }    
    }
}

Durchlaufen und Verändern einer Zeichenkette (ASCII)

Abschließend betrachten wir noch ein Beispiel aus der Kryptologie, das auch im gleichnamigen Kapitel noch einmal in erweiterter Fassung aufgegriffen wird. Wir möchten eine gegebene Zeichenkette Buchstabe für Buchstabe um drei Zeichen im Alphabet nach vorn schieben (Caesar-Verschlüsselung).

Dazu wird jeder Buchstabe zunächst in eine Zahl umgewandelt. Diese Verwandlung richtet sich nach der ASCII-Tabelle (vgl. Wikipedia-Artikel), wodurch dem A eine 65, B eine 66 usw. zugeordnet wird. Dieser Zahl wird 3 hinzuaddiert, die resultierende Zahl wird anschließend wieder in einen Buchstaben zurückverwandelt. Diese geschieht ebenfalls nach der ASCII-Tabelle, wodurch beispielsweise aus dem Buchstaben G ein J wird (Bildausschnitt aus WikiPedia).

Sollte der Buchstabe beim Umwandeln eine Zahl größer als 90 (entspricht dem Buchstaben Z) ergeben, so würde der ASCII-Bereich der Großbuchstaben verlassen. Dies wird mit der Subtraktion von 26 korrigiert, d.h. es geht hier bei der Verschiebung im Alphabet wieder von vorn los.

String zeichenkette = "GEHEIMNIS";
String ergebnis = "";
int schluessel = 3;

for (int i=0; i<zeichenkette.length(); i++) {

    int zahl = (int) zeichenkette.charAt(i);
    zahl = zahl + schluessel;
    if (zahl>90) { zahl = zahl - 26; }

    char zeichen = (char) zahl;
    ergebnis = ergebnis + zeichen;
}

Felder

In einem Feld kann eine beliebige, aber feste Anzahl gleichartiger Elemente verwaltet werden. Die Anzahl der Elemente wird dabei beim Erzeugen des Felds festgelegt und kann anschließend nicht mehr verändert werden.

Ungewohnt ist die Nummerierung der Elemente bei 0 beginnend und bei der Feldlänge-1 endend. Diese Form der Nummerierung findet in Java aber auch bei Zeichenketten konsequent Anwendung.

Wir werden im Folgenden das Verwalten primitiver Datentypen (vgl. Zahlen im obigen Beispiel) in einem Feld betrachten. Das Speichern gleichartiger Objekte in einem Feld wird im Abschnitt Felder im Kapitel Lineare Datenstrukturen behandelt.

Erzeugen eines Felds

Das folgende Beispiel zeigt zu Beginn die Syntax der Erzeugung eines Felds. Im Beispiel wird ein Feld der Länge 100 über int-Zahlen zunächst deklariert und anschließend initialisiert. Das Feld selbst ist aber zu diesem Zeitpunkt noch leer.

int[] meinfeld; // Deklaration
meinfeld = new int[100]; // Initialisierung

Als abkürzende Schreibweise können Deklaration und Initialisierung wieder zusammengefasst werden.

int[] meinfeld = new int[100]; // Deklaration + Initialisierung

Folgt die geplante Besetzung des Felds einer festen Systematik, hilft eine Schleife bei der Besetzung der einzelnen Feldelemente. Im folgenden Beispiel werden die natürlichen Zahlen 1, 2, 3 usw. in das Feld eingewiesen.

// Erzeuge Feld
int[] feld = new int[10];

// Besetze mit 1,2,3,...
for (int i=0; i<feld.length; i++) {
    feld[i] = i+1;           
}  

Sind die Elemente des Feldes bei der Erzeugung bekannt und handelt es sich um ein eher kurzes Feld, so ist die folgende Kompaktschreibweise interessant, die Deklaration, Erzeugung und Initialisierung in einer Anweisung verbindet:

int[] feld = { 1, 1, 2, 3, 5, 8, 13, 21, 34, 55 };

Durchlaufen eines Felds

Das nächste Beispiel setzt ein Feld über int-zahlen voraus und summiert bzw. multipliziert die enthaltenen Werte, unabhängig von der Länge des Feldes. Die Abfrage feld.length der Feldlänge sorgt leider oft für Verwirrung, da sie in Java im Gegensatz zu Zeichenketten für Felder nicht als Methodenaufruf mit anschließenden runden Klammern () realisiert ist.

int summe = 0;
for (int i=0; i<feld.length; i++) {
    summe = summe + feld[i];
}

int produkt = 1;
for (int i=0; i<feld.length; i++) {
    produkt = produkt * feld[i];
}

Zeichenkette in Feld verwandeln

Eine erste komplexere Aufgabe besteht darin, die in einer Zeichenkette enthaltenen Ziffern in ein Feld über int-Zahlen zu übertragen. Dazu wird im ersten Schritt ein Feld erzeugt, das genauso lang ist wie die Zeichenkette. In einem zweiten Schritt geht nun die Schleife Buchstabe für Buchstabe durch die Zeichenkette, konvertiert die einzelnen Ziffern in Zahlen (gemäß der ASCII-Tabelle entspricht 0 einer 48, 1 einer 49 usw.), wandelt sie durch die Subtraktion von 48 in die gewünschte Zahl zwischen 0 und 9 um und speichert sie an der gleichen Feldposition wie die Buchstabenposition.

String zeichenkette = "54678932";        

int n = zeichenkette.length();
int[] feld = new int[n];

for (int i=0; i<zeichenkette.length(); i++) {

    int zahl = (int) zeichenkette.charAt(i);
    zahl = zahl - 48; // ASCII-Code von 0 ist 48, von 1 ist 49 usw.
    feld[i] = zahl;

}

Zweidimensionale Felder

Zweidimensionale Felder eignen sich dazu, eine Tabelle bzw. Matrix gleichartiger Elemente zu verwalten. Das folgende Anwendungsbeispiel zeigt auf, wie bei dem Spiel Schiffe versenken die Schiffe einer Spielers in einer Matrix repräsentiert werden:

Der Spieler hat auf seinem Spielfeld (der "Seekarte") ein 2er-, ein 3er- und ein 4er-Schiff versteckt. Jedes Schiff ist durch die Koordinaten seiner Bestandteile (z.B. 0-2 und 0-3 für das 2er-Schiff) eindeutig festgelegt. Der Gegenspieler kann nun versuchen, durch einen Schuss auf eine vorgegebene Koordinate einen Teil eines Schiffes zu versenken.

Eine einfache Modellierung mit einem Java-Feld besteht darin, an Positionen mit einem Schiffsbestandteile eine 1, an allen anderen Koordinaten eine 0 abzuspeichern. Damit ergibt sich die Definition der Seekarte des Benutzers im Konstruktor der hier angegebenen Klasse.

Die erste Methode getroffen() überprüft, ob an der übergebenen Koordinate ein Schiffsbestandteil zu finden ist. Ist das der Fall, so wird er auf 0 gesetzt und damit "versenkt" und als Ergebnis true zurückgegeben. Ansonsten wird false zurückgegeben.

Die zweite Methode überprüft, ob auf der gesamten Seekarte alle Schiffe versenkt wurden (return true; am Ende) oder ob es noch mindestens ein Schiffsbestandteil auf der Seekarte (vorzeitiges return false;) gibt.

public class SchiffeVersenken {

    private int[][] meer = { { 0, 1, 1, 1, 1, 0 },
                             { 0, 0, 0, 0, 0, 0 },
                             { 1, 0, 0, 1, 1, 1 },
                             { 1, 0, 0, 0, 0, 0 } };

    public boolean getroffen(int zeile, int spalte) {
        if (meer[zeile][spalte] == 1)  {
            meer[zeile][spalte] = 0;
            return true;
        }
        else { return false; }
    }

    public boolean alleVersenkt() {
        for (int i=0; i<4; i++) {
            for (int j=0; j<6; j++) {
                if (meer[i][j]==1) {
                    return false;
                }
            }
        }
        return true;
    }

}