[Teil 1: Konzepte] [Teil 3: Arrays und Vektoren]

Einstieg in Java
Teil 2: Alles muß in eine Klasse!

Von Enno Runne und Hendrik C.R. Lock

In diesem Kursteil wagen wir den Sprung in die Java-Programmierung. Die Sprachkonzepte werden anhand eines durchgängigen Programmierbeispiels eingeführt - einer interaktiven Adreßkartei, die das Abspeichern, Laden, Suchen und Ausgeben von Adressen erlaubt.

Java ist eine objektorientierte Programmiersprache. Die Idee der Objektorientierung ist es, Probleme so zu darzustellen (zu modellieren), daß Daten und zugehörige Prozeduren geschickt gekapselt werden. Kapselung heißt dabei, daß Daten und ihre Prozeduren eine entwurfs- und programmtechnische Einheit bilden. Diese Einheit bezeichnet man als Klasse; ihre Variablen werden Elemente und ihre Prozeduren Methoden genannt. Eine Klasse ist ein Typ in der Programmiersprache, beschreibt also Eigenschaften von Objekten. Ein Objekt ist in dieser Sprechweise eine Ausprägung einer bestimmten Klasse - die Klasse "Auto" könnte als Ausprägungen etwa die Objekte "mein Golf" und "Nachbars Benz" haben. Alle Objekte einer Klasse besitzen dann dieselben Methoden und Elemente, aber ihre Elemente dürfen unterschiedliche Werte enthalten - beide "Auto"-Objekte haben z.B. die Methode "Anlassen" und das Element "Verbrauch", aber die Verbrauchswerte unterscheiden sich natürlich. Werte sind wiederum Objekte, die ganze Struktur läßt sich also schachteln.

Das klingt nach viel Theorie, wird in der Praxis aber schnell klar: Bezogen auf unser Beispiel können wir die gesamte Adresskartei als eine Klasse AdressKartei einführen, die über ihre Methoden eine Reihe von Objekten einer Klasse Adresse verwaltet. Die Adresskartei ist dann ein einzelnes Objekt der Klasse AdressKartei. Ihre Methoden realisieren das Einfügen und Wiederauffinden von Adressen.

In Java sind Werte entweder Objekte oder Daten der sogenannten Grundtypen, z.B. ganze Zahlen. Alle Klassen bauen auf den Grunddatentypen und auf einer Reihe von Standardklassen auf, z.B. System, String und File. Die wichtigsten Standardklassen betreffen die Programmierung von Oberflächen, Datenhaltung und Textverarbeitung.
Wie bereits erwähnt, bietet Java mehrere elementare Datentypen, die im folgenden tabellarisch aufgeführt werden.

Schlüsselwort Größe Bedeutung
(Integertypen)
byte
short
int
long

(Fließkommatypen)
float
double

(andere)
char
boolean

8 Bit
16 Bit
32 Bit
64 Bit


32 Bit
64 Bit


16 Bit
keine Angabe

ganze Zahl (-128 bis 127)
ganze Zahl (-32768 bis 32767)
ganze Zahl (ca. +/- 2 Mrd.)
ganze Zahl


Fließkommazahlen
Fließkommazahlen doppelter Genauigkeit


ISO-Zeichen
Wahrheitswert

Wie die Tabelle zeigt, ist die Maschinendarstellung aller Grunddatentypen festgelegt und damit unabhängig von der jeweiligen Zielmaschine. Dadurch werden Portierungsprobleme, wie sie z.B. von C her bekannt sind ("wie groß ist mein int heute?"), ausgeschlossen und die Netzfähigkeit ganz wesentlich unterstützt.

Im Vergleich zu C werden im Datentyp char über den Standardzeichensatz ASCII hinaus eine Reihe weiterer Zeichensätze und Umlaute im ISO Standard angeboten. Weil Java-Quelltexte ebenfalls in diesem Zeichensatz dargestellt werden, dürfen auch deutsche Umlaute in den Bezeichnern verwendet werden.

Die Syntax von Java folgt im wesentlichen der Syntax von C, z.B. bei Zuweisungen, logischen und arithmetischen Operatoren sowie bei Definition von Funktionen und Prozeduren. Außerdem gibt es einen neuen booleschen Typ bool, dessen Werte mit true und false bezeichnet werden. Weiterhin kennt Java keine strukturierten Daten wie C (union & Co), diese werden durch die Klassen abgelöst.

