Datentypen

Aus Programmieren-Wiki

Es gibt viele Klassifikationen von Programmiersprachen. Eine dieser Klassifizierungen ist die Unterscheidung zwischen typisierten und nicht typisierten Sprachen.

Java ist hierbei eine typisierte Sprache, was bedeutet, dass alle Variablen erst Deklariert werden müssen, bevor sie verwendet werden können. Das heißt insbesondere, dass deren Name und Typ festgelegt werden muss. Im folgenden eine Übersicht über die primitiven Datentypen in Java:

primitive Datentypen
Name Art
boolean Wahrheitswert
byte 8-bit Ganzzahl
short 16-bit Ganzzahl
int 32-bit Ganzzahl
long 64-bit Ganzzahl
float 32-bit Gleitkommazahl nach IEEE-754 Standard
double 64-bit Gleitkommazahl nach IEEE-754 Standard
char 16-bit Unicode Zeichen

Dabei werden im Allgemeinen booleans für Wahrheitswerte, ints für Ganzzahlen und chars für einzelne Zeichen verwendet.

Ob jetzt float oder double standardmäßig für Gleitkommazahlen verwendet wird kommt stark auf den Bereich in dem es benutzt wird an. Die meißten Grafik-Pakete verwenden floats, weil sie schneller zu bereichnen sind wegen ihrer Größe. Doubles werden hingegen verwendet, wenn viele Berechnungen durch ein Program getätigt werden, da durch die größere Länge der Zahlen Rundungsfehler verhindert werden.

Für diese Veranstaltung ist es demnach quasi "egal" welcher der beiden Datentypen verwendet wird, der Unterschied sollte trotzdem bewusst sein.

In der Aufzählung der primitiven Datentypen fällt auf, dass Strings nicht erwähnt werden. Das liegt daran, dass Strings an sich eine Menge an chars (später in der Vorlesung Betriebssysteme auch ein Array) mit einem sogenannten Nullterminator am Ende sind. Durch die ausführliche Unterstützung von Strings in Java könnten wir allerdings fälschlicherweise davon ausgehen, dass Strings doch ein primitiver Datentyp sind.

Oracle greift dieses Thema auch in ihrer Dokumentation auf und verweist darauf, dass Strings sogenannte "Simple Data Objects" sind. Eine genauerer Einführung in Strings gibt es hier.

Im folgenden die Standardwerte und Werteintervalle der verschiedenen Datentypen im Überblick:

Werte der Datentypen
Datentyp Standardwert Niedrigster Wert Höchster Wert
boolean false false (logische 0) true (logische 1)
byte 0 -128 127
short 0 -32_768 32_767
int 0 -2^31 2^31 -1
long 0L -2^63 2^63 -1
float (*) 0.0f 1.18 * 10^-38 3.40* 10^ 38
double (*) 0.0d 2.23* 10^-308 1.80* 10^ 308
char '\u0000' '\u0000' (also 0) '\uffff' (oder 65,535)

* floats und doubles halten sich, wie oben bereits erwähnt, an den IEEE-754 Standard. In diesem Standard ist es nicht möglich alle Werte zwischen dem niedrigst-möglichen und höchst-möglichen darzustellen. (Zum Beispiel exakt 1/3)

Auch gibt es spezielle Werte wie NaN (Not a Number) oder Infinity (sowohl positiv, als auch negativ). Mehr praktische Einsicht gibt es hier.

Beispiele in Java:

boolean b = true;
int i = 1;
double d = 2.0;
char z = 'A';
String s = "Some string";
long number = 24551929492L;    // Wir benutzen hier den Großbuchstaben L statt l, da dieser sich besser von der 1 unterscheiden lässt.
float digit = 2.0f;

Wie verwenden wir byte, short und int richtig?

Die Unterteilung der Ganzzahlen in verschiedene Typen hat einen historischen Hintergrund: Speicherplatz.

