Java-Einführung von Hubert Partl
Sie haben vielleicht schon mit Programmiersprachen wie Cobol, Fortran, Pascal, C, C++ oder Visual Basic gearbeitet oder zumindest davon gehört. Wie unterscheidet sich Java von diesen Sprachen?
Java wurde von der Firma Sun entwickelt und erstmals am 23. Mai 1995 als neue, objekt-orientierte, einfache und plattformunabhängige Programmiersprache vorgestellt. Sun besitzt das Schutzrecht auf den Namen Java und stellt damit sicher, daß nur "100 % richtiges Java" diesen Namen tragen darf. Die Sprache ist aber für alle Computersysteme verfügbar (im Allgemeinen kostenlos).
Java geht auf die Sprache Oak zurück, die 1991 von Bill Joy, James Gosling und Mike Sheridan im Green-Projekt entwickelt wurde, mit dem Ziel, eine einfache und plattformunabhängige Programmiersprache zu schaffen, mit der nicht nur normale Computer wie Unix-Workstations, PCs und Apple programmiert werden können, sondern auch die in Haushalts- oder Industriegeräten eingebauten Micro-Computer, wie z.B. in Waschmaschinen und Videorekordern, Autos und Verkehrsampeln, Kreditkarten und Sicherheitssystemen und vor allem auch in TV-Settop-Boxes für "intelligente" Fernsehapparate.
Allgemein anerkannt wurde Java aber erst seit 1996 in Verbindung mit Web-Browsern und Internet-Anwendungen sowie mit der Idee eines NC (Network Computer), der im Gegensatz zum PC (Personal Computer) nicht lokal installierte, maschinenspezifische Software-Programme benötigt, sondern die Software in Form von Java-Klassen dynamisch über das Netz (Intranet) von einem zentralen Server laden kann. Diese Idee wurde später zu einem allgemeinen "Application Service Providing" (ASP) erweitert. Inzwischen wird Java aber weniger für Applets in Web-Pages, sondern mehr für selbständige Anwendungs-Programme sowie für Server-Applikationen verwendet.
Der Name wurde nicht direkt von der indonesischen Insel Java übernommen sondern von einer bei amerikanischen Programmierern populären Bezeichnung für Kaffee.
Die wichtigsten Eigenschaften von Java sind:
Bei Java-Programmen muss zwischen zwei grundsätzlichen Arten unterschieden werden: Applikationen und Applets.
Java-Applikationen sind Computer-Programme mit dem vollen Funktionsumfang, wie er auch bei anderen Programmiersprachen gegeben ist. Applikationen können als lokale Programme auf dem Rechner des Benutzers laufen oder als Client-Server-Systeme über das Internet bzw. über ein Intranet oder als Server-Programme (Servlets, CGI-Programme) auf einem Web-Server.
Technisch gesehen zeichnen sich Java-Applikationen dadurch aus, dass sie eine statische Methode main enthalten.
Java-Applets werden innerhalb einer Web-Page dargestellt und unter der Kontrolle eines Web-Browsers ausgeführt. Sie werden meist über das Internet von einem Server geladen, und spezielle Sicherungen innerhalb des Web-Browsers ("Sandkasten", sandbox) sorgen dafür, dass sie keine unerwünschten Wirkungen auf den Client-Rechner haben können. So können Applets z.B. im Allgemeinen nicht auf lokale Files, Systemkomponenten oder Programme zugreifen und auch nicht auf Internet-Verbindungen außer zu dem einen Server, von dem sie geladen wurden.
Technisch gesehen zeichnen sich Java-Applets dadurch aus, dass sie Unterklassen der Klasse Applet sind.
Die meisten in dieser Kursunterlage beschriebenen Regeln gelten gleichermaßen für Applikationen und Applets. Die Spezialitäten, die nur für Applets gelten, sind in einem eigenen Kapitel über Applets zusammengefasst.
JavaScript ist eine Skript-Sprache, die in HTML eingebettet werden kann und bei manchen Web-Browsern (Netscape, Internet-Explorer) die Ausführung von bestimmten Funktionen und Aktionen innerhalb des Web-Browsers bewirkt.
Im Gegensatz zu Java ist JavaScript
Was brauchen Sie, um plattformunabhängige Java-Programme zu erstellen?
Das Java Development Kit (JDK) umfasst die für die Erstellung und das Testen von Java-Applikationen und Applets notwendige Software, die Packages mit den zur Grundausstattung gehörenden Java-Klassen, und die Online-Dokumentation.
Zur Software gehören der Java-Compiler, das Java Runtime Environment (die Java Virtual Machine) für die Ausführung von Applikationen, der Appletviewer für die Ausführung von Applets, ein Java-Debugger und verschiedene Hilfsprogramme.
Die Online-Dokumentation umfasst eine Beschreibung aller Sprachelemente und aller Klassen des Application Program Interface API.
Java ist eine relativ junge Programmiersprache und daher noch immer in Entwicklung, d.h. es kommen immer wieder neue Versionen mit Ergänzungen und Verbesserungen heraus:
Das JDK für ein bestimmtes System erhält man meist kostenlos (z.B. zum Download über das Internet) vom jeweiligen Hersteller, also die Solaris-Version von Sun, die HP-Version von HP, die IBM-Version von IBM. Versionen für Windows-PC, Macintosh und Linux kann man von Sun oder IBM bekommen.
Normalerweise genügt es, das Directory, in dem sich die Java-Software befindet, in die PATH-Variable einzufügen. Die anderen Variablen (CLASSPATH, JAVA_HOME) werden nur in Spezialfällen oder innerhalb der Software benötigt und müssen normalerweise nicht gesetzt werden. Die folgenden Hinweise sind also in den meisten Fällen nicht notwendig sondern nur in Spezialfällen:
Die Variable CLASSPATH gibt an, in welchen Directories und in welchen jar-Archiven nach Klassen und Packages gesucht werden soll, getrennt durch Doppelpunkte (Unix) bzw. Strichpunkte (Windows). Dies ist seit JDK 1.2 nur dann notwendig, wenn zusätzliche jar-Archive oder zusätzliche Directories (die sich auch auf Web-Servern befinden können) gebraucht werden. In diesem Fall enthält der CLASSPATH den Punkt (für das jeweils aktuelle Directory), das Start-Directory der im JDK enthaltenen Packages bzw. das Archiv-File der Klassenbibliothek. sowie die zusätzlichen jar-Archive und Directories bzw. URLs.
Wenn der CLASSPATH nicht gesetzt ist, wird seit JDK 1.2 nach allen benötigten Klassen automatisch zuerst im aktuellen Directory und dann in der zur verwendeten Java-Software gehörenden Klassenbibliothek gesucht. Im Normalfall soll der CLASSPATH deshalb gar nicht gesetzt werden.
Die Variable JAVA_HOME gibt an, in welchem Directory die Komponenten des JDK zu finden sind. Die explizite Angabe ist meistens ebenfalls nicht notwendig.
Damit man den Java-Compiler, das Runtime-System, den Appletviewer etc. einfach aufrufen kann, sollte das entsprechende bin-Directory in der PATH-Variablen enthalten sein.
Jedes Java-Source-File darf nur eine public Klasse enthalten, und der Filename muss dann der Name dieser Klasse (mit der richtigen Groß-Kleinschreibung) mit der Extension .java sein, hat also die Form
Xxxxx.java
Wenn man das Source-File auf einem PC mit MS-Word, Wordpad oder Notepad
erstellt, muss man darauf achten,
dass es mit dem Dateityp Text-File (nicht Word-File) erstellt wird
und dass nicht automatisch eine Extension .txt
angehängt wird.
Eventuell muss man also beim Speichern den Filenamen mit Quotes
in der Form
"Xxxxx.java"
schreiben, damit man nicht einen Filenamen der Form
Xxxxx.java.txt
erhält.
Der Java-Compiler erzeugt für jede Klasse ein eigenes File, das den Bytecode dieser Klasse enthält. Der Filename ist dann der Name der Klasse mit der Extension .class, hat also die Form
Xxxxx.class
Man kann auch mehrere Klassen-Files in ein komprimiertes Archiv-File zusammenfassen, das dann weniger Übertragungszeit über das Internet benötigt. Solche Java-Archiv-Files haben Namen der Form
xxx.jar
Der Aufruf des Java-Compilers erfolgt im einfachsten Fall in der Form
javac Xxxxx.java
Der Name des Source-Files muss mit der Extension .java angegeben werden. Man kann auch mehrere Source-Files angeben:
javac Xxxxx.java Yyyyy.java
Falls die Klasse andere Klassen verwendet, die neu übersetzt werden müssen, werden diese automatisch ebenfalls übersetzt, ähnlich wie bei der Unix-Utility make.
Der Compiler erzeugt für jede Klasse ein File Xxxxx.class, das den Bytecode enthält. Dieser Bytecode ist plattformunabhängig: Egal auf was für einem System der Java-Compiler aufgerufen wurde, der Bytecode kann auch auf jedem anderen Computersystem ausgeführt werden, zumindest wenn es sich um "100% pure Java" handelt, was bei manchen Microsoft-Produkten leider nicht garantiert ist.
Die Ausführung des Bytecodes einer Java-Applikation erfolgt durch Aufruf der Java Virtual Machine JVM (im Java Runtime Environment JRE) in der Form
java Xxxxx
oder
java Xxxxx parameter parameter
Der Bytecode der Klasse Xxxxx muss im File Xxxxx.class oder in einem Archiv-File (zip, jar) liegen, der Klassenname muss aber ohne die Extension .class angegeben werden.
Ältere Versionen der JVM waren noch reine Bytecode-Interpreter und daher relativ langsam. Neuere Versionen erzeugen mit Hilfe von Just-in-Time-Compilation (JIT) und Hotspot-Technologie (Analyse des Laufzeitverhaltens) eine weitgehende Optimierung und Beschleunigung der Ausführungszeit.
Die Darstellung und Ausführung eines Java-Applet erfolgt durch Aufrufen der entsprechenden Web-Page (HTML-File) mit einem Java-fähigen Web-Browser wie z.B. Netscape, Internet-Explorer oder HotJava.
Für Tests und innerhalb von Java-Schulungen empfiehlt sich die Verwendung des mit dem JDK mitgelieferten Appletviewer. Dies ist ein einfacher Browser, der alle HTML-Tags außer <applet> und <param> ignoriert und nur das im HTML-File angeführte Applet ohne den Rest der Web-Page anzeigt. Der Aufruf erfolgt in der Form
appletviewer xxxx.html
oder mit einer kompletten Internet-Adresse (URL):
appletviewer URL
Die Online-Dokumentation der Klassenbibliothek (application programming interface API) kann entweder on-line am Sun-Server http://java.sun.com/ gelesen oder zusätzlich zum JDK am eigenen Rechner installiert oder auf einem Web-Server im eigenen Bereich gespeichert und dann lokal gelesen werden. Sie liegt in der Form von Web-Pages (HTML-Files mit Hypertext-Links) vor und kann mit jedem beliebigen Web-Browser gelesen werden, z.B. mit Netscape. Man kann den jeweiligen lokalen Filenamen direkt als Aufrufparameter angeben, oder man kann sich zum File "durchklicken" und es dann in die Bookmarks eintragen und später von dort wiederum aufrufen.
Die wichtigsten Files innerhalb dieser Dokumentation sind:
Wenn Sie einen Klassennamen anklicken, erhalten Sie die komplette Beschreibung dieser Klasse und ihrer Konstruktoren, Datenfelder und Methoden sowie eine Angabe ihrer Oberklassen. Falls Sie eine Methode in der Dokumentation einer Unterklasse nicht finden, dann sehen Sie in ihren Oberklassen nach (siehe Vererbung).
Wenn Sie den Namen einer Methode (oder eines Konstruktors oder Datenfeldes) anklicken, erhalten Sie sofort die Beschreibung dieser Methode (bzw. des Konstruktors oder Datenfeldes). Dabei müssen Sie aber beachten, dass es mehrere gleichnamige Methoden und Konstruktoren sowohl innerhalb einer Klasse (siehe Overloading) als auch in verschiedenen Klassen (siehe Overriding) geben kann.
Ein Java-Archiv enthält Dateien und eventuell auch ganze Directory-Strukturen (siehe Packages) in dem selben komprimierten Format, das auch von PKZIP und Win-Zip verwendet wird. Sie werden mit dem Programm jar (java archiver) verwaltet, der Aufruf erfolgt ähnlich wie beim Unix-Programm tar (tape archiver):
Archiv-File erstellen (create):
jar -cvf xxx.jar *.class
Inhalt eines Archiv-Files anzeigen (table of contents):
jar -tvf xxx.jar
einzelne Dateien aus einem Archiv-File herausholen (extract):
jar -xvf xxx.jar Yyyy.class
Das Herausholen bzw. "Auspacken" von Archiv-Files ist meistens gar nicht nötig: Der Java-Compiler und die Java Virtual Machine können die Class-Files direkt aus dem Archiv-File lesen und laden. Zu diesem Zweck muss der Filename des Archiv-Files im Classpath angegeben sein bzw. bei Applets im ARCHIVE-Parameter des Applet-Tag im HTML-File.
Von mehreren Firmen werden Editoren, GUI-Builder, Entwicklungsumgebungen (Integrated Development Environments IDE) und andere Hilfsmittel angeboten, mit denen man Java-Applikationen, Applets, Servlets etc. möglichst bequem und einfach erstellen und testen kann. Beispiele für aktuelle und historische Tools sind Eclipse, NetBeans, IntelliJ, JTogether, JBuilder, JDeveloper, Visual Age, Visual Café, Kawa, Forte, Visual J++ und viele andere.
Übersichten über die verfügbaren Software-Tools und deren Bezugsquellen finden Sie auf den in den Referenzen angeführten Web-Servern, Erfahrungsberichte in den einschlägigen Usenet-Newsgruppen.
Unter Java Beans ("Kaffeebohnen") versteht man kleine Java-Programme (Klassen) mit genau festgelegten Konventionen für die Schnittstellen, die eine Wiederverwendung in mehreren Anwendungen (Applikationen und Applets) ermöglichen, ähnlich wie bei Unterprogramm-Bibliotheken in anderen Programmiersprachen. Dies ist vor allem im Hinblick auf das Software-Engineering von komplexen Programmsystemen interessant. Dafür gibt es ein eigenes Beans Development Kit BDK, das man zusätzlich zum JDK installieren kann, und ein Package java.beans, das ab Version 1.1 im JDK enthalten ist,
Beans werden auch von vielen der oben erwähnten Software-Tools (IDE) unterstützt. Auch die Klassenbibliothek des JDK ist seit Version 1.2 weitgehend nach den Beans-Konventionen geschrieben, und manche Software-Firmen verkaufen spezielle Java-Beans für bestimmte Anwendungen.
Es ist empfehlenswert, auch beim Schreiben eigener Java-Programme möglichst die Konventionen von Java-Beans einzuhalten, also z.B. dass jede Klasse einen Default-Konstruktor mit leerer Parameterliste haben soll, dass die Methoden für das Setzen und Abfragen von Datenfeldern Namen der Form setXxxxx und getXxxxx bzw. isXxxxx haben sollen, oder dass alle Klassen so "selbständig" programmiert werden sollen, dass sie unabhängig davon funktionieren, wie andere Klassen programmiert wurden.
Mehr über diese Grundsätze und Empfehlungen finden Sie im Kapitel über Objekte und Klassen. Für genauere Informationen über Beans und BeanInfos wird auf die Referenzen verwiesen.
Hier nochmals eine kurze Zusammenfassung der Befehle, die im Normalfall nötig sind, um ein Java-Programm zu erstellen, zu übersetzen und auszuführen:
Es ist üblich, einen ersten Eindruck für eine neue Programmiersprache zu geben, indem man ein extrem einfaches Programm zeigt, das den freundlichen Text "Hello World!" auf die Standard-Ausgabe schreibt.
So sieht dieses Minimal-Programm als Java-Applikation aus:
public class HelloWorld { public static void main (String[] args) { System.out.println("Hello World!"); } }
Dieses Java-Source-Programm muss in einem File mit dem Namen HelloWorld.java liegen. Die Übersetzung und Ausführung erfolgt dann mit
javac HelloWorld.java
java HelloWorld
Ein als Muster für objekt-orientierte Java-Programme besser geeignetes Beispiel für eine HelloWorld-Applikation finden Sie im Kapitel über Objekte und Klassen.
Ein analoges Beispiel für ein minimales HelloWorld-Applet finden Sie im Kapitel über Applets.
Schreiben Sie die oben angeführte HelloWorld-Applikation (oder eine Variante davon mit einem anderen Text) in ein Java-Source-File und übersetzen Sie es und führen Sie es aus.
Suchen Sie in der Online-Dokumentation (API) nach der Beschreibung der Methode println und des Objekts System.out.
Ein Java-Neuling ("Newbie") fragt: Ich habe das HelloWorld-Programm aus meinem Java-Buch abgeschrieben, aber es funktioniert nicht.
Ein erfahrener Programmierer ("Oldie") antwortet: Das ist schon richtig so, das HelloWorld-Beispiel dient dazu, dass Du die typischen Anfänger-Fehler kennen lernst und in Zukunft vermeiden kannst. Der erste Fehler war schon: Wenn Du uns nicht den genauen Wortlaut der Fehlermeldung, die Version Deiner Java-Software (JDK, IDE) und die relevanten Teile Deines Source-Programms dazu sagst, können wir den Fehler nicht sehen und Dir nicht helfen. In diesem Fall kann ich nur raten. Du hast wahrscheinlich einen der folgenden typischen Newbie-Fehler gemacht:
Die Syntax und die Grundregeln der Sprache Java sind weitgehend identisch mit denen von C und C++. Es fehlen jedoch Sprachelemente, die leicht zu Programmfehlern führen können, wie Pointer-Arithmetik, Operator-Overloading und Goto.
Für diejenigen, die noch keine Erfahrung mit Programmiersprachen haben, hier eine kurze Zusammenstellung der wichtigsten Elemente, die in Computer-Programmen vorkommen, und der wichtigsten Konzepte für die Programmierung. In den folgenden Abschnitten lernen Sie dann, wie Sie diese Elemente in der Programmiersprache Java formulieren können.
Computer-Programme enthalten die folgenden Elemente:
Die Anweisungen werden in einer bestimmten Reihenfolge ausgeführt, dabei gibt es auch Abzweigungen (Entscheidungen, z.B. mit if oder switch) und Wiederholungen (Schleifen, z.B. mit for, while, do).
Anweisungen, die gemeinsam ausgeführt werden sollen, werden zu sogenannten Blöcken von Anweisungen zusammengefasst, die wiederum geschachtelt werden können.
Das gesamte Programm besteht meistens aus mehreren Einheiten, die als Unterprogramme (Subroutinen, Prozeduren, Funktionen, Methoden) bezeichnet werden. Die Einheit, mit der die Verarbeitung beginnt, wird als Hauptprogramm (main-Methode) bezeichnet.
Beim Design eines komplexen Programmsystems können je nach Zweckmäßigkeit einige der folgenden Konzepte eingesetzt werden:
Es wird dringend empfohlen, die folgenden Konventionen bei der Wahl von Namen strikt einzuhalten und ausführliche, gut verständliche Namen zu verwenden:
Namen von Klassen und Interfaces beginnen mit einem Großbuchstaben und bestehen aus Groß- und Kleinbuchstaben und Ziffern. Beispiel: HelloWorld, Button2.
Namen von Konstanten beginnen mit einem Großbuchstaben und bestehen nur aus Großbuchstaben, Ziffern und Underlines. Beispiel: MAX_SPEED.
Alle anderen Namen (Datenfelder, Methoden, Objekte, Packages etc.) beginnen mit einem Kleinbuchstaben und bestehen aus Groß- und Kleinbuchstaben und Ziffern. Beispiele: openFileButton, button2, addActionListener, setSize, getSize.
Vom System intern generierte Namen können auch Dollar- oder Underline-Zeichen enthalten.
Groß- und Kleinbuchstaben haben in jedem Fall verschiedene Bedeutung (case-sensitive).
Die folgenden Namen sind reserviert und dürfen nicht neu deklariert werden:
abstract, boolean, break, byte, byvalue, case, cast, catch, char, class, const, continue, default, do, double, else, extends, false, final, finally, float, for, future, generic, goto, if, implements, import, inner, instanceof, int, interface, long, native, new, null, operator, outer, package, private, protected, public, rest, return, short, static, super, switch, synchronized, this, throw, throws, transient, true, try, var, void, volatile, while.
Die Datentypen und ihre Eigenschaften (Länge, Genauigkeit) hängen nicht vom jeweiligen Computersystem ab sondern sind in Java einheitlich festgelegt. Es wird zwischen primitiven Datentypen und Objekt-Typen (Klassen) unterschieden.
Primitive Datentypen:
Objekt-Typen:
Normale ganze Zahlen haben den Typ int und werden dezimal interpretiert. Werte vom Typ long kann man durch Anhängen von L erzeugen. Oktale Zahlen beginnen mit 0 (Null), hexadezimale mit 0x (Null und x).
Normale Zahlen mit Dezimalpunkt (oder mit E für den Exponent) haben den Typ double. Werte vom Typ float kann man durch Anhängen von F erzeugen.
Logische Konstanten sind die Wörter true und false.
char-Konstanten werden zwischen einfachen Apostrophen geschrieben, Beispiel: 'A'.
String-Konstanten werden zwischen Double-Quotes geschrieben, Beispiel: "Hubert Partl". Double-Quotes innerhalb des Strings müssen mit einem Backslash maskiert werden. Sonderzeichen können mit Backslash und u und dem Unicode-Wert angegeben werden, also z.B. '\u20ac' für das Euro-Zeichen.
Beispiele:
byte b; b=123; short i; i=-1234; int i; i=-1234; long i; i=-1234L; float x; x=-123.45F; double x; x=-123.45; x=1.0E-23; boolean b; b=true; b=false; char c; c='A'; String s; s="Abc"; s=null;
Alle Datenfelder müssen mit Angabe des Datentyps deklariert werden und können dann nur zu diesem Typ passende Werte erhalten. Diese Regel hilft, Tippfehler zu erkennen.
Deklaration eines Datenfeldes:
typ name;
Deklaration von mehreren Datenfeldern des selben Typs:
typ name1, name2, name3;
Zuweisung eines Wertes:
name = wert;
Zuweisung eines Wertes mit Typumwandlung (casting):
name = (typ) wert;
Deklaration mit Zuweisung eines Anfangswertes:
typ name = wert;
Achtung! Die Deklaration gilt immer nur für den Bereich (Klasse, Methode, Block innerhalb einer Methode), innerhalb dessen die Deklaration steht. Es kann verwirrend sein, wenn man innerhalb eines Blockes den selben Namen, der bereits für eine globale Variable gewählt wurde, für eine gleichnamige lokale Variable verwendet.
Wenn man vor die Typangabe das Wort final setzt, handelt es sich um eine Konstante, d.h. der Wert dieses Datenfeldes kann nicht nachträglich verändert werden.
Deklaration einer Variablen, die eine Referenz auf Objekte einer bestimmten Klasse enthalten kann:
ClassName name;
ClassName name = null;
Anlegen eines Objekts (Exemplar, Instanz) dieser Klasse und Zuweisung der Referenz auf dieses Objekt an die deklarierte Variable:
name = new ClassName();
wobei ClassName() für einen Konstruktor (constructor) der Klasse steht.
Deklaration, Anlegen und Zuweisen in einem:
ClassName name = new ClassName();
Deklaration und Anlegen von Strings:
String s; String s = null; s = "Hubert Partl"; String myName = "Hubert Partl";
Objekte werden mit dem Operator new und dem Konstruktor explizit angelegt und dabei der Speicherplatz für das Objekt reserviert und mit den Anfangswerten belegt.
Sobald es keine Referenz mehr auf das Objekt gibt (z.B. durch Zuweisen von null auf die einzige Referenz-Variable), wird der vom Objekt belegte Speicherplatz durch den im Hintergrund laufenden Garbage-Collector automatisch zurückgegeben und für neuerliche Verwendung frei gemacht.
Unter einem Feld (array) versteht man eine Menge von Datenfeldern des gleichen Typs. Felder eignen sich besonders für die Verarbeitung mit for-Schleifen (siehe unten).
Deklaration einer Variablen, die eine Referenz auf ein Feld enthalten kann:
typ[] name;
Anlegen eines Feldes von n Elementen dieses Typs und Zuweisung der Referenz auf dieses Feld an die Referenz-Variable:
name = new typ [n];
Deklaration und Anlegen in einem:
typ[] name = new typ [n];
Beispiel:
double[] monatsUmsatz = new double[12]; ...Eine Array-Referenz kann nacheinander Felder verschiedener Länge (aber nur gleichen Typs) enthalten. Beispiel:
double[] tagesUmsatz; ... tagesUmsatz = new double[31]; ... tagesUmsatz = new double[28]; ...Zuweisen von Werten zu den Feldelementen:
for (int i=0; i<name.length; i++) { name[i] = wert; }Achtung! Die Länge des Feldes ist n, die Indizes der Feldelemente laufen aber von 0 bis n-1. Die Länge eines Feldes erhält man mit
name.length
Bitte, beachten Sie, dass es hier nicht length() heißt.
Deklaration, Anlegen und Zuweisung von Anfangswerten in einem:
int[] daysPerMonth = { 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };legt ein Feld von 12 int-Zahlen an und belegt es mit diesen Werten.
String[] weekDay = { "So", "Mo", "Di", "Mi", "Do", "Fr", "Sa" };legt ein Feld von 7 String-Referenzen an und belegt es mit diesen Strings.
Classname[] name = new Classname [n]; for (int i=0; i<name.length; i++) { name[i] = new Classname(); }Beispiel:
int maxAnzahl = 20; int anzahl=0; Person[] teilnehmer = new Person [maxAnzahl]; for (int i=0; i<teilnehmer.length; i++) { teilnehmer[i] = null; } ... anzahl = anzahl + 1; teilnehmer[ anzahl-1 ] = new Person ("Hubert","Partl"); ...
typ[][] name = new typ [n][]; for (int i=0; i<name.length; i++) { name[i] = new typ[m]; for (int j=0; j<name[i].length; j++) { name[i][j] = wert; } }Die n Teilfelder können aber auch verschiedene Längen haben, die dann nicht in der i-Schleife sondern einzeln initialisiert werden. Beispiel:
double[][] tagesUmsatz = new double [12][]; int[] monatsLaenge = { 31,29,31,30,31,30,31,31,30,31,30,31 }; for (int monat=0; monat<tagesUmsatz.length; monat++) { tagesUmsatz[monat] = new double[ monatsLaenge[monat] ]; for (int tag=0; tag<tagesUmsatz[monat].length; tag++) { tagesUmsatz[monat][tag] = 0.0; } }
Als Ergänzung zu den hier beschriebenen Feldern (array) gibt es auch eine Klasse "Vector", mit der man dynamisch wachsende Speicherbereiche für beliebige Objekte anlegen kann, sowie eine Klasse "Hashtable". Ab JDK 1.2 gibt es noch mehr solche Klassen wie z.B. Collection, List, Map, Set, ArrayList, LinkedList, HashMap, TreeMap, ListModel, TableModel, TreeModel. Details finden Sie jeweils in der Online-Dokumentation (API).
Objekt-Operatoren: new .
Mathematische Operatoren: + - * / %
Mathematische Zuweisungen: ++ -- = *= /= %= += -=
Logische Operatoren: < > <= >= == != instanceof ! && ||
Bit-Operatoren: << >> >>> & |
Bit-Zuweisungen: <<= >>= >>>= &= |=
String-Operatoren: + equals
Achtung! Bei Referenzen auf Objekte oder Strings vergleichen die Operatoren == und != nur, ob die Referenz die selbe ist, nicht, ob der Inhalt des Strings oder des Objekts gleich ist. Dafür muss die equals-Methode des Objekts verwendet werden. Beispiel:
String vorName; ... if ( vorName.equals("Hubert") ) ...
In manchen Fällen kann der Compiler Typen automatisch umwandeln, z.B. bei der Zuweisung von int auf long oder double:
int i = ...; double d = i + 1;In vielen Fällen muss man dem Compiler aber mitteilen, in welchen Datentyp ein Wert umgewandelt werden soll. Dies wird als Casting bezeichnet, dazu wird der neue Typ zwischen Klammern angegeben:
typ1 name1 = ...; typ2 name2 = (typ2) name1;Dies ist inbesondere dann wichtig, wenn man sicherstellen will, dass eine Multiplikation mit der vollen Genauigkeit von long oder double erfolgt statt mit der geringeren Genauigkeit von int oder float, oder dass eine Division mit Bruchteilen (float oder double) statt nur ganzzahlig mit Abschneiden des Restes erfolgt. Beispiel:
int i = ... ; double d = ( (double) i ) / 100.0 ;Für die Umwandlung von float oder double auf int oder long gibt es die Methoden Math.floor, Math.ceil, Math.round, je nachdem, ob man abrunden, aufrunden oder kommerziell runden will.
Die Länge eines Strings erhält man mit der Methode
String name="Hubert Partl"; int len=name.length();
Für das Zusammenfügen von mehreren Strings zu einem längeren String gibt es den Operator +. Beispiel:
String name = vorName + " " + zuName;Für die Abfrage auf Gleichheit gibt es die Methode equals (siehe oben). Beispiel:
if (vorName.equals("Hubert") ...
Außerdem enthält die Klasse String zahlreiche weitere Methoden für das Erzeugen von Substrings oder von veränderten Strings (z.B. Zeichenersetzung, Umwandlung auf Groß- oder Kleinbuchstaben), sowie für die Abfrage von Zeichen und Substrings innerhalb eines Strings und für die alphabetische Reihenfolge von 2 Strings. Die Details entnehmen Sie bitte der Online-Dokumentation der Klasse String.
Für komplexere Anwendungen, bei denen ein Text-String in Einzelteile (z.B. Wörter) zerlegt werden soll, gibt es eine eigene Klasse StringTokenizer.
Für die Umwandlung von Strings, die Ziffern enthalten, in entsprechende Zahlenwerte gibt es eigene statische Methoden wie z.B.
int Integer.parseInt(String) long Long.parseLong(String) float Float.valueOf(String).floatValue() double Double.valueOf(String).doubleValue()Ab JDK 1.2 wird das einheitlicher, dann gibt es auch
float Float.parseFloat(String) double Double.parseDouble(String)Wie Sie die dabei eventuell auftretenden Fehler abfangen müssen, ist im Kapitel über Exceptions beschrieben.
Für die umgekehrte Umwandlung von Zahlen in Strings kann man die Methode
String s = String.valueOf(zahl);oder auch den folgenden Trick verwenden:
String s = "" + zahl;
Mathematische Funktionen können mit Hilfe der statischen Methoden in der Klasse Math aufgerufen werden. Beispiel:
x = Math.sqrt(y);
Die Details finden Sie in der Online-Dokumentation (API). Mehr über Methoden und statische Methoden finden Sie im Kapitel über Objekte und Klassen.
Statements werden mit ; (Strichpunkt, Semicolon) beendet.
Blöcke von Statements werden zwischen { und } (geschwungene Klammern, braces) eingeschlossen und können leer sein oder ein oder mehrere Statements enthalten, die jeweils mit ; beendet werden.
Alles, was zwischen /* und */ oder zwischen // und dem Zeilenende steht, wird vom Java-Compiler ignoriert und gilt als Kommentar.
Beispiele:
... // Kommentar bis Zeilenende ... /* Kommentar */ ...
Mit /** in der ersten Zeile, * am Beginn aller weiteren Zeilen und */ am Ende kann man einen Dokumentations-Kommentar schreiben, der dann mit Hilfsprogrammen extrahiert und ausgedruckt werden kann (siehe javadoc).
einfache Abzweigung (Entscheidung der Form wenn - dann):
if ( logischer Ausdruck ) { Statements; }
doppelte Verzweigung (wenn - dann - sonst):
if ( logischer Ausdruck ) { Statements; } else { Statements; }Beispiel:
if ( guthaben >= 1000000 ) { System.out.println("Gratuliere, Du bist Millionaer!"); }
Wiederholung (Schleife) mit bestimmter Anzahl:
for ( int name=Anfangswert ; logischer Ausdruck ; Wertänderung ) { Statements; }Beispiel:
for ( int monat=1; monat<=6; monat = monat + 1 ) { guthaben = guthaben + einzahlung; }
Wiederholung (Schleife) mit beliebiger Bedingung:
while ( logischer Ausdruck ) { Statements; }Wiederholung (Schleife) mit mindestens einem Durchlauf:
do { Statements; } while ( logischer Ausdruck );Beispiel:
while ( guthaben < sparziel ) { guthaben = guthaben + einzahlung; }
mehrfache Verzweigung (verschiedene Fälle):
switch ( ganzzahliger Ausdruck ) { case wert1: Statements; break; case wert2: Statements; break; ... default: Statements; }Beispiel:
switch (note) { case 1: System.out.println("sehr gut"); break; case 2: System.out.println("gut"); break; ... }Achtung! Falls die auf eine case-Angabe folgenden Statements nicht mit break; enden oder dort überhaupt keine Statements stehen, werden auch die bei der folgenden case-Angabe stehenden Statements ausgeführt. Beispiel:
switch (firstChar) { case 'j': case 'J': // ja ... break; case 'n': case 'N': // nein ... break; }
Mit continue; kann man den aktuellen Schleifendurchgang vorzeitig beenden und den nächsten Schleifendurchgang beginnen.
Mit break; kann man eine Schleife oder einen case-Block verlassen und zum Statement nach Ende der Schleife bzw. des switch-Blockes springen.
Mit labelname: kann man einen Label vor ein for-, while- oder do-Statement setzen und dann mit continue labelname; bzw. break labelname; diese (äußere) Schleife vorzeitig beenden.
Es gibt in Java kein goto-Statement zum Sprung an beliebige Stellen.
Mit System.exit(0); oder System.exit(n); kann man die Applikation vorzeitig beenden.
Bei erfolgreicher Beendigung gibt man als Exit-Code 0 an, bei einer nicht erfolgreichen Beendigung eine positive ganze Zahl als Fehler-Code, im einfachsten Fall den Wert 1.
public class ProgExpr { public static void main (String[] args) { int guthaben = 1000; System.out.println("Guthaben = " + guthaben); int einzahlung = 500; System.out.println("Einzahlung = " + einzahlung); guthaben = guthaben + einzahlung; System.out.println("Guthaben = " + guthaben); } }
public class ProgIf { public static void main (String[] args) { int guthaben = 2000000; System.out.println("Guthaben = " + guthaben); if ( guthaben >= 1000000 ) { System.out.println("Gratuliere, Du bist Millionaer!"); } } }
public class ProgFor { public static void main (String[] args) { int guthaben = 1000; int einzahlung = 500; System.out.println("Guthaben = " + guthaben); System.out.println("Einzahlung = " + einzahlung + " pro Monat"); for ( int monat=1; monat<=6; monat = monat + 1 ) { guthaben = guthaben + einzahlung; System.out.println(monat + ". Monat:"); System.out.println(" Guthaben = " + guthaben); } } }
public class ProgWhile { public static void main (String[] args) { int guthaben = 1000; int sparziel = 8000; int einzahlung = 600; System.out.println ("Guthaben = " + guthaben); System.out.println ("Sparziel = " + sparziel); while ( guthaben < sparziel ) { guthaben = guthaben + einzahlung; System.out.println ("neues Guthaben = " + guthaben); } System.out.println( "Sparziel erreicht."); } }
public class ProgSwitch { public static void main (String[] args) { int note = 2; switch (note) { case 1: System.out.println("sehr gut"); break; case 2: System.out.println("gut"); break; case 3: System.out.println("befriedigend"); break; case 4: System.out.println("genuegend"); break; case 5: System.out.println("nicht genuegend"); break; } switch (note) { case 1: System.out.print("mit Auszeichnung "); case 2: case 3: case 4: System.out.println("bestanden"); break; case 5: System.out.println("nicht bestanden"); break; } } }
public class ProgBlock { public static void main (String[] args) { int note; String beurteilung; System.out.println ("Notentabelle:"); for ( note = 1; note <= 5; note++ ) { switch (note) { case 1: beurteilung = "sehr gut"; break; case 2: beurteilung = "gut"; break; case 3: beurteilung = "befriedigend"; break; case 4: beurteilung = "genuegend"; break; case 5: beurteilung = "nicht genuegend"; break; default: beurteilung = "keine Note"; } /* end switch */ System.out.println (note + " = " + beurteilung); } /* end for */ } }
Hier noch ein relativ einfaches Beispiel für die zeilenweise Ein- und Ausgabe. Die darin verwendeten Sprachelemente werden allerdings erst in späteren Kapiteln erklärt (Packages, Exceptions, Ein-Ausgabe).
import java.io.*; public class ProgIO { public static void main (String[] args) { try { String zeile, vorName; int alter; BufferedReader infile = new BufferedReader ( new InputStreamReader (System.in) ); // String lesen System.out.println ("Bitte gib Deinen Vornamen ein:"); vorName = infile.readLine(); System.out.println ("Hallo " + vorName + "!"); // Zahl lesen System.out.println ("Bitte gib Dein Alter ein:"); zeile = infile.readLine(); alter = Integer.parseInt ( zeile.trim() ); System.out.println ("Du bist " + alter + " Jahre alt."); } catch (Exception e) { System.out.println("falsche Eingabe - " + e); } } }
Computer sind unglaublich dumme Geräte,
die unglaublich intelligente Sachen können.
Programmierer sind unglaublich intelligente Leute,
die unglaublich dumme Sachen produzieren.
("Die Presse", 30.8.1999)
Programmfehler (auf englisch bugs = Wanzen, Ungeziefer genannt) gehören zum täglichen Brot jedes Programmierers. Nur wer nichts arbeitet, macht keine Fehler. Der gute Programmierer ist nicht der, der keine Fehler macht, sondern der, der seine Fehler rasch findet und behebt.
Es gibt eigene Software-Tools, die bei der Fehlersuche helfen können. In den meisten Fällen genügt es aber, an allen wichtigen Stellen im Programm mit System.out.println oder System.err.println Informationen über das Erreichen dieser Programmstelle und über den Inhalt von wichtigen Variablen auszugeben. Damit kann man dann meistens sehr rasch finden, wo, wann und warum ein Programmfehler aufgetreten ist und was man tun muss, um ihn zu vermeiden. Beispielskizze:
... System.out.println("* Beginn der Schleife"); System.out.println("n = " + n); for (int i=0; i<=n; i++) { System.out.println("* i = " + i + ", n = " + n); ... } ...Die mit System.out.println oder System.err.println geschriebenen Test-Informationen erscheinen bei Applikationen im Ausgabe-Fenster und bei Applets in der "Java-Console" des Web-Browsers. Wenn man die Test-Ausgaben von den normalen Ausgaben einer Applikation trennen will, dann schreibt man nur die normalen Ausgaben (Programmergebnisse) auf System.out und die Test-Ausgaben auf System.err.
Wenn man die Fehler behoben hat und das Programm im Produktionsbetrieb verwenden will, muss man die Test-Ausgaben deaktivieren. Wenn man das Programm aber später verbessert oder erweitert (und das wird bei jedem brauchbaren Programm früher oder später notwendig), dann muss man sie wiederum aktivieren. Zu diesem Zweck verwendet man eine boolean Variable TEST oder DEBUG, die man in der Testversion auf true und in der Produktionsversion auf false setzt, und macht die Ausgabe der Testinformationen immer von dieser Variable abhängig. Wenn man diese Variable als static final definiert, dann kann der Compiler das Programm so optimieren, dass die Programmstücke, die nur für das Testen dienen, beim Übersetzen mit TEST = false komplett weggelassen werden und weder Speicherplatz noch Abfragezeit kosten. Beispielskizze:
public class Classname { private static final boolean TEST = false; ... public ... methodName () { ... if (TEST) { System.err.println( ... test information ... ); } ... } }Wenn man ein Paket von mehreren Klassen testen will, kann es günstiger sein, alle Definitionen in einer eigenen Klasse (Defined) festzulegen und dann in allen Klassen in der Form Defined.TEST zu verwenden. Beispielskizze:
public class Defined { public static final boolean TEST = false; public static final boolean XXXX = true; ... } public class Classname { ... if (Defined.TEST) { ... } ... }Außerdem ist es für das Verstehen und die (eventuell erst Jahre später erfolgende) Wartung der Programme wichtig, mit Kommentaren innerhalb der Programme möglichst ausführliche Hinweise auf den Zweck und die Funktionsweise des Programmes und der einzelnen Programmabschnitte sowie Erklärungen zu allen programmtechnischen Tricks anzugeben.
Mit dem Hilfsprogramm javadoc, das Teil des JDK ist, kann eine Dokumentation der Java-Programme im HTML-Format erzeugt werden, so wie die Online-Dokumentation (API) der Klassenbibliothek.
Der Aufruf von
javadoc Xxxx.java
für einzelne Source-Programme
oder
javadoc *.java
für alle Java-Programme im aktuellen Directory
erzeugt für jedes Source-Programm ein
Dokumentations-File Xxxx.html
sowie (bis JDK 1.1)
einen Package-Index packages.html,
einen Klassen-Index tree.html und
einen Namens-Index AllNames.html
bzw. ab JDK 1.2 eine größere Anzahl von
solchen HTML-Files,
die von einem Start-File index.html ausgehen.
Vorsicht,
bereits existierende gleichnamige Dateien werden dabei
überschrieben.
Javadoc extrahiert automatisch alle
public und protected
Deklarationen von
Klassen,
Datenfeldern, Konstruktoren und Methoden.
Zusätzliche Informationen kann man in Form von speziellen
Kommentaren hinzufügen,
die jeweils in
/** ... */
eingeschlossen werden,
vor der jeweiligen Deklaration stehen
und einfachen HTML-Text enthalten.
Beispiel:
/** * ein einfaches Hello-World-Programm. * <p> * Im Gegensatz zum kurzen, rein statischen "HelloWorld" * ist dieses Programm ein Musterbeispiel * für eine <b>objekt-orientierte</b> Java-Applikation. * * @author Hubert Partl * @version 99.9 * @since JDK 1.0 * @see HelloWorld */ public class HelloDoc { /** der Text, der gedruckt werden soll. */ public String messageText = "Hello World!"; /** druckt den Text messageText auf System.out aus. * @see #messageText */ public void printText() { System.out.println (messageText); } /** Test des HelloDoc-Objekts. */ public static void main (String[] args) { HelloDoc h = new HelloDoc(); h.printText(); } }Die von javadoc erstellte Dokumentation finden Sie im File HelloDoc.html.
Schreiben Sie eine einfache Applikation, in der Sie die Quadratzahlen von ein paar ganzen Zahlen berechnen und ausgeben:
1 * 1 = 1
2 * 2 = 4
3 * 3 = 9
usw. bis
20 * 20 = 400
Der Bruttopreis ist mit 100 Euro inkl. 20 % USt. angegeben. Wieviel macht die Umsatzsteuer aus? Wie hoch ist der Nettopreis?
Anmerkungen:
Der Steuerbetrag ist
brutto * prozentsatz / ( 100 + prozentsatz)
Der Nettopreis ist der Bruttopreis minus den Steuerbetrag.
Schreiben Sie eine Applikation, die für einen bestimmten Geldbetrag und einen bestimmten Zinssatz ausgibt, wie sich dieser Wert Jahr für Jahr erhöht (für 10 Jahre, jeweils Jahr und Wert).
Anmerkung: Der Wert erhöht sich jedes Jahr um den Faktor
(100.0 + zinssatz) / 100.0
Erweiterte Versionen dieses Beispiels als Applikation mit Fehlerbehandlung und als Applet mit graphischem User-Interface folgen in den Kapiteln über Fehlerbehandlungen und Applets.
In der prozeduralen Programmierung werden Daten und Prozeduren separat definiert. In der objekt-orientierten Programmierung werden die Eigenschaften und Aktionen von Objekten zusammengefasst. Welche Vorteile hat das für den Programmierer?
Bei der konventionellen Programmierung erfolgt die Definition der Datenstrukturen und der mit den Daten ausgeführten Prozeduren (Aktionen) unabhängig voneinander. Das Wissen um die Bedeutung der Daten und die zwischen ihnen bestehenden Beziehungen ist nicht einfach bei den Daten sondern mehrfach in allen Programmen, die auf diese Daten zugreifen, gespeichert.
Bei der objekt-orientierten Programmierung (OOP) werden Objekte ganzheitlich beschrieben, d.h. die Festlegung der Datenstrukturen und der mit den Daten ausgeführten Aktionen erfolgt in einem.
Die wichtigsten Vorteile der objekt-orientierten Programmierung sind:
Je kleiner und einfacher die Objekte und Klassen und die Schnittstellen zwischen ihnen gewählt werden (Kapselung, Vererbung), desto besser werden diese Ziele erreicht. Mehr darüber folgt später im Kapitel über Objekt-orientierte Analyse und Design.
DATA DIVISION. 01 DDD1. ... Datenblock 1 ... 01 DDD2. ... Datenblock 2 ... ... PROCEDURE DIVISON. AAA1 SECTION. ... Aktionen Teil 1 ... AAA2 SECTION. ... Aktionen Teil 2 ... ...
struct ddd1 { ... Datenblock 1 ... } struct ddd2 { ... Datenblock 2 ... } ... aaa1() { ... Aktionen Teil 1 ... } aaa2() { ... Aktionen Teil 2 ... } ...
public class Ccc1 { ... Daten von Objekt-Typ 1 ... ... Aktionen von Objekt-Typ 1 ... } public class Ccc2 { ... Daten von Objekt-Typ 2 ... ... Aktionen von Objekt-Typ 2 ... } ...
Als Klasse (class) bezeichnet man die Definition einer Idee, eines Konzepts, einer Art von Objekten.
Als Objekt (object), Exemplar oder Instanz (instance) bezeichnet man eine konkrete Ausprägung eines Objekts, also ein Stück aus der Menge der Objekte dieser Klasse.
Beispiele: Hubert Partl und Monika Kleiber sind Objekte der Klasse Mitarbeiter. Die Universität für Bodenkultur ist ein Objekt der Klasse Universität.
Je einfacher die Klassen gewählt werden, desto besser. Komplexe Objekte können aus einfacheren Objekten zusammengesetzt werden (z.B. ein Bücherregal enthält Bücher, ein Buch enthält Seiten, ein Atlas enthält Landkarten). Spezielle komplizierte Eigenschaften können auf grundlegende einfache Eigenschaften zurückgeführt werden (Vererbung, z.B. ein Atlas ist eine spezielle Art von Buch).
Java-Programme sind grundsätzlich Definitionen von Klassen. Sie haben typisch den folgenden Aufbau:
public class ClassName { // Definition von Datenfeldern // Definition von Konstruktoren // Definition von Methoden }
Die Reihenfolge der Definitionen innerhalb der Klasse ist grundsätzlich egal, aber ich empfehle, immer eine Konvention wie z.B. die hier angeführte Reihenfolge einzuhalten.
Objekte werden in Java-Programmen mit dem Operator new und einem Konstruktor der Klasse angelegt. Beispiel:
House myNewHome = new House();
Innerhalb der Klasse kann man das aktuelle Objekt dieser Klasse mit dem symbolischen Namen this ansprechen.
public class HelloText { public String messageText = "Hello World!"; // or: private ... public void printText() { System.out.println (messageText); } public static void main (String[] args) { HelloText h = new HelloText(); h.printText(); } }
Im Gegensatz zum statischen Hello-World-Programm lässt sich das objekt-orientierte Programm leicht erweitern. Beispiel:
public class MultiText { public static void main (String[] args) { HelloText engl = new HelloText(); HelloText germ = new HelloText(); germ.messageText = "Hallo, liebe Leute!"; HelloText cat = new HelloText(); cat.messageText = "Miau!"; engl.printText(); germ.printText(); engl.printText(); cat.printText(); engl.printText(); } }Hier werden drei Objekte der Klasse HelloText erzeugt, mit verschiedenen Texten, und dann erhalten diese Objekte die "Aufträge", ihren Text auszugeben ("Delegation"). Die Ausgabe sieht dann so aus:
Hello World! Hallo, liebe Leute! Hello World! Miau! Hello World!
Anmerkung: Eine weitere Verbesserung dieses HelloWorld-Programmes folgt im Kapitel Kapselung.
Datenfelder der Klasse werden typisch in der folgenden Form deklariert:
public typ name;
public typ name = anfangswert;
Nachdem ein Objekt dieser Klasse angelegt wurde, können seine Datenfelder in der Form
object.name
angesprochen werden. Wenn private statt public angegeben wurde, können sie jedoch von anderen Klassen nicht direkt angesprochen werden (siehe Kapselung).
Datenfelder der Klasse sind eine Art von "globalen Variablen" für alle Methoden der Klasse und für eventuelle innere Klassen.
Innerhalb der Klasse kann man die Datenfelder einfach mit
name
ansprechen, oder in der Form
this.name
wenn eine Unterscheidung von gleichnamigen lokalen Variablen oder Parametern notwendig ist.
Methoden entsprechen den Funktionen oder Prozeduren in konventionellen Programmiersprachen. Die Deklaration erfolgt in der folgenden Form:
Methode mit Rückgabewert:
public typ name () { Statements; return wert; }Methode ohne Rückgabewert:
public void name () { Statements; }Methode mit Parametern:
public typ name (typ name, typ name, typ name) { Statements; return wert; }Mit return; bzw. return wert; innerhalb der Statements kann man die Methode vorzeitig verlassen (beenden).
Nachdem ein Objekt der Klasse angelegt wurde, können seine Methoden in einer der folgenden Formen ausgeführt werden:
x = object.name();
object.name();
x = object.name (a, b, c);
object.name (a, b, c);
Man spricht in diesem Fall vom Aufruf der Methode für das Objekt oder vom Senden einer Message an das Objekt. Wenn private statt public angegeben wurde, kann die Methode von anderen Klassen nicht direkt angesprochen werden (siehe Kapselung).
Innerhalb der Klasse kann man die Methoden mit
name (parameterliste)
oder
this.name (parameterliste)
aufrufen.
Lokale Variable, die nur innerhalb einer Methode deklariert sind, können von außen nicht angesprochen werden. Bei der Deklaration von lokalen Variablen darf daher (im Gegensatz zu Datenfeldern der Klasse) weder public noch private angegeben werden.
Die Parameter werden grundsätzlich als Wert ("by value"), nicht als Name ("by reference") übergeben. Primitive Datentypen, die innerhalb der aufgerufenen Methode verändert werden, behalten also in der aufrufenden Methode ihren alten Wert. Bei Objekten und Strings wird aber nur die Referenz als Wert übergeben, nicht das Objekt selbst. Wenn das Objekt innerhalb der aufgerufenen Methode mit Hilfe dieser Referenz verändert wird, dann wirkt diese Änderung direkt auf das Objekt, also praktisch wie bei einer Übergabe als Name.
Man kann auch mehrere Methoden definieren, die den selben Namen, aber verschiedene Parameterlisten haben. Dies wird als Überladen (overloading) von Methoden auf Grund ihrer verschiedenen "Signatures" bezeichnet. Unter der Signatur einer Methode versteht man die eindeutige Kombination von Name und Parameterliste.
Man sollte das aber nur dann tun, wenn diese Methoden ähnliche Aktionen ausführen, die sich nur durch den Typ oder die Anzahl der Parameter unterscheiden.
So sind z.B. verschiedene Varianten der Methode println definiert, die je nach dem Typ des Parameters dessen Wert in einen lesbaren String umwandeln und dann diesen ausdrucken.
Wenn man vor dem Typ einer Methode das Wort static angibt, dann kann diese Methode aufgerufen werden, ohne dass vorher ein Objekt der Klasse erzeugt werden muss. Der Aufruf erfolgt dann in der Form
ClassName.name();
Statische Methoden können freilich nicht auf (nicht-statische) Datenfelder oder nicht-statische Methoden der Klasse oder auf "this" zugreifen. Innerhalb von statischen Methoden können aber Objekte mit new angelegt und dann deren Datenfelder und Methoden angesprochen werden.
Die beiden wichtigsten Anwendungsfälle für statische Methoden sind
public class Classname { public static int diff (int a, int b) { int c; c = a - b; return c; } ... }Der Aufruf erfolgt dann in der Form
z = Classname.diff (x, y);
Beispiele für statische Variable und statische Methoden sind System.out, System.exit(0), Label.CENTER, Math.sin(x) usw.
Jede Java-Applikation muss eine statische Methode mit der folgenden Signature enthalten:
public static void main (String[] args)
Diese main-Methode wird beim Starten der Applikation mit dem Befehl java aufgerufen und kann ein oder mehrere Objekte der Klasse oder auch von anderen Klassen anlegen und verwenden. Der Parameter args enthält die beim Aufruf eventuell angegebenen Parameter, mit args.length erhält man deren Anzahl.
Konstruktoren dienen zum Anlegen von Objekten der Klasse mit dem new-Operator. Sie legen den für das Objekt benötigten Speicherplatz an und initialisieren das Objekt, d.h. sie setzen es in einen gültigen Anfangszustand.
Laut Tov Are Jacobsen erfüllt der Konstruktor die Funktion eines Baumeisters, der ein Objekt mit den im Plan festgelegten Eigenschaften errichtet:
When you write a class you describe the behaviour of potential objects. Much like designing a house on paper: you can't live in it unless you construct it first.
And to do that you say "I want a new house, so I'll call the constructor":
House myNewHome;
myNewHome = new House();(Tov Are Jacobsen, comp.lang.java.help, 1998)
Im einfachsten Fall enthält die Definition der Klasse keine explizite Definition von Konstruktoren. Dann wird vom Java-Compiler ein Default-Konstruktor mit leerer Parameterliste erzeugt, der in der Form
ClassName object = new ClassName();
aufgerufen werden kann und nur den Speicherplatz anlegt und alle Datenfelder auf ihre Anfangswerte setzt.
Wenn man will, dass im Konstruktor weitere Aktionen erfolgen sollen (z.B. durch Aufruf von Methoden), so muss man den Konstruktor selbst definieren. Dies erfolgt in der Form
public ClassName() { Statements; }
Man kann auch einen oder mehrere Konstruktoren mit verschiedenen Parameterlisten definieren (Overloading). In diesem Fall wird vom Compiler kein Default-Konstruktor automatisch erzeugt: Entweder man definiert gar keinen Konstruktor oder man definiert alle explizit. Beispiel:
public class BookShelf { public Book[] book; public BookShelf (int maxBooks) { book = new Book[maxBooks]; for (int i=0; i<book.length; i++) book[i]=null; } // methods for adding and removing books ... }
In diesem Fall muss man im Konstruktor die Maximalanzahl angeben, es gibt keinen Default-Konstruktor ohne Parameter. Beispiele:
BookShelf smallShelf = new BookShelf(20); BookShelf bigShelf = new BookShelf(60);
Wenn man auch einen Default-Konstruktor haben will, was in den meisten Fällen sinnvoll ist, dann muss man ihn explizit zusätzlich angeben.
Dabei kann ein Konstruktor auch einen anderen Konstruktor mit einer entsprechenden Parameterliste aufrufen, indem man das Wort this für den Aufruf des Konstruktors angibt. Beispiel:
public class BookShelf { public Book[] book; public BookShelf() { this(20); } public BookShelf (int maxBooks) { book = new Book[maxBooks]; for (int i=0; i<book.length; i++) book[i]=null; } // methods for adding and removing books ... }
In diesem Fall kann man mit
BookShelf smallShelf = new BookShelf();
ein Regal mit der Standardgröße für maximal 20 Bücher anlegen und mit
BookShelf bigShelf = new BookShelf(60);
ein größeres Regal für maximal 60 Bücher.
Statt der Angabe von public in den obigen Beispielen sind bei Klassen, Datenfeldern und Methoden jeweils vier verschiedene Angaben möglich:
public | protected | default | private | |
selbe Klasse | ja | ja | ja | ja |
selbes Package | ja | ja | ja | nein |
Unterklasse (extends) | ja | ja | nein | nein |
überall | ja | nein | nein | nein |
Es wird dringend empfohlen, bei jeder Klasse genau zu überlegen, welche Datenfelder und Methoden "von außen" direkt angesprochen werden müssen und welche nur innerhalb der Klasse oder des Package benötigt werden. Alles, was nicht für die Verwendung von außen sondern nur für die Programmierung im Inneren notwendig ist, sollte im Inneren "verborgen" werden. Dies wird als Encapsulation oder Data Hiding bezeichnet und hat vor allem die folgenden Vorteile:
Um ungültige Datenwerte in den Datenfeldern zu vermeiden, ist es empfehlenswert, alle Datenfelder als private oder protected zu definieren und eigene public Methoden für das Setzen und Abfragen der Werte vorzusehen, die schon bei der Speicherung der Daten alle Fehler verhindern.
Per Konvention ("Java-Beans") sollen diese Methoden Namen der Form setXxxx und getXxxx haben und nach folgendem Schema definiert werden:
public class ClassName { private typ xxxx = anfangswert; public void setXxxx (typ xxxx) { // Gültigkeit des Wertes kontrollieren this.xxxx = xxxx; } public typ getXxxx() { return xxxx; } }Bei boolean Datenfeldern soll die Methode isXxxxx statt getXxxxx genannt werden. Bei Arrays (Feldern) sollen solche Methoden sowohl für das ganze Feld als auch (mit einem int-Parameter für den Index) für die einzelnen Feldelemente vorhanden sein.
Mehr über die Programmierung der Fehlerbehandlung finden Sie im Kapitel über Exceptions.
Das HelloWorld-Programm wird nach diesen Bean-Konventionen also so abgeändert, dass das Datenfeld messageText privat deklariert wird und für den Zugriff die public Methoden setMessageText und getMessageText vorgesehen werden:
public class HelloBean { private String messageText = "Hello World!"; public void setMessageText(String newText) { messageText = newText; } public String getMessageText() { return messageText; } public void printText() { System.out.println (messageText); } public static void main (String[] args) { HelloBean engl = new HelloBean(); HelloBean germ = new HelloBean(); germ.setMessageText("Hallo, liebe Leute!"); HelloBean cat = new HelloBean(); cat.setMessageText("Miau!"); engl.printText(); germ.printText(); engl.printText(); cat.printText(); engl.printText(); } }
Eine sehr ungünstige Möglichkeit, eine Klasse für Datum-Objekte zu definieren, bestünde darin, einfach nur die 3 int-Felder als public zu definieren und dann alles Weitere den Anwendungsprogrammierern zu überlassen:
public class Date1 { // very bad, don't do this! public int year; public int month; public int day;In der main-Methode können dann Objekte dieser Klasse angelegt und verwendet werden. Dabei kann es passieren, dass ungültige Werte erzeugt werden (frei nach Erich Kästner):
public static void main (String[] args) { Date1 today = new Date1(); today.day = 35; today.month = 5; today.year = 1997;Außerdem können verschiedene Programmierer verschiedene Ansichten darüber haben, in welcher Reihenfolge Tag und Monat angezeigt werden sollen, und damit die Benutzer verwirren:
System.out.println ("Today is " + today.day + ". " + today.month + ". " + today.year); System.out.println ("Today is " + today.month + ". " + today.day + ". " + today.year);Auch bei der Berechnung von Zeiträumen können manche Details übersehen werden, z.B. dass 1996 ein Schaltjahr war, 2021 aber keines ist:
Date1 marriage = new Date1(); marriage.day = 29; marriage.month = 2; marriage.year = 1996; System.out.println ("Married on " + marriage.day + ". " + marriage.month + ". " + marriage.year); Date1 silverMarriage = new Date1(); silverMarriage.day = marriage.day; silverMarriage.month = marriage.month; silverMarriage.year = marriage.year + 25; System.out.println ("Silver marriage on " + silverMarriage.day + ". " + silverMarriage.month + ". " + silverMarriage.year);Schließlich gestaltet sich auch die Programmierung von Vergleichen recht aufwändig:
if ( silverMarriage.day == today.day && silverMarriage.month == today.month && silverMarriage.year == today.year ) System.out.println ("Congratulations!"); } }
Besser im Sinne der Kapselung wäre es, die 3 Datenfelder als private zu definieren, damit sie nicht auf falsche Werte gesetzt werden können, und public Methoden und Konstruktoren vorzusehen, mit denen diese Werte gesetzt, verändert und verwendet werden:
public class Date2 { // better, but incomplete private int year; private int month; private int day;
Hier drei Konstruktoren (overloading), mit denen das Datum auf einen gültigen Anfangswert gesetzt wird. Diese Konstruktoren verwenden die unten erläuterte Methode setDate2, die die Gültigkeit der Werte sicherstellt.
public Date2() { setDate2(); } public Date2 (int year, int month, int day) { setDate2 (year, month, day); } public Date2 (Date2 other) { setDate2 (other); }
Hier nun die drei Varianten (overloading) der public Methode setDate2:
Die Variante mit leerer Parameterliste setzt das Datum auf das heutige Datum (durch Aufruf einer geeigneten Systemfunktion, hier nur skizziert, aber nicht gezeigt):
public void setDate2() { // set to today's date via system function }Die zweite Variante erlaubt die Angabe des Datums mit 3 Parametern für Jahr, Monat und Tag und überprüft, ob diese Zahlen tatsächlich ein gültiges Datum darstellen (mit Berücksichtigung von eventuellen Schaltjahren, hier nicht gezeigt). Was hier auch noch fehlt, ist die Reaktion auf solche Fehler (siehe das Kapitel über Exceptions).
public void setDate2 (int year, int month, int day) { // check for valid ranges of day and month for this year this.day = day; this.month = month; this.year = year; }Die dritte Variante setzt das Datum auf das gleiche wie ein bereits erstelltes Objekt des selben Typs. Hier ist keine neuerliche Fehlerbehandlung notwendig, da dieses Objekt ohnedies nur ein gültiges Datum enthalten kann.
public void setDate2 (Date2 other) { this.day = other.day; this.month = other.month; this.year = other.year; }
Für den Zugriff auf die privaten Datenfelder werden eigene public get-Methoden geschrieben:
public int getDay() { return day; } public int getMonth() { return month; } public int getYear() { return year; }Anmerkung: Bei der in Java 1.0 eingebauten Date-Klasse gibt es zwei getrennte Methoden getDate und getDay für den Tag innerhalb des Monats und den Wochentag.
Außerdem kann man auch weitere Methoden vorsehen, z.B. um den Monat als Wort statt als Nummer zu erhalten. Hier die englische Variante, bei der deutschsprachigen müßte man auch noch den Unterschied zwischen "Januar" in Deutschland und "Jänner" in Österreich berücksichtigen:
public String getMonthName() { String s = null; switch (month) { case 1: s = "January"; break; case 2: s = "February"; break; case 3: s = "March"; break; case 4: s = "April"; break; case 5: s = "May"; break; case 6: s = "June"; break; case 7: s = "July"; break; case 8: s = "August"; break; case 9: s = "September"; break; case 10: s = "October"; break; case 11: s = "November"; break; case 12: s = "December"; break; } return s; }
Hier noch 3 Methoden zur Berechnung von Zeitintervallen in die Zukunft oder (bei negativem Argument) in die Vergangenheit, wobei die komplizierten Details von Monats- und Jahresgrenzen und Schaltjahren hier nicht gezeigt werden. Gerade weil diese Berechnung kompliziert ist, ist es wichtig, sie innerhalb der Klasse zu programmieren und es nicht jedem einzelnen Anwender zu überlassen, sich das jedesmal neu zu überlegen.
public void addDays (int days) { // compute new date with correct day, month, and year } public void addMonths (int months) { // compute new date with correct day, month, and year } public void addYears (int years) { this.year += years; // correct day and month, if leap year problem }
Alle Klassen müssen die beiden Methoden equals (für die Abfrage auf Gleichheit von 2 Objekten) und toString (für die menschenlesbare Darstellung des Objekts als Textstring) enthalten:
public boolean equals (Object other) { if (other instanceof Date2 && ( (Date2) other).day == this.day && ( (Date2) other).month == this.month && ( (Date2) other).year == this.year) return true; else return false; } public String toString() { String s = day + " " + getMonthName() + " " + year; return s; }
Anmerkung: Die Angabe von this. ist hier nicht notwendig, macht aber die Bedeutung in diesem Beispiel klarer verständlich.
Die Verwendung dieser Klasse ist nun sehr viel einfacher, und gleichzeitig sind alle Fehlermöglichkeiten und Missverständnisse ausgeschlossen:
public static void main (String[] args) { Date2 today = new Date2(); System.out.println ("Today is " + today );
Anmerkung: Die Angabe von today.toString() ist innerhalb der println-Methode bzw. der String-Concatenation nicht nötig, diese Methode wird bei der Typ-Umwandlung automatisch aufgerufen.
Date2 yesterday = new Date2 (today); yesterday.addDays(-1); System.out.println ("Yesterday was " + yesterday ); Date2 marriage = new Date2 (1996, 2, 29); System.out.println ("Married on " + marriage ); Date2 silverMarriage = new Date2 (marriage); silverMarriage.addYears(25); System.out.println ("Silver marriage on " + silverMarriage ); if ( silverMarriage.equals(today) ) System.out.println ("Congratulations!"); } }
In Wirklichkeit ist es natürlich nicht notwendig, eine eigene Klasse für Datumsangaben zu schreiben, die ist bereits im API vorhanden und ist noch um einige Stufen komplexer als das hier gezeigte Beispiel: Die Klasse Date enthält nicht nur das Datum sondern auch die Uhrzeit, und man kann in Verbindung mit entsprechenden weiteren Klassen auch die verschiedenen Kalender, Zeitzonen, Datumsformate und Sprachen berücksichtigen (siehe das Kapitel über Datum und Uhrzeit).
Schreiben Sie eine einfache Klasse "Kurs" mit den folgenden Datenfeldern:
Fügen Sie eine main-Methode an, in der Sie diese Methoden kurz testen, indem Sie zwei oder drei Kurs-Objekte anlegen, ein paar Teilnehmer anmelden und dann die gespeicherten Informationen auf den Bildschirm ausgeben.
Frisch geplant ist halb gewonnen!
Die Erstellung von objekt-orientierten Programmen besteht aus folgenden Schritten
Die Analyse erfolgt zunächst unabhängig von der verwendeten Programmiersprache und soll die logisch richtige Sicht des Problems und damit die Grundlage für das Design liefern.
Das Design, also das Konzept für die Programmierung, berücksichtigt dann die Eigenschaften der Programmiersprache (z.B. Einfach- oder Mehrfachvererbung) und die verfügbaren Klassenbibliotheken (z.B. Schnittstellen zu Graphischen User-Interfaces oder zu Datenbanken).
Um komplexe Systeme für die objekt-orientierte Programmierung in den Griff zu bekommen, müssen sie also analysiert werden, aus welchen Objekten sie bestehen und welche Beziehungen zwischen den Objekten bestehen, welche Eigenschaften die Objekte haben und welche Aktionen mit ihnen ablaufen.
Die wichtigsten Design-Regeln für das Zerlegen von komplexen Systemen in einzelne Objekte sind:
Die Analyse und das Design ergeben sich am einfachsten, indem man versucht, das System bzw. die Aufgabenstellung mit einfachen deutschen Sätzen zu beschreiben:
Aufgabenstellung: Analyse und Design des Systems "Schulungsunternehmen, Schulungen, Teilnehmer und Trainer".
Lösungs-Skizze:
Überlegen Sie, aus welchen Objekten das System "Bücherregal mit Büchern" besteht und welche Datenfelder und Methoden die Klasse Bücherregal enthalten muss.
Es geht dabei nur um die Analyse und das Design des Systems, nicht um die konkrete Programmierung. Schreiben Sie das Ergebnis daher bitte nur in der Form von Stichworten oder graphischen Skizzen auf einem Blatt Papier auf, nicht als Java-Programm.
Überlegen Sie die Analyse und das Design für ein einfaches Konto-System:
In welchen Beziehungen stehen die im folgenden in alphabetischer Reihenfolge angeführten Begriffe (Analyse in Form von einfachen deutschen Sätzen)?
Wie können sie durch Klassen, Datenfelder und Methoden realisiert werden (Design, d.h. Entwurf bzw. Konzept für die Programmierung)?
Wie kann ich ein spezielles Programm auf ein anderes, bereits existierendes, allgemeineres Programm zurückführen?
Wie kann ich erreichen, dass meine Programme möglichst allgemein brauchbar und wieder-verwertbar sind (re-usability)?
Eine der wichtigsten Eigenschaften von objekt-orientierten Systemen stellt die sogenannte "Vererbung" von Oberklassen (Superklassen) auf Unterklassen (Subklassen) dar. Dies stellt eine wesentliche Vereinfachung der Programmierung dar und ist immer dann möglich, wenn eine Beziehung der Form "A ist ein B" besteht.
Beispiele: Ein Auto ist ein Fahrzeug (hier ist Fahrzeug der Oberbegriff, also die Superklasse, und Auto ist die spezielle Subklasse). Ein Bilderbuch ist ein Buch. Ein Mitarbeiter ist eine Person...
Keine Vererbung ist in den folgenden Fällen gegeben: Ein Bücherregal enthält Bücher. Ein Bilderbuch enthält Bilder. Ein Mitarbeiter liest ein Buch.
Der Vorteil der Vererbung besteht darin, dass man die selben Konzepte nicht mehrfach programmieren muss, sondern auf einfache Grundbegriffe zurückführen und wiederverwenden kann.
Dazu wird zunächst die Oberklasse, die den allgemeinen Grundbegriff möglichst einfach beschreibt, mit den für alle Fälle gemeinsam geltenden Datenfeldern und Methoden definiert.
Dann gibt man bei der Definition der Unterklasse mit extends an, dass es sich um einen Spezialfall der Oberklasse handelt, und braucht jetzt nur die Datenfelder und Methoden zu definieren, die zusätzlich zu denen der Oberklasse notwendig sind. Alle anderen Definitionen werden automatisch von der Oberklasse übernommen.
Beispiel:
public class Person { public String name; public Date geburtsDatum; ... } public class Beamter extends Person { public int dienstKlasse; public Date eintrittsDatum; ... }
Dann enthalten Objekte vom Typ Person die beiden Felder name und geburtsDatum, und Objekte vom Typ Beamter enthalten die vier Felder name, geburtsDatum, dienstKlasse und eintrittsDatum. Das analoge gilt für die hier nicht gezeigten Methoden.
Man kann auch Methoden, die bereits in der Oberklasse definiert sind, in der Unterklasse neu definieren und damit die alte Bedeutung für Objekte des neuen Typs überschreiben (override). Dabei braucht man eventuell nur die Teile neu zu schreiben, die zur entsprechenden Methode der Oberklasse hinzukommen, und kann für den gleichbleibenden Teil die Methode der Oberklasse mit super.name() aufrufen.
Wenn eine Methode in der Oberklasse als final deklariert ist, kann sie von Unterklassen nicht überschrieben werden.
Konstruktoren werden nicht automatisch von der Oberklasse übernommen sondern müssen neu definiert werden, wenn man mehr als den Default-Konstruktor mit der leeren Parameterliste braucht. Allerdings braucht man in diesen Konstruktoren nur diejenigen Aktionen zu definieren, die gegenüber der Oberklasse neu sind, und kann vorher - als erstes Statement - mit super(); oder super(parameterliste); den Konstruktor der Oberklasse aufrufen. Wenn man das nicht tut, wird vom Compiler automatisch der Aufruf des Default-Konstruktors super(); als erstes Statement hinzugefügt.
Referenzen auf ein Objekt der Unterklasse können sowohl in Variablen vom Typ der Unterklasse als auch vom Typ der Oberklasse abgespeichert werden. Das gilt auch bei der Verwendung in Parameterlisten. Erlaubt sind also z.B. die folgenden Zuweisungen:
Person mutter; Person vater; mutter = new Person(); vater = new Beamter();Allerdings können in diesem Fall auch für das Objekt vater nur die Datenfelder und Methoden aufgerufen werden, die für Personen definiert sind, andernfalls erhält man einen Compiler-Fehler. Man kann aber mit instanceof abfragen, um welchen Typ (Ober- oder Unterklasse) es sich zur Laufzeit handelt, und dann mit Casting die Typumwandlung durchführen. Beispiel:
public void printName (Person p) { System.out.print(p.name); if (p instanceof Beamter) { System.out.print(", " + ((Beamter)p).dienstKlasse ); } System.out.println(); }Solche Konstruktionen sind aber nur sehr selten notwendig, denn nach den Prinzipien der objekt-orientierten Programmierung sollten die Oberklassen von den Unterklassen unabhängig sein, und alles, was eine Unterklasse betrifft, sollte nur in dieser Unterklasse programmiert werden, z.B. durch Überschreiben der entsprechenden Methoden.
Schreiben Sie 3 Klassen:
Unter mehrfacher Vererbung versteht man die Möglichkeit, dass eine Klasse Unterklasse von zwei verschiedenen Oberklassen ist, also z.B. ein Dreirad ist ein Fahrzeug und ein Kinderspielzeug.
Java unterstützt keine mehrfache Vererbung, in extends kann nur eine Oberklasse angegeben werden. Eine mehrfache Angabe ist aber bei Interfaces möglich.
Interfaces sind etwas Ähnliches wie Klassen, die aber nur das Konzept für die Unterklassen skizzieren, das dann von diesen "implementiert" werden muss. Interfaces enthalten nur die "Signaturen" von Methoden; statt der Definition des Methoden-Inhalts zwischen { und } enthalten sie nur einen Strichpunkt. Die Definition eines Interface folgt dem folgenden Schema:
public interface InterfaceName { public typ name1 (parameterliste) ; public void name2 (parameterliste) ; }
Die Subklasse, die dieses Interface implementiert, muss dann alle darin skizzierten Methoden enthalten, mit der richtigen Signature (Typ, Name und Parameterliste) und mit einem konkreten Block von Statements, eventuell auch nur einem leeren Block. Beispiel:
public class ClassName implements InterfaceName { ... public typ name1 (parameterliste) { Statements; } public void name2 (parameterliste) { } }
Nach dem Wort implements können auch mehrere Interfaces angeführt werden (durch Komma getrennt). Dann muss die Klasse alle in diesen Interfaces definierten Methoden implementieren. Zusätzlich kann auch noch mit extends eine Oberklasse angegeben werden. Beispiel:
public class SubClass extends SuperClass implements Interface1, Interface2
Abstrakte Klassen (abstract class) stellen eine Mischung aus Superklassen und Interfaces dar: Ein Teil der Methoden ist in der abstrakten Klasse konkret definiert wie bei Superklassen, und von einem anderen Teil ist nur die Signature der Methoden skizziert, wie bei Interfaces. Die Definition beginnt mit
public abstract class SuperClassName
und die Vererbung in der Subklasse wie gewohnt mit
public class SubClassName extends SuperClassName
Die Subklasse muss dann konkrete Implementierungen von allen in der Superklasse nur skizzierten Methoden enthalten und kann die dort konkret definierten Methoden entweder übernehmen (Vererbung) oder überschreiben (override).
Diese kleine Denkaufgabe soll das Verständnis für die Anwendung von Interfaces vertiefen.
Das folgende einfache Programm soll funktionieren:
public class Katzenmusik { public static void main (String[] args) { LautesTier tier1 = new Katze(); LautesTier tier2 = new Hund(); for (int i=1; i<=10; i++) { tier1.gibtLaut(); tier2.gibtLaut(); } } }Wie müssen LautesTier, Hund und Katze definiert sein, damit das funktioniert?
Das Wichtige an diesem Beispiel ist, dass im Programm Katzenmusik nicht die einzelnen Eigenschaften von Hunden und Katzen angesprochen werden, sondern nur die Laute, die sie von sich geben. Dadurch wird das Programm einfach und allgemein verwendbar. Wenn wir den Hund oder die Katze durch ein anderes lautes Tier wie z.B. einen Kanarienvogel oder eine Nachtigall oder Lerche ersetzen, funktioniert das Programm genauso.
Zu diesem Zweck wird ein Interface LautesTier mit der Methode gibtLaut definiert:
public interface LautesTier { public void gibtLaut() ; }Hunde und Katzen müssen (unter anderem) dieses Interface implementieren:
public class Hund implements LautesTier { public void gibtLaut() { System.out.println("Wau wau!"); } } public class Katze implements LautesTier { public void gibtLaut() { System.out.println("Miau!"); } }Die Klassen Hund und Katze sollten natürlich noch durch weitere Interfaces, Methoden und Datenfelder ergänzt werden, mit denen alle anderen Eigenschaften dieser Tiere beschrieben werden, aber für das Programm Katzenmusik sind die nicht relevant.
Ab JDK 1.1 kann man Klassen auch innerhalb von anderen Klassen definieren, auch als private Klassen. Sie können dann nur innerhalb dieser äußeren Klasse verwendet werden und haben (im Gegensatz zu separat definierten Klassen) Zugriff auf alle in der äußeren Klasse definierten Datenfelder und Methoden, auch die privaten.
Wie im Kapitel über Objekt-orientierte Analyse und Design beschrieben, gibt es zwei Arten von Objekt-Verbindungen:
Bei Vererbung hat die Unterklasse B Zugriff auf alle nicht-privaten Datenfelder und Methoden der Oberklasse A.
Bei inneren Klassen hat die innere Klasse B Zugriff auf alle (auch die privaten) Datenfelder und Methoden der äußeren Klasse A.
Auf statische Methoden kann man jederzeit über den Klassennamen zugreifen.
In allen anderen Fällen muss das Objekt B eine Referenz auf das Objekt A "haben", wenn es auf Datenfelder oder Methoden des Objektes A zugreifen soll ("Callback"). Damit hat man dann eine wechselseitige Beziehung zwischen den Objekten, zum Beispiel in der Art wie "Du bist mein Sohn, und ich bin Dein Vater".
Es gibt zwei Möglichkeiten, wie man dem Objekt B (dem "Sohn") die Referenz auf das zugehörige Objekt A (auf den "Vater") übergeben kann:
public class A { ... B b = new B(); b.setA(this); // tell b "I am your A" ... } public class B { private A a = null; // reference to object of class A public void setA (A a) { this.a=a; } ... a.xxx(); // method of object of class A ... }
public class A { ... B b = new B(this); // tell b "I am your A" ... } public class B { private A a = null; // reference to object of class A public B (A a) { this.a=a; } ... a.xxx(); // method of object of class A ... }
Schreiben Sie eine Klasse Konto, die auch die in der Übung Person und Student erstellte Klasse Person verwendet.
Ein Konto soll folgende Eigenschaften und Methoden haben:
Überlegen Sie auch, wie Sie den Fall behandeln wollen, wenn das Konto nicht gedeckt ist, d.h. wenn versucht wird, einen Betrag abzuheben, der größer als das momentane Guthaben ist.
Zum Testen schreiben Sie eine main-Methode, in der Sie zuerst eine oder mehrere Personen anlegen, dann jeder Person ein Konto geben, und im Konto mehrere Einzahlungen und Abhebungen durchführen.
Überlegen Sie schließlich, ob und wie Sie die Klasse Konto ändern müssen, damit auch Studenten oder andere Unterklassen von Person ein Konto besitzen können, und probieren Sie das in der main-Methode aus.
Unter einem Package versteht man eine Menge von zusammengehörenden Klassen, ähnlich wie bei einer Programmbibliothek. Package ist im Wesentlichen identisch mit Directory: Alle Klassen, die im selben Directory liegen, gehören zu diesem Package. Unter-Directories gelten wiederum als eigene Packages.
In Java werden Packages einheitlich und plattformunabhängig mit Punkten zwischen den Directory-Namen geschrieben, egal, ob auf der jeweiligen Plattform Schrägstriche / oder Backslashes \ oder andere Zeichen als File-Separator verwendet werden.
Wenn man in einer Java-Klasse andere Klassen oder Interfaces ansprechen will, muss man diese im Allgemeinen "importieren". Dazu gibt man am Beginn des Source-Files mit import den Namen der Klasse mit ihrem Package-Namen an, oder man importiert gleich alle Klassen eines Package, indem man einen Stern * angibt:
import aaa.bbb.Xxxx ;
import aaa.bbb.* ;
Im ersten Fall wird die Klasse Xxxx aus dem Package aaa.bbb verfügbar gemacht, im zweiten Fall alle Klassen aus dem Package aaa.bbb. Bitte, vergessen Sie nicht den Strichpunkt, der das import-Statement beenden muss.
Wenn man seine eigenen Programme in Pakete aufteilen will, muss man jeweils zuerst mit package den Namen des eigenen Pakets angeben und dann mit import die anderen Pakete:
package pack1; import pack2.*; import pack3.*; public class Classname ...Der Compiler legt die .class-Files dann in entsprechende Subdirectories und der Aufruf erfolgt mit
java packname.Classname
Die Subdirectories der Packages werden immer relativ zu den Directories gesucht, die in der Environment-Variablen CLASSPATH angeführt sind.
Wenn der Punkt . für das jeweilige Directory in CLASSPATH enthalten ist, dann braucht man für Klassen, die im selben Directory liegen, kein import-Statement anzugeben, sondern alle Klassen, die im selben Directory liegen, stehen automatisch zur Verfügung.
Das zur Grundausstattung von Java gehörende Package java.lang und das "eigene" Package (eigenes Directory oder package-Angabe) werden vom Compiler automatisch gefunden und müssen nicht explizit importiert werden, wohl aber alle anderen wie z.B. java.util, java.text, java.awt, java.awt.event etc.
Selbstverständlich kann man Klassen und deren Datenfelder und Methoden nur dann ansprechen, wenn man die Erlaubnis dazu hat (siehe Kapselung).
Wie kann ich mein Programm in einen "optimistischen" Teil für den idealen Programmablauf und in einen "pessimistischen" Teil für die Fehlerbehandlung aufteilen?
Java bietet eine sehr bequeme Möglichkeit, etwaige Fehlersituationen zu erkennen und dann entsprechend zu reagieren, ohne gleich das ganze Programm abzubrechen. Dabei werden die Fehler von einer aufgerufenen Methode erkannt und "geworfen" (throw) und müssen dann von der aufrufenden Methode "aufgefangen" oder "abgefangen" (catch) werden.
Es gibt mehrere Arten von Fehlerbedingungen: die entweder abgefangen werden müssen oder abgefangen werden können:
Wenn eine aufgerufene Methode eine Fehlersituation erkennt, dann generiert sie ein Objekt, das den aufgetretenen Fehler beschreibt, (exception) und "wirft" diese Exception mit throw. Dies passiert meist innerhalb eines if-Blockes irgendwo innerhalb der Methode.
Damit sofort klar ist, welche Fehlersituationen in einer Methode auftreten können und daher von den aufrufenden Methoden abgefangen werden müssen, muss man schon bei der Deklaration der Methode mit throws angeben, welche Exceptions in ihr eventuell geworfen werden (eventuell mehrere, durch Komma getrennt). Falls man dies vergisst, wird man vom Compiler mit einer Fehlermeldung daran erinnert, welche Exceptions man in throws angeben muss.
Beispiel:
public void xxxMethod (parameterliste) throws XxxxException { ... if (...) throw new XxxxException(); ... }
Man kann
public class XxxxException extends YyyyException { }definiert man eine neue Exception, die den neuen Namen XxxxException und die gleichen Datenfelder und Methoden wie die bereits definierte Exception YyyyException hat. Diese Oberklasse sollte möglichst passend gewählt werden, damit die neue Exception richtig in die Hierarchie der Ober- und Unterklassen eingeordnet wird. Beispiele:
public class SalaryTooLowException extends IllegalArgumentException { }
public class StudentNotFoundException extends NoSuchElementException { }
public class LVANumberFormatException extends NumberFormatException { }
Die aufrufende Methode hat nun zwei Möglichkeiten:
try { Statements; xxxObject.xxxMethod(); Statements; } catch (XxxxException e) { System.out.println("*** error: " + e); }
Der try-Block kann ein oder mehrere Statements enthalten, und es können auch mehrere catch-Blöcke für verschiedene Fehlersituationen angegeben werden.
Wann immer eines der Statements irgendeinen Fehler "wirft", werden die restlichen Statements des try-Blocks nicht ausgeführt und es wird der (erste) catch-Block ausgeführt, der den aufgetretenen Fehler auffängt. Dann wird die Verarbeitung mit den Statements nach dem letzten catch-Block fortgesetzt oder der Fehler, falls er nicht abgefangen wurde, an die aufrufende Methode weitergeworfen. (Falls man einen finally-Block angibt, wird dieser jedenfalls auch noch ausgeführt.)
Wenn man mehrere catch-Blöcke angibt, ist die Reihenfolge wichtig, man muss zuerst die spezielleren Exceptions (Unterklassen) und dann die allgemeineren Exceptions (Oberklassen) angeben.
Im Catch-Block kann man die folgenden Methoden der Exception verwenden:
Was man im Catch-Block ausführt, hängt von der jeweiligen Anwendung ab. Meistens wird man
Wenn man vergisst, dass eine bestimmte Anweisung "gefährlich" ist und eine Fehlersituation bewirken kann, dann sagt einem der Compiler meistens mit einer Fehlermeldung, dass man sie in einen try-Block einbauen soll, und gibt auch gleich an, welche Exception man im catch-Block abfangen soll. Man muss dann entweder diesen catch-Block anfügen oder - wenn man sich mit den Details nicht beschäftigen will - die Oberklasse für alle möglichen Fehler verwenden:
catch (Exception e) { System.out.println(e); }oder eventuell sogar
catch (Throwable e) { }
Division durch Null und andere "verbotene" mathematische Berechnungen bewirken nur bei ganzzahligen Typen wie int, long etc. eine entsprechende Exception.
Bei float- und double-Zahlen bewirken Division durch Null, Logarithmus einer negativen Zahl und ähnliche "falsche" Operationen hingegen keine Exception sondern liefern als Ergebnis die speziellen Werte "unendlich" (infinite) bzw. "unbestimmt" (keine Zahl, not a number). Für die Abfrage dieser Werte und Bedingungen gibt es eigene Konstanten und Methoden in den Klassen Float und Double.
Schreiben Sie eine Applikation, die wie beim einfachen Sparbuch-Beispiel im Kapitel Syntax und Statements die Wertentwicklung eines Geldbetrages über 10 Jahre ausgibt, aber mit folgenden Erweiterungen:
Erweitern Sie die Klasse Konto aus dem Kapitel Objektverbindungen so, dass beim Versuch, das Konto zu überziehen, also einen Betrag abzuheben, der größer ist als das derzeitige Guthaben, eine passende Exception geworfen wird.
Copyright Hubert Partl - nächstes Kapitel - Inhaltsverzeichnis