Daß sich Java syntaktisch sehr stark an C angelehnt hat, macht einen Umstieg von C her einfacher - wer sich in C auskennt, braucht sich nur die neuen Konzepte anzueignen. Aus diesem Grund gehen wir in diesem Java-Kurs auch nicht näher auf die grundlegenden Programmierkonzepte ein; wir setzen also voraus, daß der Begriff einer Fallunterscheidung oder einer Schleife bekannt ist. Durch die Ähnlichkeit mit C ist es mit gewissen Einschränkungen sogar möglich, C-Programme relativ einfach nach Java umzusetzen. Die größten Einschränkungen treten hierbei im Bereich des Datenaustausches zwischen Programm und Umgebung auf. Aber auch, wenn Sie noch keine Programmiersprache perfekt beherrschen, können Sie einfach die Beispiele dieses Kurses ausprobieren und durch Experimentieren und Verändern des Codes eigene Programme erstellen.

Die Standard-Bibliothek

Die Programmiersprache Java umfaßt einen Kern und eine Standard-Bibliothek. Der Kern spiegelt die Objektorientierung wider und enthält außerdem Konzepte wie die explizit programierbare Ausnahmebehandlung und die Programmierung nebenläufiger leichtgewichtiger Prozesse (engl. threads). Die Standard-Bibliothek enthält sämtliche Standard-Klassen, und insbesondere eine Ur-Klasse Object, auf die wir im nächsten Abschnitt zurückkommen werden. Die Standard-Klassen sind in Form von Paketen (engl. packages) organisiert:

Paket Funktionalität
java.applet Programmierung von Applets
java.awt Plattformunabhängige, graphische Oberflächenprogrammierung
java.lang Basisklassen der Sprache Java, z.B. Object und String
java.io Ein- und Ausgabe
java.net Netzprogrammierung (Sockets und URLs)
java.util Wiederverwendbare Elemente, z.B. dynamische Felder, Hashtabellen und Zerteiler

Die Ur-Klasse Object

Es gibt eine Urahnin aller Klassen: die Klasse Object. Jede Klasse ist eine direkte oder über mehrer Stufen indirekte Unterklasse von Object und erbt deren Eigenschaften. Vererbung heißt hier, daß eine Unterklasse dieselben Elemente und Methoden wie ihre Oberklasse besitzt, aber daß die Unterklasse durch neue Elemente und Methoden erweitert werden darf. Java bietet nur Einfachvererbung, d.h. jede Klasse (mit Ausnahme von Object) hat genau eine Oberklasse. Mehrfachvererbung gibt es im Gegensatz zu anderen objektorientierten Programmiersprachen nicht - die Definition von "Omnibus" aus den Klassen "Auto" und "Öffentliches Verkehrsmittel" zusammen wäre also nicht möglich. Ob dies in der Praxis zu nennenswerten Einschränkungen führt, wird unter Fachleuten durchaus noch kontrovers diskutiert. Durch den Verzicht auf Mehrfachvererbung ist Java im Vergleich zu C++ einfacher und konzeptuell klarer. Mehrfachvererbung ist nämlich recht kompliziert, wenn die Vererbung gleicher Methoden aus zwei oder mehr Oberklassen zu regeln ist. Eine wichtige Funktion der Mehrfachvererbung wird in Java allerdings von sogenannten Schnittstellen (engl. interfaces) wahrgenommen, die in einem späteren Kursteil eingeführt werden.

Die Ur-Klasse Object ist Teil des Pakets java.lang und vererbt 11 Methoden, darunter equals() und toString(), die uns im Laufe des Kurses noch mehrmals begegnen werden. Die vererbte Methode equals() testet zwei Objekte auf Gleichheit, indem sie ihre Verweise auf Gleichheit testet. Die Methode toString() stellt ein Objekt als eine Zeichenkette dar, damit es zum Beispiel für Testzwecke ausgegeben werden kann. Die von Object vererbte Methode gibt in der Zeichenkette den Typ und die Speicheradresse des Objektes an.

