Einleitung

Bearbeiten

Nötiges Vorwissen

Bearbeiten

Als Vorwissen für dieses Buch wird vorausgesetzt, dass der Nutzer mit dem Computer und der Kommandozeile (Shell) umgehen kann. Des Weiteren werden Kenntnisse der objektorientierten Programmierung vorausgesetzt.

Nicht notwendig, aber nützlich, ist die Kenntnis einer anderen mit Java verwandten Programmiersprache, wie etwa C++.

Geschichte und Namensgebung

Bearbeiten

Die Programmiersprache Java wurde 1991 von James Gosling und anderen im Hause Sun Microsystems entwickelt. Der ursprüngliche Name lautete jedoch Oak für eine große Eiche außerhalb des goslingschen Büros.

Eine ausführliche Beschreibung mit Spezifikationen der Programmiersprache ist in der deutschsprachigen Wikipedia zu finden.

Versionen von Java

Bearbeiten

Es gibt mehrere Versionen von Java, wobei grob gesagt werden kann, dass die vorletzte Version die am häufigsten eingesetzte Version ist. Dieses Buch ist im Grundstil dafür geschrieben, dass mit dieser Version gearbeitet wird. Zur Zeit ist die Version 1.4 die am meisten eingesetzte Version.

Auf die neueren Features von Java 5.0 (Tiger) wird ebenfalls an den geeigneten Stellen eingegangen.

Version 1.0 (1996)

Bearbeiten

  Version 1.1 (1997)

Bearbeiten

Wesentliche Änderung:

  • Neues Event Handling

  Version 1.2 (1998) / Java 2

Bearbeiten

Wesentliche Änderung:

  • Aufnahme von Swing in die J2SE

  Version 1.3 (2000)

Bearbeiten

  Version 1.4 (2002)

Bearbeiten

Wesentliche Änderungen:

  • Unterstützung von Zusicherungen (Assertions)
  • Aufnahme einer Logging API
  • Aufnahme einer API für reguläre Ausdrücke

  Version 5.0 (2004)

Bearbeiten

Wesentliche Änderungen:

  • Generische Datentypen
  • Enumerationen
  • Neue zusätzliche Syntax für for-Schleifen über Datenstrukturen
  • Annotationen für Metadaten

Version 6.0 (2006)

Bearbeiten

Sun Microsystems hat erstmalig sehr früh einen Einblick in die neue Version (Codename Mustang) gewährt. Einige Themen stehen zur Diskussion, darunter unter anderem aspekt orientierte Entwicklung [AOP], ein Thema, welches dem Entwickler sicher ebenso viel Freude machen dürfte wie die generischen Datentypen in Version 5 oder die Logging API in Version 1.4.

Warum Java?

Bearbeiten

Hier werden Ihnen die Vor- und Nachteile von Java aufgezeigt, damit Sie sich besser entscheiden können, ob es sich für Sie lohnt, diese Programmiersprache zu erlernen.

Vorteile

Bearbeiten

Java ist mittlerweile für die verschiedensten Computersysteme verfügbar und hat eine weite Verbreitung gefunden. Ebenso bringt Java eine umfangreiche Klassenbibliothek mit, die für (fast) alle täglichen Programmieraufgaben eine Unterstützung enthält. Durch das Konzept der virtuellen Maschine ist ein einmal compilierter Programmcode auf jeder Plattform, für die eine Java VM vorhanden ist, lauffähig. Ein erneutes Übersetzen auf der jeweiligen Zielplattform (wie bei C/C++) ist nicht mehr notwendig. Ein weiterer Vorteil sind die sogenannten "Applets". Dies sind Java-Programme, die innerhalb eines Web-Browsers gestartet werden können und somit sehr einfach Applikationen über das Internet verfügbar machen.

Ein handfester Vorteil - nicht nur für große Projekte - ist die mittlerweile freie Verfügbarkeit von integrierten Entwicklungsumgebungen, allen voran die Eclipse-Plattform. Für viele Programmierer ist gerade die Werkzeugunterstützung ein entscheidendes Kriterium bei der Auswahl einer Programmiersprache. Hier ist Java eine der am aktivsten unterstützen Plattformen.

Java hat sich mittlerweile als Industriestandard etabliert.

Nachteile

Bearbeiten

Java-Programme benötigen zur Ausführung eine Laufzeit-Umgebung. Auf vielen Computern ist diese nicht vorinstalliert und muss erst separat eingerichtet werden. Gleiches gilt jedoch auch für andere beliebte Programmiersprachen, z.B. .NET oder Perl. Ebenso sind die in gängigen Browsern eingebauten Java-Applet-Laufzeit-Umgebungen häufig veraltet. Will man Java-Programme für diese Browser schreiben, so muss man sich auf eine alte Sprachversion beschränken oder auf dem Browser eine aktuelle Laufzeitumgebung nachinstallieren.

Des Weiteren ist Java nicht geeignet, um systemnahe Programme wie etwa Hardwaretreiber zu entwickeln. Das liegt im Wesentlichen daran, dass Java-Programme auf theoretisch beliebigen Rechnerarchitekturen und Betriebssystemen unverändert lauffähig sein sollen. Diese Abstraktion lässt einen direkten Zugriff auf spezifische hardwarenahe Funktionen nicht mehr zu. Andere Sprachen wie etwa C oder C++ sind für derartige Aufgaben besser geeignet.

Geschwindigkeit von Java-Programmen

Bearbeiten

Die vorherrschende Meinung zur Geschwindigkeit von Java-Programmen ist, dass Java-Programme langsamer als vergleichbare C- oder C++-Programme seien. Das kann man jedoch nicht pauschal so sagen, denn die Geschwindigkeit von Java-Programmen hängt von verschiedenen Faktoren ab.

Eine große Rolle spielt zunächst die Virtuelle Maschine (VM), auf der das Java-Programm abläuft. In den Anfangszeiten wurde Java-Code interpretiert. Dies führte zu Leistungseinbußen, die bei modernen Java-VMs praktisch nicht mehr gegeben sind. Die Java-VM von SUN beispielsweise "kompiliert" Java-Programme sozusagen zur Laufzeit. Im Ergebnis erhält man ein Programm, das kaum noch langsamer ist als ein vergleichbares, das in C oder C++ geschrieben wurde.

Einen grundsätzlichen Geschwindigkeitsnachteil haben Java-Programme beim "Anfahren", denn jedes Java-Programm muss zunächst für sich eine eigene VM starten. Außerdem dauert das Laden der Klassen sehr lange. Neuere Implementierungen der VM (2004) beheben letzteren Nachteil dadurch, dass Klassen von mehreren Programmen gleichzeitig benutzt werden können, so dass, nachdem die erste Java-Applikation gestartet wurde, für die nachfolgenden das Laden entfällt.

Einen weiteren Geschwindigkeitsnachteil haben Java-Programme dadurch, dass bei jedem Feldzugriff die Bereichsgrenzen überprüft werden. Moderne VMs können aber die Überprüfung weitestgehend "wegoptimieren", indem bei Schleifendurchläufen - anstatt bei jedem Feldzugriff - die Bereichsüberprüfung vor dem Schleifenrumpf platziert wird. Auf diese Art lassen sich etwa 80 Prozent der ansonsten anfallenden Bereichsüberprüfungen eliminieren.

Noch ein Nachteil entsteht Java-Code dadurch, dass viele Methoden (genauer: nicht-finale Instanzmethoden) zunächst virtuell sind. Auch hier haben jedoch Compiler die Möglichkeit, Optimierungen durchzuführen; und weil die Laufzeitumgebung von Java auf die Ausführung von virtuellen Methoden optimiert ist, sind Aufrufe von virtuellen Methoden in Java erheblich schneller als in den meisten C++-Compilern.

Nicht zuletzt spielt auch die Bibliothek eine Rolle. Die von SUN favorisierte Swing-Bibliothek, die in erster Linie für die Grafikausgabe zuständig ist, steht nicht im Ruf, besonders schnell zu sein.

Es gibt aber auch Bedingungen, unter denen Java-Programme Geschwindgkeitsvorteile gegenüber C- oder C++-Programmen haben. Beispielsweise ist die Objekterzeugungsgeschwindigkeit bei Java sehr hoch. Wenn es also darum geht, viele Objekte in kurzer Zeit zu erzeugen, können Java-Programme in der Praxis hier Vorteile ausspielen. Java hat auch weniger Aliasing-Probleme als C oder C++. Aliasing bedeutet, dass sich Speicherbereiche von formal unterschiedlichen Objekten überlappen. Da es in Java weniger Aliasing-Probleme gibt, können Java-Programme bei numerischen Aufgaben gewinnbringend eingesetzt werden. (Siehe Leistungsvergleich zwischen Java und C++.)

Ob Ihr eigenes Java-Programm in der Praxis schneller oder langsamer ist als eines, das in C++ geschrieben wurde, hängt aber noch von vielen anderen Faktoren ab. Wie schon angedeutet, können Java-Programme auf schnellen und auf langsamen VMs laufen. Und wenn Sie die Geschwindigkeit mit C++ vergleichen, dann spielt selbstverständlich auch die Implementierung des zugrundeliegenden C++-Compilers eine Rolle.

Groß ist der praktische Geschwindigkeitsunterschied heutzutage in den meisten Fällen jedenfalls nicht.

Jedes Java-Programm lässt sich aber unendlich langsam machen, wenn man nur unendlich schlecht programmiert. Der Hauptfaktor dabei sind also Sie.



Um mit Java zu arbeiten, benötigt man - wie in jeder anderen Programmiersprache auch - ein paar Werkzeuge, wie zum Beispiel einen Compiler und einen Editor. Diese Werkzeuge werden hier jetzt erläutert, damit Sie wissen, was Sie benötigen, wenn Sie mit Java anfangen wollen.

Allgemeines

Bearbeiten

SDK mit JRE

Bearbeiten

Zur Programmierung von Java benötigt man zum Anfang eigentlich wenige Werkzeuge. Minimal benötigt man:

  • Ein Java-Software-Development-Kit (Java SDK, früher und jetzt wieder auch JDK genannt) für das verwendete Betriebssystem.
    Das SDK muss für das Betriebssystem bestimmt sein. Im Gegensatz zu Java-Programmen selbst ist das SDK nicht plattformunabhängig. Sun stellt verschiedene SDKs für gängige Betriebssysteme wie Windows und Linux und die Sun-spezifische Unix-Variante Solaris zur Verfügung, die von Suns Webseite heruntergeladen werden können. Benötigt wird zu Anfang nur die mit JSE 6 bezeichnete aktuelle Version (JSE steht hierbei für Java Standard Edition)! Je nach Arbeitsrichtung können später noch spezielle APIs und/oder eine andere Java Edition hinzukommen.
    Wird man bei Sun für das eigene Betriebssystem nicht fündig, so ist der Betriebssystem-Hersteller der nächste Ansprechpartner. So bieten z.B. Apple und IBM Java SDKs für die eigenen Betriebssysteme auf ihren Webseiten an. Bitte achten Sie beim Herunterladen darauf, dass Sie wirklich das SDK herunterladen (ca. 50 MB) und nicht das JRE (Java Runtime Environment) - dies ist im SDK enthalten.
  • Einen beliebigen Texteditor.
    Dies kann z.B. unter Windows notepad sein (nicht sonderlich bequem) oder aber ein typischer Programmiereditor wie vi oder emacs aus der Unix-Welt.

Zusätzlich lohnt sich:

  • Die Java Dokumentation - sie beinhaltet u. a. die Java API Dokumentation im sogenannten Javadoc-Format. Aber es enthält auch eine vollständige Sprachbeschreibung, die Beschreibung aller Werkzeuge, wie Compiler oder virtuelle Maschine, Tutorien und vieles mehr.
    Es lohnt sich, die Dokumentation lokal zu installieren. Man kann sie sich z.B. auch von der Sun Webseite herunterladen.
  • Ein Webbrowser zum Lesen der Java Dokumentation.
    Einen Browser besitzen Sie zweifellos, da Sie diesen Text gerade lesen.

Wenn die Entwicklung in Java allerdings komfortabel sein soll, dann gibt es diverse so genannte integrierte Entwicklungsumgebungen (IDEs), die Ihnen die täglichen Routineaufgaben erleichtern bzw. abnehmen. Professionelle IDEs wie das sehr populäre Eclipse sind groß, mächtig und es braucht einige Zeit sie komplett zu beherrschen. Daher wird auf IDEs hier nicht weiter eingegangen. Der Umgang mit einer speziellen IDE wird ggf. Bestandteil eines eigenen Wikibook werden.

Ein Hinweis auf eine speziell für das Erlernen von Java und objektorientierter Konzepte gedachte IDE sei hier dennoch erlaubt: BlueJ wird von diversen Universitäten gepflegt, ist bewusst einfach gehalten und wird gerne im Lehrbetrieb eingesetzt.

In diesem Buch wird jedoch vom einfachsten Fall ausgegangen, dass Sie einen Editor besitzen und das Java-Software-Development-Kit.

Neben dem Begriff SDK findet man auch den Begriff JRE. Das JRE (Java Runtime Environment) ist die Laufzeitumgebung, die dazu dient, Java-Programme auszuführen. Das SDK von Sun enthält bereits eine Version des JRE, so dass man dieses nicht separat herunterladen und installieren muss. Alleine mit dem JRE lassen sich keine Programme entwickeln (es fehlt z.B. der Compiler). Zur Entwicklung braucht man immer das SDK. Zur Ausführung von Java-Programmen reicht das JRE. Die JRE ist ebenso plattformspezifisch wie das SDK.

Installation des SDK

Bearbeiten

Installieren des SDKs unter Windows

Bearbeiten

Für Windows installieren Sie einfach die selbst-extrahierende Datei. Nach dem Neustart könnten Sie schon anfangen, möchten Sie jedoch z.b. die Programme "javac.exe", "java.exe", "javaw.exe", "jar.exe" etc. von jedem Ordner aus ausführen können ohne immer den vollständigen Pfad angeben zu müssen, müssen Sie die Umgebungsvariable "PATH" verändern.

Wir nehmen mal an, Sie haben JDK an dem Ort "C:\jdk1.6.0_<version>" installiert. Nun müssen Sie die PATH-Variable um den Eintrag "C:\jdk_1.6.0_<version>\bin" erweitern. Die PATH-Variable kann man unter Windows 2000/XP unter Start -> Einstellungen -> Systemsteuerung dann auf System und Erweitert verändern. In diesem Dialog wählen Sie die Schaltfläche Umgebungsvariablen und suchen sich unter Systemvariablen die PATH-Variable aus und fügen den Pfad des SDK an. Der neue Pfad wird von den Alten durch ein Semikolon getrennt. Nach dem Bestätigen können Sie testen, ob die PATH-Variable verändert wurde, in dem Sie in der Eingabeaufforderung PATH eingeben. In der Auflistung sollten Sie nun den Pfad des SDK wiederfinden.

Unter Vista kommen Sie dort hin mit: Windows+Pause -> Erweiterte Systemeinstellungen -> Reiter Erweitert -> Button Umgebungsvariablen... -> Scrollfeld Systemvariablen.

Nach den Änderungen sollten Sie Ihren PC neu starten.

Registry Einträge

Bearbeiten

Bei der Standardinstallation werden in der Windows Registry verschiedene Schlüssel eingetragen. Diese unterscheiden sich in ihrer Position je nach Windows Version:

Version Basisschlüssel
Windows 95 HKLM\Software\JavaSoft
Windows 98 HKLM\Software\JavaSoft

Tipp: Auch wenn die Standardinstallation sich in der Registry einträgt, um somit u.a. die aktuellste Java Version zu finden, sind diese nicht zwingend. Jedoch muss ansonsten die JAVA_HOME Umgebungsvariable korrekt gesetzt sein.

Unterhalb dieser Schlüssel werden im Prefs/ Teil die Benutzereinstellungen des Pakets java.util.prefs gesichert.

Installieren des SDK unter Linux

Bearbeiten

Die Schwierigkeit unter Linux ist die Organisation der Dateien, die von Distribution zu Distribution variieren können. Deshalb wird hier nur beispielhaft erklärt, wie für welche Distribution das SDK installiert wird.

Seit einiger Zeit bietet Sun selber ein SDK für Linux an. Man kann auch dieses von Suns Webseite herunterladen. Alternativ hat bereits früher die Blackdown-Gruppe Suns Unix-SDK mit einer Lizenz von Sun nach Linux portiert. Enventuell sind für die saubere Installation des SDK von Sun bzw. Blackdown noch weitere Schritte notwendig um das SDK gut ins Betriebssystem einfügen zu können.

Zudem sind verschiedene (freie) Java-Clones für Linux erhältlich, die aber leider häufig nicht auf dem Stand von Suns SDK sind, so dass sie sich nur unter eingeschränkten Bedingungen zur Java-Entwicklung eignen.

SuSE Linux

Bearbeiten

Sie benötigen, um Java mit SuSE Linux programmieren zu können, die Professional-Ausgabe dieses Betriebssystems. Dies ist notwendig, da nur dort die benötigten Entwicklungstools mit auf den CDs bzw. DVDs enthalten sind. Alternativ können Sie sich die fehlenden Bestandteile aus dem Internet zusammen suchen. Ein guter Anlaufpunkt ist dafür der FTP-Server von SuSE.

Nun die Schritte im Einzelnen:

  1. Starten Sie Yast. Das geschieht über Kontrollzentrum -> YAST2 Module -> Software -> Software installieren.
  2. Klicken Sie nun auf die Schaltfläche Systemverwaltungsmodus und geben Sie Ihr Root-Passwort ein.
  3. Wählen Sie jetzt Paketgruppen aus und Klicken sie auf Entwicklung -> Programmiersprachen -> Java.
  4. Jetzt müssen die die Pakete Java 2 und Java 2 JRE auswählen.
  5. Klicken Sie jetzt auf Akzeptieren und die benötigte Software wird auf Ihrem Linux-Rechner installiert.

Die CLASSPATH-Umgebungsvariable müssen Sie für dieses Beispiel nicht extra setzen ;).

Suse liefert in der aktuellen Distribution (z.Zt. Suse 9.3) Java 1.4 mit, was zum Erlernen von Java ausreichen sollte. Falls dies bei Ihnen nicht der Fall sein sollte (z.B. weil Sie spezielle Features von Java 5.0 benötigen oder ausprobieren wollen) müssen Sie folgende Schritte für Ihr System nachvollziehen:

  1. Installation der JPackage-Pakete von der Suse CD/DVD (suchen nach "JPackage")
  2. Download von Suns SDK (jdk-1_5_0_04-linux-<architektur>.bin, wobei "architektur" für die jeweilige Architektur steht also z.B. "i586" oder "Amd64".
  3. Verschieben oder kopieren des heruntergeladenen SDK mit cp jdk-1_5_0_04-linux-<architektur>.bin /usr/src/packages/SOURCES
  4. Wechseln des Verzeichnisses cd /usr/src/packages
  5. Bauen der einzelen RPMs mit rpmbuild -bb SPECS/java-1.5.0-sun.spec
  6. Die fertigen Pakete liegen jetzt in /usr/src/packages/RPMS/<architektur>" - diese können mit rpm -Uhv java* installiert werden.

Das Sun JDK wurde nicht unter GPL gestellt. Daher ist sowohl Suns JDK als auch die Blackdown-Variante nicht in den Free bzw. Non-Free-Zweigen der Debian-Distributionen enthalten. Eine Linux-Binary kann jedoch von www.oracle.com heruntergeladen werden. Dort sind auch alle Installationshinweise zu finden.

Alternativ bietet Debian ein SDK, welches komplett aus freien Komponenten besteht. Während es nicht unbedingt alle Funktionalität des Sun-SDKs bietet, reicht es für viele Anwendungen aus. Die Installation erfolgt mittels

aptitude install free-java-sdk

Nach dem Setzen der JAVA_HOME-Umgebungsvariable steht ein SDK mit gewöhnlicher Bedienung zur Verfügung.

export JAVA_HOME=/usr/lib/fjsdk

Bevor man das JDK auf Red Hat Linux installieren kann, muss man sicherstellen das GCJ und seine Komponenten nicht installiert sind oder wenn sie installiert sind, das man sie entfernt.

Gentoo Linux

Bearbeiten

Die einfachste Möglichkeit Java unter Gentoo zu installieren ist, die blackdown-version zu emergen.

emerge blackdown-jdk

Bei diesem ebuild sind alle nötigen Programme enthalten. Jetzt muss man nur noch blackdown als Standard-VM setzen, was am einfachsten mittels java-config geschieht.

java-config -L

Dieser Befehl listet alle verfügbaren VMs auf. Nun wählt man die Blackdown-VM aus.

java-config -S blackdown-jdk-[eure version]

Nun sollte das Kompilieren mit javac [datei.java] klappen. Aufgerufen werden die Programme mit java [datei], ohne die .class-Endung. Wer die Sun-j2sdk emergen will sollte trotzdem erst Blackdown installieren, da bei dieser z.B. java-config schon enthalten ist, bei der Sun-Version nicht. Man kann ja danach die VM auf Sun-j2sdk setzen.

Die Installation des Java SDK(JDK) unter Ubuntu ist sehr einfach: Das JDK von Sun befindet sich in den offiziellen Quellen (multiverse) ab Version 7.04 (Feisty Fawn). Über den Paketmanager installiert man das Paket sun-java6-jdk oder führt folgenden Befehl im Terminal aus:

$ sudo apt-get install sun-java6-jdk

Überprüfen kann man die Installation, indem man im Terminal den Befehl

$ javac -version

ausführt. Folgende Meldung sollte erscheinen:

javac 1.6.0_22

Mac OS X

Bearbeiten

Java ist integraler Bestandteil des Betriebssystems Mac OS X und muss dort nicht gesondert installiert werden. Es ist herstellerseitig unter /System/Library/Frameworks/JavaVM.framework/Versions installiert. In diesem Verzeichnis befinden sich jeweils Ordner mit den entsprechenden Java-Versionen. Standardmäßig sind folgende Versionen installiert bzw. erhältlich:

Mac OS X Vorinstalliert erhältlich
10.1 1.3.1 1.3.1
10.2 1.3.1 1.4.2
10.3 1.3.1 1.4.2
10.4 1.4.2 1.5.0
10.5 1.5.0 1.6.0

Die erhältlichen Java-Versionen können automatisch über Software-Update bezogen und installiert werden. Sind mehrere Java-Versionen installiert, so kann mit Hilfe eines kleinen Programmes eingestellt werden, welche JRE verwendet wird.

Unter BSDs gibt es zwei Probleme beim Installieren von Java: zum einen darf man die Installationsdateien aus lizenztechnischen Gründen nicht automatisiert runterladen, zum anderen stellt Sun keine nativen Binärdateien von Java für BSD bereit. Man muss entsprechende Dateien also manuell herunterladen. Falls es sich dabei nicht um inoffizielle BSD-Binarys handelt, muss man zuerst eine JDK-Umgebung mit Linux-Emulator installieren die als Bootstraper fungiert, aus der man danach native Binarys kompilieren kann. Da das aber unnötig lange dauert, wird hier nur der direkte Weg über inoffizielle Pakete beschrieben.

Die einfachste Möglichkeit an ein JDK zu kommen ist der Port java/diablo-jdk15. Dazu lädt man einfach den entsprechenen Tarball von http://www.freebsdfoundation.org/downloads/java.shtml runter, legt ihn in /usr/ports/distfiles ab und gibt dann

portinstall diablo-jdk15

oder

cd /usr/ports/java/diablo-jdk15
make install clean

ein. Alternativ lädt man sich das Package von der Seite herunter und installiert es mittels

pkg_add diablo-jdk-freebsd<version>.<arch>.1.5.0.07.00.tbz

wobei Werte in spitzen Klammern vor der Eingabe ersetzt werden müssen.

Unter Pkgsrc muss man sich zuvor das JDK (jdk-1_5_0-p3-bin-duh1-bsd-i586.tar.bz2) von http://www.duh.org/NetBSD/java2/ runterladen, in /usr/pkgsrc/distfiles/ ablegen und anschließend den Port lang/scsl-jdk15 mittels

cd /usr/pkgsrc/lang/scsl-jdk15
make install clean

installieren.

Eine IDE einrichten

Bearbeiten

Eclipse ist eine kostenlose IDE, die ursprünglich von IBM ins Leben gerufen wurde und nun als Open-Source-Projekt, unterstützt von einem Konsortium namhafter Firmen wie z.B. Intel, HP, SAP oder SuSE, voran getrieben wird. Das von Haus aus als erweiterbare Plattform für Plug-ins konzipierte Framework erlangt erst durch diese seine Funktionen. Die mittlerweile beliebteste in Java geschriebene IDE ist für die Betriebssysteme Linux, Max OS X sowie MS-Windows als Download von der Hersteller-Website verfügbar und bringt in der Grundausstattung einen sehr komfortablen Java-Editor mit. Mithilfe von entsprechenden Plug-ins kann man unter Eclipse auch noch mit anderen Programmiersprachen (u. a. C/C++, Cobol und PHP) arbeiten. Die Anzahl der Plug-ins steigt sehr rasant an und reicht von kostenlosen Erweiterungen bis hin zu teueren kommerziellen Produkten.

  • Zur Installation der Entwicklungsumgebung Eclipse benötigen Sie nur das JRE oder das Java Software Development Kit (Java SDK). Letzteres enthält neben der JRE je nach Version eine mehr oder weniger umfangreiche Sammlung von Werkzeugen zum Entwickeln und Testen von Anwendungen.
  • Unter Windows beschränkt sich die Installation auf das Entpacken der *.zip Datei in das gewünschte Verzeichnis. Beim ersten Start durch einen Doppelklick auf eclipse.exe wird die Installation vervollständigt und das Framework ist einsatzbereit.
  • Unter Linux ist die Installation ebenfalls einfach: Entweder Sie wählen die bei Ihrer Distribution mitgelieferten Pakete und installieren diese, oder Sie laden die entsprechende Datei für Ihre Rechnerarchitektur herunter, entpacken diese, wechseln in das Verzeichnis, in das Sie Eclipse entpackt haben, und starten dann Eclipse mit ./eclipse.
  • Unter Mac OS X wird die Installation analog durchgeführt. Das Archiv wird entpackt. Dadurch wird ein Verzeichnis erstellt, das die ausführbare Datei enthält.

JBuilder

Bearbeiten

JBuilder ist eine IDE, die von Borland entwickelt wird. Sie ist nicht als Open Source verfügbar. Für die Foundation Version von JBuilder gibt es eine kostenlose Nutzungslizenz. Für die Professional- bzw. für die Enterprise-Version gibt es eine kostenpflichtige Nutzungslizenz.

  • Der JBuilder hat ein entsprechendes SDK bereits mit dabei. Sie sollten sich jedoch trotzdem noch ein aktuelles SDK besorgen, da hier der JBuilder streikt.
  • Aufgrund von Änderungen des Aufbaus der class-Dateien sollten Sie auf die JBuilder Version achten:
    • JBuilder bis Version 3: native implementiert, daher sehr schnell und auch für langsamere PCs geeignet.
    • JBuilder bis Version 5: JDK 1.3 und früher
    • JBuilder Version 6 bis Version X: JDK 1.4 und früher
    • JBuilder Version 2005, 2006: JDK 5 und früher, Betriebssysteme jedoch nur noch Win XP, Win 2000 oder höher
    • Für JDK 6 ist bereits eine neue Version des JBuilders in Entwicklung
  • Unter Windows reicht ein Doppelklick auf die Installationsdatei, um den JBuilder in Windows üblicher Manier zu installieren. Es gibt auch die Möglichkeit einer Silent Installation.

NetBeans

Bearbeiten

NetBeans ist eine IDE, die ursprünglich von der Firma NetBeans als Open Source entwickelt wurde. NetBeans wurde später von Sun übernommen und blieb als Open Source erhalten. Für NetBeans gibt es eine kostenlose Nutzungslizenz. Die NetBeans-IDE ist für die Betriebssysteme Linux, Mac OS X, MS-Windows sowie Solaris als Download von der Hersteller-Website verfügbar.

Diese wird auch beim Download des SDK mit angeboten, wir empfehlen Ihnen hier jedoch, die beiden Pakete NetBeans und SDK getrennt zu installieren, um diese bei Bedarf einfach deinstallieren zu können.

Java-Editor

Bearbeiten

Nur für Windwos

Der Java-Editor ist eine sehr einfache und intuitiv bedienbare IDE für Java und UML. Sie wendet sich an alle User, die nicht erst stundenlang ein Tool installieren und konfigurieren möchten, und hat trotzdem alle Attribute einer ausgewachsenen Softwareentwicklungsumgebung, wie Syntax-Highlighting, Code-Vervollständigung (bei installierter Dokumentation der Pakete) und einen visuellen GUI-Builder. Das Tool erinnert von seiner Benutzung her an Borlands JBuilder oder Delphi (mit dem es auch geschrieben wurde) und lässt sich ähnlich einfach benutzen. Klassen können modelliert und dann wie in BlueJ interaktiv getestet werden. Da das Tool ursprünglich für Schüler der Sekundarstufe II entwickelt wurde, gibt es eine recht ausführliche Dokumentation in deutscher Sprache.



Hier kommen wir direkt zu unserem ersten Programm. Wir werden ein Programm schreiben, das auf der Eingabeaufforderung (oder Konsole, wenn Sie Linux verwenden sowie Terminal unter Mac) eine Textmeldung ausgibt und sich danach einfach wieder beendet.

Die Eingabeaufforderung - auch als DOS-Fenster bekannt - erreichen Sie unter Windows indem Sie Start → Ausführen anwählen und dort cmd eingeben (Windows NT, 2000, XP, 8, 8.1) bzw. command unter Windows 95, 98, ME bzw. Win32 (Windows 3.11).
In Windows 8 und 8.1 drücken sie die Start-Taste um das Metro-Menü aufzurufen und geben einfach cmd ein und drücken Return bzw. wählen Sie das Programm cmd aus den Suchergebnissen aus.

Unter Linux können Sie über das Menü die Konsole auswählen oder mit ALT + Funktionstasten zu einer freien Konsole wechseln.

Unter Mac OSX 10.4 (Tiger) gehen Sie in den Ordner Programme und danach auf den Ordner Dienstprogramme, dort finden Sie das Terminal.app. In Mac OSX (10.5-10.10) finden sie das Programm "Terminal.app" im Finder->Programme->Dienstprogramme.

Hello Java World!

Bearbeiten

Jetzt starten Sie bitte Ihren Editor oder Ihre IDE. Dort geben Sie das folgende Programm ein. Achten Sie dabei genau auf die Groß- und Kleinschreibung! Alternativ können Sie das Programm auch einfach aus dieser Webseite kopieren und in den Editor einfügen, so sind Sie ein paar Sekunden schneller und müssen sich nicht um die Groß-/Kleinschreibung kümmern.

Machen Sie sich jetzt noch keine Gedanken darüber, was die einzelnen Befehle zu bedeuten haben, die Erklärung folgt ein wenig weiter unten.

public class HelloWorld {
      public static void main(String[] args) {
          System.out.println("Hello Java World");
          System.exit(0);
      }
}

Speichern Sie das Programm jetzt in einem Verzeichnis Ihrer Wahl unter dem Namen HelloWorld.java. Beachten Sie bitte genau auch hier die Groß- und Kleinschreibung des Dateinamens und ändern Sie den Namen nicht. Es muss unbedingt der Name sein, den Sie in der Programmzeile public class HelloWorld verwendet haben, ergänzt um die Dateinamensendung .java.

Gehen Sie jetzt in der Eingabeaufforderung in dieses Verzeichnis und übersetzen Sie das Programm mit folgendem Befehl:

javac HelloWorld.java

Ein möglicher Fehler ist "error: cannot read: HelloWorld.java". Schreiben Sie dann einfach den Pfad davor (C:\Pfad\HelloWorld.java). Ein kurzer Pfad ist empfehlenswert. Wenn die Übersetzung fehlerfrei durchgelaufen ist, können Sie das Programm mit der folgenden Eingabe ausführen:

java HelloWorld

Fallen Ihnen zwei kleine, aber wichtige Unterschiede bei den beiden Befehlen auf? Richtig:

  1. Im ersten Fall (zum Compilieren) wird das Programm javac (mit einem 'c' am Ende) verwendet. Im zweiten Fall (zum Ausführen) aber das Programm java (ohne 'c' am Ende).
  2. Beim Compilieren mit javac mussten Sie die Dateinamensendung .java mitangeben. Bei der Programmausführung mit java dürfen Sie diese Endung nicht mitangeben.

Falls eine Fehlermeldung wie

Exception in thread "main" java.lang.NoClassDefFoundError: HelloWorld

auftritt, müssen Sie nachschauen, ob der aktuelle Pfad auch im CLASSPATH enthalten ist. Wie das gemacht wird, ist weiter oben beschrieben. Ebenso sollten Sie genau prüfen, ob Sie der Java-Datei wie oben beschrieben den richtigen Namen gegeben haben, und ob Sie versehentlich die Endung des Dateinamens beim Aufruf von java angegeben haben.

Wenn das Programm fehlerfrei durchgelaufen ist, dann müsste auf der Konsole der folgende Text erscheinen:

Hello Java World

Hello Java!

Bearbeiten

An dieser Stelle haben Sie bereits Ihr erstes Java-Programm geschrieben und wollen nun wissen, was denn nun diese einzelnen Befehle und kryptischen Zeichen bedeuten. Wir gehen hier davon aus, dass Sie sich bereits mit der Objektorientierung beschäftigt haben und somit wissen was Klassen, Objekte (Instanzen), Methoden (Operationen) und Eigenschaften (Attribute) sind. Falls Sie dies noch nicht wissen sollten, folgen Sie doch einfach dem oberen Link, wo Sie sicher fündig werden.

Hier ist nochmals das Listing unseres Hello-World-Programms:

public class HelloWorld {
    public static void main(String[] args) {
        System.out.println("Hello Java World");
        System.exit(0);
    }
}

Wir haben die Zeilen durchnummeriert, um so die Bestandteile des Programms besser erklären zu können.

In der ersten Zeile definieren wir die Klasse "HelloWorld". Dabei ist zu beachten, dass die Klasse wie der Dateiname heißen muss, also in unserem Beispiel muss die Datei "HelloWorld.java" heißen. Jeder kann auf unsere Klasse zugreifen, deshalb ist sie auch "public". Man kann die Rechte von Klassen auch einschränken, aber dazu kommen wir später.

In Zeile zwei wird die Main-Methode definiert. Diese finden Sie bei jeder Java-Anwendung (Applikationen), nicht jedoch bei Applets, Servlets oder Midlets. Diese ist sozusagen das Herz jeder Java-Anwendung. Von dieser Methode ausgehend können Sie sich die komplette Funktionsweise eines Programms erschließen.

In der Zeile 3 geben wir unsere Meldung, bei uns "Hello Java World", aus, was nicht allzu viel zu sagen hat, aber der Zweck unseres Programms ist. Nebenbei bemerkt ist es sozusagen die "höhere Weihe" eines Programmierers in der Programmiersprache seiner Wahl ein "Hello World" auszugeben. Sie finden in fast jedem Tutorial zu irgendeiner Programmiersprache ein solches "Hello-World"-Programm.

Die vierte Zeile heißt, dass wir die Anwendung beenden wollen und zwar mit einem Rückgabewert von 0. Dies beendet auch gleichzeitig die Java Virtual Machine (JVM), sodass wir wieder auf der Eingabeaufforderung landen. Auch hierbei gilt, dass nur Java-Anwendungen mit diesem Befehl beendet werden. In der Eingabeaufforderung kann man diesen Wert auch auswerten, so dass man z.B. ein Java-Programm schreiben kann, das durch verschiedene Rückgabewerte den Ablauf eines Batch-Programmes oder Shellskripts steuern kann.

Java-Klassen und auch Schnittstellen (Interface) können und werden in so genannte Pakete (Packages) gruppiert. Ein Paket ist dabei einfach eine Sammlung von Klassen. Pakete werden in den geläufigen Betriebssystemen in Form von Verzeichnissen abgebildet. Klassen und Schnittstellen, welche in einem Paket liegen, beginnen dabei mit der package Anweisung, gefolgt von den Paketnamen und dem Semikolon. Zum Beispiel:

 package org.wikibooks.de.java;

Üblicherweise gruppiert man thematisch verwandte Klassen in einem Paket.

Oft ist der Domainname des Autors Teil des Paketnamens, so dass ein Programmierer schnell die Herkunft des Pakets ermitteln und auf der zugehörigen Webseite weitere Informationen zu dem Paket finden kann. Hierbei wird zudem eine Eindeutigkeit der Pakete und somit der Java Klassen / Schnittstellen erreicht. Beispiel: org.apache.log4j → www.apache.org

Klassenbibliothek

Bearbeiten

Java enthält eine sehr umfangreiche Klassenbibliothek. Es ist dringend zu empfehlen, bei der Installation des SDK auch die zugehörige Klassenbibliotheksdokumentation (API Documentation) herunterzuladen und lokal zu installieren, bzw. in die IDE zu integrieren.

Die beim SDK Standard Edition mitgelieferte Klassenbibliothek ist in Pakete (packages) eingeteilt. Es lohnt sich, sich im Laufe der Zeit zumindest einen Überblick über die vorhandenen Pakete und deren grundsätzliche Bedeutung zu verschaffen. Einige Pakete werden bei jeder Art von Java-Programmierung so häufig gebraucht, dass deren Inhalt früher oder später in Fleisch und Blut übergehen sollte.

Dabei ist es ist nicht unbedingt notwendig, jeden einzelnen Methodenparameter auswendig zu lernen (Ausnahme: Einige gerade im US-amerikanischen Raum beliebte Programmierer-Zertifizierungen fragen solches "Wissen" in ihren Zertifizierungstests ab). Methodenparameter kann man immer schnell in der API-Dokumentation nachschlagen, oder sie werden von modernen IDEs sogar direkt angezeigt. Eine grundsätzliche Vorstellung davon, was wo und wofür in der Klassenbibliothek zu finden ist, sollte man allerdings schon haben, um zügig arbeiten zu können und halbwegs effiziente Programme zu schreiben.

Zu Anfang lohnt es sich, einen Blick auf die Dokumentation der Klassen in den folgenden Paketen zu werfen:

Paket Inhalte
java.lang Basis-Klassen wie Object. Das Rückgrat der Sprache (lang = Language = Sprache)
java.io Einfache Ein- und Ausgabeklassen für Text- und Binärdaten, Dateizugriffe und Kodierung und Enkodierung von Daten.
java.util Sehr nützliche Hilfsklassen. Insbesondere finden sich hier die sog. Collection classes. Dies ist eine Sammlung von Klassen, die gängige Datenstrukturen zur Verknüpfung von Objekten bereitstellen. Dank dieser Klassen ist es für einen Java-Programmierer in den allermeisten Fällen unnötig z.B. selber Listen, Mengen oder eine Hashtable zu implementieren.
Je nachdem, welche Art von Programmen man entwickeln möchte, sollte ein Blick in folgende Pakete folgen:
java.net Basisklassen für Netzwerkkommunikation.
java.awt
javax.swing
Klassen zur Programmierung grafischer Benutzeroberflächen


Java ist eine einfach aufgebaute, leicht zu erlernende und robuste rein objektorientierte Programmiersprache. Seit der Verbreitung der javafähigen Webbrowser Mitte der 1990er Jahre ist sie neben C++ zu einer der wichtigsten imperativen Programmiersprachen aufgestiegen und hat inzwischen nicht nur auf dem Desktop (J2SE), sondern auch auf mobilen Endgeräten (J2ME) und Unternehmensanwendungen (J2EE) einen festen Platz gefunden.



Wikipedia hat einen Artikel zum Thema:

Bei Programmiersprachen wie C oder C++ wird beim Kompilieren Maschinencode erzeugt. Dieser kann dann direkt auf dem Computer ausgeführt werden.

Im Gegensatz dazu verfolgte SUN bei der Einführung der Programmiersprache das Konzept "Write once, run everywhere!". Das heißt, man wollte eine Programmiersprache entwickeln, mit der man einmal ein Programm schreibt, welches dann auf fast allen Betriebssystemen läuft. Dazu führte man das Konzept der Java Virtual Machine (JVM) ein. Die Java Virtual Machine ist eine Abstraktionsschicht zwischen dem Javaprogramm und dem eigentlichen Betriebssystem. Bei der Kompilierung mit javac wird kein nativer Maschinencode erzeugt, sondern ein Bytecode, der dann von der JVM intepretiert werden kann. Deshalb können kompilierte Javaprogramme auch nicht direkt aufgerufen werden, sondern müssen immer über die virtuelle Maschine in Form von "java test" oder "java -jar test.jar" aufgerufen werden. Man kann sich die virtuelle Maschine als Blackbox um das Java Programm vorstellen, die die Interaktion mit dem Betriebssystem übernimmt.

Java Quelltext -- javac → Java Bytecode -- java → Betriebssystem


In Java werden standardmäßig verschiedene Anwendungsarten unterschieden.

Sind mehr oder weniger komplexe Anwendungen, die speziell für das Laufen in einem Webbrowser entwickelt werden. Sie benötigen zur Ausführung eine HTML-Datei mit einem Applet-Tag in dem sie ähnlich wie ein Bild in einer HTML-Seite eingebettet werden. Außerdem haben nichtsignierte Applets nur sehr beschränkte Zugriffsrechte auf den Host-Rechner. Damit unterliegen Sie besonderen Bedingungen. Java Standard: Applets Durch die Entwicklung bei JavaScript und HTML5 sind Applets aus der Mode gekommen und werden in diesem Buch nicht länger behandelt.

Applikationen

Bearbeiten

Applikationen sind die "normalen" Anwendungen auf Ihrem Computer und haben volle Zugriffsmöglichkeiten auf alle Bestandteile des Rechners. Sie werden mit Hilfe des javac Compilers übersetzt und mit Hilfe des Java-Interpreters java bzw. javaw ausgeführt. Es gibt hierbei noch einige Besonderheiten in Zusammenhang mit Java Webstart.

Servlets

Bearbeiten

Servlets sind die Java-Variante der sog. CGI-Skripten und gehören in die Server--Welt. Hierbei wird Java-Code auf dem Server ausgeführt, der dort z.B. Dateien entgegennimmt oder in Datenbanken schreibt.

Diese Anwendungsart gehört zur Java Enterprise-Version (J2EE).

Midlets sind die kleinen Anwendungen, welche typischerweise auf Handys etc. laufen. Diese werden mit Hilfe der Java Micro-Version (J2ME) entwickelt.


Grundlagen

Bearbeiten

Worum geht es?

Bearbeiten

Variablen sind Speicherorte für Daten wie Zahlen, Texte oder Adressen. Java ist eine statisch typisierte Programmiersprache. Das bedeutet, dass zu dem Zeitpunkt, wo das Java-Programm erstellt wird bekannt sein muss, welchen Typ eine Variable hat. Typen können so genannte primitive Datentypen sein oder beliebig komplexe Klassen.

Was sind Primitive Datentypen?

Bearbeiten

Primitive Datentypen sind eine Gruppe von Typen, mit denen man Variablen erstellen kann, die Zahlen, einzelne Zeichen oder logische Werte aufnehmen. Für jeden primitiven Datentyp gibt es zudem eine eigene, so genannte Wrapperklasse, die diesen Datentyp aufnimmt. Nachdem wir die einzelnen primitiven Datentypen besprochen haben, besprechen wir die zugehörigen Wrapperklassen und zeigen Vor- und Nachteile auf.

Die primitiven Datentypen, ihr typischer Wertebereich und die zugehörige Wrapperklasse haben wir in der folgenden Tabelle für Sie aufgeführt.

Typname Größe[1] Wrapper-Klasse Wertebereich Beschreibung
boolean undefiniert[2] java.lang.Boolean true / false Boolescher Wahrheitswert, Boolescher Typ[3]
char 16 bit java.lang.Character 0 ... 65.535 (z. B. 'A') Unicode-Zeichen (UTF-16)
byte 8 bit java.lang.Byte -128 ... 127 Zweierkomplement-Wert
short 16 bit java.lang.Short -32.768 ... 32.767 Zweierkomplement-Wert
int 32 bit java.lang.Integer -2.147.483.648 ... 2.147.483.647 Zweierkomplement-Wert
long 64 bit java.lang.Long -263 bis 263-1, ab Java 8 auch 0 bis 264 -1[4] Zweierkomplement-Wert
float 32 bit java.lang.Float +/-1,4E-45 ... +/-3,4E+38 32-bit IEEE 754, es wird empfohlen, diesen Wert nicht für Programme zu verwenden, die sehr genau rechnen müssen.
double 64 bit java.lang.Double +/-4,9E-324 ... +/-1,7E+308 64-bit IEEE 754, doppelte Genauigkeit
  1. Mindestgröße, soweit bekannt
  2. https://docs.oracle.com/javase/tutorial/java/nutsandbolts/datatypes.html
  3. Wir gebrauchen zur zeit in diesem Buch die Begriffe "Boolescher Typ" und "Wahrheitswert" synonym, wünschen uns aber eine Vereinheitlichung in Richtung "Boolescher Typ" oder "Boolesche Variable, wenn Variablen dieses Typs gemeint sind.
  4. https://docs.oracle.com/javase/tutorial/java/nutsandbolts/datatypes.html

Wir zeigen ihnen nun den Umgang mit den einzelnen Typen jeweils an einem Beispiel.

Der Datentyp char kann jedes beliebige Unicode-Zeichen aufnehmen, insbesondere europäische und asiatische Zeichen sind damit abgedeckt. Hierfür benötigt er 2 Bytes an Speicherplatz pro Zeichen. Zeichen werden in Hochkommata eingeschlossen.

char ersterBuchstabe = 'A';
System.out.println("Der erste Buchstabe des Alphabets ist ein");
System.out.println(ersterBuchstabe);

Es spielt für Java keine Rolle, ob Sie eine char-Variablen ein Zeichen oder eine Zahl zuweisen. Ferner können Sie mit Zeichen auch rechnen:

public class ZeichenTest {
      public static void main(String[] args) {
	  char buchstabe1 = 'A';
	  char buchstabe2 = 65;
	  buchstabe1 += 1;
          System.out.println(buchstabe1);
          System.out.println(buchstabe2);
      }
}

Dieses Programm erzeugt die Ausgabe "B A". Addiert man zu einem Zeichen eine 1, dann ist damit das nächste Zeichen gemeint. Ebenso können Sie Zeichen auch numerisch eingeben, wie buchstabe2 = 65 zeigt. 65 ist der Zahlenwert für das Zeichen 'A'.

Ganze Zahlen

Bearbeiten

Ein byte ist der kleinste numerische Datentyp in Java, er ist 8 bit lang. Dieser Datentyp tritt zumeist im Zusammenhang mit Feldern auf, auf die wir in einem späteren Kapitel zu sprechen kommen.

byte einByte = 10; 
byte anderesByte = -10;
System.out.println(einByte + " " + anderesByte);

Obenstehendes Beispiel zeigt, wie man Variablen vom Typ byte deklariert und initialisiert.

Der Datentyp short ist 2 Bytes groß und vorzeichenbehaftet. Oft werden Konstanten mit diesem Datentyp angelegt, tatsächlich aber wird short nur selten wirklich gebraucht.

Der Datentyp int ist wohl der am häufigsten eingesetzte primitive Typ. Er belegt 4 Bytes, was in der Regel für viele Anwendungsbereiche ausreicht.

int a = 1;
int b = 1;
int c = a + b;
System.out.println("a mit dem Wert " + a + " plus B mit dem Wert " + b + " ergibt: C " + c);

der Datentyp long ist die erweiterte Form des int-Typs. Im Gegensatz zum int hat dieser Datentyp 8 Bytes. Wenn man bei der Darstellung von Zahlenliteralen Wert darauf legen möchte, dass sie zum Datentyp long gehören, dann fügt man den Suffix "L" oder "l" (kleines L) an. Das muss man aber nicht, der Compiler wandelt Literale entsprechen um.

Ein Beispiel:

 long a = 10L;
 long b = 20;
 long c = a + b;
 System.out.println(a + " + " + b + " = " + c);

Die Ausgabe ist 10 + 20 = 30.

Gleitkommatypen

Bearbeiten

Mit den Datentypen float und double können Sie eine Teilmenge der rationalen Zahlen darstellen. Sprachlich sind Gleitkomma- und Fließkommazahlen gebräuchlich. Beide Typen unterscheiden sich durch ihre Genauigkeit und den Speicherverbrauch. Gemeinsam ist, dass solche Zahlen nur ungefähr richtig gespeichert werden.

Eine Variable vom Typ float speichert eine Gleitkommazahl mit einer Größe von 4 Byte. Die Genauigkeit liegt bei 7 signifikanten Stellen, man spricht hier von einfacher Genauigkeit (engl: single precision). Zahlenliteralen stellt man ein "f" oder "F" nach, sonst nimmt der Java-Compiler an, es handele sich um double-Literale.

Ein Beispiel:

float a = 1.00f; 
float b = 3.00f;
float c = a/b;
float d = c * b; // sollte a ergeben
System.out.println(a + "/" + b + " = " + c);
System.out.println(c + "*" + b + " = " + d + " ( " + a + ")");

Ausgabe:

1.0/3.0 = 0.33333334
0.33333334*3.0 = 1.0 ( 1.0)

Dieses Beispiel zeigt gleichzeitig ein Problem mit Gleitkommazahlen, da hier (1/3) in der ersten Zeile falsch dargestellt wird, die Gesamtrechnung ist aber offenbar richtig.

Verwenden Sie den Datentyp float nur, wenn es ihnen nicht auf Präzision ankommt.

Der Typ double bietet Gleitkommavariablen mit einer Größe von 8 Byte. Die Genauigkeit liegt bei 15 signifikanten Stellen, dies bezeichnet man als doppelte Genauigkeit (engl. double precision).

double a = 1.00; 
double b = 3.00; 
double c = a/b;
double d = c * b; // sollte a ergeben
System.out.println(a + "/" + b + " = " + c);
System.out.println(c + "*" + b + " = " + d + " ( " + a + ")");

Ausgabe:

1.0/3.0 = 0.3333333333333333
0.3333333333333333*3.0 = 1.0 ( 1.0)

Boolescher Typ

Bearbeiten

Eine boolesche Variable boolean kann einen von zwei Zuständen annehmen: true oder false. Dies repräsentiert in Ausdrücken zumeist eine Bedingung ist erfüllt – oder eben nicht.

Boolsche Variablen werden zumeist im Zusammenhang mit Verzweigungen gebraucht, auf die wir im Kapitel über Kontrollstrukturen zu sprechen kommen.

boolean istWahr = true;
 
if (istWahr) {
	// Mach irgendwas
}

Casting in Java

Bearbeiten

Casting nennt man die Überführung eines Datentypen in einen Anderen. Es gibt implizites und explizites Casting. Ist die Wertemenge eines numerische Datentyps Teilmenge eines anderen Datentyps, dann lässt sich der erste Datentyp in den Zweiten überführen. Hierfür braucht man nichts weiter zu tun, man spricht vom impliziten Casting. Ist aber der Wertebereich eines Datentyps eine echte Obermenge eines anderen, dann kann es zu Datenverlusten führen, den ersten Datentyp in den Zweiten zu überführen. Beispielsweise kann man byte nach int überführen, umgekehrt aber nur explizit, das heißt, man schreibt explizit hin, dass man diese Umwandlung wünscht und akzeptiert, dass es zu Datenverlust kommen kann.

int  ii = 42;
byte bb = 100;
// implizites Casting ("automatisches Casting") weil int den größeren Wertebereich hat
ii = bb;
// explizites Casting weil der Wertebereich von int eine echte Obermenge von byte ist
bb = (byte) ii;



Häufig benötigt ein Programmierer mehrere zusammengehörige Variablen desselben Datentyps, die logisch oder verwaltungstechnisch zusammengehören. Es wäre aber sehr aufwendig, diese Variablen alle einzeln zu deklarieren und zu verarbeiten. Deswegen wird in Java, wie in anderen Programmiersprachen auch, die Verwendung von Arrays (deutsch etwa: Felder) unterstützt. In Arrays lassen sich alle primitiven Datentypen und alle Objekte speichern und systematisch bearbeiten. Alle Variablen haben einen gemeinsamen Namen, werden aber über unterschiedliche Indexe angesprochen.

Ein Array wird sehr ähnlich wie eine normale Variable deklariert: erst wird der Datentyp genannt, dann der Bezeichner für diese Variable. Bitte beachten Sie aber den Unterschied, dass hinter dem Datentyp eckige Klammern als Zeichen gesetzt werden. Sie zeigen an, dass wir es hier mit einem Array dieses Typs zu tun haben:

int [] array;

Im Gegensatz zu anderen Variablendeklarationen muss der Platz für das Array aber noch reserviert werden; die Deklaration sagt dem Compiler nur, unter welchem Namen das Feld angesprochen werden soll. (Für Leser mit Vorwissen aus anderen Programmiersprachen: Dies ist lediglich ein Zeiger. Solange dem Bezeichner kein Array zugewiesen wurde ist der Bezeichner mit null initialisiert.) Um den Platz für das Array zu reservieren und es damit funktionsfähig zu machen muss dieser Platz mit dem Schlüsselwort new ausdrücklich angefordert werden. Dafür gibt es mehrere Möglichkeiten. Die Einfachste nennt die Anzahl der Elemente, die in dem Feld gespeichert werden soll:

array = new int [10];

Jetzt verweist der Bezeichner array auf ein Feld von zehn Variablen des Typs int. Diese einzelnen Variablen sprechen wir an, indem wir den Namen des Feldes und den Index der gewünschten Variablen angeben. Im folgenden Beispiel setzen wir den Wert des 5. Wertes im Feld auf 3:

array [4] = 3;

Bitte beachten Sie dabei, dass die Zählung bei 0 beginnt. Wenn Sie den 5. Wert setzen wollen muss der Index also 4, der gewünschte Platz minus 1, sein.

Sie können einem Array auch bei der Definition Werte zuweisen:

int [] array = new int [] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };

