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