In jeder Klasse kann eine geerbte Methode individuell neu definiert werden; dieser Vorgang wird als Überschreiben bezeichnet. Außerdem kann eine geerbte Methode überladen werden. Der Unterschied ist der folgende: Beim Überschreiben wird in der Unterklasse eine Methode mit denselben Paremetern (und Typen) wie in der Obeklasse definiert. Beim Überladen hingegen wird eine Methode definiert, die in der Anzahl der Parameter oder in ihren Typen von der ursprünglichen Methode abweicht.
Im folgenden Beispiel wird die Methode equals() überladen. Für Objekte einer Klasse Koordinate, die (x,y)-Paare beinhaltet, wird damit anstelle der Gleichheit der Verweise, wie sie Java normalerweise verwendet, die Gleichheit aller Elemente getestet, die in diesem Falle erwünscht ist. Ansonsten würden nur Koordinaten als gleich erkannt, die sich auf dasselbe Objekt beziehen, aber nicht solche, deren eigentliche (x,y)-Werte übereinstimmen.

class Koordinate 
{
 int x,y;

  boolean equals(Koordinate k) 
  {
    return (x == k.x && y == k.y);
  }
}
Um die Methode equals() zu überschreiben, statt sie zu überladen, müßte der Parametertyp Object sein. Eine mögliche Realisierung und eine eingehendere Diskussion dieser Thematik wird später erfolgen.

Eine Standardklasse: String

Nachdem wir uns mit den grundlegenden Begriffen der objektorientierten Programmierung beschäftigt haben, betrachten wir nun als konkretes Beispiel die Zeichenketten in Java.

Java deckt mit seinen Standardklassen vieles von dem ab, was in einfachen Programmen benötigt wird. Die Standardklassen sind in Form von Paketen organisiert und über das Internet verfügbar.
Zeichenketten sind in Java nicht als Grunddatentyp, sondern als Klasse vorhanden. Es wird dabei zwischen zwei Arten von Zeichenketten, nämlich zwischen konstanten und veränderbaren, unterschieden:
Konstante Zeichenketten sind von der Klasse String. Sie bietet diverse Methoden, wie z. B. die Bestimmung der Länge oder den Vergleich zweier Zeichenketten. Allerdings können Objekte der Klasse String im Programmlauf nur erzeugt, nicht aber verändert werden. Veränderbare Zeichenketten werden als Objekte der Klasse StringBuffer dargestellt; sie bietet Methoden zum Einfügen und Löschen von Zeichen und ist somit für Textanwendungen geeignet. Im Gegensatz zu String können Objekte dieser Klasse jedoch nicht direkt in Ein- und Ausgabeoperationen benutzt werden, sie müssen deshalb zuvor nach String konvertiert werden. String und StringBuffer bieten also nur in Kombination eine umfassende Zeichenkettenfunktionalität, aber in vielen einfacheren Fällen reicht String alleine schon aus, wie folgende Beispiele verdeutlichen:

  String str;
  str = new String("Sabine");
Diese Anweisung erzeugt ein Objekt des Typs String, das die Zeichenkette speichert, und legt einen Verweis auf das Objekt in der Variablen str ab. Die konkrete Speicherdarstellung der Zeichenkette, d.h. wie der Rechner die Bits und Bytes ablegt, bleibt dabei dem Benutzer verborgen. Eine kürzere Schreibweise, die den selben Effekt hat, ist:
  String str = "Sabine";

Die Ausgabe einer Zeichenkette auf den Bildschirm kann mit der Methode println der Klasse System erfolgen:

  System.out.println(str);
Die Methode ist bezüglich der Grunddatentypen und der Klasse String überladen: im Falle eines Grunddatentyps wird der Wert zuvor in eine Zeichenkette umgewandelt und dann erst ausgegeben, wohingegen er im Fall eines Arguments vom Typ String direkt ausgegeben werden kann.

