Michael Neuhold Homepage
Startseite > Informatikunterricht > Java

Java


Allgemeines

Es gibt serverseitige und clientseitige Javaanwendungen. Die serverseitigen erzeugen HTML-Seiten als Ausgabe, die der Browser anzeigt. Bei den clientseitigen unterscheidet man Applets und Applikationen. Applets sind kleine Programme, die vom Browser aus aufgerufen werden (und dazu von einem Webserver heruntergeladen werden müssen), Applikationen sind Programme, die unabhängig vom Browser ausgeführt werden. Die Kompilate von Javaprogrammen (Dateien mit der Endung .class) sind sog. Bytecode. Das ist ein binärer Zwischencode für eine imaginäre Plattform, auch virtuelle Maschine (Java virtual machine, JVM) genannt, der von keinem Prozessor unmittelbar ausgeführt werden kann. Um das Programm laufen zu lassen, wird eine Laufzeitumgebung (engl. run time environment, RTE) benötigt, die JVM die emuliert, indem sie den Bytecode in Maschinenanweisungen für eine konkrete Hardwareplattform übersetzt.

Javaprogramme sind:

// definiere, zu welchem Package die Klasse gehört:
package mein.paket;

// welche Packages müssen dazugeladen werden:
import irgend.eine.klasse;
import irgend.welche.anderen.*;

// Startklasse
public class MeinProgramm {
  public static void main (String[] args) {
    // Klassendefinitionen und -instantiierungen:
    MeineKlasse meinObjekt = new MeineKlasse();

    // Methodenaufrufe:
    meinObjekt.methode( parameter1, parameter2 );
  }
}

Eine Javapplikation besteht aus Klassen. Die Startklasse muss eine main-Funktion besitzen. Die Aufrufargumente werden an den Stringarrayparameter dieser Funktion (oben args genannt) weitergereicht. Die main-Funktion muss public und static sein. Ein Aufruf auf der Kommandozeile sieht so aus:

java MeinProgramm wert1 wert2

Dazu muss die Datei MeinProgramm.class im Klassenzugriffspfad (CLASS_PATH) sein. Erzeugt wird sie durch das Kompilieren von MeinProgramm.java:

javac MeinProgramm.java

Dem Compiler wird die Quellcodedatei mit vollem Namen (.java) übergeben, dem Interpreter aber ohne Extension! Klassennamen werden mit großem Anfangsbuchstaben geschrieben, Instanz- und Variablennamen klein. Ebenso werden die primitiven Datentypen int, boolean, void usw. klein geschrieben. Auf die Methoden und Attribute einer Klasse wird mit der Punktnotation objekt.attribut zugegriffen.

Primitive Datentypen

Name Typ Wertebereich Literale
byte vorzeichenbehaftete Ganzzahl 8 Bit: -27 (-128) bis 27-1 (127) 42 (dezimal), 052 (mit führender Null: oktal), 0x2A (mit vorangestelltem 0x: hexadezimal)
short 16 Bit: -215 (-32.768) bis 215-1 (32.767)
int 32 Bit: -231 (-2.147.483.648) bis 231-1 (2.147.483.647, 2 Milliarden)
long 64 Bit: -263 (-9.223.372.036.854.775.808) bis 263-1 (9.223.372.036.854.775.807, 9 Trillionen)
float Gleitkommazahl 32 Bit 3.14 (Dezimalpunkt), 6.022E23 (Exponentialschreibweise mit E oder e: 6,022 x 1023, die Avogadro-Konstante), 42F, 43D
double 64 Bit
boolean logisch ja/nein false, true
char einzelnes Unicodezeichen in Hochkomma: 'a', '\n' (new line), '\u0431' (Unicodenotation für kyrill. kleines b: б)
void Rückgabetyp für Methoden, die keinen Wert zurückliefern gibt's nicht

Die Konvertierung von primitiven Typen in Wrapperklassen und umgekehrt musste früher explizit durchgeführt werden:

Integer i = new Integer(4);
int j = i.intValue();
Number n = new Float(3.14159);

Seit Java 1.5 gibt es eine Autoboxing bzw. Unboxing genannte automatische Konvertierung:

Integer i = 4;
int j = i;
Number n = 3.14159f;

Klassen, Member, Parameter

// Klassendeklaration
<Zugriff> <Datentyp> MeineKlasse( [parameter1 <Datentyp>, parameter2 <Datentyp>] ) {

}
// Variablendeklaration:
int a, b;
int c = 0;

Üblicherweise schreibt man pro Klasse eine Quellcodedatei. Es darf pro Quellcodedatei nur eine public-Klasse geben und die Quellcodedatei muss so heißen, wie diese Klasse.

Variablen von einem Objekttyp sind immer Referenzen auf einen Speicherbereich. Wird also eine Objektvariable einer anderen zugewiesen, bedeutet dies, dass sie auf dieselbe Objektinstanz verweisen:
MeineKlasse k1 = MeineKlasse();
MeineKlasse k2 = k1; // damit verweisen k1 und k2 auf dieselbe Instanz!!

Parameter werden immer by-Value übergeben. Parameter von einem Objekttyp werden dabei als Referenz übergeben. Mit dieser Referenz kann der Inhalt des Objekts geändert werden, aber nicht die Referenz auf eine andere Objektinstanz umgebogen werden.

Das Schlüsselwort this verweist auf die aktuelle Instanz und wird verwendet, wenn (a) zwischen Membervariablen und gleichnamigen Parametern unterschieden werden muss oder (b) eine Instanz als Parameter übergeben werden muss:
(a) this.irgendwas = irgendwas; // irgendwas ist Parameter, this.irgendwas ist Membervariable
(b) MeineKlasse k = MeineKlasse(this); // übergibt eine Referenz auf k an den Konstruktor

Zugriffsmodifikatoren