Auch dieses Array ist zehn Felder lang, die aber bei der Deklaration sofort initialisiert werden. Übrigens werden durch den Java-Compiler alle numerischen Variablen eines Arrays bei der Initialisierung automatisch auf 0 gesetzt; bei Objekten wird initial der Wert null zugewiesen. Eine manuelle Initialisierung auf diese Werte ist also überflüssig.

Bitte beachten Sie, dass Arrays bei der Definition eine konkrete Anzahl von Elementen zugewiesen wird, in unseren Beispielen zehn Werte. Diese Anzahl bleibt unveränderlich; es können weder zusätzliche Werte aufgenommen noch überflüssige Werte freigegeben werden. Java überprüft bei jedem Zugriff auf ein Array, ob der gewünschte Zugriff gültig ist. Falls der Index kleiner 0 oder größer als die Länge des Feldes ist wird eine Fehlermeldung generiert, die den Programmablauf normalerweise abbricht. Es ist also sinnvoll, den Zugriff auf Elemente des Arrays nur mit eigenen Methoden zu erlauben, die einen solchen Zugriff überwachen. Das könnte im einfachsten Fall folgendermaßen aussehen:

 public class JavaArrayTest1 {
	// Array definieren
	private double [] value;
  
	// Konstruktor; hier wird der Platz für das Array reserviert
	public JavaArrayTest1() {
		value = new double [10];   
	}
   
	// Methode zum Setzen eines Array-Wertes. Der Index wird überprüft.
	// Wenn der Index außerhalb des Gültigkeitsbereichs liegt wird der Aufruf ignoriert.
	public void setValue(int index, double wert) {
		if(index >= 0 && index < value.length)
			value [index] = wert;
	}
   
	// Methode zur Abfrage eines Array-Wertes. Der Index wird überprüft.
	// Wenn der Index außerhalb des Gültigkeitsbereichs liegt wird der Wert 0.0 zurück geliefert.
	public double getValue(int index) {
		double result = 0.0;

		if(index >= 0 && index < value.length)
			result = value [index];

		return result;
	}  
 }

Diese - sehr einfache - Fehlerbehandlung sorgt dafür, dass das Programm nur gültige Indexe verarbeitet. Wenn das Array also wie hier mit dem Schlüsselwort private für andere Klassen verborgen wird, dann kann der Zugriff nur mit den von außen sichtbaren Methoden setValue() und getValue() erfolgen, die die Gültigkeit des Zugriffs sicherstellen. Das Programm läuft stabiler.

Zusätzlich wird in den gezeigten Methoden auf eine Eigenschaft zugegriffen, die wir noch ansprechen müssen: Die Länge des Arrays. Sie wird für jedes Array in der Variablen length gespeichert, die das Programm jederzeit abfragen kann, indem der Bezeichner des Arrays durch einen Punkt und den Variablennamen length ergänzt wird. Hier geschieht es in den Abfragen zum Gültigkeitsbereich: Der an die Methode übergebene Index muss kleiner sein als der Wert in value.length. Dadurch können weitere Fehler beim Programmieren ausgeschlossen werden. Falls nämlich das Feld value im Zuge weiterer Programmierungen in der Größe geändert werden muss, kann der Programmierer einfach die Deklaration im Kopf der Klasse ändern - und fertig. Alle Abfragen passen sich automatisch an, denn der Compiler ändert während der Übersetzung des Programmcodes diese Variable in den aktuellen Wert. Außerdem wird der Quellcode der Klasse leichter lesbar, denn durch die Verwendung des Namens statt einer Zahl wird deutlicher, was diese konkrete Programmierung bewirken soll.

Standardlösungen: Die Klasse java.util.Arrays

Bearbeiten

Für viele Bearbeitungen, die auf Arrays angewendet werden können, gibt es in Java bereits Lösungen. Einige von ihnen sind in der Klasse Arrays verwirklicht, die im Package java.util untergebracht ist. Zu diesen Standardaufgaben gehören unter Anderem die Initialisierung, die Sortierung und die Suche von Werten in Arrays. Um diese Methoden zu nutzen genügt der einfache Aufruf mit dem vorgesetzten Klassenpfad, denn die Methoden sind alle statisch. Zwei Beispiele für solche Aufrufe, "fill()" und "sort()", finden Sie in folgendem Code, den Sie in Ihre Programmier-Umgebung kopieren und dort ausführen lassen können; er ist in dieser Form vollständig lauffähig.

package javaarraytest;

public class JavaArrayTest {
	public static void main(String[] args) {
		// Array definieren
		double [] array;

		// Initialisierung des Arrays; gehört normalerweise in den Konstruktor
		array = new double[10];

		// Mit Zahlen initialisieren
		java.util.Arrays.fill(array, -1.0);

		// Ausgabe des Ergebnisses auf dem Bildschirm
		print(array, "Initialisierung mit java.util.Arrays.fill()");

		// Initialisierung eines Teilbereiches
		java.util.Arrays.fill(array, 3, 6, -2.0);

		// Erneute Testausgabe
		print(array, "Initialisierung eines Teilbereiches");

		// Nun mit Zufallswerten zwischen 0.0 und 1.0 füllen
		for(int i=0; i<array.length; i++)
			array[i] = Math.random();
	
		// Wieder ausgeben
 		print(array, "Gefüllt mit Zufallszahlen");

		// Sortieren
		java.util.Arrays.sort(array);

		// Und wieder ausgeben
		print(array, "Sortiert mit java.util.Arrays.sort()");

		// Für eigene Tests: Hier einfügen; Ausgabe mit "print()", wie oben

		// Ende der main()-Methode
		return;
  	}
    
	// Ausgabeschleife; als eigene Funktion, da sie mehrfach benötigt wird
	private static void print(double[] array, String title) {
		System.out.println(title); // Überschrift anzeigen

		// Jede einzelne Zahl in eigener Zeile und nummeriert anzeigen
		for(int i=0; i<array.length; i++)
			System.out.println(i+1 + ". Zahl: " + array[i]);

		// Eine Leerzeile als Abstand zur nächsten Ausgabe einfügen
		System.out.println();
	}

	// Variante von print(), um Anzeigefehler bei eigenen Tests zu vermeiden
	private static void print(double[] array) {
		print(array, "");
	}
}

In diesem Beispiel werden zwei verschiedene Methoden der Klasse Arrays aufgerufen. Die Erste ist fill(), mit der das Array mit einem vorgegebenen Wert, hier -1, initialisiert wird. In einem zweiten Aufruf wird nur ein Teil des Arrays befüllt, indem zusätzliche Parameter für den gewünschten Bereich angegeben werden. Die Parameter geben die Untergrenze (hier 3) und die Obergrenze (hier: 6) des Index an, die für den Aufruf gelten. Dabei wird die Untergrenze eingeschlossen, die Obergrenze aber ausgeschlossen. Der Aufruf füllt in diesem Fall also die Felder array[3], array[4] und array[5] mit dem Wert -2, belässt aber den Wert array[6] wie er ist. Dieses Verhalten gilt für alle Methoden der Klasse Arrays, die Teilbearbeitungen ermöglichen.

Wichtig ist ebenfalls, dass die geänderten Felder den Platz 4, 5 und 6 im Array besetzen, denn die Java-interne Zählung der Felder beginnt bei 0 statt bei 1. In der Funktion print() ist das an der Ausgabeschleife gut zu verfolgen, denn die Schleife beginnt bei 0 und läuft nur bis zur Länge des Feldes -1, in unserem Beispiel also bis 9.

Die Übersicht über die Klasse Arrays und ihrer Funktionen finden Sie hier. Es lohnt sich, die dort genannten Lösungen für Standardaufgaben zu kennen. Sie beschleunigen die eigene Arbeit und sind zudem bereits auf korrekte Funktion geprüft.

Mehrdimensionale Arrays

Bearbeiten

In vielen Programmiersprachen sind auch Arrays von Arrays möglich; so auch in Java. Die Deklaration dieser Arrays erfolgt durch entsprechend häufiges Setzen der eckigen Klammern:

double [][] array2;
double [][][] array3;

Mit diesen Aufrufen wird für array2 ein zweidimensionales Array aus double-Werten definiert, für array3 sogar ein dreidimensionales Array (also ein Array aus Arrays aus Arrays). Diese Vervielfachung lässt sich beliebig tief stapeln.

Bei der Speicherreservierung gehen wir wieder vor wie bereits oben beschrieben und reservieren den Speicherplatz mit new. Im Folgenden gehen wir nur auf das zweidimensionale Beispiel ein; alle weiteren Ebenen werden in der gleichen Form behandelt.

Wie bei einem eindimensionalen Array muss auch ein mehrdimensionales Array seinen Speicherplatz mit dem Schlüsselwort new zugewiesen bekommen. Das kann geschehen, indem in beiden Klammern die gewünschte Zahl an Elementen angegeben wird:

array2 = new double [5][4];

Diese Form des Aufrufs ergibt eine Tabelle mit fünf Zeilen und vier Spalten. Es kann aber sinnvoll sein, die Anzahl der Spalten fortzulassen und erst später eine entsprechende Zuweisung vorzunehmen. Das sieht dann wie folgt aus:

array2 = new double [5][];

Diese Definition reserviert den Platz für fünf später zuzuweisende Arrays von double-Werten, deren Größe aber noch nicht genannt wird. Einem solchen Array lässt sich ein anderes Array folgendermaßen zuweisen:

double [][] array2;
double [] test = {1.0, 2.0, 3.0};

array2 = new double [5][];
array2 [4] = test;

Damit hat das Array array2 den Zeiger auf das Array test gespeichert. Es entsteht keine Kopie des Arrays test; vielmehr können wir auf die Werte des Arrays nun unter zwei verschiedenen Namen zugreifen. Wenn wir also den Wert test [2] von 3.0 auf 4.0 ändern und danach den Wert array2 [4][2] abfragen bekommen wir als Ergebnis 4.0 zurück.

Dieses Vorgehen ermöglicht auch die Speicherung verschieden langer Arrays in einem multidimensionalen Array. Wenn wir ein Array test2 definieren, das aus lediglich zwei Werten besteht, dann können wir es trotzdem im übergeordneten Array array2 speichern.

Werden Arrays auf diese Weise zugewiesen muss der Programmierer aber vor Abfragen immer sicherstellen, dass das untergeordnete Array, auf das zugegriffen werden soll, tatsächlich existiert, weil sonst ein Zugriff auf ein Feld versucht wird, das mit null initiiert ist. Das löst eine Fehlermeldung aus, die zum Programmabbruch führt. Zusätzlich sollte der Programmierer Zugriffe vermeiden, die auf Werte hinter dem Ende des Unter-Arrays liegen, weil auch das zu einem Programmabbruch führt. Die Länge eines Unter-Arrays kann wieder über die Variable length abgefragt werden:

// Diese Abfrage ergibt die Anzahl von Plätzen für Unter-Arrays
int len1 = array2.length;

// Diese Abfrage ergibt die Anzahl von Werten im Unter-Array mit dem Index 4
int len2 = array2 [4].length;

Anonyme Arrays

Bearbeiten

In seltenen Fällen werden Arrays nur für die Initialisierung irgendwelcher Daten oder für den Aufruf von Methoden benötigt. In solchen Fällen kann man sich die Vergabe eines Bezeichners sparen. Das sollten Sie aber immer wohlüberlegt machen, weil dadurch Übersichtlichkeit im Quellcode verloren geht.

Flexible Arrays

Bearbeiten

Eine wichtige Beschränkung bei der Arbeit mit Arrays ist die festgelegte Länge. In der Praxis werden aber häufig Listen und Felder benötigt, deren Größe zunächst unbekannt ist oder sich im Laufe der Arbeit ändern kann. Arrays sind in solchen Fällen keine gute Lösung. Java bietet dafür aber eine Reihe maßgeschneiderter Lösungen an, die im Java Collection Framework zusammengefasst sind. Die Arbeit mit diesen Klassen ist aber etwas komplizierter als die Arbeit mit Arrays. Wir werden auf sie im Abschnitt über fortgeschrittene Programmierung zurückkommen. An dieser Stelle sei nur darauf hingewiesen, dass es diese Problemlösungen bereits gibt; Sie müssen keine zusätzliche Zeit für die Lösung dieser Aufgaben einplanen.

Exceptions bei der Arbeit mit Arrays

Bearbeiten

Die Arbeit mit Exceptions wird zwar erst später in diesem Buch besprochen, doch damit das Thema Arrays geschlossen behandelt werden kann ergänzen wir hier noch die üblichen Fehlermeldungen, die beim Arbeiten mit Arrays ausgelöst werden. Sofern Sie eigene Methoden definieren, die auf Fehler bei der Eingabe so reagieren sollen wie die Klassen der Java-Bibliotheken - was empfehlenswert ist - und sie den auftretenden Fehler nicht in der aufgerufenen Methode abfangen und behandeln können oder wollen, sollten Sie diese Exceptions benutzen.

Es handelt sich um folgende Exceptions:

  • ArrayIndexOutOfBoundsException - Wird ausgelöst, falls bei einem Index ein Wert kleiner 0 oder größer als die vorgegebene Länge des Feldes angegeben wurde.
  • ArrayStoreException - Es wurde versucht, dem Array einen falschen Typ hinzu zu fügen, zum Beispiel ein Integer-Wert einem String-Array.
  • NegativeArraySizeException - Wird ausgelöst, wenn das Programm versucht, ein Array mit weniger als 0 Werten zu erzeugen.

Zusätzlich sind folgende allgemeine Exceptions üblich:

  • NullPointerException - Wird an die aufgerufene Methode als Parameter statt der erwarteten Referenz auf ein Array nur null übergeben wird diese Exception ausgelöst.
  • IllegalArgumentException - Falls bei einem Methoden-Aufruf mit zwei Parametern, die einen Teilbereich des Arrays definieren, der Index, der die Untergrenze darstellt, größer ist als der Index der Obergrenze.



Bezeichner

Bearbeiten

Bezeichner (engl.: Identifier) bezeichnen in Java Klassen, Interfaces, Variablen (bzw. Objekte), Methoden und Konstanten. Bezeichner sind also, einfach gesprochen, die Namen der zuvor genannten Elemente.

Mit Hilfe von Bezeichnern kann man die diesen Namen zugeordneten Speicherbereiche ansprechen und nutzen.

Gültige Namen

Bearbeiten

Legale Identifier können in Java aus folgenden Zeichen bestehen:

  • Buchstaben
  • Ziffern
  • Währungssymbole
  • Unterstrich

Gültige Identifier:

  • starten mit einem Buchstaben, Währungssymbol oder Unterstrich
  • starten nicht mit einer Ziffer
  • können nach dem ersten Zeichen alle Kombinationen von Buchstaben, Ziffern, Währungssymbolen enthalten
  • theoretisch ist die Anzahl der Zeichen pro Identifier unbegrenzt
  • sind keine Java-Schlüsselwörter wie new
  • Identifier sind case-sensitive, sprich Java unterscheidet zwischen Groß- und Kleinschreibung.

Gültige Bezeichner können mit folgendem Code getestet werden

Beispiel:

 
 public static void main(String[] args) {

  char startChar = 'A';
  char partChar = '_';

  if (Character.isJavaIdentifierStart(startChar)) {
   System.out.println(startChar + " is a valid start of a Java identifier.");
  } else {
   System.out.println(startChar + " is not a valid start of a Java identifier.");
  }

  if (Character.isJavaIdentifierPart(partChar)) {
   System.out.println(partChar + " is a valid part of a Java identifier.");
  } else {
   System.out.println(partChar + " is not a valid part of a Java identifier.");
  }
 }

Quelle: https://openbook.rheinwerk-verlag.de/javainsel/02_001.html#u2.1.3

Konventionen

Bearbeiten

Ähnlich wie in der "realen Welt" sollte man sich bei der Programmierung ebenfalls um einen guten Ton bemühen.

esisteinunterschiedobichalleskleinundzusammenschreibe ODER

_leserlich_formatiere_sodass_mein_Code_besser_nachvollzogen_werden_kann

Grundlegende Regel hierbei ist die Verständlichkeit und leichte Nachvollziehbarkeit des verfassten Codes (und die Kommentare nicht vergessen). Aus diesem Grund wurden die Java-Code-Konventionen geschaffen:

Klassen und Interfaces

Bearbeiten
  • Erster Buchstabe immer groß
  • "UpperCamelCase" anwenden. Wenn mehrere Worte in einem Identifier verbunden werden, sollten die Anfangsbuchstaben groß geschrieben werden.
  • Für Klassen Nomen verwenden (Hund, Katze...)
  • Für Interfaces Adjektive verwenden (Essbar...)

Methoden

Bearbeiten
  • erster Buchstabe immer klein.
  • "lowerCamelCase" anwenden.
  • Methodennamen sollten Kombinationen aus Verben und Nomen darstellen. So kann am besten beschrieben werden, wozu die Methode dient. Gute Beispiele findet man natürlich in der Java API.
  • Bsp.: getTasche(), fuelleTasche( ... )

Variablen

Bearbeiten
  • wie bei Methoden "lowerCamelCase" und erster Buchstabe immer klein.

Konstanten

Bearbeiten
  • Konstanten werden erzeugt, indem man Variablen static und final deklariert.
  • Sie werden nur in Großbuchstaben geschrieben und einzelne Worte werden durch Unterstrich voneinander getrennt.
  • Bsp.: MAX_MENGE

Variable

Bearbeiten

Deklaration

Bearbeiten

Variablen in Java sind immer lokale Variablen. Bevor man eine Variable benutzen kann, muss sie erst deklariert werden. Dies kann an beliebiger Stelle innerhalb einer Methode, in der statischen Initialisierung einer Klasse oder im Initialisierungsteil einer for-Schleife geschehen. Nur in diesem Bereich ist die Variable sichtbar. Der Name der Variablen muss innerhalb dieses Sichtbarkeitsbereichs eindeutig sein.

Da Java eine streng typisierte Sprache ist, muss bei Deklaration einer Variablen immer ein Datentyp mit angegeben werden. Dies kann ein primitiver Datentyp sein (boolean, char, byte, short, int, long, float, double) oder ein Referenztyp (eine Klasse oder Interface).

Optional kann eine Variable bei Deklaration durch einen Ausdruck zur Laufzeit mit einem Wert initialisiert werden.

Mit dem ebenfalls optionalen Schlüsselwort final wird verhindert, dass der Wert einer Variablen nach dessen Deklaration geändert werden kann; lediglich eine erstmalige Zuweisung im Konstruktor einer Klasse für Instanzvariablen ist möglich.

Die Syntax einer Variablendeklaration:

[ final ] [private|protected|public] Datentyp VARIABLENNAME [ = Ausdruck ] ;

zum Beispiel:

 
 public class Beispiel {
   static {
     int i = 0;
     // etwas Sinnvolles mit i anstellen
   }
   public void machWas() {
     long grosseGanzeZahl;
     String name;
     final float PI = 3.1415;
     boolean [] wahrheitswerte = new boolean[10];
     // etwas Sinnvolles mit grosseGanzeZahl, name und PI anstellen
     // Die Zuweisung PI = 1; verursacht einen Compilerfehler
     // Ein Zugriff auf i verursacht einen Compilerfehler
     for (int i=0; i < wahrheitswerte.length; i++) {
       // hier existiert i (ein anderes als obiges im static Block)
     }
   }
 }

Sichtbarkeits- und Zugriffsmodifizierer

Bearbeiten

Die drei Schlüsselwörter private, protected und public dienen der Modifikation der Sichtbarkeit und des Zugriffs.

  • private erlaubt nur der eigenen Klasse den Zugriff.
  • protected erlaubt der eigenen Klasse, der Paketklasse und der Elternklasse den Zugriff.
  • public erlaubt jeder Klasse, auch fremden Klassen, den Zugriff.

Ist eine Methode als private static final gekennzeichnet, ist der Zugriff am schnellsten.

Um Fehlern vorzubeugen, sollte der Zugriff so stark wie möglich eingeschränkt werden.

Initialisierung

Bearbeiten

Vor der Verwendung der Variablen muss ihnen ein Wert zugewiesen werden, der dem Datentyp entspricht. So kann man einem Integerwert(int) nicht den String "Hallo" zuweisen (zumindest nicht ohne vorherige Umwandlung)

 int i; 
 i = 12; //i muss vorher deklariert werden

oder:

 int i = 12;



Welche Operatoren gibt es in Java?

Bearbeiten

Java kennt eine Vielzahl von arithmetischen, logischen, und relationalen Operatoren, sowie einen, der außerhalb von Java keine Rolle spielt. Operatoren werden nach der Anzahl der möglichen Operanden unterteilt (unärer-, binärer- und ternärer Operator) und selbstverständlich nach der Funktion, die sie berechnen. Dieses Kapitel beschreibt die verfügbaren Operatoren in Tabellenform. Bis auf wenige Ausnahmen sollten alle Operatoren und das, was sie leisten, aus der Schule bekannt sein.

Arithmetische Operatoren

Bearbeiten
Operator Beschreibung Kurzbeispiel
+ Addition int antwort = 40 + 2;
- Subtraktion int antwort = 48 - 6;
* Multiplikation int antwort = 2 * 21;
/ Division int antwort = 84 / 2;
% Teilerrest, Modulo-Operation, errechnet den Rest einer Division int antwort = 99 % 57;
+ positives Vorzeichen, in der Regel überflüssig int j = +3;
- negatives Vorzeichen int minusJ = -j;

Für zwei besonders in Schleifen häufig anzutreffende Berechnungen gibt es eine abkürzende Schreibweise.

Operator Beschreibung Kurzbeispiel
++ Postinkrement, Addiert 1 zu einer numerischen Variablen x++;
++ Preinkrement, Addiert 1 zu einer numerischen Variablen ++x;
-- Postdekrement, Subtrahiert 1 von einer numerischen Variablen x--;
-- Predekrement, Subtrahiert 1 von einer numerischen Variablen --x;

Post- und Pre-Operatoren verhalten sich bezüglich ihrer Berechnung absolut gleich, der Unterschied ist der Zeitpunkt, wann die Operation ausgeführt wird. Zum Tragen kommt das bei Zuweisungen:

    i = 1;
    a = ++i; // i = 2 und a = 2 (erst hochzählen, dann zuweisen)

    i = 1;
    b = i++; // i = 2 und b = 1 (erst zuweisen, dann hochzählen)