Erste programmierbare Computer hatten einen sehr eingeschränkten Speicherplatz, weshalb der Typ passend, aber möglichst klein gewählt werden musste.

Inzwischen steht in so gut wie jedem Rechner mehr als genug Speicherplatz zur Verfügung.

Deshalb belegen bei Java Referenzimplementierungen von Oracle sowohl int, short als auch byte 32 bit.

Hierbei ist aber Vorsicht geboten:

Der Zahlenbereich ist nachwievor wie oben erläutert beschränkt. (Allerdings gibt es auch andere Implementierungen, in denen dies nicht der Fall ist). Es kann also entgegen der Erwartung zu Überläufen kommen.

Auch zu beachten ist, dass die Rückgabetypen aller arithmetischen Operationen vom Typ int sind.

Demnach ist es besser in Java immer den Typ int für Ganzzahlen zu verwenden.


Ein paar Beispiele was (nicht) geht:

byte b1 = 127, b2 = 1;

int i = b1 + b2; // = 128
byte b3 = (byte) (b1 + b2); // = -128 (Cast zu Byte nach dem Motto "Ich weiß was ich hier tue")
byte b3 = b1 + b2; // Erzeugt Fehler wegen fehlerhafter Konversion von int zu byte (also einem potenziellem Datenverlust)

Und was ist mit int und long?

Es sollte nachwievor generell erstmal ein Integer verwendet werden. Gibt es keine Garantie, dass die Werte innerhalb dessen Wertebereich bleibt, sollte der long-Datentyp verwendet werden.

Wann wird eine Wrapperklasse und wann der primitive Typ verwendet?

Referenztypen (oder auch "Wrapperklassen") haben einen entscheidenden Unterschied zu primitiven Typen. Diese (Referenztypen) haben eine sogenannte gemeinsame Wurzel, das heißt, alle Referenztypen sind Unterklassen von Object.

Primitive Typen hingegen haben keine solche gemeinsame Wurzel. Sie sind also alle verschieden.

Das heißt für uns, dass jede Variable die als Object deklariert wurde, jeden Wert eines beliebigen Referenztypen halten kann, nicht aber den, eines primitiven Typs.

Ein Beispiel in denen Wrapperklassen statt primitive Typen verwendet werden sollten (und warum das so getan werden sollte):

Wir wollen eine Datenstruktur erstellen, die allgemein genutzt werden kann. So zum Beispiel ein dynamisches Array. Wir würden dann "Object[]" verwenden um Elemente dort zu speichern. Dieses vorgehen würde auch funktionieren, für Referenztypen. Da primitive Typen allerdings keine gemeinsame Wurzel haben (und auch nicht gemeinsam mit Referenztypen), würden wir eine neue implementation für jeden neuen primitiven Datentyp benötigen.


Die Lösung: Wrapperklassen.

Statt jetzt 8 verschiedene Implementationen zu erstellen (7 für die primitiven Datentypen und 1 für Object), können die primitiven Werte in ein Objekt "gewrappt" werden, sodass diese auch die Implementation der Referenztypen verwenden können.

Zeichenketten (Strings) vs andere Typen

Wir können überall Zeichenketten zum speichern von Daten verwenden, da sie von ihrer Natur aus alle anderen primitiven Datentypen "emulieren" könnten.

"Nur weil es geht, heißt es nicht, dass es soll".

In den meißten Fällen ist es nämlich nicht Sinnvoll andere Datentypen durch einen String darzustellen.

String bool = "true";
if (bool.equals("true")) {
    bool = "false";
} else {
    bool = "true";
}

Dieser Code ist ein Beispiel für einen doch sehr offensichtlich falsch gewählten Datentyp. In der Regel ist die falsche Wahl des Datentyps mit mehr Aufwand und fehleranfälligem Code verbunden. Es ist also immer gut, verschiedene Implementationen durchzugehen und erstmal von Strings abzusehen.

Wann sind chars besser als Strings?

