AG-Intra.net Arbeitsgemeinschaft
Intranet

Home
Was ist ein Intranet
Grundlagen
Netzwerke
Linux
Windows
Java
Sicherheit
Datenbanken
Projekte
Links
Impressum
Mitmachen ?
Diskussionsforum
Start:
17.10.2000
Letztes Update:
25.11.2000
1. Beispiel - Hallo Welt . Java Tutorial
Copyright 2000 by Frank Gehde
<= vorherige Seite Inhaltsverzeichnis nächste Seite =>

Liebe Besucher, ein aktueller Hinweis in eigener Sache:
Es ist beabsichtigt, diese Seiten und die Domain im Januar/Februar 2004 auf einen anderen Server umzuziehen. Es ist leider nicht auszuschließen, daß es während des Umzugs zu technischen Problemen mit diesen Seiten kommen wird. Insbesondere im eMail-Bereich wird es vermutlich Probleme geben. Wenn Sie fragen haben oder mich sonstwie erreichen wollen empfehle ich an rebel@snafu.de zu posten.
Nachdem der Umzug abgeschlossen ist, wird es allerdings auch inhaltliche Änderungen während des ersten Halbjahrs 2004 geben. Keine Angst. Es werden keine Inhalte verlorengehen, aber die Struktur der Seiten wird komplett geändert. Diese Seite hat eben eine andere Entwicklung genommen seit 2000, als das Projekt gestartet wurde ;-) Ich werde mich bemühen, daß bei ihnen vorhandene alte Bookmarks wenigstens zu einem Verweis auf die Neustruktur führen, und die gesuchten Inhalte für sie trotzdem leicht und schnell auffindbar sein werden.
Die eigentlich zu dieser Seite gehörenden Domains ag-intra.com, ag-intra.org und ag-intra.de werden von mir geschlossen bzw. gelöscht und unregistriert.

1.1. JDK

1.1.1. Software beschaffen
Um fertige Java Programme zu entwickeln brauchen wir einen Compiler und eine virtuelle Maschine, sowie die Java-Libraries. All das ist glücklicherweise im JDK von Sun enthalten, welches man sich kostenlos bei "http://www.javasoft.com" herunterladen kann. Den konkreten Pfad nenne ich nicht, weil Sun vorher gerne ein Formular ausgefüllt haben will, und weil man sich sowieso auf den dortigen Seiten etwas umsehen sollte. Schließlich gibt es dann noch die Möglichkeit sich dort für die Java Developers Connection anzumelden, was ich durchaus empfehle. Man erhält dann Newsletters mit Tips und Tricks und aktuelle Informationen zu Java. Noch besser ist der Zugang zu dem entsprechenden Bereich der JDC-Seite, wo man Pre-Releases von neuen Paketen, oder neuen JDK's etc. herunterladen kann, und auch weitere wertvolle Informationen findet. Das ganze ist natürlich kostenlos.

Die Dateien des JDK, die man bei Sun herunterladen muß sind jdk1_2_2-win.exe und jdk1_2_2-doc.zip(für die Windows Version). Wenn neuere Versionen des JDK als offizielles Release vorliegen, sollte man diese nehmen. (Ich selbst werde im Verlauf dieses Tutorials irgendwann unbemerkt auf die Release 1.3 wechseln :)

1.1.2. Software installieren
Das JDK wird unter Win9x per Doppelklick in den genehmen Pfad installiert. Gleichzeitig wird das JRE (vorzugsweise auf Laufwerk c:) installiert, die spätere Nutzer ihrer Software in jedem Fall brauchen (das JRE ist das Runtime Environment, also quasi das komplette Java ohne Compiler und Tools). Das aktuelle JRE hat aber noch den entscheidenen Vorteil, daß das aktuelle Browser Plug-In bei Netscape Navigator und Internet Explorer automatisch installiert wird. Damit werden Java-Applets in der Sun-JVM ausgeführt und nicht in den virtuellen Maschinen der jeweiligen Browser, die sowieso "buggy" sind (Achtung, nach Neuinstallation des Browsers ist das PlugIn futsch, aber eine darauffolgende Neuinstallation des JDK ist auch unproblematisch, die das PlugIn dann wieder installiert).
Das jdk1_2_2-doc.zip kann man in ein beliebiges Verzeichnis entpacken. Dabei entsteht eine Ordnerhierarchie, in der sich der Ordner "docs" befindet. Diesen kopiert man nun in das Programmverzeichnis "JDK12" (oder wie man das Verzeichnis genannt hat, in das das JDK installiert wurde. Jedenfalls muß sich der Ordner "docs" in der gleichen Ordnerebene befinden, wie die Ordner "bin", "include", "demo", "jre" und "lib".)

1.1.3. Pfade setzen
Unter Umständen muß der Classpath gesetzt werden. Unter welchen Umständen das so ist, siehe bitte Dokumentation von Sun. Sinnvoll ist es jedenfalls den Pfad zum Verzeichnis "bin" in der autoexec.bat zu setzen (zB:"PATH G:\Code\jdk12\bin;c:\Windows\command"). 
Um zu überprüfen, ob der Pfad gesetzt ist, gibt man am DOS-Prompt "path" ein. Sollte der Pfad nicht gesetzt sein, ruft man die autoexec.bat einfach am DOS-Prompt mit "c:\autoexec.bat" auf. Bei abschließender Kontrolle mit "path" sollte nun alles in Ordnung sein.

1.1.4. Kompilieren
Bevor wir gleich das erste Java Programm schreiben, eine kurze Erklärung, wie man kompiliert. Der Compiler heisst javac. Abgesehen von der Tatsache, daß man ihm diverse Parameter mit auf den Weg geben kann, reicht es erstmal zu wissen, daß man das kompilieren mit "javac prgname.java" auf dem DOS-Prompt aufruft. Die Quelltexte, also unsere Arbeit sind normale Textdateien, die jedoch die Endung ".java" haben (deswegen scheidet der DOS-Editor leider aus, da er nur dreibuchstabige Endungen unterstützt). 
Auch wenn es bessere Editoren für Programmierer gibt, werden wir hier Notepad.exe als Editor nutzen. Ist das Programm wie beschrieben kompiliert, startet man es mit der JVM so "java prgname". Hier wird dann die eigentliche Endung ".class", der Datei die der Compiler erzeugt hat weggelassen. Die JVM simuliert dem Programm einen Prozessor, der direkt Java versteht. Alle Java Programme laufen in der JVM (Java Virtual Machine).

1.2. Das erste Programm

1.2.1. Coden - Los geht's
Das erste Programm, welches wir jetzt schreiben, ist ein Konsolen Programm. Nach dem Start gibt es auf dem Standard-Output von Java, auf der Textkonsole, den Text "Hallo Welt" aus. Hier der Quelltext, den wir unter "HalloWelt.java" abspeichern müssen (der Dateiname MUß immer genauso geschrieben werden wie der Klassenname):

// Kommentar
public class HalloWelt { 
 public static void main(String[] args) { 
  System.out.println("Hallo Welt!"); //auf der Konsole ausgeben
 }
}

