Kurzeinstieg Java: Druckversion

Aus Wikibooks
Zur Navigation springen Zur Suche springen

Inhalt[Bearbeiten]

Zusammenfassung des Projekts[Bearbeiten]

  • Zielgruppe: Geplante Zielgruppe sind Studierende von MINT-Fächern, die Java vor oder während der Studienzeit lernen müssen.
  • Lernziele: Dieses Buch soll seinen Lesern einen knappen Überblick über die Programmiersprache Java (aktuell in Version 8) verschaffen.
  • Buchpatenschaft / Ansprechperson: zurzeit niemand
  • Sind Co-Autoren gegenwärtig erwünscht? ja, jederzeit
  • Richtlinien für Co-Autoren: Übernehmt bitte ein komplettes Kapitel. Neueste Java-Version (zz. 8) verwenden. Keine Sachen voraussetzen, die erst später drankommen (Ausnahme Einleitung). – Bitte beachtet auch das, was Qwertz84 auf der Diskussionsseite zum Konzept geschrieben hat.
  • Themenbeschreibung: Es wird Java als Programmiersprache beschrieben und durch Übungsaufgaben ergänzt. Dabei werden hoffentlich eines Tages allgemeine Programmier-Konzepte vermittelt, die dem Grundstudium der Informatik entlehnt sind. Dieses ist keine Referenz und soll andere im Netz erhältliche Dokumentation nicht ersetzen. – Weitere Themen sind durchaus erwünscht.


Books Flat Icon Vector.svg

Dieses Buch steht im Regal Programmierung.

Einleitung[Bearbeiten]

Nötiges Vorwissen[Bearbeiten]

Wir setzen in diesem Buch voraus, dass Sie mit ihrem Computer umgehen können, Programme installieren sowie Texte mit einfachen Editoren (z. B. Notepad++, vi, emacs) schreiben können. Falls Sie es vorziehen, nicht mit einer Programmierumgebung (IDE) zu arbeiten, dann sollten Ihnen Kommandozeilen ("Shell" oder "Dosfenster" oder dergleichen) keine Probleme bereiten.

Neue Programmiersprachen lernt man leichter, wenn man schon andere Programmiersprachen kann, insbesondere solche, die so ähnlich aussehen. So braucht man sich nur auf die neue Syntax einzulassen, der Rest ergibt sich dann irgendwie. Wir erläutern Grundbegriffe wie zum Beispiel "Variablen" zwar kurz, setzen aber voraus, dass diese kurze Einführung ausreicht.

Geschichte und Namensgebung[Bearbeiten]

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

Seit 1998 gibt es den "Java Community Process", ein Verfahren, bei dem Firmen und Einzelpersonen mit dem Ziel, die Java-Entwicklung voranzutreiben, beteiligt sind.

2009/2010 erfolgte die Übernahme von Sun durch Oracle.

2014 wurde Java SE 8 ("Java 8") herausgebracht.

Detailiertere Informationen über den Verlauf der Java-Entwicklung finden Sie unter oracle.com.edgesuite.net/timeline/java/

Warum Java?[Bearbeiten]

An Java führt praktisch kaum ein Weg vorbei. Laut dem Tiobe-Index[1] ist Java die beliebteste Programmiersprache aller Zeiten, und dort unangefochten auf den ersten Plätzen vertreten. Java wird an Universitäten in Einstiegsvorlesungen gelehrt und damit ist es recht wahrscheinlich, dass bei allen Entwicklern Java zum kleinsten gemeinsamen Nenner gehört.

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 (heute Oracle) 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 Oracle 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.


  1. Siehe http://www.tiobe.com/index.php/content/paperinfo/tpci/index.html

Einrichten der Programmierumgebung[Bearbeiten]

Allgemeines[Bearbeiten]

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.

SDK mit JRE[Bearbeiten]

Zur Programmierung von Java benötigt man zum Anfang nur 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. Oracle stellt verschiedene SDKs für gängige Betriebssysteme wie Windows und Linux und die Sun-spezifische Unix-Variante Solaris zur Verfügung, die von http://www.oracle.com/technetwork/java/javase/downloads/jdk8-downloads-2133151.html heruntergeladen werden können. Benötigt wird zu Anfang nur die mit jdk-8uxx bezeichnete aktuelle Version (Java SE 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. 74-227 MB) und nicht das JRE (Java Runtime Environment) - dies ist im SDK enthalten.

  • Einen beliebigen Texteditor. Dies kann z.B. unter Windows notepad++ sein 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 Oracle-Seite http://www.oracle.com/technetwork/java/javase/documentation/jdk8-doc-downloads-2133158.html Webseite herunterladen.
  • Ein Webbrowser zum Lesen der Java Dokumentation.

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.

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]

Windows

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.

Linux

Auf der Oracle-Webseite wird das SDK als Download für Linux angeboten.

Mac OS X

Java ist integraler Bestandteil des Betriebssystems Mac OS X und muss dort nicht gesondert installiert werden. Auf der Oracle-Webseite finden sie aktuelle Downloads.

FreeBSD

Aktuelle Installationshinweise finden Sie auf der Webseite http://www.freebsd.org/de/java/.

OpenBSD

In den Ports ist das JDK enhalten ('ports/devel/jdk/1.8/'). Die IDEs "Eclipse" und "NetBeans" befinden sich in den Ports 'ports/devel/eclipse/' und 'ports/devel/netbeans/'.

Eine IDE einrichten[Bearbeiten]

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 Oracle-Website http://www.oracle.com/technetwork/java/javase/downloads/index-jsp-138363.html#javasejdk 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. Netbeans ist als relativ übersichtliche IDE Einsteigern uneingeschränkt zu empfehlen.

