Java-Syntax

Duke, das Java-Maskottchen

Die Syntax der Programmiersprache Java ist in der Java Language Specification definiert, ebenso wie die Semantik von Java.

Dieser Artikel gibt einen Überblick über die Java-Syntax und stellt einige ihrer Besonderheiten heraus. Details sind in den Java-Sprachspezifikation von Sun Microsystems aufgeführt.

Terminologie

Da Java eine Programmiersprache ist, zu deren Beschreibung überwiegend englischsprachige Begriffe benutzt werden, aber dennoch in der Literatur ebenfalls eingedeutschte Übersetzungen bestimmter Bestandteile verwendet werden, soll an dieser Stelle über die Mehrdeutigkeit aufgeklärt und ebenfalls die von diesem Artikel verwendeten Ausdrücke festgelegt werden, damit eine Verwechslung ausgeschlossen werden kann. Im Folgenden werden die in diesem Artikel verwendeten Begriffe und ihre englischen oder deutschen Entsprechungen aufgeführt.

Ausdruck
(engl. Expression) Unter einem Ausdruck wird ein beliebig komplexes Sprachkonstrukt verstanden, dessen Auswertung einen einzigen wohl definierten Wert ergibt. Im einfachsten Fall ist ein Ausdruck eine Konstante, wie z. B. das Schlüsselwort true oder auch eine Zahl (z. B. 1 oder 0x7fff). Komplexere Ausdrücke sind Vergleiche oder Berechnungen mit mehreren Variablen und Konstanten. Innerhalb der Grammatikbeschreibungen wird hier häufig noch nach verschiedenen Arten von Ausdrücken (z. B. numerisch, literal etc.) unterschieden, was hier nicht der Fall sein soll.
Anweisung
(engl. Statement, Instruction oder Command; auch Befehl genannt) Eine Anweisung bezeichnet in Java, wie auch in vielen anderen imperativen Programmiersprachen, eine einzelne Vorschrift, die im Rahmen der Abarbeitung des Programms auszuführen ist. Dazu zählen die Deklaration von Variablen, durch Semikolon abgeschlossene Ausdrücke, Kontrollstrukturen, Anweisungsblöcke, Sprunganweisungen, Synchronisierungsblöcke, Rückgaben von Werten und das Auslösen von Ausnahmen.
Anweisungsblock
(engl. Statement Block) Ein Anweisungsblock dient dazu, mehrere Anweisungen zu gruppieren. Dadurch können mehrere Anweisungen wie eine einzelne Anweisung interpretiert werden, was von einigen Sprachkonstrukten vorausgesetzt wird – wie etwa von den Kontrollstrukturen.

Lexikalische Struktur

Java-Programme werden in Unicode geschrieben. Im Gegensatz zu anderen Sprachen können Bezeichner (englisch Identifier) von Klassen, Methoden, Variablen usw. nicht nur die Buchstaben des lateinischen Alphabets und Ziffern enthalten, sondern auch Zeichen aus anderen Alphabeten, wie z. B. deutsche Umlaute oder chinesische Schriftzeichen.

Sämtliche Schlüsselwörter in Java werden klein geschrieben, z. B. „class“ oder „if“.

Buchstabensymbole (englisch literals) sind in Java die kleinstmöglichen Ausdrücke für Zahlen, einzelne Zeichen, Zeichenketten (Strings), logische Werte (true oder false) und das spezielle Wort null.

Trennzeichen (englisch separators) sind verschiedene Arten von Klammern sowie Komma, Punkt und Semikolon.

Java kennt die in Programmiersprachen üblichen logischen, Vergleichs- und mathematischen Operatoren. Die Syntax orientiert sich dabei an der Programmiersprache C++. So dient zum Beispiel das einfache Gleichheitszeichen „=“ als Zuweisungsoperator, während für Vergleiche das doppelte Gleichheitszeichen „==“ verwendet wird.

Leerzeichen, Zeilenenden und Kommentare können an beliebigen Stellen zwischen den Bezeichnern, Schlüsselwörtern, Buchstabensymbolen, Trennzeichen und Operatoren eingefügt werden.

Syntax

Datentypen

Java kennt zwei Datentyparten: primitiver Datentyp und Referenzen auf Objekte. Die primitiven Datentypen sind ein Grund, weshalb Java strenggenommen keine reine objektorientierte Sprache ist.

Primitive Datentypen

Es gibt acht primitive Datentypen. Sie haben unterschiedliche Größen und Eigenschaften und werden zum Berechnen und Speichern diskreter Zahlenwerte benutzt. Alle diese Datentypen verfügen über eine fest definierte Größe, die für alle Plattformen gleich ist. Für jeden Typ existiert eine entsprechende Wrapper-Klasse, um auch diese als echte Objekte behandeln zu können.

DatentypGröße(a)Wrapper-KlasseWertebereichBeschreibung
boolean1 Bitjava.lang.Booleantrue / falseBoolescher Wahrheitswert
char16 Bitjava.lang.CharacterU+0000 … U+FFFFUnicode-Zeichen (= Symbol) (z. B. 'A' oder '\uC3A4')
byte8 Bitjava.lang.Byte−128 … +127Zweierkomplement-Wert
short16 Bitjava.lang.Short−32.768 … +32.767Zweierkomplement-Wert
int32 Bitjava.lang.Integer−2.147.483.648 … +2.147.483.647Zweierkomplement-Wert
long64 Bitjava.lang.Long−9.223.372.036.854.775.808 …
+9.223.372.036.854.775.807
Zweierkomplement-Wert
float32 Bitjava.lang.Float±1,4E−45 … ±3,4E+38Gleitkommazahl (IEEE 754)
double64 Bitjava.lang.Double±4,9E−324 … ±1,7E+308Gleitkommazahl doppelter Genauigkeit (IEEE 754)
(a) 
Gibt die Größe des Wertebereichs an. Der tatsächliche Speicherbedarf ist abhängig von Plattform und Implementierung der Java Virtual Machine.
Typumwandlung