Wir kompilieren das Programm nun auf der MS-DOS-Eingabeaufforderung mit "javac HalloWelt.java", und wenn kein Fehler gemeldet wurde (was nicht passieren darf bei korrekter Java-Version und korrekt gesetzten Pfaden) starten wir das Programm mit "java HalloWelt". Das Programm gibt denn Text "Hallo Welt!" aus und beendet sich.
Was haben wir mit diesen Zeilen getan? Wir haben eine Klasseerzeugt. Alles was wir schreiben erzeugt im Prinzip eine Klasse. Die erste Zeile könnten wir uns schenken, sie dient nur dazu uns zu zeigen, daß man mit // Kommentare einleitet, das heisst, innerhalb einer Zeile wird alles nach // vom Compiler ignoriert. Dies sehen wir auch in Zeile 4, wo ein erklärender Text hinter der eigentlichen Programmfunktion steht. Lernt eins, nehmt Euch eins gleich zu Herzen: Kommentiert was das Zeug hält. Gutkommentierter Quellcode macht den Code auch noch nach einem Jahr verständlich (dies gilt auch für Variablen, Klassen- und Methodennamen). 
Eine Klasse kann ein lauffähiges Programm sein, ein neuer Datentyp oder auch sonst etwas anderes. Woran erkennt Java, daß es sich um ein lauffähiges Programm handelt? Eine Klasse die ein lauffähiges Programm sein soll, muß die Methode "public static void main(String arg[]) {}" in seiner Klasse haben. Genau das sehen wir bei dem HalloWelt-Programm. Genau in dieser Methode startet Java die Abarbeitung des Programms, mit ersten dort auftretenden Anweisung. Dies ist in diesem Fall "System.out.println("Hallo Welt!");". "System" ist eine Klasse aus dem Paket java.lang. "out" ist ein Feld, und stellt einen geöffneten Stream dar, der auf den Standard Output von Java (in der Regel das MS-DOS-Fenster oder eine sonstige Konsole) gerichtet ist. "println" ist eine Methode aus dem Paket java.io. Klingt kompliziert ? Ist es auch, deswegen erkläre ich das nicht näher. Dieser Methodenaufruf ist jedoch die gängige Methode, um auf der Konsole Ausgaben zu tätigen. Methodenaufrufen, bzw. nahezu alle Anweisungen folgt ein Semikolon um den Abschluß der Anweisung zu kennzeichnen. Bei einigen Java-Befehlen, zB. "if" wird genau eine Folgeanweisung erwartet. Wenn man jedoch mehrere Anweisungen benötigt, um die Konsequenz des "if" zu beschreiben, fügt man diese in einen Anweisungsblock ein, der durch { und } gekennzeichnet ist. Hinter der geschweiften Klammer muß dann kein Semikolon stehen, da Anfang und Ende des Blocks ja durch die Klammern gekennzeichnet sind. Dazu später mehr. Um vielleicht etwas herumzuspielen, könnte man die Anweisung in der main-Methode folgendermaßen abändern

 System.out.println("Hallo Welt"+arg[0]);

In der Methoden-Signatur (so nennt man die einleitende Zeile public static void main(String arg[])) wird durch String arg[] eine unbestimmte Menge von Parametern erwartet. Da die Menge unbestimmt ist, kann man Parameter auch weglassen. Die Parameter werden als String-Array übergeben. Auf diese Parameter kann man nun zugreifen, wenn man den Index des gewünschten Parameters angibt. In diesem Beispiel nehmen wir den ersten Parameter. Das heißt, daß wir das Programm HalloWelt nun zB. so aufrufen "java HalloWelt markt" (wenn wir jetzt keinen Parameter angeben, also das "markt" weglassen, dann erhalten wir eine Exception, also einen Fehler. Normalerweise fragt man daher erst ab, ob und wieviel Parameter übergeben wurden). Das Programm gibt nun "Hallo Weltmarkt" aus. Wir sehen an diesem Beispiel, wie man auf Parameter zugreift. Hätten wir noch einen zweiten Parameter, würden wir ihn mit arg[1]adressieren. "arg" ist in diesem Beispiel übrigens frei gewählt, man könnte auch "argumente" oder "paras" oder sonstwas schreiben. Wir sehen in diesem Beispiel auch wie die einfache String-Verkettung funktioniert. Nämlich einfach durch aneinanderhängen von Strings mit dem Zeichen "+" (Siehe auch unten bei der Erklärung zu Strings).

1.3. Das zweite Programm

1.3.1. Zwei Klassen
Wir werden das Programm "HalloWelt" jetzt mit einer zweiten Klasse erweitern, so daß wir nahezu alle Prinzipien eines Java-Programms zeigen und erläutern können. Wir werden nun also ein Programm haben, daß aus zwei Klassen besteht. Die Klasse, die die Methode "public static void main(String arg[]) {}" implementiert (also enthält) steuert den Programmablauf. Mit der zweiten Klasse, werden wir einen neuen Datentypen erstellen, also eine Objektdefinition, die von der ersten Klasse verwendet wird. Diese zweite Klasse sollte fast alle objektorientierten Eigenschaften von Java demonstrieren (bis auf die Vererbung). Sehen wir uns zuerst die zweite Klasse im Quellcode an:

public class InterGruss{ 
  private String gruss;             //neue Attribute für meine Klasse
  private String sprache;

  public InterGruss() {             //Konstruktor für meine Klasse
      this.gruss="Hallo Welt";
      this.sprache="deutsch";
  }
  public InterGruss(String lang) {  //Zweiter Konstruktor für meine Klasse
      if (lang.equals("deutsch")) {
          this.gruss="Hallo Welt";  //(Überladen)
          this.sprache=lang;
      }
      else if (lang.equals("english")) {
          this.gruss="Hello World";
          this.sprache=lang;
      }
      else {
          this.gruss="Hallo Welt";
          this.sprache="deutsch";
      }
  }

  public void setLang(String lang) {  //erste Methode, setzen der Sprache
      if (lang.equals("deutsch")) {   //zur Laufzeit
          gruss="Hallo Welt";
          sprache=lang;
      }
      else if (lang.equals("english")) {
          gruss="Hello World";
          sprache=lang;
      }
      else {
          gruss="Hallo Welt";
          sprache="deutsch";
      }
  }
  public String getSprache() {   //zweite Methode, zum Abfragen der
      return sprache;            //Sprache zur Laufzeit
  }
  public String getGruss() {     //dritte Methode, zum Abfragen der
      return gruss;              //Sprache zur Laufzeit
  }
}

Ziemlich lang ? Ja, stimmt, Uff. Aber daran können wir jetzt schön erklären. Aber zuvor benötigen wir noch das tatsächliche Programm, denn InterGruss hat ja keine main-Methode. Unser HalloWelt-Programm werden wir jetzt also auch noch ein wenig abändern:

public class HiWelt { 
 public static void main(String[] arg) {
  if((arg.length>1) || (arg.length==0)) {
   System.out.println("Bitte geben Sie als Argument eine Sprache ein !\n");
   System.out.println("Moeglich ist: ");
   System.out.println("deutsch ");
   System.out.println("english ");
   System.exit(0);
  }
  if((arg[0].equals("deutsch")) || (arg[0].equals("english"))) {
   InterGruss myGruss = new InterGruss(arg[0]);
   System.out.println(myGruss.getGruss());
  }
  else {
   System.out.println("Moegliche Parameter sind nur: \n");
   System.out.println("deutsch \n");
   System.out.println("english \n");
   System.exit(0);
  }
 }
}

Was haben wir hier alles getan? Eigentlich gar nicht so schwer. Zumidest was das Hauptprogramm angeht. Aber da das HauptProgramm die zweite Klasse verwendet (sehen wir an der Zeile "InterGruss myGruss = new InterGruss(arg[0]);") fangen wir ruhig einmal mit der Klasse InterGruss an.

1.3.2. Signatur
Die Klasse beginnt mit ihrer Signatur

public class InterGruss {} 

public ist dabei die sogenannte Zugriffsklasse für diese Klasse. public vereinbart dabei, daß die Klasse überall verwendet werden darf. Würde public weggelassen, so könnte die Klasse nur in dem Paket benutzt werden, in dem sie definiert ist. Es darf übrigens innerhalb einer Datei nur eine als public deklarierte Klasse enthalten sein. Der Ausdruck class in der Signatur besagt, daß wir ein Klasse definieren werden. Man könnte hier auch interfaceschreiben. Interfaces sind Klassen, die zwar abstrakt ihre Methoden deklarieren, aber keinen konkreten Code enthalten, der diese Methoden irgendwie ausführt. Dies muß dann später in einer anderen Klasse geschehen, die dieses Interface implementiert. Konzentrieren wir uns jetzt aber wieder auf die class-Klassen, weil wir mit diesen zunächst am meisten zu tun haben. Unsere Klasse bekommt den Namen InterGruss, weil sie internationale Grüße ermöglicht. 
Klassennamen beginnen übrigens immer mit einem Großbuchstaben. Bestehen Sie aus mehreren Worten werden die Anfangsbuchstaben der Folgeworte auch groß geschrieben (Bsp: MeineMehrwortKlasse).
Wenn die Klasse so heißt, dann muss auch die Datei in der sie gespeichert ist so heißen, also InterGruss.java.