Bezeichner Bedeutung Klassen Methoden Variablen Konstruktoren
public Von außen zugänglich (aufrufbar) X X X X
protected Zugänglich innerhalb desselben Packages und für alle Unterklassen (auch wenn sie sich ein einem anderen Package befinden) X X ? X
- Zugänglich nur innerhalb desselben Packages ("package friendly") X X X ?
private Nur innerhalb der aktuellen Klasse zugänglich - X X X

Membervariablen sollten nach Möglichkeit private sein. Andere Klassen sollten darauf nur über sog. Getter- (die Variablenwerte auslesen) und Setter-Methoden (die Variablenwerte setzen/ändern) zugreifen können.

Jede Klasse hat zumindest einen Konstruktor, der Initialisierungen vornimmt. Deklariert wird er wie eine Methode, nur dass sein Name mit dem Klassennamen identisch sein muss und er keinen Rückgabetyp hat. Konstruktoren werden auch nicht vererbt. Wenn man keinen Konstruktor deklariert, erzeugt Java standardmäßig einen parameterlosen Defaultkonstruktor. Sobald man einen Konstruktor deklariert, muss man auch den parameterlosen Konstruktor deklarieren, wenn man ihn braucht.

Weitere Modifikatoren

Bezeichner Bedeutung Klassen Methoden Variablen Konstruktoren
static gelten für alle Instanzen einer Klasse innere X X -
final Konstante, d.h. von Klasse kann nicht abgeleitet werden, Methode kann nicht überschrieben werden, Variable kann nach erster Wertzuweisung nicht geändert werden, Referenz kann nicht auf andere Instanz umgebogen werden X X X -
abstract Methode ohne Implementation, Klasse mit solchen Methoden X X - ?

static-Variablen werden von allen Instanzen einer Klasse geteilt. static-Methoden stehen zur Verfügung, ohne dass man die Klasse instantiieren muss. Man kann mit Klassenname.methode darauf zugreifen (z.B. System.arraycopy()). Man spricht von Klassenvariablen und -methoden. Klassenmethoden können (neben lokalen Variablen und Parametern) nur auf Klassenvariablen zugreifen! (Logisch: alle anderen existieren erst nach einer Instantiierung.) static-Methoden können nicht überschrieben werden. static-Komponenten werden beim Laden des Bytecodes initialisiert und stehen bis zum Ende des Programms zur Verfügung. main muss static sein, da die JVM keine Instanz der Startklasse erzeugt.

Auch ein Codeblock außerhalb einer Methode kann als static definiert werden. Dieser Block wird dann einmal beim Laden der Klasse ausgeführt:

public class Mitarbeiter{
  private String name;
  static {
    // Zugriff auf static-Membervariablen
  }
  ...
}
public class Staat {
  // Membervariable hauptstadt vom Typ String:
  private String hauptstadt;

  // hier kommt ein Konstruktor mit einem Parameter - kein Rückgabetyp:
  public Staat( String s ) {
    hauptstadt = s;
  }
}

Um sicherzustellen, dass von einer Klasse nur eine Instanz erzeugt werden kann, verwendet man das sog. Singleton-Modell. Dazu muss der Konstruktor private sein und eine static-Instanz von sich selbst anlegen. Zum Zugriff auf die Instanz braucht man weitere static-Methoden:

public class Singleton {
  private static Singleton singleInstance = new Singleton(); // Klasse enthält Referenz auf sich selbst

  private Singleton() { // private Konstruktor
  ...
  }

  public static Singleton getInstance() {
    return singleInstance;
  }
  ...
}

Eine abstract-Klasse ist eine, die abstrakte Methoden deklariert, das sind Methoden ohne Implementation (nur Prozedurköpfe). Abstrakte Klassen kann man nicht instantiieren (Compilerfehler). Sie kann aber Membervariablen und nicht abstrakte Methoden haben. Den Konstruktor macht man in der Regel protected. Kindklassen müssen entweder alle abstrakten Methoden implementieren oder selbst als abstract deklariert werden.

Konstruktoren werden mit dem new-Kommando aufgerufen. Die einzige Klasse, die ohne Konstruktoraufruf (wie ein primitiver Datentyp) initalisert werden kann, ist String. (Grund: Strings werden in einem Stringpool abgelegt, um zu vermeiden, dass derselbe Stringliteral mehrfach angelegt wird.)

private Staat italien = new Staat( "Rom" );
private String name = "Franz"; // oder: ... = new String( "Franz" );

Package

Die zu einer Anwendung gehörenden Klassen werden sinnvollerweise in Gruppen zusammengefasst, die untereinander in hierarchischer Beziehung stehen. Diese hierarchische Beziehung wird in Java im Filesystem abgebildet. In den Anweisungen package und import wird der Punkt als Ebenentrenner verwendet. Eine Klasse ohne package-Anweisung gehört zum namenlosen Defaultpackage.

Der Name einer Klasse besteht aus der vollständigen Benennung der Hierarchie, z.B. java.io.Writer. Um die Klasse nur mit ihrem letzen Namen zu referenzieren, also Writer, muss sie mit der import-Anweisung bekanntgemacht werden: import java.io.Writer. In der import-Anweisung kann * als Platzhalter für alle Packages dieser (und nur dieser) Hierarchieebene verwendet werden: import java.io.* macht alle Klassen aus java.io bekannt. Das Package java.lang wird immer automatisch importiert. Zu ihr gehören u.a. die Klassen Object, String, Thread.

Kommentare

Drei Arten von Kommentaren:

// bla bla
Kommentar reicht von // bis zum Ende der Zeile.
/* bla
bla */
Kommentar reicht von /* bis */ und kann sich über mehrere Zeilen erstrecken.
/** bla bla */
Wie /* ... */, aber der Kommentar wird vom javadoc (Tool zum automatischen Generieren von Klassendokumentation) als Dokumentation betrachtet.