Die Hierarchie für das Umwandeln (type casting) der primitiven Datentypen lässt sich aus der oberen Tabelle erahnen. Die numerischen Datentypen lassen sich verlustfrei in den nächstgrößeren Datentyp umrechnen, jedoch nicht umgekehrt. So kann ein „int“ implizit, also ohne speziellen Operator, in ein „long“ umgewandelt werden, jedoch nicht umgekehrt.

int i = 12345;
long l = i;

Um jedoch in umgekehrter Richtung beispielsweise einen „long“ in ein „int“ umzuwandeln, muss der Typumwandlungsoperator verwendet werden. Dabei können auch Informationen verloren gehen.

long l = 12345678901L;
int i = (int) l;

Hier tritt ein Informationsverlust auf: „i“ hat nach der Zuweisung den Wert −539222987, weil die höherwertigen Bytes abgeschnitten werden.

Der primitive Datentyp „boolean“ kann in keinen anderen Datentyp umgewandelt werden. Zeichen vom Typ „char“ können implizit in jeden ganzzahligen Typ ab „int“ umgewandelt werden. Dies hängt insbesondere damit zusammen, dass „char“ den einzigen in Java bekannten vorzeichenlosen Datentyp darstellt. Damit ist eine verlustfreie Konvertierung nach „short“ ab dem Wert 32768 nicht mehr möglich.

Referenzen

Alle Objekte und Felder liegen im Heap-Speicher und werden deshalb über eine Adresse referenziert. Der Objektzugriff in Java ist über Referenzen implementiert, welche den aus C/C++ bekannten Zeigern ähneln.[1] Die Sprachdefinition (Java Language Specification) bezeichnet sie als „Reference Values“, um deutlich zu machen, dass sie durch Call-by-Value übergeben werden.[2] In Java gibt es keine direkte Möglichkeit, die Speicheradresse einer Referenz anzuzeigen oder diese zu modifizieren, wodurch sogenannte Zeigerarithmetik in Java ausgeschlossen wird.

Object a = new Object();  // a referenziert das gerade neu erstellte Objekt
Object b = a;             // b referenziert dasselbe Objekt wie a
a = null;                 // a referenziert kein Objekt mehr, enthält somit die reservierte Adresse null.

Hierbei gilt es festzuhalten, dass auch Zeichenketten (Klasse String) Referenztypen darstellen und daher inhaltlich stets über die Methode equals() zu vergleichen sind („==“ kontrolliert nur die Referenzen, d. h. Speicheradressen). Dies steht im Gegensatz zu Programmiersprachen wie C++ und C#, welche die vollständige Operatorenüberladung unterstützen und bei "hallo"=="hallo" Inhaltsvergleiche durchführen.

Reservierte Wörter

const und goto sind zwar reserviert, aber ohne Funktion, also keine Schlüsselwörter im eigentlichen Sinn. Sie dienen lediglich dem Compiler zur Ausgabe sinnvoller Fehlermeldungen für Umsteiger von C++ oder C.

true, false und null sind Literale, jedoch ebenfalls eigentlich keine Schlüsselwörter im engeren Sinn.

Mit assert werden Assertions realisiert.

private, protected und public sind Zugriffsmodifikatoren (access modifier):

Die Klasse selbstPaket-Klassen/
innere Klassen
UnterklassenSonstige
Klassen
privateJaNein[A 1]NeinNein
(ohne)[A 2]JaJaNeinNein
protectedJaJaJaNein
publicJaJaJaJa
  1. Um inneren Klassen den Zugriff auf private Methoden und Eigenschaften dennoch zu ermöglichen, werden vom Compiler statische, paket-private Methoden erstellt, die den Aufruf, das Setzen oder das Auslesen emulieren. Diese Methoden tragen den Namen access$xxx, wobei xxx für eine fortlaufende Nummer steht.
  2. Häufig auch als „package private“ bezeichnet, obwohl es diesen Zugriffsmodifikator nicht gibt.

Private Methoden sind von der Polymorphie ausgenommen, d. h. die Definition einer Methode derselben Signatur in einer Subklasse gilt nicht als Überschreiben (sondern als Verbergen).

static kennzeichnet Implementierungen, die ohne eine Objektreferenz verwendbar sind. Das Schlüsselwort wird bei der Deklaration von Feldern, Methoden und inneren Klassen verwendet. Felder, Methoden und Klassen, die mittels static gekennzeichnet sind, werden im Kontext der Klasse verwendbar und sind an kein Objekt gekoppelt.

abstract kann vor Klassen und Methoden stehen. Abstrakte Methoden verfügen über keine Implementierung und müssen in abgeleiteten Klassen überschrieben werden. Klassen, die abstrakte Methoden enthalten, müssen selbst als abstrakt deklariert werden. Abstrakte Klassen können nicht instanziiert werden, auch wenn sie vollständig implementiert sind, wie z. B. im Fall der EventListener-Adapterklassen des Swing-Frameworks.

final kann vor Variablen, Feldern, Methoden und Klassen stehen und erzwingt Unveränderlichkeit. Bei Variablen und Membervariablen ihre Konstanz, bei Methoden die Unmöglichkeit, sie in abgeleiteten Klassen zu überschreiben und schließlich bei Klassen, dass von ihnen keine weiteren Ableitungen erfolgen können. Einer finalen Variable wird einmalig ein Wert zugewiesen, der nachträglich nicht mehr verändert werden kann. Eine finale Variable muss nicht in der Deklaration initialisiert werden, sondern kann auch erst in folgenden, alternativen Anweisungen einmalig belegt werden:

public int calcFinal(int a) {
  final int b; // hier findet noch keine Zuweisung statt
  switch (a) {
    case 1:
       b = 7;
       break;
    case 2:
       b = 3;
       break;
    default:
       b = 1;
       break;
  }
  return b;
}