1.3.3. Attribute
Als nächstes werden die Attribute der Klasse deklariert:

private String gruss;
private String sprache;

Wenn wir mal vorgreifen, und nach vorne sehen, stellen wir fest, daß wir keine main-Methode in unserer Klasse haben. Also ist diese Klasse kein Programm. Es gäbe auch noch die Möglichkeit eine statische Klasse zu erzeugen, die dann wie ein Funktionsbibliothek funktioniert. Ist aber hier nicht der Fall. Mit unserer Beispielklasse definieren wir einen neuen Datentyp namens InterGruss. Und dieser Datentyp kann zwei Werte speichern: die Sprache und den dazugehörigen Gruss. Wir brauchen also zwei Variablen zu diesem Zweck. Diese werden ganz vorn in der Klasse deklariert. Direkt nach der Signatur. Beide sollen Strings aufnehmen, daher wird ihr Typ als String angegeben (in anderen Klassen könnten wir nachher an dieser Stelle auch InterGruss als Typen angeben, da dies ja auch ein Datentyp wird). 
Variablen werden übrigens am Anfang immer klein geschrieben. Bestehen Sie aus mehreren Worten werden die Anfangsbuchstaben der Folgeworte groß geschrieben (Bsp: meineMehrwortVariable)
An dieser Stelle könnte übrigens auch schon eine Wertzuweisung stattfinden, zB.private String gruss="HalloWelt!"; Machen wir in diesem Fall aber nicht, da wir dies im Konstruktor erledigen (siehe unten). private ist in diesem Fall übrigens ein Modifier. Hier könnte zB. auchpublic stehen (oder static etc.). Wäre das Attribut, also die Variable meiner Klasse, public, dann könnte man zB. bei einer InstanzmyGrussmitmyString=myGruss.gruss direkt auf die Variable zugreifen. Ist die Variable jedoch private, dann geht das nicht. Daher implementiert man bei so einem Datentypen auch immer Zugriffsmethoden wie setVarible(Wert) und getVariable(). Dies ist dann eigentlich saubere objektorientierte Programmierung, und daher sollten die Attribute einer Klasse immer private sein. Nur Code innerhalb der Klasse kann dann auf die Variablen zugreifen. Andere Klassen, bzw. das Hauptprogramm kann die Werte nur über die Zugriffsmethode setzen und abfragen. So kapselt man Wert und darauf anwendbare Funktion in einem Objekt (eine Eigenschaft von objektorientierter Programmierung).

1.3.4. Konstruktoren
Als nächstes kommen die Konstruktoren einer Klasse:

public InterGruss() {             //erster Konstruktor
 this.gruss="Hallo Welt";
 this.sprache="deutsch";
}
public InterGruss(String lang) {  //zweiter Konstruktor
 if (lang.equals("deutsch")) {    //(überladen)
  this.gruss="Hallo Welt";
  this.sprache=lang;
 }
 else if (lang.equals("english")) {
  this.gruss="Hello World";
  this.sprache=lang;
 }
 else {
  this.gruss="Hallo Welt";
  this.sprache="deutsch";
 }
}

Wenn ich einen derartigen Datentyp geschaffen habe, dann arbeite ich nicht mit der Klasse selbst, sondern immer mit Exemplaren, oder Instanzen dieser Klasse. Ich verwende die Klasse also nicht direkt, sondern erzeuge mein eigenes Objekt mit den Klasseneigenschaften. Die Klasse ist eigentlich nur soetwas wie die Beschreibung der Objekte, mit denen ich schließlich arbeite. Diese Instanzen erzeuge ich im Hauptprogramm zB.  mit der Zeile InterGruss myGruss = new InterGruss(arg[0]); In diesem Augenblick wird der nötige Speicherplatz für mein Objekt mit dem Namen myGruss geschaffen, und es werden in diesem Augenblick immer die Anweisungen in den Konstruktoren der Klassenbeschreibung ausgeführt. Hier kann man sozusagen die Initialisierung des Objektes vornehmen, und zum Beispiel die Attribute mit voreingestellten Werten belegen. Deswegen haben wir bei der Deklaration der Attribute auch keine Wertzuweisung vorgenommen. Die Wertzuweisung im Konstruktor ist nämlich effektiver, da ich mehr als einen Konstruktor haben kann. Sehen wir uns den ersten Konstruktor an. Er sieht eigentlich wie ein normaler Methodenaufruf aus. Das es sich um einen Konstruktor handelt, erkennt Java daran, daß die Methode genauso heißt, wie die Klasse selbst. 
Konstruktoren müßen immer genauso wie die Klasse heißen.
Darauf folgt nun ein leeres Klammerpaar. Das bedeutet, daß dieser Konstruktor keine Parameter erwartet. Er würde durch new InterGruss(); aufgerufen. In dem folgenden Anweisungsblock werden dann die Attribute mit der Einstellung "deutsch" voreingestellt. Das hier auftretenden Schlüsselwort this.erklären wir beim zweiten Konstruktor.

Die Signatur des zweiten Konstruktors unterscheidet sich von der des ersten vor allem dadurch, daß hier ein Parameter erwartet wird. Nämlich ein String, der innerhalb des Konstruktors dann als "lang" angesprochen wird. Im Kommentar lesen wir das Wort "überladen". Warum haben wir einen zweiten Konstruktor, und warum überlädt er den Konstruktor? Fantastischerweise erlaubt es Java Varianten für den Aufruf von Methoden zuzulassen. Stellen wir uns eine Methode zum Zeichnen eines Kreises vor. Der Aufruf von Kreis(Radius); zeichnet einen schwarzen Kreis mit dem benannten Radius. Mit einer Variante könnte ich aber auch Kreis(Radius, Farbe); aufrufen. Dann kann ich die Farbe selbst bestimmen. Derjenige der die Klasse später verwendet, kann sich die für seinen Anwendungszweck passende Aufrufvariante selbst aussuchen. Eine Klasse mit allen sinnvollen Varianten ist komfortabel zu verwenden. In unserem konkreten Beispiel hat ja der erste Konstruktor die Werte fest auf "deutsch" voreingestellt, wenn ein Objekt der Klasse ohne Parameter erzeugt wird. Manchmal ist es notwendig, oder der Programmablauf sieht es vor, daß die Klasse beim Erzeugen bereits einen definierten Wert annimmt. Dazu dient der zweite Konstruktor. Java erkennt selbständig welcher Konstruktor zu verwenden ist, anhand des Aufrufs im Programm. Wird beim Aufruf kein Parameter übergeben, dann wird unser erster Konstruktor verwendet. Wird ein String übergeben, wird automatisch der zweite Konstruktor verwendet. Hätten wir einen dritten Konstruktor mit der Signatur public InterGruss(int index) {} könnten wir auch einen Integerwert als Parameter übergeben. Wir können soviele Konstruktoren erzeugen wie wir wollen, oder wie es nötig ist. Nur eins geht nicht, ich kann keinen weiteren Konstruktor erzeugen, der auch genau einen String als Parameter erwartet. Die Konstruktoren müßen sich durch die Anzahl der Parameter und deren Typ unterscheiden. Sonst kann Java nicht zuordnen, welcher Konstruktor zB beim Aufruf mit einem String zu verwenden ist. Diese mehrfache und variable Möglichkeit zum Programmieren von Konstruktoren und überhaupt Methoden nennt man in der objektorientierten Terminologie "überladen". Soviel zum Thema mehrere Konstruktoren.
Im zweiten Konstruktor selbst sehen wir einige Vergleichsanweisungen. Wie in jeder anderen Programmiersprache auch, gibt es in Java die if-Anweisung. Sie hat das grundsätzliches Format if(boolscher Ausdruck) Anweisung;. Der boolsche Ausdruck muß auch einer sein. Wie in Java üblich, ist True nicht das gleiche wie 1. Wenn der Ausdruck in der Klammer wahr ist, dann wird genau die eine Anweisung hinter den Klammern ausgeführt. Wenn es, wie auch in unserem Beispielprogramm, nötig ist, mehrere Anweisungen im Ergebnis auszuführen, dann müßen diese in geschweiften Klammern, also einem Anweisungsblock stehen. Unter diesen Umständen ist dann auch das Semikolon wegzulassen, da ja wie bereits oben angemerkt, die Klammern selbst Anfang und Ende des Blocks signalisieren. Der erste Vergleich, den wir durchführen lautet if (lang.equals("deutsch")). Hier sehen wir eine Besonderheit. In anderen Sprachen hätten wir vielleicht geschrieben if (lang=="deutsch"). Dies ist in Java nicht möglich (okay, manchmal schon, aber dann ist es trotzdem falsch. Siehe dazu das Bonus-Beispiel TicTacToe-Applet). Vergleiche mit Vergleichsoperatoren wie "==" sind nur mit den primitiven Datentypen möglich. String ist aber, und das sehen wir schon an der Großschreibung des Objekttypen, ein Objektdatentyp. Da dieser Vergleich nicht möglich ist, ist freundlicherweise in der Klasse String die Methode .equals("Parameter") implementiert, die im Trefferfall ein boolsches True zurückliefert. Und ein boolsches True oder False brauchen wir ja beim if-Vergleich. Ist der Parameter "deutsch" gewesen, werden im folgenden Anweisungsblock die Attribute auf "deutsch" gesetzt. War der Parameter es nicht, wird mit der Anweisung nach dem Anweisungsblock fortgesetzt. Diese lautet else if (lang.equals("english")). elsenach if bedeutet einfach "ansonsten". Und ansonsten wollen wir noch einmal einen Vergleich anstellen. Wir prüfen nun ob der Parameter "english" lautet, und setzen danach im folgenden Anweisungsblock entsprechend die Attribute auf englische Werte.