Statements

Jedes Statement muss mit einem Strichpunkt abgeschlossen werden. Statements dürfen auf mehrere Zeilen aufgeteilt werden. Man kann mehrere Statements in eine Zeile schreiben, dies ist aber unüblich. Mehrere Statements werden mit geschwungenen Klammern zu Blöcken zusammengefasst. if, else und for brauchen keine Klammern, wenn der Anweisungsblock aus nur einer Anweisung besteht. Es ist jedoch üblich, sie dennoch zu setzen.

if ( <bedingung> ) {
  a = a
      + b; // möglich, aber vielleicht nicht sinnvoll
  b = 0;
} else {
  a = b; // Klammern nicht notwendig, aber empfohlen
}

Namen von Klassen, Methoden und Variablen dürfen aus Unicode-Buchstaben und Ziffern bestehen. Beginnen können sie mit einem Buchstaben, Unterstrich (_) oder Dollarzeichen ($). Es gibt keine max. Länge. Groß-/Kleinschreibung werden unterschieden: user, User und USER sind drei verschiedene Bezeichner. Achtung bei Klassennamen: wenn Nicht-ASCII-Zeichen verwendet werden, muss auch das Dateisystem diese Zeichen unterstützen.

Eine Konvention ist, Packagenamen immer klein zu schreiben (meinpackage), für alles andere die sog. Kamelnotation (EinKamelHatZweiHöcker) zu verwenden, dabei fangen Klassen- und Interfacenamen mit Großbuchstaben an (MeineKlasse), Namen von Methoden und Variablen mit Kleinbuchstaben (gesamtSumme), Methodennamen sollten Verben sein (berechneSumme). Namen von Konstanten (final) primitiver Typen werden in Großbuchstaben und mit Unterstrich als Trenner geschrieben (MEINE_KONSTANTE).

Operatoren

Verzweigungen und Schleifen

Funktionieren praktisch wie in C.

if ( <bedingung> ) {
  <anweisungsblock>
}
if ( <bedingung> ) {
  <anweisungsblock>
} else {
  <anweisungsblock>
}
if ( <bedingung> ) {
  <anweisungsblock>
} else if ( <bedingung> ) {
  <anweisungsblock>
} else {
  <anweisungsblock>
}
switch ( <ausdruck> ) {
  case <konstante1>:
    <anweisungsblock>
    break;
  case <konstante2>:
    <anweisungsblock>
    break;
  ...
  default:
    <anweisungsblock>
}

Der Ausdruck nach dem case muss zuweisungskompatibel zu int sein, d.h. byte, short und char sind erlaubt, long, Gleitkommatypen und Klassen (einschl. String) nicht. Der Anweisungsblock wird nicht geklammert, sondern an seinem Ende steht ein break. Ohne break würde die Ausführung mit dem Anweisungsblock des nächsten case-Zweigs weitermachen, ohne die Bedingung zu prüfen (was manchmal erwünscht ist, aber auch ein häufiger Fehlerfall).

for (<startzuweisung>; <bedingung>; <änderung>) {
  <anweisungsblock>
}