Zugriffe auf finale Variablen, deren Wert dem Compiler bekannt ist, dürfen vom Compiler durch den Wert der Variable ersetzt werden. Aufrufe finaler Methoden dürfen vom Compiler durch eingebundenen Code (Inlining) ersetzt werden. Private Methoden sind automatisch final.

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 eingebunden werden muss.

strictfp kennzeichnet Klassen und Methoden, deren enthaltene Gleitkommaoperationen streng nach IEEE 754 ablaufen müssen.

package deklariert die Paketzugehörigkeit eines komplexen Datentyps. Die Namensgebung eines Pakets sollte eindeutig sein und orientiert sich meist an der Internet-Domain des Eigentümers oder Erstellers.

import importiert Symbole (vorher nur Typen, ab Java 5 auch statische Member von Klassen), so dass sie ohne voll qualifizierten Namen verwendet werden können. Der Import kann hierbei über das Wildcard * auf komplette Pakete ausgedehnt werden.

boolean, char, byte, short, int, long, float und double sind Typen. void ist der Nichtstyp, notwendig, um Methoden ohne Rückgabewerte zu kennzeichnen. Für die primitiven Typen: siehe oben.

class, interface, enum und @interface dienen zur Deklaration eigener Typen: Klassen, Interfaces (Schnittstellen für Klassen), Enums (Aufzählungstyp für typsichere Aufzählung, englisch typesafe enumeration) und Annotations für Metadaten. enum und @interface sind mit Java 5 in die Sprache aufgenommen worden.

try, catch, finally, throw, throws beziehen sich auf die Ausnahmebehandlung (englisch exception handling). Mit throw wird eine Ausnahme ausgelöst. Alle eventuell ausgelösten Ausnahmen einer Methode, die nicht von RuntimeException oder Error abstammen, müssen mit throws in der Deklaration der Methode angegeben werden. Es handelt sich also um so genannte checked exceptions. try umschließt einen Block, in dem eventuell eine Ausnahme auftreten könnte. catch fängt nach einem try-Block die dort aufgetretene Ausnahme ab, finally wird an einen try- oder catch-Block für Aufräumarbeiten wie das Schließen von Dateien angehängt.

extends und implements dienen der Vererbung: extends der genetischen Erweiterungsvererbung von Klasse zu Klasse oder Interface zu Interface und implements der Implementierungsvererbung von Interface zu Klasse. Außerdem wird extends bei Generics für Typerweiterung verwendet.

super und this beziehen sich im Objekt-Kontext auf das aktuelle Objekt in seinem tatsächlichen Morph (this) oder im Morph der Superklasse (super). this wird verwendet, um in Konstruktoren überladene Konstruktoren aufzurufen und um in Membern auf verdeckte Member äußerer Strukturen zu verweisen. super wird in Konstruktoren zum Aufruf des Superklassenkonstruktors und in überschreibenden Methoden zum Aufruf der überschriebenen Methode verwendet. Außerdem wird super bei Generics für Typeingrenzung verwendet.

new reserviert Speicher für neue Objekte (inklusive Arrays) auf dem Heap.

if, else, switch, case, default, while, do, for, break, continue dienen der Bedingungsprüfung und Schleifensteuerung und bedienen sich einer imperativen Syntax.

return liefert Werte aus einer Methode an die aufrufende Methode zurück.

volatile ist ein Modifikator für nicht-lokale Variablen und verbietet dem JIT-Compiler Registeroptimierungen auf diese Variable, weil mehrere Threads sie gleichzeitig verwenden könnten (insbesondere im Kontext nativer Methoden).

synchronized kennzeichnet einen kritischen Abschnitt im Quelltext, der nur von einem Thread gleichzeitig ausgeführt werden darf. Ein Monitor sperrt den Abschnitt, sobald ihn ein Thread betritt. Versucht ein anderer Thread den gesperrten Abschnitt zu betreten, blockiert dieser Thread so lange, bis der Monitor den Abschnitt freigibt. Jedes beliebige Java-Objekt kann als Monitor verwendet werden. Ist eine Methode mit synchronized gekennzeichnet, wird automatisch das Objekt bzw. bei statischen Methoden das Klassenobjekt als Monitor verwendet.

transient kennzeichnet nicht-persistente Variablen, die nicht serialisiert werden dürfen.

instanceof ist ein Java-Operator, der prüft, ob ein Objekt Exemplar eines angegebenen Typs oder Subtyps ist.

Pakete, Namen, Klassen, Schnittstellen

Ein Java-Paket (englisch package) enthält mehrere Klassen, Schnittstellen und Ausnahmen und bildet einen eigenen Namensraum.

Java-Quelltext ist auf mehrere Übersetzungseinheiten (englisch compilation units) aufgeteilt, von denen jede in einer eigenen Datei abgelegt ist. Jede einzelne Übersetzungseinheit definiert als erstes das Paket, dem sie angehört, importiert dann mehrere Klassen oder Schnittstellen aus anderen Paketen, und definiert schließlich eine oder mehrere Klassen und Schnittstellen.

Java unterscheidet einfache Namen, die nur aus einem Bezeichner bestehen, und vollqualifizierte Namen, die aus einer Reihe von Bezeichnern bestehen, die durch Punkte getrennt sind. Die einfachen Namen sind nur ein Hilfsmittel, das dem Programmierer Tipparbeit ersparen und die Lesbarkeit des Programms erhöhen soll. Der Compiler übersetzt sie immer in vollqualifizierte Namen.

Das folgende Beispiel zeigt ein Hallo-Welt-Programm in Java.

package org.wikipedia;

import static java.lang.System.out;

public class HalloWelt {

    public static void main(String[] arguments) {
        out.println("Hallo Welt.");
    }

}

