Am 14. November 2006 hielt ich im Rahmen des Vortragsseminars Java einen Vortrag zum Thema Sprachmerkmale von Java. Hier findet sich der komplette ausformulierte Text dazu, ebenso wie die Folien.
Wie alle Programmiersprachen, so ist auch Java eine, die einer Einführung bedarf. Sicherlich ist es bei der Komplexität nicht einfach, einen sinnvollen Überblick in akzeptabler Kürze zu geben, daher faßt dieses Dokument die wichtigsten Sprachmerkmale zusammen und geht auch mit Beispielen auf die meisten Aspekte von Java ein. Jedoch werden keine Details der Klassenbibliothek oder ähnlichem besprochen, auch bleibt die Einführung in OOP relativ kurz, was hier der oberen Grenze des Umfanges dieser Arbeit geschuldet ist.
Java ist unter recht bestimmten Gestaltungsaspekten geschaffen worden. Zunächst einmal sollte die Sprache grundsätzlich objektorientiert sein, es gibt an der Stelle zwar Ausnahmen, die historisch durch Performancegründe bedingt sind, aber insgesamt verfolgt Java dieses Paradigma doch recht erfolgreich. Java sollte plattformunabhängig sein, bzw. bestehender Java-Code sollte auf jeder Plattform laufen können, ohne das beständige Neukompilieren, wie es mit Sprachen wie C oder C++ immer nötig ist. Java erfüllt dies, indem es selbst eher seine eigene Plattform darstellt. Man kann Software für Windows schreiben, für Unix oder andere Betriebssysteme ... oder eben für die JVM, die Java Virtual Machine.
Java hat aufgrund dieser Tatsache eine beträchtlich größere Standardbibliothek, als das sonst der Fall ist; es bietet praktisch alles, was der Programmierer braucht. Möglichkeiten, grafische Oberflächen zu gestalten, XML zu verarbeiten, Verschlüsselung, Datenbankzugriff und eine ganze Reihe anderer Anwendungsfälle, für die man bei anderen Sprachen meist entweder direkt auf die API des darunterliegenden Betriebssystems oder aber auf Bibliotheken von anderen Quellen zurückgreift. In diesem Sinne kann sich der Programmierer bei Java meist ganz auf das konzentrieren, was er eigentlich schreiben will und nicht auf die Punkte, wo die Sprache nicht mit seinen Wünschen Schritt hält. Viele kennen diese Einstellung zum Programmieren auch von diversen Skriptsprachen, wo eben jenes oft auch ein Grund für deren Beliebtheit ist.
Die Syntax von Java ist größtenteils an C++ angelehnt, was es Programmierern recht einfach machen sollte, auf Java umzusteigen oder die grundlegende Syntax zu erlernen. Allerdings haben Java und C++ unter der Syntax nicht mehr viel miteinander gemein. Javas Objektmodell entstammt nämlich in gewissen Zügen der Sprache Smalltalk und nicht C++.
Smalltalk ist eine objektorientierte Programmiersprache, die 1972 entwickelt wurde und ein völlig anderes Objektmodell als das damals zehn Jahre alte Simula, an das sich C++ und andere Sprachen anlehnen. In Smalltalk haben Objekte keine Methoden, die aufgerufen werden können, sondern erhalten Nachrichten. Diese müssen nicht implementiert sein, man kann sie auch weiterreichen, was einem eine ganze Reihe interessanter Möglichkeiten eröffnet und die Sprache sehr flexibel macht. Aber der wesentliche Unterschied, der hier auch für Java relevant ist, ist die Tatsache, daß Objekte „wissen“, daß sie Objekte sind.
Das ist auch im Grunde der Unterschied zwischen Java und C++. Objekte in C++ sind lediglich Datenstrukturen, an denen Operationen hängen. Ein Objekt weiß nicht, daß es ein Objekt ist, wie es heißt, wo es in der Klassenhierarchie abgelegt ist, welche Methoden es implementiert, etc. Was in Java Reflection heißt und einem Zugriff auf gewisse Metainformationen von Objekten erlaubt, ist ein Teil dessen, was ein Objekt in Smalltalk ausmacht. Man findet sogar das Nachrichtenmodell in kleinen Ansätzen wieder, auch wenn Java hier bei weitem nicht so flexibel ist, wie Smalltalk und eher das Augenmerk auf Methoden als Möglichkeit der Interaktion mit Objekten legt.
Letztendlich wollte man Java auch einfach genug halten, um die Sprache für möglichst viele Programmierer attraktiv zu machen, dies erreicht man wohl weder mit sehr speziellen Features noch mit der Möglichkeit, die Syntaxregeln bis zum Letzten auszunutzen und Code zu schreiben, den außer dem Autor keiner mehr lesen kann.
Ein weiter Designaspekt von Java war es, dem Programmierer Operatorüberladungen bewußt nicht zu erlauben. Man mag argumentieren, daß Operatorüberladungen prinzipiell eine nette Sache sind und den Code eher lesbarer machen. Beispielsweise kann man ja mal die folgenden beiden Codefragmente vergleichen:
MyVector a, b, c;
a = b + c;
sowie
MyVector a, b, c;
a = b.add(c);
Aussagekräftiger ist wohl sicherlich das erste. Allerdings gibt es auch Beispiele, die deutlich machen, daß überladene Operatoren nicht immer zu besser lesbaren Code führen, ein Beispiel ist hier der Output Stream bei C++:
cout << "Hello World!";
Stellt sich die Frage, was ein bitweiser Shiftoperator bei Streams und Zeichenfolgen zu suchen hat. Diese Art „Mißbrauch“ von Operatoren wollte man bei Java vermeiden, damit ein Entwickler sich nicht erst durch den halben Code graben muß, um herauszufinden, ob ein Operator nun für zwei Typen überladen ist und in welcher Weise. Nicht immer ist ein
Das erste, was wohl nahezu jeder in einer neuen Sprache schreibt, ist ein „Hello World“-Programm. Bekanntermaßen tut es nichts weiter, als den Text „Hello World!“ auf dem Bildschirm auszugeben und natürlich geht das auch in Java:
public class HelloWorld {
public static void main(String[] args) {
System.out.println("Hello World!");
}
}
Ich werde hier nicht im Detail auf die einzelnen Bestandteile eingehen, das folgt in den kommenden Abschnitten. Das Beispiel dient lediglich ein wenig zur Einleitung.
Variablen sind in Java Container für zweierlei Dinge. Sie können einerseits direkt einen Wert enthalten, das ist bei primitiven Datentypen der Fall. Ansonsten enthalten sie lediglich Referenzen auf Objekte. Variablendeklarationen erfolgen im Grunde in der folgenden Form:
Typ Bezeichner[, Bezeichner[, ...]]
Was bedeutet, zunächst kommt ein Typ, gefolgt von einem oder mehreren Bezeichnern, die dann Variablen eben dieses Typs sind. Variablenbezeichner beginnen in Java immer mit einem Kleinbuchstaben.
Beispiele:
int i;
char[] blubb;
double x, y;
java.lang.Object o;
String s;
Bezeichner in Java berücksichtigen Groß-Klein-Schreibung, daher sind
Konstanten werden in Java durch den Deklarationsmodifikator
Beispiele:
final double pi = 3.14;
final String foo = "bar";
Zunächst gibt es im Grunde zwei verschiedene Arten von Typen, die im vorherigen Abschnitt schon kurz angesprochen wurden: Primitive Datentypen und Objekte. Diese beiden sind auch tatsächlich grundverschieden, wiewohl der Unterschied (zumindest für den Programmierer) gerade mit Java 5 zunehmend an Bedeutung abnimmt.
Wie eingangs schon erwähnt, handelt es sich hierbei nicht um Objekte, sondern tatsächlich um skalare Datentypen, die jeweils genau einen Wert enthalten. Diese Typen sind im Vergleich zu Objekten einigermaßen langweilig: Sie speichern exakt einen Wert, bilden keine Datenstruktur und können keine Methoden haben. Es ist eben tatsächlich nicht mehr als ein Wert.
Es gibt allerdings zu jedem dieser Typen eine Klasse, die eben diesen Typ beinhaltet und kapselt, dazu weiter unten mehr.
Java bietet eine ganze Reihe von primitiven Datentypen an, die eigentlich das meiste umfassen, was man aus anderen Sprachen kennt und auch benötigt:
Es gibt in Java vier verschiedene Ganzzahl-Typen:
| Datentyp | Bits | Wertebereich |
|---|---|---|
| 8 | −128 … 127 | |
| 16 | −32768 … 32767 | |
| 32 | −2147483648 … 2147483647 | |
| 64 | −9223372036854775808 … 9223372036854775807 |
Ganzzahl- oder auch Integertypen in Java sind grundsätzlich vorzeichenbehaftet und werden intern in Zweierkomplementdarstellung gespeichert. Es gibt keine vorzeichenlosen Zahlen in Java. Im Normalfall stört das auch nicht so sehr, es sei denn, man möchte den verfügbaren Wertebereich so gut wie möglich ausnutzen und selbst
Eine leider etwas unschöne Eigenschaft von Ganzzahlen in Java ist, daß sie keinerlei Anhaltspunkt darüber geben, ob eine Operation einen Überlauf verursacht hat oder nicht. Wer weiß, wie Zahlen in Computern gespeichert werden, wird wissen, daß sich die Zahlen quasi wie ein Kreis verhalten, d. h. die kleinste Zahl ist ein direkter Nachfolger der größten. Folglich ergibt
byte a = 127 + 1;
den Wert –128. In Java ist es (meines Wissens nach) unmöglich, sicher herauszufinden, ob ein solcher Überlauf stattgefunden hat oder nicht. Folgendes Beispiel
int i = 1000000;
System.out.println(i * i);
long l = i;
System.out.println(l * l);
liefert beispielsweise die Ausgabe
-727379968
1000000000000
und
int i = Integer.MAX_VALUE;
System.out.println(i * i)
liefert lediglich eine 1.
Man kann Ganzzahlen auf verschiedene Weise im Quelltext darstellen:
int decVal = 26; // 26, dezimal
int octVal = 032; // 26, oktal
int hexVal = 0x1a; // 26, hexadezimal
Die dezimale Schreibweise ist sicherlich die gebräuchlichste, aber je nachdem, was man so schreiben will, bietet sich eventuell auch eine der anderen beiden Darstellungen an. Oktalzahlen werden mit einer führenden Null geschrieben, wohingegen Hexadezimalzahlen am Anfang ein
Grundsätzlich werden Literale vom Typ
char capitalC = 'C';
Es gibt eine Reihe von Escapes für verschiedene Steuerzeichen, zum Beispiel:
char tab = '\t';
Die komplette Liste dieser Zeichen ist folgende:
| Escape-Sequenz | Zeichen |
|---|---|
| Backspace (BS) | |
| Tabulator (HT) | |
| Zeilenvorschub (LF) | |
| Form Feed (FF) | |
| Wagenrücklauf (CR) | |
| Doppeltes Anführungszeichen |
|
| Einfaches Anführungszeichen |
|
| Backslash |
Es gibt noch zwei Möglichkeiten (mehr oder weniger) beliebige Zeichen direkt zu erzeugen, einmal über Oktalzahlen (eine Variante, die eigentlich nur noch aus Kompatibilität zu C existiert):
char lowercaseA = '\141';
Hierbei folgen dem Backslash zwischen 1 und 3 oktale Ziffern, mit denen sich allerdings nur Zeichen zwischen U+0000 und U+00FF darstellen lassen. Da dies aber ohnehin nur aus Kompatibilitätsgründen existiert, sollte man eher auf die direkten Unicode-Escapes zurückgreifen:
char xi = '\u03be';
Hier wird direkt ein Unicode Code Point angegeben. Er wird als Hexadezimalzahl direkt nach dem
Nun kommen wir zuweilen in die Verlegenheit, den Wahrheitsgehalt einer Aussage bestimmen zu wollen. Sicherlich kann man sich dafür mit ganzen Zahlen behelfen (wie in hier nicht näher genannten Programmiersprachen), aber der hübschere Weg ist eigentlich, einen eigenen Typ für solche Ergebnisse zu haben. Zu Ehren von George Boole3 auch liebevoll
boolean wahr = true;
boolean falsch = false;
Es gelten die bekannten Regeln einer Booleschen Algebra. Dieser Datentyp hat besondere Bedeutung bei einigen Kontrollstrukturen, da beispielsweise Bedingungen, Schleifenabbrüche, etc. grundsätzlich ein Resultat vom Typ
Java bringt zwei Gleitkommadatentypen unterschiedlicher Länge mit, nämlich
double a = 1.0 / 0.0; // ergibt +Inf
double b = 1.0 / -0.0; // ergibt -Inf
double c = 0.0 / 0.0; // ergibt NaN
Die positive und negative Null sind für arithmetische Vergleiche tatsächlich identisch und nicht verschieden:
boolean x = 0.0 > -0.0; // ergibt false
Tests auf Gleichheit mit NaN sind grundsätzlich
boolean y = (c == c); // ergibt false
Weiterhin ist ∞ – ∞ ebenfalls ein unbestimmter Ausdruck, welcher in NaN resultiert.
Fließkommazahlen können auf recht verschiedene Weise repräsentiert werden:
float d = 1e1f;
float e = 2.f;
float f = 6.022137e+23f;
Hier fällt auf, daß die
double g = .3;
double h = 1e-9d;
double i = 1e137;
double j = Double.longBitsToDouble(0x400921FB54442D18L);
Wie hier zu sehen ist, entfällt für
Wie am Anfang schon erwähnt, existiert für jeden primitiven Datentyp eine sogenannte Wrapperklasse, die den jeweiligen Datentyp kapselt, sowie diverse nützliche Methoden und Konstanten für das Arbeiten mit dem jeweiligen Typ bereitstellt. Selbst wenn man mit den primitiven Typen arbeitet, so bemüht man zuweilen dennoch Methoden der zugehörigen Klasse, da sie Funktionalität wie beispielsweise das Umwandeln von Zeichenfolgen in Zahlen und andersherum bereitstellen.
Die primitiven Typen und ihre jeweiligen Wrapperklassen sind im Folgenden noch einmal aufgelistet:
| Primitiver Typ | Klasse |
|---|---|
Konvertierungen zwischen Typ und zugehörigem Objekt sind üblicherweise recht unschön zu schreiben, das sieht dann beispielsweise folgendermaßen aus (hier am Beispiel von
int i = 3;
// Konvertierung von int zu Integer
Integer j = new Integer(i);
// Konvertierung von Integer zu int
int k = j.intVal();
Hat man nun in einem Ausdruck eine Hin- und Rückkonvertierung, weil eine Methode unbedingt
int i = 3;
i = doSomething(new Integer(i)).intVal();
Sonderlich schön lesbar ist das sicher nicht. Java hat mit der Version 1.5 (Java 5) ein Feature namens Autoboxing eingeführt, welches eben solche Konvertierungen zwischen den Basistypen und ihren zugehörigen Kapselobjekten für den Programmierer transparent macht, so daß man sich nicht mehr darum kümmern muß. Man sollte allerdings wissen, daß es passiert, da jede solche Konvertierung auch einen Performanceverlust mit sich bringt; ist man also darauf angewiesen, daß das Programm möglichst schnell läuft, so sollte man auf so etwas ein wenig aufpassen.
Eine der häufigsten Dinge, mit denen Programme arbeiten, gerade, wenn sie auf irgendeine Weise mit dem Benutzer interagieren, sind Zeichenfolgen oder auch Strings. Java bietet hierfür ähnlich wie C++ eine eigene Klasse an, die fast alles bietet, was man sich in diesem Zusammenhang wünschen kann:
String s = "Hallo";
Ein wesentlicher Aspekt von Strings in Java ist, daß sie unveränderlich sind. Das heißt, sie sind (zumindest für den Programmierer) nicht einfach wie in C ein Array von Zeichen; man kann in Java nicht einfach einzelne Zeichen in einem String direkt ändern. Jede Methode, die scheinbar einen String ändert, gibt in Wirklichkeit einen neuen String zurück. Ebenso sind Strings in Java nicht null-terminiert, d. h. sie werden nicht mit dem Nullzeichen (
Weiterhin hat jedes Objekt in Java eine Methode
Die Verkettung zweier Strings kann auf verschiedene Weise bewerkstelligt werden, wenngleich zumindest die zweite Variante vom Compiler in die erste überführt wird:
"Hello, ".concat("World!");
"Hello, " + "World" + "!";
String fs = String.format("Hello, %s!", "World");
Dem aufmerksamen Beobachter wird nicht entgangen sein, daß die letzte Variante nicht wirklich eine Stringverkettung ist, sondern eher an die Syntax von
Aus der Tatsache, daß Strings in Java unveränderlich sind, folgt eine Reihe von häufigen Fehlern, die einem gern unterlaufen. Eine einfache Routine, die eine (Text-)Datei in einen String einliest, mag man vielleicht naiv in etwa folgendermaßen implementieren (mehr oder weniger Pseudocode):
s = "";
while (!f.eof())
s = s + f.readLine();
D. h. es wird zeilenweise gelesen, solange man die Datei noch nicht vollständig gelesen hat und die gelesene Zeile wird an einen String angehängt. Was hier nun passiert, ist, daß für jede Zeile ein neuer String erzeugt wird, der Inhalt des alten umkopiert und dann die gelesene Zeile angehängt wird. Verbindet man das mit einem weiteren Feature von Java, nämlich der Garbage Collection, die automatisch nicht mehr verwendeten Speicher wieder freigibt, so kommen wir auf (selbst für eine mittelmäßig kleine Datei) immense Datenmengen, die im Speicher alloziert, umkopiert und wieder freigegeben werden. Verketten von Zeichenfolgen, gerade bei langen solchen, ist also sicherlich keine Sache, die man innerhalb von oft laufenden Schleifen machen sollte. Java hat hierfür noch eine Klasse
Arrays sind, wie in anderen Sprachen auch, ein ein- oder mehrdimensionales Feld eines bestimmten Typs. Allerdings, anders als beispielsweise in Pascal, gehört in Java die Länge des Arrays nicht zum Typ. Folglich kann eine Array-Variable Referenzen auf Arrays beliebiger Länge aufnehmen. Arrays werden in Java folgendermaßen deklariert:
int[] intArr;
String[] stringArr;
Mehr als eine Dimension wird kenntlich gemacht, indem man einfach mehr als ein eckiges Klammerpaar anfügt:
int[][] matrix;
Array-Instanzen, also die Enden der Referenzen, haben allerdings grundsätzlich eine konstante Länge, die einmal festgelegt wird:
intArr = new int[4];
erzeugt ein Array vom Typ
Es ist bei der Deklaration von Arrays egal, ob man die eckigen Klammern, die ein Array kennzeichnen, nach dem Typ oder nach dem Bezeichner schreibt. Folgende Deklarationen sind also äquivalent:
int[][] foo;
int foo[][];
int[] foo[];
Man sollte allerdings zugunsten der Lesbarkeit bei einer Variante bleiben und diese konsistent durchhalten. Es sei denn, man möchte Spielereien der folgenden Art machen:
float[] arr1, arr2, matrix[];
welche zwei eindimensionale, sowie ein zweidimensionales Array (eine Matrix) in einer Zeile deklariert. Aber in den meisten Fällen sollte es nicht nötig sein, auf solche Konstrukte zurückzugreifen.
Bei vorgegebenem Inhalt von Arrays ist die Größe auch gleich implizit festgelegt. Man kann Arrays folgendermaßen gleich bei der Deklaration initialisieren:
int[] fak = { 1, 1, 2, 6, 24, 120, 720, 5040 };
Hierbei ist die Länge des Arrays offenbar 8 und man erhält durch
Java kennt verschiedene Arten von Konvertierungen eines Datentyps in einen anderen, man sollte darüber Beischeid wissen, es erspart einem so manche unliebsame Überraschung.
Betrachten wir folgendes kleine Beispiel:
int i = 12.5;
Offenbar versuchen wir hier, einem
int i = (int) 12.5;
Nun wird die 12,5 vor der Zuweisung erst in einen
Dies ist wahrscheinlich eine der am häufigsten unbewußt genutzen Konvertierungen. Wir hatten weiter oben den Operator + auch für die Stringverkettung kennengelernt. Dieser funktioniert auch, wenn man Strings mit anderen Typen oder Objekten verkettet. In diesem Falle wird im Hintergrund automatisch die
System.out.println("i = " + i);
ergibt mit unserem oben festgelegten
In Ermangelung einer sinnvollen deutschen Bezeichnung für dieses Konzept nenne ich es hier einmal ebenso wie im Englischen. Grundlage ist, daß ein (numerischer) Datentyp automatisch in einen mit größerem Wertebereich umgewandelt werden kann, wenn es der Kontext erfordert. Wenn wir uns an die nötige explizite Konvertierung erinnern, so wissen wir noch, daß
int i = 12.5;
nicht möglich war. Stattdessen mußten wir
int i = (int) 12.5;
schreiben.
float f = i;
ist hingegen problemlos möglich, da hier der Wertebereich von
| Ausgangstyp | Konvertierungstypen |
|---|---|
Nun kann es passieren, daß eine solche Konvertierung mit Genauigkeitsverlust abläuft. Solange nur zwischen Ganzzahl- oder nur zwischen Gleitkommatypen konvertiert wird, ändert sich der Wert nicht, allerdings kann es bei der Konvertierung von einem Ganzzahl- zu einem Gleitkommatyp passieren, daß die Zahl hinterher nicht mehr die gleiche ist. In folgendem Beispiel
int big = 1234567890;
float approx = big;
System.out.println(big - (int) approx);
wird
Promotion oder „Beförderung“ ist ein Konzept, welches die Implementierung von Operatoren vereinfacht, indem immer nur gleiche Typen miteinander verknüpft werden können. Hierbei wird bei gemischttypigen Ausdrücken der Typ mit dem geringeren Wertebereich in den anderen überführt, bevor die Operation ausgeführt wird:
int i = 12.5;
float f = i;
f = i * f;
In der letzten Zeile wird zunächst
Diese Konvertierungsform ist ähnlich zur vorhergehenden, allerdings wird sie beim Aufruf von Methoden und nicht beim Anwenden von Operatoren durchgeführt. Sie dient hier auch nicht zur Angleichung der Operanden sondern zum Angleichen der Parameter an die Typen, die eine Methode akzeptiert.
Beispiel:
float f = 3.0f;
double d = Math.sin(f);
Hier wird
Java kennt insgesamt 37 Operatoren, die sich wie folgt kategorisieren lassen:
| Vergleich | |
|---|---|
| Boolesche Operatoren | |
| Bitweise | |
| Arithmetisch | |
| Bedingung | |
| Zuweisung |
Im Folgenden betrachten wir uns diese mal ein wenig genauer.
Sämtliche der Vergleichsoperatoren sind nur auf numerischen Datentypen definiert und sie liefern immer ein Resultat vom Typ
Zunächst gibt es den Test auf numerische (Un-)Gleichheit. Hierfür dienen die Operatoren
boolean a = (5 == 5); // true
boolean b = (.3 != .2); // true
boolean c = (1 == 4); // false
Weiterhin gibt es die Operatoren für den numerischen Vergleich:
boolean d = (3 > 4); // false
boolean e = (8 <= 10); // true
boolean f = (2 < 2); // false
boolean g = (1.5 >= 1.25); // true
Zu den Relationen selbst werde ich hier nichts mehr sagen, sie dürften ausreichend selbsterklärend sein
Diese sind nur auf dem Typ
boolean a = !true; // false
boolean b = !a; // true
Weiterhin ein logisches Und:
boolean c = a && b; // false
boolean d = a && !c; // true
sowie ein logisches Oder:
boolean e = true || false; // true
boolean f = false || false; // false
boolean g = true || true; // true
Im Grunde also nicht sonderlich verwunderlich, die Operatorzeichen wurden auch direkt so aus C übernommen, so daß sich jeder, der C bzw. C++ beherrscht, sofort heimisch fühlen sollte.
Nun kann man mit den booleschen Operatoren zwar interessante Dinge anstellen aber für mehr als Aussagenlogik reicht es auch nicht. Manchmal möchte man vielleicht Bits in Zahlen direkt manipulieren und während das zwar schwer zu durchschauen für den gelegentlichen Leser des Quelltextes sein mag, so geht es doch und zwar mit eben den Operatoren
Diese Operatoren funktionieren nur mit Ganzzahltypen und sindliefern auch wieder einen solchen zurück. Dank Promotion ist das Ergebnis vom größeren der beiden Eingangstypen. Hilfreich ist auch bei den Operatoren, sich vor Augen zu halten, daß Ganzzahlen in Java grundsätzlich als Zweierkomplement dargestellt werden.
Zunächst gibt es ein bitweises Nicht,
int a = ~0; // -1
int b = ~-846; // 845
Die zweite Zeile ist hier interessant und bietet und eine alternative Möglichkeit, die numerische Negation nachzubilden:
int c = ~b + 1; // -845
Diese Variante nutzt die Eigenschaften des Zweierkomplements, bzw. die entsprechende Rechenvorschrift.
Natürlich können wir auch nur die jeweils gleichen Bits aus zwei Zahlen übernehmen, hierfür das bitweise Und:
int d = a & 7; // 7
int e = 231 & 14; // 6
// 11100111
// & 00000110
// = 00000110
Weiterhin haben wir ein bitweises Oder:
int f = 13 | 24; // 29
// 001101
// | 011000
// = 011101
sowie ein ausschließendes Oder:
int g = 13 ^ 24; // 21
// 001101
// ^ 011000
// = 010101
Mit dem ausschließenden Oder können wir noch andere lustige Dinge machen. Beispielsweise werden wir merken, daß der Ausdruck
x ^ -1;
genau das gleiche ergibt wie
~x;
Dies liegt daran, daß die Zahl –1 in Zweierkomplementdarstellung nur aus gesetzten Bits besteht. Somit kehrt
Es gibt die drei Schiebeoperatoren:
int h = 31 >> 3; // 3
int i = -67 >> 3; // -3
int j = 31 >>> 3; // 3
int k = -67 >>> 3; // 536870903
int l = 3 << 4; // 48
int m = 1000000000 << 2; // -294967296
Wie zu sehen ist, ergibt der
Ein vielleicht nicht auf den ersten Blick sichtbares Feature (und auch eines, welches man vielleicht nicht sonderlich häufig benötigt), ist, daß eine Shift-Operation um einen negativen Betrag in der jeweils umgekehrten Shift-Operation resultiert.
Nun kommt man häufiger in die Verlegenheit, etwas berechnen zu wollen. Auch hier bietet Java einem das übliche Spektrum an Arithmetikoperatoren. Diese sind für alle Operationen zwischen numerischen (also Ganzzahl- oder Gleitkommatypen) definiert, mit der Ausnahme des
Addition und Subtraktion dürften soweit bekannt sein:
int a = 5 + 6; // 11
float b = a + 3.2f; // 14.2
int c = a - 36; // -25
double d = b - c + a; // 50.2
Weiterhin gibt es eine Art Kurzform für den Ausdruck
a = 1;
a++; // -> a = 2
++a; // -> a = 3
b = a++; // -> b = 3, a = 4
b = --a; // -> b = a = 3
Nachdem wir nun jegliche Art und Weise, Zahlen zu addieren und zu subtrahieren, eingehend behandelt haben, schreiten wir nunmehr zu Multiplikation und Division. Auch hier eigentlich wenig überraschendes; bei Division sollte man allerdings hier und da aufpassen: Wenn man beispielsweise zwei Ganzzahlen durcheinander dividiert, wird keine Gleitkommazahl herauskommen, wegen der oben schon angesprochenen Konvertierungen.
double x = 14.2 * 13; // 184.6
int y = 3 / 2; // 1
float z = 3 / 2; // 1.0
float z1 = 3f / 2; // 1.5
Weiterhin gibt es noch die Division mit Rest, wie sie sicher noch aus der Grundschule bekannt ist. Modulo (
int mod1 = 14 % 8; // 6
double mod2 = 5.2 % 1.2; // 0.4
Es kommt häufig vor, daß man Code der folgenden Art hat:
int i;
if (a)
i = 5
else
i = 6;
oder auch
int i = 6;
if (a)
i = 5;
Dieses Konstrukt läßt sich mit dem Bedingungsoperator auch folgendermaßen schreiben:
int i = a ? 5 : 6;
Dieser Operator hat die Syntax Bedingung
Das einfachste hierbei ist sicherlich die einfache Zuweisung. Diese sieht einfach folgendermaßen aus:
a = b;
oder
x = 5 + b - a;
und inzwischen sollte sie uns nicht mehr ungewohnt vorkommen.
Häufig kommt allerdings auch das folgende Muster von Ausdrücken vor:
a += 1; // a = a + 1, entspricht auch a++ oder ++a
x *= 2; // x = x * 2
y >>>= 1; // y = y >>> 1
usw.
Wie große Teile der restlichen Syntax sind auch die Kontrollstrukturen eigentlich unverändert aus C bzw. C++ übernommen worden. Dies macht, wie üblich, den Umstieg auf Java um einiges einfacher.
Zunächst ein paar grundsätzliche Dinge zu Anweisungen und Blöcken. Die meisten Kontrollstrukturen führen Anweisungsblöcke aus. Ein solcher Anweisungsblock besteht aus einer oder mehr Anweisungen, mehrere Anweisungen werden dabei durch geschweifte Klammern gruppiert, Anweisungen werden durch ein Semikolon abgeschlossen:
a = a + b;
System.out.println("Hello World");
{
x = Math.sin(y);
y = x++;
}
Da jede einzelne Anweisung für sich genommen auch ohne geschweifte Klammern ein Anweisungsblock ist, kann man also an den Stellen, wo eine Kontrollstruktur einen Block erwartet, man aber nur eine Anweisung ausführen möchte, auch diese eine Anweisung direkt hinschreiben.
Die leere Anweisung ist wohl mit die langweiligste von allen. Sie tut nichts, dennoch gibt es sie:
;
Beruhigend allerdings, daß man nach Belieben Semikola zwischen Anweisungen einfügen kann, ohne daß es den Sinn des Programms entstellt (allenfalls vielleicht die Lesbarkeit).
Wohl eine der häufigsten Kontrollstrukturen ist die
if (a == b) doSomething();
if (b == c)
doSomething()
else
doSomethingElse();
if (c != d) {
statement1();
statement2();
}
Zuweilen möchte man den gleichen Ausdruck auf mehr als einen möglichen Wert überprüfen. Natürlich kann man das in folgender Form machen:
if (a == 1)
method1()
else if (a == 2)
method2()
else if (a == 3)
method3()
else
methodElse();
Einige Sprachen (z. B. Python) erlauben auch nur diese Variante, allerdings gibt es in Java eine elegantere Lösung (allerdings mit den gleichen Einschränkungen wie schon zu Zeiten von C):
switch (a) {
case 1:
method1();
break;
case 2:
method2();
break;
case 3:
method3();
break;
default:
methodElse();
break;
}
Zwar hat diese Schreibweise einige Vorteile gegenüber dem „
static void howMany(int k) {
switch (k) {
case 1: System.out.print("one ");
case 2: System.out.print("too ");
case 3: System.out.println("many");
}
}
public static void main(String[] args) {
howMany(3);
howMany(2);
howMany(1);
}
Was wir hier erhalten, ist folgendes:
many
too many
one too many
Was wir aber eigentlich haben wollten, wäre vielleicht folgendes gewesen:
one
two
many
Der Teufel liegt hier im Detail bzw. in der exakten Übernahme der Syntax von
Wie dem auch sei, so hätte der Code aussehen sollen, um die erwartete Ausgabe zu liefern:
static void howMany(int k) {
switch (k) {
case 1:
System.out.println("one");
break; // exit the switch
case 2:
System.out.println("two");
break; // exit the switch
case 3:
System.out.println("many");
break; // not needed, but good style
}
}
public static void main(String[] args) {
howMany(1);
howMany(2);
howMany(3);
}

Wie in obigem Cartoon schon angedeutet, gibt es zwei verschiedene Arten von allgemeinen Schleifen:
einmal durch.
Dies äußert sich auch optisch im Quelltext. Bei
while (i < 5) {
doSomething();
i++;
}
do {
doSomething();
i++;
} while (i < 5);
Im Grunde tun diese beiden Schleifen das gleiche, zumindest solange
Die Bedingung selbst muß immer vom Typ
Was ebenfalls nicht selten benötigt wird, sind Schleifen, die gezählt werden. Man hat einen Startwert, einen Endwert und eine Iteration, die irgendwann den Endwert liefert. Zumindest ist dies die häufigste Anwendung. Beispielsweise nacheinander alle Zahlen von 1 bis 10 durchlaufen. Für diese Anwendungen gibt es die folgende Syntax:
for (int i = 1; i <= 10; i++) {
...
}
Diese
i = 1;
while (i <= 10) {
...
i++;
}
Diese
Es gibt allerdings ab Java 1.5 noch eine weitere Syntax der
for (int i = 0; i < myArr.length; i++) {
...
}
welches eine Schleife über alle Elemente eines Arrays darstellt. Braucht man innerhalb der Schleife nämlich nur die jeweiligen Werte (und nicht auch noch ihren Index im Array), so kann man das auch folgendermaßen schreiben (angenommen,
for (double d : myArr) {
...
}
Tatsächlich funktioniert dieses Konstrukt nicht nur auf Arrays, sondern auch auf Collections, Dictionaries, etc. Die Syntax ist jeweils die gleiche: Vor dem Doppelpunkt steht eine Variable des im Container enthaltenen Typs, also im Falle eines String-Arrays wäre sie vom Typ String und nach dem Doppelpunkt kommt das Array oder ein Containerobjekt. Innerhalb der Schleife hat man nun in jeder Iteration jeweils das nächste Element des Containers. Im Falle von Arrays sind diese wahrscheinlich in der gleichen Reihenfolge wie im Array, allerdings muß dies bei anderen Containern nicht der Fall sein, da diese evtl. nicht einmal eine interne Reihenfolge der Elemente haben.
Wir hatten
for (int i : a) {
if (i < 50)
break;
}
springt
In dieser Form springt break lediglich aus dem direkt umgebenden Block, um genauer zu spezifizieren, aus welchem Block
block: {
doSomething();
doSomethingElse();
}
Dieser Block hat nun den Namen
block: {
for (int i = 100; i > 0; i--) {
doSomething();
if (i < 50)
break block;
doSomethingElse();
}
doSomethingUtterlyElse(); // this won't get executed
}
Neben
for (int i : a) {
if (i >= 50)
continue;
}
Ebenso wie
elsewhere: for (int i = 0; i < 10; i++) {
for (int j = 0; j < 10; j++) {
if (i == j)
continue elsewhere;
System.out.println("Differenz" + (i - j));
}
}
void PrintNumbersBelowFifty(int i) {
if (i >= 50)
return;
System.out.println(i);
}
Ebenfalls ist hier zu sehen, daß
Wenden wir uns dem
int inc(int i) {
return i + 1;
}
oder auch
int saturatedDec(int i) {
if (i > 0)
return i - 1
return 0;
}
Hier auch wieder demonstriert, daß
Nicht gültig wäre allerdings eine Methode, die
int test() {
return "blah";
}
da hier ein
Natürlich können Methoden nicht nur Werte von primitive Typen zurückgeben sondern auch Objekte. Für diesen Fall gibt es noch eine besondere Möglichkeit, neben der Rückgabe eines Objekts des eigentlichen Typs: Eine
Als Einstieg möchte ich hier exemplarisch die Umsetzung eines Moduls einer normalen prozeduralen Sprache (hier Pascal) in eine Java-Klasse durchführen, ehe ich im Detail darauf eingehe, wie so etwas gebildet wird – nicht unähnlich dem „Hello World“-Beispiel zu Anfang.
Nehmen wir uns eine einfache prozedurale Implementation eines Kellerspeichers (Stack) in Pascal zur Hand:5
interface
uses elem;
const maxs = 5;
type STACK =
record
elts : array [1..maxs] of ELEMENT;
ptr : integer;
end;
procedure empty(var s: STACK);
procedure pop(var s: STACK);
function top(s : STACK) : ELEMENT;
procedure push(var s: STACK; e: ELEMENT);
implementation
function isempty(s: STACK): boolean;
begin
isempty := (s.ptr = 0)
end;
procedure mkErrorst(var s: STACK);
begin
s.ptr := maxs + 1
end;
function iserror(s : STACK): boolean;
begin
iserror := (s.ptr > maxs)
end;
procedure empty(var s : STACK);
begin
s.ptr := 0
end;
function top(s : STACK) : ELEMENT;
begin
if isempty(s) then begin
top := errorel;
writeln('Top: Stack Empty')
end else if iserror(s) then begin
top := errorel;
writeln('Top: Stack broken')
end else
top := s.elts[s.ptr]
end;
procedure pop(var s : STACK);
begin
if isempty(s) then begin
mkErrorst(s)
end else if iserror(s) then
{do nothing}
else
s.ptr := pred(s.ptr)
end;
procedure push(var s: STACK; e: ELEMENT);
begin
if iserrel(e) then
{do nothing}
else if iserror(s) then
{do nothing}
else if s.ptr = maxs then begin
{stack full}
mkErrorst(s)
end else begin
s.ptr := succ(s.ptr);
s.elts[s.ptr] := e
end
end;
begin
end.
public int[] elts = new int[maxs];
public int ptr;
}
public class StackClass {
public static final int errorel = -99;
public static void mkErrorStack(Stack s) {
s.ptr = maxs + 1;
}
public static boolean isError(Stack s) {
return s.ptr > maxs;
}
public static void empty(Stack s) {
s.ptr = 0;
}
public static int top(Stack s) {
if (isEmpty(s)) {
System.out.println("Stack empty");
return errorel;
} else if (isError(s)) {
System.out.println("Stack broken");
return errorel;
} else
return s.elts[s.ptr];
}
public static void pop(Stack s) {
if (isEmpty(s)) {
System.out.println("Stack empty");
mkErrorStack(s);
} else if (isError(s)) {
System.out.println("Error Stack");
// do nothing
} else
s.ptr--;
}
public static void push(Stack s, int i) {
if (i == errorel) {
System.out.println("Bad Element");
// do nothing
} else if (isError(s)) {
System.out.println("Stack broken");
// do nothing
}
else if (s.ptr == maxs) {
System.out.println("Stack Overflow");
mkErrorStack(s);
} else {
s.ptr++;
s.elts[s.ptr] = i;
}
}
}
Schauen wir uns doch einmal an, wo wir hier mit Objekten gearbeitet haben. Nach sorgfältigem Durchsuchen des obigen Quelltextes wird möglicherweise auffallen, daß wir eigentlich überhaupt nicht mit Objekten gearbeitet haben. Unser „Objekt“ (der Stack) ist lediglich eine Datenstruktur, alle Methoden, die damit etwas anstellen, lagern in einer völlig anderen Klasse. Im Grunde haben wir hier also nichts weiter als eine Abbildung der Pascal-Sprachkonstrukte
Schauen wir uns zunächst an, was Klassen in Java eigentlich sind. Eine Klasse kapselt üblicherweise eine Datenstruktur und ihre zugehörigen Funktionen (Methoden genannt). Oben wäre also die eigentlich sinnvolle Variante gewesen, die zum Stack gehörenden Methoden auch in die Klasse
Klassen werden wie folgt deklariert:
class MyClass {
...
}
nach Belieben können vor
Im Allgemeinen kann man davon ausgehen, daß nahezu alles, was man in Java benötigt, durch eine Klasse repräsentiert wird. Leicht zu merken und nicht völlig fern der Wirklichkeit ist der Spruch „Klassen in Massen“, der dies illustriert. Selbst wenn man nur eine simple Datenstruktur benötigt, wird man eine neue Klasse anlegen. Dies hat den weiteren Vorteil, daß man zugehörige Methoden gleich dort mit hinein sortieren kann. In diesem Sinne könnten Klassen auch als Ersatz der Module aus anderen Sprachen angesehen werden (
Klassen, sowie ihre Daten, Methoden und Konstruktoren haben einen gewissen Sichtbarkeitsbereich innerhalb von Java. Dies kann und sollte man dazu nutzen, Implementationsdetails vor dem Nutzer der Klasse zu verbergen. Versucht man, außerhalb des deklarierten Sichtbarkeitsbereiches auf eine Klasse oder einen Bestandteil einer Klasse zuzugreifen, so schlägt dies fehl. Standardmäßig sind Klassen und ihre Bestandteile (Member genannt) lediglich im gleichen Package sichtbar (zu Packages mehr in Abschnitt „Packages“). Weiterhin gibt es den
Beispiele:
public class Test { // öffentliche Klasse
private int i; // nur innerhalb dieser Klasse sichtbar
public void blah() { // öffentliche Methode
return;
}
protected int getOne() { // geschützte Methode
return 1; // sichtbar auch in abgeleiteten Klassen
}
}
Klassen können zunächst zweierlei Arten von Membern enthalten: solche, die als
Als Felder werden Variablen oder Konstanten innerhalb einer Klasse bezeichnet. Sie sind in den meisten Fällen
Methoden sind im Grunde ähnlich wie Variablen, nur daß sie Code ausführen, statt direkt einen Wert zurückzuliefern. Dieser Vergleich hinkt zwar, das soll uns hier aber nicht weiter stören. Methoden werden grundsätzlich als Member einer Klasse deklariert und zwar in der folgenden Form:
Typ Bezeichner([Parameter])
was schon sehr an die in Abschnitt „Variablen“ gezeigte Syntax erinnert, nur daß man einer Methode auch Parameter übergeben kann. Die Deklaration der Typen dieser Parameter erfolgt in Klammern nach dem Methodennamen. Direkt danach folgt ein Block mit dem sogenannten Methodemrumpf. Dieser stellt die Implementation der Methode dar. Über Rückgabewerte und auch den besonderen Typ
Es kann in einer Klasse mehrere Methoden mit gleichem Namen und unterschiedlicher Signatur geben (Polymorphismus):
int incMe(int i) {
return i + 1;
}
double incMe(double d) {
return d + .1;
}
int incOrZero() {
return 0;
}
int incOrZero(int i) {
return i + 1;
}
Dies ist unter anderem hilfreich, wenn man verschiedene Typen anders behandeln muß oder aber für verschiedene Aufrufe der gleichen Methode unterschiedliche Parameter nutzen oder gar Teile weglassen kann.
Konstruktoren sind besondere Methoden, die dazu da sind, ein Objekt zu erzeugen und zu initialisieren. Tatsächlich muß man sich um die eigentliche Erzeugung des Objektes nicht mehr kümmern, das wird einem abgenommen. Aber man hat in einem Konstruktor schon Zugriff auf die Felder und Methoden des Objektes. Konstruktoren sind damit nie
Mancher mag aus Sprachen wie C++ vielleicht eine weitere besondere Art einer Methode kennen: den Destruktor. Ein Konzept wie dieses hat Java nicht direkt, Objekte werden vom Garbage Collector freigegeben, wenn sie nicht mehr referenziert werden und alle ihre Bestandteile lösen sich damit ebenso auf, da selbigen ja nun die Referenz fehlt. Folglich müssen Objekte in Java nicht mehr unbedingt hinter sich aufräumen, wie es in Sprachen mit expliziter Speicherverwaltung nötig ist. Dennoch gibt es etwas ähnliches, eine besondere Methode namens
Klassen können neben den oben genannten Dingen auch noch selbst Klassen enthalten, die hier lediglich der Vollständigkeit halber kurz genannt werden sollen. Ich werde hier nicht weiter im Detail darauf eingehen, da dies ein einigermaßen fortgeschrittenes Thema ist.
Innere Klassen sind normalerweise an eine Instanz iher äußeren Klasse gebunden. Weiterhin gibt es sogenannte lokale innere Klassen, welches Klassen sind, die innerhalb einer Methode deklariert wurden. Anonyme Klassen hingegen sind lokale innere Klassen, die nicht benannt wurden.
Klassen können als abstrakt (über das Schlüsselwort
Eine andere, fast schon entgegengesetzte, Deklaration für Klassen ist die Möglichkeit, sie
Mit dem nun gerade neu erworbenen Wissen darüber, was Klassen sind und wie man sie zweckmäßigerweise schreibt, können wir nun unser Beispiel vom Anfang wieder hervorholen und es mal etwas sinnvoller schreiben. Diesmal kann unser Stack auch beliebige Objekte beinhalten, nicht nur Ganzzahlen.
Die Klasse
public class Stack {
/** Maximale Anzahl an Elementen */
private int maxs;
/** Konstante für das Fehlerelement */
public static final Object errorel = null;
/** Das Array der Elemente */
public Object[] elts;
/** Zeiger auf das oberste Element im Stack */
public int ptr;
/**
* Erstellt einen neuen Stack mit der angegebenen
* Maximalgröße
*
* @param size
* die maximale Größe des Stacks
*/
public Stack(int size) {
ptr = 0;
maxs = size;
elts = new Object[size];
}
/**
* Gibt zurück, ob der Stack leer ist.
*
* @return true falls der Stack leer ist, sonst false
*/
public boolean isEmpty() {
return ptr == 0;
}
/** Wandelt den Stack in einen Fehlerstack um */
private void mkErrorStack() {
ptr = maxs + 1;
}
/**
* Gibt zurück, ob der Stack ein Fehlerstack ist
*
* @return true, falls der Stack ein Fehlerstack ist,
* sonst false
*/
public boolean isError() {
return ptr > maxs;
}
/**
* Gibt das oberste Element vom Stack zurück
*
* @return das oberste Element vom Stack falls der
* Stack nicht leer ist, sonst errorel
*/
public Object top() {
if (isEmpty()) {
System.err.println("Stack empty");
return errorel;
} else if (isError()) {
System.err.println("Stack broken");
return errorel;
} else
return elts[ptr];
}
/** Entfernt das oberste Element vom Stack */
public void pop() {
if (isEmpty()) {
System.err.println("Stack empty");
mkErrorStack();
} else if (isError()) {
System.err.println("Error Stack");
// nichts machen
} else
elts[ptr] = null;
ptr--;
}
/**
* Fügt ein Element in den Stack ein
*
* @param elem
* das einzufügende Element
*/
public void push(Object elem) {
if (elem == errorel) {
System.err.println("Bad Element");
// do nothing
} else if (isError()) {
System.err.println("Stack broken");
// do nothing
}
else if (ptr == maxs) {
System.err.println("Stack Overflow");
mkErrorStack();
} else {
ptr++;
elts[s.ptr] = elem;
}
}
}
Ich habe hier auch Gebrauch einer speziellen Kommentarform gemacht. Diese als JavaDoc bezeichnete Form ist eine einheitliche Variante, Quelltext zu dokumentieren und kann automatisch in verschiedene Formate umgewandelt werden, beispielsweise HTML. Weiterhin wurde die Größenbeschränkung für den Stack variabel gemacht und kann nun einem Konstruktor übergeben werden. Außerdem wurden Methoden, die nur zur inneren Verwendung da sind,
Vererbung ist ein mächtiges Konzept in der objektorientierten Programmierung und wird auch als Generalisierung (zum Beispiel in UML) bezeichnet. Ich habe in vergangenen Abschnitten schon ab und zu von abgeleiteten Klassen geschrieben. Damit ist Vererbung gemeint. Wenn eine Klasse von einer anderen „erbt“, so übernimmt sie alle Felder, Methoden, etc. von der „Elternklasse“ und kann diese wahlweise um weitere Member ergänzen oder aber bestehende überschreiben und damit ihr Verhalten zu ändern.
In Java dürfen Klassen nur von maximal einer anderen Klasse erben. Dies ist eine Designentscheidung und liegt darin begründet, daß Vererbung von mehr als einer Klasse im Allgemeinen sehr komplex zu implementieren ist. Im Quelltext wird Vererbung durch das Schlüsselwort
public class MyClass extends MyOtherClass {
...
}
In diesem Beispiel würden für
public Class Nat {
public static final long zero = 0L;
private long theNumber;
public Nat() {
theNumber = 0L;
}
public Nat(long l) {
// Vorsicht mit negativen Zahlen
if (l < 0)
theNumber = 0
else
theNumber = l;
}
public void inc() {
theNumber++;
}
public void dec() {
// nur dekrementieren, wenn größer 0
if (theNumber > 0)
theNumber--;
}
Und damit wir die Zahl auch ausgeben können, folgt noch die überladene Methode
public String toString() {
return new Long(theNumber).toString();
}
}
Soweit haben wir jetzt natürliche Zahlen. Rechnen können wir nicht sehr aber das ist auch nicht weiter wichtig. Nun kommt uns in den Sinn, daß es vielleicht schön wäre, auch ganze Zahlen, also den negativen Zahlenbereich zusätzlich noch zu haben. Die restliche Funktionalität soll die gleiche bleiben. Es bietet sich hier also an, die Klasse
public class Ganz extends Nat {
Nun haben wir schon alles, was wir in
public Ganz() {
// Aufrufen des übergeordneten Konstruktors
super();
}
// Überschreiben des Konstruktors
public Ganz(long l) {
theNumber = l;
}
// Überschreiben der Methode
public void dec() {
theNumber--;
}
Anders als beispielsweise in Delphi ist es in Java nicht nötig, das Überschreiben von Methoden explizit durch ein Schlüsselwort zu kennzeichnen. Man mag dazu stehen wie man mag, allerdings sind überschriebene Methoden selten eine schwer zu findende Fehlerquelle, insofern erübrigt sich eine explizite Kennzeichnung meistens und schafft oft eher nur Verärgerung, wenn man die Kennzeichnung vergißt.
Nun wollen wir unsere Klasse
public negate() {
thenumber *= -1;
}
}
Und sind hier auch einmal am Ende mit der Klasse
Vererbung bei Klassen hat den (gewollten) Nachteil, daß man lediglich von einer Klasse erben kann. Da dies für einige komplexere Anwendungen nicht ausreicht, kamen noch sogenannte Interfaces hinzu. Interfaces sind im Grunde die öffentlich sichtbaren Bestandteile einer Klasse ohne Implementation. Sie ähneln hier ein wenig abstrakten Klassen in dem Sinne, daß man aus Interfaces kein Objekt erstellen kann und die Implementation unvollständig ist. Nur, daß die Implementation bei Interfaces so ziemlich am unvollständigsten ist, was man sich überhaupt vorstellen kann.
Interfaces dürfen, wie schon erwähnt, lediglich öffentliche Bestandteile enthalten. Konkret bedeutet dies, daß man nur öffentliche Konstanten (
public interface MyInterface {
// Konstante
public static final int x = 5;
// Methodensignatur
public void doSomething();
}
Zwischen Interfaces darf man allerdings Mehrfachvererbung machen:
public interface MyInterface extends Int1, Int2, Int3 {
...
}
Außerdem dürfen Klassen mehrere Interfaces implementieren. Man beachte hier, daß es sich von der Terminologie her schon von der Vererbung unterscheidet. Klassen implementieren Interfaces, aber erben von anderen Klassen. Quelltextmäßig sieht das folgendermaßen aus:
class MyClass implements MyInterface {
// vom Interface deklarierte Methode,
// muß implementiert werden:
public void doSomething() {
...
}
...
}
Wie schon im Kommentar im Beispiel angedeutet müssen vom Interface definierte Methoden implementiert werden, vergißt man dies, so meldet einem der Compiler einen Fehler.
Interfaces sind eine mächtige, allerdings nicht allmächtige Methode der Mehrfachvererbung. Es gibt einzelne (seltene) Probleme, die sich nur mit „echter“ Mehrfachvererbung aber nicht mit Interfaces lösen lassen.
Java hat einige sogenannte “tagging interfaces”, welche im Grunde leer sind und nur eine Art Marke darstellen, welchen Zweck eine Klasse hat. Dies ist vom Standpunkt des Programms oft völlig irrelevant aber es erleichtert die Lesbarkeit, wenn man sie benutzt und sie geben auch eine generelle Menge von Objekten mit ähnlichem Sinn und Zweck an, so daß man sie auch als Typ verwenden kann.
Packages sind neben Klassen die grundelegende hierarchische Ordnungsstruktur in Java. Sie bilden direkt eine Ordnerstruktur im Dateisystem ab, allerdings sind die Komponenten eines Packages durch Punkte getrennt. Aus dem Pfad unterhalb eines Projekts
de/hypftier/vortrag/java/test
würde beispielsweise der Packagename
de.hypftier.vortrag.java.test
werden. Hier zeigt sich auch das von Sun vorgeschlagene (oder gar vorgeschriebene) Benennungsschema für öffentliche Packages. Im Grunde ist es zwar egal, wie man seine Packages gliedert und unterteilt, aber sobald man vorhat, den Code öffentlich zu machen, sollte man diesem Schema folgen, um mögliche Kollisionen mit Packages anderer Leute oder Organisationen zu vermeiden. Dieses Schema beginnt mit der Domain desjenigen, der das Package geschrieben hat; und zwar komponentenweise rückwärts geschrieben. D. h. es beginnt mit der Top-level Domain beispielsweise
Sämtliche von Java nativ bereitgestellten Klassen finden sich im
Festlegen, zu welchem Package eine Klasse gehört, kann man über die Anweisung
package de.toll.bin.ich.mypackage
vor der Deklaration der Klasse in der Datei. Dies erfordert, daß die Klassendatei sich in genau dem Verzeichnis befindet, das vom Package vorgegeben wird.
Nun ist es nicht immer angenehm, den kompletten Packagepfad vor jede Klasse zu schreiben, die man verwenden möchte, daher kann man Klassen auch importieren. Dies bedeutet, daß sie im aktuellen Namespace sichtbar und zugreifbar sind, auch wenn man nicht das Package mit davor schreibt. Dies kann man über die
import javax.crypto.*;
oder aber für einzelne Klassen:
import java.awt.AWTKeyStroke;
Fehlerbehandlung in Java ist C++ entlehnt und verhält sich damit auch ziemlich ähnlich. Ein Fehler wird durch das Auftreten einer sogenannten Ausnahme bzw. Exception. ausgelöst. Diese unterbrechen den Kontrollfluß des Programms an der Stelle ihres Auftretens. An dieser Stelle gibt es nun zwei Möglichkeiten, mit ihnen umzugehen. Einerseits kann man Ausnahmen behandeln oder aber festlegen, daß sich die Methode, in der sie auftritt nicht darum kümmert und die Ausnahme einfach an die aufrufende Methode weiterreichen.
Als Grundregel gilt, daß Ausnahmen immer entweder behandelt oder weitergereicht werden müssen. Ausgenommen hiervon sind Ausnahmen, die von
throw new IndexOutOfBoundsException();
Eine Ausnahme wird über eine
int doSomething() throws IndexOutOfBoundsException {
...
throw new IndexOutOfBoundsException();
...
}
Diese Regelung hat außerdem den Vorteil, daß Java immer weiß, ob irgendwo unbehandelte Ausnahmen sind, da jede Methode explizit deklarieren muß, wenn sie bei der Ausführung eventuell eine Ausnahme auslösen kann. Manchmal ist es zwar nervig, den halben Code mit dem gleich folgenden
Strukturierte Ausnahmenbehandlung erfolgt über die
try {
// Block, der eine Ausnahme auslösen kann
} catch (Ausnahmetyp e) {
// Fehlerbehandlung
} catch (AndererAusnahmetyp e) {
// mehr Fehlerbehandlung
} finally {
// Aufräumarbeiten
}
Wie hier zu sehen ist, schließt der
des
Mit
private int i = 0;
public int puzzle() {
try {
return i++;
} catch (RuntimeException e) {
i++;
} finally {
i++;
return 5;
}
}
Zugegeben, ein recht konstruiertes Beispiel, aber wer Lust hat, darf gerne einmal raten, was die Methode zurückgibt und welchen Wert
Es gibt neben Exceptions auch noch sogenannte Errors. Dies sind meist von der virtuellen Maschine ausgelöste Fehler, von denen sich ein Programm normalerweise nicht mehr erholen kann. Dies sind schwerwiegende Fehler wie beispielsweise
Ich möchte an dieser Stelle noch einmal einigen einzelnen Leuten für Hilfe zum Thema Java danken: Dr. Elmar Ludwig, Andreas Kohn sowie Andreas Tschritter. Ebenfalls dankend erwähnt sei hier Andreas Dähn für das Beantworten vieler Anfängerfragen zum Thema LATEX.
| Attachment | Size |
|---|---|
| Ausarbeitung | 459.64 KB |
| Folien | 147.49 KB |