Achtung !!! Fehler in der Erklärung möglich:else bezieht sich in Java immer auf das vorhergehende if. Daher kann es sein, sich das letzte alleinstehende else nur auf den else-if-english-vergleich bezieht. Siehe auch Programmierhandbuch Java 2 Plattform Seite 16.

Das dritte else soll schließlich alle anderen Möglichkeiten abfangen, den Parameter ignorieren und alle Attribute auf deutsche Werte einstellen. Kommen wir nun zum letzten noch offenen Begriff, dem .this. Das Schlüsselwort this. dient dazu einen Verweis auf das eigene Objekt herzustellen. Gerade in den Parameterdefinitionen von Konstruktoren müßte man sonst nämlich wieder neue Variablennamen erfinden, die eigentlich dasselbe bedeuten wie das Attribut. Wir haben im Beispiel so eine Erfindung eingeführt, indem der übergebene Parameter lang(uage) heisst. Eigentlich bezeichnet er ja aber die Sprache. Mit dem Verweisoperator this. hätte man den zweiten Konstruktor nämlich auch so schreiben können (und damit eigentlich besser):

public InterGruss(String sprache) {  //zweiter Konstruktor
 if (sprache.equals("deutsch")) {    //(überladen)
  this.gruss="Hallo Welt";
  this.sprache=sprache;
 }
 else if (sprache.equals("english")) {
  this.gruss="Hello World";
  this.sprache=sprache;
 }
 else {
  this.gruss="Hallo Welt";
  this.sprache="deutsch";
 }
}

Beachten Sie hier einmal die Zeile 4. Wir haben eines unserer Attribute des Datentyps "sprache" genannt, damit er das im Namen reflektiert, was er auch meint. Als Parameter für die Initialisierung des Datentyps soll aber auch die Möglichkeit bestehen, die Sprache zu übergeben. Hier nenne ich den Parameter in der natürlichen Sprache natürlich eigentlich auch wieder "sprache". Wenn ich den Parameter dann einem Attribut zuweise, würde in Zeile 4 stehen: sprache=sprache. Damit die Bezeichnung dieses Parameters aber ermöglicht wird, gibt es eben die Schreibweise this.sprache=sprache. Damit weiß der Compiler, daß mit this.sprache das Attribut der Klasse gemeint ist, und mit sprache, der nur in diesem Konstruktor gültige Parameter des Konstruktors. this.sprache und sprache sind in diesem Konstruktor zwei völlig unterschiedliche Dinge. sprache ist hier auch etwas anderes als das Attribut der Klasse. Etwas kompliziert, aber es vermeidet, daß man für ein und dieselbe Sache ständig neue Begriffe erfinden muß, wie z.B.mySprache1, mySprache2etc.

1.3.5. Zugriffsmethoden
Wie oben schon gesagt, sind die Attribute als private deklariert, und wir können nicht von außen darauf zugreifen. Ich will aber selbsverständlich wissen, welche Sprache eingestellt ist, und wie die entsprechende Begrüßung dazu lautet, sonst bräuchte ich die Klasse ja gar nicht. Daher implementieren wir als Methoden, die sogenannten Zugriffsmethoden, mit denen wir die Attribute des Datentyps InterGruss setzen und auslesen können. Die erste Methode ist die Methode zum Setzen des Wertes zur Laufzeit:

public void setLang(String lang) {  //erste Methode, setzen der Sprache
 if (lang.equals("deutsch")) {      //zur Laufzeit
  gruss="Hallo Welt";
  sprache=lang;
 }
 else if (lang.equals("english")) {
  gruss="Hello World";
  sprache=lang;
 }
 else {
  gruss="Hallo Welt";
  sprache="deutsch";
 }
}

Die Anweisungen, die in dieser Methode ausgeführt werden kennen wir eigentlich schon aus dem Konstruktor. Wir haben hier nur auf das Schlüsselwortthis.verzichtet. Das Setzen soll ja von außen gesteuert werden, daher erwarten wir einen Parameter. Entsprechend ist die Signatur der Methode aufgebaut. Der ganze Block an sich dürfte nach den bisher erläuterten Dingen eigentlich verständlich sein. Die Adressierung der Methode erfolgt dann nach folgendem Schema. Bevor ich mit diesem Objekt arbeiten kann, muß ich ja wie oben erwähnt mit InterGruss myGruss = new InterGruss(arg[0]); erstmal meine Instanz dieses Objektes erzeugen, mit der ich schließlich arbeiten kann. Mein Objekt heißt also myGruss. Wie ich schon sagte kann ich nicht einfach mit myGruss.sprache="deutsch" auf die Attribute zugreifen. Nun habe ich aber eine Methode, die das für mich erledigt. Da die Methode ja innerhalb der Klasse definiert ist, darf sie selbst auf die Attribute zugreifen.
Auf Variablen, die als private deklariert sind, kann ich von außerhalb der Klasse nicht zugreifen. Methoden, die ebenfalls in der entsprechenden Klasse implementiert sind, können aber sehr wohl auf diese Variablen zugreifen.
Mit myGruss.setLang("deutsch"); kann ich nun also eine Sprache in meinem Objekt einstellen. Die Methode gibt keinen Wert zurück, was durch das Schlüsselwort void im Quelltext signalisiert wird (vielleicht sollte die Methode einen Wert zurückgeben, aus dem ersichtlich ist, ob das Setzen der Sprache erfolgreich war?).
Nun bin ich also in der Lage, in meinem Objekt die Sprache einzustellen. Ich möchte das Objekt aber auch befragen können, welche Sprache aktuell eingestellt ist:

public String getSprache() {   //zweite Methode, zum Abfragen der
 return sprache;               //Sprache zur Laufzeit
}

Auch diese Methode ist eigentlich ganz einfach. Ihr wird kein Parameter übergeben. Sie selbst liefert einen String zurück, wie durch das Schlüsselwort String signalisiert wird. Die Anweisungreturn sprache; liefert eben den Wert des Attributes sprachenach aussen, also an den Aufrufer der Methode. Die Adressierung lautet dann zb. myString=myGruss.getSprache();
Da wir im Beispiel ja den dazugehörigen Begrüßungstext haben wollen, brauchen wir noch eine weitere Abfragemethode, und diese lautet

public String getGruss() {   //zweite Methode, zum Abfragen der
 return gruss;               //Sprache zur Laufzeit
}

Diese Methode entspricht natürlich ganz genau der vorhergehenden und wird daher nicht weiter erläutert.

Interessanterweise ist an dieser Stelle die zweite Klasse fertigt. Wir haben darin zwei Attribute festgelegt, dafür gesorgt, daß beim Anlegen der Klasse durch die Konstruktoren  gleich vernünftige Werte eingestellt sind und schließlich zwei drei Methoden definiert, mit denen der Zugriff auf die Attribute möglich ist. Die Klasse speichert eine Sprache und liefert uns dazu den passenden Begrüßungstext in dieser Sprache zurück. 

