Advanced Java Services | Arrays (Felder) |
Sobald man mehrere Variable des gleichen Typs braucht, kommt man mit solchen Ansätzen nicht
besonders weit:
int a, b, c, d, e, f, g, h, i ;
Leider auch nicht besser ist der folgende Versuch
int a1, a2, a3, a4, a5, a6, a7, a8 ;
Der obige zweite Versuch war im Prinzip die richtige Idee, aber eben nicht die richtige Syntax.
Wir wollen für eine gewisse Anzahl von Variablen gleichen Typs einen gemeinsamen Obernamen und
dann mit Hilfe dieses Namens und einem Index auf die einzelnen Variablen zugreifen. Stellen sie sich einen
Schubladenschrank (oder auch mehrere) in einem Büro vor. Jeder Schrank habe einen Namen und die
Schubladen sind durchnumeriert. Dann kann man sagen, holen Sie mal die Akten von Schrank c, Schublade 4 usw.
Statt Schrank sagen wir jetzt Array oder Feld oder Vektor und die Numerierung nennen wir Feldindex.
In Java sieht das folgendemaßen aus:
int arr[] ; // Vereinbaren der Feldvariable arr vom Typ int (Leeren Schrank bereitstellen)
arr = new int[17] ; // Array für 17 Variable vom Typ int einrichten (Schrank mit 17 Schubladen einrichten)
Der Operator new ist der Operator für Arrayerzeugung und später auch für
Objekterzeugung (wobei Felder schon ein Spezialfall von Objekten sind). new hat zwei Aufgaben
zu erledigen. Zum einen muß es Speicher bereitstellen und zum zweiten muß es diesen Speicher
initialisieren (siehe Felder als Referenzen) . new ist ein
unärer Operator, hat also eine sehr hohe Priorität
(siehe Tabelle der Operatoren). Er existiert nur als Prefixoperator,
d.h. sein Operand steht immer rechts von ihm. In unserem Beispiel werden durch new 17 Variable vom Typ
int angeschafft mit dem gemeinsamen Namen arr. Man kann die beiden obigen Schritte auch
in einen einzigen zusammenfassen:
int arr[] = new int[17] ; // Vereinbaren der Feldvariable arr und einrichten für 17
Variable vom Typ int
Beachten Sie, daß bei der späteren Verwendung des new-Operators (erste Variante) die Feldvariable ohne
eckige Klammern verwendet wird. Das Feld heißt eindimensional, weil wir nur einen Feldindex
verwenden. Doppeltindizierte Felder heßen zweidimensional usw. Über den Feldindex haben
wir lesenden und schreibenden Zugriff auf die einzelnen Variablen:
arr[13] = 34 ; // Schreibender Zugriff Wert 34 in die 13-te Schublade legen
int x = arr[7]; // Lesender Zugriff Wert in der 7-ten Schublade wird nach x kopiert
System.out.println("Feldlänge = " + arr.length ); // Wieviele Schubladen hat der Schrank ?
Felder treten eigentlich immer im Zusammenhang mit Schleifen auf. Meist verwendet man hier die for-Schleife. Das folgende Beispiel erzeugt 7 Zufallszahlen und gibt sie auf die Konsole aus:
double d_arr[] = new double[7] ; for(int i=0; i < d_arr.length ; i++) d_arr[i] = Math.random() ; for(int i=0; i < d_arr.length ; i++) System.out.println("d_arr["+i+"] = " + d_arr[i] ) ;
Punkt 3 der obigen Auflistung hat zur Folge, daß es in Java ein Obergrenze für die
Feldgröße gibt, egal um welchen Feldtyp es sich dabei handelt. Die maximale Feldlänge
ist die größte Zahl, die im Format int darstellbar ist (siehe
Tabelle der Datentypen),
nämlich 2 147 483 647. So würde ein Feld vom Typ int dieser Größe
8 589 934 588 byte Speicherplatz im Hauptspeicher beanspruchen, das sind rund 8 192 MB. Es wird nicht
viele PC-Besitzer geben, die über so einen Haupspeicher verfügen...
Das folgende Programm führt denn auch zu einer neuen Art von Fehler. Es läßt sich
problemlos compilieren, aber beim Ausführen erhält man die Fehlermeldung
Exception in thread "main" java.lang.OutOfMemoryError.
Es handelt sich hier
um einen sog. Laufzeitfehler (runtime-error), einen Fehler, der eben erst zur Laufzeit auftritt.
Im Gegensatz zu den Fehlern, die der Compiler aufdecken kann (compiletime-error).
public class TooBig { public static void main(String args[]) { int tooBig[] = new int[2147483647]; System.out.println("tooBig.length = " + tooBig.length ); } }
Ein beliebter Fehler sind Zugriffe auf Feldelemente, die gar nicht existieren. In C gibt es dazu weder vom Compiler noch zur Laufzeit Meldungen. Die Folge sind manchmal schwer zu lokalisierende Programmabstürze. In Java dagegen führt ein falscher Arrayzugriff zwar auch nicht zu einer Meldung des Compilers, aber der Fehler wird zur Laufzeit erkannt. Das Programm bricht mit einer Ausnahme (Exception) der Sorte ArrayIndexOutOfBoundsException ab und man kann der Konsolmeldung sogar entnehmen, in welcher Zeile der falsche Arrayzugriff stattfindet.
public class TooBad { public static void main(String args[]) { int arr[] = new int[5] , i ; for( i=0; i < arr.length ; i++ ) arr[i] = i ; System.out.println(arr[i]); } }
Die Variable i hat beim Verlassen der Schleife den Wert arr.length, also hier 5. Ein Feldelement mit dem Index 5 existiert jedoch nicht.
Im Gegensatz zu C muß in Java die Feldgröße keine Konstante sein, sie könnte durchaus auch von einem Benutzer bestimmt werden:
public class HowMuch { public static void main(String args[]) { int arr[] , len ; System.out.print("Wieviele int-Variabeln brauchen Sie denn :" ); len = Stdin.intEingabe(); if ( len > 0 && len < 10000 ) arr = new int[len] ; } }
Ähnlich wie der Compiler Felder initialisiert kann man kleine Felder auch selbst initialisieren.
Dazu vereinbaren wir zuerst eine Feldvariable (Referenz) :
char ch_arr[] ;
Im zweiten Schritt geben wir in einem Statement die Feldinhalte durch Aufzähung der Elemente bekannt:
ch_arr = new char[ ] { 'a' , 'g' , 'a' , 'p' , 'i' } ;
// Hier darf new char[ ] nicht weggelassen
werden !
Offensichtlich eignet sich dieses Verfahren nur für kurze Feldlängen. Auch in diesem Fall
lassen sich beide Schritte in der Vereinbarung zusammenfassen:
char ch_arr[] = new char[ ] { 'a' , 'g' , 'a' , 'p' , 'i' } ;
In letzerem Fall kann man auch kürzer wie folgt schreiben:
char ch_arr[] = { 'a' , 'g' , 'a' , 'p' , 'i' } ;
Die Klasse Arrays aus dem package jabva.util ist ein weiteres Beispiel für eine Klasse, die nur statische Methoden enthält. In ihr sind einige nützliche Methoden für die Arbeit mit Arrays. Mit Hilfe dieser Methoden kann man z.Bsp. Felder mit Werten füllen, Felder vergleichen oder Felder sortieren. So gibt es allein 18 Varianten (Überladungen) der Methode fill. Die Methoden füllen entweder ein ganzes Feld mit jeweils ein und dem gleichen Wert oder nur einen Teil eines Feldes mit jeweils dem gleichen Wert.
static void |
fill(typ[] a, typ val) Assigns the specified typ value to each element of the specified array of typs. |
static void |
fill(typ[] a, int fromIndex, int toIndex, typ val) Assigns the specified typ value to each element of the specified range of the specified array of typs. |
Hier steht typ für einen der 8 primitiven Datentypen bzw. für den Datentyp Object.
Wie üblich bei Bereichsangaben sind die Indexangaben von (einschließlich) bis (ausschließlich).
Wenn etwa in einem long-Array alle Feldelemente den wert 17 haben sollen, dann schreiben wir die
Anweisung
long arr[] = new long[20] ; Arrays.fill(arr, 17);
analog für ein short-Array
short arr[] = new short[20] ; Arrays.fill(arr, (short)17);
oder für ein String-Array
String arr[] = new String[20] ; Arrays.fill(arr, "Hello Java"); Arrays.fill(arr, arr.length/2 , arr.length, "Hello World");
Mit der obigen dritten Anweisung überschreiben wir die zweite Hälfte des String-Arrays nachträglich mit dem String "Hello World".
Wie am Anfang legen wir wieder eine Feldvariable an, diesmal zur Abwechslung vom Typ double.
double d_arr[] ;
Die Variable d_arr ist ein erstes Beispiel für eine Referenz. Statt Referenz sagt man auch
Pointer oder Zeiger. Solche Variablen enthalten keine Werte, sondern Adressen von anderen Variablen. Nach
der obigen Zeile ist d_arr zunächst noch nicht mit einem sinnvollen Inhalt belegt, aber
als Speicherplatz vorhanden:
Nun legen wir ein Array mit 5 Elementen an:
d_arr = new double[5] ;
Nachdem mit new double[5] das Array angelegt worden ist, haben wir die folgende Situation:
Der new Operator hat einen zusammenhängenden Block von 5 double-Variablen angelegt und diese
mit 0.0 vorbelegt. Außerdem legt er die Anfangsadresse (hier XXXX) dieses Blocks in der
Referenzvariablen d_arr ab (der Wert dieser Adresse ist für uns uninteressant). Man sagt,
die Referenzvariable d_arr zeigt auf den Anfang des neuen Speicherblocks.
Des weiteren wird noch ein Speicherplatz length angeschafft,
der die Länge des Arrays speichert. Auf die Feldvariablen d_arr[0] bis d_arr[4] haben
wir lesenden und schreibenden Zugriff, auf length haben wir nur lesenden Zugriff oder mit anderen
Worten, length ist eine Konstante, auf d_arr haben wir weder lesenden noch schreibenden
Zugriff, wir können d_arr nur verwenden, um auf die anderen Speicherplätze zuzugreifen.
Diese Art des Zugriffs nennt man dereferenzieren. In diesem Fall können wir d_arr auf sechs
Arten dereferenzieren und erhalten dadurch die fünf double-Variablen d_arr[0] bis
d_arr[4] und die Feldlängenkonstante d_arr.length .
Wir betrachten die folgende Situation
int arr[] ; // Feldreferenz anlegen
arr= new int[17] ; // Feld für 17 int-Variablen angelegt
// einige Zeilen Programmcode
arr= new int[34] ; // Feld für 34 int-Variablen angelegt
Obiger Programmausschnitt ist fehlerfrei und wäre in C++ eine Todsünde. Zur selben
Referenz wird ein neuer Speicher für ein neues Array angefordert. Die alten Feldelemente sind nicht
mehr ansprechbar und hängen im Speicher. Der Programmierer hat vergessen, den nicht mehr
benützten Speicher freizugeben. In Java dürfen wir uns so eine "Schlamperei" erlauben.
Zur Laufzeit tritt nämlich ein Müllschlucker (garbage collector) in Aktion, der immer wieder
den Speicher nach nicht mehr verwendeten (nicht mehr ansprechbaren) Bereichen durchforstet und
diese dann freigibt. Der garbage collector arbeitet als Hintergrundprozeß mit niedriger
Priorität. Er tritt immer wieder in Aktion, aber man kann als Programmierer nicht bestimmen, wann
er seine Arbeit beginnt. Es sind keine zeitlichen Vorhersagen möglich. Man kann lediglich durch
die Zuweisung
arr = null ;
dem garbage collector signalisieren, daß eine Referenz nicht mehr gebraucht wird. Trotzdem bestimmt
er den Zeitpunkt für die Entsorgung. Wir haben keinen Einfluß darauf.
Natürlich kann man immer mit einer for-Schleife arbeiten.
short arr[] = new short[20] ; short arr2[] = new short[20] ; Arrays.fill(arr, (short)17); for(int i=0; i < arr2.length; i++) arr2[i] = arr[i] ;
Das ist etwas raffinierter. Hier ein kleines Beispielprogramm.
public class ArrayCopy { public static void main(String args[]) { int[] arr = { 1, 2, 3, 4, 5, 6, 7 } , brr ; Object ob = arr.clone(); // clone kopiert das Array und liefert eine Referenz auf die Kopie zurück // Die Referenz hat aber den Typ Object brr = (int[])ob ; // Die Referenz wird zum Feldtyp gecastet und zugewiesen Arrays.fill( arr, 17) ; // Das Ausgangsarray wird mit 17 gefüllt System.out.println("arr --------") ; for(int i=0; i<arr.length; i++) System.out.println(arr[i]) ; System.out.println("brr --------") ; for(int i=0; i<brr.length; i++) System.out.println(brr[i]) ; } }
Zum Beweis, daß es sich tatsächlich um eine Kopie handelt, werden die Feldelemente von arr alle auf 17 gesetzt. Die Ausgabe zeigt, daß brr davon nicht berührt wird. Die bisher behandelte Theorie reicht nicht aus, den Vorgang genau zu erklären, deshalb folgt hier eine
Arrays sind Objekte, die das Interface Cloneable implementiert haben. Dadurch wird die Methode
clone() aus der Klasse public und damit verwendbar. Die Methode kopiert das Array und liefert einen
Zeiger auf das kopierte Array zurück, der allerdings vom Typ Object ist. Dieser Zeiger muß dann noch
auf den richtigen Typ gecastet werden. Im Unterschied zur Methode arraycopy() (siehe nächster Abschnitt)
muß hier die Zielreferenz nicht initialisiert werden. Auch das macht clone() .
Man kann sich kurz das folgende Schema merken, mit dem man beliebige Arrays kopieren kann.
datentyp[] arr , brr ; // arr werde irgendwie mit Werten gefüllt brr = (datentyp[])arr.clone() ; // brr ist nun eine Kopie von arr
Die Klasse System bietet die statische Methode arraycopy() an. Auch hier wird von der Tatsache Gebrauch gemacht, daß eine Arrayreferenz bereits eine Objektreferenz ist.
static void |
arraycopy(Object src, int srcPos, Object dest, int destPos, int length) Copies an array from the specified source array, beginning at the specified position, to the specified position of the destination array. Parameters: src - the source array. srcPos - starting position in the source array. dest - the destination array. destPos - starting position in the destination data. length - the number of array elements to be copied. |
Liest man die Erklärung der Parameter, dann wird das folgende Beispiel (hoffentlich) klar.
public class ArrayCopy2 { public static void main(String args[]) { int[] digits = { 0, 1, 2, 3, 4, 5, 6, 7, 9, 9 } , octaldigits = new int[8] ; System.arraycopy(digits, 0, octaldigits, 0, 8) ; // Parameter: Quellarray, Startposition, Zielarray, Startposition, Anzahl der Elemente System.out.println("digits --------") ; for(int i=0; i<digits.length; i++) System.out.println(digits[i]) ; System.out.println("octaldigits --------") ; for(int i=0; i<octaldigits.length; i++) System.out.println(octaldigits[i]) ; } }
Das Beispiel macht die folgende Ausgabe
digits -------- 0 1 2 3 4 5 6 7 9 9 octaldigits -------- 0 1 2 3 4 5 6 7
Zum Schluß sei noch erwähnt, daß sowohl arrycopy() von System wie auch clone() von Arrays als native Methoden implementiert sind. Native Methoden sind in einer plattformabhängigen Sprache geschrieben (meist C/C++), liefern aber plattformunabhängige Ergebnisse. Die beiden Methoden arbeiten deswegen schneller als etwa eine Schleife.
Bei zweidimensionalen Feldern braucht man zwei Indices um auf die einzelnen Elemente zuzugreifen. Wir betrachten die verschiednen Arten ein zweidimensionales Array zu initialisieren.
Die einfachste Art ein zweidimensionales Array einzurichten ist die direkte Angabe der Werte in geschweiften Klammern, siehe den Abschnitt Initialisierung von Arrays etwas weiter oben. Diese Syntax läßt sich sehr einfach erweitern:
char[][] zweidim = { {'*'} , {' ', '*'} , {' ', ' ', '*'} , {' ', '*'} , {'*'} };
Man sieht hier sofort, daß ein zweidimenionales Array als Array von Arrays aufgebaut wird. In diesem Fall beherbergt das Hauptarray fünf Unterarrays mit unterschiedlichen Längen. Der Zugriff auf die Elemente erfordert zwei Indices. Mit dem ersten Index wählt man eines der fünf Unterarrays aus, mit dem zweiten Index bewegt man sich dann im Unterarray selbst. Die Numerierung beginnt dabei wie stets mit 0. Mit zweidim[2] spreche ich also das dritte UnterArray an und mit zweidim[2][2] erwischt man den Stern im dritten Unterarray. Da man für jedes Unterarray ebenfalls die Arraylänge mit length zur Verfügung hat, ist der Zugriff gar nicht so schwer. Typisch für mehrdimensionale Arrays ist der Zugriff mit geschachtelten for-Schleifen:
for(int i=0; i < zweidim.length; i++) { for(int j=0; j < zweidim[i].length; j++) System.out.print(zweidim[i][j]); System.out.println(); }
Durch das obige Array wird das folgende Muster auf die Konsole ausgegeben.
* * * * *
Ein zweidimenionales Array wird gerne als Matrix mit Zeilen und Spalten aufgefaßt, was übrigens nicht der tatsächlichen Speicherung im RAM entspricht. Für diese Vorstellung ist es wichtig, daß die Reihenfolge der Indices entscheidend ist: Der erste Index ist der Zeilenindex, der zweite Index der Spaltenindex. Merkregel:
Zeilenindex vor Spaltenindex
Als erstes initialisieren wir das Hauptarray:
int zweidim[][] = new int[4][];
oder auch so
int[][] zweidim = new int[4][];
Unser Array wird also vier Zeilen umfassen. Wichtig dabei: Das Klammerpaar auf der linken Seite muß leer sein.
Nun kommen die Unterarrays.
zweidim[0] = new int[3]; zweidim[1] = new int[2]; zweidim[2] = new int[1]; zweidim[3] = new int[0];
Hier ist kein Schreibfehler passiert. Ein Array darf auch die Länge 0 haben. In dieser Form hat das natürlich keine praktische Bedeutung. Es gibt aber in der API Methoden, die ein Array der Länge 0 zurückgeben können. Auf diese Weise kann man nämlich vermeiden, daß im Nichterfolgsfall null zurückgegeben wird.
Bei Initialisierung mit new werden alle Feldelemente mit 0 vorbelegt. Die geschachtelte Schleife, mit der wir eine eigene Belegung erreichen wieht genauso aus wie im ersten Beispiel:
for(int i=0; i < zweidim.length; i++) { for(int j=0; j < zweidim[i].length; j++) // i-te Zeile { zweidim[i][j] = (i+1)*(j+1); } }
Die Ausgabe ergibt das folgende Muster.
1 2 3 2 4 3
Rechteckige und damit auch quadratische Arrays sind einfach zu definieren, weil man das Hauptarray und die Unterarrays zusammen definieren kann. So initialisiert die folgende Zeile
int[][] zweidim = new int[4][3]; // alle Elemente haben den Wert 0
eine Matrix mit 4 Zeilen und 3 Spalten. Die eckigen Klammern können auch nach dem Bezeichner stehen.
int[][] zweidim = new int[4][3]; // alle Elemente haben den Wert 0
Das eben gesagte gilt sinngemäß auch für drei- und mehrdimensionale Felder. Ein dreidimenionales Feld kann man sich noch als dreidimensionales Gitter vorstellen bei dem die Gitterpunkte bestimmte Werte haben. Hier mal ein Beispiel eines unregelmäßigen dreidimensionalen Gitters.
int[][][] dreidim = { { {1, 2} , {4} , {} } , { {5} , {6, 7, 8} } , { {9} } } ;
Die leeren Klammern sind kein Syntaxfehler!
Maximum, Minimum und Mittelwert eines Arrays
Sortieren eine Feldes mit BubbleSort
Der König und seine Gefangenen