Primitive Datentypen und Felder (rrays) 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 Speicherplatz Darstellungsbereich byte 8 Bit = 1 Byte [ 128,127] short 16 Bit = 2 Byte [ 32768,32767] int 32 Bit = 4 Byte [ 2 31,2 31 1] long 64 Bit = 8 Byte [ 2 63,2 63 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 usdruck 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 ddition bzw. die Subtraktion von 1. Man kann beide in Präfix Notation (vor dem rgument) und Suffix Notation (nach dem rgument) 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++; // aktuelle Werte: k=20 und i=21 int l = --k; // aktuelle Werte: 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. ls 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 usnahmen (int, char) tragen diese Klassen jeweils den gleichen Namen, aber mit Großbuchstaben am nfang: Byte, Short, Integer, Long, Float, Double, Boolean, Character Wie ein kurzer Blick in daie Systembeschreibung PI (pplication 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 bschnitt 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 typen, d.h. ihr Name ist nicht mit einem konkreten Objekt dieses Typs, sondern mit einer (Verweis) assoziiert, die auf solch einen Objekt oder aber auf null (ein symbolischer usdruck für NICHTS) verweist. Mit einer Zuweisung wird in einem solchen Fall nicht das Objekt kopiert, sondern nur die auf dieses Objekt. en sind mehr als nur ein einfacher Zeiger, aber man kann sich eine 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 (rray). Felder Ein Feld oder rray 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 rray 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 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 rray
entweder schon deklariert sein, oder es muss im Speicher angelegt werden. uch 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 rray gespeichert werden sollen, direkt aufzulisten (womit die Feldlänge implizit festgelegt wird). Das folgende Beispiel demonstriert diese Varainten: int[] a1; // a1 ist ( auf) null int[] a2 = new int[4]; /* a2 ist ( auf) ein int-rray der Laenge 4, in dem alle Eintraege den Default-Wert 0 haben */ int[] a3 = {1,2,3}; // a3 ist ( auf) ein int-rray der Laenge 3 int[] a4 = a2; // a4 ist ( auf) auf gleiches rray wie a2 int[] a5 = a1; // a5 ist ( auf) null Wie man sieht, wird bei einer Zuweisung nur die übertragen, es erfolgt keine Kopie des eigentlichen Feldes im Speicher. uf den i ten Eintrag eines rrays 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]; // i hat den Wert 2, denn die Nummerierung beginnt mit 0 a2[3] = 5; // eine 0 wird mit 5 ueberschrieben int j = a4[3] /* j hat den Wert 5 weil a2 und a4 auf das gleiche rray verweisen */ Die letzte Zeile macht noch einmal deutlich, dass nach Zuweisung von rray 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; // beide haben den Wert 3 n1 = 5; // 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 en auf Gleichheit getestet werden und es nicht darauf ankommt, ob die referenzierten Objekte gleich sind oder nicht. uch diesen Effekt kann man an einem einfachen Beispiel demonstrieren: int[] = {2,3,4} // ein erstes rray mit Eintraegen 1,2,3 wir angelegt int[] B = ; // B ist auf das gleiche rray int[] C = {2,3,4} // ein zweites rray mit Eintraegen 1,2,3 wir angelegt boolean c = ( == C); /* c ist false, denn die en verweisen auf zwei verschiedene Speicherabschnitte */ boolean b = ( == B); // b ist true, beide en verweisen auf erstes rray In der folgenden Grafik ist dargestellt, wie die usführung der ersten drei Zeilen des Codes im Speicher realisiert wird.
Code Variable Speicher int[ ] = {2,3,4}; int[ ] B = ; B int[ ] C = {2,3,4}; B C
Um eine wirkliche Kopie eines rrays zu erzeugen, verwendet man die Funtion clone(). us Gründen, die erst später klar werden, muss aber zusätzlich noch eine Typumwandlung erfolgen: int[] = {1,2,3} // ein erstes rray mit Eintraegen 1,2,3 wird angelegt int[] B = (int[]).clone(); /* ein zweites rray mit Eintraegen 1,2,3 wird als Kopie des ersten rrays angelegt */ boolean b = ( == B); /* b ist false, en sind verschieden Durch die Verwendung von mehreren Klammerpaaren können höherdimensionale rrays, mit anderen Worten Felder von Feldern, angelegt werden. Das folgende Beispiel zeigt wieder die verschiedenen Möglichkeiten auf, solche rrays zu deklarieren und zu definieren. int[][] ; // auf null int[][] B = new int[3][]; /* auf Feld der Laenge 3, dessen Eintraege jeweils en auf null sind */ int[][] C = new int[3][2]; /* auf Feld der Laenge 3, dessen Eintraege jeweils en 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 ufmerksamkeit 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 = {{1,2,3}{4,5}}; int[][] copy = (int[][]) data.clone(); copy[0][0] = 100; // data[0][0] hat auch den Wert 100 copy[1] = new int[] {7,8,9}; // data[1] hat sich nicht geaendert