Eine wichtige Grundoperation auf Zeichenketten ist die Verkettung, die durch den Operator + ausgedrückt wird - dieser Operator ist somit überladen, da er natürlich auch die gewohnte Addition von Zahlen ausdrückt. Für seine Anwendung gilt: ist eines der Argumente von + vom Typ Object oder einer beliebigen Unterklasse, so wird das Objekt mittels seiner Methode toString in eine Zeichenkette umgewandelt, und der Operator bezieht sich dann auf eine Verkettung. Sind beide Argumente jedoch numerische Typen, so bezieht er sich auf eine Addition. Auch die Addition ist überladen, da hier unterschiedliche numerische Typen kombiniert werden dürfen. Hierzu ein Beispiel:

  System.out.println("" + 1 + 2 );
  System.out.println( 1 + 2 );
  System.out.println("" + 1 + (1 + 2) );
ergibt jeweils die Ausgaben 12, 3, und 13. Im letzten Fall bewirkt die innere Klammerung die Interpretation von + als Addition, in Übereinstimmung mit der Regel zur Auflösung der Überladung.

Verweise

Verweise in Java unterscheiden sich ganz wesentlich von den Zeigertypen in C - Zeiger in C sind lediglich Speicheradressen. Ein Verweis in Java ist hingegen der einzig mögliche Zugriffsweg zu einem Objekt. Er entsteht bei der Erzeugung des Objekts mittels des Operators new und kann danach nur noch an Variable (und Elemente) zugewiesen werden. Technisch gesehen ist ein Verweis eine Speicheradresse, aber Adressen werden in Java systematisch vor dem Benutzer verborgen: Die Bildung von Adressen, Adressarithmetik wie in C, und Zugriffe auf den Speicher über Adressen existieren nicht. Die Verbannung von Adressen ist Teil des Sicherkeitskonzepts von Java - ansonsten wären beliebige Zugriffe im Speicher möglich.
Somit identifiziert eine Variable ein Objekt, wenn sie den Verweis auf dieses Objekt enthält. Das heißt auch, daß die Variable nicht das Objekt selbst enthält, Objekte werden also nicht durch die Zuweisung kopiert. Ähnliche Überlegungen gelten auch für Vergleiche, wie schon oben beim Beispiel der Koordinaten anklang.

Ein Objekt ist nicht mehr erreichbar, wenn keine Variable eines erreichbaren Objektes oder einer aufgerufenen Methode diesen Verweis speichert. Die virtuelle Java Maschine besitzt eine Speicherverwaltung, die nichterreichbare Objekte aufsammelt und den von ihnen bisher belegten Speicherplatz für die Erzeugung neuer Objekte verfügbar macht. Der Programmierer muß sich also um die sogenannte Garbage Collection nicht mehr selbst kümmern.

Definition einer Klasse

Auf Zeichenketten aufbauend definieren wir jetzt unsere erste Klasse Adresse, die wir für die Adressverwaltung benötigen. Eine Adresse soll zunächst nur drei Felder für den Vornamen, den Namen und das Alter enthalten:
class Adresse 
{
  String vorname, name;
  int alter;

  Adresse(String vn, String nn, int a)
    {
      vorname = vn;
      name = nn;  
      alter = a;
    }
}
Der Name der Klasse folgt dem Schlüsselwort class, die Definition der Elemente und Methoden (in dieser Reihenfolge) wird durch geschweifte Klammern umschlossen. Die Klasse Adresse enthält damit drei Elemente und eine Methode Adresse, die der Erzeugung dient.

Methoden einer Klassendefinition, die denselben Namen wie die Klasse tragen, heißen Konstruktoren und werden für die Erzeugung neuer Objekte benutzt, hier z. B. Adresse. Durch die explizite Programmierung des Konstruktors einer Klasse bestimmt der Benutzer die Initialisierung der Elemente. Die Initialwerte werden dabei durch die Parameter der Konstruktormethode übergeben.

Ein neues Objekt wird durch Verwendung des Operators new angelegt, dabei wird implizit immer der Konstruktor der angesprochenen Klasse aufgerufen.

  Adresse adr = new Adresse(new String("Sabine"), new String("Müller"), 13);
Durch Adresse adr wird eine neue Variable mit Bezeichner adr vereinbart. Das Argument von Operator new ist ein Konstruktor.