In diesem Beispiel wird eine Klasse „HalloWelt“ definiert, die dem Paket „org.wikipedia“ angehört. Per Konvention orientieren sich die Paketnamen in Java an den Internet-Domänen ihrer Entwickler, in diesem Fall also „org.wikipedia“ für die Domäne „wikipedia.org“. Die Richtung des Namens wird umgedreht, weil bei Internet-Domänen der Name der äußersten Einheit hinten steht, während er in Java vorne steht. Das Paket „org.wikipedia“ befindet sich also „innerhalb“ des Pakets „org“.

Eine Import-Anweisung definiert einen einfachen Namen für einen vollqualifizierten Namen. So definiert z. B. die Anweisung „import java.io.File“ den einfachen Namen „File“ für die Klasse, die mit vollem Namen „java.io.File“ heißt. Neben Imports von Klassen und Schnittstellen können seit Java Version 5 auch statische Felder oder Methoden von Klassen importiert werden. Ein solcher „statischer Import“ wird durch das zusätzliche Schlüsselwort „static“ festgelegt. Im obigen Beispiel wird das statische Feld „out“ der Klasse „java.lang.System“ importiert und anschließend unter dem kurzen Namen „out“ verwendet.

Das wichtigste Konstrukt der Programmiersprache Java ist, da sie eine objektorientierte Sprache ist, die Klasse. Die Deklaration einer Klasse wird mit dem Schlüsselwort „class“ eingeleitet. Anschließend folgt der Name der Klasse und dann werden – in geschweiften Klammern – die Felder und Methoden der Klasse definiert.

Eine Besonderheit von Java stellt die Schnittstelle dar. Eine solche Schnittstelle besteht nur aus Methoden-Deklarationen, deren Implementierung erst von den Klassen festgelegt werden, die sie „implementieren“. Die Deklaration einer Schnittstelle sieht ähnlich aus wie die Deklaration einer Klasse, sie wird jedoch mit dem Schlüsselwort „interface“ eingeleitet.

package org.wikipedia;

public interface Article {

    String getName();

    void setContent(String aContent);

}

Da alle Methoden einer Schnittstelle abstrakt sind, kann das Schlüsselwort „abstract“ bei den einzelnen Methoden entfallen. Ebenso kann das Schlüsselwort „public“ vor den einzelnen Methoden entfallen, weil Methoden einer Schnittstelle immer öffentlich sind.

Ab Java 8 kommt die Möglichkeit hinzu, in Schnittstellen statische Methoden sowie „default“-Methoden zu definieren. Darüber hinaus können Schnittstellen mit der Annotation „@FunctionalInterface“ versehen werden, um anzuzeigen, dass es sich bei ihr um eine funktionale Schnittstelle handelt. Eine solche ist dadurch definiert, dass sie genau eine abstrakte Methode deklariert; die Annotation ist nicht dafür erforderlich. Funktionale Schnittstellen können kurz und bündig durch Lambda-Ausdrücke oder Methodenreferenzen implementiert werden.

Methoden

Das eigentliche Verhalten eines Objekts wird in Methoden definiert. Die Signatur einer Methode besteht aus ihrem Namen und den Typen ihrer Parameter. Außerdem hat jede Methode einen bestimmten Rückgabetyp oder „void“, wenn sie nichts zurückgibt, und kann eine Reihe von Ausnahmen in einer so genannten „throws“-Klausel definieren.

Beispiel einer konkreten Methode:

public double summe(double a, double b) throws NotANumberException {
    if (Double.isNaN(a) || Double.isNaN(b)) {
        throw new NotANumberException();
    }
    return a + b;
}

Diese Methode erwartet zwei doppelt genaue Gleitkommazahlen als Parameter und gibt die Summe der beiden ebenfalls als Gleitkommazahl zurück. Falls eine der beiden Zahlen keine gültige Gleitkommazahl ist, wirft die Methode eine Ausnahme namens „NotANumberException“.

Beispiel einer abstrakten Methode:

public abstract double summe(double a, double b) throws NotANumberException;

Attribute

Attribute sind Variablen, die zu einem Objekt gehören. Sie sind durch einen Typ und einen Namen definiert und können wahlweise bereits wenn das Objekt erzeugt wird initialisiert werden.

Beispiel einer Variablendeklaration ohne Initialisierung:

private int x;

Klassenvariablen werden als statische Attribute deklariert. Diese Attribute existieren nicht einmal pro Objekt, sondern nur einmal pro Klasse. Das Schlüsselwort „final“ verhindert, dass eine Variable nach ihrer Initialisierung ihren Wert ändert. Es wird zum Beispiel verwendet, um Konstanten zu definieren.

Beispiel einer statischen und finalen Klassenvariablendeklaration mit Initialisierung:

private static final int X = 2;

Felder

In der Programmiersprache Java ist ein Feld (englisch Array) ein eigenständiges Objekt. Dieses kann Daten eines bei der Deklaration festgelegten Typs aufnehmen. Der Speicher wird erst bei der Initialisierung des Feldes belegt. Dazu muss eine Länge (englisch Size) angegeben werden, die beschreibt wie viele Elemente eines Typs das Feld besitzen soll. Die Größe eines Arrays ist fix und kann nach der Instanziierung nicht mehr geändert werden. In Java wird ein Feld immer mit einem positiven Integer indiziert. Dadurch beschränkt sich die mögliche Länge eines Arrays auf 0 bis 231 − 1.

Die einzelnen Elemente eines Feldes werden – analog zu den Attributen anderer Objekte – bei der Instanziierung des Feldes mit dem Nullwert des jeweiligen Feld-Grundtyps initialisiert. Für Felder von numerischen Primitivtypen ist dies 0 bzw. 0.0, für ein Feld mit dem Grundtyp boolean ist es der Wert false, für Felder von Objektreferenzen die leere Referenz null. Bei der Erzeugung eines Feldes von Objektreferenzen werden also außer dem Feld selbst noch keine Objekte erzeugt.