Wir haben schon gelernt, dass Strings kein primitiver Datentyp sind sondern ein Objekt, das mehrere chars zu einem String-Objekt "zusammenfügt".

Sollen nur einzelne Zeichen gespeichert werden, gibt es mehrere Vorteile char als Datentyp zu wählen:

  • geringerer Speicherverbrauch
  • Vergleich mithilfe von == statt .equals()
  • Arithmetik auf den Zeichen möglich (da sie als Zahlencode repräsentiert werden, a+5 = f)
  • chars haben immer genau ein Zeichen, Strings haben auch 0 oder mehr als 1 Zeichen, was den Code robuster machen kann

Warum kann ich nicht mehrere Werte durch Kommata trennen in einem String?

Die Verwendung von Trennzeichen in einem String kann leicht zu Problemen führen, wenn einer der zu speichernden Werte das Trennzeichen enthält. Außerdem können auf diese Weise nur Strings gespeichert werden. Der schwerwiegendste Punkt ist allerdings der unnötig, sehr erschwerte Zugriff der Daten und ggf. Konvertierung in eine nützliche Form.

Wie arbeite ich richtig mit Strings?

Wir haben uns angeschaut wann wir Strings nicht benutzen sollten, wenn wir diese aber verwenden, was sollen wir dabei beachten?:

Vergleiche

Wenn zwei Strings auf gleichen Inhalt geprüft werden sollen, muss beachtet werden, dass jeder String ein Objekt ist. Bei einem Vergleich mit == wird bei Referenztypen die Referenz, nicht aber der Wert verglichen. Das heißt, dass folgendes Szenario auftreten kann:

String a = "abc";
String b = "abc";
if (a == b) { // This usually results in false, eventhough the strings have the same value
    System.out.println("Equal!"); 
}

Beim Ausführen dieses Codes wird es kaum dazu kommen, dass die Strings als identisch evaluiert werden. Das liegt daran, dass sie unterschiedliche Referenzen haben.

Wenn wir deren Werte vergleichen wollen, müssen wir die String::equals(Object other) Methode verwenden. Diese vergleicht beide Strings Zeichen für Zeichen, statt deren Referenzen zu vergleichen.

Konkatenation von Strings

Strings in Java sind immutable. Das bedeutet, dass ihr inhalt nicht verändert werden kann. Stattdessen wird, wenn z.B.

String string = "abc";
string += "def";

ausgeführt wird ein komplett neuer String erstellt und in string gespeichert. Wenn ein String häufig manipuliert wird, z.B. wenn in einer Schleife andere Strings angehängt werden, ist ein StringBuilder oder StringJoiner oft besser geeignet. Der Unterschied zwischen den Beiden ist gering, aber sehr nützlich: Das Trennzeichen, dass der StringJoiner entgegennimmt wird, anders als beim StringBuilder, nicht hinter den zuletzt angefügten Teil gehängt. Das kann insbesondere dann hilfreich sein, wenn Strings in Schleifen gebaut werden, bei denen zwischen den Teil-Strings Zeilenumbrüche sind. Generell implementieren beide Klassen modifizierbare Zeichenketten, welche operationen zum Anhängen, Einfügen, Ersetzen und Löschen von Teil-Strings anbieten. Nachdem alle Änderungen erfolgt sind, kann mithilfe der .toString() Methode ein neuer String erstellt werden. So werden nur 2 Objekte, der StringBuilder (oder StringJoiner) und der fertige String erstellt, während ohne StringBuilder (oder Joiner) für jede Änderung ein neues String-Objekt benötigt wird.

Strings mit gleichem Format

Wenn Strings in ein festes Format zusammengefügt werden müssen, kann auch String.format() benutzt werden. Diese nennen wir auch Format-Strings. Wenn die Anzahl der anzuhängenden Strings zur Kompilierzeit nicht bekannt ist, ist dies allerdings nicht möglich.

https://docs.oracle.com/javase/specs/jls/se6/html/typesValues.html