Enthält die Klasse keine explizite Konstruktordefinition durch den Programmierer, so verfügt sie automatisch über einen impliziten parameterlosen Konstruktor, der das Objekt anlegt und dessen Elemente initialisiert. Damit stellt sich die Frage, wie Elemente einer Klasse überhaupt initialisiert werden, deren Initialisierung nicht explizit programmiert wurde. Dies betrifft im einzelnen auch Elemente, die nicht explizit durch einen vorhandenen Konstruktor initialisiert werden. In Java gilt hierfür die Regel, daß grundsätzlich alle Variablen (und Elemente von Objekten) initialisiert werden. Die initialen Werte sind typabhängig: Verweise werden mit null, numerische Variablen mit 0, und boolesche Variablen mit false initialisiert. Der Sonderwert null signalisiert, daß die Variable keinen gültigen Verweis enthält. Der Zugriff über eine solche Variable löst eine Ausnahme aus, die über eine Ausnahmebehandlung abgefangen werden kann (Genaueres hierzu wird in einem späteren Teil behandelt).
Es ist durchaus möglich, ein Objekt auch wie im folgenden zu initialisieren. Dabei sieht man auch, daß man auf die Elemente eines Objekts direkt mit Objekt.Element zugreifen kann:

  adr         = new Adresse();
  adr.name    = "Sabine";
  adr.vorname = "Müller";
  adr.alter   = 13;
Die Initialisierung durch den Konstruktor ist jedoch die kompaktere und systematischere Variante, da sie wirklich nur die notwendige Initialisierung durchführt - im obigen Beispiel werden die Elemente erst implizit initialisiert und dann auf die gegebenen Werte gesetzt, anstatt sie gleich mit dem gewünschten Wert zu belegen.

Java bietet die Möglichkeit, Namen von Methoden zu überladen, also auch den Konstruktor. Damit die Überladung eindeutig bestimmt werden kann, wird gefordert, daß sich Anzahl und Typ der Argumente von solchen Definitionen unterscheiden. Im folgenden Beispiel unterscheiden sich die zwei Konstruktoren in ihrer Argumentzahl:

class Adresse 
{
  String vorname, name;
  int alter;

  Adresse(String vorname, String nachname, int alter)
  {
    this.vorname = vorname;
    name = nachname;  
    this.alter = alter;
  }

  Adresse(String vorname, String nachname)
  {
    this.vorname = vorname;
    name = nachname;  
  }
}

Hier wurde zusätzlich mit this ein neues Konzept eingeführt, nämlich der Verweis auf das aktuelle Objekt, also der Verweis auf dasjenige Objekt, dessen Methode gerade ausgeführt wird. Im Beispiel benötigten wir this, weil die Namen der Funktionsparameter - vorname und alter - die gleichnamigen Klassenelemente verdecken.

Um eine Adresse formatiert ausgeben zu können, erweitern wir die Klasse nun um eine Ausgabemethode. Deren Definition liegt wie oben innerhalb der geschweiften Klammern der Klassendefinition von Adresse.

  void Ausgeben()
  {
     System.out.println(vorname + " " + name);
     System.out.println("Alter: " +alter );
  }
Die Bezeichner name, vorname und alter beziehen sich hier natürlich auf die gleichnamigen Elemente der Klasse.

Weiterhin definieren wir die Standardmethode toString, mit deren Hilfe das Objekt z.B. zu Testzwecken ausgegeben werden kann.

  public String toString()
  {
     return "Adresse (" + name + " " + vorname + " " + alter + ")" ;
  }
Wie in C unterscheidet Java zwischen Prozeduren und Funktionen: Prozeduren sind Funktionen vom Ergebnistyp void, d.h. sie geben kein Resultat zurück. Im Gegensatz zu C ist das Schlüsselwort void nicht optional.

Die Hauptmethode main

Die Hauptmethode ist diejenige Prozedur, die bei Aufruf des Programms automatisch gestartet wird. Sie wird durch den Namen main bezeichnet. Die Hauptklasse ist dementsprechend die Klasse, die main definiert. Im allgemeinen wird kein Objekt dieser Klasse explizit gebildet, sie dient nur als statische Kapselung des Hauptrogramms und seiner Daten. Nur Java-Programme, die eine solche Hauptklasse enthalten, können auch als Java-Applikation aufgerufen werden - ohne Hauptklasse wäre ja nicht klar, welcher Programmcode überhaupt aufzurufen ist. Wird eine virtuelle Java Maschine mit einer solchen Hauptklasse gestartet, erzeugt sie ein Objekt dieser Hauptklasse und startet deren main-Methode.