Echte mehrdimensionale Felder gibt es in Java nicht. Dennoch ist es möglich, mehrdimensionale Felder zu erstellen, indem Felder in Feldern verschachtelt werden. Im Falle eines zweidimensionalen Feldes wird hier zunächst ein Feld erstellt, welches Referenzen auf Felder des gewünschten Typs aufnehmen kann. Anschließend wird jedem Platz dieses Feldes eine Referenz auf ein eindimensionales Feld des gewünschten Grundtyps zugewiesen.

Eindimensionales Feld

Deklaration

<Datentyp>[] <Variablenbezeichner>; // Deklaration
<Datentyp> <Variablenbezeichner>[]; // Alternative Deklaration (seltener verwendet)

Bei der Deklaration wird der Datentyp des Feldes festgelegt. Dabei wird jedoch kein Feld allokiert, sondern wie bei allen anderen Objekten nur eine Referenz eines Types definiert, der ein Feld zugewiesen werden kann.

Instanziierung

<Variablenbezeichner> = new <Datentyp>[<Anzahl>]; // Instanziierung eines eindimensionalen Feldes

Bei der Instanziierung eines eindimensionalen Feldes muss sowohl dessen Datentyp als auch die Anzahl der Elemente bekannt sein. Wie bereits am Anfang angemerkt ist die Länge eines Feldes auf nichtnegative ganzzahlige Werte beschränkt. Das Schlüsselwort new hat hier dieselbe Bedeutung wie bei allen anderen Objekten und legt eine Instanz des Typs an. Dabei wird der benötigte Speicher für das Objekt als auch für dessen Elemente reserviert. Im Gegensatz zu anderen Objekten verfügen Felder aber über keine Konstruktoren und es ist nicht möglich, durch explizite Ableitung Subtypen eines Feldtyps zu bilden.

Beispiel eines eindimensionalen Feldes des Typs int der Länge 3

int[] feld = new int[3];  // auf der linken Seite wird die Objektreferenz deklariert
                          // auf der rechten Seite wird das Feld initialisiert

Die Deklaration und Instanziierung muss nicht getrennt voneinander erfolgen, sondern kann wie üblich auch als eine Anweisung geschrieben werden. Beim Zugriff auf die Elemente ist zu beachten, dass Java mit dem Index 0 anfängt zu zählen. So wird in diesem Beispiel mit Index 0 auf das erste Element zugegriffen und mit Index 2 auf das letzte Element.

int[] feld = {1,2,3}

ist eine Verkürzung von

int[] feld = new int[3];
feld[0]=1;
feld[1]=2;
feld[2]=3;

Auslesen von Werten

int a = feld[0];         // das Auslesen erfolgt durch Angabe des Index
objekt.methode(feld[1]); // Einzelwerte eines Feldes können wie Variablen des gleichen Typs verwendet werden
int b = feld[3];         // würde in diesem Beispiel eine IndexOutOfBoundsException auslösen, da es kein 4. Element gibt

Zuweisung von Werten

feld[0] = 5;             // Java ist "nullbasiert"
feld[1] = (int) 2.7;     // bei Zuweisungen muss wie bei normalen Variablen der Typ angepasst werden
feld[2] = 0;             // die Indexierung läuft immer bis n-1

Länge eines Feldes Viele Algorithmen benötigen die Länge eines Feldes, etwa um alle Einträge abzuarbeiten. Die Länge eines Feldes ist zur Laufzeit bekannt und kann über die Objektvariable length abgefragt werden, die selbst vom Typ int ist. Diese Variable ist stets konstant und kann nicht geändert werden. Eine Zuweisung eines anderen Wertes ist also nicht möglich.

int len = feld.length;   // Liest die Länge des Feldes aus und speichert sie in der Variablen len
feld.length = 5;         // diese Zuweisung würde einen Fehler beim Kompilieren ergeben, da sie nicht zulässig ist

Mehrdimensionale Felder

Beispiel der Konstruktion eines zweidimensionalen double-Arrays.

Wie bereits angemerkt, kennt Java keine mehrdimensionalen Felder. Dennoch können Felder ineinander verschachtelt werden, um so eine analoge Struktur zu erhalten. Ein zweidimensionales Feld ist dabei mit einer Tabelle vergleichbar, wobei das erste (äußere) indizierende Feld auf die Zeile verweisen würde. Die Zeile selbst ist wiederum ein Feld, dessen Indizes auf die Spalte der Zeile, also auf den entsprechenden Eintrag, verweist, wie es auch in der Abbildung ersichtlich ist.

In Java können mehrdimensionale Felder durch die Angabe mehrerer eckiger Klammerpaare [] realisiert werden, wobei jedes Paar für eine weitere Dimension des Feldes steht. Das äußere Feld, welches das nächstinnere indiziert, steht dabei auf der linken Seite, bzw. ist das Klammerpaar, das als erstes definiert wurde. Dies ist am Beispiel eines Bildes ersichtlich, das üblicherweise mit [y][x] indiziert werden würde, wobei hier zunächst die Zeile (y) ermittelt wird und anschließend die Spalte (x) aus der Zeile.

Beispiel der Erstellung eines zweidimensionalen Feldes

double[][] feld = new double[zeilen][spalten];

Bei diesem Beispiel ist zu beachten, dass nicht nur das äußere Feld (hier feld genannt) allokiert wird, sondern auch die Felder für die „Zeilen“. Sie sind also nicht null und wurden ebenfalls mit angelegt. Ihre Einträge werden, wie von einem eindimensionalen Feld zu erwarten, mit 0 initialisiert.

In einer Matrix werden die waagrechten Felder als Zeilen, die Senkrechten als Spalten bezeichnet. Ein einzelnes Element ist durch den jeweiligen Zeilen- und Spaltenindex eindeutig bestimmt. Die Syntax für eine Matrix oder allgemein für mehrdimensionale Felder ist fast identisch. Der Unterschied liegt in der Anzahl der geschlossenen eckigen Klammern, die Auskunft darüber geben, wie viele Dimensionen das Feld haben wird.

