2.3 Datentypen, Typisierung, Variablen und Zuweisungen
Java nutzt, wie es für imperative Programmiersprachen typisch ist, Variablen zum Ablegen von Daten. Eine Variable ist ein reservierter Speicherbereich und belegt – abhängig vom Inhalt – eine feste Anzahl von Bytes. Alle Variablen (und auch Ausdrücke) haben einen Typ, der zur Übersetzungszeit bekannt ist. Der Typ wird auch Datentyp genannt, da eine Variable einen Datenwert, auch Datum genannt, enthält. Beispiele für einfache Datentypen sind: Ganzzahlen, Fließkommazahlen, Wahrheitswerte und Zeichen. Der Typ bestimmt auch die zulässigen Operationen, denn Wahrheitswerte lassen sich nicht addieren, Ganzzahlen schon. Dagegen lassen sich Fließkommazahlen addieren, aber nicht XOR-verknüpfen. Da jede Variable einen vom Programmierer vorgegebenen festen Datentyp hat, der zur Übersetzungszeit bekannt ist und sich später nicht mehr ändern lässt, und da Java stark darauf achtet, welche Operationen erlaubt sind, und auch von jedem Ausdruck spätestens zur Laufzeit den Typ kennt, ist Java eine statisch typisierte und streng (stark) typisierte Programmiersprache.[ 60 ](Während in der Literatur bei den Begriffen statisch typisiert und dynamisch typisiert mehr oder weniger Einigkeit herrscht, haben verschiedene Autoren unterschiedliche Vorstellungen von den Begriffen streng (stark) typisiert und schwach typisiert. )
[»] Hinweis
In Java muss der Datentyp einer Variablen zur Übersetzungszeit bekannt sein. Das nennt sich dann statisch typisiert. Das Gegenteil ist eine dynamische Typisierung, wie sie etwa JavaScript verwendet. Hier kann sich der Typ einer Variablen zur Laufzeit ändern, je nachdem, was die Variable enthält.
Primitiv- oder Verweistyp
Die Datentypen in Java zerfallen in zwei Kategorien:
Primitive Typen: Die primitiven (einfachen) Typen sind fest in der Sprache Java eingebaute Datentypen für Zahlen, Unicode-Zeichen und Wahrheitswerte.
Referenztypen: Mit diesem Datentyp lassen sich Objektverweise etwa auf Zeichenketten, Datenstrukturen oder Zwergpinscher verwalten.
Für diese Teilung entschied sich Sun seinerzeit aus einem einfachen Grund: Java wurde als Programmiersprache entworfen, die kleine, schwache Geräte unterstützen sollte, und auf denen musste die Java-Software, die am Anfang noch interpretiert wurde, so schnell wie möglich laufen. Unterscheidet der Compiler zwischen primitiven Typen und Referenztypen, so kann er relativ leicht Bytecode erzeugen, der ebenfalls zwischen den beiden Typen unterscheidet. Damit kann die Laufzeitumgebung auch den Programmcode viel schneller ausführen, und das mit einem relativ einfachen Compiler. Das war in der Anfangszeit ein wichtiges Kriterium, ist heute aber nicht mehr wichtig, weil die Laufzeitumgebung diverse Optimierungen vornimmt.
[»] Sprachvergleich mit Smalltalk und .NET *
In Smalltalk ist alles ein Objekt, auch die eingebauten Sprachdatentypen. Für Zahlen gibt es einen Basistyp Number sowie Integer, Float, Fraction als Untertypen. Immer noch gibt es arithmetische Operatoren (+, -, *, /, //, \\, um sie alle aufzuzählen), aber das sind nur Methoden der Klasse Number.[ 61 ](Die Dokumentation für das GNU Smalltalk zeigt auf: http://www.gnu.org/software/smalltalk/manual-base/html_node/Number_002darithmetic.html#Number_002darithmetic. ) Für Java-Entwickler sind Methodennamen wie + oder - ungewöhnlich, doch in Smalltalk sind sie das nicht. Syntaktisch unterscheidet sich ein 1 + 2 in Java und Smalltalk nicht, nur in Smalltalk ist die Addition ein Nachrichtenaufruf an das Integer-Objekt 1 und an die Methode + mit dem Argument 2, das wiederum ein Integer-Objekt ist – die Objekte baut der Compiler selbstständig aus den Literalen auf. Eine Klasse Integer für Ganzzahlen besitzt weitere Methoden wie asCharacter und floor.[ 62 ](http://www.gnu.org/software/smalltalk/manual-base/html_node/Integer.html) Es ist wichtig zu verstehen, dass dies nur das semantische Modell auf der Sprachseite ist; das hat nichts damit zu tun, wie später die Laufzeitumgebung diese speziellen Nachrichtenaufrufe optimiert. Durch die Einteilung von Java in primitive Datentypen und Referenztypen haben die Sprachschöpfer einen Bruch des objektorientierten Prinzips in Kauf genommen, um die interpretierte Laufzeit Anfang der 1990er zu optimieren – eine Optimierung, die aus heutiger Sicht unnötig war.
In .NET ist es eher wie in Java: Der Compiler kennt die eingebauten Datentypen und lässt ihnen eine Sonderbehandlung zukommen; es sind keine Methodenaufrufe. Auch im Bytecode (Common Intermediate Language, kurz CIL in .NET genannt) finden sich Anweisungen wie Addition und Subtraktion wieder. Doch es gibt noch einen Unterschied zu Java: Der Compiler bildet Datentypen der .NET-Sprachen auf .NET-Klassen ab, und diese Klassen haben Methoden. In C# ist der eingebaute Datentyp float mit dem Datentyp Single (aus dem .NET-Paket System) identisch, und es ist egal, ob Entwickler float f oder Single f schreiben. Doch Single (respektive float) hat im Vergleich zu Smalltalk keine mathematischen Operationen, aber dennoch ein paar wenige Methoden wie ToString().[ 63 ](https://msdn.microsoft.com/en-us/library/system.int32) In .NET verhalten sich folglich die eingebauten Datentypen wie Objekte: Sie haben Methoden, haben aber die gleiche Wertsemantik (zum Beispiel bei Methodenaufrufen) wie in Java und sehen auch im Bytecode ähnlich aus, was ihnen die gleiche gute Performance verleiht.
Wir werden uns im Folgenden erst mit primitiven Datentypen beschäftigen. Referenzen werden nur dann eingesetzt, wenn Objekte ins Spiel kommen. Diese nehmen wir uns in Kapitel 3, »Klassen und Objekte«, vor.
2.3.1 Primitive Datentypen im Überblick
In Java gibt es zwei Arten eingebauter primitiver Datentypen:
arithmetische Typen (ganze Zahlen – auch integrale Typen genannt –, Fließkommazahlen, Unicode-Zeichen)
Wahrheitswerte für die Zustände wahr und falsch
Tabelle 2.5 vermittelt dazu einen Überblick. Anschließend betrachten wir jeden Datentyp präziser.
Belegung (Wertebereich) | |
---|---|
16-Bit-Unicode-Zeichen (0x0000 … 0xFFFF) | |
–27 bis 27 – 1 (–128 … 127) | |
–215 bis 215 – 1 (–32.768 … 32.767) | |
–231 bis 231 – 1 (–2.147.483.648 … 2.147.483.647) | |
–263 bis 263 – 1 | |
1,40239846E–45f … 3,40282347E+38f | |
4,94065645841246544E–324 … 1,79769131486231570E+308 |
Bei den Ganzzahlen fällt auf, dass es eine positive Zahl »weniger« gibt als negative. Das liegt an der Kodierung im Zweierkomplement.
Für float und double ist das Vorzeichen nicht angegeben, da die kleinsten und größten darstellbaren Zahlen sowohl positiv als auch negativ sein können. Mit anderen Worten: Die Wertebereiche unterscheiden sich nicht – anders als etwa bei int – in Abhängigkeit vom Vorzeichen.[ 64 ](Es gibt bei Fließkommazahlen noch »Sonderzahlen«, wie plus oder minus unendlich, aber dazu in Kapitel 22, »Testen mit JUnit« später mehr. )
[»] Detailwissen
Genau genommen sieht die Sprachgrammatik von Java keine negativen Zahlenliterale vor. Bei einer Zahl wie -1.2 oder -1 ist das Minus der unäre Operator und gehört nicht zur Zahl. Im Bytecode selbst sind die negativen Zahlen wieder abgebildet.
Tabelle 2.6 zeigt eine etwas andere Darstellung.
Typ | Größe | Format |
---|---|---|
Ganzzahlen | ||
byte | 8 Bit | Zweierkomplement |
short | 16 Bit | Zweierkomplement |
int | 32 Bit | Zweierkomplement |
long | 64 Bit | Zweierkomplement |
Fließkommazahlen | ||
float | 32 Bit | IEEE 754 |
double | 64 Bit | IEEE 754 |
Weitere Datentypen | ||
boolean | 1 Bit | true, false |
char | 16 Bit | 16-Bit-Unicode |
[»] Hinweis
Strings werden bevorzugt behandelt, sind aber lediglich Verweise auf Objekte und kein primitiver Datentyp.
Zwei wesentliche Punkte zeichnen die primitiven Datentypen aus:
Alle Datentypen haben eine festgesetzte Länge, die sich unter keinen Umständen ändert. Der Nachteil, dass sich bei einigen Hochsprachen die Länge eines Datentyps ändern kann, besteht in Java nicht. In den Sprachen C(++) bleibt dies immer unsicher, und die Umstellung auf 64-Bit-Maschinen bringt viele Probleme mit sich. Der Datentyp char ist 16 Bit lang.
Die numerischen Datentypen byte, short, int und long sind vorzeichenbehaftet, Fließkommazahlen sowieso. Dies ist leider nicht immer praktisch, aber wir müssen stets daran denken. Probleme gibt es, wenn wir einem Byte zum Beispiel den Wert 240 zuweisen wollen, denn 240 liegt außerhalb des Wertebereichs, der von –128 bis 127 reicht. Ein char ist im Prinzip ein vorzeichenloser Ganzzahltyp.
Wenn wir die numerischen Datentypen (lassen wir hier char außen vor) nach ihrer Größe sortieren wollten, könnten wir zwei Linien für Ganzzahlen und Fließkommazahlen aufbauen:
byte < short < int < long
float < double
[»] Hinweis
Die Klassen Byte, Integer, Long, Short, Character, Double und Float deklarieren die Konstanten MAX_VALUE und MIN_VALUE, die den größten und kleinsten zulässigen Wert des jeweiligen Wertebereichs bzw. die Grenzen der Wertebereiche der jeweiligen Datentypen angeben:
System.out.println( Byte.MIN_VALUE ); // -128
System.out.println( Byte.MAX_VALUE ); // 127
System.out.println( Character.MIN_VALUE ); // '\u0000'
System.out.println( Character.MAX_VALUE ); // '\uFFFF'
System.out.println( Double.MIN_VALUE ); // 4.9E-324
System.out.println( Double.MAX_VALUE ); // 1.7976931348623157E308
Es gibt für jeden primitiven Datentyp eine eigene Klasse mit Hilfsmethoden rund um diesen Datentyp. Mehr zu diesen besonderen Klassen folgt in Kapitel 10, »Besondere Typen der Java SE«.
2.3.2 Variablendeklarationen
Mit Variablen lassen sich Daten speichern, die vom Programm gelesen und geschrieben werden können. Um Variablen zu nutzen, müssen sie deklariert (definiert[ 65 ](In C(++) bedeuten »Definition« und »Deklaration« etwas Verschiedenes. In Java kennen wir diesen Unterschied nicht und betrachten daher beide Begriffe als gleichwertig. Die Spezifikation spricht nur von Deklarationen. )) werden. Die Schreibweise einer Variablendeklaration ist immer die gleiche: Hinter dem Typnamen folgt der Name der Variablen. Die Deklaration ist eine Anweisung und wird daher mit einem Semikolon abgeschlossen. In Java kennt der Compiler von jeder Variablen und jedem Ausdruck genau den Typ.
Deklarieren wir ein paar (lokale) Variablen in der main(…)-Methode:
public class FirstVariable {
public static void main( String[] args ) {
String name; // Name
int age; // Alter
double income; // Einkommen
char gender; // Geschlecht ('f' oder 'm')
boolean isPresident; // Ist Präsident (true oder false)
boolean isVegetarian; // Ist die Person Vegetarier?
}
}
Links steht entweder ein primitiver Typ (wie int) oder ein Referenztyp (wie String). Viel schwieriger ist eine Deklaration nicht – kryptische Angaben wie in C gibt es in Java nicht.[ 66 ](Das ist natürlich eine Anspielung auf C, in dem Deklarationen wie char (*(*a[2])())[2] möglich sind. Gut, dass es mit cdecl ein Programm zum »Vorlesen« solcher Definitionen gibt. ) Ein Variablenname (der dann Bezeichner ist) kann alle Buchstaben und Ziffern des Unicode-Zeichensatzes beinhalten, mit der Ausnahme, dass am Anfang des Bezeichners keine Ziffer stehen darf. Auch darf der Bezeichnername mit keinem reservierten Schlüsselwort identisch sein.
Mehrere Variablen kompakt deklarieren
Im vorangehenden Listing sind zwei Variablen vom gleichen Typ: isPresident und isVegetarian.
boolean isPresident;
boolean isVegetarian;
Immer dann, wenn der Variablentyp der gleiche ist, lässt sich die Deklaration verkürzen – Variablen werden mit Komma getrennt:
boolean isPresident, isVegetarian;
Variablendeklaration mit Wertinitialisierung
Gleich bei der Deklaration lassen sich Variablen mit einem Anfangswert initialisieren. Hinter einem Gleichheitszeichen steht der Wert, der oft ein Literal ist. Ein Beispielprogramm:
public class Obama {
public static void main( String[] args ) {
String name = "Barack Hussein Obama II";
int age = 48;
double income = 400000;
char gender = 'm';
boolean isPresident = false;
}
}
Wir haben gesehen, dass bei der Deklaration mehrerer Variablen gleichen Typs ein Komma die Bezeichner trennt. Das überträgt sich auch auf die Initialisierung. Ein Beispiel:
boolean sendSms = true,
bungaBungaParty = true;
String person1 = "Silvio",
person2 = "Ruby the Heart Stealer";
double x, y,
bodyHeight = 165 /* cm */;
Die Zeilen deklarieren mehrere Variablen auf einen Schlag. x und y am Schluss bleiben uninitialisiert.
Zinsen berechnen als Beispiel zur Variablendeklaration, -initialisierung und -ausgabe
Zusammen mit der Konsolenausgabe können wir schon einen einfachen Zinsrechner programmieren. Er soll uns ausgeben, wie hoch die Zinsen für ein gegebenes Kapital bei einem gegebenen Zinssatz (engl. interest rate) nach einem Jahr sind.
public class InterestRates {
public static void main( String[] args ) {
double capital = 20000 /* Euro */;
double interestRate = 3.6 /* Prozent */;
double totalInterestRate = capital * interestRate / 100; // Jahr 1
System.out.print( "Zinsen: " );
System.out.println( totalInterestRate ); // 720.0
}
}
[+] Tipp
Strings können mit einem Plus aneinandergehängt werden. Ist ein Segment kein String, so wird es in einen String konvertiert und dann angehängt.
System.out.println( "Zinsen: " + totalInterestRate ); // Zinsen: 720.0
Mehr Beispiele dazu folgen in Abschnitt 2.4.11, »Überladenes Plus für Strings«.
2.3.3 Automatisches Feststellen der Typen mit var
Java 10 hat die Erweiterung gebracht, dass der Variablentyp bei gewissen Deklarationen entfallen kann und wir einfach stattdessen var nutzen können:
var name = "Barack Hussein Obama II";
var age = 48;
var income = 400000;
var gender = 'm';
var isPresident = false;
Wir sehen, dass im Gegensatz zu unserem vorherigen Beispiel nicht mehr die Variablentypen wie String oder int bei der Variablendeklaration explizit im Code stehen, sondern nur noch var. Das heißt allerdings nicht, dass der Compiler die Typen offenlässt! Der Compiler braucht zwingend die rechte Seite neben dem Gleichheitszeichen, um den Typ feststellen zu können. Das nennt sich Local-Variable Type Inference. Daher gibt es in unserem Programm auch eine Unstimmigkeit, nämlich bei var income = 400000, die gut ein Problem mit var aufzeigt: Die Variable ist kein double mehr wie vorher, sondern 400000 ist ein Ganzzahl-Literal, weshalb der Java-Compiler der Variablen income den Typ int gibt.
Die Nutzung von var soll Entwicklern helfen, Code kürzer zu schreiben, insbesondere wenn der Variablenname schon eindeutig auf den Typ hinweist. Finden wir eine Variable text vor, ist der Typ String naheliegend, genauso wie age ein int ist oder ein Präfix wie is oder has auf eine boolean-Variable hinweist. Aber wenn var auf die Kosten der Verständlichkeit geht, darf die Abkürzung nicht eingesetzt werden. Auch der Java-Compiler gibt Schranken vor:
var ist nur dann möglich, wenn eine Initialisierung einen Typ vorgibt. Eine Deklaration der Art var age; ohne Initialisierung ist nicht möglich und führt zu einem Compilerfehler.
var kann nur bei lokalen Variablen eingesetzt werden, wo der Bereich überschaubar ist. Es gibt aber noch viele weitere Stellen, wo in Java Variablen deklariert werden – dort ist var nicht möglich.
[»] Sprachvergleich
Java ist mit var relativ spät dran.[ 67 ](http://openjdk.java.net/jeps/286) Andere statisch typisierte Sprachen bieten die Möglichkeit schon länger, etwa C++ mit auto oder C# auch mit var. Auch JavaScript nutzt var, allerdings in einem völlig anderen Kontext: In JavaScript sind Variablen erst zur Laufzeit typisiert, und alle Operationen werden erst zur Ausführungszeit geprüft, während Java die Typsicherheit mit var nicht aufgibt.
2.3.4 Finale Variablen und der Modifizierer final
Variablen können mit dem Modifizierer final deklariert werden, sodass genau eine Zuweisung möglich ist. Dieses zusätzliche Schlüsselwort verbietet folglich eine weitere Zuweisung an diese Variable, sodass sie nicht mehr verändert werden kann. Ein üblicher Anwendungsfall sind Konstanten:
int width = 40, height = 12;
final int area = width * height;
final int perimeter;
final var random = Math.random() * 100;
perimeter = width * 2 + height * 2;
area = 200; // Compilerfehler
perimeter = 100; // Compilerfehler
Im Fall einer versuchten zweiten Zuweisung meldet der Compiler von Eclipse: »The final local variable … cannot be assigned. It must be blank and not using a compound assignment.«; IntelliJ meldet über den Java-Compiler »cannot assign a value to final variable …«.
Java erlaubt bei finalen Werten eine aufgeschobene Initialisierung. Das heißt, dass nicht zwingend zum Zeitpunkt der Variablendeklaration ein Wert zugewiesen werden muss. Das sehen wir im Beispiel an der Variablen perimeter.
Werden Variablen deklariert und initialisiert, können final und var zusammen eingesetzt werden. Einige Programmiersprachen bieten hier ein eigenes Schlüsselwort, z. B. val, Java jedoch nicht.
[»] Ausblick
Auch Objektvariablen und Klassenvariablen können final sein. Allerdings müssen die Variablen dann entweder bei der Deklaration belegt werden oder in einer aufgeschobenen Initialisierung im Konstruktor. Wir werden uns dies in Kapitel 6, »Eigene Klassen schreiben«, noch einmal genauer ansehen. Werden finale Variablen vererbt, so können Unterklassen diesen Wert auch nicht mehr überschreiben (das wäre ein Problem, aber vielleicht auch ein Vorteil für manche Konstanten).
Das Schlüsselwort final hat noch zusätzliche Bedeutungen im Zusammenhang mit Vererbung. Das werden wir uns ebenfalls später anschauen.
2.3.5 Konsoleneingaben
Bisher haben wir Methoden zur Ausgabe kennengelernt und random(). Die println(…)-Methoden »hängen« am System.out- bzw. System.err-Objekt, und random() »hängt« am Math-Objekt.
Der Gegenpol zu printXXX(…) ist eine Konsoleneingabe. Hier gibt es unterschiedliche Varianten. Die einfachste ist die mit der Klasse java.util.Scanner. In Abschnitt 5.10.2, »Yes we can, yes we scan – die Klasse Scanner«, wird die Klasse noch viel genauer untersucht. Es reicht aber an dieser Stelle zu wissen, wie Strings, Ganzzahlen und Fließkommazahlen eingelesen werden.
Eingabe lesen vom Typ | Anweisung |
---|---|
String | String s = new java.util.Scanner(System.in).nextLine(); |
int | int i = new java.util.Scanner(System.in).nextInt(); |
double | double d = new java.util.Scanner(System.in).nextDouble(); |
Verbinden wir die drei Möglichkeiten zu einem Beispiel. Zunächst soll der Name eingelesen werden, dann das Alter und anschließend eine Fließkommazahl:
public class SmallConversation {
public static void main( String[] args ) {
System.out.println( "Moin! Wie heißt denn du?" );
String name = new java.util.Scanner( System.in ).nextLine();
System.out.printf( "Hallo %s. Wie alt bist du?%n", name );
int age = new java.util.Scanner( System.in ).nextInt();
System.out.printf( "Aha, %s Jahre, das ist ja die Hälfte von %s.%n",
age, age * 2 );
System.out.println( "Sag mal, was ist deine Lieblingsfließkommazahl?" );
double value = new java.util.Scanner( System.in ).nextDouble();
System.out.printf( "%s? Aha, meine ist %s.%n",
value, Math.random() * 100000 );
}
}
Eine Konversation sieht somit etwa so aus:
Moin! Wie heißt denn du?
Christian
Hallo Christian. Wie alt bist du?
37
Aha, 37 Jahre, das ist ja die Hälfte von 74.
Sag mal, was ist deine Lieblingsfließkommazahl?
9,7
9.7? Aha, meine ist 60769.81705995359.
Die Eingabe der Fließkommazahl muss mit Komma erfolgen, wenn die JVM auf einem deutschsprachigen Betriebssystem läuft. Die Ausgabe über printf(…) kann ebenfalls lokalisierte Fließkommazahlen schreiben, dann muss jedoch statt des Platzhalters %s die Kennung %f oder %g verwendet werden. Das wollen wir in einem zweiten Beispiel nutzen.
Zinsberechnung mit der Benutzereingabe
Die Zinsberechnung, die vorher feste Werte im Programm hatte, soll eine Benutzereingabe bekommen. Des Weiteren erwarten wir die Dauer in Monaten statt in Jahren. Zinseszinsen berücksichtigt das Programm nicht.
public class MyInterestRates {
public static void main( String[] args ) {
System.out.println( "Kapital?" );
double capital = new java.util.Scanner( System.in ).nextDouble();
System.out.println( "Zinssatz?" );
double interestRate = new java.util.Scanner( System.in ).nextDouble();
System.out.println( "Anlagedauer in Monaten?" );
int month = new java.util.Scanner( System.in ).nextInt();
double totalInterestRate = capital * interestRate * month / (12*100);
System.out.printf( "Zinsen: %g%n", totalInterestRate );
}
}
Die vorher fest verdrahteten Werte sind nun alle dynamisch:
Kapital?
20000
Zinssatz?
3,6
Anlagedauer in Monaten?
24
Zinsen: 1440,00
Um den Zinseszins berücksichtigen zu können, muss eine Potenz mit in die Formel gebracht werden. Die nötige Methode dazu ist Math.pow(a, b), was a hoch b berechnet. Finanzmathematikern ist das als Übung überlassen.
[+] Dialogeingabe
Soll die Eingabe nicht von der Konsole kommen, sondern von einem eigenen Dialog, hilft eine Klasse aus dem Swing-Paket:
String input = javax.swing.JOptionPane.showInputDialog( "Eingabe" );
2.3.6 Fließkommazahlen mit den Datentypen float und double
Für Fließkommazahlen (auch Gleitkommazahlen genannt) einfacher und erhöhter Genauigkeit bietet Java die Datentypen float und double. Die Datentypen sind im IEEE-754-Standard beschrieben und haben eine Länge von 4 Byte für float und 8 Byte für double. Fließkommaliterale können einen Vorkommateil und einen Nachkommateil besitzen, die durch einen Dezimalpunkt (kein Komma) getrennt sind. Ein Fließkommaliteral muss keine Vor- oder Nachkommastellen besitzen, sodass auch Folgendes gültig ist:
double d = 10.0 + 20. + .11;
Nur den Punkt allein zu nutzen ist natürlich Unsinn, wobei .0 schon erlaubt ist.
[»] Hinweis
Der Datentyp float ist mit 4 Byte, also 32 Bit, ein schlechter Scherz. Der Datentyp double geht mit 64 Bit ja gerade noch. Die IA32-, x86-64- und Itanium-Prozessoren unterstützen mit 80 Bit einen »double extended«-Modus und damit bessere Präzision.
[»] Hinweis
Der Compiler meldet keinen Fehler, wenn eine Fließkommazahl nicht präzise dargestellt werden kann. Es ist kein Fehler, Folgendes zu schreiben:
double pi = 3.141592653589793238462643383279502884197169399375105820974944592;
Der Datentyp float *
Standardmäßig sind die Fließkommaliterale vom Typ double. Ein nachgestelltes f (oder F) zeigt dem Compiler an, dass es sich um ein float handelt.
[zB] Beispiel
Gültige Zuweisungen für Fließkommazahlen vom Typ double und float sehen so aus:
double pi = 3.1415, delta = .001;
float ratio = 4.33F;
Auch für den Datentyp double lässt sich ein d (oder D) nachstellen, was allerdings nicht nötig ist, wenn Literale für Kommazahlen im Quellcode stehen; Zahlen wie 3.1415 sind automatisch vom Typ double. Während jedoch bei 1 + 2 + 4.0 erst 1 und 2 als int addiert werden, dann das Ereignis in double konvertiert wird und anschließend 4.0 addiert wird, würde 1D + 2 + 4.0 gleich mit der Fließkommazahl 1 beginnen. So ist auch 1D gleich 1. bzw. 1.0.[ 68 ](Ein Literal wie 1D macht deutlich, warum Bezeichner nichts mit einer Ziffer anfangen können: Wenn eine Variablendeklaration wie double 1D = 2; erlaubt wäre, dann wüsste der Compiler bei println(1D) ja gar nicht, ob 1D für das Literal steht oder für die Variable. )
[»] Frage
Was ist das Ergebnis der Ausgabe?
System.out.println( 20000000000F == 20000000000F+1 );
System.out.println( 20000000000D == 20000000000D+1 );
Tipp: Was sind die Wertebereiche von float und double?[ 69 ](Prüfe zum Verständnis die Ausgabe von: System.out.println( Float.toHexString( 20000000000F ) ); System.out.println( Float.toHexString( 20000000000F + 1F ) ); System.out.println( Double.toHexString( 20000000000D ) ); System.out.println( Double.toHexString( 20000000000D + 1D ) );)
Noch genauere Auflösung bei Fließkommazahlen *
Einen höher auflösenden bzw. präziseren Datentyp für Fließkommazahlen als double gibt es nicht. Die Standardbibliothek bietet für diese Aufgabe in java.math die Klasse BigDecimal an, die in Kapitel 21, »Bits und Bytes, Mathematisches und Geld«, näher beschrieben ist. Das ist sinnvoll für Daten, die eine sehr gute Genauigkeit aufweisen sollen, wie zum Beispiel Währungen.[ 70 ](Einige Programmiersprachen besitzen für Währungen eingebaute Datentypen, wie LotusScript mit Currency, das mit 8 Byte einen sehr großen und genauen Wertebereich abdeckt. Erstaunlicherweise gab es einmal in C# den Datentyp currency für ganzzahlige Währungen. )
[»] Sprachvergleich
In C# gibt es den Datentyp decimal, der mit 128 Bit (also 16 Byte) auch genügend Präzision bietet, um eine Zahl wie 0,000000000000000000000000001 auszudrücken.
2.3.7 Ganzzahlige Datentypen
Java stellt fünf ganzzahlige Datentypen zur Verfügung: byte, short, char, int und long. Die feste Länge von jeweils 1, 2, 2, 4 und 8 Byte ist eine wesentliche Eigenschaft von Java. Ganzzahlige Typen sind in Java immer vorzeichenbehaftet (mit der Ausnahme von char); einen Modifizierer unsigned wie in C(++) gibt es nicht.[ 71 ](In Java bilden long und short einen eigenen Datentyp. Sie dienen nicht wie in C(++) als Modifizierer. Eine Deklaration wie long int i ist also genauso falsch wie long long time_ago. ) Negative Zahlen werden durch Voranstellen eines Minuszeichens gebildet. Ein Pluszeichen für positive Zeichen ist möglich. int und long sind die bevorzugten Typen. byte kommt selten vor und short nur in wirklich sehr seltenen Fällen, etwa bei Arrays mit Bilddaten.
Ganzzahlen sind standardmäßig vom Typ int
Betrachten wir folgende Zeile, so ist auf den ersten Blick kein Fehler zu erkennen:
System.out.println( 123456789012345 ); //
Dennoch übersetzt der Compiler die Zeile nicht, da er ein Ganzzahlliteral ohne explizite Größenangabe als 32 Bit langes int annimmt. Die obige Zeile führt daher zu einem Compilerfehler, da unsere Zahl nicht im gültigen int-Wertebereich von –2.147.483.648 … +2.147.483.647 liegt, sondern weit außerhalb: 2147483647 < 123456789012345. Java reserviert also nicht so viele Bits wie benötigt und wählt nicht automatisch den passenden Wertebereich.
Mit long zum mehrfachen Milliardär
Der Java-Compiler betrachtet jedes Ganzzahl-Literal automatisch als int. Sollte der Wertebereich von etwa plus/minus 2 Milliarden nicht reichen, greifen Entwickler zum nächsthöheren Datentyp, zum long. Dass eine Zahl long ist, muss ausdrücklich angegeben werden. Dazu wird an das Ende von Ganzzahlliteralen vom Typ long ein l oder L gesetzt. Um die Zahl 123456789012345 gültig ausgeben zu lassen, ist Folgendes zu schreiben:
System.out.println( 123456789012345L );
[+] Tipp
Das kleine »l« hat sehr viel Ähnlichkeit mit der Ziffer Eins. Daher sollte bei Längenangaben immer ein großes »L« eingefügt werden.
Der Datentyp byte
Ein byte ist ein Datentyp mit einem Wertebereich von –128 bis +127. Eine Initialisierung wie
byte b = 200; //
ist also nicht erlaubt, da 200 > 127 ist. Somit fallen alle Zahlen von 128 bis 255 (hexadezimal 0x80 – 0xFF) raus. In der Datenverarbeitung ist das Java-byte, weil es ein Vorzeichen trägt, nur mittelprächtig brauchbar, da insbesondere in der Dateiverarbeitung Wertebereiche von 0 bis 255 erwünscht sind.
Java erlaubt zwar keine vorzeichenlosen Ganzzahlen, aber mit einer expliziten Typumwandlung lassen sich doch Zahlen wie 200 in einem byte speichern:
byte b = (byte) 200;
Der Java-Compiler nimmt dazu einfach die Bitbelegung von 200 (0b00000000_00000000_00000000_11001000), schneidet bei der Typumwandlung die oberen drei Byte ab und interpretiert das oberste dann gesetzte Bit als Vorzeichen-Bit. Bei der Ausgabe fällt das auf:
byte b = (byte) 200;
System.out.println( b ); // -56
Mehr zur Typumwandlung folgt in Abschnitt 2.4.10, »Die Typumwandlung (das Casting)«.
Der Datentyp short *
Der Datentyp short ist selten anzutreffen. Mit seinen 2 Byte kann er einen Wertebereich von –32.768 bis +32.767 darstellen. Das Vorzeichen »kostet« wie bei den anderen Ganzzahlen 1 Bit, sodass nicht 16 Bit, sondern nur 15 Bit für Zahlen zu Verfügung stehen. Allerdings gilt wie beim byte, dass auch ein short ohne Vorzeichen auf zwei Arten initialisiert werden kann:
short s = (short) 33000;
System.out.println( s ); // -32536
2.3.8 Wahrheitswerte
Der Datentyp boolean beschreibt einen Wahrheitswert, der entweder true oder false ist. Die Zeichenketten true und false sind reservierte Wörter und bilden neben konstanten Strings und primitiven Datentypen Literale. Kein anderer Wert ist für Wahrheitswerte möglich, insbesondere werden numerische Werte nicht als Wahrheitswerte interpretiert.
Der boolesche Typ wird beispielsweise bei Bedingungen, Verzweigungen oder Schleifen benötigt. In der Regel ergibt sich ein Wahrheitswert aus Vergleichen.
2.3.9 Unterstriche in Zahlen
Um eine Anzahl von Millisekunden in Tage zu konvertieren, muss einfach eine Division vorgenommen werden. Um Millisekunden in Sekunden umzurechnen, brauchen wir eine Division durch 1.000, von Sekunden auf Minuten eine Division durch 60, von Minuten auf Stunden eine Division durch 60, und die Stunden auf Tage bringt die letzte Division durch 24. Schreiben wir das auf:
long millis = 10 * 24 * 60 * 60 * 1000L;
long days = millis / 86400000L;
System.out.println( days ); // 10
Eine Sache fällt bei der Zahl 86.400.000 auf: Besonders gut lesbar ist sie nicht. Die eine Lösung ist, es erst gar nicht zu so einer Zahl kommen zu lassen und sie wie in der ersten Zeile durch eine Reihe von Multiplikationen aufzubauen – mehr Laufzeit kostet das nicht, da dieser konstante Ausdruck zur Übersetzungszeit feststeht.
Die zweite Variante macht durch Unterstriche Zahlen besser lesbar, denn der Unterstrich gliedert die Zahl in Blöcke. Anstatt ein numerisches Literal als 86.400.000 zu schreiben, ist auch Folgendes erlaubt:
long millis = 10 * 86_400_000L;
long days = millis / 86_400_000L;
System.out.println( days ); // 10
Die Unterstriche machen die 1.000er-Blöcke gut sichtbar.[ 72 ](Bei Umrechnungen zwischen Stunden, Minuten usw. hilft auch die Klasse TimeUnit mit einigen statischen toXXX()-Methoden. )
[zB] Beispiel
Hilfreich ist die Schreibweise auch bei Literalen in Binär- und Hexadezimaldarstellung, da die Unterstriche hier ebenfalls Blöcke absetzen können:
int i = 0b01101001_01001101_11100101_01011110;
long l = 0x7fff_ffff_ffff_ffffL;
Mit 0b beginnt ein Literal in Binärschreibweise, mit 0x in Hexadezimalschreibweise (weitere Details folgen in Kapitel 21, »Bits und Bytes, Mathematisches und Geld«).
Der Unterstrich darf in jedem Literal stehen, zwei aufeinanderfolgende Unterstriche sind aber nicht erlaubt, und er darf nicht am Anfang stehen.
[»] Hinweis
Die Unterstriche in Literalen sind nur eine Hilfe wie Leerzeichen zur Einrückung. Im Bytecode ist davon nichts mehr zu lesen. In der Klassendatei sehen 0b01101001_01001101_11100101_01011110 und 0b01101001010011011110010101011110 identisch aus, insbesondere weil sie sowieso als Ganzzahl 1766712670 abgelegt sind.
2.3.10 Alphanumerische Zeichen
Der alphanumerische Datentyp char (von engl. character, Zeichen) ist 2 Byte groß und nimmt ein Unicode-Zeichen auf. Ein char ist nicht vorzeichenbehaftet. Die Literale für Zeichen werden in einfache Hochkommata gesetzt. Spracheinsteiger verwechseln häufig die einfachen Hochkommata mit den Anführungszeichen der Zeichenketten (Strings). Die einfache Merkregel lautet: ein Zeichen – ein Hochkomma; mehrere Zeichen – zwei Hochkommata (Gänsefüßchen).
[zB] Beispiel
Korrekte Hochkommata für Zeichen und Zeichenketten:
char c = 'a';
String s = "Heut' schon gebeckert?";
Da der Compiler ein char automatisch in ein int konvertieren kann, ist auch int c = 'a'; gültig.
2.3.11 Gute Namen, schlechte Namen
Für die optimale Lesbarkeit und Verständlichkeit eines Programmcodes sollten Entwickler beim Schreiben einige Punkte berücksichtigen:
Ein konsistentes Namensschema ist wichtig. Heißt ein Zähler no, nr, cnr oder counter? Auch sollten wir korrekt schreiben und auf Rechtschreibfehler achten, denn leicht wird aus necessaryConnection sonst nesesarryConnection. Variablen ähnlicher Schreibweise, etwa counter und counters, sind zu vermeiden.
Abstrakte Bezeichner sind ebenfalls zu vermeiden. Die Deklaration int TEN = 10; ist absurd. Eine unsinnige Idee ist auch die folgende: boolean FALSE = true, TRUE = false;. Im Programmcode würde dann mit FALSE und TRUE gearbeitet. Einer der obersten Plätze bei einem Wettbewerb für die verpfuschtesten Java-Programme wäre uns gewiss.
Unicode-Sequenzen können zwar in Bezeichnern aufgenommen werden, doch sollten sie vermieden werden. In double übelkübel, \u00FCbelk\u00FCbel; sind beide Bezeichnernamen gleich, und der Compiler meldet einen Fehler.
0 und O und 1 und l sind leicht zu verwechseln. Die Kombination »rn« ist schwer zu lesen und je nach Zeichensatz leicht mit »m« zu verwechseln.[ 73 ](Eine Software wie Mathematica warnt vor Variablen mit fast identischem Namen. ) Gültig – aber böse – ist auch: int ínt, ìnt, înt; boolean bôõleañ;
[»] Bemerkung
In China gibt es 90 Millionen Familien mit dem Nachnamen Li. Das wäre so, als ob wir jede Variable temp1, temp2 … nennen würden.
Ist ein Bezeichnername unglücklich gewählt (pneumonoultramicroscopicsilicovolcanoconiosis ist schon etwas lang), so lässt er sich problemlos konsistent umbenennen. Dazu wählen wir im Menü Refactor • Rename – oder auch kurz (Alt)+(ª)+(R); der Cursor muss auf dem Bezeichner stehen. Eine optionale Vorschau (engl. preview) zeigt an, welche Änderungen die Umbenennung nach sich ziehen wird. Neben Rename gibt es auch noch eine andere Möglichkeit: Dazu lässt sich auf der Variablen mit (Strg)+(1) ein Popup-Fenster mit Local Rename öffnen. Der Bezeichner wird selektiert und lässt sich ändern. Gleichzeitig ändern sich alle Bezüge auf die Variable mit.
2.3.12 Keine automatische Initialisierung von lokalen Variablen
Die Laufzeitumgebung – bzw. der Compiler – initialisiert lokale Variablen nicht automatisch mit einem Nullwert bzw. Wahrheitsvarianten nicht mit false. Vor dem Lesen müssen lokale Variablen von Hand initialisiert werden, andernfalls gibt der Compiler eine Fehlermeldung aus.[ 74 ](Anders ist das bei Objektvariablen (und statischen Variablen sowie Feldern). Sie sind standardmäßig mit null (Referenzen), 0 (bei Zahlen) oder false belegt. )
Im folgenden Beispiel seien die beiden lokalen Variablen age und adult nicht automatisch initialisiert, und so kommt es bei der versuchten Ausgabe von age zu einem Compilerfehler. Der Grund ist, dass ein Lesezugriff nötig ist, aber vorher noch kein Schreibzugriff stattfand.
int age;
boolean adult;
System.out.println( age ); // Local variable age may not
// have been initialized.
age = 18;
if ( age >= 18 ) // Fallunterscheidung: wenn-dann
adult = true;
System.out.println( adult ); // Local variable adult may not
// have been initialized.
Weil Zuweisungen in bedingten Anweisungen vielleicht nicht ausgeführt werden, meldet der Compiler auch bei System.out.println(adult) einen Fehler, da er analysiert, dass es einen Programmfluss ohne die Zuweisung gibt. Da adult nur nach der if-Abfrage auf den Wert true gesetzt wird, wäre nur unter der Bedingung, dass age größer gleich 18 ist, ein Schreibzugriff auf adult erfolgt und ein folgender Lesezugriff möglich. Doch da der Compiler annimmt, dass es andere Fälle geben kann, wäre ein Zugriff auf eine nicht initialisierte Variable ein Fehler.
Eclipse zeigt einen Hinweis und einen Verbesserungsvorschlag an, wenn eine lokale Variable nicht initialisiert ist.