Primitive Datentypen und Felder (Arrays)

Werbung
Primitive Datentypen und Felder (Arrays)
Primitive Datentypen
Java stellt (genau wie Haskell) primitive Datentypen für Boolesche Werte, Zeichen, ganze Zahlen und Gleitkommazahlen zur Verfügung. Der wichtigste Unterschied zu Haskell besteht darin, dass man in Java zwischen vier verschiedenen Typen für ganze Zahlen und zwei verschiedenen Typen für Gleitkommazahlen
wählen kann (und darüber hinaus muss man sich daran gewöhnen, dass die Typbezeichnungen mit Kleinbuchstaben beginnen). Diese verschiedenen Typen unterscheiden sich nach Größe des für sie reservierten
Speicherplatzes und folglich auch nach dem Bereich der darstellbaren Zahlen. Die folgende Tabelle zeigt
die vier Typen von ganzen Zahlen mit Vorzeichen (signed integers):
Typbezeichnung
byte
short
int
long
Speicherplatz Darstellungsbereich
8 Bit = 1 Byte
[−128, 127]
16 Bit = 2 Byte
[−32768, 32767]
32 Bit = 4 Byte
[−231 , 231 − 1]
64 Bit = 8 Byte
[−263 , 263 − 1]
Für Gleitkommazahlen gibt es neben dem bekannten Typ float mit 4 Byte Speicherplatz auch den Typ
double mit 8 Byte Speicherplatz. Dieser wird primär zur Verbesserung der Präzision der Darstellung und
nur sekundär zur Erweiterung des Darstellungsbereichs verwendet.
In der Reihenfolge byte, short, int, long kann der Wert einer Variable eines niederen Typs immer
einer Variable des höheren Typs zugewiesen werden. Gleiches gilt für float und double und für die Zuweisung von ganzzahligen Werten auf Gleitkomma–Variable. Da der umgekehrte Weg leicht zu Fehlern
führen kann, ist eine solche Zuweisung nur dann möglich, wenn diese Typumwandlung (type cast) explizit
gefordert wird. Dazu wird, wie das folgende Beispiel zeigt, der Typ, in den umgewandelt werden soll, in
Klammern vor den umzuwandelnden Ausdruck gestellt:
int i = 1000;
short k = (short)i;
Typumwandlungen, bei denen der umzuwandelnde Wert nicht im Darstellungsbereich des neuen Typs liegt,
führen zu Fehlern.
Für die sechs genannten numerischen Typen kann man die arithmetischen Operationen +,-,* und / verwenden, wobei die Operation / bei allen ganzzahligen Typen auch die ganzzahlige Division ausführt. Der
Rest bei der ganzzahligen Division wird mit der Operation % bestimmt. Der Inkrement–Operator ++ und
der Dekrement–Operator -- sind für alle numerische Typen anwendbar und bewirken die Addition bzw.
die Subtraktion von 1. Man kann beide in Präfix–Notation (vor dem Argument) und Suffix–Notation (nach
dem Argument) verwenden. Ein Unterschied macht sich dann bemerkbar, wenn der Operator in einer Wertzuweisung angewendet wird: Bei Präfix–Notation wird erst die Operation ausgeführt und dann der Wert
zugewiesen, bei Suffix–Notation erfolgt zuerst die Wertzuweisung und dann die Operation. Das folgende
Beispiel verdeutlicht diesen Unterschied:
int i = 20;
ink k = i++;
int l = --k;
// aktuelle Werte:
// aktuelle Werte:
k=20 und i=21
k=19 und l=19
Die Vergleichsoperationen ==,!=,<,<=,>,>= liefern auf allen numerischen Typen Boolesche Werte. Variable vom Typ boolean können nur die Werte true und false annehmen. Als Operationen auf Booleschen
Werten kann man die Negation ! (einstellig), die Konjunktion && sowie die Disjunktion || verwenden.
Der Typ char verfügt (wie in Haskell) über 16 Bit, mit denen alle Unicode–Zeichen dargestellt werden
können. Werte vom Typ char können ohne explizite Typumwandlung auf Variable der Typen int, long,
float, double zugewiesen werden, für die Gegenrichtung ist eine explizite Typumwandlung erforderlich.
Zu jedem primiten Datentyp ist eine sogenannte Wrapper–Klasse definiert. Mit zwei Ausnahmen (int,
char) tragen diese Klassen jeweils den gleichen Namen, aber mit Großbuchstaben am Anfang:
Byte, Short, Integer, Long, Float, Double, Boolean, Character
Wie ein kurzer Blick in daie Systembeschreibung API (Application Programming Interface, zu finden unter
-> http://java.sun.com/j2se/1.5.0/docs/api/) verrät, stellen die Wrapper–Klassen eine Reihe nützlicher Funktionen zur Verfügung. Darüber hinaus bieten sie aber auch die Möglichkeit, Zahlen oder Zeichen wie ein
Objekt (-> nächste Themen) zu behandeln.
Bei der Deklaration einer Variablen eines primitiven Typs wird (bei der Programmausführung) ein ensprechend großer Abschnitt im Speicher reserviert, der mit dem Namen der Variablen assoziiert ist. Wenn mit der
Deklaration noch keine Wertzuweisung erfolgt, wird der Speicherplatz mit einem sogenannten Default–Wert
belegt, nämlich 0 für alle ganzzahligen Typen, 0.0 für Gleitkommatypen, false für boolean und das durch
16 Nullen codierte Zeichen NUL für den Typ char. Bei einer Zuweisung der Form x = ausdruck; wird der
Wert von ausdruck auf den Speicherplatz von x kopiert. Variable eines primitiven Typs haben also immer
einen Wert und können deshalb auch als Werttypen bezeichnet werden. Im Gegensatz dazu sind alle anderen
Datentypen in Java sogenannte Referenztypen, d.h. ihr Name ist nicht mit einem konkreten Objekt dieses
Typs, sondern mit einer Referenz (Verweis) assoziiert, die auf solch einen Objekt oder aber auf null (ein
symbolischer Ausdruck für NICHTS) verweist. Mit einer Zuweisung wird in einem solchen Fall nicht das
Objekt kopiert, sondern nur die Referenz auf dieses Objekt. Referenzen sind mehr als nur ein einfacher Zeiger, aber man kann sich eine Referenz gut als einen Zeiger auf einen bestimmten Speicherinhalt vorstellen.
Das Prinzip kommt bereits bei einem Datentyp zum Tragen, der eine Zwitterstellung zwischen primitiven
Datentypen und Objekten einnimmt, dem sogenannten Feld (Array).
Felder
Ein Feld oder Array repräsentiert einen Folge von Daten gleichen Typs und belegt dabei einen zusammenhängenden Speicherabschnitt. Die Daten in einem Feld der Länge n sind von 0 bis n − 1 nummeriert. Es
gibt zwei Möglichkeiten, ein Array zu deklarieren, in der Vorlesung bevorzugen wir die Varainte
typename [ ] arrayname;
aber alternativ kann auch
typename arrayname [ ];
verwendet werden. Die Leerzeichen zwischen dem Namen und der öffnenden Klammer bzw. zwischen den
Klammern wurden nur zur besseren Lesbarkeit gesetzt, man kann auf beide verzichten. Mit einer solchen
Deklaration wird eine Referenz angelegt, die auf null, also auf nichts verweist. Wie bei primitiven Datentypen kann man die Deklaration auch mit einer Zuweisung verbinden. Dazu muss das zugewiesene Array
entweder schon deklariert sein, oder es muss im Speicher angelegt werden. Auch für das Neuanlegen gibt
es zwei Möglichkeiten, nämlich nur die Feldlänge anzugeben (und damit alle Speicherzellen mit Default–
Werten zu füllen) oder alle Daten, die im Array gespeichert werden sollen, direkt aufzulisten (womit die
Feldlänge implizit festgelegt wird). Das folgende Beispiel demonstriert diese Varainten:
int[] a1;
// a1 ist (Referenz auf) null
int[] a2 = new int[4]; /* a2 ist (Referenz auf) ein int-Array der Laenge 4, in
dem alle Eintraege den Default-Wert 0 haben */
int[] a3 = {1,2,3};
// a3 ist (Referenz auf) ein int-Array der Laenge 3
int[] a4 = a2;
// a4 ist (Referenz auf) auf gleiches Array wie a2
int[] a5 = a1;
// a5 ist (Referenz auf) null
Wie man sieht, wird bei einer Zuweisung nur die Referenz übertragen, es erfolgt keine Kopie des eigentlichen Feldes im Speicher.
Auf den i–ten Eintrag eines Arrays a kann man mit a[i] zugreifen, die Länge steht als Eigenschaft a.length
zur Verfügung. Wir illustrieren das an einer Fortsetzung des obigen Beispiels:
int i = a3[1];
a2[3] = 5;
int j = a4[3]
// i hat den Wert 2, denn die Nummerierung beginnt mit 0
// eine 0 wird mit 5 ueberschrieben
/* j hat den Wert 5 weil a2 und a4 auf das gleiche Array
verweisen */
Die letzte Zeile macht noch einmal deutlich, dass nach Zuweisung von Array–Variablen (wie in unserem
Beispiel a4=a2;) jede Änderung an dem durch die eine Variable referenzierten Objekt auch für die andere
Variable wirksam ist. Das ist ein fundamentaler Unterschied zu Variablen für primitiven Datentypen:
int n1 = 3;
int n2 = n1;
n1
= 5;
//
// beide haben den Wert 3
// n1 hat den Wert 5, aber n2 hat immer noch den Wert 3
Bei der Verwendung des Operators == auf Variable eines nichtprimitiven Typs muss man beachten, dass
die Referenzen auf Gleichheit getestet werden und es nicht darauf ankommt, ob die referenzierten Objekte
gleich sind oder nicht. Auch diesen Effekt kann man an einem einfachen Beispiel demonstrieren:
int[] A
int[] B
int[] C
boolean c
=
=
=
=
{2,3,4}
A;
{2,3,4}
(A == C);
boolean b = (A == B);
//
//
//
/*
ein erstes Array mit Eintraegen 1,2,3 wir angelegt
B ist Referenz auf das gleiche Array
ein zweites Array mit Eintraegen 1,2,3 wir angelegt
c ist false, denn die Referenzen verweisen auf zwei
verschiedene Speicherabschnitte */
// b ist true, beide Referenzen verweisen auf erstes Array
In der folgenden Grafik ist dargestellt, wie die Ausführung der ersten drei Zeilen des Codes im Speicher
realisiert wird.
Code
int[ ] A = {2,3,4};
Variable
A
A
Speicher
Referenz
3
4
2
3
4
2
3
4
2
3
4
Referenz
int[ ] B = A;
int[ ] C = {2,3,4};
2
B
Referenz
A
Referenz
B
Referenz
C
Referenz
Um eine wirkliche Kopie eines Arrays zu erzeugen, verwendet man die Funtion clone(). Aus Gründen, die
erst später klar werden, muss aber zusätzlich noch eine Typumwandlung erfolgen:
int[] A
int[] B
= {1,2,3}
// ein erstes Array mit Eintraegen 1,2,3 wird angelegt
= (int[]) A.clone();
/* ein zweites Array mit Eintraegen 1,2,3 wird
als Kopie des ersten Arrays angelegt */
boolean b = (A == B);
/* b ist false, Referenzen sind verschieden
Durch die Verwendung von mehreren Klammerpaaren können höherdimensionale Arrays, mit anderen Worten Felder von Feldern, angelegt werden. Das folgende Beispiel zeigt wieder die verschiedenen Möglichkeiten auf, solche Arrays zu deklarieren und zu definieren.
int[][] A;
int[][] B = new int[3][];
// Referenz auf null
/* Referenz auf Feld der Laenge 3, dessen
Eintraege jeweils Referenzen auf null sind */
int[][] C = new int[3][2];
/* Referenz auf Feld der Laenge 3, dessen
Eintraege jeweils Referenzen auf int-Felder
der L\"ange 2 sind */
int[][] D = new int[][2];
// Fehler
int[][] E = {{1,2}{2,2,5}{4}};
// gueltig trotz verschiedener Laengen
Bei der Verwendung der Methode clone() ist wieder volle Aufmerksamkeit geboten. Entwerfen Sie für das
folgende Beispiel ein grafischen Schema nach obigem Vorbild, um sich die in den Kommentaren genannten
Fakten klar zu machen.
int[][] data
int[][] copy
copy[0][0]
copy[1]
=
=
=
=
{{1,2,3}{4,5}};
(int[][]) data.clone();
100;
// data[0][0] hat auch den Wert 100
new int[] {7,8,9}; // data[1] hat sich nicht geaendert
Herunterladen