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.
zurück
zur Hauptseite |