Eclipse[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.
  • Alternativ lässt sich Eclipse, plattformunabhängig und für diverse Szenarien voreingestellt, über http://profiles.yatta.de beziehen.

Java-Editor[Bearbeiten]

Der Java-Editor ist eine sehr einfache und intuitiv bedienbare IDE für Java und UML. Die Entwicklungsumgebung gibt es nur für Windows. Laut Herstellerangaben wendet sich diese IDE an Einsteiger. Syntax-Highlighting, Code-Vervollständigung (bei installierter Dokumentation der Pakete) und ein visueller GUI-Builder sind Bestandteil der IDE. Die IDE erinnert von seiner Benutzung her an Borlands JBuilder oder Delphi und lässt sich ähnlich einfach benutzen. Klassen können modelliert und dann wie in BlueJ interaktiv getestet werden.

Erste Schritte[Bearbeiten]

Erste Schritte[Bearbeiten]

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 (8, 8.1). 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.

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 (~/meineJavaProgramme/HelloWord.java unter MacOS und Linux, C:\Pfad\HelloWorld.java unter Windows). 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. 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.

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

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

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 ein kultureller Aspekt, in einem Buch zur Programmierung ein "Hello World"-Programm zu zeigen. Man ehrt damit die Erfinder der Sprache C, Brian Kernighan und Dennis Ritchie. Sie finden in fast jedem Buch zu Programmiersprachen 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.

Pakete[Bearbeiten]

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

Ob man in seine eigenen Quelltexte eine packacke-Anweisung schreibt oder nicht ist abhängig vom Umfang seines Programmes. Schreibt man keine package-Anweisung hinein, dann gehören automatisch alle Klassen zu einem gemeinsamen Paket. Arbeitet man mit anderen Paketen von Drittherstellern, dann kann es zu Namenskollisionen kommen. Hat man beispielsweise eine Klasse Auto geschrieben und importiert nun von einem Hersteller eine Klasse Auto, dann kommt es zu Konflikten. Gruppiert man seine Klassen in Paketen, dann ist immer klar, welche Auto-Klasse gemeint ist. Ein Paket stellt also einen eigenen Namensraum dar.

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:

  • java.lang, Basis-Klassen wie Object. Quasi das Rückgrat der Sprache (lang meint Language, auf Deutsch also 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 nach dem, welche Art von Programmen man entwickeln möchte, sollte ein Blick in folgende Pakete folgen:

  • java.net, Basisklassen für Netzwerkkommunikation,
  • java.awt und javax.swing, Klassen zur Programmierung grafischer Benutzeroberflächen

Der Aufbau von Java-Quelltexten[Bearbeiten]

Java Quelltexte bestehen aus höchstens einer package-Anweisung. Es folgen einige Klassen, den Aufbau von Klassen besprechen wir in einem späteren Kapitel. Jede Klasse besteht aus einer optionalen Menge von Konstanten und Variablen sowie Methoden. Die Buchteile "Grundlagen" und "Objektorientierung" beschäftigen sich mit dem Aufbau von Java-Programmen. Spätere Kapitel beschreiben weniger die Syntax und Semantik von Java, dafür zeigen sie, wie etwas gemacht wird.

Bezeichner[Bearbeiten]

Bezeichner sind alle Worte, die einer Klasse, Methode, Variablen oder Konstanten und so fort einen Namen geben. Hierbei haben sich einige Regeln eingebürgert, die im Rahmen der Java-Coding-Standards definiert sind. Weitere Hinweise dazu im Kapitel Programmierstil. Grundsätzlich dürfen Sie alle Zeichen verwenden. Bezeichner dürfen nur mit Buchstaben, "$" und dem Unterstrich "_" beginnen. Buchstaben dürfen aus der Menge aller Unicode-Letter gebildet werden. Anschließend dürfen Buchstaben, Ziffern, "$" und "_" folgen. Bezeichner dürfen keine reservierten Schlüsselwörter sein. Tiefergehende Details dazu finden Sie unter http://docs.oracle.com/cd/E19798-01/821-1841/bnbuk/index.html.

Um bei der Menge an Regeln zu überprüfen, ob ein Wort ein Java-Bezeichner ist, kann man beispielsweise folgendes Programm benutzen. Das Programm ist für den Einstieg schon sehr lang und benutzt viele Sprachelemente, die erst in späteren Kapiteln besprochen werden. Auf der Kommandozeile können Worte übergeben werden, diese Worte werden dann getestet. Für das erste Zeichen des Wortes gibt es die statische Methode (wir kommen später darauf zu sprechen…) java.lang.Character.isJavaIdentifierStart() und für jedes weitere Zeichen java.lang.Character.isJavaIdentifierPart(). Beide Methoden werden mit einem Zeichen als Argument aufgerufen.

// Das folgende Programm überprüft, ob ein Bezeichner nach den Java-Regeln aufgebaut ist.
class Bezeichner {

  public static void main(String[] args) {

    if(args.length < 1) {
      System.out.println("Aufruf: java Bezeichner \"irgend ein Bezeichner\"");
    }

    boolean istGültigerBezeichner;              // Variable vom Typ boolean

    for(String arg : args) {                    // über alle Elemente im args-Array iterieren
      System.out.println("Teste " + arg);       // Ausgabe
      char[] zeichenArray = arg.toCharArray();  // Aufruf der String-Methode: String in Char-Array umwandeln


      // Teste das erste Zeichen als erlaubtes Anfangszeichen
      istGültigerBezeichner = java.lang.Character.isJavaIdentifierStart(zeichenArray[0]);

      // Teste alle folgenden Zeichen
      for(int i = 1; i < zeichenArray.length; i++) {    // Über alle Zeichen im zeichenArray iterieren
          istGültigerBezeichner &= java.lang.Character.isJavaIdentifierPart(zeichenArray[i]);
      }

      System.out.println("Ergebnis: " + arg + (istGültigerBezeichner ? " ist ein" : " ist kein") + " gültiger Bezeichner." );
    }
  } // Ende von main
}

Der Parameter args der Methode main() ist eine Aneinanderreihung von Parametern, die auf der Kommandozeile übergeben werden. An den eckigen Klammern [] erkennt man, dass es sich um ein so genanntes Feld (engl. Array) handelt. Wenn keine Kommandozeilenparameter angegben werden, dann ist args.length Null. In diesem Fall wird mit System.out.println() erklärender Text ausgegeben. Die erste for iteriert über alle Kommandozeilenparameter, die Zweite über alle Zeichen eines Wortes. Ob das betreffende Wort ein gültiger Java-Bezeichner ist, wird in der Variablen istGültigerBezeichner festgehalten. Dabei ist istGültigerBezeichner true, wenn jede Prüfung eines Zeichens im Wort true ergibt. Dafür sorgt der Operator &=.

Variablen[Bearbeiten]

Variablen sind benannte Speicherorte. Variablen haben in Java grundsätzlich einen Typ, der vom Programmierer festgelegt wird. Die Gültigkeit einer Variable hängt davon ab, wo sie deklariert wurde. Variablen, die in Klassen außerhalb von Methoden deklariert wurden sind allen Methoden der Klasse bekannt. Variablen, die innerhalb von Methoden deklariert wurden, sind innerhalb der Methode bekannt. Methodenparameter sind in der gesamten Methode bekannt. Innerhalb eines Gültigkeitsbereiches darf es keine zwei Variablen mit demselben Namen geben. Als Merkregel reicht zumeist aus: "Die Gültigkeit einer Variablen reicht von "{" bis "}" der gleichen Tiefe.". Auf Besonderheiten weisen wir an passender Stelle hin.

Probieren Sie es aus:

public class Testklasse {

  public static void main(String[] args) {

    int v = 33;
  
    {     // neuer Block
        int v = 99;
        System.out.println(v);
    }
    System.out.println(v);
  }  
     
}

Hier wird die Variable "v" in einem neuen Block, bestehend aus einem geschweiften Klammerpaar, neu deklariert. Ist das erlaubt? Finden Sie es heraus.

Java Virtual Machine[Bearbeiten]

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 möglichst allen Betriebssystemen läuft. Dazu führte man das Konzept der Java Virtual Machine (JVM) ein.

Java VM

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.

Das Konzept der Java-VM ist so erfolgreich, dass es auch mit anderen Programmiersprachen genutzt werden kann. So ist jython ein Werkzeug, dass aus Python-Programmtext Java-VM-Code erzeugt.

javac und jython

Übungen zur Einleitung[Bearbeiten]

Übungen[Bearbeiten]

Einige der Aufgaben sind so gestellt, dass man sie ausprobieren muss.

1 Java-Programme sind immer langsamer als C++-Programme

stimmt
stimmt nicht

2 Der Erfinder (Vorname Nachname) der Sprache Java

heißt:

3 Um einen Java-Quelltext zu kompilieren und anschließend auszuführen, muss ich auf der Kommandozeile folgendes eingeben:

'java Testprogramm.java', anschließend 'java Testprogramm'
'javac Testprogramm.java', anschließend 'java Testprogramm'
'javac Testprogramm.java', anschließend 'javac Testprogramm.class'
nur 'java Testprogramm.class'

4 Ausprobieren: Um den Text Hallo, Welt! auszugeben kann ich folgendes machen:

System.out.println("Hallo, Welt!");
System.out.print("Hallo, Welt"); System.out.println("!");
System.out.print("Hallo, Welt!"); System.out.println();
char[] xx = "Hallo, Welt!".toCharArray(); for(char x : xx) System.out.print(x); System.out.println();

5 Welche der folgenden Aussagen sind wahr?

In einem Java-Programm kann es höchstens 3 Variablen mit demselben Namen geben.
Ein Feld (Array) erkennt man an eckigen Klammern.
Bezeichner dürfen niemals mit Ziffern beginnen.
Die JVM wird auch von anderen Programmiersprachen genutzt.
In jeden Java-Quelltext gehört eine package-Anweisung geschrieben!
$_ ist ein gültiger Bezeichner

Primitive Datentypen[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.

Zeichen[Bearbeiten]

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]

byte[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.

short[Bearbeiten]

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.

int[Bearbeiten]

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);

long[Bearbeiten]

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 Zahlen nur ungefähr richtig gespeichert werden. Solche Details behalten wir uns für das Kapitel "Zahlen und etwas Mathematik" vor.

float[Bearbeiten]

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.

double[Bearbeiten]

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;

Wrapperklassen[Bearbeiten]

Was sind Wrapperklassen?[Bearbeiten]

Wrapperklassen (das deutsche Wort "Hüllklasse" ist ungebräuchlich) sind Datentypen, die einen primitiven Datentyp aufnehmen, in einer Klasse packen und nützliche Methoden bereitstellen. Nutzt man Wrapperklassen, stehen einem alle Möglichkeiten objektorientierter Programmierung zur Verfügung (z. B. Vererbung), was die zugehörigen primitiven Datentypen nicht leisten.

Wie funktioniert das Ein- und Auspacken?[Bearbeiten]

public class Testklasse {

  public static void main(String[] args) {

      int iWert = 3;
      Integer iWrapper = iWert;     // Einpacken
      int iWert2 = iWrapper;        // Auspacken
      System.out.println(iWert + " " + iWrapper + " " + iWert2);

  }    
}

Dieses Beispiel funktioniert mit allen primitiven Datentypen auf die gleiche Weise, weswegen wir es nicht für jeden Datentyp duplizieren wollen. Die Umwandlung eines primitiven Typs in eine Klasse und umgekehrt ist eine Konvertierung, die man "autoboxing" beziehungsweise "unboxing" nennt. Anders als beim Casting haben die Datentypen keinen unterschiedlichen Wertebereich.

Welche nützlichen Methoden werden bereitgestellt?[Bearbeiten]

Schreib mit:

  • typische und nützliche Methoden der Wrapperklassen (ohne Mathe und Strings, beides kommt später)

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.

Welche Vor- und Nachteile hat man durch Wrapperklassen?[Bearbeiten]

Schreib mit:

  • Vorteile von primitiven Datentypen (z. B. Geschwindigkeit -> Belege?)
  • Wo sind Wrapper zwingend erforderlich?

Felder[Bearbeiten]

Felder[Bearbeiten]

Felder (engl. Array) ist eine Zusammenfassung von mehreren Variablen desselben Datentyps zu einer gemeinsamen Struktur mit einem gemeinsamen Namen. Besonders am Feld ist, dass die Größe vom Programmierer fest vorgegeben wird und sich anschließend nicht mehr ändern kann. Der Zugriff auf die Daten im Array erfolgt per Index, wobei das erste Element den Index 0 hat.

Die Felder-Syntax kennen Sie schon aus einem früheren Beispiel:

...
 public static void main(String[] args)
...

hierbei bezeichnet String[] ein Feld, wobei jedes Element ein String ist.

 public class ArrayTest1
 {
   private double[] zahlenFeld;
  
   public ArrayTest1()
   {
     zahlenFeld = new double[10];   
   }
   
   public void setZahlInFeld(int index, double wert)
   {
     zahlenFeld[index]=wert;
   }
   
   public double getZahlInFeld(int index, int wert)
   {
     return zahlenFeld[index];
   }  
 }

Die Eigenschaft zahlenfeld wird als Array mit double-Elementen deklariert:

    private double[] zahlenFeld;

Das Kennzeichen hierfür sind die eckigen Klammern.
Hinweis: Was public und private bedeutet, wird im nächsten Kapitel erklärt.

Im Konstruktor wird dann ein neues Array mit 10 Elementen angelegt:

    zahlenFeld = new double[10];

Auf die einzelnen Elemente kann man mit Angabe des Index zugreifen:

    zahlenFeld[5]=27.3;

setzt beispielsweise zahlenFeld mit dem Index 5 auf den Wert 27,3.
So erklärt sich die get- und die set-Methode im obigen Beispiel. Natürlich muss bei einem Array noch der Index genannt werden, weshalb sich der Übergabeparameter index ergibt.

Der höchste Index eines Arrays mit 10 Elementen ist 9.

Greift man auf einen Index außerhalb des festgelegten Bereichs zu, so erhält man eine Fehlermeldung. Bei dem obigen Beispiel würde ein Zugriffsversuch auf den Index 10 bereits zu einem Fehler führen.

Größe des Feldes ermitteln[Bearbeiten]

Die Eigenschaft length speichert die Anzahl der Elemente eines Feldes.

   public static void main(String[] args)
   {
     String[] name = { "Hans", "Josef", "Peter" };
     for (int i = 0; i < name.length; i++)
     {
       System.out.println(i + "tes Element: " + name[i]);
     }
   }

Anwendungsbeispiel: Bestimmen des Maximums[Bearbeiten]

public class HelloWorld {
	public static double getGroessteZahl(double[] zahlenfeld){
		double maximum=zahlenfeld[0];
		for (int i=1; i<zahlenfeld.length; i++){
			if (zahlenfeld[i] > maximum){
				maximum=zahlenfeld[i];				
			}
		}
		return maximum;
	}
	
	public static void main (String[] args){
		double[] zahlenfeld;
		zahlenfeld = new double[8];
		zahlenfeld[0] = 1.4;
		zahlenfeld[1] = -5.2;
		zahlenfeld[2] = 0;
		zahlenfeld[3] = 123;
		zahlenfeld[5] = 123;
		zahlenfeld[6] = -1000;
		zahlenfeld[7] = 7;
		System.out.println("Das Maximum ist " + getGroessteZahl(zahlenfeld));
	}
}

Ausgabe:

Das Maximum ist 123.0

Mehrdimensionales Feld[Bearbeiten]

In Java werden mehrdimensionale Felder durch Felder realisieren welche als Elemente weitere Felder speichern.

int [][] multidimensional = new int [2][2];

In unserem Beispiel speichert ein mehrdimensionales Feld zwei Felder mit Platz für jeweils zwei Integer-Werte. Die Werte können wie schon bei einem eindimensionalen Feld mithilfe eines Index abgerufen, zugewiesen oder geändert werden. Der einzige Unterschied besteht darin, dass wir nun zwei Indizes verwenden müssen um einen Speicherplatz eindeutig zu beschreiben.

multidimensional[0][0] = 1;
multidimensional[0][1] = 2;
multidimensional[1][0] = 3;
multidimensional[1][1] = 4;

Mehrdimensionale Felder sind aber nicht auf zwei Dimensionen beschränkt, sondern lassen sich auf beliebig viele Dimensionen erweitern.

int [][][] multiMULTIdimensional = new int [2][3][2];
multiMULTIdimensional[0][0][0] = 785;
multiMULTIdimensional[0][0][1] = -15;;

Einsatzmöglichkeiten für mehrdimensionale Felder, sind zum Beispiel Raster.

boolean[][] grid = new boolean [10][10];
int x=4,y = 1;
grid[x][y] =true;

Ein solches Raster kann zum Beispiel für Binärbilder eingesetzt werden, bei welchem jeder Pixel nur die Farben schwarz oder weiß annehmen kann.

Operatoren[Bearbeiten]

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 Variable x++;
++ Preinkrement, Addiert 1 zu einer numerischen Variable ++x;
-- Postdekrement, Subtrahiert 1 von einer numerischen Variable x--;
-- Predekrement, Subtrahiert 1 von einer numerischen Variable --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 beide Argumente true sind boolean krümelmonster = istBlau && magKekse;
|| Oder, true, wenn mindestens ein Operand true ist boolean machePause = hungrig || durstig;
^ Exor, true wenn genau ein Operand true ist boolean zustandPhilosoph = denkt ^ isst;

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
>> Rechtsverschiebung, alle Bits des Operanden werden um eine Stelle nach rechts verschoben (fehlt)
>>> Rechtsverschiebung mit Auffüllung von Nullen (fehlt)
<< Linksverschiebung, entspricht bei positiven ganzen Zahlen einer Multiplikation mit 2, sofern keine "1" rausgeschoben wird. (fehlt)

Zuweisungsoperatoren[Bearbeiten]

Zu vielen Operatoren aus den vorstehenden Tabellen gehören Schreibweisen, mit denen 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 weißt 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.

Vergleichsoperator instanceof[Bearbeiten]

  • instanceof überprüft ob ein Objekt eine Instanz einer bestimmten Klasse ist. Ein Beispiel hierzu stellen wir ihnen später vor.

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

Fallen[Bearbeiten]

Es gibt zum Glück wenige 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...

Kontrollstrukturen[Bearbeiten]

Wie funktioniert die bedingte Verzweigung?[Bearbeiten]

Verzweigungen ändern den linearen Programmfluss. In Abhängigkeit von einer Bedingung wird ein Programmteil ausgeführt – oder auch nicht. Verzweigungen werden mit if(bedingung) {etwas code} erzeugt. Wenn sich die Bedingung zu true auswerten lässt, dann wird der Programmcode im Block ausgeführt:

int zahl = 2;
if( zahl % 2 == 0 ) {  // wenn die Zahl gerade ist ...
  System.out.println("2 ist eine gerade Zahl"); // dann führe dieses aus, sonst nicht!
} // fertig

Im Fall, dass etwas zusätzlich ausgeführt werden soll, wenn die Bedingung nicht erfüllt ist, dann schreibt man das mit else:

int zahl = 2;
if( zahl % 2 == 0 ) {  // wenn die Zahl gerade ist ...
  System.out.println(zahl + " ist eine gerade Zahl"); // dann führe dieses aus
} 
else {
  System.out.println(zahl + " ist ungerade.");  // sonst führe dieses aus.
} // fertig

Die geschweiften Klammern kann man bei einzelnen Anweisungen übrigens weglassen.

 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 Verzweigungen immer in geschweifte Klammern zu fassen. Dadurch lassen sich die einzelnen Ausdrücke besser dem jeweiligen if zuordnen.

Was macht der Bedingungsoperator?[Bearbeiten]

Für manche Anwendungsfälle eignet sich der Bedingungsoperator ?:. Sein Aufbau wird durch folgendes Programm verdeutlicht:

public class Testklasse  {
  public static void main(String[] args) {
    int zahl = 4;
    String geradeText = (zahl % 2 == 0) ? " gerade" : "ungerade";
    System.out.println("Die Zahl " + zahl + " ist " + geradeText); 
  }
}

Dieser Operator wird zumeist in Ausdrücken benutzt, bei denen etwas zugewiesen wird. Wenn also eine Bedingung erfüllt ist, dann gib den ersten Teil als Ausdruck zurück, sonst den Teil, der hinter dem : steht. Bedingungsoperatoren verwendet man überall dort, wo eine if-else-Konstruktion zu lang wäre.

Wie kann man lesbar vielfach verzweigen?[Bearbeiten]

if-else-Konstrukte kann man beliebig verschachteln. Bei einer Auswahl über sehr viele Kategorien werden solche Konstrukte schnell unübersichtlich. Hier kann switch helfen. Diese Art der Mehrfachverzweigung können Sie nur mit ganzzahligen numerischen Typen (int, byte, short), Zeichen (char) und String verwenden.

 
public class Testklasse {
  public static void main(String[] args) {
      String wochentag = "Montag";
      switch( wochentag ) {
        case "Montag":
        case "Dienstag":
        case "Mittwoch":
          System.out.println("Die Woche nimmt kein Ende...");
          break;            // Bei Montag, Dienstag oder Mittwoch ist hier Schluss
        case "Donnerstag":
          System.out.println("Morgen ist schon Freitag");
          break;            // Nur bei "Donnerstag" ist hier Schluss
        case "Freitag":
          System.out.println("Endlich Freitag");
          break;
        default:            // alle anderen Fälle
          System.out.println("Endlich Wochenende!!!!!");
          break;
      } // Ende von switch()
  }
}

Mit switch wird die Auswahl getroffen, mit case werden die einzelnen Fälle selektiert. Hat man Fälle, die man nicht im einzelnen explizit abfangen möchte, dann folgt default.

Der Wert hinter case muss eine Konstante sein und dient dem Vergleich mit dem übergebenen Wert (hier Montag). 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. Java in Versionen vor 7 erlaubten keine Variablen vom Typ String.

For-each-Schleife[Bearbeiten]

Besonders im Zusammenhang mit Feldern ist eine for-Schleife gebräuchlich, die über jedes Element des Arrays iteriert. "Für jedes Element eines Arrays mache folgendes...":

public class T  {
  public static void main(String[] args) {
      int[] zahlen = {1, 2, 3, 4, 5, 6};        // Array

      for(int zahl : zahlen) {     // für jede Zahl im Array...
        System.out.println(zahl + " ist eine sehr schöne Zahl.");    // gib sie aus
      }
  }
}

Die Variable zahl ist nur innerhalb der Schleife bekannt. Der Datentyp muss zum Datentyp der Feld-Elemente passen. Neben Arrays sind allgemeine Collection-Klassen (wir kommen später darauf zu sprechen) die Kernanwendungen dieser Schleife, die Oracle als "enhanced for statement" bezeichnet.

Allgemeine 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
    • Bedingung mit Limitierung auf den Endwert. Ist diese Bedingung true dann wird die Zählschleife weiterhin ausgeführt
    • Schrittweite
  • Schleifenkörper (Schleifenrumpf)
    • Anweisungen, die pro Schleifendurchlauf ausgeführt werden sollen

Die Syntax[Bearbeiten]

Die grundlegende Syntax sieht folgendermaßen aus:

for( Initialisierung Zahlervariable; Limitierung; Zählen ){
	...
	Schleifenkörper mit Anweisungen
	...
}

Beispiele:

for(int zahl = 1; zahl < 10; zahl++) {  // 
        System.out.println(zahl);
      }

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

  • Zunächst muss eine Zählervariable auf einen Startwert initialisiert werden int zahl=1. Wird diese Variable im Schleifenkopf deklariert, ist sie ausschließlich im Schleifenkopf und im Schleifenkörper bekannt. Außerhalb der Schleife kann die Zählervariable nicht angesprochen werden.
  • Dann wird die Limitierung for-Schleife festgelegt: i < 10. Sie läuft so lange wie zahl kleiner als 10 ist. Trifft diese Bedingung nicht zu, werden die Anweisungen im Schleifenkörper nicht ausgeführt. Die Limitierung muss also immer einen boolschen Wert (true oder false) ergeben.
  • Zuletzt muss noch definiert werden in welchen Intervallen gezählt wird: zahl++. Die Schleife zählt also in Schritten von 1 nach oben bzw. addiert nach jedem Schleifendurchlauf 1 auf die Zählervariable zahl.

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 Limitierung: 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 Limitierung: 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 Limitierung: 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 Limitierung: 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 wiederkehrenden Anweisungen auszuführen. Sei es das Füllen eines Arrays, das Erzeugen einer gewissen Anzahl an Objekten oder die Ausgabe der ersten Zeilen einer Datenbankabfrage.

public class Testklasse  {

  public static void main(String[] args) {

      // erzeugt ein Array mit zehn Integer-Werten
      int[] einArray = new int[10];

      // nun wird das Befüllen des Arrays ebenfalls durch eine Schleife realisiert
      // es werden nur ungerade Zahlen ins Array gesteckt
      for(int i = 0; i < einArray.length; i++) {
        einArray[i] = 2 * i + 1;
      }

      // For-Schleife, die die im Array enthaltenen Werte ausgibt
      for( int zahl : einArray ){
          System.out.println( zahl );
      }
  }
}

Mittels .length erhält man die Länge des Arrays als ganze Zahl. Somit lässt sich immer die Länge des Arrays bestimmen, das ist praktisch, man ist so weniger auf Konstanten angewiesen.

Ein Array kann also mittels einer for-Schleife durchlaufen werden, da man einen Startwert = 0 sowie einen Endwert = .length - 1 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].

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];  // Platz für 100 Objekte vom Typ Integer