1.3.6. Die Hauptklasse
Wir können die Klasse nicht direkt verwenden, da sie keine main-Methode implementiert. Also benötigen wir jetzt ein Hauptprogramm, welches diese Klasse verwendet. Auf den ersten Blick sieht unser oben gezeigtes HiWelt-Programm viel länger aus als unser erstes HalloWelt-Programm. Das liegt aber nur daran, daß es sehr ausführliche Hinweise an den Benutzer ausgibt. Um das Programm zu analysieren, werden wir diese Blöcke kürzen:

public class HiWelt { 
 public static void main(String[] arg) {
  if((arg.length>1) || (arg.length==0)) {
   System.out.println("Bitte geben Sie als Argument eine Sprache ein !\n");
   System.exit(0);
  }
  if((arg[0].equals("deutsch")) || (arg[0].equals("english"))) {
   InterGruss myGruss = new InterGruss(arg[0]);
   System.out.println(myGruss.getGruss());
  }
  else {
   System.out.println("Parameter sind nur: \ndeutsch \nenglish \n");
   System.exit(0);
  }
 }
}

Zuerst erkennen wir erleichtert, daß wir hier direkt am Anfang wieder eine main-Methode haben, an der das Programm gleich gestartet wird. Der Aufruf des Programms soll zB so erfolgen: java HiWelt english  Das Programm erwartet also in jedem Fall einen Parameter (wir haben zwar auch für den Fall, daß kein Parameter vorhanden ist Vorkehrungen in unserer InterGruss-Klasse getroffen, aber unser HiWelt macht davon keinen Gebrauch). Weil genau ein Parameter erwartet wird, wird auch zuerst auf zuviele oder zuwenige Parameter geprüft (arg ist ja ein Array. Siehe also auch Erklärung zu Arrays). Der Parameter arg liegt ja als Arrayobjekt vor. Da für Arrays auch eine Methode.length existiert, können wir damit auf mehr als einen Parameter oder keinen Parameter prüfen. In diesem Fall gehen auch die Vergleichsoperatoren > und ==. Jeder einzelne Ausdruck nimmt einen Wahrheitswert an. Da im Falle von es trifft entweder das eine ODER das andere zu, die folgende Anweisung ausgeführt werden soll, verwenden wir einen logischen Vergleichsoperator, nämlich den oder-Operator ||. Ist auch nur einer der beiden Ausdrücke wahr, ist der gesamte Ausdruck wahr und die if-Bedingung ist erfüllt. Man kann das auch mit einem logischen UND machen, wenn auf jeden Fall beide Ausdrücke wahr sein sollen. Das wäre dann&&, aber ist im hiesigen Beispiel natürlich sinnlos. Trifft unsere Bedingung zu, dann hat man zuwenig oder zuviel Parameter angegeben. Daher wird mit der bereitsbekannten Methode System.out.println("Bitte ...");ein Hinweis an den Nutzer ausgegeben. Da eine weitere Abarbeitung des Programms nun sinnlos ist, wird es sofort und auf der Stelle mit der nächsten Anweisung beendet. Dafür sorgt die Methode System.exit(0); Das Programm, und die virtuelle Javamaschine in der es läuft, werden sofort beendet. Es wird zwar kein Fehler als Rückgabewert zurückgegeben (die 0), aber dies ist ja durch den Hinweis dokumentiert.
War die Bedingung falsch, bedeutet es, daß genau ein Parameter übergeben wurde. In diesem Fall prüfen wir als nächstes, ob es sich um die erlaubten Parameter handelte. Wenn nicht, wird die else Anweisung ausgeführt, die wiederum einen Hinweis ausgibt, und das Programm beendet. Handelt es sich aber um die erlaubten Parameter, tut das programm endlich was es soll, und vor allem: Es verwendet unsere zweite Klasse.

if((arg[0].equals("deutsch")) || (arg[0].equals("english"))) {
 InterGruss myGruss = new InterGruss(arg[0]);
 System.out.println(myGruss.getGruss());
}

Weil das Arbeiten mit Variablen, Instanzen und Referenzen so wichtig ist, wird es etwas weiter unten noch einmal detaillierter beschrieben. Hier erzeugen wir eine Instanz der Klasse InterGruss, und erhalten damit ein Objekt, daß die von uns gewünschten Werte aufnimmt und manipulieren kann. Wir sehen, daß ein Argument übergeben wird InterGruss(arg[0]) und somit unser zweiter Konstruktor aufgerufen wird. Das Objekt hat also gleich den richtigen Inhalt. Als nächstes geben wir den Rückgabewert der Methode myGruss.getGruss() direkt mit der Anweisung System.out.println(); aus. Damit haben wir dem Hauptprogramm die gewünschte Sprache übergeben, und wurden dann in dieser Sprache begrüßt.

1.4. Varibalen, Referenzen und Instanzen
Das erste Beispiel dieses Tutorials haben wir jetzt abgearbeitet. Weil es aber wichtig ist, werden wir uns zum Abschluß noch einmal mit der trockenen Theorie der Variablen beschäftigen.Das Thema ist so kompliziert, daß man gar nicht weiß, wo man anfangen soll. Es ist nicht an sich kompliziert, sondern nur aufgrund von einigen Besonderheiten. Wenn ich eine Variable verwenden will, dann kann ich dazu sogenannte primitive Datentypen verwenden oder auch Objektdatentypen. Ich kann nur beide im Verlauf des Programms nicht gleich behandeln. Daher werden insbesondere am Anfang immer wieder Fehler auftauchen, weil man mit einem bestimmten Objektdatentypen arbeitet, aber eine Operation auf ihn anwenden will, die nur bei primitiven Datentypen möglich ist. Zu allem Überfluss kann ich ja, wie oben gezeigt, durch Erzeugen von Klassen auch noch eigene Datentypen definieren.

1.4.1. Primitiven Datentypen
Fangen wir erstmal bei den primitiven Datentypen an. Davon gibt es vier:

- boolean (logischer Typ)
- int, byte, short, long (Ganzzahltypen)
- float, double (Gleitpunkttypen)
- char (Zeichentyp)

Die Typbezeichnung primitiver Datentypen beginnt immer mit einem Kleinbuchstaben. Diese Datentypen werden direkt mit ihrem Wert abgelegt, so daß ich direkt darauf zugreifen kann. Bei Vergleichen z.B. in einer if-Bedingung kann ich mit dem Operator "==" arbeiten. Diesen kann ich nur bei primitiven Datentypen verwenden (abgesehen vom Vergleich i==null). Der Erzeugungsprozeß ist sehr einfach. Dazu kann man z.B. schreiben:

int i;
i=10234;

In der ersten Zeile deklariere ich i als primitiven Datentyp. Ab Java 1.2 wird i dann auch gleich mit einem Wert, nämlich der 0 initialisiert, besitzt also schon einen Inhalt auf den ich gleich zugreifen kann. In der zweiten Zeile weise ich der Variablen i einen Wert mit dem Zuweisungsoperator "=" zu. Primitive Datentypen sind also immer durch ihre Definition initialisiert.
Wenn ich sowieso wünsche, daß der Wert am Anfang zugewiesen wird, kann ich das auch kürzer schreiben:

int i=10234;

Ich kann diesen primitiven Datentyp jetzt auch bei Vergleichen heranziehen:

if(i==10233) Anweisung;

Da man direkt auf den Wert eines primitiven Datentyps zugreifen kann, ist dieser Vergleich zulässig. Die primitiven Datentypen sind übrigens alle vorzeichenbehaftet. Der Wert byte kann somit z.B. positive Werte nur bis 127 speichern. Ein short speichert Werte bis 32.768 und ein int-Datentyp speichert Werte bis über 2 Milliarden. Diese Auflösung ist plattformunabhängig.
Die boolschen Datentypen nehmen nur die Werte true und false an. Ein Vergleich mit 1 oder 0 ist also unzulässig.
Wenn ich einer Variablen mit dem primitiven Datentyp longeinen Wert als Literal zuweisen will, darf ich das nicht mit long ml=10;machen, sondern muß es so schreiben:

long ml=10L;

So kann Java das Literal eines long-Typen vom Literal eines int-Typen unterscheiden. Wenn ich Variablen Casten will, d.h. einer Variablen den Wert einer Variablen anderen Typs zuweisen will, so klappt das immer, wenn ich einem großen Zahlentyp einen Wert eines kleineren Zahlentyps zuweise. Manchmal will man aber auch einen Wert mit Gewalt in einen anderen Typen hineinpressen. Dann kann man Casten indem man dem zuzuweisenden Wert einfach den gewünschten Zieltyp in Klammern voranstellt. z.B. so:

char mc = 'a';
int mi = (int)mc;

In diesem Fall enthält dann übrigens die Variable miden Zahlencode für das Unicode-Zeichen "a".

Warum gibt es die primitiven Datentypen eigentlich? Java möchte eine streng objektorientierte Sprache sein, und dürfte so etwas wie primitive Datentypen eigentlich gar nicht zulassen. Normalerweise müßten alle Variablen als Objekte realisiert werden, wie wir sie gleich kennenlernen werden. Die primitiven Datentypen sind jedoch ein Zugeständnis an die Effizienz beim Programmieren. Stellen Sie sich vor, Sie müßten jedesmal beim Programmieren einer Schleife die Zählervariable erst Deklarieren, dann Erzeugen und schließlich die Werte zuweisen. Auch bei den privaten Variablen einer Klasse bietet sich die Verwendung von primitiven Variablen zur Vereinfachung an. Kommen wir daher nun zu den nächsten Datentypen.

1.4.2. Objektdatentypen 1 - Wrapper-Klassen
Für jeden der primitiven Datentypen gibt es eine sogenannte Wrapper-Klasse. Diese stellt dann keine primitive Variable mehr dar, sondern ein Objekt. Der Konvention über die Schreibweisen gemäß, beginnen die Namen der Wrapper-Klassen wie bei Objekten üblich mit einem Großbuchstaben. So ist der Name der Wrapper-Klasse für den primitiven Datentyp int gleichInteger. Die Wrapperklasse für char heißt Character, die für float Float, die für double Doubleund soweiter. Wenn ich vorhin bei den primitiven Datentypen gesagt habe, daß ich auf die Werte direkt zugreifen kann, so gilt das bei den Objektdatentypen nicht mehr. Der Objektdatentyp speichert nämlich nicht mehr direkt den Wert der Variablen, sondern eine Referenz darauf (daher redet man auch von Referenzklassen). Wenn ich den Wert einer primitiven Variablen wissen möchte, dann greife ich direkt darauf zu. Wenn ich den Wert einer Variablen einer Wrapper-Klasse ermitteln will, dann nutze ich eine Methode dieser Klasse und übergebe ihr die Referenz auf das Objekt. In der Referenz ist also nicht der Wert der Variablen abgespeichert, sondern so etwas wie ein Zeiger auf das Objekt, welches diesen Wert (für mich völlig transparent) verwaltet. Wenn ich also zu dieser Referenz 4 hinzuaddieren würde, dann würde sich nicht der Wert der Variablen um 4 erhöhen, sondern der Zeiger würde um 4 verändert, und nicht mehr dorthin zeigen, wo das Objekt gespeichert ist, sondern irgendwohin wo nicht das Objekt abgelegt ist.
Um eine Variable mit einer Wrapper-Klasse zu erzeugen geht man in der Regel in drei Schritten vor:

Integer myInt;  // Deklaration der Variablen. Die Variable zeigt auf 
                                 // "null" (aus dem Paket java.lang.util) also auf "nichts"

new Integer();  // Erzeugen der Variablen. Der Speicherplatz für diese
                                 // Variable wird jetzt erzeugt. Sie zeigt jetzt also auf
                                 // diesen Speicherbereich

myInt=new Integer(10234);  // Zuweisen des Speicherbereichs zu der 
                                                        // deklarierten Variable

Diese ganze Prozedur kann auch abgekürzt werden:

Integer myInt=new Integer(10234);

myInt ist jetzt das Objekt mit dem ich arbeite. Sein Typ ist die Wrapperklasse Integer. myInt ist eine Instanz der Klasse Integer. Ich habe nur eine Klasse Integer in Java, aber ich kann mit beliebig vielen Instanzen dieser Klasse in meinen Programmen hantieren. Der Ausdruck "new" entspricht praktisch dem Aufruf des Konstruktors dieser Klasse. Damit ist ein Objektdatentyp genau dann initialisiert, wenn der Konstruktor aufgerufen wird. 
Da der Wert nicht direkt in myInt gespeichert ist, sondern die Referenz auf genau diese Instanz, kann ich myInt so nicht bei einem Vergleich mit dem Operator "==" verwenden wie einen primitiven Datentypen:

if(myInt==10233) Anweisung;  // ganz ganz falsch !!!

Vergleiche sind aber nötig. Daher implementieren auch alle Wrapper-Klasse spezielle Zugriffsmethoden mit denen die Objekte manipuliert werden können. Zum Vergleich gibt es zum Beispiel überall die Zugriffsmethode equals(). Der oben genannte Vergleich würde dann zB. so aussehen:

if(myInt.equals(10233)) Anweisung; // richtig !!!

Die Methode equals() ist so implementiert, daß sie einen Wahrheitswert zurückgibt, und somit hervorragend für die if-Anweisung geeignet ist. 
Um auf den Wert dieser Variablen zuzugreifen, gibt es z.B. die MethodeintValue(), die den Inhalt der Variablen entsprechend zurückgibt, z.B.:

System.out.println(myInt.intValue());

Interessant ist hierbei, daß es bei der Klasse Integer z.B. auch die Methode doubleValue() gibt, die den als int abgelegten Wert im double-Format ausgibt. Auf diese Weise ist kein Casting notwendig. Um nachzusehen, welche Methoden möglich sind, hält man sich wie oben erwähnt am besten immer die API-Dokumentation des JDK im Browser offen, so daß man dort immer schnell nachsehen kann.

1.4.3. Objektdatentypen 2 - String-Klasse
Bis jetzt haben wir noch nicht soviel über Strings gehört. Strings sind eigentlich keine primitiven Datentypen, aber weichen in der Handhabung auch etwas von den Wrapper-Klassen ab. In der Praxis gestaltet sich der Umgang mit Strings relativ einfach. Dabei wird einem jedoch nicht immer gleich offenbar, was eigentlich im Hintergrund passiert. Einen String kann ich z.B. so erzeugen:

String myString = new String("Hallo");

Java erlaubt jedoch die vereinfachte Schreibweise

String myString = "Hallo";

Interessanterweise ist der Ausruck "Hallo" sofort bei seinem Auftreten ein String-Objekt. Diese Verhaltensweise gibt es nur bei Strings. Deswegen könnte man z.B. auf so einen String direkt eine Methode der Klasse String anwenden. z.B.:

len = "Hallo".length();

Da ein String gleich ein Objekt ist, darf man Strings auch nicht mit dem Vergleichsoperator "==" vergleichen. Dies geht (man kann es gar nicht oft genug wiederholen) nur mit primitiven Datenobjekten. Da der Vergleich "Hallo"=="Hello" nicht zulässig ist, greifen wir wieder auf die bekannte Schreibweise zurück:

if(myString.equals("Hallo")) Anweisung;

Es gibt eine feine Sache in Java, nämlich die String-Verkettung. Dies ist auch dann genau das, was man in der Praxis oft tut, aber wobei ich vorhin meinte, daß man nicht weiß was im Hintergrund passiert. Zeigen wir erstmal eine einfache String-Verkettung:

String myString = "Hallo"+" Welt";

