Advanced   Java   Services Arrays (Felder) Back Next Up Home

Einführung

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 ;


eindimensionale Felder

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 ?

Einige wichtige Punkte zu Arrays

Felder und Schleifen

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] ) ;

Maximale Feldgröße und OutOfMemoryError

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 );
    }
}

ArrayIndexOutOfBoundsException

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.


Dynamische Feldgrößen

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] ;

    }
}

Initialisierung von Arrays

Ä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' } ;


Arrays initialisieren mit der Klasses Arrays

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".


Felder als Referenzen

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:


arrayzeiger1.jpg

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:

arrayzeiger2 arrayzeiger2

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 .


Garbage Collector

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.


Arrays kopieren

Arrays zu Fuß kopieren

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] ;

Arrays kopieren mit Object.clone()

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


Erklärung für Fortgeschrittene

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

Arrays kopieren mit System.arraycopy()

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.


Zwei- und mehrdimensionale Felder

Bei zweidimensionalen Feldern braucht man zwei Indices um auf die einzelnen Elemente zuzugreifen. Wir betrachten die verschiednen Arten ein zweidimensionales Array zu initialisieren.


Ein zweidimensionales Array mit vorgegebenen Werten 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

Getrennte Initialisierung von Haupt- und Unterarrays

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

Initialisierung rechteckiger zweidimenionaler Arrays

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

Kurzer Ausflug in die dritte Dimension

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!


Übungen

Maximum, Minimum und Mittelwert eines Arrays

Sortieren eine Feldes mit BubbleSort

Der König und seine Gefangenen

Valid XHTML 1.0 Strict top Back Next Up Home