Die Startzuweisung kann gleichzeitig eine Variablendeklaration enthalten. Diese Schleifenvariable ist dann nur im Schleifenblock gültig:
for (int i = 10; i < 100; i++ ) { // i wird hier deklariert und ist nur im for-Block bekannt

while (<bedingung>) {
  <anweisungsblock>
}
do {
  <anweisungsblock>
} while (<bedingung>);

break beendet die Schleife komplett, continue bricht nur die aktuelle Ausführung ab und springt zur Auswertung der Laufbedingung. Beiden kann ein Label mitgegeben werden (um z.B. bei geschachtelten Schleifen mehrere Ebenen zu überspringen). Die Schleife muss dann mit einem Label versehen werden.

außen:
do {
  <anweisungsblock>
  innen:
  do {
    <anweisungsblock>
    if (<bedingung>) {
      break außen;
    }
    <anweisungsblock>
  } while (<bedingung>); // ende der innen-Schleife
  <anweisungsblock>
} while (<bedingung>); // ende der außen-Schleife
// hier geht's weiter, wenn die Ausführung auf das break trifft

Arrays

Arrays sind immer Klassen, abgeleitet von Klasse Object. Zur Deklaration gibt es zwei Schreibweisen:
C: int a[], b[];
Java: int [] a, b; // hier ist [] nur einmal nötig

Als Klassen müssen Arrays mit new instantiiert werden. Der Konstruktor heißt wie der Elementtyp (MeineKlasse[anzahlelemente]). Die Arrayelemente werden automatisch mit Nullwerten initialisert. Der Index beginnt wie in C bei 0:

a = new int[6]; // erzeugt a[0] bis a[5]

Ist der Elementtyp selbst eine Klasse, müssen natürlich die einzelnen Elemente wiederum mit new instantiiert werden:

MeineKlasse [] k;
k = new MeineKlasse[5];
k[0] = new MeineKlasse();

Für die Instantiierung des Arrays und aller Elemente in einem Aufwaschen gibt es folgende Schreibweise:

MeineKlasse k[] = {
  new MeineKlasse('4711'),
  new MeineKlasse('08/15'),
  new MeineKlasse();
} // erzeugt ein Array MeineKlasse[3] und instantiiert seine Elemente

Mehrdimensionale Arrays sind in Java Arrays von einem Arraytyp. Beim Instantiieren muss nur die erste Dimension festgelegt werden. Mehrdimensionale Arrays müssen daher nicht "rechteckig" sein. Für rechteckige Arrays gibt es aber eine Kurzschreibweise. Das Attribut length enthält die Anzahl der Arrayelemente:

   // nicht rechteckiges Array:
   int zweiD [][] = new int [10][]; // die erste Dimension muss hier festgelegt werden
   zweiD[0] = new int[6]; // erst beim Instantiieren wird die zweite Dimension festgelegt
   zweiD[1] = new int[4]; // die Arrays der zweiten Dimension können verschieden groß sein
   // rechteckiges Array:
   int zweiD[][] = new int [10][6]; // 10 Arrays mit je 6 int
   for (int i = 0; i < zweiD.length; i++) // durchläuft das ganze Array

Mit der statischen Methode System.arraycopy() kann man Arrays kopieren. Aber Achtung: bei Arrays von Objekten werden Referenzen kopiert, nicht die Objektinhalte.

Vererbung und Polymorphismus

Subklassen werden abgeleitet mit extends:
public class Mitarbeiter extends Person {
public class Abteilungsleiter extends Mitarbeiter {

Java erlaubt nur Einfachvererbung, d.h. eine Kindklasse kann nur eine Vaterklasse haben. Statt der in C++ möglichen Mehrfachvererbung gibt es in Java die sog. Interfaces. Eine Klasse, die ohne extends deklariert ist, wird immer von Klasse Object abgeleitet.

Die Kindklasse erbt alle Membervariablen und Methoden der Vaterklassen, allerdings nicht die Konstruktoren. Üblicherweise wird man in der Kindklasse zusätzliche Variablen und Methoden deklarieren. Indem man Methoden mit gleichem Namen, gleichen Parametern und gleichem Rückgabetyp definiert, überschreibt man die Funktionalität gleichnamiger Methoden der Vaterklasse. Dabei darf der Zugriffsmodifikator nicht eingeschränkter sein, als der der überschriebenen Methode. Oft möchte man dabei explizit auf Membervariablen oder Methoden der Vaterklasse zugreifen, dies geschieht mit dem Schlüsselwort super:

public int berechneDies() {
  private int n = super.berechneDies();
  return n * 2;
}

Als Polymorphismus bezeichnet man die Möglichkeit, Kindklassen als Instanzen ihrer Vaterklassen auftreten zu lassen. Dabei wird erst zur Laufzeit ermittelt, welche Methode (die der Klasse, als die eine Variable deklariert wurde, oder der Klasse, mit der instantiiert wurde) eigentlich aufzurufen ist (virtuelle Methoden). Dies macht Java automatisch (in C++ ist hierfür die Verwendung des Schlüsselwortes virtual notwendig). Polymorphismus ermöglicht auch, dass Arrayelemente verschiedenen Elementtyp haben:

private Person p = new Mitarbeiter();
p.gibDaten(); // was wird hier aufgerufen? Antwort: Mitarbeiter.gibDaten
// heterogenes Array:
Person [] q = new Person[10];
q[0] = new Mitarbeiter();
q[1] = new Abteilungsleiter();

Ob ein Objekt Instanz einer Klasse ist, kann mit dem instanceOf-Operator ermittelt werden. Dabei muss immer vom speziellen zum allgemeinen Fall abgefragt werden, denn if (p instanceOf Person ) liefert true für Instanzen von Abteilungsleiter, Mitarbeiter und Person. Mit expliziter Typumwandlung kann eine Referenz auf eine Vaterklasse in eine solche auf eine Kindklasse umgewandelt werden. Für das Umgekehrter ist keine Typumwandlung notwendig, eine bloße Zuweisung reicht:

public void machWas( Mitarbeiter m ) {
  if (m instanceOf Abteilungsleiter) {
    Abteilungsleiter a = (Abteilungsleiter) m;
  }
  Person p = m; // keine Cast notwendig

Innerhalb einer Klasse darf derselbe Methodenname mehrfach verwendet werden, solange sich die Parameter in Anzahl und/oder Datentyp unterscheiden (sog. Überladen). Der Rückgabetyp kann unterschiedlich sein, das alleine ist aber zuwenig. Dasselbe gilt für das Überladen von Konstruktoren. Dabei kann mit this(<parameter>) ein anderer Konstruktor derselben Klasse, mit super(<parameter>) ein Konstruktor der Vaterklasse aufgerufen werden; diese Anweisung muss die erste im Konstruktor sein. Enthält ein Konstruktor keinen this- oder super-Aufruf, fügt der Compiler implizit den Aufruf super() (d.h. Aufruf des parameterlosen Defaultkonstruktors der Vaterklasse) hinzu. Gibt es diesen parameterlosen Konstruktor nicht, erhält man eine Compilerfehlermeldung.

Innere Klassen

Eine Klassendeklaration darf auch innerhalb einer anderen Klasse stehen (sog. Innere oder Verschachtelte Klasse). Innere Klassen haben Zugriff auf die privaten Attribute der Klasse, innerhalb derer sie stehen. Beim Kompilieren werden für innere Klassen class-Dateien mit dem Namen der äußeren und inneren Klasse getrennt durch $ (AußenKlasse$InnenKlasse.class) angelegt. Öffentliche innere Klassen werden von außerhalb der äußeren Klasse auf besondere Weise referenziert (nämlich im Kontext einer Instanz der äußeren Klasse):

AußenKlasse außen = new AußenKlasse();
AußenKlasse.InnenKlasse innen = außen.new InnenKlasse();
// oder:
AußenKlasse.InnenKlasse innen = new.AußenKlasse().new InnenKlasse();

Zur Unterscheidung zwischen einem Parameter, einer gleichnamigen privaten Variable der Innenklasse und einer gleichnamigen der Außenklasse kann man this.var und Außenklasse.this.var benutzen. Innere Klassen dürfen auch innerhalb einer Methode deklariert werden. Allerdings können sie nicht auf die Parameter und nicht-finalen Variablen der Methode zugreifen (weil diese zur Laufzeit nach Beenden der Methode nicht mehr existieren).

Innere Klassen können abstrakt sein, innere Klassen können Teil einer Hierarchie weiterer innerer Klassen sein, daher ist hier auch protected-Zugriff möglich. Innere Klassen, die static deklariert sind, verlieren ihren Status als innere Klasse und haben keinen Zugriff auf die äußere Klasse. Innere Klassen können auch keine static-Member haben.

Interfaces

Ein Interface ist im Prinzip eine abstrakte Klasse, die nur Konstanten (static final) und abstrakte Methoden enthalten darf. Die Schlüsselwörter static, final, abstract sind implizit und können weggelassen werden. Ein Interface wird mit dem Schlüsselwort interface deklariert . Ein Interface kann von beliebig vielen Klassen implementiert werden. Eine Klasse kann (zusätzlich zur oder statt der Ableitung von einer Vaterklasse) mehrere Interfaces implementieren. Dies ist gefahrlos möglich, da Interfaces (im Gegensatz zur Mehrfachvererbung in C++) keinerlei Implementation enthalten. Der Sinn von Interfaces ist es, sicherzustellen, dass ein Satz von Methoden (der durch das Interface definiert ist) auf jeden Fall implementiert wird; und die Klassenschnittstelle offenzulegen, ohne die Implementierung preiszugeben. Der Polymorphismus gilt auch für Interfaces. Auch Interfaces können von anderen Interfaces abgeleitet werden.

public interface Fliegend {
  public void starten();
  public void landen();
  public void fliegen();
}

public class Flugzeug implements Fliegend {
  public void starten() {
    // Implementierung hier
  }
  public void landen() {
    // Implementierung hier
  }
  public void fliegen() {
      // Implementierung hier
  }
}

public class Vogel extends Lebewesen implements Fliegend {
 ...
}

  Fliegend F = Superman.getClarkKent(); // Polymorphismus (Klasse Superman ist nach dem Singleton-Muster gebaut)
  public interface MyInterface extends SomeInterface; // Ableitung

Exceptions

Die Klasse Errors beschreibt Fehler, auf die ein Programm kaum sinnvoll reagieren kann und die daher zum Abbruch des Programms führen sollten. Ausnahmen (Exceptions) sind Fehler, auf die ein Programm reagieren kann (z.B. Zugriff auf nicht-existierende Array-Elemente, Öffnen einer nicht vorhandenen Datei, keine Netzwerkverbindung mehr u.ä.). Ausnahmen werden abgefangen, indem man einen try-Block bildet und diesem für jede Ausnahme, die man behandeln möchte, einen catch-Block folgen lässt. Daran kann sich ein finally-Block anschließen, der in jedem Fall ausgeführt wird, egal ob eine Ausnahmen aufgetreten ist oder nicht (außer im try-Block wird mit System.exit() ein harter Ausstieg vorgenommen).

try {
  // tue irgendwas
}
catch (AusnahmeGrün grün) {
  // behandle Fehlerfall Grün
}
catch (AusnahemBlau blau) {
  // behandle Fehlerfall Blau
}
finally {
  // das hier wird (fast) immer ausgeführt
}

Wird eine Ausnahme nicht behandelt, wird sie eine Aufrufebene höher geleitet, solange bis sie behandelt wird oder die main-Prozedur erreicht ist. Behandelt auch diese die Ausnahme nicht, bricht das Programm mit einer Fehlermeldung ("java.lang.ArrayIndexOutOfBoundsException: bla bla") ab. Try-catch-Blöcke können natürlich geschachtelt werden. Sehr häufig wird man in catch-Blöcken wiederum try-Blöcke finden.

Eine Methode sollte Exceptions, die sie erzeugt, entweder selbst behandeln oder der aufrufenden Methode durch das Schlüsselwort throws anzeigen, dass sie diese Exception nicht behandelt, also an den Aufrufer weitergibt. Da Exceptions Klassen sind, gilt auch für sie der Polymorphismus: eine FileNotFoundException ist eine Unterklasse von IOException, eine IOException kann daher auch eine FileNotFoundException abfangen.

public void readSomeFile( String fileName )
  throws URISyntaxException, FileNotFoundException {

Wenn man eine Methode überschreibt, kann die neue Methode der Kindklasse die gleichen Exceptions werfen wie die Methode der Vaterklasse, Subklassen dieser Exceptions oder weniger Exceptions. Sie kann keine Exceptions werfen, die Superklassen der Exceptions, die die Methode der Vaterklasse wirft, sind oder die die Methode der Vaterklasse gar nicht wirft.

Man kann durch Ableitung von der Klasse Exception neue Ausnahmen definieren. Mit throw wird die Exception geworfen. Oft wird man in einem catch-Block auf eine Ausnahme reagieren (z.B. Logging), um sie am Ende weiterzuwerfen.

public class MeineAusnahme extends Exception {
  public MeineAusnahme( String fehlerMeldung ) {
    super(fehlerMeldung); // rufe den Konstruktor der Vaterklasse auf
    ...
  }
}
// die Ausnahme erzeugen:
throw new MeineAusnahme("Kann nicht, mag nicht, darf nicht");
// Ausnahme weiterwerfen:
catch( MeineAusnahme e ){
  // tue irgendwas
  throw e;
}

Konsole und Dateien

Die Parameter, die beim Start an ein Javaprogramm übergeben werden, reicht die JVM an das args-Array der main-Methode weiter. Darüberhinaus können beim Aufruf des Programms mit dem Schalter -D sog. Properties definiert werden, das sind Name=Wert-Paare vergleichbar den Umgebungsvariablen. Die Liste der Properties wird mit System.getProperties() abgefragt, der Wert eines Properties mit der Methode getProperty der Properties-Klasse.

// Aufruf des Programms test mit dem Property stadt
java -Dstadt=rom test
// Auswertung der Property:
Properties eigen = System.getProperties();
String wert = eigen.GetProperty( "stadt" );

Zur Ausgabe auf die Konsole verwendt man die print- und println-Methode der statischen PrintStream-Klassen System.out und System.err. Sie sind überladen für alle primitiven Typen, für char[], String und Object. Zum Einlesen von der Konsole verwendet man die statische InputStream-Klasse System.in. Allerdings braucht man zum Konvertieren des Bytestroms in Unicodezeichen und zum zeilenweisen Einlesen zusätzliche Hilfsobjekte aus Package java.io. Zum Beenden der Eingabe muss man das EOF-Zeichen eingeben (Unix: Strg-d, Windows: Strg-z-Enter).

import java.io.*;
public class Eingabe {
  public static void main (String[] args) {
    String s;
    InputStreamReader byteStrom = new InputStreamReader(System.in);
    BufferedReader zeichenPuffer = new BufferedReader(byteStrom);
    try {
      while( ( s = zeichenPuffer.readLine() ) != null ) {
        System.out.println(s); // wieder ausgeben
      }
      zeichenPuffer.close();
    } catch (IOException ausnahme) {
      ausnahme.printStrackTrace();
    }
  }
}

Die Klasse File wird verwendet, um Informationen über Dateien und Verzeichnisse abzufragen: getName, getPath, exists, canWrite, isDirectory, length u.ä. Die Klasse BufferedReader und ihre Methode readLine zum zeilenweisen Einlesen von Text wurden bereits oben vorgestellt. An den Konstruktor muss man ein Reader-Objekt übergeben, z.B. InputStreamReader (Unterklasse von Reader) oder FileReader (Unterklasse von InputStreamReader). Der Konstruktor von FileReader wieder braucht entweder ein File-Objekt oder den Filenamen als String. Zur Ausgabe von Text verwendet man die Klasse PrintWriter und ihre Methoden print und println. Zum Zugriff auf das File gibt es verschiedene Konstruktoren, z.B. einen, dem man ein Writer-Objekt übergibt (z.B. FileWriter).

import java.io.*;
String s;
// Einlesen von Datei:
File datei = new File("blabla.txt");
try {
  BufferedReader eingabe = new BufferedReader(new FileReader(datei));
  while ( ( s = eingabe.readLine() ) != null ) {
    ...
  }
  eingabe.close();  // Schließen des Eingabepuffers schließt Datei
} catch (FileNotFoundException e) { // für den Fall, dass es die Datei nicht (mehr) gibt
  System.err.println("Datei gibt's nicht");
} catch (IOException f) { // sonstige Ausnahmen beim Dateizugriff
  ...
}
// Ausgabe in Datei:
try {
  PrintWriter ausgabe = new PrintWriter(new FileWriter(datei));
  while (s != null) {
    ausgabe.println(s);
    s := ...
  }
  ausgabe.close();
} catch (IOException e) {
 ...
}

Rechnen und Zeichenketten

Die Klasse java.lang.Math kann nicht instantiiert (Konstruktor ist private) und nicht abgeleitet (final) werden. Sie enthält die Konstanten PI und E, und viele mathemat. Funktionen wie sqrt (Wurzel), pow (Exponent), random (Zufallszahl zw. 0 und 1), log, exp (Logarithmus), sin, cos, tan (Trigonometrie), max, min, abs, ceil (Aufrunden), floor (Abrunden), round u.a.

Objekte der Klasse String sind unveränderbare Unicode-Zeichenketten. Die Methoden concat (Zusammenhängen), replace (Zeichenersetzung), substring (Teilstring), toLowerCase, toUpperCase (in Klein-/Großschreibung umwandeln) und trim (Weißzeichen abschneiden) erzeugen neue Stringobjekte. Zum Durchsuchen gibt es die Methoden endsWith, startsWith, indexOf (Indexpos., an der ein Zeichen oder ein Teilstring steht), charAt (Zeichen an der Pos.); zum Vergleichen equals, equalsIgnoreCase, compareTo; die Stringlänge wird mit length ermittelt.

Objekte der Klasse StringBuffer sind veränderbare Unicode-Zeichenketten. Mit den Methoden append, insert, reverse, setCharAt, setLength kann der Inhalt des Objekts verändert werden. Methode equals vergleicht die Identität von Referenzen, nicht des Inhalts!

Collections und Iteratoren

Zur Verwaltung von "Ansammlungen" von Objekten gibt es das Interface Collections (unsortiert, nicht eindeutig, d.h. mehrere Elemente dürfen den gleichen Wert haben), davon abgeleitet sind die Interfaces Set (unsortiert, eindeutig) und List (sortiert, nicht eindeutig). Implementationen sind die Klassen HashSet (abgeleitet von Set), ArrayList, LinkedList und Vector (abgeleitet von List). Stack ist abgeleitet von Vector. Um die Elemente einer Collection durchzugehen, gibt es zwei Interfaces: Enumerator und Iterator. Iterator ist jünger und definiert auch eine Methode zum Löschen (remove). Die wichtigsten Iteratormethoden sind hasNext (gibt es ein weiteres Element?) und next (liefere Referenz auf nächstes Element zurück). Der von Iterator abgeleitete ListIterator hat auch die Methoden hasPrevious, previous, set (ändere das aktuelle Element) und add (füge Element vor dem nächsten Element ein).

List meineListe = new ArrayList();
meineListe.add("Paris");
meineListe.add(new Integer(4711));
meineListe.add(new Float(3.14159F);
Iterator meineElemente = meineListe.iterator();
while (meineElemente.hasNext() ) {
  System.out.println.(meineElemente.next());
}

Ein Problem mit Collections ist, dass sie im Prinzip jeden Objekttyp aufnehmen können. Oft möchte man aber eine Liste, die nur bestimmte Typen aufnehmen kann. Es ist aber nicht möglich, zur Compilezeit zu prüfen, ob hinzugefügte Elemente einen sinnvollen Objekttyp haben. Daher wurden in Java 1.5 die sog. Generics eingeführt. Mit der Spitzklammernnotation teilt man dem Compiler mit, welchen Objekttyp eine Collection haben soll.

List<ElementTyp> meineListe = newArrayList();
...
for(Iterator<String> i = meineListe.iterator() ; i.hasNext() ; ) {
  ElementTyp meinElement = i.next();
  //do something useful
}

GUI-Programmierung mit dem Abstract Window Toolkit (AWT)

Das AWT verwendet Betriebssystemkomponenten, sein Look and Feel entspricht daher der jeweiligen Plattform. Eine Alternative wäre Swing, das auf allen Plattformen gleich aussieht, aber langsamer ist.

Containter und Fenster

Die Klasse Container ist, wie der Name schon sagt, nur ein Container für andere Elemente. Die wichtigsten Kindklassen sind Panel (eine rechteckige Fläche, auf der man Komponenten plazieren kann) und Window (ein Fenster, dessen Größe man nicht ändern kann und das keine Titelleiste hat). Die wichtigsten Kindklassen von Window wiederum sind Frame (ein klassisches Fenster mit Titelleiste, dessen Größe man ändern kann) und Dialog (Größe nicht änderbar). Komponenten sind Button (Knopf, Befehlsschaltfläche), Checkbox (Kontrollkästchen), Choice (Pull-Down-Liste), Label (Zeichenkette), List (dynamische Liste), Scrollbar, TextComponent, davon abgeleitet: TextField (einzeiliges Texteingabefeld) und TextArea (mehrzeilig). Sie werden mit add einem Container hinzugefügt.

         Component
        /         \
  Container     +----+----+---+--+---+-----+
      |         |    |    |   |  |   |     |
   +-------+  Button | Choice | List | TextComponent
   |       |      Checkbox  Label Scrollbar  |
 Panel   Window                         +---------+
   |       |                            |         |
Applet  +------+                    TextField TextArea
        |      |
      Frame  Dialog

Komponentenanordnung mit Layoutmanager

Über die Größe und Anordnung von GUI-Elementen in einem Container entscheidet ein sog. Layoutmanager. Die wichtigsten sind:

FlowLayout
Vorgabe für Panel (und daher auch Applet). Die Komponenten werden von links nach rechts angeordnet und standardmäßig zentriert ausgerichtet. Die Größe der Komponenten bleibt unverändert, ihre relative Lage wird verändert. Ausrichtung (FlowLayout.LEFT, FlowLayout.RIGHT, FlowLayout.CENTER) und Abstände können dem Konstruktor übergeben werden: setLayout(new FlowLayout(FlowLayout.LEFT));
BorderLayout
Vorgabe für Frame und Dialog. Die Containerfläche ist in die fünf Regionen NORTH/oben, SOUTH/unten, EAST/rechts, WEST/links und CENTER/Mitte eingeteilt. NORTH und SOUTH werden horizontal ausgerichtet, EAST und WEST vertikal, CENTER horizontal und vertikal. Die relative Lage der Komponentent bleibt unverändert, ihre Größe wird geändert. Jede Region kann nur ein Element enthalten.
GridLayout
Die Containterfläche ist in gleich große Zellen unterteilt, die Anzahl der Zeilen und Spalten wird dem Konstruktor übergeben: new GridLayout(3,2). Einer der beiden Parameter kann 0 sein. In diesem Fall ergibt sich die Anzahl der Zeilen bzw. Spalten aus der Anzahl der hinzugefügten Komponenten. Die Zellen werden der Reihe nach (in Leserichtung) befüllt.
import java.awt.*;

public class MyFrame {
  private Frame f;
  private Button b1;
  private Button b2;

  public MyFrame() {
    f = new Frame("Hello world"); // Parameter ist Fenstertitel
    b1 = new Button("OK");        // Parameter ist Buttonbeschriftung
    b2 = new Button("Cancel");
  }

  public void launchFrame() {
    f.setBackground(Color.blue);
    f.setLayout(new FlowLayout()); // Default ist BorderLayout
    f.add(b1);
    f.add(b2);
    f.pack(); // Fenster auf kleinstmögliche Größe bringen
    // oder f.setSize(200,200);
    f.setVisible(true); // erst hiermit wird das Fenster sichtbar
  }

  public static void main(String args[]) {
    MyFrame guiWindow = new MyFrame();
    guiWindow.launchFrame();
  }
}

Ereignisbehandlung

Ein Event ist ein Objekt, das beschreibt, was passiert ist (Mausbewegung, Mausklick, Tastatureingabe). Damit eine Komponente auf ein solches Ereignis reagieren kann, muss es einen Ereignis-Listener registrieren (add<Eventtype>Listener(<listenerInstance>)). Der Listener muss das geeignete Interface implementieren und dadurch die Methode bereitstellen, die als Reaktion auf das Ereignis aufgerufen wird. Eine Komponente kann (oder muss) auch mehr als einen Listener registrieren.

b = new Button("Hit me");
  b.setActionCommand("ButtonHit");  // frei definierbarer Bezeichner, der vom Listener abgefragt werden kann
...
  b.addActionListener(new ButtonHandler()); // registriert eine ButtonHandler-Instanz als Listener
...
import java.awt.event.*;

public class ButtonHandler implements ActionListener {
  public void actionPerformed(ActionEvent e) {
    System.out.println("I have been hit");
    System.out.println(e.getActionCommand());
  }
}

Da Listener ein Interface implentieren und einige der Interfaces mehrere Methoden haben, für die jeweils eine (wenn auch leere) Implementation bereitgestellt werden muss, gibt es die Eventadapter. Das sind Klassen, die bereits das jeweilige Interface implementieren und bei denen man nur noch die Methoden überschreiben muss, die man braucht. Häufig nutzt man hierbei sog. anonyme innere Klassen, bei denen man die Klassendefinition direkt hinter einen Ausdruck mit Konstruktoraufruf schreibt:

private Frame f;
f.addMouseMotionListener(new MouseMotionApdapter() {
  public void mouseDragged(MouseEvent e) {
    ...
  }
}; // muss mit Strichpunkt schließen

Multithreading

Wenn Aufgaben eines Programmes zeitlich parallel ablaufen können sollen, verteilt man sie auf mehrere Threads. Damit eine Klasse threadfähig wird, muss sie das Interface Runnable implementieren und eine Implementation von public void run zur Verfügung stellen. Alternativ kann sie auch von der Klasse Thread abgeleitet werden (die dieses Interface bereits implementiert) und die Methode run überschreiben. Ein Thread wird gestartet, indem man seine start-Methode aufruft. Diese setzt den Thread in den Zustand runnable. Wenn der Scheduler den Thread tatsächlich startet, wird seine run-Methode aufgerufen. Der Thread ist dann im Zustand running. Wird der Thread unterbrochen, bevor er fertig ist, ist er im Zustand blocked.

public class ThreadBeispiel {
  public static void main(String args[]) {
    MyRunner r = new MyRunner();
    Thread t = new Thread(r); // der Konstruktor von Thread benötigt eine Instanz von Runnable als Parameter
    t.start(); // macht den Thread lauffähig
  }
}

class MyRunner implements Runnable {
  public void run() {
   ...
  }
}

Es gibt verschiedene Möglichkeiten, wie ein Thread andere Threads zum Zug kommen lassen kann: Thread.sleep(<millisec>) gibt Threads mit geringerer Priorität die Möglichkeit zur Ausführung, indem er sich selbst für die angegebene Mindestdauer schlafen legt; Thread.yield gibt Threads mit gleicher Priorität diese Chance. Die Methode join lässt den Thread darauf warten, dass der Thread, für den join aufgerufen wird, fertig wird. Zusätzlich kann man dabei einen Timeout in Millisek. übergeben. Weitere Methoden für die Threadbehandlung: Thread.currentThread liefert eine Referenz auf den aktuellen Thread zurück, isAlive testet, ob der Thread noch existiert (egal ob runnable, running oder blocked), getPriority liefert die aktuelle Priorität zurück, setPriority setzt sie (dazu gibt es die Konstanten Thread.MIN_PRIORITY, Thread.NORM_PRIORITY, Thread.MAX_PRIORITY).

Wenn verschiedene Threads auf dieselben Daten zugreifen, muss der Zugriff auf diesen Code "geschützt" werden, um zu verhindern, dass durch gleichzeitigen Zugriff ein inkonsistenter Datenzustand entsteht. Dies geschieht, indem ein Codeblock mit synchronized gekennzeichnet wird. Als Parameter übergibt man ein Sperrobjekt (entweder ein eigenes Objekt oder this). Man kann auch eine Methode mit synchronized kennzeichnen, der Code wird dann mit this synchronisiert.

public class MyThread{
  ...
  public void gib() {
   synchronized(this) {
     ...
   }
  }
  // oder:
  public synchronized void gib() {
    ..
  }
  // oder auch:
  Object lock = new Object();
  public void gib() {
    synchronized(lock) {
     ...
    }
  }
}

Wenn die Codeausführung auf synchronized trifft, versucht sie, das angegebene Objekt zu sperren. Gelingt dies, kann kein anderer Thread es sperren, bis es freigegeben ist. Ist das Objekt bereits gesperrt, wird der Thread in einen Pool aller Threads, die auf dieses Objekt warten, gegeben. Ein Objekt wird automatisch freigegeben, wenn der im zugeordnete synchronized-Block wieder verlassen wird (auch durch eine break-Anweisung oder eine Exception).

Wenn ein Thread T1 das Objekt A gesperrt hält und zur Fortführung auf die Freigabe von B wartet, während gleichzeitig ein Thread T2 Objekt B gesperrt hält und auf die Freigabe von A wartet, ist ein Deadlock entstanden. Java tut nichts, um eine solche Situation zu erkennen oder zu vermeiden. Dies liegt in der Verantwortung des Entwicklers.

Mit den Methoden wait und notify, die innerhalb eines synchronized-Blocks für das Sperrobjekt aufgerufen werden, kann man mitteilen, dass man auf ein Ereignis, eine bestimmte Datensituation o.ä. warten will, bzw. dass soeben ein Ereignis, eine Datensituation eingetreten ist. wait gibt den Thread in den Wartepool des Sperrobjekts.

public synchronized void nimm() {
  while (<bedingung>) {
    try {
      this.wait();
    } catch (InterruptedException e) {
    }
  }
  ...
}

public synchronized void gib() {
  this.notify(); // kann hier schon aufgerufen werden, weil this erst am Ende freigegeben wird
    ... // ändere die Daten, sodass <bedingung> von nimm erfüllt ist
}
              unblocked     +---------+   blocking event
                   +--------| blocked |<-------+
                   |        +---------+        |
NEW                v                           |        run()  DEAD
 o---------->+----------+                   +---------+-------->x
   start()   | runnable |<----Scheduler---->| running |
             +----------+                   +---------+------+
                  ^                            |             | wait()
                  |                            |             v
                  |        +-----------+<------+       +-----------+
                  +--------|  blocked  |  synchronized |  blocked  |
           acquires lock   | lock pool |               | wait pool |
                           +-----------+<--------------+-----------+
                                           notify()
                                         interrupt()

Weitere Themen


Autor: Michael Neuhold (E-Mail-Kontakt)
Letzte Aktualisierung: 06. Nov. 2014