Wie wir sehen ist der Verkettungsoperator ein einfaches Plus-Zeichen. Und jetzt ist es an der Zeit zu erwähnen, daß der Inhalt eines String-Objektes unabänderlich ist. Das klingt erstmal ungewöhnlich. In  VisualBasic nutze ich z.B. einen String, weise ihm zur Laufzeit beliebig lange andere Inhalte zu, und dies ist hier nicht möglich. Die gute Nachricht lautet, daß es noch eine Klasse gibt, nämlichStringBuffer. Hier kann man mit veränderlichen Strings arbeiten. Ob oder wie oft das nötig ist, muß jeder für sich selbst entscheiden. In der Regel kommt man aber mit den einfachen String-Objekten aus. Und damit kommen wir wieder zu der oben gezeigten Verkettung zurück. Intern werden bei diesem Ausdruck drei Objekte vom Typ String angelegt. Jeweils eins für die beiden Strings auf der rechten Seite des Zuweisungsoperators und eines auf der linken Seite. Einem Buch entlehnt, kann man an dieser Stelle auch noch einmal das Prinzip von Referenzen zeigen und erläutern. Dazu die folgenden Zeilen:

String meinErsterString = "Hallo";
String meinZweiterString = meinErsterString;

Wir haben in der ersten Zeile ein String-Objekt erzeugt. Der Ausdruck "meinErsterString" enthält nicht, wie oben gesagt, den Wert des Strings, sondern eine Referenz (einen Verweis) auf ein Objekt, welches den String transparent verwaltet. Die zweite Zeile übergibt also nicht den Inhalt des ersten Strings an den String "meinZweiterString", sondern den Verweis. Das bedeutet, daß jetzt auch der zweite String auf dieses eine Objekt verweist! Wenn ich jetzt folgende Zeile hinzufüge

meinErsterString += " Welt";