Operatoren für Vergleiche

Bearbeiten

Das Ergebnis dieser Operationen ist aus der Menge true, false:

Operator Beschreibung Kurzbeispiel
== gleich 3 == 3
!= ungleich 4 != 3
> größer als 4 > 3
< kleiner als -4 < -3
>= größer als oder gleich 3 >= 3
<= kleiner als oder gleich -4 <= 4

Boolesche Operatoren

Bearbeiten
Operator Beschreibung Kurzbeispiel
! Negation, invertiert den Ausdruck boolean lügnerSpricht = !wahrheit;
&& Und, true, genau dann wenn alle Argumente true sind boolean krümelmonster = istBlau && magKekse;
|| or true, wenn mindestens ein Operand true ist boolean machePause = hungrig || durstig;
^ Xor true wenn genau ein Operand true ist boolean zustandPhilosoph = denkt ^ ist;

Operatoren zur Manipulation von Bits

Bearbeiten
Operator Beschreibung Kurzbeispiel
~ (unäre) invertiert alle Bits seines Operanden 0b10111011 = ~0b01000100
& bitweises "und", wenn beide Operanden 1 sind, wird ebenfalls eine 1 produziert, ansonsten eine 0 0b10111011 = 0b10111111 & 0b11111011
| bitweises "oder", produziert eine 1, sobald einer seiner Operanden eine 1 ist 0b10111011 = 0b10001000 | 0b00111011
^ bitweises "exklusives oder", wenn beide Operanden den gleichen Wert haben, wird eine 0 produziert, ansonsten eine 1 0b10111011 = 0b10001100 ^ 0b00110111
Operator Beschreibung Kurzbeispiel
>> Arithmetischer Rechtsshift: Rechtsverschiebung, alle Bits des Operanden werden um eine Stelle nach rechts verschoben, stand ganz links eine 1 wird mit einer 1 aufgefüllt, bei 0 wird mit 0 aufgefüllt 0b11101110 = 0b10111011 >> 2
>>> Logischer Rechtsshift: Rechtsverschiebung mit Auffüllung von Nullen 0b00101110 = 0b01011101 >>> 1
<< Linksverschiebung, entspricht bei positiven ganzen Zahlen einer Multiplikation mit 2, sofern keine "1" rausgeschoben wird. 0b10111010 = 0b01011101 << 1

Zuweisungsoperatoren

Bearbeiten

Zu vielen Operatoren aus den vorstehenden Tabellen gehört eine Schreibweise, mit der gleichzeitig zugewiesen werden kann. Damit spart man sich oft etwas Schreibarbeit. Also, statt etwa x = x * 7; zu schreiben kann man etwas verkürzt schreiben: x *= 7;.

Operator Beschreibung Kurzbeispiel
= einfache Zuweisung int var = 7;
+= Addiert einen Wert zu der angegebenen Variablen plusZwei += 2;
-= Subtrahiert einen Wert von der angegebenen Variablen minusZwei -= 2;
/= Dividiert die Variable durch den angegebenen Wert und weist ihn zu viertel /= 4;
*= Multipliziert die Variable mit dem angegebenen Wert und weist ihn zu vierfach *= 4;
%= Ermittelt den Modulo einer Variablen und weist ihn der Variablen zu restModulo11 %= 11;
&= "und"-Zuweisung maskiert &= bitmaske;
|= "oder"-Zuweisung
^= "exklusives oder"-Zuweisung
^= bitweise "exklusive oder"-Zuweisung
>>= Rechtsverschiebungzuweisung
>>>= Rechtsverschiebungzuweisung mit Auffüllung von Nullen
<<= Linksverschiebungzuweisung achtfach <<= 3;

Bedingungsoperator

Bearbeiten

Den einzigen ternären Operator ?: stellen wir im Kapitel Kontrollstrukturen vor.

Konkatenation

Bearbeiten

Zwei Strings lassen sich mit "+" aneinanderschreiben, so wie Sie es schon aus früheren System.out.println("Hallo" + " Welt" + "!");-Beispielen kennen.

Rangfolge von Operatoren

Bearbeiten

Die Rangfolge der Operatoren (engl. "operator precedence" oder auch "precedence rules") bestimmt in der Regel[1], in welcher Reihenfolge sie ausgewertet werden. Es geht darum, Klammern zu sparen. Weiß man, dass && einen höheren Rang als || hat, dann wird der Ausdruck (A && B) || C zu A && B || C. Selbstverständlich darf man trotzdem Klammern setzen.

Ganz allgemein gilt, dass Ausdrücke von links nach rechts ausgewertet werden. Das gilt nicht für Zuweisungsoperatoren.

In der folgenden Tabelle[2] werden die Operatoren und ihre Ränge aufgeführt. Je weiter oben ein Operator in der Tabelle auftaucht, desto eher wird er ausgewertet. Operatoren mit dem gleichen Rang (in der gleichen Zeile) werden von links nach rechts ausgewertet.

Rangfolge Typ Operatoren
1 Postfix-Operatoren, Postinkrement, Postdekrement x++, x--
2 Einstellige (unäre) Operatoren, Vorzeichen ++x, --x, +x, -x, ~b, !b
3 Multiplikation, Teilerrest a*b, a/b, a % b
4 Addition, Subtraktion a + b, a - b
5 Bitverschiebung d << k, d >> k, d >>> k
6 Vergleiche a < b, a > b, a <= b, a >= b, s instanceof S
7 Gleich, Ungleich a == b, a != b
8 UND (Bits) b & c
9 Exor (Bits) b ^ c
10 ODER (Bits) b | c
11 Logisch UND B && C
12 Logisch ODER B || C
13 Bedingungsoperator a ? b : c
14 Zuweisungen a = b, a += 3, a -= 3, a *= 3, a /= 3, a %= 3, b &= c, b ^= c, b |= c, d <<=k, d >>= k, d >>>= k
  1. siehe nächsten Abschnitt Fallen
  2. Zum Teil entnommen von https://docs.oracle.com/javase/tutorial/java/nutsandbolts/operators.html

Es gibt zum Glück wenig Fallstricke im Gebrauch von Operatoren. Postfix-Operatoren werden immer zuerst nach dem aktuellen Wert, den sie haben ausgewertet, erst dann erfolgt die Operation darauf.

Nicht in allen Fällen kann man sich bei Beachtung der Rangfolge Klammern sparen. Versuchen Sie doch einmal, den Ausdruck int x = ++y++; auszuwerten (wobei y vorher deklariert wurde). Trotz der klaren Vorrangregeln lässt sich dieser Ausdruck nicht kompilieren. Gut für alle, die einen solchen Quelltext lesen müssen...

Für weitere schwer zu durchschauende Fallen siehe auch Strukturierte Programmierung / Komplexe Ausdrücke.

Rechengenauigkeit

Bearbeiten

Beim Rechnen mit zwei Datentypen unterschiedlicher Genauigkeit (z.B. int und long) muss beachtet werden, dass als Ergebnis der "genaue" Datentyp berechnet wird.

long = int + long;

Wird nur ein "ungenauerer" Wert benötigt, so muss das dem Compiler mitgeteilt werden (cast).

int = (int) (int + long);

Die Genauigkeit ist hier durch den Zahl gegeben. Ein Datentyp int kann eine 32-Bit-Ganzzahl darstellen, während der Typ long 64-Bit-Ganzzahlen aufnehmen kann (= 8 Byte). Deshalb "passt" ein int-Wert problemlos in einen long-Wert. Umgekehrt kann es da schon passieren, dass der long-Wert größer als der größtmögliche int-Wert ausfällt!



Verzweigung (if)

Bearbeiten

Eine einfache Verzweigung wird durch das "if-then-else"-Konstrukt gelöst, wobei lediglich if und else in Java Schlüsselwörter sind.

if (<boolescher Ausdruck>) {
  // Anweisung(en) 1
} else {
  // Anweisung(en) 2
}

Eine if-Verzweigung führt stets die nachfolgende Anweisung aus, wenn der <boolesche Ausdruck> wahr ist. Sollen mehrere Anweisungen ausgeführt werden so sind diese in einen Block zusammenzufassen (mit Hilfe der geschweiften Klammern). Die else Bedingung ist optional - auch hier gilt dass mehrere Anweisungen in einen Block zusammenzufassen sind.

boolean beenden = false;
//[...]
if (beenden) 
  System.out.println ("Oh ich soll mich beenden");
  System.exit(0);

Das vorstehende Beispiel zeigt einen typischen Fehler in Zusammenhang mit dem if-Konstrukt. Der Befehl System.exit(0); wird immer ausgeführt, da kein Block gebildet wurde.

Es ist auch eine Frage des guten Programmierstils solche if-Abfragen immer in geschweifte Klammern zu fassen, dadurch lassen sich die einzelnen Ausdrücke besser dem jeweiligen if zuordnen.

boolean beenden = false;
//[...]
if (beenden) {
  System.out.println ("Oh ich soll mich beenden");
}
  System.exit(0);

Beim unteren Quelltext wird im Gegensatz zum oberen klar, das System.exit(0) immer ausgeführt wird, da es nicht in den Klammern steht.

Ternärer Operator (?:)

Bearbeiten

Wenn nur zwischen zwei Ausgabewerten unterschieden werden soll, so gibt es für die If-Then-Else-Anweisung eine Kurzform; und zwar den "Ternary-Operator".

(<boolescher Ausdruck>) ? AusgabewertTrue : AusgabewertFalse;
Beispiel

Anstelle der Anweisung

 //[...]
 if (gender.equals("männlich")) {
    System.out.println ("Sehr geehrter Herr");
 } else {
    System.out.println ("Sehr geehrte Frau");
 }

kann man kürzer schreiben

 System.out.println( (gender.equals("männlich") ) ? "Sehr geehrter Herr" : "Sehr geehrte Frau");

oder vielleicht etwas eleganter

 System.out.println( "Sehr geehrte" +(gender.equals("männlich") ? "r Herr" : " Frau" ));

Mehrfachverzweigung (switch)

Bearbeiten

Das if-Konstrukt führt in Zusammenhang mit mehreren Möglichkeiten meist zu einer unschönen Kaskadierung. Hier kann switch helfen. Um dieses einzusetzen müssen Sie einen Datentyp int abprüfen bzw. einen kleineren (also byte, short, char), da dieser durch die JVM automatisch gecastet wird.

 switch (myIntWert) {
 case 0 : 
     System.out.println ("Mein Wert ist 0.");
     break;
 
 case 1 : 
     System.out.println ("Mein Wert ist 1.");
 
 case 2 :
     System.out.println ("Mein Wert ist 1 oder 2.");
     break;
 
 default :
     System.out.println ("Ich habe einen anderen Wert.");
 }

Eine switch Anweisung wird stets mit dem Schlüsselwort case und meist mit default und break verwendet. Der Wert hinter dem case muss eine Konstante sein und dient dem Vergleich mit dem übergebenen Wert (hier myIntWert). Wenn diese gleich sind werden alle Anweisungen bis zum nächsten break oder dem Ende des Blocks ausgeführt. Wurden keine Übereinstimmung gefunden so werden die Anweisung nach default ausgeführt. Seit Java 7 ist es ebenfalls möglich, den Datentyp String in Switch-Anweisungen zu verwenden. Zuvor war es seit Java 5.0 lediglich möglich, anstelle von String den Datentyp enums zu benutzen.

For-Schleife

Bearbeiten

Die for-Schleife oder Zählschleife zählt von einem vorgegebenen Startwert zu einem ebenfalls vorgegebenen Endwert und führt für jeden Schritt von Start- bis Endwert alle Anweisungen im Schleifenkörper aus. Es muss also zusätzlich festgelegt werden, in welchen Intervallen bzw. Schritten von Start bis Ende gezählt wird.

For-Schleifen bestehen aus folgenden Teilen bzw. Anweisungen und Bedingungen:

  • Schleifenkopf
    • Initialisierung der Zählervariable auf den Startwert
    • Startbedingung mit Limitierung auf den Endwert - muss immer einen boolschen Wert ergeben (true | false)
    • Anweisung zum Zählen
  • Schleifenkörper
    • Anweisungen, die pro Schleifendurchlauf ausgeführt werden sollen

Die Syntax

Bearbeiten

Die grundlegende Syntax sieht folgendermaßen aus:

for(/*Initialisierung Zählervariable*/; /*Startbedingung*/; /*Zählen*/) {
	//...
	//Schleifenkörper mit Anweisungen
	//...
}

// Bsp.: for-Schleife, die von 0 bis 9 in 1er-Schritten
// durchlaufen wird
for(int i=0; i < 10; i++) {
	//...
	//Anweisungen;
	//...
}

In obigem Beispiel sieht man eine for-Schleife, die von 0 bis 9 zählt. Warum nur bis 9? Weil die Startbedingung i kleiner als 10 und nicht kleiner gleich 10 lautet.

  • Zunächst muss eine Zählervariable auf einen Startwert initialisiert werden int i=0. Wird diese Variable im Schleifenkopf deklariert, ist sie auch innerhalb ihres Namensraumes, also dem Schleifenkopf selbst sowie dem Schleifenkörper bekannt. Außerhalb der Schleife kann die Zählervariable folglich nicht angesprochen werden.
  • Dann wird die Startbedingung für die for-Schleife festgelegt: i < 10. Sie läuft also "solange i kleiner als 10 ist". Trifft diese Bedingung nicht zu, werden die Anweisungen im Schleifenkörper nicht ausgeführt. Die Startbedingung muss also immer einen boolschen Wert (true | false) ergeben.
  • Zuletzt muss noch definiert werden in welchen Intervallen gezählt wird: i++. Die Schleife zählt also in Schritten von 1 nach oben bzw. addiert nach jedem Schleifendurchlauf 1 auf die Zählervariable i.

Ablauf einer for-Schleife

Bearbeiten

Nehmen wir folgende Schleife an:

for( int i=0; i < 3; i++ ){
	System.out.println( i );
}

Sie wird folgendermaßen durchlaufen:

  1. Start: Initialisierung der Zählervariable auf den Wert 0
  2. Prüfung der Startbedingung: i = 0 ist kleiner als 3 ( i < 3 = true ), also dürfen die Anweisungen des Schleifenkörpers ausgeführt werden
  3. Erster Durchlauf: Die Zählervariable i wird am Bildschirm ausgegeben: 0
  4. Hochzählen: Die Zählervariable wird um 1 erhöht: 0 + 1 = 1
  5. Prüfung der Startbedingung: i = 1 ist kleiner als 3 ( i < 3 = true ), also dürfen die Anweisungen des Schleifenkörpers ausgeführt werden
  6. Zweiter Durchlauf: Die Zählervariable i wird am Bildschirm ausgegeben: 1
  7. Hochzählen: Die Zählervariable wird um 1 erhöht: 1 + 1 = 2
  8. Prüfung der Startbedingung: i = 2 ist kleiner als 3 ( i < 3 = true ), also dürfen die Anweisungen des Schleifenkörpers ausgeführt werden
  9. Dritter Durchlauf: Die Zählervariable i wird am Bildschirm ausgegeben: 2
  10. Hochzählen: Die Zählervariable wird um 1 erhöht: 2 + 1 = 3
  11. Prüfung der Startbedingung: i = 3 genauso groß wie 3 ( i < 3 = false ), also dürfen die Anweisungen des Schleifenkörpers nicht mehr ausgeführt werden

Verwendungszweck

Bearbeiten

For-Schleifen sind nützlich um eine bekannte Anzahl an immer wiederkehrenden Anweisungen auszuführen. Klassisch hierfür dürfte das Auslesen der Werte eines Arrays mittels einer Schleife sein. Beispielsweise dürfte ohne Schleifen die Ausgabe von in einem Array enthaltenen Werten sehr mühsam für den Programmierer sein. Dies würde bei zehn Werten wie folgt aussehen:

// erzeugt ein Array mit zehn Integer-Werten
int[] einArray = { 1,2,3,4,5,6,7,8,9,10 };

System.out.println( einArray[0] );
System.out.println( einArray[1] );
System.out.println( einArray[2] );
System.out.println( einArray[3] );
System.out.println( einArray[4] );
System.out.println( einArray[5] );
System.out.println( einArray[6] );
System.out.println( einArray[7] );
System.out.println( einArray[8] );
System.out.println( einArray[9] );

Diese Aufgabe lässt sich mittels einer Schleife erheblich erleichtern:

// erzeugt ein Array mit zehn Integer-Werten
int[] einArray = new int[10];
// nun wird das Befüllen des Arrays ebenfalls durch eine Schleife realisiert
for(int i=0; i < einArray.length; i++){
        einArray[i] = i + 1;
}

// For-Schleife, die die im Array enthaltenen Werte ausgibt
for( int i=0; i < einArray.length; i++ ){
	System.out.println( einArray[i] );
}

Mittels .length erhält man die Länge des Arrays als int-Wert. Somit lässt sich immer die Länge des Arrays bestimmen, auch wenn man diese zum Zeitpunkt der Programmierung noch nicht kennt.

Ein Array kann also mittels einer for-Schleife durchlaufen werden, da man einen Startwert = 0 sowie einen Endwert = .length hat. Da man alle Werte aus dem Array auslesen möchte (und nicht nur bestimmte), wird die Zählervariable immer um eins erhöht.

Im Schleifenkörper kann nun jeder Arrayplatz mit der Zählervariable angesprochen werden: einArray[i].

Innerhalb des Schleifenkörpers kann jede Art von Anweisung durchgeführt werden. Muss man beispielsweise eine größere Anzahl an Parametern nacheinander an ein und dieselbe Methode übergeben, kann man dies mittels einer Schleife tun:

// wiederholter Methodenaufruf in einer Schleife
for(int i=0; i < einArray.length; i++){
	eineMethode( einArray[i] );

	einObjekt.eineMethode( einArray[i] );
}

Es ist natürlich ebenfalls möglich mehrmals Objekte des gleichen Typs zu erstellen.

// ein Array zur Aufnahme von Objekten erzeugen
Integer[] einArray = new Integer[100];

// Befüllen des Arrays mit 100 Objekten vom Typ Integer
for( int i=0; i < einArray.length; i++ ){
	einArray[i] = new Integer( i );
}

Abkürzung

Bearbeiten

Bei Arrays gibt es eine Kurzschreibweise, um über ein vorher belegtes Array zu iterieren:

int[] einArray  = {1, 2, 3};
for( int i : einArray ) {
      System.out.println(i);
}

Variationen

Bearbeiten

Bei der Implementierung des Schleifenkopfes ist man nicht strikt an die in obigem Beispiel vorgestellte Form gebunden. Vieles ist möglich, solange man sich an die Syntax hält. Die folgenden Beispiele sollen nur kleine Anregungen sein.

// Bsp.: Initialisierung der Zählvariable
for( byte i = -120; i < 100; i++ ){
	System.out.println( i );
}	

int i = 50;
for(  ; i < 100; i++ ){
	System.out.println( i );
}

// Bsp.: Implementierung der Startbedingung
// Bsp.: abwärts zählen
for( int i = 150; i >= 100; i-- ){
	System.out.println( i );
}

// Bsp.: größeres Intervall
for( int i = 1; i <= 100; i+=10 ){
	System.out.println( i );
}
	
for( int i = 1; i <= 100; i*=2 ){
	System.out.println( i );
}

// Endlosschleife
for( ; ; ){
	System.out.println( 1 );
}

Schleife mit Vorabprüfung (while)

Bearbeiten

Die while-Schleife führt den Anweisungsblock aus, solange die Prüfung true ergibt. Die Prüfung der Bedingung erfolgt dabei vor dem Betreten der Schleife. Beispiele

 while (true) {} // Endlosschleife
 int i = 0;
 while (i < 10) { i++; } // Ausführungsanzahl 10
 int i = 0;
 while (i < 0) {
   // wird nicht ausgeführt.
   System.out.println ("Come to me - PLEEEAAASE!");
 }

Schleife mit Nachprüfung (do)

Bearbeiten

Die do-Schleife führt alle beinhalteten Anweisungen solange aus, wie die Prüfung true ergibt. Die Prüfung der Bedingung erfolgt dabei nach dem ersten Schleifendurchlauf. Zu einer do Schleife wird stets das Schlüsselwort while zur Bedingungsangabe benötigt. Beispiele

 do {} while (true); // Endlosschleife
 int i = 0;
 do { i++; } while (i < 10); // Ausführungsanzahl 10
 int i = 0;
 do {
   System.out.println("Thanx to come to me.");
 } while (i < 0);

Schleifen verlassen und überspringen (break und continue)

Bearbeiten

Innerhalb einer do, while oder for-Schleife kann mit break die gesamte Schleife verlassen werden. Soll nicht die gesamte Schleife, sondern nur der aktuelle Schleifendurchlauf verlassen und mit dem nächsten Durchlauf begonnen werden, kann die Anweisung continue verwendet werden. Im folgenden Beispiel werden in einem Iterator über eine Personenkartei alle Personen gesucht, die zu einer Firma gehören. Dabei werden gesperrte Karteikarten übersprungen. Wird eine Person gefunden, die zu dieser Firma gehört und als Freiberufler tätig ist, wird die weitere Suche abgebrochen, da angenommen wird, dass ein Freiberufler der einzige Angehörige seiner Firma ist.

  String desiredCompany = "Wikimedia";
  Person[] persons;
  StringBuffer nameListOfWikimedia;
  
  for (int i=0;i<MAX_PERSONEN;i++) {
    if (persons[i].isLocked()) {
      continue; // Gesperrte Person einfach überspringen
    } 
    String company = persons[i].getCompany();
    if (company.equals(desiredCompany)) {
      nameListOfWikimedia.append(persons[i].getName());
      if (persons[i].isFreelancer()) {
        break; // Annahme: Ein Freelancer hat keine weiteren Mitarbeiter
      }
    }
  }

Diese Aussprünge ähneln den goto-Befehlen anderer Programmiersprachen, sind aber nur innerhalb der eigenen Schleifen erlaubt. Bei Schachtelung mehrerer Schleifen können diese auch mit Bezeichnern (Labels) versehen werden, so dass sich die break- oder continue-Anweisung genau auf eine Schleife beziehen kann:

  catalog: while(catalogList.hasNext()) {
    product: while (productList.hasNext()) {
      price: while (priceList.hasNext()) {
        [...]
        if (priceTooHigh) {
          continue product;
        }
      }
    }
  }

Übungen

Bearbeiten
  1. Schreibe ein einfaches Programm, welches für jeden Monat die Anzahl der Tage ausgibt. Nutze hierzu die einfache Verzweigung.
  2. Schreibe ein einfaches Programm, welches für jeden Monat die Anzahl der Tage ausgibt. Nutze hierzu die Mehrfachverzweigung.
  3. Schreibe eine einfache Applikation, welche mit Hilfe der Zählschleife die Übergabeparameter ausgibt. Sofern du das JDK 1.5 oder höher verwendest nutze hierfür die alte und neue Variante.
  4. Wie oft wird die folgende do-Schleife ausgeführt und warum?
 int i = 10;
 do {
   i -= 3;
 } while (i > 5);



Die Sprache Java gehört zu den objektorientierten Programmiersprachen. Die Grundidee der objektorientierten Programmierung ist die softwaretechnische Abbildung in einer Art und Weise, wie wir Menschen auch Dinge der realen Welt erfahren. Die Absicht dahinter ist, große Softwareprojekte einfacher verwalten zu können, und sowohl die Qualität von Software zu erhöhen als auch Fehler zu minimieren. Ein weiteres Ziel der Objektorientierung ist ein hoher Grad der Wiederverwendbarkeit von Softwaremodulen.

Allgemeines zum Thema Objektorientierung findet sich im dazugehörigen Wikipedia-Eintrag. Dieses Kapitel beschäftigt sich speziell mit der Umsetzung in Java.



Ab der frühen Kindheit wird uns beigebracht, hinter den Dingen ein Schema zu erkennen. Wir lernen, dass Bello ein Hund ist. Zu diesen Bildern ordnen wir Eigenschaften und Fähigkeiten zu ("Farbe ist schwarz" oder "kann bellen"). Diese Verallgemeinerung ist eine der Stärken der Menschen. Es wird wohl daran liegen, dass die meisten Menschen nicht mehr als sieben Dinge gleichzeitig berücksichtigen können. Wen wundert es, wenn wir versuchen dieses Vorgehen in unsere Programmiersprachen zu übertragen. Somit können wir Dinge aus der Wirklichkeit auf die Maschine übertragen, weil sie abbildbar sind.

Der Hund wird in Java als Klasse repräsentiert. Bello wird als Objekt/Exemplar der Klasse verstanden.


Wesentliche Ziele von objektorientierter Programmierung sind Übersichtlichkeit, einfache Modifizierbarkeit, Flexibilität und einfache Wartung des Programmcodes. Erreicht wird dies durch den Ansatz, Objekte als relativ eigenständig zu verstehen. So ist es beispielsweise für das Gassigehen unerheblich, welchen Hund wir nun konkret an die Leine nehmen: Das kann Bello sein, es kann aber auch der Hund der Nachbarin sein, die im Urlaub ist. Das Vorgehen ist beide Male das Gleiche, unabhängig davon welche Farbe der Hund hat oder welcher Rasse er angehört.
Um Gassigehen zu können, muss es sich um einen Hund handeln. Nur dieser Punkt interessiert. Ob der Hund womöglich auch noch prämierter Gewinner von Schönheitswettbewerben ist oder auf einem Skateboard fahren kann, ist unwichtig.

Das hört sich sehr banal an, hat aber weitreichende Konsequenzen. Es ist ein fundamentales Konzept von objektorientierter Programmierung nur diejenigen Eigenschaften eines Objektes zu betrachten, die einen auch wirklich interessieren.

Durch einen Übersetzungsfehler des Wortes „instance" aus dem Englischen wird immer wieder von einer Instanz gesprochen, wobei hier der Begriff „Instanz" falsch ist. Besser passende Übersetzungen für „instance" sind „Objekt", „Ausprägung", „Exemplar", oder auch „realisiertes Beispiel".



Aufbau einer Java-Klasse

Bearbeiten
public class Testklasse {
    private static final int KLASSENKONSTANTE = 42;
    private static int anzahl;

    public static void gib_etwas_aus() {
        System.out.println(KLASSENKONSTANTE + " Anzahl der übergebenen Argumente:" + anzahl);
    }

    public static void main(String[] args) {
        for(String i : args)
          System.out.println(i);
        anzahl = args.length;
        gib_etwas_aus();
    }
}

Jede mit "public" gekennzeichnete Klasse muss in einer eigenen Datei gespeichert werden, die Klassenname.java heißt. Schlüsselwörter wie "public" und "private" sind Zugriffsmodifizierer, "final" bezeichnet in diesem Kontext eine Konstante und wird ebenso wie "static" weiter unten erklärt. Die "Testklasse" enthält zwei Methoden: "gib_etwas_aus()" und "main(String[] args)". "klassenkonstante" und "anzahl" nennt man Klassenvariablen, man erkennt Klassenvariablen am Schlüsselwort "static".

class AndereKlasse {
    public int eineVariable;

    public AndereKlasse(int eingabe) {
        eineVariable = eingabe;
    }
}

public class Testklasse  {
    public static void main(String[] args) {
        AndereKlasse a = new AndereKlasse(42);
        System.out.println(a.eineVariable);
    }
}

Dieses Beispiel enthält zwei verschiedene Klassen. Die Variable "eineVariable" in der Klasse "AndereKlasse" ist eine Instanz-Variable, hingegen ist "a" aus der Methode "main()" eine lokale Variable. In der Variablen "a" ist das Objekt AndereKlasse gespeichert. Klasse und Objekt sind also zwei verschiedene Dinge.

Zugriffsmodifizierer

Bearbeiten

In der Objektorientierung kennt man so genannte Zugriffsmodifizierer (engl. access modifier), die die Rechte anderer Objekte einschränken (Kapselung) oder die ein bestimmtes Verhalten von einem Unterobjekt verlangen (Abstraktion/Vererbung).

Manchmal hört man auch die Bezeichnung Sichtbarkeitsmodifizierer (engl. visibility modifier). Diese Bezeichnung ist aber eigentlich falsch, weil die Zugriffsmodifizierer den Zugriff auf einen Member verbieten, der Member als solches bleibt jedoch sichtbar, z.B. über Reflection. Dennoch sollte man diese Bezeichnung verstehen, da sie unter Java-Programmierern weit verbreitet ist.

Java kennt folgende Zugriffsmodifizierer:

Die Klasse selbst,
innere Klassen
Klassen im
selben Package
Unterklassen Sonstige
Klassen
private Ja Nein Nein Nein
(default) Ja Ja Nein Nein
protected Ja Ja Ja Nein
public Ja Ja Ja Ja


private ist der restriktivste Zugriffsmodifizierer. Er verbietet jeglichen Zugriff von außerhalb der Klasse auf den entsprechend modifizierten Member. Auf eine private Variable kann nur die Klasse selbst zugreifen, ebenso auf einen privaten Konstruktor, eine private Methode oder einen privaten geschachtelten Datentyp.

Klassenvariablen werden üblicherweise als private deklariert. Das Verändern der Variablen wird über passende Methoden, meist Getter und Setter ermöglicht. Dieses Prinzip nennt man Geheimnisprinzip.

/** Mutable 2D-Punkt-Klasse. */
public class Punkt2D {
    private int x, y; // private Variablen
    public Punkt2D(final int x, final int y) {
        setX(x);
        setY(y);
    }
    public int getX() { return x; }
    public int getY() { return y; }
    public void setX(final int x) { this.x = x; }
    public void setY(final int y) { this.y = y; }
}

Allgemein werden sämtliche Member als private deklariert, die ein Implementierungsdetail darstellen, auf das sich keine andere Klasse verlassen bzw. das von keiner anderen Klasse verwendet werden darf.

(default)

Bearbeiten

Als "default" oder "package private" bezeichnet man die Zugreifbarkeit für den Fall, dass kein Zugriffsmodifizierer angegeben wurde. Auf einen package private Member können nur Klassen zugreifen, die sich im selben Paket wie die Klasse des Members befinden.

Der "default" Zugiffsmodifizierer wird dann eingesetzt, wenn eine Klasse auf die Daten einer anderen Klasse (innerhalb des selben Packages) Zugriff haben soll, aber diese Funktionalitäten nach außen nicht verfügbar gemacht werden sollen.

Dieser Zugriffsmodifizierer wird insbesondere bei der Entwicklung von API eingesetzt.


protected

Bearbeiten

Mit dem Zugriffsmodifizierer protected ist der Zugriff nicht nur Klassen aus dem selben Package (wie "default"), sondern auch Subklassen der Klasse erlaubt. Dies gilt auch, wenn die betreffenden Subklassen aus einem anderen Package sind als die Klasse des betreffenden Members.

Der Zugriffsmodifizierer protected wird verwendet, wenn es nur für Subklassen Sinn ergibt, den betreffenden Member zu verwenden.

Diese Zugriffsmodifizierer findet in der API-Programmierung Einsatz. Auch Muster wie das Schablonenmuster (engl. template pattern) nutzen diesen Mechanismus.


Der Zugriffsmodifizierer public gestattet sämtlichen Klassen Zugriff auf den betreffenden Member. Er ist der freizügigste Zugriffsmodifizierer.

Die Zugreifbarkeit public findet man hauptsächlich bei Methoden, die von anderen Klassen verwendet werden sollen z.B. bei Konstruktoren.


Welcher Zugriffsmodifizierer?

Bearbeiten

Welchen Zugriffsmodifizierer soll man nun verwenden? Im einfachsten Fall verwendet man private für Variablen sowie public für Methoden, Konstruktoren und Datentypen. Eine Regel, die sich in der Praxis sehr bewährt hat, lautet "so streng wie möglich, so freizügig wie nötig".

Polymorphie-Modifizierer

Bearbeiten

Polymorphie-Modifizierer sind dazu da, ein bestimmtes Verhalten einer Unterklasse zu erzwingen, bzw. dieses zu erleichtern. Das heißt, dass wenn eine Klasse von einer abstrakten Klasse abgeleitet werden soll, die Unterklasse diese Attribute bzw. Methoden implementieren muss, wenn es eine konkrete Klasse werden soll. Doch nun zu den einzelnen Modifizierern selbst:

abstract

Bearbeiten

Es können Methoden und Attribute als abstract bezeichnet (deklariert) werden, was bedeutet, dass entweder die Unterklasse diese implementieren muss oder aber die abgeleitete Klasse ebenfalls als abstrakt deklariert werden muss.

Von einer abstrakten Klasse können keine Instanzen gebildet werden, so dass diese immer erst implementiert werden muss, um das gewünschte Ergebnis zu erreichen. Ein Beispiel:

public abstract class Berechne {
    public abstract int berechne(int a, int b);
}

Dies ist eine abstrakte Klasse mit einer Methode. Was genau berechnet werden soll, steht hier aber nicht - deswegen heißt diese auch "abstrakt". Jetzt erweitern wir diese Klasse um eine Addition zu erhalten:

public class Addiere extends Berechne {
    public int berechne(int a, int b) {
        int c = a + b;
         return c;
     }
}

Wie man an diesem konkreten Beispiel sieht, wird erst in der "konkreten" Klasse der Algorithmus implementiert.

Es können Klassen, Methoden, Attribute und Parameter als final bezeichnet (deklariert) werden. Einfach ausgedrückt bedeutet final in Java "du kannst mich jetzt nicht überschreiben".

Für finale Klassen bedeutet dies, dass man von ihr nicht erben kann (man kann keine Unterklasse erzeugen). Sie kann also nicht als Vorlage für eine neue Klasse dienen. Grundlegende Klassen, wie zum Beispiel die String-Klasse sind final. Wenn sie es nicht wäre, dann könnte man von ihr erben und ihre Methoden überschreiben und damit das Verhalten der erweiterten Klasse verändern.

Finale Methoden können in Subklassen nicht überschrieben werden.

Finale Attribute und auch Klassen-Variablen können nur ein einziges Mal zugewiesen werden. Sobald die Zuweisung erfolgt ist, kann eine finale Variable ihren Wert nicht mehr ändern. Bei Member-Variablen muss die Zuweisung bei der Instanzierung, bei Klassen-Variablen beim Laden der Klasse erfolgen.

Finale Parameter können ausschliesslich den beim Methodenaufruf übergebenen Wert besitzen. In der Methode selbst lassen sie sich nicht überschreiben.

Ein Beispiel:

 public int getSumme (final int summand1, final int summand2) {
    return summand1 + summand2;
 }

Der Compiler hat die Möglichkeit, finale Member-Variablen, denen Konstanten zugewiesen werden, direkt im kompilierten Code zu ersetzen.

Ein Beispiel:

 public class AntwortAufAlleFragenDesUniversums {
     private final long antwort = 23;
     public long gibAntwort() {
         return antwort;
     }
 }

Der Compiler macht daraus:

 public class AntwortAufAlleFragenDesUniversums {
     public long gibAntwort() {
         return 23;
     }
 }


Wie Sie sehen, hat der Compiler hier einfach die finale Variable durch den Wert ersetzt. Diese Optimierung ist aber nur möglich, da der Wert konstant ist und direkt zugewiesen wird.

In folgendem Beispiel kann der Compiler nicht optimieren:

 public class WievielMillisSindVerstrichen {
     private final long antwortInstanz = System.currentTimeMillis();
     private final static long antwortKlasse = System.currentTimeMillis();
 
     public long gibAntwortInstanz() {
         return antwortInstanz;
     }
 
     public long gibAntwortKlasse() {
         return antwortKlasse;
     }
 }

Der Wert der Variable antwortKlasse wird beim Laden der Klasse zugewiesen. Die Klasse wird dann geladen, wenn sie das erste Mal zur Laufzeit benötigt wird. Der Wert der Variable antwortInstanz wird beim Instanzieren der Klasse, genauer gesagt bei der Initialisierung des Objekts auf Ebene der Klasse WievielMillisSindVerstrichen, also mit new WievielMillisSindVerstrichen() zugewiesen.

Noch eine Warnung beim Verwenden von Konstanten in Java. In Java deklariert man Konstanten wie folgt:

 public class Konstante {
     public final static int ICH_BIN_EINE_KONSTANTE = 42;
 }

 public class Beispiel {
     public static void main(String[] args) {
         System.out.println(Konstante.ICH_BIN_EINE_KONSTANTE);
     }
 }
javac Konstante.java Beispiel.java

Der Java Compiler ersetzt dann überall die Variable durch dessen Konstante. Dadurch gelangt die Konstante (42) und nicht die Variable (ICH_BIN_EINE_KONSTANTE) in das Kompilat (Konstante.class und Beispiel.class).

java Beispiel

gibt wie erwartet die Zahl 42 aus.

Wenn ich jetzt den Wert der Konstante von 42 auf 99 ändere und nur die Klasse Konstante compiliere, wird es gefährlich:

 public class Konstante {
     public final static int ICH_BIN_EINE_KONSTANTE = 99;
 }
javac Konstante.java
java Beispiel

gibt immer noch die Zahl 42 aus, obwohl ich doch den Wert der Variable ICH_BIN_EINE_KONSTANTE geändert habe. Warum? Als das Kompilat der Klasse Beispiel (Beispiel.class) erzeugt wurde, war der Wert von ICH_BIN_EINE_KONSTANTE noch 42. Der Compiler hat zu dem Zeitpunkt den Wert 42 in die Klasse Beispiel eingesetzt. Weil die Klasse Beispiel nicht nochmals kompiliert wurde steht im Kompilat immer noch den Wert 42 und der wird dann auch ausgegeben.

javac Beispiel.java
java Beispiel

hilft und gibt dann auch 99 aus.

Konsequenzen: Bei einer Änderung von public/protected final static sollte immer der gesamte Code neu kompiliert werden. Wenn das nicht möglich ist (z.B. in einer Klasse, die von verschiedenen anderen Projekten benutzt wird), sollte man auf die Verwendung von Konstanten verzichten und stattdessen einfach eine Methode verwenden:

 public class Konstante {
     private final static int ICH_BIN_EINE_KONSTANTE = 99; // ungefährlich, da nur innerhalb der Klasse der Zugriff möglich ist
 
     public static getDieKonstante() {
         return ICH_BIN_EINE_KONSTANTE;
     }
 }