Beispiel der Erstellung eines zweidimensionalen Feldes mit unterschiedlichen Größen in den „Unterfeldern“

double[][] feld = new double[3][];
feld[0] = new double[3];
feld[1] = new double[2];
feld[2] = new double[1];

Bei diesem Beispiel ist zu beachten, dass im ersten Befehl nur das äußere Feld (hier feld genannt) allokiert wird und die einzelnen Elemente mit null initialisiert werden. In den folgenden Befehlen werden diesen Elementen dann die „Unterfelder“ zugewiesen, es ergibt sich hier ein „dreieckiges“ Feld.

Wie bei eindimensionalen Arrays gibt es auch bei mehrdimensionalen Arrays eine verkürzte Variante:

int[][] array2D={{1,2,3},{4,5,6}};
//Entspricht "=new int[2][3];" mit den darauf folgenden Zuweisungen
//"array2D[0][0]=1;"
//"array2D[0][1]=2;"
//(...)
//"array2D[1][2]=6;"

Wo die eckigen Klammern ([]) gesetzt werden, bleibt dem Programmierer überlassen:

int[][] A

ist das Gleiche wie

int A[][]

oder auch

int[] A[]

Allgemeine Syntax

<Datentyp> [] <Feldvariable> = new <Datentyp> [<Anzahl>] ;

Die Auslassungspunkte ‘…’ stehen hier stellvertretend für weitere Klammerpaare, die jeweils eine weitere Dimension erzeugen. Der Zugriff auf die Elemente erfolgt analog zu denen eines eindimensionalen Feldes. Dabei ist jedoch zu beachten, dass auch auf das jeweilige äußere Feld zugegriffen werden kann, um z. B. die Anzahl der Zeilen zu bestimmen. Ausgehend vom Beispiel eines zweidimensionalen Feldes kann die Anzahl wie folgt bestimmt werden.

Auslesen und zuweisen von Werten

double wert = feld[y][x];                // liefert den Wert des Feldes x,y
feld[y][x] = wert;                       // weist dem Feld x,y einen Wert zu

Bestimmung der Länge des Feldes und der Unterfelder

int zeilenanzahl = feld.length;          // weist der Variable "zeilenanzahl" den Wert von "feld.length" zu

Die Länge der Unterfelder kann über den Index des Feldes bestimmt werden.

int spaltenanzahl = feld[index].length;  // weist der Variable "spaltenanzahl" den Wert von "feld[index].length" zu

In Langform, bzw. nach einzelnen Schritten aufgelöst, würde dies wie folgt aussehen. Daraus wird ebenfalls ersichtlich, dass es sich bei den Zeilen um Objekte – eindimensionale Felder – handelt.

double[] zeilenfeld = feld[index];       // ermittelt das erste "Unterfeld" mit gegebenen Index
int spaltenanzahl = zeilenfeld.length;   // weist der Variable "spaltenanzahl" den Wert von "zeilenfeld.length" zu


Anweisungen und Kontrollstrukturen

Struktogramm eines Linearen Programms

Java unterstützt die üblichen Anweisungen, die auch aus anderen imperativen Programmiersprachen bekannt sind. Einfache Anweisungen werden mit Semikolon abgeschlossen. Mehrere Anweisungen können mit geschweiften Klammern zu einem Anweisungsblock zusammengefasst werden. Kontrollstrukturen werden ebenfalls wie Blöcke behandelt und müssen (bis auf eine Ausnahme) daher nicht mit einem Semikolon abgeschlossen werden.

Als Kontrollstrukturen stehen die Bedingte Anweisung und Verzweigung und Case-Anweisung, sowie die While-, Do-while- und For-Schleife und ihre verschiedenen Ausprägungen zur Verfügung.

Bedingte Anweisung und Verzweigung

Struktogramm einer if-Anweisung
Struktogramm einer if-else-Anweisung

Die Bedingte Anweisung und Verzweigung, im Folgenden If-Anweisung genannt, ermöglicht es Anweisungen unter Voraussetzung einer bestimmten Bedingung ausführen zu lassen. Dazu wird das Schlüsselwort „if“ verwendet. Dahinter folgt ein Paar von runden Klammern, in dem die Bedingung definiert werden kann. Die Bedingung ist ein Ausdruck der als Ergebnis einen booleschen Wert (true oder false) liefern muss. Dies unterscheidet Java von anderen Sprachen wie etwa C, bei denen auch andere Werte als Bedingung erlaubt sind. Weiterhin ist anzumerken, dass das häufig anzutreffende Schlüsselwort „then“ nicht existiert.

Das Schlüsselwort „else“ kann verwendet werden um eine Anweisung im Falle des Nichtzutreffens der Bedingung auszuführen. Es ist optional und in diesem Fall spricht man von einer Verzweigung, da entweder die erste oder die zweite Anweisung ausgeführt wird. Alternativ kann anstatt „else“ ein „else if“ verwendet werden, das wie ersteres „if“ eine weitere Bedingung erwartet. Dadurch ist möglich Kaskaden von Verzweigungen aufzubauen. Ohne Strukturierung durch Blöcke (geschweifte Klammern) besteht jedoch die Gefahr eines Dangling else, bei dem nicht eindeutig ist, welcher If-Anweisung der Else-Zweig zuzuordnen ist.

Allgemeine Syntax

if (<Bedingung>)
    <Anweisung>;
else
    <Anweisung>;

Beispiel für eine einfache If-Anweisung mit einem Block von zwei Ausgaben

if (i < 0) {
    System.out.print("Die Zahl ist negativ"); // Bedingte Ausgabe
    System.out.println();                     // Zeilenumbruch auf der Konsole
}

Komplexeres Beispiel