(entspricht übrigens "meinErsterString = meinErsterString+" Welt";) dann passiert etwas ähnliches wie bei dem Beispiel oben, wo ich erklärte, daß drei Objekte erzeugt werden. Für meinErsterString wird in diesem Augenblick ein völlig neues Objekt erzeugt, und der Verweis darauf wird in meinErsterString gespeichert. Ich habe also das String-Objekt nicht geändert, sondern ein vollkommen neues erzeugt. Wo vorher noch beide Bezeichner auf ein und dasselbe Objekt verwiesen haben, zeigen sie nun auf unterschiedliche Objekte.
Es ist also kompliziert, was bei einer String-Verkettung im Hintergrund abläuft, aber die Handhabung sieht doch einfach und praktisch aus, oder? Sollten einmal logische Fehler im Programmablauf auftauchen, so kann man ja mal überprüfen, ob die Logik der Verweise, also der Referenzen eingehalten wurde.
String-Verkettungen haben noch eine freundliche Eigenschaft, die auch im Hintergrund abläuft. Ich kann nämlich auch folgende Kette bilden:

String meinErsterString = "Hallo ";
String meinZweiterString = "te Welt!";
int i=3;
System.out.println(meinErsterString+i+meinZweiterString);

Die String-Verkettung sorgt freundlicherweise dafür, daß die Methode String.valueOf() automatisch bei allen Bestandteilen der Verkettung aufgerufen wird, die kein String-Objekt sind. Dies macht sie je nach Objekt unterschiedlich, aber halt auch freundlicherweise im Hintergrund, so daß man sich nicht selbst darum zu kümmern braucht.
Ich hoffe, man sieht jetzt, warum String-Verkettung im Hintergrund zwar kompliziert, aber eine feine Sache ist, weil man diese Dinge im Hintergrund nicht explizit berücksichtigen muß !
Ergänzend kann man vielleicht noch ein Beispiel zum Konvertieren von Strings aufzeigen. Um ein Integer-Objekt in einen String zu verwandeln nutzt man die Methode:

String myString;
Integer myInt = new Integer(412);
myString = myInt.toString();

Der umgekehrte Weg sieht etwa so aus:

String myString = "412";
int myInt = Integer.parseInt(myString);

1.4.4. Objektdatentypen 3 - Selbstdefinierte Klassen
Dies waren erstmal die grundlegenden Datentypen, die in Java schon definiert sind. Es gibt noch weitere sinnvolle Datentypen, aber die verhalten sich genauso, wie Datentypen, die wir auch selbst definieren können. Es ist tatsächlich möglich, Klassen zu erzeugen, die ein Hauptprogramm sind (wir erinnern uns an die main-Methode) und auch Klassen die kaum Daten speichern können aber eine Art Funktionsbibliothek darstellen. Oft werden Klassen aber so definiert, daß sie einen Datentypen darstellen. In unserer zweiten HiWelt-Variante hatten wir ja schon einen Datentyp InterGruss. Dieser sieht nicht von Anfang an ganz sinnvoll aus. Deswegen werden wir hier einen sinnvolleren Datentypen zeigen, den sich eigentlich jeder vorstellen kann:

public class Koordinate {
 public float x=0.0f;                      //Attribute
 public float y=0.0f;

 public Koordinate(float x, float y) {    //Konstruktor
  this.x=x;
  this.y=y;
 }
}

Das man einen Datentypen gebrauchen kann, der Koordinaten aufnimmt, erscheint doch schon logischer, oder? Unser Datentyp hat seine Attribute und einen Konstruktor. Damit funktioniert er auch schon. Diese Klasse muss kompiliert werden, und schon kann ich in einer anderen Klasse diesen neuen Datentypen wie folgt verwenden:

Koordinate myKoord = new Koordinate(3f, 4f);

(Durch das angehängte f kennzeichne ich wie oben beschrieben, daß es sich um einen floatingpoint typen handelt) In diesem konkreten Beispiel fällt auf, daß ich keine get- oder set-Methode programmiert habe. Dafür habe ich aber die Attribute public und nichtprivatedeklariert. Somit kann ich auf die Werte dann so zugreifen:

float myX = myKoord.x;
float myY = myKoord.y;

Dies ist zwar nicht ganz schön, funktioniert aber trotzdem und hält das Beispiel kurz. Alle möglichen Manipulationen, die ich mir für meine eigenen Datentypen ausdenken kann, kann ich in der Klassendefinition meines Datentyps programmieren. Ich könnte zum Beispiel eine 2D-Transformation oder -Rotation um den Nullpunkt als Methode programmieren, die ich mitmyKoord.rotate(90);aufrufen könnte. Alle diese Manipulationen kann ich als Methode in der Klasse implementieren. So sind meiner Fantasie beim Erfinden von Datentypen und darauf anwendbaren Manipulationen keine Grenzen gesetzt. Ein anderes Bespiel wäre zum Beispiel ein DatentypPersonaldatensatz, der die Attribute Vorname, Nachname, Strasse, PLZ und Ort besitzt. etc. etc.
Zur grundsätzlichen Erzeugung von Datentypen ist jetzt nicht mehr zu sagen. Aber sehr wichtig in der objektorientierten Sprache Java ist, daß ich die Datentypen auch erweitern kann (soweit sie nicht alsfinal deklariert sind). Ich kann nun z.B. einen Datentypen erzeugen, der zu einer Koordinate auch den Zeitpunkt aufnimmt, zu dem die Koordinate gültig ist (denken sie nur an Animationen). Dazu würde ich eigentlich einen neuen Datentypen erzeugen, der die gleichen Zeilen besitzt wie oben, aber eben ein Attribut mehr hat, und eine Zeile im Konstruktor mehr hat. Ich kann mir das aber auch ersparen, indem ich einfach meinen DatentypKoordinate erweitere. Das sähe so aus:

public class ZeitKoordinate extends Koordinate {
 public int time=0;                                   //zusätzliches Attribut

 public ZeitKoordinate(float x, float y, int time) {  //Konstruktor
  super(x, y);
  this.time=time;
 }
}

Ich habe tatsächlich nur die Zeitkomponente hinzugefügt. Die Attribute und auch den Konstruktor für die Raumkordinaten erbt diese neue Klasse von meiner alten Klasse Koordinate. Ich muß mich nur um den zusätzlichen Parameter time kümmern. In der Signatur der Klasse taucht das Schlüsselwort extendsauf. Damit signalisiere ich, daß ich eine Klasse erweitern will, nämlich die darauffolgend genannte. Ich deklariere ein Attribut, und dieses Attribut wird als zusätzliches zur Oberklasse (also der Klasse die ich erweitert habe) betrachtet. Im Konstruktor muß ich jetzt auch dieses Attribut mit einem Wert belegen. Um auch die beiden Werte der Oberklasse zu belegen, rufe ich einfach den Konstruktor der Oberklasse selbst auf, und reiche ihm die Werte durch, die er benötigt. Dies geschieht mit dem Ausdruck super(). Meine neue Klasse ZeitKoordinatehat aber denoch alle drei Attribute, die sie haben soll, und ich kann so ein Objekt erzeugen:

ZeitKoordinate myZKoord = new ZeitKoordinate(3f, 4f, 4);

Und so kann ich die Werte abfragen:

float myX = myZKoord.x;
float myY = myZKoord.y;
int myTime = myZKoord.time;

Zu diesen Eweiterungen gibt es auch im nächsten Beispiel, der einfachen AWT-Anwendung mehr Erklärungen.

1.4.5. Arrays
Arrays haben auch wieder eine Sonderstellung. Bei Arrays handelt es sich tatsächlich um Objekte, die auch vom Superobjekt in Java, der Klasse Object abgeleitet sind. Trotzdem darf ich die Klasse Array nicht erweitern, oder Unterobjekte von ihr bilden.
Im Gegensatz zu anderen Sprachen kennt Java nur eindimensionale Arrays. Mehrdimensionale Arrays realisiert Java, indem ein Element eines Arrays auch wieder ein Array sein kann. So können Arrays geschachtelt werden. Einer der Vorteile liegt zum Beispiel darin, daß die Länge der verschiedenen Dimensionen nicht unbedingt gleich sein muß.
Wie auch bei Strings, hat ein Array immer eine feste Größe. Es ist nicht möglich, die Größe des Arrays nachträglich zu ändern. Also gibt es keine Möglichkeit zu redimensionieren. Als Alternative kann man Vektorobjekte verwenden, mit denen sich so etwas ähnliches wie redimensionierbare Arrays realisieren lassen. Vektorobjekte sind aber eine Hilfklasse aus dem Paket java.util, und werden deswegen hier nicht behandelt. In einem der anderen Beispiele gibt es dazu mehr.
Zum Anlegen, also Deklarieren von Arrays geht man folgendermaßen vor:

int[] myArray;

alternativ ist auch die folgende Schreibweise erlaubt:

int myArray[];

Bei der Deklaration dieser Referenzen darf noch keine Anzahl von Elementen angegeben werden. Dies geschieht erst bei der Initialisierung. Dabei wird auch gleich der Speicher für die Daten reserviert. Die Initialisierung kann mit der Deklaration verbunden werden, oder danach mit newdurchgeführt werden. Bei der direkten Deklaration werden die Werte für das Array gleich mit angegeben:

int[] myArray = {0, 1, 2, 4, 8};

Die Anzahl der Elemente wird hierbei nicht angegeben, sondern von Java automatisch ermittelt. Wenn man ein Array einzeln vorher deklariert, wie vorher gezeigt, dann muß man die Initialisierung mit dem new-Operator durchführen:

int [] myArray;
myArray = new int[5];

Je nach Datentyp, ist nun das Array deklariert, und der Speicherbereich ist zugewiesen. Die Elemente bekommen (seit Java 2) einen voreingestellten Wert. Bei dem hier verwendeten Datentyp int enthält jedes Element eine 0. Auf die Elemente kann ich nun zugreifen. Wenn ich als Datentyp für das Array keinen primitiven Datentyp verwende, sondern ein Objekttyp (zB. String) dann wird der Inhalt nicht im Array abgelegt, sondern nur die Referenz auf die jeweiligen Objekte.
Die geschachtelten Arrays werden übrigens ähnlich angelegt. Bei der direkten Deklaration würde ein pseudo-zweidimensionales Array so erzeugt:

int[][] myDualArray;
myDualArray = new int[5][5];

Dabei gibt es im übrigen einige Feinheiten zu beachten. Wenn Sie jedoch beim Initialisieren mit new, immer auch die Anzahl der Schachtelungsebenen komplett angeben, und auch jeder Schachtelungsebene die entsprechende Tiefe mit auf den Weg geben, dann kann nichts schiefgehen.

Eine weitere Besonderheit bei Arrays, sind die sogenannten anonymen Arrays. Stellen Sie sich die Situation vor, daß Sie irgendeine Methode aufrufen wollen, und diese als Argument ein Array erwartet. Jetzt müßten Sie vor dem Aufruf erst ein Array deklarieren, es initialisieren und dann an die Methode weiterreichen. Wenn dieses Array aber im folgenden Ablauf des Programms nicht mehr benötigt wird, dann ist das ziemlich viel Aufwand. Daher haben die Java-Entwickler die Verwendung von anonymen Arrays eingeführt (wie Einwegflaschen). Das anonyme Array steht dann an der entsprechenden Stelle mit folgender Syntax:

new int[] {0, 1, 2, 4, 8}

Ich zitiere hier zur Veranschaulichung eine praktische Anwendung aus dem Java 2 Programmierhandbuch  in der eine Instanz eines Polygonobjektes (aus dem Paket java.awt) erzeugt wird:

Polygon myPolygon;
myPolygon = new Polygon(new int[] {1, 3, 3}, new int[] {1, 2, 1}, 3);

Wie sie sehen, werden dem Konstruktor drei Argumente übergeben. Bei den ersten beiden erwartet der Konstruktor ein Array. Diese werden hier anonym an genau der geforderten Stelle erzeugt und dem Konstruktor übergeben. Der oben gezeigte längere Erzeugungsprozess entfällt.

Die Indizierung von Arrays startet übrigens immer bei Null. Auf das erste Datenelement eines Arrays greife ich also mit

int x = myArray[0];

zu. Beim Zugriff auf einen Index der nicht existiert, wird ein Fehler ausgeworfen. Bei geschachteleten Arrays sieht der Zugriff so aus:

int x = myDualArray[2][3];

Beachten Sie hier, daß die Schachtelungstiefen nicht durch Kommata getrennt sind, sondern einzeln in eckigen Klammern stehen müßen.
Die Länge eines Arrays kann ich mit der Methode .lengthin Erfahrung bringen:

arrayLen = myArray.length;

Das Kopieren von Arrays ist im übrigen nicht trivial. Beachten Sie dazu auch die Ausführungen zu Strings und den Abläufen dabei im Hintergrund, die bei Arrays ähnlich sind. Zum Kopieren muß man entweder die Methode Object.clone() oder System.arraycopy() verwenden. Zum Clonen betrachten Sie folgendes Beispiel:

int [] arrayEins = {0, 1, 2, 4, 8};

int [] arrayZwei;
arrayZwei = new int[5];

arrayZwei = arrayEins.clone();

Werden Arrays mit Objekttypen als Element kopiert, so werden nur die Verweise, aber nicht die Objekte selbst kopiert. Die Methode System.arraycopy() kann dazu verwendet werden nur Teilbereiche eines Arrays zu kopieren. Nur der Vollständigkeit halber wird das gerade gezeigte Clone-Beispiel nun mit arraycopy nachvollzogen (also auch das komplette Array kopiert):

int [] arrayEins = {0, 1, 2, 4, 8};

int [] arrayZwei;
arrayZwei = new int[5];

System.arraycopy(arrayEins, 0, arrayZwei, 0, arrayEins.length);

Die Arraybezeichnungen dürften klar sein, die Nullen stehen für die jeweilige Indexposition und das letzte Argument ist die Anzahl der zu kopierenden Elemente.

Abschließend ist noch hinzuzufügen, daß die Klassejava.util.Arrays einige feine Methoden zur Manipulation von Arrays bereitstellt, wie zB. Suchen, Sortieren und Füllen von Arrays.

1.5. Download Quelltexte
Das erste Beispiel in diesem Tutorial sowie eine Menge Zusatzinformation zur Programmierung in Java sind somit durchgearbeitet. Die Quelltexte zu den Dateien HalloWelt.java, HiWelt.java und InterGruss.java können Sie als Zip-Archiv auch herunterladen: Download Quelltexte.

Als nächstes werden Sie das Paket java.awt kennenlernen, mit dem Sie grafische Oberflächen erstellen können. Kenntnisse zu diesem Paket sind sowohl bei Applets als auch bei Swing-Anwendungen dringend nötig.


<= vorherige Seite Inhaltsverzeichnis nächste Seite =>

zurück zur Hauptseite

Copyright 2000 by Frank Gehde