Ein letztes Beispiel soll aufzeigen, dass mit final lediglich die Zuweisung (das Überschreiben), nicht aber der Gebrauch geschützt wird.

 import java.util.List;
 import java.util.LinkedList;
 
 public class NamensListe {
     private final List interneNamensliste = new LinkedList();
 
     public void neuerName(String name) {
         interneNamensliste.add(name);
     }
 
     public int anzahlNamen() {
         return interneNamensliste.size();
     }
 
     public List gibListe() {
         return interneNamensliste;
     }
 }

Wie man hier schön sieht, kann man auf finalen Member-Variablen, hier die internenNamensliste, Methoden ausführen (wie das Hinzufügen mit interneNamensliste.add(name)). Da die Member-Variable interneNamensliste final ist, kann man ihr keinen anderen Wert zuweisen. Eine zusätzliche Methode

 public void entferneListe() {
     interneNamensliste = null;
 }

ist nicht zulässig und würde beim Kompilieren mit einer Fehlermeldung enden. Richtig riskant ist die Methode gibListe(). Indem die interneNamensliste der Außenwelt (außerhalb der Klasse) verfügbar gemacht wird, kann man von der Außenwelt auch alle Methoden der Liste aufrufen. Damit ist die Liste außerhalb des Einflussbereiches der Klasse. Da hilft es auch nicht, dass die Member-Variable interneNamensliste final ist.

Es können Methoden und Klassenvariablen als static bezeichnet (deklariert) werden.

Statische Methoden und Variablen benötigen keinerlei Instanzen einer Klasse, um aufgerufen zu werden. Ein Beispiel für einen statischen Member ist z.B. die Konstante PI in der Klasse java.lang.Math.

Auch die Methoden dieser Klasse können einfach aufgerufen werden, ohne vorher eine Instanz dieser Klasse anzulegen, z.B. java.lang.Math.max(3,4).

Sofern eine statische Klassenvariable erst zur Laufzeit dynamisch einen Wert erhalten soll, können Sie dies mit einem statischen Block erreichen.

Beispiel:

 public class LoadTimer {
   
   static {
     ladeZeit = System.currentTimeInMillis ();
   }
   private static long ladeZeit;
 }

Es ist dennoch möglich, statische Methoden oder Attribute über ein Objekt aufzurufen, davon wird aber dringend abgeraten. Denn dies führt zu unangenehmen Ergebnissen.

Beispiel:

 public class SuperClass {
  public static void printMessage(){
   System.out.println("Superclass: printMessage");
  }
 }

 public class SubClass extends SuperClass {
  public static void printMessage(){
   System.out.println("Subclass: printMessage");
  }
 }

 public class StrangeEffect {
  public static void main(String[] args){
   SubClass object = new SubClass();
   object.printMessage();
   
   SuperClass castedObject = (SuperClass)object;
   castedObject.printMessage();
  }
 }

Die Ausgabe ist:

Subclass: printMessage
Superclass: printMessage

Erstaunlich ist die zweite Zeile: Obwohl unser object vom Typ SubClass ist, wird die Methode von SuperClass aufgerufen. Offensichtlich funktioniert hier das Überschatten nicht. Das liegt daran, dass statische Methodenaufrufe nicht vom Laufzeittyp abhängen! Konkret bedeutet dies, dass die Entscheidung welche statische Methode nun aufgerufen werden soll, unabhängig davon getroffen wird, welcher Klasse das Exemplar angehört.

Um diesen irreführenden Effekt zu vermeiden, sollte man statische Methoden immer auf Klassen aufrufen und nicht auf Objekten.

strictfp

Bearbeiten

strictfp kennzeichnet Klassen und Methoden, deren enthaltene Fließkommaoperationen streng auf eine Genauigkeit von 32 bzw. 64 Bit beschränkt sind. Dadurch wird sichergestellt, dass die JVM darauf verzichtet, Fließkommaoperationen intern mit einer höheren Genauigkeit zu berechnen (beispielsweise 40 bzw. 80 Bit) und nach Abschluss der Berechnung wieder auf 32 bzw. 64 Bit zu kürzen. Dies ist wichtig, da es sonst bei verschiedenen JVMs oder verschiedener Hardware zu unterschiedlichen Ergebnissen kommen kann und somit das Programm nicht mehr plattformunabhängig ist.

Ausdrücke, die zur Kompilierzeit konstant sind, sind immer FP-strict.

native kann nur vor Methoden stehen und bedeutet, dass die Implementierung der betreffenden Methode nicht in Java, sondern einer anderen Programmiersprache geschrieben wurde, und von der virtuellen Maschine über eine Laufzeitbibliothek gelinkt werden muss. Die Syntax der Methode entspricht dann einer abstrakten Methode. Ein Beispiel:

 public native void macheEsNichtInJava ();

Um eine solche Methode zu verwenden muss sie nach dem Compilieren über einen "Java-Pre-Compiler" (javah) gejagt werden, d.h. dieser generiert aus einer "native" - Methode ein entsprechenden C-Header und Rumpf, der dann mit Leben gefüllt werden kann. Diese Rümpfe werden als dll unter Windows bzw. lib unter Linux/Unix compiliert. Diese compilierten Libs müssen dann aber auch zur Laufzeit des Programms zugreifbar sein, andernfalls erhält der Nutzer eine Exception.



Klassen, Objekte, Instanzen

Bearbeiten

Eine Klasse beschreibt die (allgemeine) Definition. Alles mit Ausnahme der primitiven Datentypen (int, boolean, long etc.) in Java ist abgeleitet von "java.lang.Object". Das heißt, dass jede Java-Klasse, die Sie schreiben, bestimmte Methoden bereits von "Object" geerbt hat.

Ein Objekt – auch Instanz genannt – ist ein bestimmtes Exemplar einer Klasse und wird zur Laufzeit des Programms erzeugt.

Konstruktoren

Bearbeiten

Um eine Instanz einer Klasse zu erschaffen, wird der Konstruktor benutzt. Der Konstruktor ist namentlich wie die Klasse zu benennen. Die Syntax entspricht hierbei einer Methode jedoch gibt der Konstruktor keinen Wert, das heißt auch kein void, zurück.

 package org.wikibooks.de.javakurs.oo;
 public class MeineKlasse 
 {
   // Nun der Konstruktor
   public MeineKlasse ()
   {
   ...
   }
 }

Destruktoren

Bearbeiten

Destruktoren gibt es in Java nicht. Es besteht eine gewisse Wahrscheinlichkeit dafür, dass die finalize Methode einer Instanz vor dessen Zerstörung durch die Garbage Collection aufgerufen wird. Dies ist jedoch nicht sichergestellt.

Methoden in "java.lang.Object"

Bearbeiten

Hier sind nun einmal die wichtigsten Methoden, die man in allen Java-Objekten finden und aufrufen kann, aufgeführt.

toString()

Bearbeiten

Diese Methode ist während der Entwicklung eines Programmes von großem Nutzen. Jede Klasse, welche toString() überschreibt, kann hier den relevanten Inhalt der Klasse in Textform ausgeben. Mit System.out.println(object); kann man nun den Inhalt eines Objektes ansehen.

equals(Object)

Bearbeiten

Java unterscheidet zwei Arten von Gleichheit:

Identisch: Zwei Objekte sind identisch, wenn sie beide das selbe Objekt (exakt am selben Ort im Speicher) referenzieren, was mittels "==" getestet wird. In der Objektorientierung heißt das auch, dass diese eine gemeinsame Instanz haben. Stellen Sie sich das einfach so vor, als wenn Hausnummern in einer Stadt verglichen werden: Die Hausnummern sind zwar gleich, aber es können verschiedene Leute im Haus wohnen.

Beispiel:

 Object obj1 = new Object();
 Object obj2 = obj1;
 if (obj1==obj2)
 {
     // beide Objekte obj1 und obj2 sind identisch, da sie beide am selben Ort im Speicher liegen.
 }

Gleich resp. equal: Zwei Objekte sind gleich, wenn sie denselben semantischen Inhalt repräsentieren und das wird mittels equals(..) getestet. Um beim Hausbeispiel zu bleiben: Hier vergleichen wir die Leute, die in einem Haus wohnen, mit der Liste von Leuten, die in diesem Haus wohnen sollen.

Beispiel:

 String name1 = new String("Peter Muster");
 String name2 = new String("Peter Muster");
 if (name1.equals(name2)) 
 {
      // beide Objekte repräsentieren denselben Inhalt, sie sind aber nicht am selben Ort gespeichert
      // name1 == name2 ergibt deshalb false, name1 und name2 sind gleich, aber nicht identisch.
 }

Ein Beispiel aus der realen Welt: Zwillinge sind gleich, aber nicht identisch. Weshalb: wenn man dem einen Zwilling z.B. die Haare färbe, dann behält der andere Zwilling seine Haarfarbe. Dasselbe gilt für Objekte:

Wenn zwei Objekte gleich, nicht aber identisch sind, dann kann ich im ersten Objekt etwas ändern, ohne dass sich dadurch das zweite Objekt ändert.

Wenn zwei Objekte identisch sind und ich ändere im ersten Objekt etwas, dann ist diese Änderung auch beim zweiten Objekt sofort vorhanden (beide Objekte sind eben identisch).

Wenn Objekte identisch sind, dann sind sie immer auch gleich (zumindest sollte es so sein, sofern man equals korrekt implementiert hat).

Benutzung der Methode equals: Hier muss man sicherstellen, dass das Objekt, auf welchem die Methode equals aufgerufen werden soll, nicht null ist. Typischweise sieht man dann sowas:

 if (name1!=null && name1.equals(name2)) {...}

Eigene Implementierung Wenn Sie eine eigene Klasse anlegen sollten Sie stets diese Methode implementieren.

hashCode()

Bearbeiten

Die Methode hashCode() liefert einen int als Rückgabewert und wird überall dort verwendet wo direkt oder indirekt mit Hashtables oder Hash-Sets gearbeitet wird. Zur Definition einer Hashtable siehe weiter unten. Wichtig: wenn zwei Objekte gleich (equals) sind, dann sollten sie unbedingt denselben hashCode zurückliefern. Umgekehrt ist es aber erlaubt, dass, wenn zwei Objekte nicht gleich sind, ihre beiden hashCodes gleiche Werte zurückliefern. Deshalb sollte man, wenn man equals() überschreibt auch gerade hashCode() überschreiben.

Und was geschieht, wenn man equals überschreibt, aber den hashCode nicht? Solange man die Objekte nicht in einer Hashtable/HashMap oder einem HashSet unterbringt hat man keine Probleme. Das ändert sich aber, sobald man die Objekte in Hashtables/HashMap oder HashSets unterbringen will. Um zu verstehen, was dabei "schiefgeht", muss man verstehen, wie Hashtables funktionieren.

Zur Definition einer Hashtable: Es ist eine Datenstruktur, welche erlaubt, sehr schnell Objekte anhand ihres Schlüssels abzulegen und anhand dieses Schlüssels wieder aufzufinden. Einfach ausgedrückt ist eine Hashtable ein Array von Schlüssel-Objekt-Paaren einer bestimmten Größe. Der Hash-Wert des Schlüssels modulo Größe der Hashtable dient dabei als Index, wo der Schlüssel selbst und das dazugehörige Objekt abgelegt werden. Falls nun mehrere Schlüssel an denselben Platz in der Hashtable platziert werden, so wird (in Java, nicht zwingend aber in anderen Programmiersprachen) einfach eine Liste benutzt, wo alle Schlüssel-Werte-Paare der entsprechenden Hashtable-Position gespeichert werden. Um Objekte aus der Hashtable herauszuholen, wird fast dasselbe gemacht wie beim Einfügen. Es wird die Position in der Hashtable berechnet (hash-Code des Schlüssels modulo Hashtable-Größe) und dann alle dort gefunden Objekte mittels der equals-Methode verglichen. Sobald die equals-Methode erfolgreich war, so hat man das richtige Objekte gefunden. Das Ganze ist dann besonders effizient, wenn die Hashtable klein ist und alle Schlüssel genau einen eigenen Index rsp. Hashtable-Position haben. Als Faustregel gilt: Ist eine Hashtable zu 50% gefüllt, sinkt die Effizienz bei weiterem Einfügen schnell. Zudem sollten die hashCode-Werte möglichst gut verteilt sein. Am schlimmsten wäre es, wenn alle Objekte derselben Klasse als hashCode denselben Wert zurückliefern. Die Größe einer Hashtable kann man beim Erzeugen definieren. Zusätzlich kann man ihr einen Füllgrad angeben, ab wann sie sich vergrößern soll. Die Vergrößerung einer Hashtable ist eine sehr teure Operation, die man indirekt durch Hinzufügen eines neuen Schlüssel-Werte-Paares auslöst. Dabei werden alle Schlüssel-Werte-Paare in einer neuen, größeren Hashtable abgelegt. Das geschieht zwar automatisch, ist aber zeitaufwändig.

Was geschieht nun, wenn man in einer Klasse K equals überschreibt, nicht aber hashCode? Dann wird der hashCode der Oberklasse benutzt (was bei der Objekt-Klasse mehr oder weniger der Speicheradresse gleichkommt). Wenn nun als Schlüssel Instanzen der Klasse K benutzt werden, dann werden alle Instanzen mit großer Wahrscheinlichkeit gleichmäßig in der Hashtable abgelegt (das wäre ja noch gut). Beim Auffinden eines Objektes anhand eines Schlüssels wird der hashCode des Schlüssels berechnet, um die Position in der Hashtable herauszufinden. Wenn nun zwei Schlüssel gleich sind, aber unterschiedliche hashCodes haben, dann wird jedes Schlüssel-Wert-Paar an eine andere Position abgelegt. Die Folge: man findet Objekte mit großer Wahrscheinlichkeit nicht mehr, da man am "falschen" Ort sucht.

Beispiel, was falsch läuft:

 import java.util.*;
 
 public class K extends Object {
   private String content;
  
   public K(String content) {
      this.content = content;
   }
  
   public boolean equals(Object obj) {
      if (this==obj) {
         return true;
      }
      if (obj==null) {
         return false;
      }
      if (!(obj instanceof K )) {
         return false; // different class
      }
      K other = (K) obj;
      if (this.content==other.content) {
         return true;
      }
      if (this.content==null && other.content!=null) {
         return false;
      }
      // this.content can't be null
      return this.content.equals(other.content);
   }
 
   public static void main(String[] args) {
      K k1 = new K("k"); // let's say has hashCode 13 
      K k2 = new K("k"); // let's say has hashCode 19
 
      Map map = new HashMap();
      map.put(k1, "this is k1");
 
      String str = (String) map.get(k2);
      // str will be null because of different hashCode's
      System.out.println(str);
      // next line will print "true" because k1 and k2 are the same, so they should also return the same hashCode
      System.out.println(k1.equals(k2));
   }
 }

Was fehlt, ist der hashCode():

 public class K extends Object {
 ...
 
  public int hashCode() {
     if (this.content==null) {
        return 0;
     }
     return this.content.hashCode();
  }
 }

wait(), notify() und notifyAll()

Bearbeiten

Die sogenannte Vererbung ermöglicht es Informationen (Variablen) und Verhalten (Methoden / Operationen) weiterzugeben. Dies ist eine wesentliche Möglichkeit um Redundanz zu vermeiden. Die Erben fügen dann weitere Informationen und/oder Verhalten hinzu. Zwei Klassen stehen dabei zueinander als Superklasse (Erblasser) und Subklasse (Erbe) in Beziehung.

Vererbung ist ein zentrales Thema in der objektorientierten Programmierung (OOP) - siehe auch w:Objektorientierte Programmierung.

Erzeugen und Zerstören von Subklassen

Bearbeiten

Das Erzeugen und Zerstören von Subklassen erfolgt analog zu einer normalen Klasse. Intern werden jedoch auch die Standardkonstruktoren der jeweiligen Superklasse beim Erzeugen und analog die finalize Methode beim Zerstören aufgerufen. Dies ist wichtig, da wir die Informationen und das Verhalten der Superklasse verwenden. Evtl. Grundstellen der Informationen oder Aufräumarbeiten beim Zerstören unserer Instanz sind somit auch bei den ererbten Informationen notwendig.

Überschreiben von Methoden

Bearbeiten

In der Subklasse können Methoden der Basisklasse "überschrieben" werden. Damit wird der Inhalt der ursprünglichen Methode verändert.

 package org.wikibooks.de.javakurs.oo;
 public class MeineKlasse 
 {
   // Nun der Konstruktor
   public MeineKlasse () 
   {
   //...
   }
   //ursprüngliche Methode
   public methode()
   {
      System.out.println("Wir sind in der Basisklasse");
   }
 }
 
 public class MeineSubklasse extends MeineKlasse 
 {
   public methode()
   {
      System.out.println("Wir sind in der Subklasse");
   }
 }

Wird die methode()-Methode für ein Objekt des Typs MeineSubklasse aufgerufen, wird der Satz "Wir sind in der Subklasse" ausgegeben.

Überschriebene Methoden werden für die Anwendung der Laufzeit-Polymorphie in Java benötigt. Dabei werden Methoden erst zur Laufzeit (im Gegensatz zur Compile-Zeit) den Objekten zugeordnet.

Anmerkung: Im Deutschen wird das Verb "überschreiben" benutzt. Im Englischen heißt es jedoch "override", nicht "overwrite".

Mit dem JDK 1.6 / Java 6 sollte die zudem eine Annotation (Anmerkung) angebracht werden:

 ... 
 public class MeineSubklasse extends MeineKlasse 
 {
   @Override
   public methode()
   {
      System.out.println("Wir sind in der Subklasse");
   }
 }
 ...

Javaspezifische Implementierung

Bearbeiten

In Java sind alle Objekte von der Klasse Object abgeleitet. Object ist somit die Basisklasse von allen anderen Klassen. Alle Klassen sind Subklassen von Object.

Man leitet eine eigene Klasse von der Basisklasse durch extends ab:

  public class Beispiel extends Object
  {
    //...
  }

Als static-deklarierte Elemente gehören keinem Objekt an, sondern der Klasse (oder Schnittstelle), in der sie definiert sind. Das berühmteste Beispiel ist hier

 public static void main(String[] args)
 {
   //...
 }

Die main-Methode muss als static deklariert sein, da vor ihrem Aufrufen ja kein Objekt instanziiert sein kann.

Als static können sowohl Variablen als auch Methoden deklariert werden.

Da static-Methoden von allen Objekten unabhängig sind, haben sie Einschränkungen:

  • Es können aus ihnen heraus nur andere static-Methoden aufgerufen werden.
  • Sie können auch nur auf static-Variablen zugreifen.
  • static-Methoden und -Variablen können nicht mit this oder super angesprochen werden.

abstract

Bearbeiten

Als abstract-deklarierte Methoden werden in der Basisklasse nur deklariert. Die Definition findet dann in den Subklassen statt.

 abstract class BeispielKlasse 
 {
    abstract void schreibIrgendwas();
 }
 
 class BeispielSubklasse extends BeispielKlasse 
 {
    void schreibIrgendwas()
    {
       System.out.println("Irgendwas");
    }
 }

In der Basisklasse wird nur angegeben, dass die Methode existiert, aber nicht wie sie implementiert ist. Damit wird also zugesichert, dass jedes Objekt des Typs BeispielKlasse - und damit auch jedes Objekt einer davon abgeleiteten Klasse - die Methode schreibIrgendwas() besitzen muss.

In BeispielSubklasse muss die abstrakte Methode daher implementiert werden, ansonsten gibt es einen Compilerfehler. Wird eine abstrakte Methode in einer Unterklasse nicht implementiert, so muss diese Klasse selbst auch wiederum als abstract gekennzeichnet werden.

Generell muss jede Klasse, die mindestens eine abstrakte Methode enthält, auch selbst als abstrakt deklariert werden. Dies hat den Effekt, dass keine Objekte direkt von dieser Klasse erstellt werden können. Im obigen Beispiel wäre es also nicht zulässig, ein Objekt der Klasse BeispielKlasse mit

 new BeispielKlasse();

zu erzeugen.

Durch die Benutzung von final kann das Ableiten einer Klasse oder Überschreiben einer Methode verhindert werden.

 final class BeispielKlasse 
 {
    void schreibIrgendwas()
    {
      //...
    }
 }
 
 class BeispielSubklasse extends BeispielKlasse 
 {
   //...
 }

Das funktioniert nicht. Durch das final-Schlüsselwort kann von der Klasse Beispielklasse nicht abgeleitet werden.

 class BeispielKlasse 
 {
    final void schreibIrgendwas()
    {
      //...
    }
 }
 
 class BeispielSubklasse extends BeispielKlasse 
 {
    void schreibIrgendwas(){
      //...
    }
 }

Hier wird zwar die Subklasse erstellt, jedoch kann die Methode schreibIrgendwas() nicht überschrieben werden.

Um den Konstruktor der Basisklasse aufzurufen, wird die Methode super() verwendet. Sie wird im Konstruktor der Subklasse verwendet. super() wird benutzt, um private-Elemente der Basisklasse anzusprechen.

 class Beispiel extends Object
 {
    protected variable;
 
    Beispiel()
    {
       super();
       variable = 10;
    }  
 }

super() ruft hier den Konstruktor von Beispiel auf, und könnte somit private-Elemente manipulieren.

Als zweite Anwendungsmöglichkeit kann super im Zusammenhang mit einem Element der Basisklasse benutzt werden.

 super.methodeABC();
 super.variableXYZ;

Durch diese Aufrufe werden aus der Subklasse heraus die Methoden/ Variablen der Basisklasse aufgerufen.

Einfluss von Modifizierern auf die Vererbung

Bearbeiten

public und protected

Bearbeiten

Sowohl public- wie auch protected-Elemente werden an die abgeleitete Klasse vererbt. An ihrem public bzw. protected-Status ändert sich nichts.

 public methode();
 protected variable;

Private-Elemente werden nicht vererbt.

 private variable;

packagevisible

Bearbeiten

Dies ist der voreingestellte Modifizierer, der angewandet wird, wenn kein anderer Modifizierer angegeben wird. Durch ihn können nur Objekte aus dem gleichen Paket auf die Elemente zugreifen.

 class Beispielsklasse 
 {
   //...
 }

Er wird ohne Einschränkungen an die Subklasse vererbt.



Was ist ein Interface?

Bearbeiten

Interfaces als Java-Typ beschreiben eine öffentliche Schnittstelle der implementierenden Klassen. Interfaces sind hierbei eine Möglichkeit die Probleme der Mehrfachvererbung (in Java sowieso nicht möglich) elegant zu umgehen.

Schlüsselwort für die Deklaration eines Interface ist interface, welches anstelle der class Deklaration tritt. Ein Interface selbst kann nicht instanziert werden.

package de.wikibooks.org.oo;
public interface MyInterface {}

Deklaratorische Interfaces

Bearbeiten

Ein deklaratorisches Interface deklariert keine weiteren Methoden. Es stellt somit lediglich sicher, dass alle implementierenden Klassen vom Typ des Interface sind.

Ein Beispiel für ein Deklaratorisches Interface ist unser MyInterface.

package de.wikibooks.org.oo;
public class IchBinVomTypMyInterface implements MyInterface {
  
  public static void main (final String [] args) {
    final IchBinVomTypMyInterface instance = new IchBinVomTypMyInterface ();
    System.out.println ('''instance instanceof MyInterface''');
  }
 
}

"Normale" Interfaces

Bearbeiten

Normalerweise deklarieren Interfaces Methoden, die durch die implementierenden Klassen definiert werden. Die deklarierten Methoden sind dabei immer von der Sichtbarkeit public, auch wenn dies nicht explizit erwähnt wird.

package de.wikibooks.org.oo;
public interface NormalInterface {
  void go ();
}

Interface als Konstantensammlung

Bearbeiten

Interfaces werden auch gern zur Sammlung von Konstanten verwendet. Ein schönes Beispiel hierfür ist in javax.swing.WindowConstants zu finden. Jedoch wird davon abgeraten, Interfaces als Konstantensammlungen zu benutzen. Denn eigentlich definieren Interface neue Typen. Sun Microsystems hat dieses Problem nun adressiert und bietet mittlerweile einen eigenen Ausdruck für den Import von Konstanten aus anderen Klassen an: import static. Siehe dazu die Schlüsselwortreferenz für import.

Interfaces benutzen

Bearbeiten

Um ein Interface zu benutzen, muss dieses mit implements hinter dem Klassennamen spezifiziert werden. Anschließend müssen alle deklarierten Methoden des Interfaces implementiert (oder die Klasse als abstract deklariert) werden.