// Befüllen des Arrays mit 100 Objekten vom Typ Integer
for( int i = 0; i < einArray.length; i++ ) {
	einArray[i] = new Integer( 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 mit einer negativen Zahl
for( byte i = -120; i < 100; i++ ){
	System.out.println( i );
}	

int i = 50; // Bsp.: Variable für den Startwert außerhalb der Schleife festlegen
for(  ; i < 100; i++ ){
	System.out.println( i );
}

// 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 );
}

// Ausgabe aller 2er-Potenzen kleiner als 100
for( int i = 1; i <= 100; i*=2 ){
	System.out.println( i );
}

// Endlosschleife
for( ; ; ){
	System.out.println( "Hallo, Welt!" );
}

Schleife mit Vorabprüfung (while)[Bearbeiten]

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

 while (true) {} // Endlosschleife

 int i = 0;
 while (i < 10) {  // Ausführungsanzahl 10
    i++;
 }

 int i = 0;
 while (i < 0) {  // wird nicht ausgeführt.
    System.out.println ("Schleifentest");
  }

Kopfgesteuerte Schleifen werden benutzt, wenn man sich nicht sicher ist, ob eine Schleife überhaupt ausgeführt werden muss. Sie wird also "mindestens 0-mal" ausgeführt.

Einige Sortieraufgaben erfordern beispielsweise genau so ein Vorgehen: "Solange meine Sachen nicht sortiert sind, sortiere". Wenn die Sachen schon sortiert sind, braucht nichts sortiert und der Schleifenkörper braucht folglich nicht besucht zu werden.

Schleife mit Nachprüfung (do)[Bearbeiten]

Rumpfgesteuerte Schleifen sind quasi das Gegenstück zu kopfgesteuerten Schleifen. 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 ("Hallo, Welt!");
 } while (i < 0);

Die do-Schleife wird mindestens 1-mal ausgeführt.

Hier wird die do-Schleife benutzt, um mit Bubblesort ein Array aufsteigend zu sortieren:

public class Testklasse  {

  public static void main(String[] args) {

    // unsortiertes Array
    int[] einArray = {10, 9, 8, 7, 6, 5, 4, 3, 2 };
    int hilfsvariable; 
    boolean vertauscht;

    do {
      vertauscht = false;
      for(int index = 0; index < einArray.length - 1; index++) {
        if(einArray[index] > einArray[index + 1]) { // wenn falsche Reihenfolge

          // vertausche 2 Nachbarn im Array
          hilfsvariable = einArray[index];
          einArray[index] = einArray[index + 1];
          einArray[index + 1] = hilfsvariable;

          // es wurde vertauscht
          vertauscht = true;
        } // ende von if
      } // ende von for
    } while (vertauscht); // weitermachen, so lange das Array unsortiert ist

    // Array ausgeben
    for(int elem : einArray) {
        System.out.print( elem + " " );
    }
    System.out.println();
  }
}

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);

Ausnahmen[Bearbeiten]

Was sind Ausnahmen?[Bearbeiten]

Ausnahmen sind Unterbrechungen vom normalen, geplanten Programmfluss. In der Regel entstehen sie durch Fehler, die der Anwender oder der Programmierer gemacht hat. Auf Ausnahmen kann man reagieren, alternativ bricht das Programm in der Regel ab. Java stellt Sprachelemente bereit, mit denen Ausnahmen abgefangen und erzeugt werden können. Ferner sind Ausnahmen in der Umgebung von Java kategorisiert.

Grundstruktur der Ausnahmebehandlung[Bearbeiten]

Es wird ein Block, der unter Umständen eine Ausnahme auslösen könnte mit try { … } eingeschlossen. Anschließend folgt catch(AusnahmeTyp x) { … }. Hier wird die eigentliche Ausnahme aufgefangen. Es ist möglich, mehrere Ausnahmen in einem catch-Block unterzubringen. Ist die spezielle Ausnahme im catch-Parameter nicht aufgeführt, dann wird sie trotzdem behandelt: Entweder wird Programm abgebrochen, oder die Ausnahme wird weitergereicht. Diese Fallunterscheidung ist in der Praxis einfacher, als es sich liest:

void benutzeGangschaltung() {

  try {
    legeRückwärtsgangEin();  // kann "WirSindMitVollgasVorwärtsUnterwegsAusnahme" werfen
  }
  catch( WirSindMitVollgasVorwärtsUnterwegsAusnahme x) {
    bremseFahrzeugBisStillstand();
  }
}

Ohne try-und-catch könnte manches Schlimme passieren.

Mehrere Ausnamhmen können folgendermaßen abgefangen werden:

void benutzeGangschaltung() {

  try {
    legeRückwärtsgangEin();  // kann "WirSindMitVollgasVorwärtsUnterwegsAusnahme" werfen
    beschleunigeAuto();      // kann "WirFahrenGegenParkendesAutoAusnahme" werfen
  }
  catch( WirSindMitVollgasVorwärtsUnterwegsAusnahme |  WirFahrenGegenParkendesAutoAusnahme x) {
    bremseFahrzeugBisStillstand();
  }
}

Ausnahmebehandlung lässt sich auch anders verschachteln, etwa hintereinander oder ineinander. Aber das Grundprinzip ist immer das Gleiche.

Umwandelausnahme[Bearbeiten]

public class Testklasse {
  public static void main(String[] args) {
    Integer i = 0;

    try {
      i = new Integer("12");
    }
    catch( NumberFormatException e ) {
      System.err.println("Der String konnte leider nicht umgewandelt werden!" + e.getMessage() + i);
    }
    System.out.println(i);
  }
}

Das vorstehende Beispiel benutzt genau die einfache Grundstruktur. NumberFormatException wird geworfen, wenn die Konvertierung eines Wortes in eine Zahl Integer("12") nicht erfolgreich war. Das Objekt e stellt die Methode getMessage() bereit, mit der der Fehler weiter beschrieben werden kann.

Natürlich wird eine Ausnahme nur dann geworfen, wenn tatsächlich ein Fehler passiert. So ein Fehler ist hier gezeigt:

public class Testklasse {
  public static void main(String[] args) {
    Integer i = 0;

    try {
      i = new Integer("zwölf");
    }
    catch( NumberFormatException e ) {
      System.err.println("Der String konnte leider nicht umgewandelt werden!" + e.getMessage() + i);
    }
    System.out.println(i);
  }
}

Selber werfen[Bearbeiten]

Ausnahmen können Sie selber werfen. Das folgende Programm zeigt, wie es geht. Jede Methode, die eine Ausnahme wirft, sollte mit throws Ausnahme1, Ausnahme2, …, AusnahmeN gekennzeichnet werden.

class Weitwurf {

  public static String Σ(String a, String b) throws NumberFormatException {

    java.lang.Integer i, j, summe;

    try {
      i = new Integer(a);
    } catch (NumberFormatException e) {
      System.err.println(a + " konnte nicht in eine ganze Zahl gewandelt werden. ");
      throw e;
    }

    try {
      j = new Integer(b);
    } catch (NumberFormatException e) {
      System.err.println(b + " konnte nicht in eine ganze Zahl gewandelt werden. ");
      throw e;
    }

    /* wenn wir hier sind, lief alles glatt */
    summe = i + j;                                      // Summe bilden
    String ergebnis = Integer.toString(summe, 10);      // String Zahl zur Basis 10
    return ergebnis;
  }

  public static void main(String[] args) {
    String summe = "0";
    try {
      summe = Σ("12", "13");
    } catch (NumberFormatException e) {
      System.err.println("Mindestens einer der Summanden lässt sich nicht in eine Zahl umwandeln.");
    }
    System.out.println(summe);
  }
}

Die Funktion Σ(String a, String b) bereichnet die Summe der übergebenen Argumente, dazu werden die Argumente in Zahlen konvertiert (Integer(a)), anschließend addiert und wieder in einen Text konvertiert (Integer.toString(summe, 10)). Wenigstens der erste Teil ist weniger konstruiert, als es auf den ersten Blick erscheint. Selbstverständlich können Sie ein Programm schreiben, das die Kommandozeilenargumente aufaddiert und als String −vielleicht sogar zur Basis 2− ausgibt, oder? Wenn bei der Konvertierung etwas schief läuft, dann wird in diesem Beispiel per throw e die Ausnahme einfach erneut ausgelöst und somit an die aufrufende Funktion main weitergereicht. Modifizieren Sie obenstehendes Beispiel doch so, dass tatsächlich eine Ausnahme geworfen wird.

finally[Bearbeiten]

Schreib mit:

  • Wann ist finally {} hilfreich?
  • Wann wird der finally {}-Block betreten?
  • Beispiel

assert[Bearbeiten]

Schreib mit:

  • Was ist assert?
  • Wann ist es sinnvoll?
  • Was hat das mit Ausnahmen zu tun?

Eigene Ausnahmen schreiben[Bearbeiten]

Es wird sie bestimmt überraschen, aber Ausnahmen sind ganz normale Objekte. Der Typ dieser Objekte ist eine Klasse, die von einer anderen Klasse die Eigenschaft erbt, eine Ausnahme zu sein. Und über Vererbung lassen wir uns erst später aus. Haben Sie noch etwas Geduld.

Zahlen und etwas Mathematik[Bearbeiten]

Wie kommen die Zahlen in das Programm?[Bearbeiten]

Zahlen kommen entweder durch Konstanten oder Nutzereingaben in ein Programm und werden dort verarbeitet. Das soll explizit auch Dateien einschließen. Die Zahlen liegen dort als Text vor, also beispielsweise "12" als String und müssen dann umgewandelt werden. Wrapperklassen übernehmen solche Aufgaben, wie unser Taschenrechner zeigt:

public class Argumentenrechner {

    public static void main(String[] args) {
        double summe = 0;
        
        for(String arg : args) {
            Double z = new Double(arg);
            summe += z;
        }
        System.out.println(summe);
    }
}

Das Programm addiert die Zahlen, die auf der Kommandozeile übergeben werden. Diese Zahlen kommen aus einem String-Feld und werden mit Hilfe einer Wrapperklasse in eine Zahl umgewandelt, anschließend addiert und ausgegeben. Sie bemerken, dass wir den Fall, dass eine Zahl nicht umgewandelt werden kann ("dreizehn") nicht abfangen. Hier würde uns eine Ausnahme helfen.

Zahlen können auch in anderen Basen gelesen werden. Die Zahl "1101" beispielsweise ist dezimal "13". Die Klasse Integer hat auch hierfür eine passende statische Methode:

public class BinaerLeser {

    public static void main(String[] args) {
        
        String s = "1101";  // binär für 13
        Integer i = Integer.parseInt(s, 2);  // lies zur Basis 2
        System.out.println("Binär: " + s + " Dezimal: " + i);
        
    }   
}

Den umgekehrten Fall, nämlich Zahlen in einen String einzubetten kennen sie schon: Den Operator + haben wir oft benutzt. Die Wrapperklassen stellen zusätzlich dazu Methoden bereit, beispielsweise toString(), die im nächsten Beispiel vorkommt.

Welche Zahlendarstellungen kennt Java?[Bearbeiten]

Zahlen kann man binär, oktal, dezimal und hexadezimal ausgeben. Daneben kann man sich eine eigene Basis definieren. Die Klasse Integer enthält die passenden Methoden:

public class Darstellung {

    public static void main(String[] args) {
        
        int z = 13;        
        System.out.println("Binär: " + Integer.toBinaryString(z) );
        System.out.println("Oktal: " + Integer.toOctalString(z) );
        System.out.println("Dezimal: " + z );
        System.out.println("Hexadezimal: " + Integer.toHexString(z) );    
        System.out.println("Basis 13: " + Integer.toString(z, 13) );    

    }    
}

Literale, die Zahlen zu einer der typischen Zahlenbasen darstellen sind:

int binaer = 0b1101; // Zahl 13 binär geschrieben
int oktal = 015;    // Zahl 13 oktal geschrieben
int hexadezimal = 0xd;  // 13 hexadezimal geschrieben

Bei binärer Zahlendarstellung verwendet man "0b" als Präfix, anschließend eine Folge von "0" und "1". Oktalzahlen haben den Präfix "0", dann Ziffern aus der Menge "0" bis "7" und Hexadezimalzahlen den Präfix "0x", anschließend folgen Ziffern aus der Menge "0"-"9" sowie "A"-"F". Bezüglich der Großschreibung der verwendeten Buchstaben im Präfix wie auch als Ziffern ist Java tolerant, "0xABCD" ist dasselbe wie "0XabCd".

Welche mathematischen Konstanten kennt Java?[Bearbeiten]

In der Java-Klasse Math sind die Konstanten für die eulersche Zahl e und die Kreiszahl π hinterlegt. Die Konstanten sind so groß wie ein double und werden in der offiziellen Dokumentation beschrieben als diejenigen double-Zahlen, welche den mathematischen Konstanten am nächsten kommen:

public class Konstanten {

    public static void main(String[] args) {
        
        System.out.println("e = " + Math.E);
        System.out.println("π = " + Math.PI);
       
    }    
}

Mit welchen Zahlentypen kann man rechnen?[Bearbeiten]

Die Klasse Math hat statische Methoden für die Datentypen double als größten Datentypen. Wo es sinnvoll ist, werden für float und ganzzahlige Typen eigene Methoden bereitgestellt. Die Grundrechenarten funktionieren auf allen primitiven numerischen Typen wie auch auf den zugehörigen numerischen Objekt-Typen (siehe Kapitel Wrapperklassen). Es gibt in der Java-API keinen Typ, der komplexe Zahlen verarbeiten kann, hier muss man auf Pakete von Drittherstellern zugreifen.

Math enthält dabei unter anderem Methoden aus den Folgenden Gruppen

  • Logarithmen zu verschiedenen Basen,
  • Absolutbeträge,
  • Trigonometrische Funktionen,
  • Runden
  • Potenzieren und
  • Wurzelziehen

Wie benutzt man trigonometrische Funktionen?[Bearbeiten]

Java kennt einige trigonometrische Funktionen und benutzt dabei immer Radiant als Winkelmaß. Zur Umrechnung der im Alltag gebräuchlichen Einheit Dezimalgrad nach Radiant und umgekehrt gibt es die statischen Methoden toRadians() und toDegrees():

public class Winkel {

    public static void main(String[] args) {
        
        double winkelInGrad = 90;
        double winkelInRad = Math.toRadians(winkelInGrad);
        System.out.println("Winkel in Grad = " + winkelInGrad);
        System.out.println("Winkel in Radiant = " + winkelInRad);
        
        winkelInRad += Math.PI / 36.0;
        
        System.out.println("nun Winkel in Grad = " + Math.toDegrees(winkelInRad));
        System.out.println("Winkel in Radiant = " + winkelInRad);
          
    }    
}

Zum Gebrauch der Funktionen sin() und cos() hier ein Beispiel, das Punkte auf dem Einheitskreis berechnet:

public class Einheitskreis {

    public static void main(String[] args) {
        
        double x, y;
        for(double winkel = 0.0; winkel < 2.0 * Math.PI; winkel+= Math.PI / 10.0) {
            x = Math.cos(winkel);
            y = Math.sin(winkel);
            System.out.println("Winkel " + winkel + " x = " + x + " y = " + y);
            
        }
    }
}

Wie nutzt man Zufallszahlen?[Bearbeiten]

Sowohl die Klasse Math wie auch Random aus dem Paket 'java.util' können Zufallszahlen bereitstellen. In Math erzeugt die statische Methode random() Pseudozufallszahlen im rechts halboffenen Intervall . Damit ist es leicht, einen Würfel zu programmieren:

public class Zufall {

    public static int zufall6() {
        double r = Math.random() * 6.0 + 1.0; // Zufallszahl in [1.0, 7.0)        
        return (int) Math.floor(r);          // abrunden
    }
    
    public static void main(String[] args) {
        for(int i = 0; i < 10; i++) {
            System.out.println("Würfel " + zufall6());
        }
    }
}

Die Methoden der Klasse Random sind nicht statisch. Es muss also ein Exemplar erzeugt werden. Die Klasse bietet weitaus mehr als pure gleichverteilte Zufallszahlen, doch zunächst ein Besuch im Casino:

import java.util.Random;
public class Casino {

    public static void main(String[] args) {
        
        Random rnd = new Random();
        int zZahl;  // Zufallszahl
        String farbe = "keine";
        
        for(int i = 0; i < 20; i++) {
            zZahl = rnd.nextInt(37);  // 0..36
            farbe = "ungerade, Zahl, rot";
            if(zZahl == 0) {
                farbe = "grün, Bank gewinnt";
            }
            else if(zZahl % 2 == 0) {
                farbe = "gerade Zahl, schwarz";
            }
            System.out.println("Die Kugel rollt... " + zZahl + " (" + farbe + ")");
        }
    }
}

Es wird ein Objekt vom Typ Random erstellt, die Methode nextInt() liefert entweder eine Zufallszahl, bei der 32 Bits zufällig gesetzt werden oder, wenn wie im Beispiel ein Parameter N verwendet wird eine Zufallszahl im ganzzahligen Bereich von bis .

Je nach Anwendungsgebiet sind besonders verteilte Zahlen interessant. Die Methode nextGaussian() liefert normalverteilte Zahlen mit Mittelwert 0.0 und Standardabweichung 1.0. Leider war es das auch schon, mehr Verteilungen werden nicht angeboten. Je nach Einsatzgebiet muss man auf Dritthersteller ausweichen oder selber schreiben.

Falls die Zufallszahlen kryptographischen Standards entsprechen sollen, dann lohnt ein Blick in die Klasse SecureRandom. Hier ein Beispiel, das der Originaldokumentation zu 'java.security.SecureRandom' entlehnt ist. Es werden gleichzeitig N viele zufällige Bytes erzeugt.

import java.security.SecureRandom;
        
public class Krypton {

    public static void main(String[] args) {
        
        SecureRandom srnd = new SecureRandom();
        byte zufallsBytes[] = new byte[13];
        srnd.nextBytes(zufallsBytes);
        
        for(int i = 0; i < 13; i++) {
            System.out.println((i+1) + ". Zufallszahl ist " + zufallsBytes[i]);
        }
    }
}

Das Array zufallsBytes wird von der Methode nextBytes() mit Zufallszahlen gefüllt.

Informatik[Bearbeiten]

Schreib mit

  • 2er Komplement: Wie sind ganze (positive und negative) Zahlen aufgebaut?
  • Fließkommazahlen: Wie sind Zahlen nach IEEE-Norm aufgebaut, was bedeutet das für Rundungen und den ==-Operator?

Kleine Textbearbeitung[Bearbeiten]

Was sind Strings?[Bearbeiten]

Strings (dt. etwa "Zeichenreihe" oder "Zeichenkette"), sind Zeichenketten variablen Länge. Jeder beliebige Text, einschließlich Leerzeichen kann als String verwendet werden. Der Datentyp String ist eine Klasse mit einer Vielzahl von Methoden, von denen einige in diesem Kapitel vorgestellt werden.

Wie erzeugt man Strings?[Bearbeiten]

In den folgenden beiden Beispielen beschwert sich Scotty über ein Problem mit dem Warpkern:

public class Scotty {

  public static void main(String[] args) {
   String s = new String("Captain, wir haben ein Problem mit dem Warpkern.");
      System.out.println(s + " Stringlänge: " + s.length());
  } 
}
public class Scotty {

  public static void main(String[] args) {
      String s = "Captain, wir haben ein Problem mit dem Warpkern.";
      System.out.println(s + " Stringlänge: " + s.length());
  }
}

Beide Programme verhalten sich absolut identisch. Im ersten Beispiel wird ein String-Objekt erzeugt, in dem auf klassische Weise new aufgerufen wird. new in Verbindung mit dem Klassennamen ruft den Konstruktor der Klasse auf, der das Exemplar erstellt. Im zweiten Beispiel zeigen wir, dass ein String-Literal dasselbe ist wie ein per new erzeugter String. Welcher dieser beiden Varianten Sie in ihren Programmen den Vorzug geben ist ganz Ihnen überlassen. Es gibt viele weitere Konstruktoren, es ist String s = new String() ebenfalls eine Möglichkeit, einen neuen String zu erzeugen. Das wiederum ist dasselbe wie String s = "".