Bei der Definition der Methode main müssen einige Randbedingungen beachtet werden: diese Methode muß immer als statisch und öffentlich deklariert werden. Das bedeutet das Folgende:
Eine Methode wird durch Verwendung des Schlüsselworts static als statisch vereinbart. Damit gehört sie fest zu dieser Klasse und nicht zu einzelnen Objekten. Eine statische Methode kann deshalb auch ohne Objekt aufgerufen werden, analog zu einer Prozedur in C. Außerdem kann eine statische Methode niemals überschrieben werden.
Wichtig ist weiterhin, daß main öffentlich ist, ihm also das Schlüsselworts public vorangestellt wird. Eine öffentliche Methode ist für alle anderen Klassen und deren Methoden sichtbar. Dies ist notwendig, weil normalerweise Methoden einer Klasse nur für diese, alle ihre Unterklassen, und für Klassen im selben Paket sichtbar sind. Alle anderen Klassen wissen nichts von ihrer Existenz.

Die eben genannten Schlüsselwörter sind sogenannte Modifikatoren und als solche auch auf Klassen und Variablen anwendbar. Modifikatoren und die Thematik der Sichtbarkeitsbereiche werden in einem späteren Teil noch ausführlicher behandelt.

Unser Hauptprogramm sieht nun folgendermaßen aus:

public class AdressProgramm
{
  public static void main(String argv[])
  {
    Adresse adr = new Adresse("Sabine", "Müller", 13);
    adr.Ausgeben();
    System.out.println(adr);
  }
}
Es erzeugt ein Objekt Adresse und gibt es formatiert aus. Die Anweisung System.out.println(adr) übergibt an println ein Objekt. Die Methode println besorgt sich deshalb implizit mittels adr.toString() die Zeichenkettendarstellung des Objekts.

Die Datei mit dem Hauptprogramm enthält zusätzlich noch die Definition der Klasse Adresse, die für das Hauptprogramm sichtbar ist. Es ist zu beachten, daß der Name dieser Datei AdressProgramm.java lauten sollte, d.h. daß der Name der Hauptklasse mit dem der Datei übereinstimmt (erweitert um die Endung .java).
Der Grund für diese Namenskonvention ist, daß alle als öffentlich vereinbarten Klassen in einer Quelldatei gleichen Namens abgelegt werden müssen. Das bedeutet auch, daß jede Quelldatei höchstens eine öffentliche Klasse enthält. Diese Konvention erfordert der Übersetzer javac. Der Übersetzer espresso hingegen verhält sich hier mit Absicht liberaler, weil die Konvention eine Flut von Quelldateien zur Folge haben kann.

Übersetzung und Aufruf

Wir verwenden die Übersetzer javac oder espresso. Der Aufruf der Übersetzer sieht wie folgt aus:

  javac AdressProgramm.java
bzw.
  espresso AdressProgramm.java

Der Übersetzer legt im aktuellen Dateiverzeichnis für jede Klasse getrennt eine Klassendatei an, in unserem Fall die Dateien AdressProgramm.class und Adresse.class. Aus diesem Grund spielt es zunächst auch keine Rolle, ob Hauptprogramm und Klassendefinitionen in einem gemeinsamen oder in getrennten Quelldateien abgelegt werden. Die erzeugten Klassendateien enthalten Typinformationen und den Bytecode für die virtuelle Java Maschine.

Der Start unserer kleinen Anwendung erfolgt mit dem Kommando

  java AdressProgramm
mit dem die virtuelle Maschine gestartet wird, auf der die Applikation dann abgearbeitet wird. Es erzeugt erwartungsgemäß folgende Ausgabe:
Sabine Müller
Alter: 13
Adresse (Müller Sabine 13)

Das war also unser erstes Java-Programm! In der nächsten Ausgabe von Internet online geht es unter anderem um Felder und Vektoren (d.h. dynamisch vergrößerbare eindimensionale Felder). Bis dann wünschen wir frohes Experimentieren!


[Teil 1: Konzepte] [Teil 3: Arrays und Vektoren]