if (i < 0) {
    System.out.println("Die Zahl ist negativ");
}
else if (i == 0) {
    System.out.println("Die Zahl ist Null");
}
else if (i < 5) {
    System.out.println("Die Zahl ist kleiner als 5");
}
else {
    System.out.print("Die Zahl ist größer oder gleich 5");
}

Der Ternäre Operator

Für eine einfache If-Anweisung existiert eine zusätzliche Kurzschreibweise. Diese kann innerhalb eines Ausdrucks verwendet werden, ist aber auf ein „Entweder-oder“ beschränkt. D. h. sie muss in jedem Fall einen konkreten Wert zurückliefern. Der Operator selbst ist das Fragezeichen ‘?’ vor dem ein boolescher Ausdruck stehen muss. Hinter dem Fragezeichen folgt der Rückgabewert für den Fall, dass der boolesche Ausdruck wahr (true) ist. Anschließend folgt ein Doppelpunkt ‘:’ nach dem wiederum der Rückgabewert folgen muss, falls der boolesche Ausdruck nicht wahr (false) ist.

Syntax

<Bedingung> ? <Ausdruck> : <Ausdruck>

Beispiel

int a = (i<0) ? -i : i; // kehrt vor der Zuweisung an a das Vorzeichen von i um, wenn i negativ ist (Betrag von i)

Switch-Anweisung

Struktogramm einer Switch-Anweisung
  • Eine Fallunterscheidung, wobei der Ausdruck ein Zeichen, eine Ganzzahl, eine Zeichenkette oder ein Aufzählungstyp sein kann.
switch (''Ausdruck'') {
   case ''Konstante1'': ''Anweisung1''; break;
   case ''Konstante2'': ''Anweisung2''; break;
   default: ''Anweisung3'';
}

Schleifen

Java unterscheidet vier verschiedene Arten von Schleifen.

  • Wiederhole eine Anweisung solange die Bedingung wahr ist, wobei die Bedingung vor der Anweisung geprüft wird (While-Schleife).
while (''Bedingung'') ''Anweisung'';
  • Wiederholung einer Anweisung solange die Bedingung wahr ist, wobei die Bedingung erst nach der Ausführung geprüft wird (Do-while-Schleife).
do ''Anweisung''; while (''Bedingung'');
  • Initialisiert einen Wert, wiederholt die Anweisung solange die Bedingung wahr ist und führt eine zweite Anweisung nach jedem Schleifendurchlauf aus (For-Schleife).
for (''Initialisierung''; ''Bedingung''; ''Anweisung2'') ''Anweisung1'';
  • Zudem existiert eine erweiterte For-Schleife, auch Foreach-Schleife genannt, die es erlaubt, für jedes Element eines iterierbaren Objektes (Reihungen, Listen etc.) Anweisungen auszuführen.
for (''Element'' : ''Kollektion'') ''Anweisung'';

Die nachfolgenden Beispiele zeigen den Einsatz einer solchen erweiterten For-Schleife, einmal angewendet auf den Basisdatentyp char und einmal auf eine Liste.

char[] chars = new char[] { 'a', 'b', 'c' }; // neues Zeichen-Array anlegen
for (char c : chars) {
    System.out.println("Zeichen: " + c); // Jedes Zeichen auf die Konsole schreiben.
}

List<Character> charlist = new ArrayList<Character>(); // neue Zeichen-Liste anlegen
charlist.add('a'); // Werte hinzufügen (mit Autoboxing)
charlist.add('b');
charlist.add('c');
for (Character c : charlist) {
    System.out.println("Zeichen: " + c); // Jedes Zeichen auf die Konsole schreiben.
}

Ausdrücke

Im Gegensatz zu anderen Programmiersprachen, wie beispielsweise C, ist die Reihenfolge der Auswertung von Ausdrücken in Java bereits in der Sprachdefinition festgelegt: Binäre infix-Operatoren gleichen Rangs, die keine Zuweisungsoperatoren sind, werden immer von links nach rechts ausgewertet. Außerdem gilt – wie in C auch – dass die Auswertung von logischen Ausdrücken verkürzt werden kann, wenn das Ergebnis bereits feststeht. Im folgenden Beispiel wird also garantiert zuerst überprüft, ob „objekt“ nicht „null“ ist. Ist dieser Teilausdruck false (also objekt ist eine Nullreferenz), wird der rechte Operand von && gar nicht ausgewertet, d. h. die Methode inOrdnung() wird nicht gesucht oder gar gerufen. Somit ist der Gesamtausdruck sicher und kann niemals eine „NullPointerException“ verursachen. (Eine „NullPointerException“ wird in Java immer dann geworfen, wenn versucht wird, die Objektreferenz „null“ aufzulösen.)

if (objekt != null && objekt.inOrdnung()) {
    System.out.println("in Ordnung");
}
else {
    System.out.println("ungeprüft oder Prüfung misslungen");
}

Werden statt der logischen Operatoren && und || die Bit-Operatoren & und | verwendet, so müssen beide Teilergebnisse bitweise verknüpft werden und die Verarbeitung kann nicht beim linken Ausdruck abbrechen, falls der false ist. Hat also die Variable objekt den Wert null, so versucht die Java-VM, den aus dem ersten Ausdruck resultierenden Wert false mit dem Ergebnis des zweiten Ausdrucks bitweise zu verknüpfen. Um den zweiten Ausdruck zu berechnen, versucht die VM objekt zu dereferenzieren und wirft eine NullPointerException.

Funktionale Schnittstellen und Lambda-Ausdrücke

Ein Lambda-Ausdruck ist eine namenlose (anonyme) Methode, die (als Objekt) weitergegeben werden kann. Lambda-Ausdrücke beschreiben also Funktionsobjekte. Der Begriff geht auf den Lambda-Kalkül zurück, eine formale Sprache zur Untersuchung von Funktionen (siehe auch funktionale Programmierung). Lambda-Ausdrücke in Java gibt es seit Java 8 und umfassen Parameterlisten, einen Rückgabetyp und einen Body:

(<Datentyp> <Variablenbezeichner>,...,<Datentyp> <Variablenbezeichner>) -> <Anweisungsblock>
(<Datentyp> <Variablenbezeichner>,...,<Datentyp> <Variablenbezeichner>) -> <Ausdruck>
(<Variablenbezeichner>,...,<Variablenbezeichner>) -> <Anweisungsblock>
(<Variablenbezeichner>,...,<Variablenbezeichner>) -> <Ausdruck>

Die runden Klammern können weggelassen werden, wenn die Anzahl der enthaltenen Elemente genau eins ist. Die Angabe der Parametertypen ist nur dann erforderlich, wenn der Compiler sie nicht selbst aus dem Kontext des Ausdrucks ableiten kann.[3] Eine Funktion, die die Summe von zwei Argumenten liefert, lässt sich folgendermaßen ausdrücken:

(i,j) -> i+j

Sie haben selbst keinen konkreten Typ und können daher nur in einem Kontext verwendet werden, der einen Typ vorgibt, etwa Methodenaufrufe, Return-Anweisungen und Variableninitialisierungen. Der vorgegebene Typ muss eine funktionale Schnittstelle sein. In folgendem Beispiel ist ein Comparator<String>-Objekt mit Hilfe eines Lambda-Ausdrucks definiert. Der sort-Aufruf bewirkt die Sortierung von someStrings nach deren Länge, in absteigender Reihenfolge.

List<String> someStrings = ...
Collections.sort(someStrings, (x,y) -> y.length() - x.length());

Lambda-Ausdrücke helfen, die oft schlecht lesbaren, langen Deklarationen innerer, anonymer Klassen, zu vermeiden.

In Java heißen Schnittstellen, die nur über eine Operation (abstrakte Methode) verfügen, funktionale Schnittstellen. Eine abstrakte Klasse mit genau einer abstrakten Methode zählt allerdings nicht als funktionale Schnittstelle.

Methodenreferenzen stellen ebenfalls eine ab Java 8 verfügbare Möglichkeit dar, funktionale Schnittstellen zu implementieren. Sie haben eine der Formen

<Klassenreferenz>::<Methodenbezeichner>
<Ausdruck>::<Methodenbezeichner>

In folgendem Code wird die Methodenreferenz System.out::println als ein java.util.function.Consumer<String>-Objekt verwendet.

static <A> Consumer<A> doTwice(Consumer<A> consumer) {
    return a -> {
        consumer.accept(a);
        consumer.accept(a);
    };
}
static void foo() {
    doTwice(System.out::println).accept("Hello");
}

Kommentare

Kommentare sind Teile des Quellcodes, welche Informationen für den menschlichen Leser enthalten und vom Computer bei der Weiterverarbeitung ignoriert werden.

Man unterscheidet zwischen Zeilenkommentare und Blockkommentare. Der Zeilenkommentar folgt an einer Zeile. Die Syntax besteht aus einem Doppelschrägstrich. Der Blockkommentar umfasst die Beschreibung mehrerer Zeilen. Die Syntax dafür besteht aus einem Schrägstrich und darauf folgendes Mal-Zeichen. Mit dem Mal-Zeichen und dem Schrägstrich wird der Kommentar geschlossen. Beim Kompilieren werden die Kommentare vom Compiler ignoriert.

Beispiel:

 int i = 10; // Zeilenkommentar: hier bekommt die Variable i den Wert 10 zugewiesen.
 int j = 0;

/* Blockkommentar: hier beginnt eine While-Schleife
....
j wird um Eins erhöht, solange j kleiner i ist.*/
 while (j < i) {
  j++;
 }

Zur Dokumentation ganzer Methoden oder Klassen wird oft das Javadoc-System verwendet.

Generische Programmierung

Mit Version 5 von Java wurde das Sprachmittel der generischen Programmierung eingeführt.

Einzelnachweise

  1. Scott Stanchfield: Java is Pass-by-Value, Dammit! JavaDude.com, abgerufen am 5. November 2010 (englisch).
  2. Types, Values, and Variables. (Nicht mehr online verfügbar.) In: Java Language Specification. Oracle (Sun), archiviert vom Original am 9. März 2012; abgerufen am 6. November 2010 (englisch).  Info: Der Archivlink wurde automatisch eingesetzt und noch nicht geprüft. Bitte prüfe Original- und Archivlink gemäß Anleitung und entferne dann diesen Hinweis.@1@2Vorlage:Webachiv/IABot/docs.oracle.com
  3. Martin Gerlach: Java SE 8 Neuerungen (Teil 1): Lambda-Ausdrücke und Default-Methoden In: Neo Tech Blog (erstellt am 17. November 2014, abgerufen am 16. Dezember 2016).

Auf dieser Seite verwendete Medien

Feld-java.png
Autor/Urheber: Niabot, Lizenz: CC BY 3.0
Bestandteile eines mehrdimensionalen Feldes in Java am Beispiel eines zweidimensionalen double-Arrays.
Zählschleife.png
Zählschleife (For-Schleife)
Mehrseitige Auswahl.png
Mehrseitige Auswahl (Switch-Anweisung)
Kopfgesteuerte Schleife.png
Kopfgesteuerte Schleife (While-Schleife)
Einseitige Auswahl.png
Einseitige Auswahl (if-Anweisung)
Lineares Programm.png
Lineares Programm
Duke (Java mascot) waving.svg
Autor/Urheber: sbmehta converted to SVG from Sun Microsystems AI version., Lizenz: BSD
Duke, the Java Mascot, in the waving pose. Duke images are now Free Graphics see duke:Project Home Page for more details.
Zweiseitige Auswahl.png
Zweiseitige Auswahl (if-else-Anweisung)
Fußgesteuerte Schleife.png
Fußgesteuerte Schleife, Do-While-Schleife