Strings kann man auch aus einem Zeichenfeld char[] erstellen:

 public static void main(String[] args) {
      char[] scottysArray = {'C', 'a', 'p', 't', 'a', 'i', 'n',
        ',', ' ', 'w', 'i', 'r', ' ', 'h', 'a', 'b', 'e', 'n',
        ' ', 'e', 'i', 'n', ' ',
        'P', 'r', 'o', 'b', 'l', 'e', 'm', ' ',
        'm', 'i', 't', ' ', 'd', 'e', 'm', ' ',
        'W', 'a', 'r', 'p', 'k', 'e', 'r', 'n', '.'};
      String s = new String(scottysArray);

Hier wird ein Konstruktor aufgerufen, der das Exemplar mit einem Feld initialisiert.

Wie vergleicht man Strings miteinander?[Bearbeiten]

Zunächst die schlechte Nachricht: Der Operator == hat in Bezug auf den Inhalt von Strings keine Bedeutung. Für typische Vergleiche von Strings miteinander muss man also ein bisschen mehr schreiben:

public class Spock {

    public static void main(String[] args) {
        
        String spocksWort = "Unlogisch!";
        
        // Exakte Schreibung
        if(spocksWort.equals("Unlogisch!")) {
            System.out.println("So rot kann der Alarm gar nicht sein.");
        }
        else if(spocksWort.equalsIgnoreCase("UNLOGISCH!")) {
            System.out.println("Faszinierend!");
        }
    }
}

Um die exakte Gleichheit von zwei Strings zu überprüfen nimmt man equals(). Kommt es auf die Groß- und Kleinschreibung nicht so an, dann equalsIgnoreCase().

Möchte man herausfinden, ob ein String mit einem bestimmten String beginnt oder endet, dann kommen die Methoden startsWith() und endsWith() zum Einsatz. Ob ein String einen anderen enthält, überprüft man mit contains():

public class McCoy {

    public static void main(String[] args) {
        
        String s1 = "McCoy: Die einen Menschen altern schneller und die anderen langsamer.";
        if(s1.startsWith("McCoy:")) {
            System.out.println("Achtung, McCoy spricht.");
        }
        if(s1.endsWith(".")) {
            System.out.println("* ein Satz");
        }
        if(! s1.contains("Spock")) {
            System.out.println("* es geht vermutlich nicht um Spock");
        }
    }
}

Wie liest man Strings von der Tastatur ein?[Bearbeiten]

Bequem und ohne zu viel Schreibaufwand kann man Strings einlesen, wenn man die Klasse Scanner aus dem Paket java.util benutzt. Diese Klasse ist recht praktisch, sie stellt eine Reihe von Methoden zur Verfügung, mit denen man Strings und Zahlen lesen kann. Scanner liest von einer Quelle, das kann ein anderer String sein, oder, wie in diesem Beispiel, von einem ohnehin vorhandenen InputStream-Objekt. Na, erraten Sie Spocks Kosenamen?

import java.util.Scanner;

class Kosename
{
    public static void main(String[] args) {

      Scanner zeichenScanner = new Scanner(System.in);
      System.out.print("Wie lautet der Kosename von Spock? ");
      String eingabe = zeichenScanner.next();
      if( eingabe.equals ("Spitzohr") ) {
          System.out.println("Ja, richtig.");
      }
      else {
          System.out.println("\"Spitzohr\" wäre richtig gewesen.");
      }
    }
}

Der Scanner wird mit etwas verknüpft, aus dem er lesen kann. Ähnlich wie System.out Ein Ausgabeobjekt[1] ist, so ist System.in ein Eingabeobjekt[2]. Diese Objekte stehen Ihnen immer zur Verfügung. Die Methode next() liest das nächste verfügbare Wort ein. Worte werden durch Leerzeichen getrennt. Probieren Sie aus was passiert, wenn Sie nur Return drücken.

Einen Scanner kann man mit hasNext() befragen, ob im zugehörigen Eingabekanal Zeichen vorliegen. Bei der Standardeingabe blockiert dieser Aufruf. Der Scanner wartet auf Zeichen und gibt nur dann true zurück, wenn Zeichen da sind. Auf false, wenn keine Zeichen da sind, wartet man ewig. Will man Eingabeworte auswerten und erwartet nur endlich viele Worte, dann kann man sich mit zwei Scannern behelfen. Einer liest die Eingabe komplett (mit nextLine()), der Andere (matheScanner) arbeitet auf der Eingabe des Ersten. So lässt ich leicht ein Taschenrechner schreiben, wobei Operatoren und Zahlen bei der Eingabe jeweils durch Leerzeichen getrennt werden müssen:

import java.util.Scanner;

class Rechner
{
    public static void main(String[] args) {

      int summe = 0;
      boolean erwarteZahl = true;
      boolean plus = true;

      Scanner zeilenScanner = new Scanner(System.in);
      System.out.print("Rechnung, nur +, -, keine Vorzeichen: ");
      String eingabeZeile = zeilenScanner.nextLine();

      Scanner matheScanner = new Scanner(eingabeZeile);
      while( matheScanner.hasNext() ) {
          if( erwarteZahl && matheScanner.hasNextInt() ) {
            int eingabeZahl = matheScanner.nextInt();
            summe += plus ? eingabeZahl : -eingabeZahl;
          } else if (! erwarteZahl ) {
            String operator = matheScanner.next();
            if( operator.equals("+") ) plus = true;
            else if( operator.equals("-") ) plus = false;
            else {
              System.err.println("unbekannter Operator");
              System.exit(-1);
            }
          }
          else {
              System.err.println("irgendwas ging schief");
              System.exit(-1);
          }
          erwarteZahl = !erwarteZahl;
      }
      System.out.println("Summe ist: " + summe);
    }
}

Der zeilenScanner liest wie im ersten Beispiel von der Tastatur. Seine Aufgabe ist nach zeilenScanner.nextLine() erledigt. Ab da übernimmt matheScanner, der einen String als Eingabe erhält. Solange weitere Eingabeworte vorhanden sind, wird entweder eine Zahl oder ein Operator abwechselnd eingelesen. Für diese Abwechslung sorgt die Variable erwarteZahl. Mit hasNextInt() wird geprüft, ob eine Zahl vorliegt, mit nextInt() liest der Scanner diese Zahl.

Für die Eingabe von Worten über die Tastatur ist die Scannerklasse gut zu gebrauchen. Man muss sich im dabei allerdings bewusst sein, dass hasNext(), next() und verwandte Methoden blockieren können. Das Blockieren können Sie sehen, wenn Sie in obigen Beispiel beide Scanner versuchen zu vereinen, etwa so:

public static void main(String[] args) {
      String eingabe;
      Scanner zeilenScanner = new Scanner(System.in);
      System.out.print("Eingabe: ");
      while( zeilenScanner.hasNext() ) {
            System.out.println("Lese nun Eingabe mit next().");
            eingabe = zeilenScanner.next();
            System.out.println("Eingabe war: " + eingabe);

      }
}

Das Programm beendet sich nicht, wenn die Eingabe zu Ende gelesen wurde. Viel mehr wartet hasNext(), bis wieder eine vollständige Zeile eingelesen werden kann, die mit Return abgeschlossen wurde.


Es gibt noch weitere Möglichkeiten, Texte von der Tastatur einzulesen. InputStreamReader wandelt Byte-Eingaben in Zeichen-Eingaben um. Es wirkt also als Filter. Eine der Möglichkeiten, die man mit diesem Filter hat ist, Zeichensätze darauf anzuwenden. Wir verwenden InputStreamReader lediglich als Brücke zu BufferedReader. Diese Klasse wiederum stellt einen eigenen Zwischenspeicher bei der Eingabe zur Verfügung und ist recht effizient.

import java.io.InputStreamReader;
import java.io.BufferedReader;
import java.io.IOException ;

class Sulu
{
    public static void main(String[] args) {
        InputStreamReader isr = new InputStreamReader(System.in);
        BufferedReader br = new BufferedReader(isr);
        System.out.print("Wie lautet der Vorname von Lieutenant Hikaru Sulu? ");
        try {
          String eingabe = br.readLine();
          System.out.println("Du hast " + eingabe + " eingegeben.");
          br.close();
        }
        catch( IOException e ) {}
    }
}

BufferedReader hat seinerseits Methoden wie readLine() und read(), um Text einzulesen. Mit close() teilt man mit, dass man keine weiteren Texte lesen möchte, man weist BufferedReader an, den Zwischenspeicher bei nächster Gelegenheit wieder freizugeben.

Kleine Anmerkung: Die Kombination aus InputStreamReader und BufferedReader wird in manchem Quelltext auch als BufferedReader br = new BufferedReader(new InputStreamReader(System.in)) geschrieben. Beachten sie, dass beides gleichwertig ist und sich höchstens in der Lesbarkeit unterscheidet.


  1. Die zugehörige Klasse ist PrintStream.
  2. Die zugehörige Klasse ist InputStream.

Wie gibt man Strings formatiert aus?[Bearbeiten]

Sie werden sicher häufig in die Situation kommen, in einen String Zahlen einbetten zu wollen. Genau das ist das Hauptthema der String-Methode format(). Wir zeigen zunächst einmal, wie ganze Zahlen formatiert werden können. Auf eine einzelne ganze Zahl kann in einem formatierten String mehrfach zugegriffen werden, indem jedes Argument mit einem Index angesprochen wird. Bei vielen Argumenten müssen Sie also nur zählen können:

public class Kennung {

    public static void main(String[] args) {
        
        String s = String.format("Das Raumschiff mit der Kennung NCC-%1$d "
                + "hat oktal die Kennung NCC-%1$o und%nhexadezimal "
                + "NCC-%1$X.%nAls Unicode-Zeichen ist das NCC-%1$c", 1701);
        System.out.println(s);
    }
}

Das Argument ist 1701. Auf dieses Argument wird mehrfach mit 1$ zugegriffen. Hätten wir mehrere Argumente, würden wir sie mit $2 und so weiter ansprechen können. Der Formatierungsausdruck beginnt mit einem Prozentzeichen "%". Dann folgt bei Zugriffen auf Argumente die Argumentnummer. Zuletzt folgt das Konversionszeichen, im Beispiel ein o, um die oktale Darstellung des Arguments einzubinden. Um in einem Formatstring das Prozentzeichen einzubinden geben Sie %% ein, für eine neue Zeile %n.

Praktisch für Fließkommazahlen sind Argumente zur Breite und der Anzahl der Nachkommastellen. Um Ihnen die Auswirkung der Breite vorführen zu können, wird im folgenden Programm eine Lineal ausgegeben, sehen Sie selbst:

public class WbTest {

    public static void main(String[] args) {
        
        System.out.println("12345678901234567890");  // Lineal
        
        String s = String.format("%1$.2f %n%1$5.2f %n%1$10.2f", Math.PI);
        System.out.println(s);
    }
}

Nach der Argumentnummer folgt die Breite, dann ein Punkt und dann die Anzahl der Nachkommastellen. Abgeschlossen wird der Ausdruck von f für Fließkommazahlen. Die Breite bestimmt, wie breit das Argument zusammen mit den Nachkommastellen dargestellt werden sollen. Sind weniger als Breite viele Zeichen im Argument vorhanden, wird der Rest mit Leerzeichen aufgefüllt. Die Breite ist übrigens optional.

Mit Formatstrings lassen sich nicht viel mehr nützliche Dinge machen. Merken Sie sich %1$d und %1$.2f und schon haben Sie die wesentlichen Elemente immer bei der Hand.

Eine weitere Klasse namens Formatter aus dem Paket 'java.util' erweitert die Möglichkeiten um Lokalisierung.

Wie kann man einen String modifizieren?[Bearbeiten]

Die einzige richtige Antwort lautet "gar nicht", weil Strings sich nicht ändern lassen. Der Fachausdruck hierfür ist "immutable". Die in der String-Klasse vorhandenen Methoden zum Ändern von Strings geben jeweils einen neuen String zurück, bei dem die Änderung dann erfolgt ist. Die Methoden toUpperCase() und toLowerCase() erzeugen neue Strings, in Groß- oder Kleinbuchstaben. So etwas könnte ganz praktisch sein, wenn man Werkzeuge wie Quelltextformatierer baut. Viele Anwendungsfälle wird es für die Methoden aber nicht geben. Wollen Sie ein einzelnes Zeichen ändern, dann hilft sicher replace() weiter, insbesondere, wenn Scotty wieder übertreibt:

public class Scotty {

    public static void main(String[] args) {
        
            String s = "Mr. Scott, veranschlagen Sie die Reparaturzeiten übrigens immer 3-mal so lange, wie nötig?";            
            System.out.println(s.replace('3', '4'));

    }
}

Anders sieht es schon bei split() und subString() aus. Sie zerteilen einen Text in mehrere Teile, zum Beispiel Worte, oder extrahieren einen zusammenhängenden Abschnitt aus einem Text. Das ist für jede Art von Textanalyse interessant.

Mit der Methode trim() kann man dafür sorgen, dass den Text umgebende Leerzeichen entfernt werden. Praktisch für alle mit split() erzeugten Teilstrings und besonders für entgegengenommene Benutzereingaben aus Dialogen.

Zusammenführen lassen sich Strings mit concat() oder dem schon besprochenen Operator +. Falls Sie vorhaben, viele Strings zusammenzuführen, dann werfen Sie bitte einen Blick in die Dokumentation zur Klasse StringBuilder, die diese Aufgabe schneller erledigt.

Wie sucht und ersetzt man mit regulären Ausdrücken?[Bearbeiten]

Übungen zu den Grundlagen[Bearbeiten]

Methoden[Bearbeiten]

Wie sind Methoden aufgebaut?[Bearbeiten]

Methoden bestehen aus einem Methodenkopf und einem Methodenrumpf.

Der Methodenkopf besteht aus:

  • höchstens einem Zugriffsmodifizierer (engl. access level modifier) aus der Menge public, private und protected,
  • weiteren Modifizierern wie beispielsweise static
  • genau einem Rückgabetype wie double, 'String' oder dergleichen. Wenn es keine Rückgaben gibt, dann ist der Rückgabetyp void.
  • Der Name der Methode,
  • die Parameterliste und
  • eine Menge an Ausnahemetypen, die von der Methode geworfen werden können.

Falls die Methode ein Konstruktor ist, dann entfällt der Rückgabetyp vollständig. Das erwartet Sie im nächsten Kapitel.

Die Parameterliste ist entweder leer () oder besteht aus einer Folge von Typen und Parameternamen. Parameter selber werden genau dann als Werteparameter (engl. call by value) übergeben, wenn der zugehörige Typ ein primitiver Datentyp ist. Sonst handelt es sich um einen Referenzparameter (engl. call by reference).

Vorsicht Falle: In Programmiersprachen und speziell Java wird die Signatur einer Methode[1] definiert als die Kombination aus dem Methodennamen und der Menge an Parametertypen. In der Theorie der abstrakten Datentypen gehört oft der Rückgabetyp[2] mit hinzu.

Es sind beispielsweise

public static String addiere(String a, String b) throws NumberFormatException

void f()

gültige Methodenköpfe mit den Signaturen addiere(String, String) und f(void).

Der Methodenrumpf wird in den bekannten Klammern eingefasst. Im Methodenrumpf darf man mindestens folgendes in Bezug auf die umschließende Klasse:

  • die eigenen Parameter lesen und beschreiben (sofern sie beschreibbar sind),
  • alle Methoden der eigenen Klasse ausführen,
  • Eigenschaften, auch Attribute genannt (engl. member variable) der Klasse lesen und beschreiben (sofern sie beschreibbar sind).

Beispiele für vollständige Funktionen kennen Sie aus den früheren Kapiteln.

Die Auswirkung der Werte- und Referenzparameter zeigt das folgende Beispiel. Der Parameter v ist ein Werteparameter, r[] hingegen ein Referenzparameter:

class Parametertest
{
    static void callByValue(int v) { 
        v = 10;
    }

    static void callByReference(int[] r) {
        r[0] = 10;
    }

    public static void main(String[] args) {

        int a = 3;
        System.out.println("a=" + a);
        callByValue(a);
        System.out.println("a=" + a);

        int[] b = {1};
        System.out.println("b[0]=" + b[0]);
        callByReference(b);
        System.out.println("b[0]=" + b[0]);

    }
}


Das Schlüsselwort final wird in Parameterlisten benutzt, um konstante Parameter zu kennzeichnen. Der Inhalt dieser Parameter darf nicht geändert werden. Von den folgenden drei Methoden lässt sich nur eine kompilieren:

static void callByValue(final int v) {
        v++;
}

static void callByReference(final int[] r) {
        r[0] = 10;
}

static void callByReference(final int[] r) {
        r = new int[10];
}


Mit drei Punkten nach dem Typnamen werden variable Argumentenlisten gekennzeichnet. Aus dem Kapitel Kleine_Textbearbeitung kennen Sie eine Funktion, die so etwas tut, format() kann jede Zahl von Parametern verarbeiten. Variabel ist dabei nur die Anzahl der Argumente des selben Typs:

class Variadic
{
    static void addiere(int... args) {
        int summe = 0;
        for(int i : args) {
            summe += i;
        }
        System.out.println("summe= " + summe);
    }

    public static void main(String[] args) {

        addiere(1, 2, 3);
    }
}

Die drei Punkte (...) folgen unmittelbar auf den Typ. Links von einem variablen Parameter dürfen weitere Parameter deklariert werden, rechts davon keiner. Variable Parameter verhalten sich im Zugriff wie ein Array.

Hinweis: Das Unicode-Zeichen "…" kann man dafür leider nicht verwenden.

  1. vergl. http://docs.oracle.com/javase/tutorial/java/javaOO/methods.html
  2. vergl.  Abstrakter Datentyp (bessere Quelle gewünscht)

Was bedeutet static?[Bearbeiten]

Sowohl Methoden wie auch Attribute können mit static deklariert werden. Beide Sorten Klassenmitglieder sind damit sofort nutzbar, es muss nicht erst ein Objekt der Klasse erzeugt werden. Beispielsweise waren alle Konstanten und Methoden der Klasse 'Math' statisch deklariert.

Was ist eine anonyme Funktion?[Bearbeiten]

Schreib mit:

  • Was sind anonyme Funktionen und wozu sind sie gut?

Klassen[Bearbeiten]

Wie sind Klassen aufgebaut?[Bearbeiten]

In diesem Abschnitt führen wir Sie in die Welt der Klassen ein. Es werden gleich zwei Klassen benutzt. Die Struktur der Klasse Testklasse kennen Sie aus allen früheren Beispielen. Neu ist, dass nun eine zweite Klasse hinzukommt:

class Beispielklasse {
    
    public static final int KONSTANTE = 42;
    private static int anzahl = 0;
    private String name;
    
    public void sagNameUndAnzahl() {
        System.out.println(String.format("Beispielklasse(%1$s) hat %2$d Instanz(en)", name, anzahl));
    }
    
    Beispielklasse(String n) {
        anzahl++;
        name = n;
    }
}

public class Testklasse {
    public static void main(String[] args) {
        // Platz für 2 Beispielklassen
        Beispielklasse[] klassenRaum = new Beispielklasse[2];
        
        System.out.println("Kermit betritt den Raum...");
        klassenRaum[0] = new Beispielklasse("Kermit");
        klassenRaum[0].sagNameUndAnzahl();
        
        System.out.println("Piggy betritt den Raum...");
        klassenRaum[1] = new Beispielklasse("Piggy");        
        klassenRaum[0].sagNameUndAnzahl();
        klassenRaum[1].sagNameUndAnzahl();
    }
}

Das Beispiel enthält Beispielklasse und Testklasse. Beispielklasse hat keinen Zugriffsmodifizierer während Testklasse public deklariert wurde.

Testklasse erstellt zwei Instanzen von Beispielklasse. Anschließend wird die Methode sagNameUndAnzahl() für jede Instanz aufgerufen.

Beispielklasse enthält eine Klassenkonstante namens KONSTANTE (nicht verwendet), eine Klassenvariable namens anzahl sowie eine Instanzvariable namens name. Der Clou bei Klassenvariablen ist, dass sie über alle Instanzen hinweg denselben Wert haben. Das können Sie leicht an der Programmausgabe überprüfen. Die Instanzvariable wird nicht geteilt, jede Instanz hat ihren eigenen Wert für name.

Ebenfalls enthält Beispielklasse einen Konstruktor. Konstruktoren erkennt man daran, dass sie denselben Namen wie die Klasse haben. Dieser Konstruktor nimmt einen String entgegen und speichert ihn in der Instanzvariable name. So kann die Methode sagNameUndAnzahl() auf den Namen der Klasse zugreifen.

Wer darf was?[Bearbeiten]

In diesem Abschnitt geht es darum, welche Methode einer Klasse eigentlich auf welche Klasse- oder deren Methoden zugreifen darf. Es geht dabei auch um die relative Lage zweier Klassen zueinander. Nach diesem Abschnitt vergessen Sie bitte den Begriff der relativen Lage schnell wieder. Folgende relativen Lagen wollen wir unterscheiden:

  1. Paketebene: Es gibt ein Paket wie etwa 'gemeinsames.paket' in dem sich die Klassen A und B befinden. Beide Klassen gehören somit zum selben Paket. Paket selber ist eine Gliederungsebene oberhalb von Klassen. Mit Paketen befassen wir uns erst in einem späteren Kapitel.
  2. Verwandtschaftsebene: Zwei Klassen können in unterschiedlichen Paketen sein, aber miteinander verwandt. Das bedeutet, dass durch den später erläuterten Mechanismus der "Vererbung" die Klasse 'DarthVader' zum Vater von 'Luke' wird. In der Terminologie der objektorientierten Programmierung spricht man übrigens man eher davon, dass eine Klasse aus einer anderen abgeleitet wurde und es eine Super- und Subklasse gibt. Andere Sprechweisen sind möglich, wir vertiefen das später.
  3. Weltebene: Zwei Klassen teilen sich weder ein Paket noch sind sie miteinander verwandt. Sie haben einfach nichts miteinander zu tun.

Die gute Nachricht ist, es gibt ein paar einfache Fälle, die man unterscheiden kann. Besonders leicht ist es, wenn man bei einer einzelnen Klasse bleibt: Innerhalb einer Klasse darf nämlich jeder alles.

private

Auf private Attribute und Methoden kann von außen nicht zugegriffen werden. Damit ist private der restriktivste Zugriffsmodifizierer.

public

Jeder auf Weltebene (und damit auch allen anderen Ebenen) darf auf mit public gekennzeichnete Attribute und Methoden zugreifen.

package-private

Als package-private (deutschsprachiger Begriff gesucht) gilt ein Attribut oder eine Methode, wenn kein Zugriffsmodifizierer angegeben wurde. Das Zugriffsrecht haben nur Klassen, die im selben Paket liegen.

protected

Mit dem Zugriffsmodifizierer protected ist der Zugriff auf Verwandtschaftsebene erlaubt.

Welchen Zugriffsmodifizierer soll man nehmen?

Wenn Sie der Einzige sind, der eine Klasse benutzt, dann spielt diese Frage keine Rolle. Im allgemeinen Fall verwendet man private für Variablen sowie public für Methoden, Konstruktoren und Datentypen. Eine Regel, die sich in der Praxis bewährt hat, lautet "so streng wie möglich, so freizügig wie nötig".

Gibt es noch weitere Modifizierer?[Bearbeiten]

strictfp

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 zum Zeitpunkt der Übersetzung konstant sind, sind immer FP-strict.

native

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 von einen "Java-Pre-Compiler" (javah) bearbeitet werden. Dieser generiert aus einer nativen Methode einen entsprechenden C-Header und Funktionsrumpf, der dann mit Leben gefüllt werden kann. Diese Rümpfe werden als 'dll' unter Windows bzw. 'lib' unter Linux/Unix compiliert. Diese Bibliotheken müssen dann aber auch zur Laufzeit des Programms zugreifbar sein, andernfalls wird eine Ausnahme erzeugt.

What is this?[Bearbeiten]

Das Schlüsselwort this bezieht sich auf Elemente einer Klasse. Es kommt beispielsweise zum Einsatz, wenn Parameter einer Methode und ein Attribut einer Klasse gleich lauten, wie im anschließenden Beispiel, oder bei "polymorphen Konstruktoren", die im Kapitel über Polymorphie vorgestellt werden.

class AllgemeineKlasse {
    public final int x;
    private final int y;
    
    void zeigeInhalt() {
        System.out.println(x + " " + y);
    }
    // Konstruktor
    AllgemeineKlasse(int x, int y) {
        this.x = x;
        this.y = y;
    }
}

In diesem Beispiel hat der Programmierer zwei Attribute der Klasse mit einzelnen Buchstaben benannt. Die Parameter des Konstruktors haben denselben Namen wie die Attribute. Die Zuweisung x=x im Konstruktor schlug fehl, weswegen er das Schlüsselwort this eingesetzt hat. Wir haben ihm trotzdem gekündigt[1]. this.x hat hier die Wirkung von "das Attribut x in dieser Klasse" oder besser "… in diesem Objekt".

Auf this kann man mit statischen Methoden nicht zugreifen, da this immer an das Objekt gebunden ist – und Objekte gibt es im statischen Fall nicht.

  1. Siehe dazu das Kapitel über Programmierstile

Vererbung[Bearbeiten]

Was ist Vererbung?[Bearbeiten]

Vererbung ist ein Mechanismus in der objektorientierten Programmierung (OOP), bei dem man aus allgemeinen Klassen spezialisierte oder erweiterte Klassen herstellt. Das hat nichts mit "Vererbung" in einem biologischen Sinn zu tun. Aus einem "geometrischen Objekt" (allgemeine Klasse) wird ein Kreis (spezialisiert) und aus einer Pflanze eine Kartoffel. In jedem Fall hat die Kartoffel alle Eigenschaften einer Pflanze und ein Kreis alle Eigenschaften eines geometrischen Objekts. Wir können aus einem "Skywalker" zwar einen "Anakin Skywalker" herstellen, aber aus Anakin leider nicht Luke Skywalker. Im Sinne der OOP wäre dann nämlich ein Luke auch ein Anakin, und das würde keinen Sinn ergeben.

Aus diesen Gründen spricht man häufig von "Ableitung", abgeleitete Klassen sind Subklassen oder Unterklassen. Diejenigen Klassen, von denen abgeleitet wird, nennt man Basisklassen oder Superklassen. In diesem Bereich hat man mit vielen Begriffspaaren zu tun, die aus der Zeit stammten, als OOP etwas völlig Neues war. Hier müssen Sie ein wenig flexibel bleiben. In diesem Buch sprechen wir von Vererbung, Super- und Subklasse, manchmal auch von Basisklassen.

Wie sieht Vererbung aus?[Bearbeiten]

Wir bleiben vorerst bei der Familie Skywalker. Ein beliebiges Familienmitglied kann offenbar ein Jedi sein. Die Methoden setzeJedi() und istJedi() sind können die Eigenschaft setzen oder abfragen. Das eigentliche Attribut ist privat. Allgemeine Skywalker sind natürlich keine Jedis:

class Skywalker {
    private boolean istEinJedi;
    
    public Skywalker() {
        System.out.println("Ich bin ein Skywalker");
        setzeJedi(false);
    }
    
    // setter
    protected void setzeJedi(final boolean jedi) {
        istEinJedi = jedi;
    }
    
    // getter
    protected boolean istJedi() {
        return istEinJedi;
    }
}


class AnakinSkywalker extends Skywalker {
    private boolean hatEinLichtschwert;
    
    public AnakinSkywalker() {
        System.out.println("Ich bin Anakin Skywalker");
        setzeJedi(false);  // war ein Jedi
        hatEinLichtschwert = true;
    }
    
    public boolean hatLichtschwert() {
        return hatEinLichtschwert;
    }

}


public class Todesstern {
    public static void main(String[] args) {
        
        AnakinSkywalker darthVader = new AnakinSkywalker();
        if(!darthVader.istJedi())
            System.out.println("war ein Jedi");
        
        if(darthVader.hatLichtschwert())
            System.out.println("hat ein Lichtschwert");

    }
}

Nicht anders sieht es bei AnakinSkywalker aus. Der war früher mal ein Jedi, heute ist er keiner mehr. Dafür hat er ein Lichtschwert, was eine neue Eigenschaft ist. AnakinSkywalker erbt alle Eigenschaften eines Skywalkers. Dafür sorgt das Schlüsselwort extends. Anakin wird dadurch spezieller, in dem er neue Eigenschaften und Methoden bekommt. Man könnte auch sagen, seine Fähigkeiten werden erweitert.

Ist das nicht super?[Bearbeiten]

Nun tritt Luke auf den Plan. Für Luke ist es selbstverständlich, ein Jedi zu sein. So selbstverständlich, dass er seine eigenen Methode istJedi() implementiert. Beachten Sie, dass wir den Rückgabetyp von istJedi() von boolean auf String geändert haben. In manchen Fällen möchte er gerne explizit auf die istJedi()-Implementierung der Superklasse zugreifen, und in manchen Fällen eben auf seine eigene.

class Skywalker {
    private boolean istEinJedi;
        
    public Skywalker() {
        System.out.println("Ich bin ein Skywalker");
        setzeJedi(false);
    }
                                    
    // setter                    
    protected void setzeJedi(final boolean jedi) {
        istEinJedi = jedi;
    }
                                                            
    // getter
    protected String istJedi() {
        return istEinJedi ? "ja" : "nein";
    }
                                                                                }
                                                                                 
class LukeSkywalker extends Skywalker {
    
    public LukeSkywalker() {
        setzeJedi(true);        // natürlich ein Jedi
        System.out.println("Ich bin Luke, ich werde niemals zur dunklen Seite wechseln!");
        System.out.println("Jedistatus: " + super.istJedi());  
    }
     
    // neue Rückgabe
    protected String istJedi() {
        return "Selbstverständlich bin ich ein Jedi!";
    }
}
 

class Todesstern
{
    public static void main(String[] args) {
        LukeSkywalker luke = new LukeSkywalker();
    }
}

Und genau das macht das Schlüsselwort super möglich. super.istJedi() ruft die Methode der Superklasse auf, damit können Subklassenimplementierungen von Superklassenimplementierungen unterschieden werden.

Nehmen Sie hier folgendes mit:

  1. Sie dürfen in einer Subklasse Methoden der Superklasse überschreiben. Das ist dann eine neue Implementation, und wann auch immer Sie die überschriebene Methode aufrufen, es wird diejenige Methode in ihrer Klasse gewählt.
  2. Um auf Methoden und Attribute der Superklasse zuzugreifen, benutzen Sie super.

Wie kann man das Überschreiben verhindern?[Bearbeiten]

Nun kann selbstverständlich jeder Skywalker eine eigene Implementierung von istJedi() haben und sich so unter Umständen fälschlich als Jedi ausgeben. Das sollte verhindert werden. Benutzen Sie in einer Superklasse das Schlüsselwort final, dann darf nichts und niemand ihre Methode überschreiben. Schreiben Sie in der Klasse Skywalker einfach:

class Skywalker {
...
    final protected String istJedi() {
        return istEinJedi ? "ja" : "nein";
    }
}

So scheitert jeder Versuch, sich in Subklassen fälschlich als Jedi auszugeben.

Wie kann man Vererbung verhindern?[Bearbeiten]

Wie im vorangegangenen Abschnitt, hilft final weiter. Diesmal setzen Sie final nicht vor eine oder mehrere Methoden, sondern vor die ganze Klasse. Es ergibt beispielsweise wenig Sinn, von einer Klasse "Todesstern" Eigenschaften zu erben, denn er ist schon ein Spezialfall eines Zwergmondes[1] und der Einzige seiner Art:

final class Todesstern {
...
}


  1. vergl. https://xkcd.com/1458/

Wie fordert man zum Implementieren auf?[Bearbeiten]

Man kann Klassen so schreiben, dass sie nichts oder nur einen Teil ihrer Funktionalität selber implementieren. Die Implementierung muss dann in der Subklasse erfolgen. Methoden, die nur aus dem Methodenkopf ohne Implementierung bestehen, nennt man abstrakte Methoden. Ist mindestens eine Methode einer Klasse abstrakt, dann ist es auch gleich die ganze Klasse. Von solchen Klassen kann man kein Objekt erzeugen. Was sollte ein Aufruf der nicht implementierten Methode auch tun? Statische Methoden und Eigenschaften einer abstrakten Klasse können Sie wie gewohnt nutzen. Das Schlüsselwort für abstrakte Klassen und Methoden ist, Sie erraten es, abstract.

abstract class Stormtrooper {
    
    static boolean hatGewehr = true;
    
    Stormtrooper() {
        System.out.println("Ich bin ein Stormtrooper");
    }
    
    // Abstrakte Methode, muss in Subklasse implementiert werden
    abstract public void marschiereLos();    
    
    static void schiesse() {
        System.out.println("peng!");
    }
}


class WeisserStormtrooper extends Stormtrooper {
 
    WeisserStormtrooper() {
        System.out.println("Ich bin ein weißer Stormtrooper");
    }
    
    public void marschiereLos() {
        System.out.println("Auf geht's!");
    }    
}


public class Angriff {
    public static void main(String[] args) {
        
        if(Stormtrooper.hatGewehr) {            
            WeisserStormtrooper s = new WeisserStormtrooper();
            s.marschiereLos();
            Stormtrooper.schiesse();
        }
    }
}

Es muss jede Klasse, die mindestens eine abstrakte Methode enthält, selber abstrakt deklariert werden. Abstrakte Klassen haben Ähnlichkeiten zu Interfaces, die wir später behandeln.

Wie verwendet man Modifizierter in Zusammenhang mit Vererbung?[Bearbeiten]

Sowohl public-, protected- wie auch 'package-private'-Elemente von Klassen werden vererbt. In Subklassen bleibt diese Zugriffsmodifizierung erhalten. Auf private-Elemente können Sie in Subklassen nicht zugreifen. Versuchen Sie, in ihrem Vererbungsbaum so private wie möglich zu bleiben. Verwenden Sie final für alle Methoden und Attribute, die nicht weitervererbt werden sollen. Die Kombination von final und private ergibt bei Methoden keinen Sinn, hier reicht private schon aus. Bei private Konstanten können Sie static verwenden, wenn diese Konstante in allen Instanzen denselben Wert haben soll.

Wer bin ich?[Bearbeiten]

In diesem Buch können wir Ihnen nicht auf alle Fragen eine Antwort geben, aber soviel sei verraten: Wenn Sie ein Objekt im Sinne der OOP von Java sind und sich in ein bestimmtes anderes Objekt umwandeln (casten) lassen, dann können wir prüfen, ob Sie von letztgenanntem Objekt abstammen. Die 'Umwandeln-Bedingung' ist leider eine sehr harte Bedingung, die man nicht umgehen kann. Man kann also nicht prüfen, ob ein Frosch eine Instanz eines Prinzen ist. Weniger märchenhaft ist das Beispiel das zeigt, wie es funktioniert. Drei Klassen stehen durch Vererbung miteinander in Verbindung. Mit instanceof lässt sich nun prüfen, ob Objekte a und c Instanzen der Beispielklassen sind.

class A {
    public A() {}
}

class B extends A {
    public B() {}
}

class C extends B {
    public C() {}
}

class Instanzentest
{
    public static void main(String[] args) {
        A a = new A();
        C c = new C();

        System.out.println(c instanceof A);
        System.out.println(a instanceof A);
        System.out.println(a instanceof C);
        System.out.println(""  instanceof String);
    }
}

Ein Objekt, das sich in ein Objekt eines jeden Typs umwandeln lässt, ist null. null instanceof WasAuchImmer ergibt immer false, weil null niemals Instanz von etwas ist. Das Objekt a kann man nicht gegen zum Beispiel 'String' testen, weil sich a nicht in einen 'String' umwandeln lässt.

Welche Fallen gibt es im Zusammenhang mit OOP?[Bearbeiten]

Im Rahmen der OOP werden Fragen aufgeworfen, die sich mit der Vererbung als Theorie befassen. Im Rahmen der Ersetzbarkeitstheorie[1] ist ein Problem bekannt, das so genannte "Kreis-Ellipsen-Problem", das, bezogen auf unsere Java-Terminologie, wie folgt lautet: Für eine beliebige Superklasse gilt, dass seine Subklasse dieselben Eigenschaften hat. Bei einer Ellipse können Sie beide Radien unabhängig voneinander ändern. Ein Kreis ist bekanntermaßen ein Spezialfall einer Ellipse. Wenn Sie nun den Kreis (Subklasse) von Ellipse (Superklasse) ableiten, dann erhalten Sie einen Kreis mit zwei getrennt voneinander änderbaren Radien. Wenn Sie es nun umgekehrt probieren, also Ellipse von Kreis ableiten, dann stellen Sie fest, dass die Ellipse nichts mit dem einzelnen Radius anfangen kann.

  1. vergl. w:Liskovsches Substitutionsprinzip

Wie baut man sich eine Ausnahme?[Bearbeiten]

Hinter Ausnahmen verbergen sich teilweise recht lange Vererbungsbäume. Die Basisklasse ist 'java.lang.Throwable'. Einer der Konstruktoren benötigt einen String, mit dem die Ausnahme beschrieben wird. Fangen Sie eine solche Ausnahme mit catch ab, dann können Sie mit getMessage() diese Beschreibung erhalten.

Von 'Throwable' werden zwei Klassen abgeleitet namens 'Error' und 'Exception'. Von der Klasse Error sind Ausnahmen abgeleitet, die nicht abgefangen werden sollen, sondern zum Abbruch des Programms führen. Alle Ausnahmen, die abgefangen werden dürfen, erben von der Klasse Exception. Exception ist damit die Basisklasse aller Ausnahmen, mit denen wir uns beschäftigen wollen.

Das Beispiel konstruiert eine Ausnahme, die im Hauptprogramm ausgelöst wird.

class MeineAusnahme extends Exception {

    public MeineAusnahme(String nachricht) {
        super(nachricht);       // Exception-Konstruktor mit Nachricht aufrufen
    }
}

class Ausnahmetest
{
    public static void main(String[] args) {
        // Ausnahme erzeugen
        MeineAusnahme ausnahme = new MeineAusnahme("Hallo, Welt!");
        try {
            throw ausnahme;                     // Ausnahme auslösen        
        }
        catch( MeineAusnahme e ) {
            System.err.println(e.getMessage()); // Nachricht 
            e.printStackTrace();                // Hinweise, wer die Ausnahme wo ausgelöst hat
        }
    }
}

Mit super() wird dabei derjenige Konstruktor der Superklasse Exception ausgewählt, der Nachrichten speichern kann. Das ist ein Vorgriff zu "polymorphen Konstruktoren" aus dem nächsten Kapitel. Die Ausnahme wird erzeugt wie jedes andere Objekt auch, mit throw ausgelöst und mit catch wieder eingefangen. Der "Stacktrace" ist diejenige Programmausgabe, die Sie sehen, wenn ein Programm wegen einer Ausnahme abgebrochen wird. Der Stacktrace enthält den Ort und das Objekt, wo die Ausnahme erzeugt wurde.

Stammen wir wirklich alle von einem gemeinsamen Objekt ab?[Bearbeiten]

Alle Objekte stammen vom der Klasse 'Object' ab, selbst leere Klassen. Die Ausnahme bildet allerdings null, wie Sie sicher schon geahnt haben.

class OhneNamen {}

class ObjectTest
{
    public static void main(String[] args) {
        OhneNamen dummy = new OhneNamen();
        System.out.println(dummy instanceof Object);
     
    }
}

Damit ist Object die Superklasse aller Superklassen. Selbst ein 'Object'-Objekt stammt von Object ab. Jedes Objekt erhält dadurch eine Reihe von Methoden mit auf den Weg, von denen einige in diesem Programm vorgestellt werden:

class OhneNamen {}

class ObjectTest
{
    public static void main(String[] args) {
        OhneNamen dummy = new OhneNamen();
        OhneNamen andererDummy = new OhneNamen();

        System.out.println(dummy.toString());   // OhneNamen + '@' + Zahl
        System.out.println(dummy.equals(dummy) + " " + dummy.equals(andererDummy)); // true false
        System.out.println(dummy.getClass());   // "class OhneNamen"
    }
}

Damit gibt es also einen Anfang aller Vererbung und einen Grundstock an geerbten Methoden. Na, wenn das mal kein Zeichen ist.

Interfaces[Bearbeiten]

Was ist ein Interface?[Bearbeiten]

Interfaces als Java-Typ beschreiben eine öffentliche Schnittstelle der implementierenden Klassen. Schlüsselwort für die Deklaration eines Interface ist interface, welches anstelle der class Deklaration tritt. Ein Interface selbst kann nicht instanziiert werden.

interface MyInterface {}

public class Test implements MyInterface {

  public static void main(String[] args) {
    System.out.println("Test");
  }
  
}

Im einfachsten Fall deklariert ein Interface keinerlei Methoden. Schnittstellen werden benutzt, um sich auf einen gemeinsamen Satz von Eigenschaften zu einigen. Beispielsweise ist die Eigenschaft "sortierbar" zu sein verbunden mit der Möglichkeit, zwei Dinge mit "kleiner oder gleich" vergleichen zu können. Also etwa "Aachen ≤ Dresden" weil "Aachen" weniger (oder gleichviele) Buchstaben wie "Dresden" hat. Algorithmen können sich dann auf einen Satz von Methoden verlassen, die Klassen zu implementieren haben, um als sortierbar zu gelten.

interface Sortierbar {
  boolean ist_kleiner_gleich(int a);
}

class SortierbareKlasse implements Sortierbar {

    private int wert;

    public boolean ist_kleiner_gleich(int x) {
      return wert <= x;
    }
     
    public SortierbareKlasse(int arg) {
      wert = arg;
    }
}
 
public class T  {
  public static void main(String[] args) {
    System.out.println("Test");
  }
}

Eine weitere Sichtweise von Schnittstellen bildet einen Vertrag. Eine Klasse implementiert ein Interface bedeutet, einen Vertrag über die Methoden der Klasse zu erfülllen.

Wie definiert man Konstanten in Schnittstellen?[Bearbeiten]

interface SpieleFussball {
    int SPIELER_ZAHL = 11; // auf jeder Seite, wenn alles gut läuft 
    void renneDurchDieGegend(); 
    void schiesseTor();
}
 
class FcBayernMuenchen implements SpieleFussball { 
 
    FcBayernMuenchen() {
        System.out.println("Bayern steht mit " + SPIELER_ZAHL + " Spielern auf dem Platz.");
    }
     
    public void renneDurchDieGegend() {
        // ...
    }
     
    public void schiesseTor() {
        // ...
    }

}
 
class Fussball
{
    public static void main(String[] args) {
        FcBayernMuenchen fcbm = new FcBayernMuenchen();
    }
}

Wie nutzt man mehrere Schnittstellen?[Bearbeiten]

Wie vererbt man Schnittstellen?[Bearbeiten]

Polymorphie[Bearbeiten]

Überschreiben von Methoden[Bearbeiten]

Beim Überschreiben bekommen abgeleitete Klassen eine eigene Version mindestens einer Methode der Basisklasse.

class Tier {
    public void sagWas() {
        System.out.println("was soll ich denn sagen?");
    }
}


class Biene extends Tier {
    public void sagWas() {
        System.out.println("summmm!");
    }
}


class Frosch  extends Tier {
    public void sagWas() {
        System.out.println("Quak!");
    }
}


public class Überschreibung  {
    public static void main(String[] args) {
    
        Tier[] tiergehege;
        tiergehege = new Tier[3];
    
        tiergehege[0] = new Tier();
        tiergehege[1] = new Biene();
        tiergehege[2] = new Frosch();
    
        for(Tier t  : tiergehege)
            t.sagWas();
    }
}

Worauf muss man beim Überschreiben achten?[Bearbeiten]

Wenn Sie uns bis hierher gefolgt sind, dann wissen Sie, dass Methoden bevorzugt lokal aufgerufen werden. Das sollte beim Überschreiben nicht anders sein. Doch was ist hier lokal, wenn der Konstruktor einer Subklasse eine überschriebe Methode aufruft, deren Original vom Konstruktor der Superklasse aufgerufen wird? Probieren Sie es aus und staunen Sie:

class Superklasse {
    
    public Superklasse() {
        System.out.println("Superklasse, Konstruktor");
        methode();
    }
    
    protected void methode(){
        System.out.println("Superklasse, Methode");
    }
}

class Subklasse extends Superklasse {
    
    public Subklasse() {
        System.out.println("Subklasse, Konstruktor");
        methode();
    }
    
    protected void methode() {
        System.out.println("Subklasse, Methode");
    }
}

public class  AndersAlsManDenkt {
    public static void main(String[] args) {        
        Subklasse s = new Subklasse();
    }
}

Die Ausgabe überrascht. Die Subklasse ruft bei der Initialisierung den Konstruktor der Superklasse auf, der wiederum die überschriebe Variante der Methode methode() in der Subklasse. Man erwartet jedoch, dass der Konstruktor der Superklasse seine eigene, klassenlokale Methode aufruft.

Dieses seltsame Verhalten, dass eine Subklasse das Verhalten von Superklassen bei der Initialisierung ändern kann, kann zu unerwünschtem Verhalten führen. Die Lehre daraus sollte sein, alle vom Konstruktor aufgerufenen Methoden als final zu deklarieren.

Überladen[Bearbeiten]

Beim Überladen hat man mehrere Methoden mit demselben Namen in einer Klasse, die sich alle in der Parameterliste unterscheiden.

public class Überladung  {

    static int meinPlus(int a, int b) {
        return a + b;
    }

    static int meinPlus(int a) {
        return ++a;
    }

    public static void main(String[] args) {

        System.out.println( "meinPlus(3, 4)=" + meinPlus(3, 4) +
                            " meinPlus(17)= " + meinPlus(17) );

    }
}

Überladen von Operatoren[Bearbeiten]

Intern gibt es in Java überladene Operatoren. Der Operator "+" kann Zahlen addieren wie auch Strings zusammenfügen. Bisher gibt es noch keine Möglichkeit, selber Operatoren zu überladen.

Aufzählungstypen[Bearbeiten]

Was sind Aufzählungstypen?[Bearbeiten]

Mit Aufzählungstypen werden Sammlungen von Konstanten bezeichnet. Eine Variable kann nur Werte aus einer aufzählbaren Menge an vorbelegten Konstanten annehmen. Java kennt dafür ein eigenes Schlüsselwort, nämlich enum. Und so sieht es aus:

enum MintFakultaeten {
    MATHEMATIK,
    CHEMIE,
    BIOLOGIE,
    PHYSIK,
    ELEKTROTECHNIK,
    MASCHINENBAU
}

public class EnumTest {
    public static void main(String[] args) {
        MintFakultaeten fakultaet = MintFakultaeten.MATHEMATIK;        
        System.out.println(fakultaet);
    }
}

Variablen kann eine Konstante aus der Menge zugewiesen werden, es wird immer der Typname (MintFakultaeten im obigen Beispiel) der Konstanten vorangestellt. Aufzählungen bilden eine Subklasse von 'java.lang.Enum' und erben damit die Methode toString() von Object. Falls Sie je über die Menge der Konstanten iterieren müssen, probieren Sie es doch mit folgendem Code:

for(MintFakultaeten fak :  MintFakultaeten.values()) {
            System.out.println(fak + " " + fak.ordinal() );
}

Die Dokumentation zu values() zu finden ist übrigens nicht ganz leicht. Ansonsten können Sie mit ordinal() eine Konstante in eine Zahl umwandeln.

Wie werden Aufzählungen erweitert?[Bearbeiten]

Einfache Aufzählungen, wie im letzten Abschnitt gezeigt, bestehen aus Konstanten, die einen Zahlenwert repräsentieren. Konstante Einheiten bestehen aber in der Realwelt zumeist aus deutlich mehr Informationen, ein konstantes Buch etwa aus der Seitenzahl, dem Autor und einer ISBN und eine konstante Fakultät besteht etwa aus einem Dekan und den Studierenden. So etwas zu modellieren ist mit enum ebenfalls möglich:

enum Fakultaet {
    // Fakultäten, Dekan, Anzahl Studierender
    MATHEMATIK("Herr Meyer", 7000),
    PHYSIK("Frau Schulze", 1000),
    CHEMIE("Frau Özdal", 14000);

    // Konstanten durch Konstruktor initialisiert
    final String DEKAN;
    final int ANZAHL_STUDIERENDE;

    // Konstruktor
    private Fakultaet(String dekan, int anzahlStudierende) {
        DEKAN = dekan;
        ANZAHL_STUDIERENDE = anzahlStudierende;
    }
}


class EnumTest
{
    public static void main(String[] args) {
        Fakultaet f = Fakultaet.MATHEMATIK;
        System.out.println(f + " wird vertreten durch " + f.DEKAN + " und hat " + f.ANZAHL_STUDIERENDE  + " Studierende.");
        System.out.println("Wir haben folgende Fakultäten:");
        for(Fakultaet c : Fakultaet.values()) {
            System.out.println(" -> " + c );
        }
    }
}

Die Fakultäten bestehen aus einer kleinen Menge von Konstanten. Zu jeder Konstanten gehören weitere Angaben. Der Konstruktor, der vom Programmierer nicht selber aufgerufen werden kann, baut aus diesen Angaben die enum-Konstanten für die Fakultäten (MATHEMATIK, …). Die Konstanten der Fakultäten müssen ganz oben in der Klasse stehen, der Konstruktor muss private oder packet-private deklariert werden.

Und sonst?[Bearbeiten]

Besuchen Sie doch einmal die Klasse 'java.lang.Enum' in der Originaldokumentation. Dort werden weitere Klassen beschrieben, die die Möglichkeiten der hier vorgestellten enums erweitern.

Pakete[Bearbeiten]

Übungen zur OOP[Bearbeiten]

Programmierstil[Bearbeiten]

TODO einarbeiten:

Was sind Programmierstile?[Bearbeiten]

Programmierstile legen fest, wie Java-Quelltext zu formatieren ist. Dabei geht es nicht um die Funktion ihres Programms, sondern um das Aussehen. Wir haben drei 'Hallo-Welt'-Beispiele vorbereitet.

  • Ein Extrembeispiel spart an Leerzeichen,
class Hallo{public static void main(String[]args){System.out.println("Hallo, Welt!");}}
  • ein Beispiel, bei dem die Blockklammern untereinander gesetzt wurden,
class Hallo
{
    public static void main(String[] args)
    {
        System.out.println("Hallo, Welt!");
    }
}
  • und ein Beispiel, das so formatiert wurde, wie die meisten Quelltexte in diesem Buch.
class Hallo {
    public static void main(String[] args) {
        System.out.println("Hallo, Welt!");
    }
}

Alle drei Beispiele führen dasselbe Programm aus. Nun hat jeder Programmierer seine eigenen Vorlieben. Es haben sich zwei[1] Firmenstandards herausgebildet. Das sind zum Einen die von Sun veröffentlichten "Code Conventions for the Java Programming Language", die hoffnungslos veraltet[2] sind. Zum Anderen der "Google Java Style", den Sie unter http://google-styleguide.googlecode.com/svn/trunk/javaguide.html finden.

Diese "Standards" sind nicht demokratisch legetimiert und so bilden sich innerhalb von Unternehmen, Abteilungen, Universitäten, Fachbereichen, einzelnen Seminaren und Programmiererforen eigene Programmierstile heraus, die sich in Details unterscheiden.

In diesem Kapitel stellen wir in Beispielen den "Google Java Style" vor. Wenn Sie in diesem Buch außerhalb dieses Kapitels Quelltexte finden, die dem nicht entsprechen, dann dürfen Sie nach Herzenslust meckern oder korrigieren, wobei das Korrigieren die für das Buch bessere Alternative ist.

Ein Tipp fürs Leben haben wir noch: Sie sollten niemals(!) über "den richtigen Programmierstil" diskutieren. Die Lebenswochen, die mit solchen Diskussionen ins Land gehen, bekommen Sie nicht zurück. Halten Sie sich in Foren, Universitätsseminaren und so fort einfach an den gegebenen Programmierstil und stecken Sie ihre Leidenschaft in korrekten Code.

Die sich hier anschließenden Abschnitte sind einige wichtige Punkte zum Programmierstil.

  1. Falls Sie weitere Standards kennen, dann immer herein damit ins Buch.
  2. vergl. http://www.oracle.com/technetwork/java/javase/documentation/codeconvtoc-136057.html

Aufbau von Java-Dateien[Bearbeiten]

Java-Dateien enden auf '.java' und beinhalten Unicode-Zeichen. Jede Klasse mit Zugriff public befindet sich in einer eigenen Datei.

Die Reihenfolge der Dateiinhalte ist:

  1. Kommentar mit Copyright-Informationen, falls es die gibt
  2. package-Ausdruck, falls es den gibt
  3. import-Anweisungen, falls es Importe gibt. Importe beziehen sich immer auf genau eine Klasse. Verwenden Sie keine "*"-Importe.
  4. genau eine public-Klasse.

Beispiele

/* Copyright by John User */

package user.lang.java;

import meine.spezielle.Klasse;
import meine.spezielle.AndereKlasse;

public class Testklasse {

}

Aufbau von Klassen[Bearbeiten]

  • Konstruktoren gehören zusammen,
  • weitere Methoden werden nach logischen Gesichtspunkten geordnet,
class Matrix {

  // Konstruktoren
  public Matrix() {}

  public Matrix(final int zeilen, final int spalten) {}

  // Treppennormalformmethoden
  public void addiereZeileZuZeile(final int zeileQuelle, final int zeileZiel){}

  public void tauscheZeileMitZeile(final int zeileQuelle, final int zeileZiel){}

  public void multipliziereZeileMitZahl(final double zahl){}

  // Multiplikationsmethoden
  
}

Regeln zur Benennung[Bearbeiten]

CamelCase
  • Bezeichner bestehen aus ASCII-Zeichen,
  • Paketnamen aus durch Punkte getrennte Worte in Kleinbuchstaben
  • Klassennamen sind Nomen und beginnen mit einem Großbuchstaben. Mehrere Nomen werden mit Binnenmajuskeln zusammengeführt (class BuchSeitenVorlage) (engl. UpperCamelCase)
  • Methodennamen sind Verben oder Verb-Nomen-Kombinationen. Sie beginnen mit einem Kleinbuchstaben, Teilworte werden mit Binnenmajuskeln ergänzt (engl. lowerCamelCase). Die Verben stehen im deutschen Sprachgebrauch im Imperativ (liesBuch()).
  • Namen von Konstanten werden durchgängig großgeschrieben, Teilworte werden durch Unterstriche getrennt (ANZAHL_STECHMUECKEN_IN_SCHWEDEN).
  • Methodenparameter werden klein und mit Binnenmajuskel geschrieben, Parameternamen, die nur aus einem einzelnen Zeichen bestehen, sind zu vermeiden.

Blockklammern[Bearbeiten]

  • Verwenden Sie Blockklammern auch dann, wenn sie optional sind. Das betrifft beispielsweise bedingte Verzweigung mit einer einzeiligen Anweisung,
  • setzen Sie Blockklammern bei catch auf folgende Weise: } catch(…) {, dasselbe gilt für else
  • nach jeder öffnenden Klammer folgt eine neue Zeile,
  • die schließende Blockklammer liegt unter dem ersten Zeichen des Ausdrucks, der beendet wird.

Einrückungen[Bearbeiten]

  • Jeder Block erhöht die Einrückung um genau 2 Leerzeichen,

Beispiele

if (Bedingung) { // neuer Block beginnt
␣␣Anweisung1();
␣␣if (noch eine Bedingung) { // weitere Blockebene
␣␣␣␣mehrAnweisungen();
␣␣}
}

Leerzeilen[Bearbeiten]

  • zwischen zwei Methoden in der Klasse,
  • im Methodenkörper, wenn zusammengehörige Bereiche dadurch kenntlich gemacht werden können.

Leerzeichen[Bearbeiten]

Leerzeichen stehen

  • zwischen reservierten Schlüsselwörtern (catch, if, …) und einer öffnenden Klammer (,
  • vor jeder öffnenden Blockklammer {,
  • nach jeder schließenden Blockklammer, wenn weitere Elemente auf der Zeile folgen (} else {),
  • nach ',', "'", und ':', und der schließenden Klammer von Typumwandlungen
  • beidseitig um //, : in einer for-each-Schleife,

Hierbei gibt es Ausnahmen bei Anmerkungen (engl. annotation) und mehrdimensionalen Feldern mit nur einem Element.

Spezielle Formatierungen[Bearbeiten]

  • Die Elemente einer einfachen Aufzählung kommen in eine Zeile,
  • es wird nur eine Variable pro Zeile deklariert,
  • Felder werden so deklariert, dass die eckigen Klammern sich dem Typ und nicht dem Namen anschließen,
  • switch()-Blöcke enthalten immer einen default-Bereich,
  • numerische Literale (z. B. 100L) werden immer mit Großbuchstaben ergänzt, sofern sie überhaupt Buchstaben enthalten.

Kommentare[Bearbeiten]

  • Kommentare, die mit // … gebildet werden (Zeilenkommentare) brauchen nicht exakt untereinanderstehend ausgerichtet zu werden. Vor und nach dem Kommentarzeichen steht ein Leerzeichen.
  • Blockkommentare werden mit /* Kommentar */ gebildet. Jede neue Zeile im Blockkommentar beginnt mit einem Sternchen-Leerzeichen '*␣'. Sternchen stehen untereinander. Das erste Kommentarzeichen '/' ist auf die gleiche Weise eingerückt, wie der Ausdruck, auf den sich der Kommentar bezieht.

Googles Java Style schreibt übrigens nicht vor, was kommentiert werden soll.

Beispiele

if (Bedingung) { // Kommentar
  Anweisung; // Kommentar, nicht ausgerichtet
} // Ende von if

class Element {

  /* Ein Kommentar
   * über mehrere
   * Zeilen
   */
  Element() {
    
  }

  /* 
   * Das gleiche
   * nochmal
   */
  Element(String elementName) {
    
  }
}




Dokumentationskommentare[Bearbeiten]

Einleitung mit /**
Schluss mit */
Jede Klasse wird mit dem Kommentar /** beschrieben. Dies hat einen wichtigen Grund: Mit dem Dokumentationsprogramm javadoc werden diese speziellen Kommentare ausgelesen und zu einer html-Dokumentation zusammengefasst. Ein gutes Beispiel ist die offizielle Java-API http://java.sun.com/j2se/1.5.0/docs/api/ Sie wurde mit dem javadoc-Programm erstellt - und somit auch mit den Kommentarkonventionen geschrieben.

Vor allem anstehende Aufgaben sollte man mit TODO kennzeichnen, um nicht zu vergessen, dass an der vermerkten Stelle noch etwas zu berichtigen ist

Zeilenlänge[Bearbeiten]

Im Grunde genommen gibt es aus DOS-Zeiten eine Minimaleinschränkung von 80 Zeichen. Bei den größeren Displays heutzutage sind aber Zeilenlängen von 120 Zeichen aber schon in Ordnung. Unschön kann jedoch der Ausdruck von Code auf Papier werden. Daher sollte man sich mit Bedacht auf eines festlegen!

Schlüsselwörter[Bearbeiten]

Die folgenden Schlüsselwörter sind in Java reserviert und dürfen nicht für Variablenbezeichner verwendet werden:

abstract

assert

boolean

break

byte

case

catch

char

class

const

continue

default

do

double

else

enum

extends

final

finally

float

for

goto

if

implements

import

instanceof

int

interface

long

native

new

package

private

protected

public

return

short

static

strictfp

super

switch

synchronized

this

throw

throws

transient

try

void

volatile

while

Die Schlüsselworte const und goto werden nicht verwendet.

Komplexe Typen[Bearbeiten]

Java kennt auch die Definition eigener Typen. Dabei gibt es vier verschiedene Arten eigener Typen: Klassen, Schnittstellen, Aufzählungstypen und Annotationen.

Klassen: class[Bearbeiten]

Eine Klasse wird mit dem Schlüsselwort class definiert. Klassen, die an hierarchisch oberster Stelle in einem Quelltext stehen, heißen nicht-geschachtelte Top-Level-Klassen. Klassen, die in einer anderen Klasse definiert sind, heißen geschachtelte Klassen.

Eine geschachtelte Klasse, die als static deklariert wurde, heißt geschachtelte Top-Level-Klasse. Eine geschachtelte Klasse, die nicht als static deklariert wurde, heißt innere Klasse. Eine Klasse, die in einer Methode definiert wurde, heißt lokale Klasse. Darüberhinaus besteht auch die Möglichkeit, für die einmalige Verwendung Klassen ohne Namen und ohne das Schlüsselwort class zu deklarieren. Solche Klassen heißen anonyme Klassen.

Nicht-geschachtelte Top-Level-Klassen sind der Regel-Fall. Wenn es sich bei einer Klasse um eine nicht-geschachtelte Top-Level-Klasse handelt, darf die Klasse nur die Zugreifbarkeit public oder package default besitzen und außerdem nicht als static deklariert werden. Eine nicht-geschachtelte Top-Level-Klasse, die als public deklariert ist, muss zudem genauso heißen wie die Quelltext-Datei, inklusive Berücksichtigung der Groß- und Kleinschreibung. Das gilt auch unter Betriebssystemen, die eigentlich nicht zwischen Groß- und Kleinschreibung unterscheiden, wie z.B. Microsoft Windows.

Geschachtelte Klassen dürfen jede Zugreifbarkeit tragen.

Sämtliche Klassen dürfen als entweder abstract, final oder keines von beidem, nicht jedoch beides zugleich deklariert werden. Eine als abstract deklarierte Klasse heißt abstrakte Klasse, eine als final deklarierte Klasse heißt finale Klasse.

Es ist nicht möglich, abstrakte Klassen zu instanzieren. Stattdessen muss man sich eine geeignete Subklasse suchen oder selbst schreiben.

Es ist nicht möglich, eine Subklasse einer finalen Klasse zu bilden.

Schnittstellen (Interfaces): interface[Bearbeiten]

Eine Schnittstelle ist eine vollständig abstrakte Klasse. Da eine vollständig abstrakte Klasse keine Schwierigkeiten für die Implementierung in Bezug auf Mehrfachvererbung mit sich bringen kann, gestattet Java die Mehrfachvererbung nur in Bezug auf Interfaces. Um dies konsequent zu gewährleisten, unterscheidet Java streng zwischen Klassen und Interfaces.

Ein Interface kann keine Konstruktoren und somit auch keine Instanzinitialisierer enthalten.

Methoden in Interfaces sind automatisch public und abstract. Sie dürfen keine andere Zugreifbarkeit als public haben. Sie dürfen nicht final, static oder native sein.

Variablen in Interfaces sind automatisch public, static und final. Sie dürfen keine andere Zugreifbarkeit als public haben.

Ansonsten gilt für Interfaces das gleiche wie für Klassen. Interfaces können also auch Typen, z.B. innere Klassen enthalten.

Annotationen (Annotations): @interface[Bearbeiten]

Eine Annotation ist eine spezielle Schnittstelle für die Hinterlegung von Informationen über Java-Programme. Annotationen sind mit der Version 5.0 in Java eingeführt worden und werden hauptsächlich zur Ergänzung der Möglichkeiten von Reflection und zur Code-Generierung verwendet.

Für Annotationen gelten vergleichbar strenge Regeln wie für Interfaces.

Aufzählungstypen (Enumerations): enum[Bearbeiten]

null[Bearbeiten]

null ist kein Schlüsselwort im eigentlichen Sinn sondern ein Literal. Es dient dir dazu den Wert einer komplexen Datentyps zu setzen oder zu prüfen. Es handelt sich insoweit jedoch um einen Bezeichner, welchen du nicht vergeben kannst.

void[Bearbeiten]

void wird verwendet um bei der Signatur einer Methode den Rückgabetyp leer zu deklarieren.

public void ichHabeKeineRueckgabe () {}

package[Bearbeiten]

package deklariert die Paketzugehörigkeit eines komplexen Datentyps. Die Namensgebung eines Pakets sollte eindeutig sein und orientiert sich meist an der URI/URL des Eigentümers bzw. Erstellers. Bei einer Paket angabe handelt es sich grds. um die erste Anweisung in einem Java Quelltext.

package org.wikibooks.de;

import[Bearbeiten]

import importiert komplexe Typen, so dass sie ohne voll qualifizierten Namen verwendet werden können. Es können durch Nutzung des Wildcard * auch alle komplexen Typen eines Pakets importiert werden.

import javax.swing.JFrame;
import java.awt.*;
public class ImportBeispiel {
  public ImportBeispiel () {
    JFrame swingFenster = new JFrame (); // Verwendung mit einfachen import
    javax.swing.JMenuBar swingMenueZeile = new javax.swing.JMenuBar (); // Verwendung ohne import 
// Verwendung mit Wildcard import Frame awtFenster = new Frame (); MenuBar awtMenueZeile = new MenuBar (); } }

Seit Java 5.0 können auch statische Member von Klassen importiert werden.

Literatur und Webverzeichnis[Bearbeiten]

Zitate[Bearbeiten]

Weiterführende Informationen[Bearbeiten]

Bibliographie[Bearbeiten]

Hanspeter Mössenböck, Sprechen Sie Java? Eine Einführung in das systematische Programmieren