Es können auch durchaus mehrere Interfaces implementiert werden. Diese werden dann durch Kommata getrennt: ... implements Interface1, Interface2, Interface3 {

package de.wikibooks.org.oo;
public class Walker implements NormalInterface {
  public void go () {}
}



Das Paket java.lang enthält die elementaren Grundtypen von Java. Es wird per Default immer bereitgestellt, so dass es nicht notwendig ist einen import dieses Pakets vorzunehmen.


Object - die Mutter aller Klassen

Bearbeiten

Die Klasse Object ist die Wurzel im Vererbungsgraphen von Java. Jede Klasse, die nicht explizit von einer anderen Klasse erbt, erbt automatisch von Object. Somit ist Object die einzige Javaklasse ohne Vorfahren ("parent"). In Object sind nur eine handvoll Methoden versammelt, die aber wichtig für das gesamte Java-Laufzeitsystem sind. Diese Methoden werden von den abgeleiteten Klassen überschrieben und jeweils angepasst. Die Methoden kann man verschiedenen Zwecken zuordnen:


Verwendungszweck Methodennamen
Erzeugung- und Initialisierung Object() - Defaultkonstruktor
Objektbeseitigung finalize() - wird vom Garbage Collector aufgerufen
inhaltlicher Vergleich zweier Objekte equals()
vollständige Kopie clone() - erzeugt eine "tiefe" (physikalische) Kopie eines Objektes (vergleichbar mit Copyconstructor in C++)
Serialisierung in String toString() - schreibt das aktuelle Objekt mit allen Infos in einen String
Laufzeitklasse getClass() - gibt die Laufzeitklasse zurück
eindeutiger Identifier hashCode() - gibt die eindeutige ID der Klasse zurück
Threadverwaltung notify(), notifyAll(), wait()


Die Klasse Math ist das Matheobjekt mit allen Operationen für einfache numerische Berechnungen. Neben Konstanten PI und E werden auch viele mathematische Operationen wie Wurzelziehen, Exponentialzahlen, Sinus und Cosinus zur Verfügung gestellt. Alle Konstanten und Methoden in der Math-Klasse sind static, damit man kein eigenes Math-Objekt für jede Berechnung anlegen muss. Der Ergebnistyp fast aller Operationen ist double.


Konstanten: PI, E

  double wert = Math.PI;

Wurzelziehen, Logarithmus und Exponentialfunktion: sqrt, log, pow

  double x = Math.sqrt( 2 );
  double y = Math.pow( 2,10 );  // 2^10 = 1024

Trigonometrische Funktionen: sin, cos, acos, asin, atan, atan2

  double wert  = Math.sin( 0.0 );
  double wert2 = Math.sin( Math.PI );

Wertetabelle für die ersten 10 Sinuswerte im Intervall [0..2*PI]

  double schrittweite = 2.0*Math.PI/10.0;
  for( double x=0.0; x<=2*Math.PI; x=x+schrittweite )
  {
    System.out.println( "f( "+x+" ) = "+Math.sin( x ));
  }

Minimum und Maximum: min, max

   int x = Math.min( 2,4 );  //Ergebnis ist 2 
   int y = Math.max( 2,4 );  //Ergebnis ist 4

Absolutwert, Runden und Abschneiden: abs, ceil, floor, rint, round

  double x = Math.abs( -4.1 );   //Ergebnis ist 4.1
  int    y = Math.round( 4.2 );  //Ergebnis ist 4

Umrechnung Grad (0..360) in Radian (0..2*PI)

  x = Math.toDegrees( Math.PI );
  y = Math.toRadians( 90 );

Pseudozufallswert ausgeben aus dem Bereich von größer oder gleich 0.0 bis kleiner 1.0

  double x = Math.random();


Was ist der Sinn von Wrapperklassen?

Bearbeiten

Wrapperklassen ("Hüllklassen") dienen als Verbindungsglied zwischen den Ordinaltypen von Java (byte, short, int, long, float, double, char) und den Klassen in Java und dabei insbesondere der Klasse String. Ein Grundproblem bei der Benutzung von Oberflächen wie AWT und Swing ist, dass bei Eingaben (z.B. in ein Textfeld) immer nur Texte verwaltet werden. Diese Texte müssen in "richtige" Zahlen verwandelt werden, um mit ihnen zu rechnen, und dann wieder in Texte zurückgewandelt werden, um sie in den GUI-Komponenten wieder anzuzeigen. Für dieses Verfahren, was auch als Boxing/Unboxing bekannt ist, werden die Wrapper-Klassen gebraucht. Bis auf die Klasse Character stammen alle Wrapper-Klassen von java.lang.Number ab. Es gibt nun zu jedem ordinalen Datentyp eine Wrapperklasse:

Ordinaltyp Wrapperklasse
byte Byte
short Short
int Integer
long Long
float Float
double Double
boolean Boolean
char Character

Umwandlung von Ordinaltyp nach String

Bearbeiten

Beispiele für die Wandlung von Ordinaltyp -> String:

  int i=10;
  Integer ii = new Integer( i );
  String s = ii.toString();

oder kürzer:

  String s1 = Integer.toString( 10 );
  String s2 = Float.toString( 3.14f );

Umwandlung von String nach Ordinaltyp

Bearbeiten

Beispiele für die Wandlung von String -> Ordinaltyp:

  String s = "10";
  int   i = Integer.parseInt( s );
  float f = Float.parseFloat( s );

Umwandlung in andere Zahlensysteme

Bearbeiten

Mit Wrapperklassen ist auch die Umwandlung vom Dezimalsystem in andere Zahlensysteme (Hex, Oct, Dual) möglich. Auch für diese Operationen braucht kein neues Integer-Objekt angelegt werden weil die Operationen static sind.

Beispiele für die Umwandlung (das Ergebnis ist aber ein String!):

  System.out.println( "42 = "+Integer.toBinaryString( 42 )+"b (binär)");
  System.out.println( "42 = "+Integer.toHexString(    42 )+"h (hexadezimal)");
  System.out.println( "42 = "+Integer.toOctalString(  42 )+"o (octal)");

Autoboxing/Autounboxing

Bearbeiten

In Java 5.0 wird vom Compiler mittlerweile ein automatisches Boxing/Unboxing vorgenommen. Dadurch ist es unnötig geworden, Ordinaltypen aus den Wrapperklassen mittels den toByte/toShort/toInt/toLong/toFloat/toDouble Methoden zu gewinnen. Andersherum kann einem Objekt einer Wrapperklasse direkt ein Ordinaltyp zugewiesen werden. Dieses Feature verkürzt den Code und macht ihn lesbarer. Allerdings muss nun der Programmierer dafür Sorge tragen, dass nicht ständig zwischen einem Wrapper-Objekt und einem Ordinaltyp hin- und hergewechselt werden muss, da dies enorm viele Resourcen beansprucht.

Beispiel:

 Integer i = 5;
 byte b = Byte.valueOf("7");

Welche Vor- und Nachteile hat man durch Wrapperklassen?

Bearbeiten

WrapperKlassen haben den Nachteil, dass der Overhead beim instanziieren geringfügig höher ist, sie sind einige Bytes größer als der dazugehörige primitive Datentyp.

Erstellt man größere Mengen, wirkt sich dies negativ auf die Geschwindigkeit und den benötigten Speicher eines Programmes aus.

Wrapper sind zum Beispiel erforderlich, um threadsichere primitive Datentypen zur Verfügung zu stellen.



Dieses Kapitel überschneidet sich mit Java Standard: Zeichenketten. Aus praktischen Gründen gehören die Inhalte auf jeden Fall zu den Grundlagen. Es ist zu überlegen, ob das Kapitel "String" als Teil von java.lang erhalten bleibt und sich schwerpunktmäßig mit den theoretischen Grundlagen befassen soll oder ob es gestrichen werden kann.

Die Klasse String und StringBuffer

Bearbeiten

Zur Verarbeitung von Texten oder neudeutsch Zeichenketten gibt es in Java die Klasse String. Strings ermöglichen das effiziente Erstellen, Vergleichen und Modifizieren von Texten. Wie man am großen "S" schon sieht, ist String eine Klasse und kein Ordinaltyp wie char (Ordinaltypen beginnen immer mit einem Kleinbuchstaben). Strings werden sehr häufig benutzt, wenn man Werte aus GUI-Oberflächen auslesen bzw. verändern oder Textverarbeitung betreiben möchte. Darum gibt es für Strings einige Vereinfachungen, um den Umgang mit ihnen zu erleichtern:

  • zur Benutzung muss man kein String-Objekt erzeugen (man kann es natürlich trotzdem tun)
  • man kann Strings einfach mit einer Zuweisung einer Zeichenkette = ".." initialisieren
  • Strings können einfach mit Hilfe des +-Zeichens aneinandergehängt werden (Konkatenation)
  • Die Klasse String muss nicht explizit importiert werden
  • Vorsicht: Um zwei Strings auf Gleichheit bzgl. des Inhalts zu überprüfen, kann man nicht == benutzen. Mit == wird überprüft, ob die beiden Objekte identisch sind, nicht ob deren Inhalt der gleiche ist. Für den Test auf Inhaltsgleichheit benutzt man deshalb die Methode equals() die von der Klasse String bereitgestellt wird.

ein paar Beispiele:

  String s1 = "T-Rex";               // verkürzte Initialisierung
  String s2 = "Dino";
  String s3 = new String( "Dino" );  // s2 ist nun eine Kopie des String "Dino"
  if( !s1.equals(s2) )               // wenn beide Strings ungleich sind
    s1 = s2 +" "+ s1;                // dann mache daraus einen String "Dino T-Rex"
  if( s2 != s3)                      // Vorsicht, dies ergibt wahr!
    s1 = s2 +" "+ s3;                // hieraus wird nun "Dino Dino"

Unicode-Unterstützung

Bearbeiten

Die Zeichenketten (Strings) in Java verwenden zur Laufzeit eine UTF-16-Codierung, um die einzelnen Zeichen zu speichern. Das bedeutet, dass normalerweise ein Zeichen 16 Bit belegt. Beim Schreiben und Lesen von Dateien kann eine Konvertierung in verschiedene Zeichensatzcodierungen erfolgen.

Immutable-Objekte

Bearbeiten

Zeichenketten (Strings) sind unveränderbare Objekte (immutable). Das bedeutet, dass ein Zeichenkettenobjekt, das einmal erzeugt worden ist, nicht verändert werden kann. Methoden, die Zeichenketten verändern, wie z.B. substring(), erzeugen in Wirklichkeit ein neues Zeichenkettenobjekt. Das gilt auch für "+" und "+=".

Man sollte beachten, daß substring() zwar ein neues Zeichenkettenobjekt erzeugt, aber weiterhin dieselbe interne Struktur für die Zeichen der Zeichenkette referenziert. Wenn man also eine sehr lange Zeichenkette hat, die nur temporär verwendet wird, und dann mit substring() davon eine kurze Zeichenkette gewinnt, die man lange aufbewahrt, bleiben intern die Zeichen der langen Zeichenkette weiterhin gespeichert. Normalerweise ist das erwünscht.



Bei den bisherigen Formulierungen sind Sinn und Inhalt dieses Kapitels nicht klar. <T> bezieht sich auf generische Typen, während Class eine Grundlage für "alles" bietet. Beides ist wichtig und müsste genauer herausgearbeitet werden.

Klassen und generische Typen

Bearbeiten

Die Klasse Class repräsentiert Klassen und Interfaces in einer laufenden Java-Applikation.

Sie stellt in Java eine Möglichkeit dar, Metadaten, sprich Informationen über Informationen zu erhalten. Man erhält also Informationen über die beerbte Klasse bzw. das implementierte Interface, nicht das Objekt an sich.

Eine Anwendungsmöglichkeit ist die Verwendung von Methoden eines Objektes, die einem zum Zeitpunkt der Programmierung unbekannt sind, jedoch mittels Class zur Laufzeit ermittelt werden können (siehe auch: java.lang.reflect.Method).

Des Weiteren ist es möglich Objekte zum Zeitpunkt der Programmierung unbekannten Datentyps zur Laufzeit zu erzeugen (siehe auch: java.lang.reflect.Constructor).

Der Type Parameter <T>

Bearbeiten

Seit Java 1.5 muss ein Type Parameter an die Klasse Class übergeben werden. Durch die Angabe des Typs kann die Typprüfung zur Kompilierzeit vorgenommen werden.

Der Type Parameter T bestimmt welchen Datentypen ein Class-Objekt abbilden soll. Beispielsweise ist die Angabe für String Class<String>. Wenn der Typ unbekannt ist, wird ein Fragezeichen, eine so genannte Wildcard verwandt Class<?>.

Eine Wildcard gestattet also jeden Datentypen in einem Class-Objekt abzubilden.

Wildcards ihrerseits können eingeschränkt werden. Beispielsweise möchte man nur Erben von Number zulassen:

Integer ob = new Integer(4);
Class<? extends Number> cl = ob.getClass();

Ein Class Objekt erzeugen

Bearbeiten

Die Klasse Class besitzt keine Konstruktoren, die public sind. Jedes Objekt in Java besitzt jedoch eine Methode getClass(), die ein Class-Objekt als Repräsentation der beerbten Klasse des Objektes bereitstellt:

// Ein Integer-Objekt erzeugen
Integer einObjekt = new Integer(20);

// Ein Class-Objekt der Klasse Integer erzeugen
Class<Integer> classObjekt = einObjekt.getClass();

// Namen der Klasse ausgeben
System.out.println( classObjekt.getName() );

Eine andere Möglichkeit zur Erzeugung ist die statische Methode Class.forName( "Klassenname" ). Hierbei muss jedoch beachtet werden, dass dann nur eine Wildcard (?) als Type Parameter möglich ist, da zur Kompilierzeit noch nicht feststeht welchen Datentyp der übergebene String "Klassenname" darstellen soll:

// Ein Class-Objekt erzeugen, dass die Klasse Integer abbilden soll
Class<?> classObjekt = Class.forName( "java.lang.Integer" );

Beachte: Man muss dem Klassennamen den kompletten Paketpfad der abzubildenden Klasse voranstellen. Bsp.: java.sql.ResultSet.

Das Abbilden primitiver Datentypen ist ebenfalls möglich:

// Den primitiven Datentypen double als Class-Objekt
Class<?> classObjekt = Class.forName("double");

// Den primitiven Datentypen int als Class-Objekt
Class<?> classObjekt = Class.forName("int");

// Den primitiven Datentypen int als Class-Objekt per TYPE-Field
Class<Integer> f = Integer.TYPE;

Die dritte Möglichkeit ist die Erzeugung über das Signalwort class:

Class<? extends Number> cl = Integer.class;

Anwendungsbeispiel mit Method

Bearbeiten

Nehmen wir an, man hätte folgendes Interface.

public interface MyInterface {	

	public String getName();	

	public void setName( String name );

}

Nun soll dieses Interface von irgendwem implementiert werden können und wir möchten, dass er seine Implementierung einfach nur in einen Ordner legen muss, damit die verwendende Applikation das Interface aufnehmen kann.

Hier hätten wir eine solche Implementierung.

public class MyInterfaceImplementation implements MyInterface{	

	private String name = "Meine Implementierung";

	public String getName() {
		return this.name;
	}

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

}

Jetzt stehen wir vor dem Problem, dass wir eine Klasse mit uns unbekanntem Namen zur Laufzeit verfügbar machen müssen. Das erledigen wir mittels java.io.File.

String path = "C:\\Dokumente und Einstellungen\\wenGehtsWasAn\\Desktop";

File directory = new File( path );
System.out.println( directory.exists() );
if( directory.exists() ) {
   // gibt alle Dateien und Verzeichnisse in einem Verzeichnis als File Array zurück
   File[] files = directory.listFiles();

   // Splittet jeden Dateinamen in Bezeichnung und Endung
   // siehe "regular expression" und String.split()
   String name[] = files[i].getName().split("\\.");
   System.out.print( name[0] + "." );
   System.out.println( name[1] );
}

So, nun hätten wir schonmal den Namen der Datei und die Endung. Uns interessieren natürlich nur die .class Dateien. Jetzt müssen wir die Klasse wie gesagt noch laden. Class.forName( "Name" ), funktioniert, solange alle zu ladenden Klassen in dem selben Verzeichnis liegen wie die aufrufende Klasse.

try {
  Class<?> klasse = Class.forName( "MyInterfaceImplementation" );
  MyInterface impl = ( MyInterface ) klasse.newInstance();
  System.out.println( impl.getName() );
} catch ( ClassNotFoundException ex ) {
  System.out.println( ex.getMessage() );                    
} catch ( InstantiationException ex ) {
  System.out.println( ex.getMessage() );                    
} catch (IllegalAccessException ex) {
  // Wird geworfen, wenn man einen access-modifier nicht beachtet
  // Man kann mittels reflect die modifier aber auch ändern						
  System.out.println( ex.getMessage() );
}

Da die zu ladenden Klassen aber eher in einem anderen Verzeichnis liegen werden, kann man auf einen ClassLoader zurückgreifen.

URL sourceURL = null;
try {
  // Den Pfad des Verzeichnisses auslesen			
  sourceURL = directory.toURI().toURL();
} catch ( java.net.MalformedURLException ex ) {
  System.out.println( ex.getMessage() );
}

// Einen URLClassLoader für das Verzeichnis instanzieren
URLClassLoader loader = new URLClassLoader(new java.net.URL[]{sourceURL}, Thread.currentThread().getContextClassLoader());

Der java.net.URLClassLoader kann auf die übergebenen URL`s zugreifen und aus diesen Quellen Klassen laden. Das kann auch ein Webverzeichnis sein, muss also nicht auf dem ausführenden Rechner liegen.

Abschließend noch ein Beispiel für die aufrufende Klasse. Es beruft sich auf obiges Interface und die Implementierung.

import java.lang.reflect.*;
import java.io.File;
import java.net.URL;
import java.net.URLClassLoader;

public class MyManagement {	

  public static void main( String bla[] ) {
    // erste Möglichkeit - Klassen liegen in selbem Verzeichnis wie diese Klasse
    try {
       Class<?> klasse = Class.forName( "MyInterfaceImplementation" );
       MyInterface impl = ( MyInterface ) klasse.newInstance();
       System.out.println( impl.getName() );
    } catch ( ClassNotFoundException ex ) {
       System.out.println( ex.getMessage() );                    
    } catch ( InstantiationException ex ) {
       System.out.println( ex.getMessage() );                    
    } catch (IllegalAccessException ex) {
       // Wird geworfen, wenn man einen access-modifier nicht beachtet
       // Man kann mittels reflect die modifier aber auch ändern			
       System.out.println( ex.getMessage() );
    }

    String path = "C:\\Dokumente und Einstellungen\\wenGehtsWasAn\\Desktop";

    File directory = new File( path );
    System.out.println( directory.exists() );
    if( directory.exists() ) {
       File[] files = directory.listFiles();
			
       URL sourceURL = null;
       try {
          // Den Pfad des Verzeichnisses auslesen		
          sourceURL = directory.toURI().toURL();
       } catch ( java.net.MalformedURLException ex ) {
          System.out.println( ex.getMessage() );
       }

       // Einen URLClassLoader für das Verzeichnis instanzieren
       URLClassLoader loader = new URLClassLoader(new java.net.URL[]{sourceURL}, Thread.currentThread().getContextClassLoader());

       // Für jeden File im Verzeichnis...
       for( int i=0; i<files.length; i++ ) {

       // Splittet jeden Dateinamen in Bezeichnung und Endung
       // siehe "regular expression" und String.split()
       String name[] = files[i].getName().split("\\.");
                
       // Nur Class-Dateien werden berücksichtigt
       if( name[1].equals("class") ){
					
          try {
             // Die Klasse laden						
             Class<?> source = loader.loadClass( name[0] );

             // Prüfen, ob die geladene Klasse das Interface implementiert
             // bzw. ob sie das Interface beerbt
             // Das Interface darf dabei natürlich nicht im selben Verzeichnis liegen	
             // oder man muss prüfen, ob es sich um ein Interface handelt Class.isInterface()				
             if( MyInterface.class.isAssignableFrom( source ) ) {
							
                MyInterface implementation = ( MyInterface ) source.newInstance();

                Method method = source.getDeclaredMethod( "getName", new Class<?>[]{} );

                System.out.println( method.invoke( implementation, new Object[]{} ) );
							
             }							                        
          } catch (InstantiationException ex) {
             // Wird geworfen, wenn die Klasse nicht "instanziert" werden kann
             System.out.println( ex.getMessage() );
          } catch (IllegalAccessException ex) {
             // Wird geworfen, wenn man einen access-modifier nicht beachtet
             // Man kann mittels reflect die modifier aber auch ändern		
             System.out.println( ex.getMessage() );
          } catch ( NoSuchMethodException ex ) {
             // Wird geworfen, wenn die Class die spezifizierte Methode nicht implementiert
             System.out.println( ex.getMessage() );
          } catch ( ClassNotFoundException ex ) {
             // Wird geworfen, wenn die Klasse nicht gefunden wurde						
             System.out.println( ex.getMessage() );
          } catch ( InvocationTargetException ex ) {
	     // Wird geworfen, wenn die aufreufene, über Method
	     // reflektierte Methode eine Exception wirft
	     System.out.println( ex.getCause().getMessage() );
          }
        }
      }
    }
  }

}

Nur noch zur Erläuterung: getDeclaredMethod() gibt ein Method-Objekt zurück, das die über den String sowie durch das Class-Array identifizierte Methode des Class-Objektes repräsentiert.

Will man die Methode ausführen bzw. aufrufen, muss man invoke verwenden. Dabei werden das Objekt, aus dem die Methode aufgerufen werden soll und die Parameter als Object-Array übergeben.

Möchte man eine Methode aus einer statischen Klasse aufrufen, muss man statt einem Object null übergeben.

Method method = source.getDeclaredMethod( "getName", new Class<?>[]{} );

System.out.println( method.invoke( implementation, new Object[]{} ) );


Forgeschrittene Themen

Bearbeiten

Java Standard: Applets


Für die Dateiverarbeitung gibt es in der J2SE zwei wesentliche Pakete:

  • java.io als bereits unter Java 1.0 vorhandenes Paket und
  • java.nio als Erweiterung von java.io um dessen Probleme zu beheben.

Dabei werden durch beide Pakete allgemeine Funktionalitäten bereitgestellt, welche über die "Datei"-Verarbeitung im eigentlichen Sinne hinausgehen. So basiert die Verarbeitung auf Datenströmen, wie sie durch eine Datei, eine Netzwerkverbindung oder z.B. auch über den Parallelport empfangen und/oder gesendet werden können.




Überblick

Bearbeiten

In Java wird die Datenverarbeitung durch Datenströme (engl. Streams) realisiert. Zu diesem Zweck gibt es zwei Oberklassen, welche die Basisfunktionalitäten bereitstellen:

Die Daten können dabei als geordnete Folge von einzelnen Bytes angesehen werden. Um die verschiedenen Funktionalitäten abzubilden gibt es schließlich eine Reihe von Implementierungen, die diese Oberklassen erweitern.

Insbesondere für die Nutzung von Textdateien stehen zwei weitere wesentliche Klassen zur Verfügung:

  • Reader für eingehende Daten und
  • Writer für ausgehende Daten.

Reader und Writer arbeiten hierbei auf Basis von UNICODE und somit des Datentyps char welcher zwei (!) Bytes repräsentiert.

InputStream

Bearbeiten

Der reine InputStream ist eine Abstrakte Java klasse. Von ihm werden folgende InputStreams abgeleitet:

  • AudioInputStream
  • ByteArrayInputStream
  • FileInputStream
  • FilterInputStream
BufferedInputStream
CheckedInputStream
CipherInputStream
DataInputStream
DeflaterInputStream
DigestInputStream
InflaterInputStream
GZIPInputStream
ZipInputStream
LineNumberInputStream
ProgressMonitorInputStream
PushbackInputStream
  • ObjectInputStream
  • PipedInputStream
  • SequenceInputStream
  • StringBufferInputStream

Der InputStream ist die Super Klasse und repräsentiert einen byte Strom. Die von ihm abgeleiteten Klassen sind auf bestimmte Bereiche spezialisiert, z.b. liest der ObjectInputStream Objekte für die Weiterverarbeitung ein.

OutputStream

Bearbeiten

Genau wie der InputStream ist auch der OutputStream eine Abstrakte Klasse in Java. Von ihm werden folgende Ströme abgeleitet:

  • ByteArrayOutputStream
  • FileOutputStream
  • FilterOutputStream
BufferedOutputStream
CheckedOutputStream
CipherOutputStream
DataOutputStream
DeflaterOutputStream
GZIPOutputStream
ZipOutputStream
DigestOutputStream
InflaterOutputStream
PrintStream
LogStream
  • ObjectOutputStream
  • PipedOutputStream

Abgeleitete Klassen von Reader:

  • BufferedReader
LineNumberReader
  • CharArrayReader
  • FilterReader
PushbackReader
  • InputStreamReader
FileReader
  • PipedReader
  • StringReader

Abgeleitete Klassen von Writer:

  • BufferedWriter
  • CharArrayWriter
  • FilterWriter
  • OutputStreamWriter
FileWriter
  • PipedWriter
  • PrintWriter
  • StringWriter




Dateien und Java

Bearbeiten

Dateien sind in Java leider nicht ganz so einfach zu benutzen wie in C++ und Delphi. In Java laufen alle Ein- und Ausgaben über Streams, egal ob es sich nun um Lesen und Schreiben von Konsole oder in und aus Dateien handelt. Auf der einen Seite ist diese Vorgehensweise zwar für Einsteiger relativ umständlich, andererseits dafür aber auch sehr elegant, da sämtliche IO nach dem gleichen Prinzip läuft: egal ob Konsole, Dateien, Netzwerk oder Internet - alles wird genau gleich abgehandelt, nur die Namen der Klassen heißen anders.

Zum Einstieg sieht man hier ein kleines Programm, das eine Nachricht (einen String) in eine Textdatei schreibt. Dazu müssen drei Streams miteinander verkoppelt werden: nämlich FileOutputStream muss in einen OutputstreamWriter gepackt werden und dieser wieder in einem BufferedWriter. Das ganze muss natürlich in einem try..catch-Block passieren, da sich Java sonst weigert das Programm überhaupt zu übersetzen.

import java.io.*;
  import java.text.*;
  import java.util.*;
  public class TextdateiTest 
  {
    public static void main( String args[] )
    {
       String news = "Hallo Textdatei";
       try 
       {
          BufferedWriter datei = new BufferedWriter(
                                 new OutputStreamWriter(
                                 new FileOutputStream( "test.txt" ) ) );
          System.out.println( "Message für die Textdatei = "+news );
          datei.write( news, 0, news.length() );
          datei.newLine();
          datei.close();
       } 
       catch( IOException e ) 
       {
          System.out.println( "Achtung Fehler: "+e );
       }
    }
  }




Einleitung

Bearbeiten

Nebenläufigkeit (concurrency) ist die Fähigkeit eines Systems, zwei oder auch mehrere Aufgaben (scheinbar) gleichzeitig auszuführen. In Java kann die Ausführungsparallelität innerhalb eines Programmes mittels Threads (lightweight processes) erzwungen werden. Laufen mehrere Threads parallel, so spricht man auch von Multithreading.

Threads erzeugen und starten

Bearbeiten

Threads sind Bestandteil des Java-Standardpackages java.lang.

Methode 1: Die Thread-Klasse

Bearbeiten

Die Klasse Thread implementiert die Schnittstelle Runnable.

Prinzipielle Vorgehensweise:

  • Eine Klasse, abgeleitet von Thread, erstellen
  • Die Thread-Methode public void run () überschreiben
  • Instanz(en) der Thread-Subklasse bilden
  • Die Thread-Instanz(en) mittels public void start() starten

Beispiel:

public class SimpleThread extends Thread
{
  public void run()
  {
    for (int i = 0; i<=100; i++)
    {
      System.out.println(getName() + ": " + i);

      try
      {
        sleep(50);
      }
      catch(InterruptedException ie)
      {
        // ...
      }
    } 
  }
}

public class App
{
  public static void main(String[] args)
  {
    SimpleThread thread1 = new SimpleThread();
    SimpleThread thread2 = new SimpleThread();
 
    thread1.start();
    thread2.start();
  }
}

oder

public class SimpleThread extends Thread
{
  public SimpleThread(String id)
  {
    start();
  } 


  public void run()
   {
    for (int i = 0; i<=100; i++)
    {
      System.out.println(getName() + ": " + i);

      try
      {
        sleep(50);
      }

      catch(InterruptedException ie)
      {
        // ...
      }
    } 
  }
}

public class App
{
  public static void main(String[] args)
  {
    new SimpleThread("1");
    new SimpleThread("2");
  }
}

Methode 2: Das Runnable-Interface

Bearbeiten

Prinzipielle Vorgehensweise:

  • Eine Runnable implementierende Klasse erstellen
  • Die Runnable-Methode public void run () überschreiben
  • Instanz(en) der Runnable implementierenden Klasse bilden
  • Thread-Instanz(en) erstellen. Als Parameter wird eine Runnable-Instanz übergeben.
  • Die Thread-Instanz(en) mittels public void start() starten
public class SimpleRunnable implements Runnable
{ 
  public void run()
  { 
    for (int i = 0; i<=100; i++)
     {
      System.out.println(Thread.currentThread().getName() + ": " + i);

      try
      {
        Thread.sleep(50);
      }

      catch(InterruptedException ie)
      {
        // ...
      }
    } 
  }
}


public class App
{
  public static void main(String[] args)
  {
    Runnable r1 = new SimpleRunnable();
    Runnable r2 = new SimpleRunnable();

    new Thread(r1).start();
    new Thread(r2).start();    
  }
}

Der main-Thread

Bearbeiten

Jede Java-Applikation besitzt zumindest einen Thread, den main-Thread.

public class App
{
  public static void main(String[] args)
  {
    Thread t = Thread.currentThread();

    System.out.println("Name = " + t.getName());
    System.out.println("Id = " + t.getId());
    System.out.println("Priorität = " + t.getPriority());
    System.out.println("Zustand = " + t.getState());
  }
}

zeigt

Name = main
Id = 1
Priorität = 5
Zustand = RUNNABLE


Thread-Zustände

Bearbeiten

Threads können in verschiedenen Zuständen vorliegen:

Thread.State Erläuterung
NEW erzeugt, aber noch nicht gestartet
RUNNABLE lauffähig; wird in der Java Virtual Machine (JVM) ausgeführt oder wartet auf die Freigabe von Ressourcen
BLOCKED geblockt; wartet auf einen Monitor-Lock (siehe Synchronisation)
WAITING wartet; das ist der Fall wenn eine der folgenden Methoden aufgerufen wurde:

Object.wait(...) ohne Timeout
Thread.join(...) ohne Timeout
LockSupport.park()

TIME_WAITING wartet eine definierte Zeitspanne; das ist der Fall wenn eine der folgenden Methoden aufgerufen wurde:

Thread.sleep(...)
Object.wait(...) mit Timeout
Thread.join(...) mit Timeout
LockSupport.parkNanos(...)
LockSupport.parkUntil(...)

TERMINATED beendet; eine einmal beendete Thread-Instanz kann nicht mehr erneut gestartet werden

Abfragen kann man den Thread-Zustand mit der bereits in vorhergehenden Abschnitt verwendeten Funktion

Thread.State.getState().

Threads beenden

Bearbeiten

Das stop-Chainsaw Massacre

Bearbeiten

Die Klasse Thread implementiert die Funktion void stop() zur manuellen Beendigung eines Threads. Diese Funktion ist problembehaftet, daher als deprecated gekennzeichnet und soll nicht benutzt werden.

Der run-Suizid

Bearbeiten

Implementiert man in der run()-Methode keine Endlosschleife, dann löst sich das Problem durch Zeitablauf von selbst.

public class SimpleThread extends Thread
{ 
  public void run()
  { 
    for(int i = 0; i<=100; i++)
    {
      System.out.println(getState());
    } 
  }
}


public class App
{
  public static void main(String[] args)
  {
    SimpleThread t = new SimpleThread();    
    t.start();

    try
    {
      Thread.sleep(1000);      
    }
    catch(InterruptedException ie)
    {
    }

    System.out.println(t.getState());
  }
}

ergibt

...
RUNNABLE
RUNNABLE
TERMINATED

Flag-Methode

Bearbeiten
public class SimpleThread extends Thread
{    
  volatile boolean running = false;  // Flag

  public void stopIt() 
  {
    running = false;
  }
  
  public void run()
  {    
    running = true;

    while(running == true)
    {
      System.out.println(getState());
    }   
  }
}


public class App
{
  public static void main(String[] args)
  {
    SimpleThread t = new SimpleThread();    
    t.start();

    try
    {
      Thread.sleep(1000);
    }
    catch(InterruptedException ie)
    {
      // ...
    }

    t.stopIt();    
  }
}

Referenz-Methode

Bearbeiten

Eine Variante der Flag-Methode ist hier angegeben. Sie benutzt kein Extra-Flag sondern stattdessen die Referenz zum Thread.

public class Applet implements Runnable
{
  Thread thread;
  
  public void start()
  {
    thread = new Thread(this);
    thread.start();
  }
  
  public void stop()
  {
    thread = null;
  }
  
  public void run()
  {
    Thread myThread = Thread.currentThread();
    while (thread == myThread)
    {
      // tu etwas
    }
  }
}

Interrupt

Bearbeiten

Mit Hilfe der Thread-Methoden

void interrupt()

und

boolean isInterrupted()

können wir auch einen Thread beenden.

public class SimpleThread extends Thread
{     
  public void run()
  {    
    while(isInterrupted() == false)
    {
      System.out.println(getState());
    }   
  }
}


public class App
{
  public static void main(String[] args)
  {
    Thread t = new SimpleThread();    
    t.start();

    try
    {
      Thread.sleep(1000);
    }
    catch(InterruptedException ie)
    {
      // ...
    }

    t.interrupt();    
  }
}

Threads anhalten

Bearbeiten

Ein Thread kann mit den Funktionen

static void sleep(long millis)

static void sleep(long millis, int nanos)

für millis Millisekunden (+ nanos Nanosekunden) angehalten werden. Diese Funktion kennen wir schon aus früheren Codebeispielen.

Zusätzlich kann anderen wartenden Threads der Vortritt gewährt werden.

static void yield()

zum temporären Pausieren des momentan ausgeführten Threads, um andere Threads ausführen zu können.

public class SimpleThread extends Thread
{  
  public void run()
  {         
    for(int i = 0; i<=1000; i++)
    {
      System.out.println(getName() + ": " + i);

      if(i%2 == 0)
      {
        yield();
      }
    }   
  }
}


public class App
{
  public static void main(String[] args)
  {
    new SimpleThread().start();
    new SimpleThread().start();
    new SimpleThread().start();
  }
}

ergibt

...
Thread 0: 9
Thread 0: 10
Thread 1: 9
Thread 1: 10
Thread 2: 9
Thread 2: 10
...

Auf das Ende eines Threads warten

Bearbeiten

void join()

Warte auf das Ende eines Threads

void join(long millis)

void join(long millis, int nanos)

Warte längstens millis Millisekunden (+ nanos Nanosekunden) auf das Ende eines Threads. Übergibt man als Parameter 0, so bedeutet dies, dass beliebig lange auf das Ende des Threads gewartet wird.

public class SimpleThread extends Thread
{  
  public void run()
  {    
    for(int i = 0; i<=1000; i++)
    {
      System.out.println(getState());
    }   
  }
}
 

public class App
{
  public static void main(String[] args)
  {
    SimpleThread t = new SimpleThread();    

    t.start();

    try
    {
      t.join();      
    }
    catch(InterruptedException ie)
    {
      // ..
    }

    System.out.println(t.getState());
  }
}

ergibt

...
RUNNABLE
RUNNABLE
TERMINATED

Thread-Priorität

Bearbeiten

Durch Anwendung der Thread-Methode

void setPriority(int newPriority)

kann die Priorität eines Threads im Bereich von 1 (Thread.MIN_PRIORITY) bis 10 (Thread.MAX_PRIORITY) geändert werden. Ein Thread erhält zuerst immer die Priorität des Threads, in dem er gestartet wurde. Der main-Thread weist standardmäßig die Priorität 5 auf. Die konkrete Umsetzung der zugewiesenen Priorität hängt dabei sehr stark vom jeweiligen Betriebssystem ab.

Die Abfrage der momentanen Thread-Priorität geschieht mittels der Thread-Methode

int getPriority()


Scheduler

Bearbeiten

Die Ausführungsplanung zum Umschalten zwischen aktiven Threads und Prozessen nennt man Scheduling. Mögliche Scheduling-Strategien:

  • Prioritätssteuerung (Preemption): Es wird immer der Thread mit der höchsten Priorität ausgeführt
  • Zeitsteuerung (Time-Slicing): Der Scheduler weist den einzelnen Threads Zeitabschnitte zu, während der sie zur Ausführung gelangen
  • Prioritäts- und Zeitsteuerung kombiniert

Dämonen

Bearbeiten

Ein Dämon ist ein Thread der im Hintergrund ausgeführt wird. Mit der Thread-Methode

void setDaemon(boolean on)

kann zwischen den Thread-Typen Dämon-Thread (on = true) und konventionell im Vordergrund laufendem Thread (off = false) umgeschaltet werden.

Abfragen kann man den Thread-Typ mittels

boolean isDaemon()

Nicht jeder Thread eignet sich zum Dämon-Thread. Es gilt folgende Regel: Eine Java-VM beendet sich, wenn keine Nicht-Dämon-Threads mehr laufen.

Ein prominenter Dämon ist übrigens der Garbage Collector - es würde auch wenig Sinn ergeben, wenn er weiter arbeiten würde, nachdem ein Programm zu Ende ist.

Threadgruppen

Bearbeiten

Threads kann man auch gruppieren. Beim Start einer Applikation wird automatische eine main-Threadgruppe angelegt. Der main-Thread ist Teil dieser Gruppe. All Threads sind automatisch Bestandteil einer Threadgruppe.

public class SimpleThread extends Thread
{  
  SimpleThread(ThreadGroup tg, String name)
  {
    super(tg, name);
  }

  public void run()
  {         
    try
    {
       sleep(5000);
    } 
    catch(InterruptedException ie)
    {
      // ...
    }
  }
}


public class App
{
  public static void main(String[] args)
  {     
    ThreadGroup tg = new ThreadGroup("Testgruppe");
    Thread t1 = new SimpleThread(tg, "t1");
    Thread t2 = new SimpleThread(tg, "t2");

    t1.start();
    t2.start();

    Thread array[] = new Thread[tg.activeCount()];

    tg.enumerate(array);

    for(Thread t: array)
    {
      System.out.println(t.getName() + " ist Gruppenmitglied von " + tg.getName() );
    }
  }
}

Threads synchronisieren

Bearbeiten

Race Conditions

Bearbeiten

Definition (aus Wikipedia, der freien Enzyklopädie vom 27.08.2005):

Als Race Condition (zu deutsch Wettlaufsituation oder Wettkampfbedingung) bezeichnen Programmierer Konstellationen, in denen das Ergebnis einer Operation vom zeitlichen Verhalten bestimmter Einzeloperationen abhängt. Unbeabsichtigte Race Conditions sind ein häufiger Grund für schwer auffindbare Programmfehler, so genannte Heisenbugs. ...

Genau solche Race Conditions können auch bei Threads vorkommen. Greifen mehrere Threads auf bestimmte Ressourcen (Dateien, Variablen, Datenbanken, Drucker, etc.) zu, so kann sich der Programmierer nicht darauf verlassen, dass die Threads dies immer in einer bestimmten Reihenfolge und kollisionsfrei tun. Das hängt auch von Faktoren ab, die der Programmierer nicht beeinflussen kann, zum Beispiel der Scheduling-Strategie oder der konkreten Umsetzung der gewünschten Thread-Priorität.

Deshalb muss eine Programmiersprache Mechanismen bereitstellen, um derartige Probleme zu lösen. Eine Methode wird als thread-sicher bezeichnet, wenn sie bedenkenlos von Threads aufgerufen werden kann.

Atomare Operationen

Bearbeiten

Definition (aus Wikipedia, der freien Enzyklopädie vom 27.08.2005):

Eine Atomare Operation [...] bezeichnet eine Operation im Computer, welche durch keine andere Operation unterbrochen werden kann. Atomare Operationen sind wichtig beim Synchronisieren von Daten. ...

Auch diesen Aspekt muss man beim Programmieren mit Threads beachten (Stichwort: Transaktionen bei Datenbanken).

Selbst bei einfachen Variablen kann man sich nicht auf atomares Verhalten verlassen. Um sicherzustellen, dass Objekt- oder Klassenvariablen vor jedem Zugriff auf den aktuellen Stand gebracht werden verwendet man das Schlüsselwort volatile (flüchtig, launisch, unbeständig).

volatile long l;

Das Schlüsselwort synchronized

Bearbeiten

Greifen mehrere Threads auf dieselben Ressourcen zu, so kann es zwecks Problemvermeidung notwendig sein Threads zu synchronisieren.

Eine Methode können wir durch das Schlüsselwort synchronized kennzeichnen

 synchronized void xxx()
 {
   // ...
 }

Die VM wird diese Methode nun bei Bedarf automatisch sperren und entsperren.

Auch einzelne Code-Blöcke können synchronisiert werden

synchronized(objekt)
{
  // ...
}

Monitore

Bearbeiten

Die JVM definiert für Synchronisationszwecke sogenannte Monitore. Jedes Objekt mit synchronisiertem Code ist in Java ein Monitor. Dieser Monitor besitzt einen Monitor-Lock (Lock, Sperre) und führt eine Warteliste von Threads die ausgesperrt wurden. Beendet ein Thread eine synchronized-Methode oder einen synchronized-Block, so wird die Sperre aufgehoben und der nächste Thread kommt zum Zug. Der Aufruf von sleep() oder yield() hebt eine solche Sperre allerdings nicht auf.

Deadlocks

Bearbeiten

Ein Problem bei Synchronisation können sogenannte Deadlocks (Verklemmungen) darstellen. Dabei sperren sich zwei oder mehrere Threads gleichzeitig von benötigten Ressourcen aus. Thread A wartet darauf, dass Thread B eine Ressource freigibt. Gleichzeitig wartet aber Thread B, dass Thread A seine gesperrte Ressource freigibt. Setzt man im Vorfeld keine geeigneten Maßnahmen, dann werden die beiden Threads ewig warten und mit den Threads auch der genervte und ratlose Programmbenutzer.

Zur Erkennung von Deadlock-Situationen können Deadlock-Detection-Utilities hilfreich sein. Aktuelle Java-Releases und auch IDEs wie zum Beispiel Eclipse 3.1 bieten derartige Möglichkeiten.

Das wait-notify-Konzept

Bearbeiten

Mit Hilfe der Object-Methode

public final void wait()

können Threads in einen Wartezustand versetzt werden. Sie geben dann den Monitor frei.

Besitzt ein Thread den Monitor eines Objektes, so kann er durch

void notify()

oder

void notifyAll()

wartende Threads benachrichtigen und aus dem Wartezustand erlösen.

Concurrent-Programming ab Java 5.0

Bearbeiten

Ab Java 5.0 werden in den Packages

  • java.util.concurrent
  • java.util.concurrent.atomic
  • java.util.concurrent.locks

zusätzlich zur konventionellen Thread-Programmierung weitere Möglichkeiten für nebenläufiges Programmieren bereitgestellt. Nachfolgend werden einführend in diese Thematik ganz kurz ein paar Klassen und Möglichkeiten dieser Pakete angesprochen.

Callable

Bearbeiten

Das Interface Callable<> dient ähnlichen Zwecken wie das Interface Runnable, ist aber ein bisschen flexibler. Außerdem ist anstelle der run-Methode die call-Methode zu implementieren .

import java.util.concurrent.*;

public class CallableThread implements Callable<Integer> 
{
  private int i;

  CallableThread(int i)
  {
    this.i = i;
  }

  public Integer call()
  {
    for (int j=0; j<=1000; j++)
    {
      System.out.println("Thread " + i + ":" + j);
    }

    return i;
  }
}

Executors

Bearbeiten

Die Klasse Executors enthält Fabriks- und Hilfsmethoden für

  • Callable
  • Executor
  • ExecutorService
  • ScheduledExecutorService
  • ThreadFactory

Future und FutureTask

Bearbeiten

Die Schnittstelle Future<> implementiert Runnable. Die Klasse FutureTask<> implementiert Future<>.

Thread-Pools

Bearbeiten

Zwecks optimaler Performance kann die Kreierung von Thread-Pools sinnvoll sein. Thread-Pools fassen Threads zu gemanagten Kollektionen zusammen.

import java.util.concurrent.*;

public class App
{
  public static void main(String[] args)
  {     
    ExecutorService es = Executors.newCachedThreadPool();
    FutureTask<Integer> f1 = new FutureTask<Integer>(new CallableThread(1));
    FutureTask<Integer> f2 = new FutureTask<Integer>(new CallableThread(2));

    es.execute(f1);
    es.execute(f2);
  }
}

Zeitgesteuerte Task-Ausführung

Bearbeiten

TimerTask implementiert Runnable und kann ein- oder mehrmalig durch einen Timer ausgeführt werden.

import java.util.*;

public class Task extends TimerTask 
{
  public void run()
  {
    System.out.println("Hallo!"); 
  }
}


public class App
{
  public static void main(String[] args)
  {
    Timer timer = new Timer();
    timer.schedule(new Task(), 1000, 2000);
  }
}

Die schedule()-Methode gibt es mit unterschiedlichen Signaturen. Im Beispiel wurde eine Initialverzögerung (delay) von 1000ms und eine Wiederholung (period) alle 2000ms gewählt.


Java Standard: Threads Grundlagen Java Standard: Threads Runnable


Das Paket java.util beinhaltet zahlreiche Klassen und Interfaces für alltägliche Aufgaben und ist für Sie als eine Art Werkzeugladen zu betrachten. Dabei werden Klassen für die Zeichenkettenbearbeitung, für die Datumsverarbeitung, für die Generierung von "Zufällen", die Komprimierung und nicht zu vergessen für die Datenhaltung zur Laufzeit bereitgestellt. Auch die Logging-API ist unterhalb von java.util angesiedelt.




Für die Erstellung von grafischen Oberflächen stehen verschiedene APIs zur Verfügung.

  • Das Graphics Object – Wer es gern etwas aufwendiger mag, darf seine Oberflächen auch selbst programmieren. Hierfür gibt es das Graphics bzw. Graphics2D Objekt.
  • AWT – Das Abstract Window Toolkit (AWT) als Implementation einer plattformspezifischen aber -unabhängigen Oberfläche ist im Java Software Development Kit der Standard Edition enthalten.
  • SwingSwing als plattformunspezifische und -unabhängige Oberfläche ist im Java Software Development Kit [JDK] der Standard Edition ab Version 1.2 bzw. Java 2 enthalten.
  • SWT – Das Standard Widget Toolkit (SWT) ist eine plattformabhängige Oberflächen-API.
  • Java3D – Die Java3D-API steht alternativ für 3D-Oberflächen zur Verfügung.

Siehe auch Wikipedia:



Einführung

Bearbeiten

Die Schnittstelle, um graphische Objekte wie Shapes, oder aber auch Bilder zu zeichnen, ist in Java das Graphics-Objekt. Dieses ist seit der Version 1.0 im JDK enthalten, mit der Version 1.2 ist das Graphics2D Objekt hinzugefügt worden.

Beginnen wir mit einem Beispiel. Die einfachste Variante, um mal einige Erfahrungen mit Graphics zu sammeln, ist die paint(Graphics g) Methode von dem JFrame. Als erstes legen wir einen Nachfahren von JFrame an, welcher die Methode überschreibt:

import java.awt.Color;
 import java.awt.Font;
 import java.awt.Graphics;
 import javax.swing.JFrame;
 public class MyFrame extends JFrame {
   @Override
   public void paint(Graphics g) {
     super.paint(g);
     g.setFont(new Font("Dialog", Font.PLAIN, 18));
     g.setColor(Color.BLUE);
     g.drawString("Hallo Welt", 10,40);
   }
 }

Nun wird noch ein wenig Start-Up Code benötigt, welcher das Fenster erzeugt und anzeigt:

import javax.swing.JFrame;
 import javax.swing.WindowConstants;
 public class SimpleFrame {
   public static void main(String[] args) {
     JFrame frame = new MyFrame();
     frame.setSize(200,300);
     frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
     frame.setVisible(true);
   }
 }

Aufbau des Bildschirms

Bearbeiten

Der Bildschirm hat den Aufbau eines Rasters, wobei die einzelnen Punkte Pixel genannt werden. Der Punkt 0,0 des Rasters ist die linke obere Ecke des Bildschirms. Wenn ein Objekt auf dem Bildschirm gezeichnet werden soll, muss man die Koordinaten dafür angeben:

g.drawString("Hallo Welt", 10,40);

Am Punkt 10,40 wird nun begonnen, das Objekt zu zeichnen. Der Punkt 10,40 ist dabei der linke obere Punkt des zu zeichnenden Objektes.

Aufbau des Graphics

Bearbeiten

Der Bildschirm oder auch der Drucker, wird in Java der Device Space genannt, das Gegenstück ist der User Space. Wenn mittels Graphics gezeichnet wird, wird auf dem User Space gezeichnet. Der User Space ist eine abstrakte Zeichnungsebene, auf der ohne Gedanken an das Endgerät (Monitor oder Drucker) gezeichnet werden kann. Aus diesem Grund wird der User Space nicht in Pixel sondern in Units unterteilt. 72 Units bilden einen Inch. Java konvertiert während des Renderns vom User Space in den Device Space.

Einfache Beispiele

Bearbeiten

Versuchen wir nun eine einfache Linie zu zeichnen. Dazu verwenden wir obigen Code und bauen ihn entsprechend um:

MyFrame:
  public void paint(Graphics g) {
    super.paint(g);
    Graphics2D g2 = (Graphics2D)g;
    Line2D line = new Line2D.Double(30,30,80,80);
    g2.setStroke(new BasicStroke(4));
    g2.draw(line);
  }

In SimpleFrame deaktivieren wir die Titelleiste, so lässt sich das Frame einfacher als Zeichenfläche gebrauchen:

SimpleFrame:
   public static void main(String[] args) {
    JFrame frame = new MyFrame();
    frame.setSize(200,300);
    frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
    frame.setUndecorated(true);
    frame.setVisible(true);
   }

Wie Sie vielleicht gemerkt haben, besitzen die Java2D Klassen wie Line2D keinen öffentlichen Konstruktor. Sie müssen statt dessen eine der inneren Klassen instanziieren - in diesem Fall Double.

Rechtecke

Bearbeiten

Rechtecke werden mit drawRect, fillRect, drawRoundRect oder fillRoundRect gezeichnet.

public void paint (Graphics g) {
   g.fillRoundRect(startX,startY,breite,hoehe,radiusAbrundungX,radiusAbrundungY);
 }



Das Abstract Window Toolkit [AWT] ist eine API zum Entwickeln quasi-plattformunabhängiger grafischer Oberflächen mit Java. Diese "Plattformunabhängigkeit" wurde erreicht, indem komplexere Oberflächenelemente wie Bäume und Tabellen, welche nicht auf allen Betriebssystemen vorhanden sind, nicht in das AWT aufgenommen wurden. Diese Beschränkungen können Sie umgehen, indem Sie die seit Java 1.2 im J2SE enthaltenen Swing Komponenten verwenden.

Für die Entwickler von Applets ist das AWT vielfach jedoch noch der kleinste gemeinsame Nenner, da alle heute gängigen Browser lediglich den Java 1.1 Standard beherrschen.

Grafische Komponenten

Bearbeiten

AWT-Komponenten sind entweder im Package java.awt oder in den dazugehörenden Subpackages zu finden.

Fenster mit Rahmen

Bearbeiten

Die Klasse Window repräsentiert ein Fenster ohne Rahmen und ohne Menüleiste. Frame ist eine Subklasse von Window und beinhaltet Fensterrahmen und die Möglichkeit eine Menüleiste einzubinden.

Beispiel:

import java.awt.*;
import java.awt.event.*;

public class TestFrame extends Frame
{
  public TestFrame () 
  {
    setTitle("Ein reines, unbeflecktes Frame");  // Fenstertitel setzen
    setSize(400,100);                            // Fenstergröße einstellen  
    addWindowListener(new TestWindowListener()); // EventListener für das Fenster hinzufügen
                                                 // (notwendig, damit das Fenster geschlossen werden kann)
    setVisible(true);                            // Fenster (inkl. Inhalt) sichtbar machen
  }

  class TestWindowListener extends WindowAdapter
  {
    public void windowClosing(WindowEvent e)
    {
      e.getWindow().dispose();                   // Fenster "killen"
      System.exit(0);                            // VM "killen" 
    }    	
  }

  public static void main (String args[]) 
  {
    new TestFrame ();
  }
}
 
(Screenshot: Linux/KDE)

Für die Erstellung von Menüs sind folgende wesentliche Java-AWT-Klassen notwendig:

  • MenuBar - die Menüleiste, wie sie auch in Ihrem Browser zu finden ist
  • Menu - ist ein Menü oder Untermenü (z.B. "Datei")
  • MenuItem - ist ein Menüeintrag (z.B. "Neues Fenster")

Um Untermenüs zu erzeugen, fügen Sie das Untermenü dem Menü mit der Methode add (MenuItem) hinzu.


Beispiel:

import java.awt.*;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;

public class MenuSample {
  public MenuSample () {
    Frame f = new Frame ("Menübeispiel");
    f.addWindowListener(new WindowAdapter () {
      public void windowClosing (final WindowEvent e) {
        System.exit(0);
      }
    });
    f.setMenuBar(this.getMenubar ());
    f.setSize(400,100);
    f.setVisible(true);
  }

  protected MenuBar getMenubar () {
    // Menüleiste anlegen
    MenuBar menueLeiste = new MenuBar ();
    // Ein Menü anlegen
    Menu datei = new Menu ("Datei");
    // Einen Menüeintrag anlegen
    MenuItem oeffnen = new MenuItem ("Öffnen");
    // Den Eintrag dem Menü hinzufügen
    datei.add (oeffnen);
    // Das Menü der Leiste hinzufügen
    menueLeiste.add(datei);

    // Noch ein Menü anlegen
    Menu extra = new Menu ("Extras");
    // ... und noch ein Menü
    Menu schriftart = new Menu ("Schriftart");
    //...das Menü dem Extramenü als Untermenü hinzufügen
    extra.add(schriftart);
    // Das Untermenü mit Einträgen füllen
    schriftart.add("Sans");
    schriftart.add("Sans Serif");
    schriftart.addSeparator();
    schriftart.add("Courier");
    // Das Extramenü der Leiste hinzufügen
    menueLeiste.add(extra);
    return menueLeiste;
  }

  public static void main (String[] args) {
    MenuSample menusample = new MenuSample ();
  }
}
 
(Screenshot: Linux/KDE)

Schaltflächen

Bearbeiten

AWT-Schaltflächen sind durch die Klasse Button zu erstellen.

Beispiel:

import java.awt.*;
import java.awt.event.*;

public class TestFrame extends Frame
{
  Button button = new Button("Schaltfläche");

  public TestFrame () 
  {
    setTitle("Schaltflächenbeispiel");                              
    addWindowListener(new TestWindowListener());
    button.setForeground(Color.RED);                    // Vordergrundfarbe auf "rot" setzen
    button.setBackground(Color.WHITE);                  // Hintergrundfarbe auf "weiß" setzen 
    button.addActionListener(new TestActionListener()); // EventListener für Schaltfläche hinzufügen
    add(button);                                        // Schaltfläche zum Fenster hinzufügen         
    pack();                                             // Fenstergröße auf die benötigte Größe 
                                                        // "zusammenpacken"
    setVisible(true);                           
  }

  class TestWindowListener extends WindowAdapter
  {
    public void windowClosing(WindowEvent e)
    {
      e.getWindow().dispose();                   
      System.exit(0);                            
    }    	
  }
  
  class TestActionListener implements ActionListener
  {
    public void actionPerformed(ActionEvent e) 
    {
      System.out.println("Schaltfläche wurde gedrückt");
    }         	
  }

  public static void main (String args[]) 
  {
    new TestFrame ();
  }
}
 
(Screenshot: Linux/KDE)

Beschriftungen

Bearbeiten

Beschriftungen können Sie mit Label generieren. Positionieren können Sie die Beschriftungen über die Methode void setAlignment(int alignment) mit den Konstanten

  • Label.LEFT
  • Label.CENTER
  • Label.RIGHT
import java.awt.*;
import java.awt.event.*;

public class TestFrame extends Frame
{
  Label label = new Label("Beschriftung");

  public TestFrame () 
  {
    setTitle("Beschriftungsbeispiel");                              
    addWindowListener(new TestWindowListener());
    label.setAlignment(Label.CENTER);                   // Label zentrieren
    add(label);                                         // Label zum Fenster hinzufügen         
    setSize(300,150);										    
    setVisible(true);                           
  }

  class TestWindowListener extends WindowAdapter
  {
    public void windowClosing(WindowEvent e)
    {
      e.getWindow().dispose();                  
      System.exit(0);                            
    }    	
  }
  
  public static void main (String args[]) 
  {
    new TestFrame ();
  }
}
 
(Screenshot: Linux/KDE)

Texteingabefelder

Bearbeiten

Einzeilig

Bearbeiten

Einzeilige Texteingabefelder lassen sich mit der Klasse TextField erstellen. Den Inhalt eines Texteingabefeldes können Sie mittels der Methode String getText() auslesen. Zum Zwecke der Erstellung von Passworteingabefeldern lässt sich die Anzeige des Eingabetextes durch die Methode void setEchoChar(char c) maskieren.

Beispiel:

import java.awt.*;
import java.awt.event.*;

public class TestFrame extends Frame
{	
  public TestFrame () 
  {
    setTitle("TextEntry-Beispiel"); 
    addWindowListener(new TestWindowListener());
    add(new TextField("Hallo"));                 // Texteingabefeld mit einer Textvorgabe 
    pack();
    setVisible(true);                           
  }

  class TestWindowListener extends WindowAdapter
  {
    public void windowClosing(WindowEvent e)
    {
      e.getWindow().dispose();                  
      System.exit(0);                            
    }           
  }
 
  public static void main (String args[]) 
  {
    new TestFrame ();
  }
}
 
(Screenshot: Linux/KDE)

Mehrzeilig

Bearbeiten

Mehrzeilige Texteingabefelder können Sie mit der Klasse TextArea erstellen. Auch diese Klasse implementiert bzw. erbt eine Vielzahl von Methoden durch welche sie das Erscheinungsbild anpassen und Text setzen oder abfragen können.

Beispiel:

import java.awt.*;
import java.awt.event.*;

public class TestFrame extends Frame
{	
  public TestFrame () 
  {	  
    setTitle("TextArea-Beispiel"); 
    addWindowListener(new TestWindowListener());
   
    /* 5 Zeilen hoch, 30 Spalten breit */ 
    add(new TextArea("Hallo Welt!\nWo sind deine vielgerühmten Genies?", 5,30));   

    pack();
    setVisible(true);                           
  }

  class TestWindowListener extends WindowAdapter
  {
    public void windowClosing(WindowEvent e)
    {
      e.getWindow().dispose();                  
      System.exit(0);                            
    }           
  }
 
  public static void main (String args[]) 
  {
    new TestFrame ();
  }
}
 
(Screenshot: Linux/KDE)

Kontrollkästchen (Checkboxes) und Optionsfelder (Radiobuttons)

Bearbeiten

Zur Erstellung von Kontrollkästchen dient die Klasse Checkbox. Mittels boolean getState() ist der Zustand einer Checkbox abfragbar. Durch eine Zusammenfassung mehrerer Checkboxes in einer CheckboxGroup sind auch Radiobuttons realisierbar.

Beispiele:

import java.awt.*;
import java.awt.event.*;

public class TestFrame extends Frame
{		
  public TestFrame () 
  {		  
    setTitle("Checkbox-Beispiel"); 
    addWindowListener(new TestWindowListener());
    add(new Checkbox("Farbdarstellung", true));
    pack();
    setVisible(true);                           
  }

  class TestWindowListener extends WindowAdapter
  {
    public void windowClosing(WindowEvent e)
    {
      e.getWindow().dispose();                  
      System.exit(0);                            
    }           
  }
 
  public static void main (String args[]) 
  {
    new TestFrame ();
  }
}
 
(Screenshot: Linux/KDE)

Aufklappbare Auswahlliste (Choice)

Bearbeiten

Ein Choice ist eine ausklappbarer Auswahlliste. Die momentane aktive Auswahl wird auch im eingeklappten Zustand angezeigt. Die Funktion eines Choice ist vergleichbar mit einer Gruppe von Radiobuttons. Durch die Methode void add(String item) lassen sich Strings in die Auswahlliste eingefügen. Die momentan gewählte Option lässt sich durch die Methode int getSelectedIndex() oder String getSelectedItem() abfragen.

Beispiel:

import java.awt.*;
import java.awt.event.*;

public class TestFrame extends Frame
{	
  Choice choice = new Choice();	

  public TestFrame () 
  {		  
    setTitle("Choice-Beispiel"); 
    addWindowListener(new TestWindowListener());
   
    choice.add("Farbdarstellung");
    choice.add("Graustufen");
    choice.add("B/W");
    add(choice);
   
    pack();
    setVisible(true);                           
  }

  class TestWindowListener extends WindowAdapter
  {
    public void windowClosing(WindowEvent e)
    {
      e.getWindow().dispose();                  
      System.exit(0);                            
    }           
  }
 
  public static void main (String args[]) 
  {
    new TestFrame ();
  }
}
 
(Screenshot: Linux/KDE)

Auswahlliste (List)

Bearbeiten

Eine weitere Möglichkeit zur Selektion verschiedener Optionswerte bietet die List (nicht zu verwechseln mit dem Kollektions-Interface java.util.List). Bei einer List ist im Gegensatz zum/zur Choice standardmäßig keine Vorauswahl getroffen. Ein Listeneintrag kann aber mittels der Methode void select(int index) vorausgewählt werden. In einer Auswahlliste können mehrere Einträge ausgewählt werden (Multiselektion), wenn ein derartiges Verhalten vom Programmierer über den List-Konstruktor oder die Methode void setMultipleMode(boolean b) vorgesehen wird.

Beispiel:

import java.awt.*;
import java.awt.event.*;

public class TestFrame extends Frame
{	
  List list = new List();	

  public TestFrame () 
  {		  
    setTitle("List-Beispiel"); 
    addWindowListener(new TestWindowListener());
   
    list.add("Farbdarstellung");
    list.add("Graustufen");
    list.add("B/W");
      
    add(list);
   
    pack();
    setVisible(true);                           
  }

  class TestWindowListener extends WindowAdapter
  {
    public void windowClosing(WindowEvent e)
    {
      e.getWindow().dispose();                  
      System.exit(0);                            
    }           
  }
 
  public static void main (String args[]) 
  {
    new TestFrame ();
  }
}
 
(Screenshot: Linux/KDE)

Ein Panel ist eine simple Containerklasse zur Aufnahme anderer AWT-Komponenten. Besonders im Zusammenhang mit komplexeren AWT-GUI-Layouts ist der Einsatz der Klasse Panel im Zusammenwirken mit Layout-Managern unverzichtbar. Auch als "Zeichenfläche" eignet sich ein Panel. Panel-Konstruktoren sind Panel() und Panel(LayoutManager layout) . AWT-Komponenten werden mit diversen add-Methoden zu einem Panel hinzugefügt. Applet ist eine Panel-Subklasse.

Beispiel 1:

import java.awt.*;
import java.awt.event.*;

public class TestFrame extends Frame
{	
  Panel panel= new Panel();	

  public TestFrame () 
  {		  
    setTitle("Panel-Beispiel"); 
    addWindowListener(new TestWindowListener());
   
    panel.add(new Button("OK"));
    panel.add(new Button("Abbrechen"));
    add(panel);
         
    pack();
    setVisible(true);                           
  }

  class TestWindowListener extends WindowAdapter
  {
    public void windowClosing(WindowEvent e)
    {
      e.getWindow().dispose();                  
      System.exit(0);                            
    }           
  }
 
  public static void main (String args[]) 
  {
    new TestFrame ();
  }
}
 
(Screenshot: Linux/KDE)

Beispiel 2:

import java.awt.*;
import java.awt.event.*;

public class TestFrame extends Frame
{	
  public TestFrame () 
  {		  
    setTitle("Panel-Beispiel"); 
    addWindowListener(new TestWindowListener());
   
    add(new DrawingPanel());
         
    setSize(300,100);
    setVisible(true);                           
  }

  class DrawingPanel extends Panel
  {
    public void paint(Graphics g)
    {
      g.setColor(Color.GREEN);
      g.fillRect(20, 10, 50, 50);
      g.setColor(Color.RED);
      g.fillOval(100, 10, 50, 50);
      g.setColor(Color.BLUE);
      g.drawString("Hallo Welt!", 200, 40);
      g.setColor(Color.WHITE);
      g.drawRect(180, 10, 100, 50);		  
    }
  }  
 
  class TestWindowListener extends WindowAdapter
  {
    public void windowClosing(WindowEvent e)
    {
      e.getWindow().dispose();                  
      System.exit(0);                            
    }           
  }
 
  public static void main (String args[]) 
  {
    new TestFrame ();
  }
}
 
(Screenshot: Linux/KDE)

Ein Canvas ist eine leere AWT-Komponente ohne Grundfunktionalität. Ein Canvas kann wie ein Panel als reine "Zeichenfläche" verwendet werden oder als Grundlage für die Erstellung eigener AWT-Komponenten dienen.

ScrollPane

Bearbeiten

Eine ScrollPane ist ein Container zur Aufnahme einer AWT-Komponente. Eine ScrollPane bietet vertikale und horizontale Scrollbars. Wann und wie diese Scrollbars erscheinen, kann vom Programmierer festgelegt werden (SCROLLBARS_ALWAYS, SCROLLBARS_AS_NEEDED, SCROLLBARS_NEVER ). Des Weiteren kann auch das Verhalten der Scrollbars beinflusst werden (z.B. ob Scrolling auch mit dem Mausrad möglich sein soll oder die initiale Scrollposition).

Beispiel:

import java.awt.*;
import java.awt.event.*;

public class TestFrame extends Frame
{	
  ScrollPane scroll = new ScrollPane();
  Panel panel = new Panel();
 
  public TestFrame () 
  {		  
    setTitle("ScrollPane-Beispiel"); 
    addWindowListener(new TestWindowListener());
   
    for(int i=1; i<=10; i++)
    {
      panel.add(new Label("Beschriftung " + i));
    }
   
    scroll.add(panel);
    add(scroll);
         
    setSize(300,100);
    setVisible(true);                           
  }
 
  class TestWindowListener extends WindowAdapter
  {
    public void windowClosing(WindowEvent e)
    {
      e.getWindow().dispose();                  
      System.exit(0);                            
    }           
  }
 
  public static void main (String args[]) 
  {
    new TestFrame ();
  }
}
 
(Screenshot: Linux/KDE)

Dialog ist eine Window-Subklasse. Dialoge können modal oder nichtmodal sein. Ein Dialog wird immer mit einem Konstruktor erstellt, dem als erster Parameter ein Frame, ein anderer Dialog oder nullübergeben wird. Sinnvollerweise wird die Klasse Dialog für Benutzereingaben, Meldungsfenster, About-Dialoge oder dergleichen genutzt.

Dateiauswahldialog

Bearbeiten

Mit der Klasse FileDialog lässt sich sehr einfach ein modaler Dateiauswahldialog generieren.

Beispiel:

import java.awt.*;
import java.awt.event.*;

public class TestFrame extends Frame
{ 
  Button button = new Button("Dateidialog aufrufen");
  FileDialog fd;

  public TestFrame () 
  {		  
    setTitle("FileDialog-Beispiel"); 
    addWindowListener(new TestWindowListener());
   
    fd = new FileDialog(this, "Dateidialog");
  
    add(button);
   
    button.addActionListener(new ActionListener()
    {
      public void actionPerformed(ActionEvent e) 
      {			 	
        fd.setVisible(true);
      }    		
    });
     
    setSize(300,100);
    setVisible(true);                           
  }
 
  class TestWindowListener extends WindowAdapter
  {
    public void windowClosing(WindowEvent e)
    {
      e.getWindow().dispose();                  
      System.exit(0);                            
    }           
  }
 
  public static void main (String args[]) 
  {
    new TestFrame ();
  }
}
 
(Screenshot: Linux/KDE)

Layoutmanager

Bearbeiten

Die Layoutmanager für AWT und Swing werden in dem Kapitel Swing: Layoutmanager behandelt.

FlowLayout

Bearbeiten

Beispiel zum FlowLayout:

 import java.awt.*;
 import java.awt.event.*;
 
 public class TestFrame extends Frame
 {
   public TestFrame () 
   {
     setTitle("FlowLayout-Beispiel");                              
     addWindowListener(new TestWindowListener());
     
     setLayout(new FlowLayout());                 // FlowLayout setzen
     
     for(int i=1; i<=10; i++)
     {
       add(new Label("Beschriftung" + i));
     }
     
     setSize(300,150);										    
     setVisible(true);                           
   }
 
   class TestWindowListener extends WindowAdapter
   {
     public void windowClosing(WindowEvent e)
     {
       e.getWindow().dispose();                  
       System.exit(0);                            
     }    	
   }
   
   public static void main (String args[]) 
   {
     new TestFrame ();
   }
 }
 
(Screenshot: Linux/KDE)

BorderLayout

Bearbeiten

Dieses Layout platziert Komponenten in 5 möglichen Bereichen: oben, unten, links, rechts, zentriert.

Beispiel zum BorderLayout:

 import java.awt.*;
 import java.awt.event.*;
 
 public class TestFrame extends Frame
 {
   public TestFrame () 
   {
     setTitle("BorderLayout-Beispiel");                              
     addWindowListener(new TestWindowListener());
     
     setLayout(new BorderLayout());                                      // BorderLayout setzen
     
     add(new Label("Centertruder", Label.CENTER), BorderLayout.CENTER);  // CENTER
     add(new Label("Westruder", Label.CENTER), BorderLayout.WEST);       // WEST
     add(new Label("Eastruder", Label.CENTER), BorderLayout.EAST);       // EAST
     add(new Label("Northtruder", Label.CENTER), BorderLayout.NORTH);    // NORTH
     add(new Label("Southtruder", Label.CENTER), BorderLayout.SOUTH);    // SOUTH
     
     setSize(300,150);
     setVisible(true);                           
   }
 
   class TestWindowListener extends WindowAdapter
   {
     public void windowClosing(WindowEvent e)
     {
       e.getWindow().dispose();                  
       System.exit(0);                            
     }    	
   }
   
   public static void main (String args[]) 
   {
     new TestFrame ();
   }
 }
 
(Screenshot: Linux/KDE)

GridLayout

Bearbeiten

Beispiel zum GridLayout:

 import java.awt.*;
 import java.awt.event.*; 
 
 public class TestFrame extends Frame
 {
   public TestFrame () 
   {
     setTitle("GridLayout-Beispiel");                              
     addWindowListener(new TestWindowListener());
    
     setLayout(new GridLayout(4,3));              // GridLayout setzen
    
     for (int i=1; i<=10; i++)
     {
       add(new Label("Beschriftung" + i));  	
     }
    
     setSize(300,150);
     setVisible(true);                           
   }
 
   class TestWindowListener extends WindowAdapter
   {
     public void windowClosing(WindowEvent e)
     {
       e.getWindow().dispose();                  
       System.exit(0);                            
     }           
   }
  
   public static void main (String args[]) 
   {
     new TestFrame ();
   }
 }
 
(Screenshot: Linux/KDE)

GridBagLayout

Bearbeiten

Das GridBagLayout ist ein komplexer LayoutManager2[1], mit welchem eine Menge an Möglichkeiten geboten werden. Die eigentliche Ausrichtigung der Komponenten erfolgt über das GridBagConstraints Objekt, welches beim Hinzufügen zum Container anzugeben ist.

Beispiel zum GridBagLayout:

 import java.awt.*;
 import java.awt.event.*;
 
 public class TestFrame extends Frame
 { 
   GridBagLayout grid = new GridBagLayout();
   GridBagConstraints straints = new GridBagConstraints();
   Button button;
    
   public TestFrame () 
   {		  
     setTitle("GridBagLayout-Beispiel");
     setLayout(grid);                             // GridBagLayout setzen
     addWindowListener(new TestWindowListener());
    
     straints.gridx = straints.gridy = 0;
     straints.gridheight = straints.gridwidth = 1;
     straints.fill = GridBagConstraints.BOTH;
     button = new Button("Hallo");
     grid.setConstraints(button, straints);
     add(button);
 
     straints.gridy = 1;
     button = new Button("du");
     grid.setConstraints(button, straints);
     add(button);
 
     straints.gridx = 1;
     straints.gridy = 0;
     straints.gridheight=2;
     button = new Button("Welt!");
     grid.setConstraints(button, straints);
     add(button);
    
     straints.gridx = 0;
     straints.gridy = 2;
     straints.gridheight = 1;
     straints.gridwidth = 2;
     button = new Button("Was ist ein GridBag?");
     grid.setConstraints(button, straints);
     add(button);
        
     pack();
     setVisible(true);                           
   }
    
   class TestWindowListener extends WindowAdapter
   {
     public void windowClosing(WindowEvent e)
     {
       e.getWindow().dispose();                  
       System.exit(0);                            
     }           
   }
  
   public static void main (String args[]) 
   {
     new TestFrame ();
   }
 }
 
(Screenshot: Linux/KDE)

NullLayout

Bearbeiten

Ein NullLayout ist im eigentlichen Sinne kein Layout. Hierbei können die Elemente frei, mittels Positionsangaben, auf der Oberfläche positioniert werden.

Beispiel zum "Null-Layout":

 import java.awt.*;
 import java.awt.event.*;
 
 public class TestFrame extends Frame
 {
   Label label;	
 	
   public TestFrame () 
   {
     setTitle("Null-Layout-Beispiel");                              
     addWindowListener(new TestWindowListener());
    
     setLayout(null);                                     // "Null-Layout" setzen
    
     for(int i=1; i<=5; i++)
     {
       label = new Label("Beschreibung" + i);
       label.setBounds(10+i*20, 20+i*(25-2*i), 100, 15);  // x, y, breite, höhe
       add(label);        	
     }
    
     setSize(300,150);
     setVisible(true);                           
   }
 
   class TestWindowListener extends WindowAdapter
   {
     public void windowClosing(WindowEvent e)
     {
       e.getWindow().dispose();                  
       System.exit(0);                            
     }           
   }
  
   public static void main (String args[]) 
   {
     new TestFrame ();
   }
 }
 
(Screenshot: Linux/KDE)

Ereignisse

Bearbeiten

Damit AWT-Komponenten nicht nur mäßig schön am Bildschirm erstrahlen, sondern auch Funktionalität aufweisen, müssen sie auf Benutzeraktionen reagieren können.

Das Ereignis-Delegations-Modell (Event Delegation Model, Delegation Based Event Handling) ab Java 1.1 basiert auf dem Prinzip des Beobachter-Entwurfsmusters (Observer Pattern).

Was ein Beobachter-Entwurfsmusters ist, kann folgenden Quellen entnommen werden:

Jede AWT-Komponente kann Ereignisquelle sein. Jedes Objekt kann Ereignisempfänger sein. Damit ein Objekt Ereignisse empfangen kann, muss es bei einer Ereignisquelle als Ereignisempfänger registriert werden.

AWT-Ereignisse

Bearbeiten

AWT-Komponenten, als Ereignisquellen, reagieren auf externe Aktivitäten (z.B. Tastendruck, Mausbewegung, Mausklick) mit der Konstruktion von Ereignis-Objekten. Je nach Typ der Aktivität wird ein geeignetes Objekt erzeugt. Nachfolgendes Bild zeigt ein Klassendiagramm (basierend auf der Java 2 SE 5.0-API-Dokumentation), aus dem diese Ereignis-Objekte generiert werden. Alle möglichen AWT-Ereignisse sind Subklassen der abstrakten Klasse AWTEvent. Auch zusätzliche Swing-Events werden von dieser Klasse abgeleitet.

 

Ereignisempfänger (EventListener)

Bearbeiten

Als Ereignisempfänger bietet das Package java.awt.event verschiedene EventListener-Interfaces. Diese Ereignisempfänger werden bei der das Ereignis auslösenden AWT-Komponente registriert. Zu diesem Zweck besitzen die AWT-Komponenten geeignete add<X>Listener-Methoden. Für manche Listener-Interfaces existieren Adapter-Klassen. Diese implementieren das entsprechende Interface mit leeren Funktionsrümpfen.

Beispiel: Schaltflächen lösen beim Anklicken ein ActionEvent aus. Der Ereignisempfänger ist vom Typ ActionListener. Die Methode zum Registrieren dieses Ereignisempfänger heißt void addActionListener(ActionListener l). Das entsprechende Implementierungsbeispiel wurde bereits im Absatz Schaltflächen gezeigt.

Ereignisempfänger (EventAdapter)

Bearbeiten

Die Ereignisempfänger (EventListener) sind Interfaces. Dem großen Vorteil, die Problematik der nicht vorhandenen Mehrfachvererbung umgangen zu haben, steht somit (leider) der erhöhte Implementierungsaufwand entgegen. Um dieses Ärgernis zu Umgehen hat Sun netterweise sogenannte Adapterklassen bereitgestellt. Diese enthalten eine leere Defaultimplementation aller zu überschreibenen Methoden (siehe NullPattern). Lediglich für den ActionListener gibt es keinen Adapter - nunja dort gibt es auch nur eine Methode zu überschreiben.

  Bewertung: Diese Wikiseite besteht im Wesentlichen aus Listen und/oder Überschriften, wobei hier Fließtext stehen sollte. Es wird Hilfe zur Überarbeitung gewünscht.


Mit Swing hat Sun Microsystems eine API (Application Programming Interface, zu Deutsch Programmierschnittstelle) geschaffen um den Beschränkungen des AWT zu entkommen. Hierdurch können auch komplexe grafische Oberflächen wie Bäume oder Tabellen Verwendung finden. Swing - seit Java 1.2 zu finden im Paket javax.swing - benutzt hierbei jedoch zahlreiche Funktionalitäten des java.awt Paket so dass Vorkenntnisse im Bereich AWT sinnvoll sind.

Grafische Oberflächenelemente

Bearbeiten

Icon ist eine grafische Komponente für die Anzeige von - meist kleinen - Bildern.

ImageIcon

Bearbeiten

ImageIcon ist eine direkte Unterklasse von Icon.

 
JLabel

JLabel als äquivalent zu java.awt.Label und stellt eine "einfache" Beschriftung dar, welche optional um ein Icon erweitert werden kann. Eine JLabel Instanz kann hierbei einer anderen Komponente als Beschriftung über die Methode setLabelFor (JComponent) zugewiesen werden.

AbstractButton

Bearbeiten

Mit AbstractButton werden die allgemeinen Eigenschaften von Schaltflächen bereitgestellt. Hierzu gehören:

  • setMnemonic zur Definition des Buchstabens über den per Tastatur die Schaltfläche aktiviert werden kann.
  • doClick zur Aktivierung der Schaltfläche ohne Benutzerinteraktion
  • setIcon um ein Bild der Schaltfläche hinzuzufügen. Hierzu kommen noch die Methoden setDisabledIcon, setSelectedIcon, setRolloverIcon, setDisabledSelectionIcon und setRolloverSelectedIcon zur genauen Steuerung.
 
JButton

Der Typ JButton ist die Implementierung für eine "normale" Schaltfläche. Auf die Betätigung der Schaltfläche kann über einen ActionListener bzw. ein Action Objekt reagiert werden.

JToggleButton

Bearbeiten

Ein JToggleButton ist eine Schaltfläche, welche beim ersten Betätigen aktiviert wird und ein zweites gesondertes Betätigen zur Deaktivierung benötigt.

JCheckBox

Bearbeiten
 
JCheckBox

Eine JCheckBox ist ein Auswahlfeld, welches an- und ausgeschaltet werden kann.

JRadioButton

Bearbeiten

Ein JRadioButton ist ein Auswahlfeld, welches an- und ausgeschaltet werden kann. JRadioButtons werden hierbei üblicherweise nur in Verbindung mit einer ButtonGroup verwendet.

 

ButtonGroup

Bearbeiten

Eine ButtonGroup dient dazu insbesonder RadioButtons zusammenzufassen, so dass nur eine JRadioButton aktiv ist.

Bei einem Border handelt es sich um einen Rahmen, der um beliebige andere grafische Komponenten gelegt werden kann.

JToolTip

Bearbeiten

Bei einem JToolTip handelt es sich um Tooltip-Element, wie man es von verschiedenen Programmen kennt. Es zeigt nach kurzem Zeitraum eine kurze Hilfe zu einem bestimmten Element an.

JTextComponent

Bearbeiten

JTextField

Bearbeiten

und JTextField erzeugt ein Textfeld

JPasswordField

Bearbeiten
 

Das JPasswordField ist ein spezielles JTextField, welches die Zeichen nicht auf dem Bildschirm darstellt, sondern ein alternatives Zeichen zeigt, das so genannte Echozeichen. Standardmäßig ist das ein Sternchen. So lassen sich Passwort-Felder anlegen, die eine Eingabe verbergen.

JFormattedTextField

Bearbeiten

Das formatierte Eingabefeld dient dazu, Eingaben nur in einer bestimmten vorgegebenen Form zuzulassen. Im Gegensatz zu anderen Sprachen werden dabei die fest definierten Zeichen mit als Inhalt zurückgegeben. Eine einfache Möglichkeit, Formatierungen vorzugeben ist der MaskFormatter.

JTextArea

Bearbeiten

JTextPane

Bearbeiten

JTextPane erzeugt einen Textcontainer, der nur dazu da ist, mit Text gefüllt zu werden. Der in ihm enthaltene Text ist nicht editierbar.

JEditorPane

Bearbeiten

Mit Hilfe des JEditorPane kann ein bereits formatierter Text editiert werden. Das JEditorPane unterstützt als Formate RTF (Rich Text Format) und HTML.

JScrollBar

Bearbeiten

JScrollPane

Bearbeiten

JProgressBar

Bearbeiten

Mit der JProgressBar lässt sich der Fortschritt einer Aktion visualisieren. Bei der Instanziierung wird eine Unter- und Obergrenze angegeben. Innerhalb der Aktion, deren Fortschritt visualisiert werden soll, muss der aktuelle Wert der JProgressBar in bestimmten Abständen inkrementiert werden.

JComboBox

Bearbeiten

Die JComboBox beinhaltet verschiedene Einträge, die angewählt werden können.

Das JList-Element beinhaltet eine Liste von Einträgen, die, wie in der JComboBox, angewählt werden können.

JViewPort

Bearbeiten

JMenuBar

Bearbeiten

In Swing können alle Hauptfenster mit Ausnahme von JWindow eine Menüleiste haben. Dabei handelt es sich um eine Instanz der Klasse JMenuBar, die dem Hauptfenster durch Aufruf von addJMenuBar hinzugefügt wird.

JMenuItem

Bearbeiten

Ein einzelnes Element innerhalb eines JMenu, beispielsweise "Datei-Drucken".

Dient als Container für JMenuItems und JSeparators.

JCheckBoxMenuItem

Bearbeiten

Hat gegenüber dem JMenuItem den Vorteil, dass ein Haken beim aktuellen Status erscheint. Die entsprechenden Methoden sind: isSelected und setSelected

JRadioButtonMenuItem

Bearbeiten

JSeparator

Bearbeiten

Die einzelnen Elemente eines JMenu können mit dem JSeparator in logische Gruppierungen unterteilt werden. Es handelt sich um eine Trennlinie.

JRootPane

Bearbeiten

JLayeredPane

Bearbeiten

JDesktopPane

Bearbeiten

JInternalFrame

Bearbeiten

JPopupMenu

Bearbeiten

Ein JPopupMenu wird meist als Kontextmenü genutzt und entsprechend z.B. über die Maus oder Tastatur aktiviert.

JToolBar

Bearbeiten

Eine Toolbar kann man vereinfacht als die kleinen Leisten mit Bildchen beschreiben, wie man sie in nahezu jeder Textverarbeitung sieht.

 
JTabbedPane

JTabbedPane ermöglicht das Aufteilen der grafischen Oberfläche über sog. Reiter. Diese können beliebig positioniert und auch mit Icons versehen werden.

Codebeispiel

JSplitPane

Bearbeiten

JSplitPane ermöglicht die Aufteilung der grafischen Oberflächen in zwei Bereiche, wahlweise horizontal oder vertikal. Standardmässig kann der Anwender hierbei die Größen der zwei Komponenten der JSplitPanel beliebig in Ihrer Größe ändern.

Eine JTable ermöglicht eine grafische Tabelle, optional auch mit Tabellenkopf.

Beispiel

Bearbeiten
import javax.swing.table.DefaultTableModel;
import javax.swing.*;
import java.awt.BorderLayout;
import java.util.Vector;

public class GUIFilmTabelle extends JPanel {

  //Vektor für Spaltennamen
  private Vector columnNames = new Vector();

  //Vektor für Daten
  private Vector data = new Vector();

  public GUIFilmTabelle()
  {
    super(new BorderLayout());
    //TableModel: Tabellenmanipulation, Daten
    MyDefaultTableModel model = new MyDefaultTableModel(data, columnNames);
    //Tabelle: Anzeige
    JTable table = new JTable(model); 
    model = (MyDefaultTableModel) table.getModel();
    JComboBox comboBox = new JComboBox();
    comboBox.addItem("UP");
    comboBox.addItem("DOWN");
    table.getColumnModel().getColumn(1).setCellEditor(new DefaultCellEditor(comboBox));
    // ScrollPane zu JPanel hinzufügen
    add(new JScrollPane(table), BorderLayout.CENTER);    
  }
 
  // Inner class MyDefaultTableModel: Tabellen-Model
  public class MyDefaultTableModel extends DefaultTableModel {
    
    public MyDefaultTableModel(Vector data, Vector columnNames) {
      super(data, columnNames); 
      setDataVector(data,columnNames);
      this.addColumn("Name");
      this.addColumn("UP/DOWN");   
    }
    
    public Class getColumnClass(int col) {
      Vector v = (Vector) this.getDataVector().elementAt(0);
      return v.elementAt(col).getClass();
    }
       
    public boolean isCellEditable(int row, int col) {
      Class columnClass = getColumnClass(col);
      return columnClass != ImageIcon.class;
    }
  }
}

Swing stellt eine Komponente für die Darstellung von Baumstrukturen zur Verfügung - JTree.

Layoutmanager

Bearbeiten

BorderLayout

Bearbeiten

Dieses Layout platziert Komponenten in 5 möglichen Bereichen: oben, unten, links, rechts, zentriert.

Beispiel zum BorderLayout:

 import java.awt.*;
 import java.awt.event.*;
 
 public class TestFrame extends Frame
 {
   public TestFrame () 
   {
     setTitle("BorderLayout-Beispiel");                              
     addWindowListener(new TestWindowListener());
     
     setLayout(new BorderLayout());                                      // BorderLayout setzen
     
     add(new Label("Centertruder", Label.CENTER), BorderLayout.CENTER);  // CENTER
     add(new Label("Westruder", Label.CENTER), BorderLayout.WEST);       // WEST
     add(new Label("Eastruder", Label.CENTER), BorderLayout.EAST);       // EAST
     add(new Label("Northtruder", Label.CENTER), BorderLayout.NORTH);    // NORTH
     add(new Label("Southtruder", Label.CENTER), BorderLayout.SOUTH);    // SOUTH
     
     setSize(300,150);
     setVisible(true);                           
   }
 
   class TestWindowListener extends WindowAdapter
   {
     public void windowClosing(WindowEvent e)
     {
       e.getWindow().dispose();                  
       System.exit(0);                            
     }    	
   }
   
   public static void main (String args[]) 
   {
     new TestFrame ();
   }
 }
 
(Screenshot: Linux/KDE)

FlowLayout

Bearbeiten

Durch die Verwendung eines FlowLayouts werden alle GUI-Komponenten in eine Zeile der Reihe nach angehängt, bis diese durch deren Größe nicht mehr positioniert werden können. Sollte dies der Fall sein, so werden die folgenden Komponenten in die nächste Zeile versetzt. Beim FlowLayout kann jedoch kein Platz zwischen den einzelnen Grafikobjekten freigelassen werden (in der Hinsicht auf die Positionierung anderer Komponenten).

ViewportLayout

Bearbeiten

GridLayout

Bearbeiten

Dieses Layout bildet ein Gitter, dessen Zellen alle gleich groß sind. Die Anzahl der Spalten und Reihen, die dieses Gitter enthalten soll, kann spezifiziert werden. Pro Zelle kann dann genau eine Komponente zum Layout hinzugefügt werden. Die Komponenten werden in ihrer Größe gestreckt, damit sie die ganze Zelle ausfüllen. Außerdem kann ein horizontaler und vertikaler Abstand zwischen den einzelnen Zellen in Pixeln angegeben werden.

Beispiel zum GridLayout:

 import java.awt.*;
 import java.awt.event.*; 
 
 public class TestFrame extends Frame
 {
   public TestFrame () 
   {
     setTitle("GridLayout-Beispiel");                              
     addWindowListener(new TestWindowListener());
    
     setLayout(new GridLayout(4,3));              // GridLayout setzen
    
     for (int i=1; i<=10; i++)
     {
       add(new Label("Beschriftung" + i));  	
     }
    
     setSize(300,150);
     setVisible(true);                           
   }
 
   class TestWindowListener extends WindowAdapter
   {
     public void windowClosing(WindowEvent e)
     {
       e.getWindow().dispose();                  
       System.exit(0);                            
     }           
   }
  
   public static void main (String args[]) 
   {
     new TestFrame ();
   }
 }
 
(Screenshot: Linux/KDE)

GroupLayout

Bearbeiten

ScrollPaneLayout

Bearbeiten

Für gewöhnlich wird dieses Layout nie explizit benutzt. Stattdessen erzeugt man ein ScrollPanel, welches sich dann automatisch dieses Layout setzt, um die ScrollBars, die Eckkomponenten, den Inhalt und die Zeilen- und Spaltenheader zu setzen.

GridBagLayout

Bearbeiten

Das GridBagLayout ist ein komplexer LayoutManager2[2], mit welchem eine Menge an Möglichkeiten geboten werden. Die eigentliche Ausrichtigung der Komponenten erfolgt über das GridBagConstraints Objekt, welches beim Hinzufügen zum Container anzugeben ist.

Beispiel zum GridBagLayout:

 import java.awt.*;
 import java.awt.event.*;
 
 public class TestFrame extends Frame
 { 
   GridBagLayout grid = new GridBagLayout();
   GridBagConstraints straints = new GridBagConstraints();
   Button button;
    
   public TestFrame () 
   {		  
     setTitle("GridBagLayout-Beispiel");
     setLayout(grid);                             // GridBagLayout setzen
     addWindowListener(new TestWindowListener());
    
     straints.gridx = straints.gridy = 0;
     straints.gridheight = straints.gridwidth = 1;
     straints.fill = GridBagConstraints.BOTH;
     button = new Button("Hallo");
     grid.setConstraints(button, straints);
     add(button);
 
     straints.gridy = 1;
     button = new Button("du");
     grid.setConstraints(button, straints);
     add(button);
 
     straints.gridx = 1;
     straints.gridy = 0;
     straints.gridheight=2;
     button = new Button("Welt!");
     grid.setConstraints(button, straints);
     add(button);
    
     straints.gridx = 0;
     straints.gridy = 2;
     straints.gridheight = 1;
     straints.gridwidth = 2;
     button = new Button("Was ist ein GridBag?");
     grid.setConstraints(button, straints);
     add(button);
        
     pack();
     setVisible(true);                           
   }
    
   class TestWindowListener extends WindowAdapter
   {
     public void windowClosing(WindowEvent e)
     {
       e.getWindow().dispose();                  
       System.exit(0);                            
     }           
   }
  
   public static void main (String args[]) 
   {
     new TestFrame ();
   }
 }
 
(Screenshot: Linux/KDE)

CardLayout

Bearbeiten

Ein Layout, vergleichbar mit Karteikarten.

BoxLayout

Bearbeiten
  • LINE_AXIS
  • PAGE_AXIS
  • X_AXIS
  • Y_AXIS

OverlayLayout

Bearbeiten

Mit dem OverlayLayout ist es möglich mehrere Componenten übereinander anzuordnen.


SpringLayout

Bearbeiten

"NullLayout"

Bearbeiten

Ein NullLayout ist im eigentlichen Sinne kein Layout. Hierbei können die Elemente frei, mittels Positionsangaben, auf der Oberfläche positioniert werden. Beispiel zum "Null-Layout":

 import java.awt.*;
 import java.awt.event.*;
 
 public class TestFrame extends Frame
 {
   Label label;	
 	
   public TestFrame () 
   {
     setTitle("Null-Layout-Beispiel");                              
     addWindowListener(new TestWindowListener());
    
     setLayout(null);                                     // "Null-Layout" setzen
    
     for(int i=1; i<=5; i++)
     {
       label = new Label("Beschreibung" + i);
       label.setBounds(10+i*20, 20+i*(25-2*i), 100, 15);  // x, y, breite, höhe
       add(label);        	
     }
    
     setSize(300,150);
     setVisible(true);                           
   }
 
   class TestWindowListener extends WindowAdapter
   {
     public void windowClosing(WindowEvent e)
     {
       e.getWindow().dispose();                  
       System.exit(0);                            
     }           
   }
  
   public static void main (String args[]) 
   {
     new TestFrame ();
   }
 }
 
(Screenshot: Linux/KDE)

Aktionen

Bearbeiten

Über die grafische Benutzeroberfläche setzt der Anwender Befehle zur Durchführung von Aktionen ab. Hierzu unterstützt Swing unter anderem Tastatur und Maus als Eingabegeräte. Die Ereignisse werden über Swing's Event Dispatch Thread an die für ein bestimmtes Ereignis (Event) registrierten Empfänger (EventListener) weitergeleitet. Einige Elemente von Swing, z. B. JMenuItem und JButton können hierbei die Low Level Events wie Mouse Clicked automatisch in semantisch höherwertige Semantic Events (z. B. ActionEvent) übersetzen welche dann wiederum durch den EDT an die jeweiligen Empfänger (ActionListener) weitergeleitet werden.

Um dem Programmierer die Nutzung von ActionEvents zu vereinfachen, unterstützt Swing daher mit dem Interface Action sowie dessen abstrakter Implementierung AbstractAction das sogenannte Command Pattern.

Damit der EDT nicht durch langwierige Operationen blockiert wird und die Oberfläche "hängt" (vgl. Responsiveness), ist es eine übliche Vorgehensweise, die langwierige Arbeit in einem Worker Thread durchzuführen. Hierzu bietet Swing die Klasse SwingWorker. Diese nimmt dem Programmierer die Koordination des EDT mit dem Worker Thread weitgehend ab.




Die 3D-Programmierung läßt das Herz jedes Spielers höher schlagen. Endlich einmal ein eigenes Spiel realisieren und Freunde damit tief beeindrucken. Java bietet dazu sowohl Highlevel APIs wie Java3D an aber auch Lowlevel APIs wie OpenGL JOGL an. Der Unterschied zwischen beiden ist die Herangehensweise: OpenGL ist vergleichbar mit einem Assembler für Graphik. Es kennt nur Punkte, Linien, Dreiecke und einfache Polygone sowie Vektoren und Matrizen für die Transformationen. Man ist gezwungen alles von Hand zu machen, dafür ist die Anwendung aber auch sehr performant. Es ist sogar möglich die Graphikhardware direkt mittels Vertex- und Pixelshadern zu programmieren. Java3D benutzt dagegen einen Szenengraphen zur Organisation der Szene. Mit dieser baumartigen Struktur kann man sehr einfach und übersichtlich neue Körper, Sounds oder Animationen in die Szene integrieren und miteinander interagieren lassen. Die Highlevel APIs verwenden die Lowlevel APIs zum Rendering.



Download

Bearbeiten

Bevor Sie mit Java3D arbeiten können, müssen Sie dieses herunterladen: http://java3d.java.net/binary-builds.html

Am besten laden Sie sich das ZIP-Archiv herunter und entpacken Sie es in Ihren Classpath.

Grundlegendes

Bearbeiten

Java3D ist eine Highlevel 3D-Graphik-API und wurde 1997 von SUN, HP und SGI initiiert. Im Gegensatz zu OpenGL und DirectX verwendet Java3D einen Szenengraphen zur Verwaltung der 3D-Objekte und benutzt lowlevel Rendering-APIs, wie OpenGL und DirectX, zum Darstellen der 3D-Objekte. Vom Szenengraphen her ähnelt Java3D stark VRML2, allerdings sind die Events zwischen den Objekten nicht als ROUTES sondern als spezielle Java-Events realisiert. Wie VRML2 ist Java3D nicht nur in der Lage zu 3D-Objekte darzustellen, sondern kann auch Sound, Verhalten, Animationen und unkonventionelle Eingabe- und Ausgabegeräte managen. 3D-Objekte müssen nicht in Java3D Geometrieformat abgelegt sein - es gibt auch eine ganze Anzahl von Loadern für verschiedene 3D-Formate wie OBJ, WRL, 3DS oder DXF.

Java3D wird nicht mit den offiziellen Java SDK- oder JRE-Releases ausgeliefert. Aus diesem Grund muß Java3D manuell ins Java-System eingebunden werden.

Das Programmieren in Java3D ist erst einmal gar nicht so einfach, da man verschiedene Dinge braucht um überhaupt etwas angezeigt zu bekommen:

  • einen leeren Szenengraphen (VirtualUniverse)
  • ein Fenster zum Rendern (Frame + 3D-Canvas)
  • ein Objekt zum Anzeigen (z.B. eine farbige Box)
  • eine Lichtquelle - damit man die Kiste überhaupt sehen kann (ist in unserem Beispiel schon in die ColorBox integriert)
  • einen Betrachter mit Standort und Blickrichtung (Sie sind ja jetzt in 3D)
  • eine BoundingSphere (Hüllkugel), da Java3D so auf Optimierung aus ist, daß nur Dinge, die in der aktuellen Szene (sprich: der BoundingSphere) enthalten sind überhaupt gezeichnet werden. Andere Optimierungsschritte sind z.B. das Setzen der Capability-Bits in der Transformgruppe, da zur Laufzeit nur Dinge verändert werden können (z.B. mittels Animation), deren Capability-Bits vorher gesetzt wurden.

Man teilt die Bestandteile nun in Content-Branch (alles was angezeigt werden soll, sprich: Lichtquelle und Box) und View-Branch (die Beschreibung eines virtuellen Betrachters und die Umsetzung auf den Rendercanvas und den Frame) ein. Das hört sich zuerst einmal relativ aufwendig an, ermöglicht aber später die einfache und elegante Erweiterung um mehrere "Levels" als Content-Braches und mehrere "Spieler" als View-Branches.

Das erste Beispiel besteht nun aus drei Teilen:

  • createScenegraph(): baut den Content-Branch auf (Colorbox + Animation + Transformation)
  • HelloWorld: Konstruktor - baut den View-Branch (vordefiniertes SimpleUniverse) auf und integriert den Content-Branch aus createSceneGraph()
  • main-Methode: ruft den Konstruktor HelloWorld() im Fenster MainFrame auf (dieses läßt sich als Applet oder als Application betreiben)

Ein erstes Beispiel (eine bunte Kiste mit eingebauter Lichtquelle) könnte nun so aussehen:

import java.applet.Applet;
import java.awt.BorderLayout;
import java.awt.event.*;
import java.awt.GraphicsConfiguration;
import com.sun.j3d.utils.applet.MainFrame;
import com.sun.j3d.utils.geometry.ColorCube;
import com.sun.j3d.utils.universe.*;
import javax.media.j3d.*;
import javax.vecmath.*;
 
public class HelloUniverse extends Applet
{
  public BranchGroup createSceneGraph()
  {
      BranchGroup objRoot = new BranchGroup();
      TransformGroup objTrans = new TransformGroup();
      objTrans.setCapability( TransformGroup.ALLOW_TRANSFORM_WRITE );
      objRoot.addChild( objTrans );
      objTrans.addChild( new ColorCube( 1.0 ));
      Transform3D yAxis = new Transform3D();
      Alpha rotationAlpha = new Alpha( -1, 4000 );
      RotationInterpolator rotator= new RotationInterpolator(rotationAlpha, objTrans, yAxis, 0.0f, (float) Math.PI*2.0f);
      BoundingSphere bounds = new BoundingSphere( new Point3d( 0.0,0.0,0.0 ), 100.0 );
      rotator.setSchedulingBounds( bounds );
      objRoot.addChild( rotator );
      objRoot.compile();
      return objRoot;
  }
  public HelloUniverse()
  {
      setLayout(new BorderLayout());
      GraphicsConfiguration config = SimpleUniverse.getPreferredConfiguration();
      Canvas3D c=new Canvas3D( config );
      add( "Center", c );
      SimpleUniverse u=new SimpleUniverse( c );
      u.getViewingPlatform().setNominalViewingTransform();
      BranchGroup scene=createSceneGraph();
      u.addBranchGraph( scene );
  }
  public static void main( String[] args )
  {
      new MainFrame( new HelloUniverse(), 600, 600 );
  }
}

Nun ein Beispiel mit Mausnavigation und einer weissen Box, die man mit der Maus drehen kann. Dazu wird im Unterschied zum ersten Beispiel einfach die Methode createSceneGraph() angepaßt. Damit Java3D das MouseBehavior und die Box kennt muß es noch oben in die import-Liste eingetragen werden mittels:

  import com.sun.j3d.utils.behaviors.mouse.MouseRotate;  //  Mousebehavior
  import com.sun.j3d.utils.geometry.*;                   //  Box
  ...
public BranchGroup createSceneGraph()
{
    BranchGroup objRoot = new BranchGroup();
    Transform3D zTrans = new Transform3D( );
    zTrans.set( new Vector3f( 0.0f,0.0f,-10.0f ) );
    TransformGroup objTrans = new TransformGroup( zTrans  );
    objRoot.addChild( objTrans );
      // Box erzeugen und einhängen 
    Box prim      = new Box();
    objTrans.addChild( prim );
      // BoundingSpere für Mousebehavior und Lichtquelle erzeugen
    BoundingSphere bounds = new BoundingSphere( new Point3d(0.0,0.0,0.0), 100.0 );
      // Mouse-Rotation-Behavior ersetzen und in Transformgruppe einhängen + Capabilitybits setzen
    MouseRotate behavior = new MouseRotate( objTrans );
    objTrans.addChild( behavior );
    behavior.setSchedulingBounds( bounds );
    objTrans.setCapability( TransformGroup.ALLOW_TRANSFORM_WRITE );
    objTrans.setCapability( TransformGroup.ALLOW_TRANSFORM_READ );
      //Lichtquelle erzeugen und in Szenengraphen hängen
    Color3f lColor1 = new Color3f(1.0f, 1.0f, 1.0f);
    Vector3f lDir1  = new Vector3f( 0f, 0f, -1.0f);
    DirectionalLight lgt1 = new DirectionalLight( lColor1, lDir1 );
    lgt1.setInfluencingBounds( bounds );
    objRoot.addChild( lgt1 );
      // Content-Branch optimieren und zurückgeben
    objRoot.compile();
    return objRoot;
  }



Einleitung

Bearbeiten

  OpenGL
  JOGL

Grundlegendes

Bearbeiten

JOGL wird nicht mit den offiziellen Java SDK- oder JRE-Releases ausgeliefert. Aus diesem Grund muss JOGL manuell ins Java-System eingebunden werden.

Download der Jogl-Bibliothek

Bearbeiten

Auf der JOGL-Website finden sich für verschiedene Betriebssysteme und Prozessoren vorkompilierte jogl-Bibliotheken. Zusätzlich stehen dort auch die API-Manuals, sowie die Quellcode-Dateien zum Download bereit.

Entpacken

Bearbeiten

Entpacken der plattformabhängigen Datei, z.B.: unzip jogl-x.y.z-linux-i586.zip

Einbinden

Bearbeiten

Um diese Bibliotheken nun in das Java-System einzubinden gibt es mehrere Möglichkeiten. Prinzipiell gilt:

  • Die Dateien jogl.jar und gluegen-rt.jar müssen in den CLASSPATH eingebunden werden.
  • Die nativen Bibliotheksdateien (libjogl_cg.so, libjogl.so, libjogl_awt.so ) müssen im java.library.path gefunden werden, sonst hagelt es bei der Programmausführung nur Laufzeitfehler.

Wie sie das bewerkstelligen, bleibt ihnen überlassen. In manchen Tutorials wird empfohlen, die Dateien in die entsprechenden Java-Unterverzeichnisse zu kopieren. Davon wird aber im aktuellen Jogl-User's Guide dringend abgeraten. Nachfolgend wird eine Kommandozeilen-Variante gezeigt:

  1. Die JOGL-Bibliotheksdateien werden in das Projektverzeichnis kopiert.
  2. Kompilierung im Projektverzeichnis: javac -classpath jogl.jar:gluegen-rt.jar JoglTest.java
  3. Programmstart im Projektverzeichnis: java -classpath jogl.jar:gluegen-rt.jar: -Djava.library.path=. JoglTest

Die nachfolgenden Beispiele basieren auf dem JOGL-Release-Candidate 1.1.0-rc3. Frühere JOGL-Versionen wiesen u.a. eine andere Packagestruktur auf.

Ein simples Programmbeispiel

Bearbeiten
import javax.swing.*;
import javax.media.opengl.*;

public class JoglTest extends JFrame 
{
  GLCanvas canvas;	

  public JoglTest()
  {
    GLCapabilities cap = new GLCapabilities();
    //frueher: canvas = GLDrawableFactory.getFactory().createGLCanvas(cap);
    //jetzt:
    canvas = new GLCanvas(cap);

    canvas.addGLEventListener(new SceneView());
   
    getContentPane().add(canvas);
   
    setTitle("Simples Jogl-Beispiel");
    setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    setSize(300,200);
    setVisible(true);    
  }
 
  class SceneView implements GLEventListener
  {
    public void init(GLAutoDrawable arg0) 
    {
      GL gl = arg0.getGL();
  
      gl.glClearColor(1.0f, 1.0f, 1.0f, 0.0f);
  	  	  
      gl.glMatrixMode(GL.GL_PROJECTION);
      gl.glOrtho(-100, 100, -100, 100, -100, 100);
  
      gl.glMatrixMode(GL.GL_MODELVIEW);
    }

    public void display(GLAutoDrawable arg0) 
    {
      GL gl = arg0.getGL();
           
      gl.glClear(GL.GL_COLOR_BUFFER_BIT);
      gl.glColor3f(1.0f, 0.0f, 0.0f);
      gl.glRectf(-50.0f, -50.0f, 50.0f, 50.0f);  
    }

    public void reshape(GLAutoDrawable arg0, int arg1, int arg2, int arg3, int arg4) 
    {
    }

    public void displayChanged(GLAutoDrawable arg0, boolean arg1, boolean arg2) 
    {
    }
  }

  public static void main(String args[]) 
  {
    new JoglTest();   
  }
}

 

Ein einfaches 3D-Beispiel

Bearbeiten
import javax.swing.*;
import javax.media.opengl.*;
import com.sun.opengl.util.GLUT;


public class JoglTest extends JFrame 
{
  GLCanvas canvas;      

  public JoglTest()
  {
    GLCapabilities cap = new GLCapabilities();
    
    canvas = new GLCanvas(cap);

    canvas.addGLEventListener(new SceneView());
   
    getContentPane().add(canvas);
   
    setTitle("Simples Jogl-Beispiel");
    setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    setSize(400, 400);
    setVisible(true);    
  }
 
  class SceneView implements GLEventListener
  {
    public void init(GLAutoDrawable arg0) 
    {
      GL gl = arg0.getGL();
      float l_position[] = {100.0f, 100.0f, 200.0f, 1.0f};
  
      gl.glEnable(GL.GL_LIGHTING);
      gl.glEnable(GL.GL_LIGHT0);
      gl.glEnable(GL.GL_COLOR_MATERIAL);
      gl.glEnable(GL.GL_DEPTH_TEST);
      gl.glEnable(GL.GL_NORMALIZE);
      gl.glEnable(GL.GL_POLYGON_SMOOTH);
      gl.glLightfv(GL.GL_LIGHT0, GL.GL_POSITION, l_position, 0);  
  
      gl.glClearColor(0.7f, 0.7f, 0.7f, 0.0f);
                  
      gl.glMatrixMode(GL.GL_PROJECTION);
      gl.glOrtho(-100, 100, -100, 100, -100, 100);
  
      gl.glMatrixMode(GL.GL_MODELVIEW);
      gl.glRotatef(35.0f, 1.0f, 0.0f, 0.0f);    // Rotation um die x-Achse
      gl.glRotatef(-25.0f, 0.0f, 1.0f, 0.0f);   // Rotation um die y-Achse
    }


    public void display(GLAutoDrawable arg0) 
    {
      GL gl = arg0.getGL();
      GLUT glut =  new GLUT();
           
      gl.glClear(GL.GL_COLOR_BUFFER_BIT | GL.GL_DEPTH_BUFFER_BIT);
      gl.glColor3f(0.2f, 1.0f, 0.3f);
      glut.glutSolidTeapot( 50.0 ) ;
    }


    public void reshape(GLAutoDrawable arg0, int arg1, int arg2, int arg3, int arg4) 
    {
    }


    public void displayChanged(GLAutoDrawable arg0, boolean arg1, boolean arg2) 
    {
    }
  }


  public static void main(String args[]) 
  {
    new JoglTest();   
  }
}

 

Weitere OpenGL-Bindings für Java

Bearbeiten



Überblick

Bearbeiten

Das Sound-Paket von Java besteht aus zwei grundsätzlich verschiedenen Komponenten.

  • javax.sound.sampled – Hier sind die Klassen zur Bearbeitung, Aufnahme und Wiedergabe von Audiodaten gruppiert, die aus einzelnen Abtastwerten bestehen.
  • MIDI-Daten – In diesem Paket befinden sich die Klassen, die für den Input/Output, die Sequenzierung und die Synthese von MIDI-Daten nötig sind.

Einleitung

Bearbeiten

Darstellung im Computer

Bearbeiten

Zuerst einmal muss man sich die Frage stellen, wie Schallwellen überhaupt im Computer dargestellt werden sollen. Hilfreich dafür ist ein einfaches Beispiel mit Mikrofonen. Schall besteht aus Druckwellen, die sich durch die Luft fortpflanzen. Die einfachste Möglichkeit ist also sicherlich die Aufzeichnung der Schwingungskurven, die ein Teilchen erfährt, wenn es den Schallwellen ausgesetzt wird. Mikrofone übersetzen nun diese Luftschwingungen in elektrische Schwingungen. Bis hierhin ist aber noch immer nichts digitales geschehen. Computer können aber nur digitale, also diskrete Daten bearbeiten. Mit den analogen Schwingungen können sie nichts anfangen. Deshalb werden diese Schwingungen nun diskretisiert.

Nun gibt es mehrere Fragen, die es zu beantworten gilt:

  • Wie exakt, also mit welcher Präzision sollen die Amplituden der Schwingungen aufgezeichnet werden?
  • Wie oft pro Sekunde soll überhaupt ein Wert aufgezeichnet werden?
  • Mit wievielen Mikrofonen soll die Geräuschwelle aufgezeichnet werden?

Hier kommen nun die Begriffe Samplerate, Samplesize und Kanäle ins Spiel. Die Samplerate gibt an, wieviele Signale pro Sekunde abgetastet werden sollen, die Einheit ist somit Hertz. Die Samplesize definiert den Wertebereich und damit Präzision der Abtastwerte, sie wird in Bits gemessen. Die Anzahl der Kanäle gibt an, wieviele Tonspuren simultan aufgezeichnet werden. Für eine handelsübliche CD sind beispielsweise 44100 Abtastwerte pro Sekunde, 16 Bit Samplesize und zwei Kanäle, also stereo üblich. Bei DVDs beispielsweise wird oft mit 20 oder sogar 24 Bit Samplesize bei einer Samplerate von 96 oder 192 kHz gearbeitet. Wegen der geringen Übertragungsrate sind diese hohe Qualitäten für die Übertragung per Telefon nicht im Geringsten geeignet.

Anfallende Datenmengen

Bearbeiten

Für unkomprimierte Audiodaten lässt sich die benötigte Datenmenge pro Sekunde ganz einfach berechnen:

Samplerate SR, Samplesize S und Kanäle K

Datenmenge in Bytes =  

Schauen wir uns also mal ein kleines Beispiel an. Wir haben einen üblichen Song von 3 Minuten Länge mit CD-Qualität, das sind 180 Sekunden. Dann benötigen wir bereits:

 

Dies entspricht mehr als 31 Megabytes. Dies veranschaulicht, warum Kompressionsverfahren so wichtig sind.


Darstellung im Computer

Bearbeiten

Zuerst einmal muss man sich die Frage stellen, wie Schallwellen überhaupt im Computer dargestellt werden sollen. Hilfreich dafür ist ein einfaches Beispiel mit Mikrofonen. Schall besteht aus Druckwellen, die sich durch die Luft fortpflanzen. Die einfachste Möglichkeit ist also sicherlich die Aufzeichnung der Schwingungskurven, die ein Teilchen erfährt, wenn es den Schallwellen ausgesetzt wird. Mikrofone übersetzen nun diese Luftschwingungen in elektrische Schwingungen. Bis hierhin ist aber noch immer nichts digitales geschehen. Computer können aber nur digitale, also diskrete Daten bearbeiten. Mit den analogen Schwingungen können sie nichts anfangen. Deshalb werden diese Schwingungen nun diskretisiert.

Nun gibt es mehrere Fragen, die es zu beantworten gilt:

  • Wie exakt, also mit welcher Präzision sollen die Amplituden der Schwingungen aufgezeichnet werden?
  • Wie oft pro Sekunde soll überhaupt ein Wert aufgezeichnet werden?
  • Mit wievielen Mikrofonen soll die Geräuschwelle aufgezeichnet werden?

Hier kommen nun die Begriffe Samplerate, Samplesize und Kanäle ins Spiel. Die Samplerate gibt an, wieviele Signale pro Sekunde abgetastet werden sollen, die Einheit ist somit Hertz. Die Samplesize definiert den Wertebereich und damit Präzision der Abtastwerte, sie wird in Bits gemessen. Die Anzahl der Kanäle gibt an, wieviele Tonspuren simultan aufgezeichnet werden. Für eine handelsübliche CD sind beispielsweise 44100 Abtastwerte pro Sekunde, 16 Bit Samplesize und zwei Kanäle, also stereo üblich. Bei DVDs beispielsweise wird oft mit 20 oder sogar 24 Bit Samplesize bei einer Samplerate von 96 oder 192 kHz gearbeitet. Wegen der geringen Übertragungsrate sind diese hohe Qualitäten für die Übertragung per Telefon nicht im Geringsten geeignet.

Anfallende Datenmengen

Bearbeiten

Für unkomprimierte Audiodaten lässt sich die benötigte Datenmenge pro Sekunde ganz einfach berechnen:

Samplerate SR, Samplesize S und Kanäle K

Datenmenge in Bytes =  

Schauen wir uns also mal ein kleines Beispiel an. Wir haben einen üblichen Song von 3 Minuten Länge mit CD-Qualität, das sind 180 Sekunden. Dann benötigen wir bereits:

 

Dies entspricht mehr als 31 Megabytes. Dies veranschaulicht, warum Kompressionsverfahren so wichtig sind.


Einleitung

Bearbeiten

Um zu verstehen, warum das Paket javax.sound.sampled auf den ersten Blick so seltsam aufgebaut wurde, muss man sich erst einmal die Zielsetzung vor Augen halten:

  • Einheitliche Schnittstelle
  • Erweiterbarkeit

Das Problem bei der Entwicklung dieses Paketes war, dass auf die Verschiedenheit der Systeme, auf denen die Javaprogramme später laufen würden, unbedingt geachtet werden musste. Z.B. besitzen nicht alle Soundkarten neben der normalen Ausgabe auch noch einen Line Out. Außerdem werden nicht alle Audioformate nativ von dem zur Soundkarte gehörenden Mixer unterstützt. Einige Soundkarten lassen die gleichzeitige Wiedergabe mehrerer Spuren zu, andere wiederum nicht. Dies sind nur einige wenige Beispiele, es gibt noch sehr viele mehr. Trotzdem muss eine einheitliche Schnittstelle zum Soundsystem angeboten werden, um die API plattformunabhängig zu halten. Ein weiterer Punkt ist die Erweiterbarkeit des Paketes. Mit speziellen Klassen sollte es möglich sein, dass auch andere Formate unterstützt werden. Die Dienste, die diese neue Klassen anbieten, müssen natürlich für die Javaprogramme verfügbar sein, ohne dass diese eigentlich davon wissen. Erreicht wurde das mit dem Unterpaket javax.sound.sampled.spi, wobei SPI für Service Provider Interface steht.

Die Klasse AudioSystem

Bearbeiten

Zentrale Anlaufstelle für alle Belange, die mit dem Einlesen oder Schreiben von Datenströmen mit Sounddaten zu tun haben, ist die Klasse AudioSystem. Sämtliche Methoden in AudioSystem sind als statisch deklariert. Die Wichtigsten sind:

  • AudioFileFormat getAudioFileFormat(File file);
  • AudioInputStream getAudioInputStream(URL url);
  • AudioInputStream getAudioInputStream(AudioFormat targetFormat, AudioInputStream sourceStream);

Wenn es darum geht, mit Datenquellen und Datensenken zu arbeiten, ist die Vorgehensweise meistens wie folgt:

  1. Das Java-Programm erzeugt ein Line.Info Objekt, das beschreibt, was genau das Programm nun tun möchte, also ob eine Datensenke oder eine Datenquelle erwünscht ist und ob der Datenfluss mit einem Port oder einem bestimmten Mixer verbunden werden soll. Zusätzlich können noch erwünschte Audioformate und Puffergrößen spezifiziert werden. Es müssen nicht alle Informationen angegeben werden.
  2. Als nächstes wird das AudioSystem gefragt, ob es diese Anforderungen erfüllen kann.
  3. Zuletzt wird dann im positiven Falle vom AudioSystem der entsprechende Datenfluss abgerufen. Mit diesem kann nun gearbeitet werden.

Ziel war es also, dass das Java-Programm zum Zeitpunkt des Kompilierens nicht wissen muss, mit welchen Audioformaten es während seiner Laufzeit konfrontiert wird, auf welchen Plattformen es läuft und welche Audiokapatibilitäten diese zur Verfügung stellt.

Das Interface Line

Bearbeiten

Ein ganz zentraler Punkt in diesem Paket ist das Interface Line. Eine Line repräsentiert alle Datenflüsse, die im AudioSystem auftreten. Es kann sich dabei um die Wiedergabe, Aufnahme, verschiedene Ports oder Pipes in den Mixer hinein oder heraus handeln. Mit diesen Audiodatenflüssen können ganz grundlegende Dinge getan werden. Bevor man damit arbeiten kann, muss man sie öffnen. Sobald dieses geschehen ist, werden Systemresourcen belegt. Nach der Benutzung sollte die Line auch wieder geschlossen werden, damit die Systemresourcen freigegeben werden können. Es ist möglich die Controls einer Line abzufragen. Dies sind Objekte, welche die Audiodaten in gewisser Weise beeinflussen. Beispielsweise gehören dazu das Verändern der Balance, der Lautstärke, der allgemeinen Lautstärke oder auch die Samplerate. Nicht alle Controls sind bei allen Lines verfügbar.

Das Subinterface Port

Bearbeiten

Ports repräsentieren die typischen Ein- und Ausgabemöglichkeiten der Soundkarte. Zu den Wiedergabeports gehören zum Beispiel Speakers, Headset oder Line Output. Zu den Aufnahmeports gehören beispielsweise das Mikrofon, das CD-Laufwerk oder der Line Input. Auch hier gilt wieder, dass nicht auf allen Systemen alle Ports vorhanden sind.

Vollständig implementiert wurden Ports erst im J2SDK 5.0




Drucken mit Java

Bearbeiten

Seit Java1.2 ist es relativ einfach mit Java einen angeschlossenen Drucker anzusprechen. Dazu benutzt man, ähnlich wie bei der Verarbeitung von Bildern, die Klasse Toolkit. Zuerst wäre es ja einmal interessant die aktuellen Parameter des angeschlossenen Druckers zu ermitteln. Dazu holt man das Default-Toolkit, erzeugt einen dummy-Frame und ermittelt die Werte des Druckers. Dies könnte so geschehen:

import java.awt.*;

import javax.swing.UIManager;

public class PrinterInfo1 {
	public static void main( String[] args ) {
		try {
			// Natives Look&Feel verwenden
			UIManager.setLookAndFeel( UIManager.getSystemLookAndFeelClassName() );
		} catch ( Exception e ) {}
		
		Toolkit tk = Toolkit.getDefaultToolkit();
		// Dialog mit Druckeroptionen erstellen
		PrintJob pj = tk.getPrintJob( new Frame(), "", null );
		
		if ( pj != null ) {
			// Aufloesung holen
			int aufloesung = pj.getPageResolution();
			Dimension dim  = pj.getPageDimension();
			
			System.out.println(
				"Druckerauflösung (DPI): " + aufloesung + "\n" +
				"breite : " + dim.width + "\n" +
				"hoehe  : " + dim.height + "\n" +
				"Pixel pro Seite: " + (aufloesung * dim.width * dim.height)
			);
		} else {
			System.out.println( "Ich kann leider keinen angeschlosssenen Drucker finden ..." );
		}
	}
}

Text auf dem Drucker ausgeben

Bearbeiten

Nachdem wir nun gesehen haben wie man ermittelt ob ein Drucker angeschlossen ist und welche physikalischen Parameter er besitzt möchten wir als nächstes ein paar Zeilen auf dem Drucker selbst ausgeben:


 import java.awt.Toolkit;
 class PrinterInfo2
 {
   public static void main( String args[] )
   {
     Toolkit tk = Toolkit.getDefaultToolkit();
     PrintJob pj = tk.getPrintJob( new Frame(), "", null );  //dummy-Frame erzeugen
     if ( pj != null )
     {  // FileOutputStream in PrintWriter packen
       PrintWriter pw = new PrintWriter( new FileOutputStream( "PRN:" ));
       pw.println( "Hallo lieber Drucker" );
       pw.println( "... und das ist Zeile2 ..." );
       pw.close();  // resource freigeben
     }
  }
 }



Das Paket java.util enthält viele Hilfsklassen für den alltäglichen Gebrauch. Von generischen Datenstrukturen wie Vector, Stack und Hashtabelle, über Kalender und Datum bis hin zum Collections Framework, der Internationalisierung und einem Bit-Array bietet das Paket alles, was man sich für die tägliche Arbeit wünscht. Auch ein Timer ist vorhanden.

In diesem Abschnitt werden mehrere Themen ausführlicher beschrieben, die für alltägliche Programmieraufgaben von Nutzen sein können. Hier folgt nun ein Überblick.

Überblick über die behandelten Themen

Bearbeiten



ACHTUNG – veraltet (deprecated). Laut Dokumentation der Java API sollte stattdessen die Methode split(Regular Expression) der Klasse String genutzt werden.

Mit der Klasse java.util.StringTokenizer kann eine Zeichenkette in einzelne Elemente zerlegt werden, indem 1-n Trennzeichen übergeben werden.

import java.util.StringTokenizer;
 
 public class Test {
 
   public static void main(String[] args) {
     String s = "Hallo;du;schoene;Welt"; // einzelne "Tokens" (Elemente) die 
                                         // durch ein Trennzeichen (hier ";") voneinander getrennt sind
        
     StringTokenizer st = new StringTokenizer(s, ";"); // trenne den String durch das Trennzeichen ;
        
     while (st.hasMoreTokens()) { 
        System.out.println(st.nextToken()); // Token für Token ausgeben
     }
   }
 }

Diese Klasse wird folgende Ausgabe erzeugen:

Hallo
du
schoene
Welt

Ein gleiches Verhalten kann seit Java 1.4 durch die Regulären Ausdrücke mit Hilfe der Methode split (String regex) direkt auf dem String erreicht werden. Hierdurch wird ein Array mit Strings zurückgeliefert.




Für die Datumsverarbeitung gibt es in Java mehrere Datentypen. Hierzu zählen

  • java.util.Date
  • java.util.Calendar
  • java.util.GregorianCalendar

sowie für unsere Datenbankfreunde

  • java.sql.Date

Datum und Uhrzeit

Bearbeiten

Der Umgang mit Datum und Uhrzeit ist über die Klassen "Date" und "Calendar" geregelt.

Date (veraltet)

Bearbeiten

Die aktuelle Zeit und das aktuelle Datum kann man z.B. mit Hilfe der Date-Klasse so erfragen (Achtung: dieses Vorgehen ist deprecated, d.h. veraltet - funktioniert aber trotzdem und ist recht einfach zu verstehen. Die neue Herangehensweise sieht man im nächsten Beispiel):

Date dat = new Date();
  System.out.println( "Datum  :  "+dat.getDay()+"."+dat.getMonth()+"."+dat.getYear() );
  System.out.println( "Uhrzeit: "+dat.getHours()+":"+dat.getMinutes()+":"+dat.getSeconds() );

Calendar (aktuell)

Bearbeiten

Nach der neuen Methode verwendet man jetzt die Klasse Calendar zur Ermittlung von Datum und Zeit.

Calendar cal = Calendar.getInstance ();
    
  // Die Monate werden mit 0 (= Januar) beginnend gezaehlt!
  // (Die Tage im Monat beginnen dagegen mit 1)

  System.out.println( "Datum: " + cal.get( Calendar.DAY_OF_MONTH ) +
                      "." + (cal.get( Calendar.MONTH ) + 1 ) +
                      "." + cal.get( Calendar.YEAR ) );

  System.out.println( "Uhrzeit: " + cal.get( Calendar.HOUR_OF_DAY ) + ":" +
                      cal.get( Calendar.MINUTE ) + ":" +
                      cal.get( Calendar.SECOND ) + ":" +
                      cal.get( Calendar.MILLISECOND ) );

Dieser Code gibt folgende Daten aus (Hier wurde das Programm am 9.8.2011 um 16:23:2:362 geöffnet):

Datum: 9.8.2011
Uhrzeit: 16:23:2:362




Random ist eine Klasse zur Erzeugung von Pseudozufallszahlen. Die statische Funktion Math.random() macht z.B. direkt Gebrauch davon.

Random erzeugt eine Pseudo-Zufallszahl, d.h. keine richtige Zufallszahl. Der Unterschied liegt darin, dass zwei Random-Instanzen, wenn sie direkt gleichzeitig gestartet werden, genau die gleichen Zufallszahlen erzeugen, was bei "echten" Zufallszahlen nicht der Fall wäre. Um dieses Manko abzuschalten, gibt es eine abgeleitete Klasse SecureRandom, die "echte" Zufallszahlen mit anderen numerischen Algorithmen erstellt als Random. Random benutzt einen 48-Bit großen Startwert ("seed") zur Erzeugung der Zufallszahlen. Die Zufallszahlen (z) liegen immer im Bereich  .

Ein einfaches Beispiel zur einfachen Erzeugung von Lottozahlen – ein Array aus 10 Integern wird mit Math.random() initialisiert:

  final int MAX = 10;                                    // Konstante für Feldgroesse
  int daten[] = new int[MAX];                            // integer-Feld erzeugen
  for (int i = 0; i < MAX; i++){                          // Schleife über alle Elemente
    daten[i] = (int)Math.floor((Math.random() * 49) + 1);}      // Zufallszahl zuweisen  [1..49]

Da Math.random eine Zahl kleiner 1 erzeugt, muss man, um eine Zahl zwischen 1 und 49 zu erhalten, das Ergebnis der Funktion Math.random mit 49 multiplizieren und diese Zahl dann um 1 vergrößern.

Ein einfaches Beispiel zur Erzeugung von Lottozahlen mit Hilfe der Random-Klasse

  public static final int MAX = 10;  // Konstante für Feldgroesse
  int daten[] = new int[MAX];      // integer-Feld erzeugen
  Random rand = new Random();
  for( int i=0; i<MAX; i++ )         // Schleife über alle Elemente
  {     
    daten[i] = rand.nextInt(49) + 1;
  }




Für die Datenkompression stellt die J2SE eigene Pakete bereit:

  • java.util.zip für die Verarbeitung von GZ und ZIP
  • java.util.jar für die Verarbeitung von JAR

sinnvolle Vorkenntnisse

Bearbeiten

Sie sollten das Prinzip der Streams verstanden haben.



Das Paket java.util bietet mit dem Collection Framework einige interessante Klassen, die von der Funktionalität ähnlich arbeiten wie die Standard Template Library (STL) von C++. Dazu gehören Vector, Array, Sets, Maps, abstrakte Datentypen und Iteratoren. Ab Java1.5 (Java5) sind die Collections auch generisch, d.h. die Collections arbeiten mit Generics.

In den folgenden Abschnitten werden Beispiele für die Anwendungen dieser Datenstrukturen und auch die Vorteile der Generizität aufgezeigt.

Unter einem Vector kann man sich eine Liste vorstellen, der beliebig viele Objekte hinzugefügt werden können. Dies ist der gravierende Vorteil gegenüber einem Array, bei dem man zur Laufzeit eine fixe Länge zuweisen muss. Ist aber die Anzahl der Elemente einer Liste von komplexen Bedingungen abhängig, so kann die Berechnung der Array-Länge schwierig werden. In solchen Fällen ist es ratsam, einen Vector zu verwenden.

Im folgenden Beispiel wird abhängig von einer zufälligen Ja-Nein-Entscheidung ein String-Wert dem Vector hinzugefügt.

//Initialisierung
Vector beliebigeListe = new Vector();
Random zufallsGenerator = new Random();

//Befuellen der Liste
for(int i = 0; i < 30; i++) {
  if (zufallsGenerator.nextBoolean()) {
    beliebigeListe.add(new Integer(i));
  }
}

//Ausgabe aller Listenelemente
int elemAnz = beliebigeListe.size();
for(int i = 0; i < elemAnz; i++) {
  System.out.println(beliebigeListe.get(i).toString());
}

Lässt man diesen Code mehrmals laufen, so stellt man fest, dass der Vector unterschiedlich viele Elemente enthalten kann. Bei der Ausgabe der Vector-Elemente wurde die toString()-Methode verwendet, die in der Klasse Object definiert ist, und somit für jeden Typ einen String-Wert zurück gibt. Was geschieht aber, wenn man aber andere Operationen mit den Elementen durchführen will?

Wird wie gesagt nur ein Vector ohne die Anwendung von Generizität erstellt, so muss man bei der Bearbeitung der Elemente Cast-Operationen durchführen, d.h. man muss überprüfen, ob es sich bei einem Element, um den richtigen Typ für eine bestimmte Operation handelt.

Hier wird in einem ersten Beispiel der Umgang mit der Collection Set (Menge) demonstriert. Ein Set ist eine Datenstruktur, in der jedes Element nur einmal vorkommen darf, d.h. der Test obj.equals( obj2 ) darf nicht positiv sein. Hier wird nun beispielhaft der Umgang mit einem TreeSet demonstriert. Der Set wird aufgebaut und mit einem Iterator ausgelesen:

  final int MAX = 10;
  Set ss = new TreeSet();
  for( int i = 0; i < MAX; i++ ) 
  {
     System.out.println( "  - Integer(" + i + ")  speichern" );
     ss.add( new Integer( i ));
  }
  System.out.println();
  Iterator i = ss.iterator();
  while( i.hasNext() ) 
  {
     System.out.println( i.next() );
  }




Das Kapitel Test, Trace und Logging wurde von Sun Microsystems lange Zeit nicht mit der (vielleicht) notwendigen Sorgfalt betrachtet. So fanden erst mit dem JDK 1.4 entsprechende Spracherweiterungen und eine API Eingang in die Standard Edition.

Ersatzweise wurden Teile schon vorab aufgegriffen. Folgende Möglichkeiten stehen inzwischen zur Verfügung:




Mit Logging wird das Protokollieren eines Programmablaufes beschrieben.

Grundsätzlich reicht dazu schon ein System.out.println("Nachricht"); aus. Allerdings bringt es die weitere Entwicklung recht schnell mit sich, dass das Interesse an "alten" Debug-Nachrichten schwindet oder diese das Protokoll unleserlich oder überfrachtet erscheinen lassen. So ist der Entwickler gezwungen, diese nun überflüssigen Nachrichten entweder auszukommentieren oder zu löschen. Beides erfordert einigen Aufwand: In welcher Klasse wird die Nachricht erzeugt? Wird die Nachricht eventuell noch einmal benötigt? Usw.

Seit Java 1.4 wird die "Java Logging API" ausgeliefert. Damit ist es möglich, im Programmcode den einzelnen Nachrichten verschiedene Dringlichkeiten, so genannte Loglevels, zuzuordnen. Über diese Loglevels ist es möglich, zentral zu steuern, welche Nachrichten von welcher Klasse mit welcher Dringlichkeit mitzuprotokollieren sind. Für die "Java Logging API" werden diese Einstellungen in der logging.properties eingetragen. Die Original-Datei liegt im Java Installationsverzeichnis unterhalb des lib Verzeichnis.

# Der ConsoleHandler gibt die Nachrichten auf std.err aus
handlers= java.util.logging.ConsoleHandler

# Alternativ können weitere Handler hinzugenommen werden. Hier z.B. der Filehandler
#handlers= java.util.logging.FileHandler, java.util.logging.ConsoleHandler

# Festlegen des Standard Loglevels
.level= INFO
 
############################################################
# Handler specific properties.
# Describes specific configuration info for Handlers.
############################################################

# Die Nachrichten in eine Datei im Benutzerverzeichnis schreiben
java.util.logging.FileHandler.pattern = %h/java%u.log
java.util.logging.FileHandler.limit = 50000
java.util.logging.FileHandler.count = 1
java.util.logging.FileHandler.formatter = java.util.logging.XMLFormatter

# Zusätzlich zu den normalen Logleveln kann für jeden Handler noch ein eigener Filter 
# vergeben werden. Das ist nützlich wenn beispielsweise alle Nachrichten auf der Konsole ausgeben werden sollen
# aber nur ab INFO in das Logfile geschrieben werden soll.
java.util.logging.ConsoleHandler.level = ALL
java.util.logging.ConsoleHandler.formatter = java.util.logging.SimpleFormatter
 
############################################################
# Extraeinstellungen für einzelne Logger
############################################################

# Für einzelne Logger kann ein eigenes Loglevel festgelegt werden.
de.wikibooks.loggingapi.level = FINEST
de.wikibooks.assertions.level = SEVERE

Die Properties sind in einer Baumstruktur organisiert. Der erste Knoten also die Wurzel des Baumes ist ".level"; hier wird der Default-Loglevel festgelegt. Alle Kinder erben den Loglevel des Eltern-Knotens. Also in obigem Beispiel sind "de" und "de.wikibooks" auf dem Log-Level der Wurzel, erst "de.wikibooks.loggingapi" und dessen Kinder haben den Log-Level FINEST.

Falls nicht die Original-logging.properties-Datei benutzt werden soll, kann über die System-Property java.util.logging.config.file die stattdessen zu verwendende Datei angegeben werden (der Beispielpfad D:\pfad\zur\datei\logging.properties ist in Windows-Notation angegeben. Andere Betriebssystem verwenden ggf. eine andere Notation) :

-Djava.util.logging.config.file=D:\pfad\zur\datei\logging.properties

In die Java-Klassen müssen einige Imports eingefügt werden

import java.util.logging.Level;
import java.util.logging.Logger;

Dann muss der Logger erstellt werden

private static Logger jlog =  Logger.getLogger("de.wikibooks");
static {
	jlog.log(Level.SEVERE, "Nachricht");
}

Das Protokoll dieser Anweisungen sieht dann in etwa so aus.

11.01.2007 14:41:48 de.wikibooks <clinit>
SCHWERWIEGEND: Nachricht
Bearbeiten

Ein Werkzeug, um Java-Code automatisch mit Logging-Ausgaben zu versehen: Loggifier



Das Gegenstück zur Exception ist die Assertion (deutsch etwa: "Behauptung"). Mit ihr werden Programmierfehler aufgespürt und gemeldet, wenn das Programm in einem Testmodus gestartet wird. Sie werden deswegen meist an Stellen in der Programmierung eingebaut, an denen die Verarbeitung der Daten bereits erfolgt ist.

Um eine solche Überprüfung in ein Programm einzupflegen gibt es ein eigenes Schlüsselwort: assert. Es wird an einer beliebigen Stelle im Code eingebaut, gefolgt von einer Abfrage, die entweder true oder false zurückgibt. Falls diese Überprüfung positiv endet, wird das Programm fortgesetzt; andernfalls wird eine Exception ausgelöst, ein AssertionError, der eine Meldung auf dem Bildschirm ausgibt und das Programm üblicherweise an dieser Stelle beendet.

assert x>3;

An dieser Stelle wird im Programmcode die Variable x darauf getestet, dass sie einen Wert größer drei ergibt; andernfalls wird der Programmlauf mit einer Fehlermeldung abgebrochen. Weil eine solche Standard-Fehlermeldung oft zusätzliche Informationen liefern sollte, als es mit dem Standardtext der assert-Meldung möglich ist, kann man den Befehl um einen eigenen Fehlertext erweitern. Dazu wird der Teil hinter der Prüfung mit einem Doppelpunkt abgetrennt und dann die gewünschte Meldung angegeben:

assert x>3 : "Fehlerhafte Programmierung in der Funktion abc(): x < 4 und damit ungültig!";

So kann das Problem deutlich leichter eingekreist werden. Natürlich sind auch andere Fehlermeldungen denkbar, zum Beispiel die Ausgabe der Variablen selbst:

assert x>3 : x;

Auch wesentlich aufwendigere Prüfungen sind möglich, indem für die Tests beliebige Funktionen definiert werden, die als Ergebnis true oder false liefern. In diesem Fall muss aber sehr genau darauf geachtet werden, dass die Testfunktion keine Zustände des Programms beeinflusst, also Variablen innerhalb der Klasse verändert werden. Da die Prüfung im normalen Betrieb unterbleibt würde auch die Veränderung des Status der Klasse nicht vorgenommen werden, sprich: die Klasse reagiert mit Test anders als ohne Test. Solch einen Fehler aufzuspüren kostet jede Menge graue Haare.

Auf diese Weise kann man fehlerhaften Code in der Programmierung relativ leicht einkreisen. Wichtig ist, dass der Test hinter dem Schlüsselwort als Ergebnis true zurückliefert, wenn die getestete Programmierung ein korrektes Ergebnis liefert.

Aktivierung des Softwaretests

Bearbeiten

Die Verwendung des Schlüsselwortes assert hat einen zusätzlichen Vorteil: Sie kann im Code des Programmes auch dann stehen bleiben, wenn alle Softwaretests erfolgreich abgeschlossen wurden und die Software für die Benutzer freigegeben werden kann. Bei einem normalen Aufruf von Java wird das Schlüsselwort und alle damit verbundenen, möglicherweise zeitintensiven Tests bei der Ausführung komplett ignoriert. Die Tests müssen beim Aufruf des Programms ausdrücklich angeschaltet werden. Wahrscheinlich arbeiten Sie mit einer Entwicklungsumgebung; bitte suchen Sie sich hier die entsprechende Einstellung aus den Menüs heraus. Falls es keine solche Einstellung gibt oder falls Sie Java-Programme über die Kommandozeile starten, gehen Sie bitte wie folgt vor:

Unter Java Version 1.4 wird das Programm über den Compiler mit einer eigenen Option übersetzt, die die Überprüfungen einschaltet; der auszuliefernde Code muss später ohne diese Option erneut compiliert werden:

javac -source 1.4 WikiBooks

Ab Java Version 5 wird die Prüfung direkt im Interpreter vorgenommen, es bedarf also keines zusätzlichen Compilerlaufes. Das hierfür vorgesehene Flag lautet -ea (oder -enableassertations) zum Einschalten der Prüfung; dahinter wird der Name des Paketes angefügt:

java -ea WikiBooks

Eine einzelne Klasse innerhalb eines Paketes kann ebenfalls getestet werden, ohne dass dafür das komplette Paket mitgetestet wird. In diesem Fall wird hinter dem Kommandozeilenparameter -ea: (jetzt direkt hinter dem Parameter mit Doppelpunkt angefügt) der Pfad zur Klasse angegeben und dahinter das Paket genannt, in dem die Klasse zu finden ist:

java -ea:testumgebung.wikibooks.BeispielKlasse WikiBooks

Wenn in dem zu testenden Paket weitere Pakete stecken, die zu testen sind, ist auch das problemlos möglich. Dafür müssen hinter dem vollständigen Pfad nur drei Punkte angegeben werden:

java -ea:testumgebung.wikibooks...

Gelegentlich ist es sinnvoll, einzelne Teile des Paketes auszunehmen. Auch das ist mit einem Kommandozeilenparameter möglich. Er lautet -da bzw. -disableassertions und wird mit dem kompletten Pfad des auszuschließenden Teiles aufgerufen:

java -ea WikiBooks -da:testumgebung.wikibooks.BeispielKlasse

Selbstverständlich lässt sich auch hier wieder mit dem -ea-Parameter ein Teil des ausgeschlossenen Paketes doch wieder testen, indem der Parameter mit dem kompletten Pfad zu dem betreffenden Code angehängt wird.

Übungen

Bearbeiten

Alles verstanden? Zum Selbsttest gibt es hier einige Übungen. Die Lösungen dafür finden Sie hier.

1: Ein switch()-Test

Bearbeiten

In einem Programm finden Sie folgende switch-Anweisung:

switch (ja_nein) {
	case JA:
		this.doYes();
		break;
	case NEIN:
		this.doNo();
		break;
	default:
		assert false;

Obwohl in der assert-Anweisung kein Test vorgenommen wird, funktioniert diese Anweisung ordnungsgemäß: falls die Variable ja_nein einen anderen Wert als JA oder NEIN enthält wird das Programm mit einer Fehlermeldung abgebrochen.

1.1) Erklären Sie, warum die Anweisung auch ohne einen Test die Ausführung des Programms abbricht. 1.2) Welche Fehlermeldung wird der Programmierer zu Gesicht bekommen?

2: Do it yourself

Bearbeiten

Als Grundlage gegeben sei folgende Funktion, die einen Würfelwurf simulieren soll:

public int dice() {
	return (int)math.random() * 6;
}

Schreiben Sie eine Funktion public int diceTest(), die die Summe von 100 Würfen mit dieser Funktion ermittelt und zurückliefert. Stellen Sie dabei bei jedem Wurf sicher, dass eine Zahl zwischen 1 und 6 gewürfelt wird. Nutzen Sie dabei zwei Tests: Einen für Zahlen größer 6 und einen für Zahlen kleiner 1. Tatsächlich ist ein Programmabbruch durch eine der assert-Anweisungen zu erwarten. Welche?



Java ist eine der ersten Programmiersprachen welche von Grund auf mit Netzwerkunterstützung entwickelt wurde. Es waren auch die Applets welche Java zum Durchbruch in der Programmierwelt verhalfen.

Eine der wichtigsten Neuerungen in Java war, dass man einen sehr geringen Aufwand als Programmierer leisten muss, um eine Software netzwerkfähig zu machen. Die "Magie" in Java (oder konkret die Socketklasse) erledigt den Hauptteil der Arbeit. Jeder der schon einmal einen kleinen Server / Client in C geschrieben hat, kann davon berichten wie aufwendig dies sein kann. Im Gegensatz dazu bietet das java.net.* package alle Grundlagen, die es braucht um in 5 Minuten ein kleines Netzwerkbasierendes Programm zu schreiben.

Grundlagen der Netzwerkprogrammierung

Bearbeiten

In diesem Kapitel werden die Grundzüge netzwerkbasierter Software, Protokolle, Sockets, Streams etc. erläutert. – Hinweis: Es gibt Überschneidungen mit den folgenden Kapiteln.

Sockets für Clients

Bearbeiten

In diesem Kapitel wird beschrieben, wie ein Programm Verbindung mit einem Server aufnimmt und Daten mit diesem austauschen kann.

In den meisten Fällen geschieht die Datenübertragung zwischen Server und Host auf der Basis von TCP. Die java.net.Socket Klasse stellt uns eine solche TCP - Verbindung zur Verfügung. Es sind jedoch nicht alle Ports eines Betriebssystems für die Datenübertragung auf Basis von TCP ausgelegt. Um zu wissen auf welche Ports wir eine TCP Verbindung öffnen könnten, können wir uns mit Java einen eigenen kleinen Portscanner "basteln".

1  import java.net.*; 
2  import java.io.*; 
3  
4  public class PortScanner { 
5 
6  public static void main(String[] args) { 
7     
8    String host = "localhost"; 
9 
10   if (args.length > 0) { 
11    host = args[0]; 
12   } 
13   for (int i = 1; i < 1024; i++) { 
14     try { 
15       Socket s = new Socket(host, i); 
16       System.out.println("There is a server on port " + i + " of "  
17        + host); 
18     } 
19     catch (UnknownHostException e) { 
20       System.err.println(e); 
21       break; 
22     } 
23     catch (IOException e) { 
24       // must not be a server on this port 
25     } 
26   }
27  } 
28 }

Das Programm testet alle Ports im Bereich 1 - 1024. Ist auf einem dieser Ports ein TCP - fähiger Server am "lauschen", so wird eine entsprechende Meldung ausgegeben. Wichtig an diesem Programm ist Zeile 15. Hier wird einer der Konstruktoren der Klasse Socket gerufen.

Hier die schnellere Variante:

1 package PortScanner;
2
3 /*
4  * Der Portscanner ist ein gutes Beispiel für den sinnvollen Einsatz von Threads.
5  * Ansonsten müsste man bei jedem Port zu dem die Connection nicht hinhaut auf einen
6  * Timeout warten bevor man weitermacht. Dieser Timeout ist zwar nicht sehr lang,
7  * aber wenn man 1000 Ports testet lange genug.
8  */
9
10 public class PortscannerMain {
11	
12	public static void main(String[] args) {
13		String host = "localhost";
14		for (int i = 0; i < 1023; i++) {
15			Connthread curr = new Connthread(host, i);
16			Thread th = new Thread(curr);
17			th.start();
18			try {
19				Thread.sleep(1);
20			}
21			catch (InterruptedException ex) { }
22		}
23	}
24 }

1 package PortScanner;
2
3 import java.io.IOException;
4 import java.net.*;
5
6 public class Connthread implements Runnable {
7	int i;
8	String host;
9	public Connthread(String host, int i) {
10		this.i = i;
11		this.host = host;
12	}
13	 
14	public void run() {
15		try {
16			Socket target = new Socket(host, i);
17			System.err.println("Connected to " + host + " on Port " + i);
18			target.close();
19		}
20		catch (UnknownHostException ex) {
21			System.out.println("Unkown Host " + host);
22		}
23		catch (IOException ex) {System.out.println(i);};
24	}
25
26 }

Sockets für Server

Bearbeiten

In diesem Kapitel wird beschrieben wie man einen Server schreibt, welcher auf eingehende Verbindungen "hört" und auf die Anfragen antwortet.


Java Standard: Socket ServerSocket (java.net) UDP und TCP IP


Einleitung, Grundbegriffe

Bearbeiten
  • RMI

RMI ist die Abkürzung von 'Remote Method Invocation' und bedeutet 'Aufruf einer entfernten Methode' (i.a. über ein Netzwerk). Der Aufruf selbst ist transparent, d.h. der Nutzer merkt nichts davon, dass die Methode auf einem entfernten Rechner implementiert ist und dort abgearbeitet wird. Parameter und Ergebnis werden transparent zwischen Client und Server ausgetauscht. Im Hintergrund übernehmen 2 Klassen (genannt Skeleton/Stub) stellvertretend für Server/Client die Kommunikation.

Das hier beschriebene Beispiel setzt Java 1.5 oder höher voraus, damit entfällt die zuvor erforderliche Nutzung der 'Java-RMI-Tools'.

  • Anforderungen an die Applikation

Definiere ein Interface für das 'remote object' (im Beispiel 'Hello'), die von Server und Client gemeinsam benutzte Methode.

  • RMI-Server
1. Implementiere eine Instanz des 'remote object' ('HelloImpl', Name beliebig)
2. Exportiere die Instanz

das geschieht mit 'UnicastRemoteObject.exportObject' oder durch Erweiterung der Instanz mit 'UnicastRemoteObject'. Dadurch wird automatisch der Konstruktor von 'UnicastRemoteObject' aufgerufen, der den Export durchführt.

Durch den Export entsteht die Skeleton-Klasse, die Kommunikation und Datenaustausch mit dem Client durchführt.

3. Registry

Erzeuge eine Registry in der das 'remote object' mit der Methode 'registry.[re]bind' angemeldet wird. Dabei ist ein eindeutiger Name und ein Kommunikationsport (Standard 1099) festzulegen. Diesen Namen benutzt später die Stub-Klasse des Client (der Partner der Skeleton-Klasse des Servers), um die RMI durchzuführen.

Nach dieser Anmeldung ist der Server bereit, Client-Anfragen entgegen zu nehmen. Die Anfragen werden in der implementierten Klasse 'HelloImpl' behandelt.

  • RMI-Client

Zuerst verschafft man sich Zugang zur Registry des Servers, um dort nach dem gewünschten 'remote object' zu fragen (Naming.lookup). Steht dieses zur Verfügung wird automatisch eine Stub-Klasse bereit gestellt, die die Kommunikation und den Datenaustausch mit dem Server durchführt.

  • Parameter/Ergebnis der aufzurufenden Methode des 'remote object'
  1. Es wird in jedem Falle 'call by value' durchgeführt.
  2. Einfache Variable
  3. Objekte

Werden Objekte benutzt müssen diese das Interface 'Serializable' implementieren (Datenübertragung als serieller Bytestrom).

Die meisten Standardklassen von Java erfüllen diese Bedingung, so dass für den Anwender in diesem Falle kein weiterer Aufwand entsteht.

Im Beispiel wird für das Ergebnis des Aufrufs der entfernten Methode eine eigene Klasse (RmiResult) verwendet, die auf Client und Server bekannt sein muss.

  • Dynamisches Laden von Objekten

Dieser Punkt ist für das Funktionieren des Beispiels unerheblich. Hier wird nur eine Zusatzmöglichkeit für die verteilte Verarbeitung beschrieben.

Unter RMI ist es möglich, Objekte über das Netz zu laden. Dabei können sowohl Server als auch Client Quelle oder Ziel sein. Für das Laden kann u.a. das http-Protokoll verwendet werden, wenn auf Quellseite ein Webserver vorhanden ist.

Hier wurde dafür ein Beispiel mit der Klasse 'LoadClient' erstellt, die auf Client-Seite die Klasse 'TClient' vom RMI-Server (TServer) lädt und abarbeiten lässt.

Das zuvor beschriebene Beispiel mit RMI-Client und RMI-Server kann natürlich ohne 'LoadClient' verwendet werden. 'TClient' ist allerdings so programmiert, dass es mit oder ohne 'LoadClient'-Klasse funktioniert.

Beim dynamischen Laden von externem Bytecode ist ein eigener Sicherheitsmanager erforderlich und in der Datei '.java.policy' sind die entsprechenden Zugriffsrechte zu erteilen (das Ziel braucht Connect-Recht bei der Quelle und für den Bytecode Zugriffsrecht auf dem eigenen Rechner). Im Beispiel wurde für den RMI-Client folgende Policy benutzt:

grant {
  permission java.net.SocketPermission "rechner_name:80", "connect";
  permission java.net.SocketPermission "rechner_name:1024-", "connect, resolve";
};

'rechner_name' ist die Webadresse des Servers oder einfacher aber risikoreicher:

grant {
  permission java.security.AllPermission;
};

Die Policy-Datei ist im Home-Verzeichnis des Nutzers, unter Windows bspw.:
"c:\Dokumente und Einstellungen\nutzername\"

Mit der Folgeanweisung kann eine andere Policy-Datei benutzt werden:
java -Djava.security.policy=rmi/client.policy rmi/LoadClient

Das Interface der 'remote method' und die Ergebnisklasse

Bearbeiten

Müssen bei Server und Client verfügbar sein.

package rmi;

import java.rmi.Remote;
import java.rmi.RemoteException;

public interface Hello extends Remote {
  //String-Parameter, Ergebnis ist eigene Klasse mit serialisierbaren Objekten
  public RmiResult sayHello(String txt) throws RemoteException;
}

package rmi;
import java.util.GregorianCalendar;

public class RmiResult implements java.io.Serializable {
  String name;
  String adresse;
  GregorianCalendar birthday;
}

Der RMI-Server

Bearbeiten
package rmi;

import java.rmi.registry.Registry;
import java.rmi.registry.LocateRegistry;
import java.rmi.RemoteException;
import java.rmi.server.RemoteServer;
import java.io.*;

public class TServer {
    int port;

    public TServer(int port) {
      this.port = port;
      worker();
    }

  void worker() {
    try {
      //Server-Logging in eine Datei
      PrintStream logFile = new PrintStream(
        new FileOutputStream("rmi/TServerLog.txt",true));
      RemoteServer.setLog(logFile);
      //Instanz mit der implementierten Methode
      HelloImpl hello = new HelloImpl();

      //Export des Objekts in 'HelloImpl' selbst, wo automatisch der Konstruktor
      //von UnicastRemoteObject aufgerufen wird, der das Objekt exportiert.

      //Registry anlegen, Methode anmelden, Generierung der Skeleton-Klasse
      Registry registry = LocateRegistry.createRegistry(port);
      registry.rebind("Hello", hello);

      String strtSrv = Thread.currentThread() + ", Server ready...";
      System.err.println(strtSrv);
      logFile.println(strtSrv);
      logFile.println("---------------");
      logFile.flush();
    } catch (Exception e) {
      System.err.println("Server exception: " + e.getMessage());
      e.printStackTrace();
    }
  }
  public static void main(String args[]) {
    int port = 1099;
    TServer ts = new TServer(port);
  }
}

Die Implementierung der 'remote method'

Bearbeiten
package rmi;

import java.rmi.server.UnicastRemoteObject;
import java.util.GregorianCalendar;

//die Erweiterung sorgt für den automatischen Export des Objekts
public class HelloImpl extends UnicastRemoteObject implements Hello {
  int cnt = 0;

  public HelloImpl() throws java.rmi.RemoteException {
  }
  
  //synchronized verhindert Unterbrechung durh andere Anfragen, das wäre
  //kritisch in Bezug auf die Variable 'cnt'.
  public synchronized RmiResult sayHello(String txt) {
    System.err.println("sayHello-Thread: this=" +
      Integer.toHexString(this.hashCode()) + ", " + Thread.currentThread());
    cnt++;
    RmiResult rr = new RmiResult();
    rr.name = "Hallo, mein Name ist: ..., alias Sabbat, alias Wallenstein !";
    rr.adresse = "Laufende Nr.:" + cnt + ", " + txt;  //Parameter der Methode
    rr.birthday = new GregorianCalendar(1941,11,24);
    return rr;
  }
}

Der RMI-Client

Bearbeiten

Der vom Schalter 'loadclient' abhängige Code ist nur für den Fall bedeutsam, dass der RMI-Client über die Klasse 'LoadClient' und nicht als eigenständige Applikation über die Shell aufgerufen wird.

package rmi;

import java.rmi.*;
import java.util.Date;
import java.text.SimpleDateFormat;

public class TClient {
  public static void main( String[] args ) throws Exception {
    //Aufruf aus der Shell als eigenständige Applikation
    TClient tc = new TClient(false);
  }

  Hello stub;
  String par = "Adresse: Mitteldeutschland";

  //Aufruf über 'cl.newInstance()' in LoadClient
  public TClient() throws Exception {
    this(true);
  }
  public TClient(boolean loadclient) throws Exception {
    String FEHLER_TEXT = "\n*** Moegliche Ursachen:\n" +
           "*** Server nicht aktiv oder 'remote object' nicht registriert!";
    System.err.println("*** loadclient=" + loadclient);

    try {
      //kontaktiere Server bezüglich Port 1099
      //ist auf dem Server eine Methode 'Hello' registriert ?
      //generiere eine Stub-Klasse für den Client
//      stub = (Hello) Naming.lookup("Hello");  //Server lokal
      //Server entfernt (siehe Doku von 'Naming.lookup')
      stub = (Hello) Naming.lookup("//rechner_name:1099/Hello");
    } catch (Exception ce) {
      System.err.println(ce.getMessage() + FEHLER_TEXT);
      System.exit(1);
    }

    if (!loadclient)
      worker2();
  }

  //Diese Methode wird von LoadClient (dynamisches Laden erforderlicher Objekte)
  //aufgerufen.
  public RmiResult worker1() throws RemoteException {
    RmiResult erg = stub.sayHello(par);  //remote method
    return erg;
  }
  //Diese Methode wird durch Direktruf von TClient aufgerufen und hier beginnt
  //die eigentliche Applikation mit dem transparenten Aufruf der
  //entfernten Methode.
  public void worker2() throws RemoteException {
    RmiResult erg = stub.sayHello(par);  //remote method
    System.out.println(erg.name );
    System.out.println( erg.adresse );
    SimpleDateFormat sdf = new SimpleDateFormat("EEEE, dd.MM.yyyy");
    Date bd = erg.birthday.getTime();  //(Gregorian)Calendar-Objekt -> Date
    System.out.println(sdf.format(bd));
  }
}

Klasse LoadClient

Bearbeiten

Zusatzmöglichkeit für die verteilte Verarbeitung.

package rmi;

import java.rmi.RMISecurityManager;
import java.rmi.server.RMIClassLoader;
import java.lang.reflect.*;
import java.text.SimpleDateFormat;
import java.util.Date;

//Hier wird die TClient-Klasse vom Webserver des RMI-Servers dynamisch
//geladen, instanziert und ihre Methode 'worker1' aufgerufen, die ein Ergebnis
//vom Typ 'RmiResult' zurückliefert, das dann bearbeitet werden kann.
public class LoadClient {

  public static void main(String[] args) {
    new LoadClient();
  }
  Method clMethod;
  Object[] obargs;
  Object obj;
  public LoadClient() {
    //setze einen SecurityManager ein, in der Policy-Datei '.java.policy'
    //muss das Laden von externem Bytecode erlaubt sein
    System.setSecurityManager(new RMISecurityManager());
    try {
      // 1. Lade die 'rmi.TClient'-Klasse von der angegebenen Web-Adresse.
      Class cl = RMIClassLoader.loadClass("http://rechner_name/wicki/","rmi.TClient");
      // 2. Bilde eine Instanz der Klasse und suche deren Methode 'worker1'.
      //    Durch 'cl.newInstance()' wird auch der Konstruktor von TClient
      //    abgearbeitet, der den Schalter 'loadclient' setzt.
      obj = cl.newInstance();
      Method[] methods = cl.getMethods();  //alle Methoden der Klasse
      //Argumentliste für aufzurufende Methode(0: keine Param.)
      obargs = new Object[0];
      for (int i = 0; i < methods.length; i++) {
        //suche die Methode in 'TClient', die die entfernte Methode aufruft
        if (methods[i].getName().compareTo("worker1") == 0) {
          clMethod = methods[i];
          System.err.println("In LoadClient, ReturnType:" +
                             clMethod.getReturnType().getName());
          worker();  //zur Abarbeitung, dort Aufruf entfernte Methode
          break;
        }
      }
    } catch (Exception e) {
      System.err.println("Exception:" + e.getMessage());
      e.printStackTrace();
    }
  }

  //Hier kann die eigentliche Abarbeitung mit dem Aufruf der entfernten Methode
  //und der Auswertung des Ergebnisses beginnen.
  public void worker() throws Exception {
    // 3. Rufe die unter 2. gefundene Methode 'clMethod' der Instanz 'obj' auf,
    //    'obargs' ist die in diesem Falle leere Argumentliste für die Methode.
    RmiResult erg = (RmiResult)(clMethod.invoke(obj,obargs));
    System.out.println("In worker ... ");
    System.out.println( erg.name );
    System.out.println( erg.adresse );
    SimpleDateFormat sdf = new SimpleDateFormat("EEEE, dd.MM.yyyy");
    Date bd = erg.birthday.getTime();  //(Gregorian)Calendar-Objekt -> Date
    System.out.println(sdf.format(bd));
  }

}

Abarbeitung des Beispiels

Bearbeiten
Beispiel TServer/TClient
Rechner A Rechner B
Dateien in 'basispfad/rmi': TServer,Hello,HelloImpl,RmiResult TClient,Hello,RmiResult
Übersetzen cd basispfad
javac rmi/*.java
cd basispfad
javac rmi/*.java
Abarbeiten cd basispfad
java rmi/TServer
cd basispfad
java rmi/TClient
Beispiel LoadClient
Rechner A Rechner B
Dateien in 'basispfad/rmi': TServer,Hello,HelloImpl,RmiResult LoadClient,RmiResult,client.policy
Dateien in 'pfad_webserver/wicki/rmi': class-Dateien: TClient,Hello,RmiResult
Übersetzen wie Beispiel 1 cd basispfad
javac rmi/*.java
Abarbeiten wie Beispiel 1 cd basispfad
java -Djava.security.policy=rmi/client.policy rmi/LoadClient




Einleitung, Grundbegriffe

Bearbeiten
  • Verweise zum Thema CORBA
Vorlage zum Beispiel
Beschreibung CORBA
Überblick: RMI, CORBA, IDL
  • CORBA

CORBA (Common Object Request Broker Architecture) unterstützt verteilte Anwendungen, die plattformübergreifend also unabhängig von Sprache und Betriebssystem sind. Darin unterscheidet es sich von RMI, das nur zwischen JAVA-Anwendungen möglich ist. CORBA beschreibt die Architektur und ein ORB (Object Request Broker) stellt eine spezielle Implementierung dar.

Ein Server könnte also auf einem Linux-System laufen und in C++ geschrieben sein während ein Client auf einem Windows-PC gestartet wird und in Java geschrieben ist. Der Aufruf selbst ist transparent, d.h. der Nutzer merkt nichts davon, dass die Methode auf einem entfernten Rechner implementiert ist und dort abgearbeitet wird. Parameter und Ergebnis werden transparent zwischen Client und Server ausgetauscht.

CORBA benutzt die IDL (Interface Definiton Language) zur Beschreibung der Schnittstelle zwischen Server und Client, zu der die zu benutzenden entfernten Methoden gehören. Ein IDL-Compiler (für Java 'idlj') generiert daraus die erforderlichen Klassen (stub, skeleton und weitere Hilfsklassen). Im Hintergrund übernehmen diese generierten Klassen stellvertretend für Server/Client Kommunikation und Datenaustausch.

  • POA: Portable Object Adapter

Der POA als Teil des ORB vermittelt die Anforderung des Clients (Aufruf der entfernten Methode) an den Server, der diese Methode implementiert hat.

  1. Vererbungs-Modell (POA):
    Die Implementierungsklasse erbt (per 'extends') von der Skeleton-Klasse, die der IDL-Compiler generiert hat.
  2. Delegation-Modell (POA/Tie) benutzt 2 Klassen:
    Eine Tie-Klasse erbt vom Skeleton (POA), delegiert aber die Anforderung an die eigentliche Implementierungs-Klasse.
  • Anforderungen an die Applikation

Für die Verbindung zwischen Client und Server ist die Schnittstelle zwischen beiden zu definieren (im Beispiel 'Hello.idl').

  • CORBA-Server
  1. Schreibe eine öffentliche Serverklasse nach der Vorlage im Beispiel (HelloServer.java)
  2. Implementiere in dieser Serverklasse die Interfaces (im Beispiel 'HelloImpl','AdderImpl') in Form von je einer Klasse
  3. Generiere die Klassen für die Verbindung Client/Server mit dem idlj-Compiler
  4. Übersetze nun alle Klassen
  • CORBA-Client
  1. Schreibe eine öffentliche Clientklasse nach der Vorlage im Beispiel (HelloClient.java)
  2. Formuliere in dieser Klasse die Aufrufe der entfernten Methoden.
  3. Generiere die Klassen für die Verbindung Client/Server mit dem idlj-Compiler
  4. Übersetze nun alle Klassen

Das Interface für die entfernten Methoden

Bearbeiten

Muss bei Server und Client verfügbar sein. Es folgt die Datei 'Hello.idl':

//Ein 'module' ist das CORBA Äquivalent zu einem package in Java.
module HelloApp
{
  //Rückgabe-Objekt der Methode 'sayHello' (als Java-Klasse).
  //Es wird generiert: Standardkonstruktor und Konstruktor mit 3 Parametern,
  //die den 3 Strukturelementen entsprechen.
  struct Person {
    string firstName;
    string lastN