Zum Inhalt springen

2D-Arrays in Java: deklarieren, initialisieren, durchlaufen, ausgeben

Ein schlankes offenes Regal aus gebürstetem Stahl vor Tannengrün mit drei Böden: Auf jedem steht ein eigenes Tablett aus hellem Holz mit einer Reihe aus drei klaren Glasmurmeln, und die mittlere Murmel auf dem mittleren Tablett leuchtet bernsteinfarben

Ein zweidimensionales Array in Java ist ein Array, dessen Elemente selbst Arrays sind. Du deklarierst es mit zwei Paaren von eckigen Klammern, und du greifst mit zwei Indizes auf ein Element zu – der erste wählt die Zeile, der zweite die Spalte:

int[][] intMatrix = {{2, 3, 6}, {4, 5, 1}};
int value = intMatrix[1][2]; // Zeile 1, Spalte 2 – liefert 1

Dass jede Zeile ein eigenes Array ist, hat Folgen, die du kennen solltest: Die Zeilen dürfen verschieden lang sein, clone() kopiert nur die äußere Ebene, und wer die Schleifen in der falschen Reihenfolge schachtelt, wartet bei einer großen Matrix siebenmal so lange wie bei der richtigen Reihenfolge.

In diesem Artikel erfährst du,

  • wie ein 2D-Array in Java im Speicher aufgebaut ist,
  • wie du es deklarierst und initialisierst – mit Werten, mit Standardwerten oder Zeile für Zeile,
  • wie du auf Elemente zugreifst, die Zeilen- und Spaltenanzahl ermittelst und das Array durchläufst,
  • wie du ein 2D-Array ausgibst, füllst, kopierst und sortierst,
  • was Jagged Arrays sind und wofür sie sich eignen,
  • wie 3D-Arrays funktionieren
  • und wann ein flaches eindimensionales Array die bessere Wahl ist – mit Messwerten.

Die Beispiele in diesem Artikel

Den vollständigen Code findest du im GitHub-Repository java-arrays, im Package eu.happycoders.twodimensional. Jeder Abschnitt hat dort eine eigene Klasse mit main()-Methode, die die gezeigten Ausgaben erzeugt. Wo der Artikel eine Exception zeigt, fängt die Klasse sie ab und gibt sie aus, damit die folgenden Beispiele weiterlaufen. Die Messung aus dem letzten Abschnitt liegt im Unterpackage benchmark.

Was ist ein 2D-Array in Java?

Java kennt keine echten mehrdimensionalen Arrays. Ein int[][] ist ein eindimensionales Array, dessen Elemente wiederum Referenzen auf int-Arrays sind – die Zeilen der Matrix.

Das Array intMatrix aus dem Einstieg sieht im Speicher so aus:

Zweidimensionales int-Array intMatrix: das äußere Array hält zwei Referenzen, die auf zwei innere Arrays mit den Werten 2, 3, 6 und 4, 5, 1 zeigen
Eine Matrix mit zwei Zeilen und drei Spalten als Array aus zwei Arrays

Links das äußere Array mit zwei Referenzen, rechts die beiden inneren Arrays mit je drei Werten. Insgesamt drei Objekte auf dem Heap – nicht eines.

Deshalb kannst du mit einem einzelnen Index eine ganze Zeile herausgreifen, denn intMatrix[0] beispielsweise ist ein int[]:

int[][] intMatrix = {{2, 3, 6}, {4, 5, 1}};

int[] row0 = intMatrix[0];
int[] row1 = intMatrix[1];
System.out.println("row0 = " + Arrays.toString(row0));
System.out.println("row1 = " + Arrays.toString(row1));

System.out.println("intMatrix[1][2] = " + intMatrix[1][2]);
row0 = [2, 3, 6]
row1 = [4, 5, 1]
intMatrix[1][2] = 1

Welcher Index ist die Zeile und welcher die Spalte? Der erste Index wählt das innere Array aus, der zweite die Position darin. Dass das innere Array eine Zeile ist und nicht eine Spalte, ist eine Konvention – Java schreibt sie nicht vor, aber alle Beispiele in diesem Artikel und so gut wie aller Java-Code halten sich daran. Dementsprechend ist intMatrix.length die Anzahl der Zeilen, intMatrix[0].length die Anzahl der Spalten.

2D-Array deklarieren

Ein zweidimensionales Array deklarierst du mit zwei Paaren von eckigen Klammern hinter dem Elementtyp:

int[][] intMatrix;
String[][] stringMatrix;

Eine Größe gibst du bei der Deklaration nicht an – die legst du erst bei der Initialisierung fest.

Java erlaubt die Klammern auch im C-Stil hinter dem Variablennamen, und sogar gemischt:

int intMatrix[][];
int[] mixedMatrix[];

Alle drei Schreibweisen erzeugen denselben Typ. Ich empfehle die erste, mit beiden Paaren von eckigen Klammern am Typ: Die gängigen Java-Style-Guides bevorzugen sie, und sie ist die einzige, bei der der Typ int[][] als Ganzes lesbar ist.

2D-Array initialisieren

Ein 2D-Array kannst du auf drei Arten initialisieren: mit festen Werten, mit Standardwerten einer festen Größe oder Zeile für Zeile.

Mit Werten initialisieren

Die kürzeste Form ist das Array-Literal mit geschweiften Klammern – ein äußeres Klammerpaar und ein inneres Paar pro Zeile:

int[][] intMatrix = {{2, 3, 6}, {4, 5, 1}};

Dieselbe Matrix entsteht mit new gefolgt vom Typ und zwei Paaren von leeren eckigen Klammern vor dem Array-Literal. Bei Verwendung von new darfst du vor dem Variablennamen auch var schreiben, denn der Compiler kann den Typ dann von der rechten Seite ablesen:

int[][] intMatrix = new int[][] {{2, 3, 6}, {4, 5, 1}};
var sameMatrix = new int[][] {{2, 3, 6}, {4, 5, 1}};

Die new-Form brauchst du auch dann, wenn du Deklaration und Initialisierung auf zwei Anweisungen verteilst, denn das nackte Literal {{2, 3, 6}, {4, 5, 1}} ist nur in einer Deklaration erlaubt:

int[][] intMatrix;
intMatrix = new int[][] {{2, 3, 6}, {4, 5, 1}};

Mit Standardwerten initialisieren

Kennst du die Werte noch nicht, aber die Größe, gibst du beide Dimensionen in den Klammern an:

int[][] intMatrix = new int[2][3];

Das erzeugt zwei Zeilen mit je drei Elementen, alle mit dem Standardwert des Elementtyps: 0 bei int, 0.0 bei double, false bei boolean und null bei Referenztypen wie String:

int[][] intMatrix = new int[2][3];
System.out.println("new int[2][3] = " + Arrays.deepToString(intMatrix));

String[][] stringMatrix = new String[2][3];
System.out.println("new String[2][3] = " + Arrays.deepToString(stringMatrix));
new int[2][3] = [[0, 0, 0], [0, 0, 0]]
new String[2][3] = [[null, null, null], [null, null, null]]

Die Methode Arrays.deepToString() zeige ich im Abschnitt 2D-Array ausgeben. Die Elemente setzt du danach einzeln:

intMatrix[0][0] = 2;
intMatrix[0][1] = 3;
intMatrix[0][2] = 6;
intMatrix[1][0] = 4;
intMatrix[1][1] = 5;
intMatrix[1][2] = 1;

Übrigens erzeugt new int[2][3] alle drei Objekte – das äußere Array und die zwei Zeilen – mit einer einzigen Bytecode-Instruktion, multianewarray. Du musst die Zeilen also nicht selbst anlegen.

Zeile für Zeile initialisieren

Du darfst die zweite Dimension auch weglassen. Dann entsteht nur das äußere Array, und seine Elemente – also die Zeilen – sind null:

int[][] intMatrix = new int[2][];
System.out.println("new int[2][] = " + Arrays.deepToString(intMatrix));

intMatrix[0] = new int[] {2, 3, 6};
intMatrix[1] = new int[] {4, 5, 1};
System.out.println("rows assigned = " + Arrays.deepToString(intMatrix));
new int[2][] = [null, null]
rows assigned = [[2, 3, 6], [4, 5, 1]]

Diese Form brauchst du, wenn die Zeilen erst später entstehen – etwa beim Einlesen einer Datei – oder wenn sie verschieden lang sein sollen. Dazu mehr im Abschnitt Jagged Arrays: Zeilen unterschiedlicher Länge.

Wie du eindimensionale Arrays initialisierst, füllst und aus Listen und Streams erzeugst, zeigt der Artikel Arrays in Java initialisieren.

Auf Elemente zugreifen

Lesen und Schreiben funktionieren mit zwei Indizes, beide beginnen bei 0:

int[][] intMatrix = {{2, 3, 6}, {4, 5, 1}};

int value = intMatrix[1][2];
System.out.println("intMatrix[1][2] = " + value);

intMatrix[1][2] = 9;
System.out.println("intMatrix = " + Arrays.deepToString(intMatrix));
intMatrix[1][2] = 1
intMatrix = [[2, 3, 6], [4, 5, 9]]

Da eine Zeile ein eigenes Array ist, kannst du die Zeile in einer eigenen Variablen halten und über diese Variable Felder der Zeile lesen oder schreiben. Eine Änderung ist in der Matrix sichtbar, denn beide Variablen zeigen auf dasselbe innere Array:

int[] row1 = intMatrix[1];
row1[0] = 7;
System.out.println("intMatrix = " + Arrays.deepToString(intMatrix));
intMatrix = [[2, 3, 6], [7, 5, 9]]

Ein Index außerhalb des gültigen Bereichs führt zu einer ArrayIndexOutOfBoundsException – für die Zeile ebenso wie für die Spalte:

System.out.println(intMatrix[0][3]);
Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: Index 3 out of bounds for length 3
System.out.println(intMatrix[2][0]);
Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: Index 2 out of bounds for length 2

Die Meldung nennt nicht, welcher der beiden Indizes falsch war – aber die Länge verrät es: length 3 ist die Länge einer Zeile, also war die Spalte falsch; length 2 ist die Anzahl der Zeilen.

Zeilen- und Spaltenanzahl eines 2D-Arrays

Die Anzahl der Zeilen ist die Länge des äußeren Arrays, die Anzahl der Spalten die Länge einer Zeile:

int[][] intMatrix = {{2, 3, 6}, {4, 5, 1}};
int rows = intMatrix.length;
int columns = intMatrix[0].length;
System.out.println("rows = " + rows + ", columns = " + columns);
rows = 2, columns = 3

intMatrix[0].length setzt zweierlei voraus: dass es eine Zeile 0 gibt und dass diese Zeile nicht null ist. Bei einem Array ohne Zeilen wirft der Zugriff eine ArrayIndexOutOfBoundsException:

int[][] intMatrix = new int[0][];
int columns = intMatrix[0].length;
Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: Index 0 out of bounds for length 0

Bei einem Array, das mit new int[2][] ohne zweite Dimension angelegt wurde und dessen Zeilen noch nicht zugewiesen sind, wirft der Zugriff eine NullPointerException:

int[][] intMatrix = new int[2][];
int columns = intMatrix[0].length;
Exception in thread "main" java.lang.NullPointerException: Cannot read the array length because "intMatrix[0]" is null

Prüfe also zuerst, ob rows größer als 0 ist und ob intMatrix[0] nicht null ist.

Bei einem Jagged Array sagt die Länge der ersten Zeile nichts über die Längen der anderen Zeilen. Wie du die Länge bei verschieden langen Zeilen auswertest, wie viel Speicher ein 2D-Array belegt und wie groß es werden kann, steht im Artikel Array-Länge in Java.

2D-Array durchlaufen

Zwei geschachtelte for-Schleifen durchlaufen die Matrix zeilenweise – die äußere Schleife über die Zeilen, die innere über die Spalten der aktuellen Zeile:

for (int row = 0; row < intMatrix.length; row++) {
  for (int col = 0; col < intMatrix[row].length; col++) {
    System.out.println(
        "intMatrix[" + row + "][" + col + "] = " + intMatrix[row][col]);
  }
}
intMatrix[0][0] = 2
intMatrix[0][1] = 3
intMatrix[0][2] = 6
intMatrix[1][0] = 4
intMatrix[1][1] = 5
intMatrix[1][2] = 1

Beachte die Grenze der inneren Schleife: intMatrix[row].length, nicht intMatrix[0].length – so läuft die Schleife auch über ein Jagged Array ohne Exception.

Brauchst du die Indizes nicht, sind zwei For-each-Schleifen kürzer. Die äußere liefert jede Zeile als int[], die innere jeden Wert:

int sum = 0;
for (int[] row : intMatrix) {
  for (int value : row) {
    sum += value;
  }
}
System.out.println("sum = " + sum);
sum = 21

Dasselbe als Stream: Arrays.stream(intMatrix) liefert einen Stream<int[]>, und flatMapToInt(Arrays::stream) löst jede Zeile in ihre Werte auf. Danach stehen dir sum(), max() und alle anderen Operationen eines IntStream zur Verfügung:

int sum = Arrays.stream(intMatrix)
    .flatMapToInt(Arrays::stream)
    .sum();
System.out.println("sum = " + sum);
sum = 21

Die Reihenfolge der Schleifen – erst Zeilen, dann Spalten – ist nicht nur Konvention. Vertauschst du sie, läuft die innere Schleife über alle Zeilen hinweg, und das kostet bei einer 4.096 × 4.096-Matrix das 6,9-Fache der Zeit. Die Messung dazu steht im Abschnitt Speicher und Performance: 2D-Array oder flaches Array?

2D-Array ausgeben

System.out.println(intMatrix) gibt nur den Typ und den Hash-Code des äußeren Arrays aus, und Arrays.toString() kommt eine Ebene weiter – die Methode zeigt die Referenzen auf die Zeilen:

int[][] intMatrix = {{2, 3, 6}, {4, 5, 1}};
System.out.println(intMatrix);
System.out.println(Arrays.toString(intMatrix));
[[I@2b2fa4f7
[[I@7ad041f3, [I@251a69d7]

Beides ist in der Regel nicht das, was wir ausgeben wollen. Für verschachtelte Arrays gibt es deshalb Arrays.deepToString(). Die Methode steigt in jede Ebene hinab und gibt die Werte aus:

System.out.println(Arrays.deepToString(intMatrix));
[[2, 3, 6], [4, 5, 1]]

Soll jede Zeile in einer eigenen Textzeile stehen, kombinierst du eine For-each-Schleife mit Arrays.toString():

for (int[] row : intMatrix) {
  System.out.println(Arrays.toString(row));
}
[2, 3, 6]
[4, 5, 1]

Für eine Ausgabe in Spalten, wie du sie von einer Tabelle erwartest, formatierst du jeden Wert mit printf() auf eine feste Breite. Das folgende Beispiel füllt eine 4 × 4-Matrix mit den Produkten der Zahlen 7 bis 10 und gibt sie aus; %4d reserviert vier Zeichen pro Zahl und füllt links mit Leerzeichen auf:

int[][] products = new int[4][4];
for (int i = 0; i < products.length; i++) {
  for (int j = 0; j < products[i].length; j++) {
    products[i][j] = (i + 7) * (j + 7);
  }
}

for (int[] row : products) {
  for (int value : row) {
    System.out.printf("%4d", value);
  }
  System.out.println();
}
  49  56  63  70
  56  64  72  80
  63  72  81  90
  70  80  90 100

2D-Array füllen

Die Methoden Arrays.fill() und Arrays.setAll() arbeiten auf eindimensionalen Arrays. Für ein 2D-Array musst du sie für jede Zeile aufrufen.

Der folgende Code erzeugt das kleine Einmaleins. Arrays.setAll() ruft das Lambda für jeden Index j der Zeile auf und schreibt das Ergebnis hinein; die Zeilennummer muss dafür in einer effectively final Variablen stehen:

int[][] products = new int[11][11];
for (int i = 0; i < products.length; i++) {
  int row = i; // das Lambda braucht eine effectively final Variable
  Arrays.setAll(products[i], j -> row * j);
}
System.out.println("products[7] = " + Arrays.toString(products[7]));
products[7] = [0, 7, 14, 21, 28, 35, 42, 49, 56, 63, 70]

Sollen alle Elemente denselben Wert bekommen, ist Arrays.fill() pro Zeile der kürzeste Weg:

for (int[] row : products) {
  Arrays.fill(row, -1);
}

Verlockend ist es, Arrays.fill() stattdessen auf das äußere Array anzuwenden und ihm eine fertige Zeile zu übergeben:

int[][] matrix = new int[3][];
Arrays.fill(matrix, new int[] {1, 2, 3, 4});
System.out.println("matrix = " + Arrays.deepToString(matrix));
matrix = [[1, 2, 3, 4], [1, 2, 3, 4], [1, 2, 3, 4]]

Die Ausgabe sieht richtig aus. Aber new int[] {1, 2, 3, 4} wird nur einmal ausgewertet, und Arrays.fill() schreibt dieselbe Referenz in alle drei Zeilen:

Zweidimensionales int-Array matrix mit drei Referenzen, die alle auf dasselbe innere Array mit den Werten 1, 2, 3, 4 zeigen
Arrays.fill() mit einer Zeile: alle drei Referenzen zeigen auf dasselbe innere Array

Alle drei Referenzen des äußeren Arrays zeigen auf das eine innere Array. Änderst du jetzt ein einzelnes Element, ändert es sich in jeder Zeile:

matrix[1][1] = 99;
System.out.println("matrix = " + Arrays.deepToString(matrix));
matrix = [[1, 99, 3, 4], [1, 99, 3, 4], [1, 99, 3, 4]]

Richtig ist Arrays.setAll() auf dem äußeren Array, denn das Lambda wird für jede Zeile erneut aufgerufen und erzeugt jedes Mal ein neues Array:

Arrays.setAll(matrix, _ -> new int[] {1, 2, 3, 4});
matrix[1][1] = 99;
System.out.println("matrix = " + Arrays.deepToString(matrix));
matrix = [[1, 2, 3, 4], [1, 99, 3, 4], [1, 2, 3, 4]]

Die 99 steht jetzt nur in einer Zeile.

Jagged Arrays: Zeilen unterschiedlicher Länge

Weil jede Zeile ein eigenes Array ist, müssen die Zeilen nicht gleich lang sein. Ein Array mit unterschiedlich langen Zeilen heißt Jagged Array (ausgefranstes Array), und es lässt sich sogar als Array-Literal schreiben:

int[][] twoDimensionalArray = {{2, 3, 6}, {4, 5, 1, 9, 7}};

Im Speicher sieht das so aus – zwei innere Arrays mit drei und fünf Elementen:

Zweidimensionales int-Array twoDimensionalArray mit Zeilen unterschiedlicher Länge: die erste Referenz zeigt auf ein Array mit den Werten 2, 3, 6, die zweite auf ein Array mit den Werten 4, 5, 1, 9, 7
Ein Jagged Array mit einer Zeile der Länge 3 und einer der Länge 5

Jagged Arrays eignen sich für Dreiecksformen. Das pascalsche Dreieck beispielsweise hat in Zeile n genau n + 1 Einträge; mit new int[4][] legst du nur das äußere Array an und gibst jeder Zeile ihre eigene Länge:

int[][] triangle = new int[4][];
for (int row = 0; row < triangle.length; row++) {
  triangle[row] = new int[row + 1];
  triangle[row][0] = 1;
  triangle[row][row] = 1;
  for (int col = 1; col < row; col++) {
    triangle[row][col] = triangle[row - 1][col - 1] + triangle[row - 1][col];
  }
}
System.out.println(Arrays.deepToString(triangle));
[[1], [1, 1], [1, 2, 1], [1, 3, 3, 1]]

Jede Zeile hat so viele Elemente, wie sie braucht – eine 4 × 4-Matrix hätte sechs Elemente verschwendet. Code, der intMatrix[0].length als Spaltenanzahl verwendet, ist für ein Jagged Array dagegen falsch; die innere Schleife muss die Länge jeder Zeile einzeln abfragen.

2D-Array kopieren

clone() kopiert ein Array – aber nur eine Ebene tief. Bei einem 2D-Array entsteht ein neues äußeres Array, dessen Referenzen auf dieselben Zeilen wie die des geklonten Arrays zeigen:

int[][] original = {{2, 3, 6}, {4, 5, 1}};

int[][] copy = original.clone();
copy[0][0] = 99;
System.out.println("original = " + Arrays.deepToString(original));
System.out.println("copy     = " + Arrays.deepToString(copy));
System.out.println("same row: " + (original[0] == copy[0]));
original = [[99, 3, 6], [4, 5, 1]]
copy     = [[99, 3, 6], [4, 5, 1]]
same row: true

Die Änderung an der Kopie ist auch im Original sichtbar, denn original[0] und copy[0] sind dasselbe Objekt:

Zwei äußere Arrays original und copy mit je zwei Referenzen, die auf dieselben zwei inneren Arrays mit den Werten 2, 3, 6 und 4, 5, 1 zeigen
clone() kopiert nur das äußere Array – beide zeigen auf dieselben Zeilen

Eine unabhängige Kopie – eine tiefe Kopie – klont jede Zeile einzeln:

int[][] deepCopy = new int[original.length][];
for (int row = 0; row < original.length; row++) {
  deepCopy[row] = original[row].clone();
}
deepCopy[0][0] = 42;
System.out.println("original = " + Arrays.deepToString(original));
System.out.println("deepCopy = " + Arrays.deepToString(deepCopy));
original = [[99, 3, 6], [4, 5, 1]]
deepCopy = [[42, 3, 6], [4, 5, 1]]

Die Änderung an der Kopie ist auf diese Weise nicht mehr im Original sichtbar.

Als Stream kann eine tiefe Kopie mit nur einer Anweisung erzeugt werden: map(int[]::clone) klont jede Zeile, toArray(int[][]::new) sammelt die Klone in einem neuen äußeren Array:

int[][] streamCopy = Arrays.stream(original)
    .map(int[]::clone)
    .toArray(int[][]::new);

Dieselbe Falle wie bei der Kopie gibt es beim Vergleich. Arrays.equals() vergleicht die Elemente des äußeren Arrays – also Referenzen auf die Zeilen –, und zwei getrennt erzeugte Matrizen mit gleichen Werten in den Zeilen sind damit ungleich. Erst Arrays.deepEquals() vergleicht die Werte in den Zeilen, und Arrays.deepHashCode() ist das passende Gegenstück für den Hash-Code:

int[][] a = {{1, 2}, {3, 4}};
int[][] b = {{1, 2}, {3, 4}};
System.out.println("Arrays.equals(a, b)     = " + Arrays.equals(a, b));
System.out.println("Arrays.deepEquals(a, b) = " + Arrays.deepEquals(a, b));
Arrays.equals(a, b)     = false
Arrays.deepEquals(a, b) = true

Die übrigen Kopiermethoden – System.arraycopy(), Arrays.copyOf() und Arrays.copyOfRange() – kopieren ebenfalls nur eine Ebene. Wie sie sich unterscheiden, zeigt der Artikel Arrays in Java initialisieren.

2D-Array sortieren

Auch Arrays.sort() arbeitet auf nur einer Ebene. Für ein 2D-Array hast du zwei sinnvolle Möglichkeiten: jede Zeile für sich sortieren oder die Zeilen anhand einer Spalte umordnen.

Jede Zeile für sich sortierst du beispielsweise in einer For-each-Schleife:

int[][] intMatrix = {{2, 3, 6}, {4, 5, 1}};
for (int[] row : intMatrix) {
  Arrays.sort(row);
}
System.out.println(Arrays.deepToString(intMatrix));
[[2, 3, 6], [1, 4, 5]]

Um die Zeilen umzuordnen, übergibst du Arrays.sort() zusammen mit dem äußeren Array einen Comparator. Das folgende Beispiel sortiert Punkte, deren erste Spalte die x-Koordinate ist, nach dieser Koordinate – und danach absteigend nach der zweiten Spalte:

int[][] points = {{3, 7}, {1, 9}, {2, 4}};

Arrays.sort(points, Comparator.comparingInt(point -> point[0]));
System.out.println(Arrays.deepToString(points));

Arrays.sort(points, Comparator.comparingInt((int[] point) -> point[1]).reversed());
System.out.println(Arrays.deepToString(points));
[[1, 9], [2, 4], [3, 7]]
[[1, 9], [3, 7], [2, 4]]

Beim zweiten Aufruf steht der Typ int[] am Lambda-Parameter, denn ohne ihn kann der Compiler den Typ nicht ableiten, sobald reversed() hinter comparingInt() folgt.

Die Sortierverfahren hinter Arrays.sort() und die Alternativen für Listen und Streams beschreibt der Artikel Sortieren in Java.

3D-Arrays und mehr Dimensionen

Das Prinzip setzt sich fort: Ein int[][][] ist ein Array von int[][], also ein Array von Arrays von Arrays. Jedes weitere Paar von eckigen Klammern fügt eine Ebene von Referenzen hinzu:

int[][][] cube = new int[2][3][4];
cube[1][2][3] = 42;
System.out.println(Arrays.deepToString(cube));
System.out.println(cube.length + " x " + cube[0].length + " x " + cube[0][0].length);
[[[0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0]], [[0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 42]]]
2 x 3 x 4

Arrays.deepToString() und Arrays.deepEquals() steigen beliebig tief hinab, und auch das Literal verschachtelt sich weiter: {{{1, 2}, {3, 4}}, {{5, 6}, {7, 8}}} ist ein 2 × 2 × 2-Array.

Kennst du die Anzahl der Dimensionen erst zur Laufzeit, hilft Array.newInstance() aus java.lang.reflect. Die Methode nimmt den Elementtyp und beliebig viele Längen entgegen und liefert ein Object, das du auf den Array-Typ castest:

int[][] viaReflection = (int[][]) Array.newInstance(int.class, 2, 3);
System.out.println(Arrays.deepToString(viaReflection));
[[0, 0, 0], [0, 0, 0]]

In der Praxis bleibt es meist bei zwei, selten drei Dimensionen. Wer mehr braucht, hat in der Regel eine Datenstruktur vor sich, die als Klasse mit sprechenden Feldern lesbarer ist als als int[][][][].

Speicher und Performance: 2D-Array oder flaches Array?

Statt als 2D-Array kannst du eine Matrix auch als flaches Array speichern: als ein einziges eindimensionales int[r * c] für r Zeilen und c Spalten, in dem die Zeilen hintereinander liegen. Das Element in Zeile row und Spalte col liegt dann am Index row * c + col:

Eine 3 × 4-Matrix als drei Zeilen und darunter als flaches Array mit zwölf Elementen; das Element in Zeile 1, Spalte 2 ist in beiden hervorgehoben und liegt im flachen Array am Index 1 · 4 + 2 = 6
Eine 3 × 4-Matrix als flaches Array: Zeile für Zeile hintereinander, Element [1][2] am Index 6

Oben die Matrix mit drei Zeilen, darunter das flache Array mit denselben zwölf Werten. Das hervorgehobene Element steht in Zeile 1, Spalte 2 – im flachen Array am Index 1 · 4 + 2 = 6.

Der folgende Code kopiert die Matrix Zeile für Zeile in ein flaches Array und liest das Element in Zeile 1, Spalte 2 auf beiden Wegen:

int[][] matrix = {{2, 3, 6, 4}, {5, 1, 9, 7}, {8, 0, 3, 2}};
int rows = matrix.length;
int columns = matrix[0].length;

int[] flat = new int[rows * columns];
for (int row = 0; row < rows; row++) {
  for (int col = 0; col < columns; col++) {
    flat[row * columns + col] = matrix[row][col];
  }
}
System.out.println("flat = " + Arrays.toString(flat));
System.out.println("matrix[1][2] = " + matrix[1][2]);
System.out.println("flat[1 * 4 + 2] = " + flat[1 * columns + 2]);
flat = [2, 3, 6, 4, 5, 1, 9, 7, 8, 0, 3, 2]
matrix[1][2] = 9
flat[1 * 4 + 2] = 9

Ein 2D-Array mit n Zeilen besteht aus n + 1 Objekten, jedes mit eigenem Header und Längenfeld. Ein int[2][3] belegt auf dem Heap 72 Bytes, ein flaches int[6] mit denselben Werten 40 Bytes – wie sich diese Zahlen zusammensetzen, rechnet der Artikel Array-Länge in Java vor. Bei langen Zeilen fällt der Unterschied dagegen nicht mehr ins Gewicht: Eine 4.096 × 4.096-Matrix belegt als 2D-Array 80 KB mehr als ihre 64 MB Nutzdaten.

Entscheidender als der Speicher ist, wie du die Matrix durchläufst. Jede Zeile liegt als Array am Stück im Speicher; wer sie zeilenweise liest, bekommt die nächsten Werte aus der CPU-Cache-Line, die der erste Zugriff geladen hat. Wer spaltenweise liest – die äußere Schleife über die Spalten und die innere Schleife über die Zeilen –, springt bei jedem Zugriff in ein anderes Array und damit in eine andere Cache-Line.

Wie groß der Unterschied ist, habe ich mit JMH gemessen. Der Benchmark berechnet die Summe aller Elemente einer quadratischen int-Matrix – zeilenweise und spaltenweise, als 2D-Array und als flaches Array. Die Tabelle zeigt die Zeit pro Durchlauf in Mikrosekunden; das flache Array ist nur zeilenweise aufgeführt, warum, steht nach der Tabelle.

Zeilen × Spalten2D zeilenweise2D spaltenweiseFlach zeilenweise
64 × 64 (16 KB)0,32 µs1,08 µs0,35 µs
1.024 × 1.024 (4 MB)184 µs526 µs181 µs
4.096 × 4.096 (64 MB)3.992 µs27.594 µs3.635 µs

Aus der Tabelle lassen sich zwei Ergebnisse ablesen:

  • Zeilenweise gegen spaltenweise: Spaltenweise dauert der Durchlauf bei der kleinen Matrix 3,4-mal so lange wie zeilenweise, bei der großen 6,9-mal: 27,6 statt 4,0 Millisekunden. Die 64 MB sind viermal so groß wie der L2-Cache des M5 Pro mit 16 MB, und die spaltenweise Schleife springt bei jedem Zugriff in eine andere Zeile – und damit in eine andere Cache-Line, von der sie zunächst nur 4 Bytes verwendet.
  • Flaches Array gegen 2D-Array: Zeilenweise ist das flache Array bei der großen Matrix 9 % schneller als das 2D-Array, bei der mittleren gleich schnell und bei der kleinen 10 % langsamer – dort kostet die Multiplikation für den Index mehr, als der gesparte Zeilenzugriff bringt.

Spaltenweise habe ich das flache Array ebenfalls gemessen. Bei 4.096 Spalten dauert der Durchlauf 51,5 ms, fast doppelt so lange wie beim 2D-Array. Bei 4.100 Spalten sind es nur noch 21,6 ms – obwohl die Elemente einer Spalte dann sogar weiter auseinanderliegen.

Der Wert bei 4.096 Spalten zeigt eine Eigenschaft des Caches, nicht des Arrays, und fehlt deshalb in der Tabelle. Wie der Cache ihn verursacht, erklärt die folgende Hinweisbox.

Meine Empfehlung: Nimm das 2D-Array. Es ist lesbar, die Zeilen lassen sich einzeln herausgreifen, und zeilenweise durchlaufen weicht es in der Messung höchstens 10 % vom flachen Array ab. Das flache Array lohnt sich, wenn du Millionen kleiner Matrizen hältst – dort sparst du pro Matrix die Header der Zeilen – oder wenn du die Daten ohnehin als einen Block an eine Bibliothek oder einen ByteBuffer übergibst. In beiden Fällen gehört die Index-Berechnung in eine Klasse mit get(row, col) und set(row, col, value), damit sie nur an einer Stelle steht.

Häufige Fehler

  • Schleifen in der falschen Reihenfolge. Die innere Schleife läuft über die Zeilen statt über die Spalten; bei 64 MB dauert der Durchlauf dann 6,9-mal so lange.
  • Arrays.fill() mit einer Zeile. Alle Zeilen zeigen auf dasselbe innere Array; richtig ist Arrays.setAll() mit einem Lambda, das für jede Zeile ein neues Array erzeugt.
  • clone() als tiefe Kopie. Die Kopie teilt sich die Zeilen mit dem Original; klone stattdessen jede Zeile einzeln.
  • Arrays.equals() für zwei Matrizen. Verglichen werden die Referenzen auf die Zeilen, nicht deren Werte; für die Werte brauchst du Arrays.deepEquals().
  • Arrays.toString() für die Ausgabe. Das Ergebnis sind Zeilen-Referenzen wie [I@7ad041f3; für die Werte nimm Arrays.deepToString().
  • intMatrix[0].length bei einem Jagged Array. Die Länge der ersten Zeile gilt nicht für die anderen Zeilen; die innere Schleife muss die jeweilige Zeilenlänge mit intMatrix[row].length abfragen.

Zusammenfassung

Ein 2D-Array in Java ist ein Array von Arrays: Das äußere Array hält eine Referenz pro Zeile, jede Zeile ist ein eigenes eindimensionales Array. Du deklarierst ein 2D-Array mit int[][], initialisierst es mit einem Literal wie {{2, 3, 6}, {4, 5, 1}}, mit new int[2][3] für Standardwerte oder mit new int[2][] und einer Zuweisung pro Zeile. intMatrix.length ist die Anzahl der Zeilen, intMatrix[row].length die Länge einer Zeile – und die darf sich von Zeile zu Zeile unterscheiden.

Die Methoden der Klasse Arrays arbeiten auf nur einer Ebene, ebenso clone(): fill(), setAll(), sort() und clone() musst du pro Zeile aufrufen, und für Ausgabe und Vergleich nimmst du deepToString() und deepEquals(). Durchlaufen wird die Matrix zeilenweise; bei einer 4.096 × 4.096-Matrix braucht die spaltenweise Schleife 27,6 statt 4,0 Millisekunden.

Meine Empfehlungen:

  • Schreib die Klammern an den Typ: int[][] intMatrix.
  • Lass die äußere Schleife über die Zeilen und die innere Schleife über die Spalten laufen, mit intMatrix[row].length als Grenze.
  • Fülle die Zeilen mit Arrays.setAll() oder einer Schleife – nie mit Arrays.fill() auf dem äußeren Array.
  • Klone jede Zeile einzeln, wenn du eine unabhängige Kopie brauchst.
  • Nimm ein flaches Array nur für Millionen kleiner Matrizen oder für Daten, die als ein Block weitergereicht werden – und kapsle dann die Index-Berechnung.

Wie du eindimensionale Arrays anlegst, füllst und kopierst, zeigt der Artikel Arrays in Java initialisieren; wie viel Speicher ein Array belegt und wie groß es werden kann, der Artikel Array-Länge in Java. Ein Algorithmus, der eine Matrix als 2D-Array verwendet, ist der Floyd-Warshall-Algorithmus – mit einer Distanzmatrix als int[][].

Wenn dir dieser Artikel gefallen hat, freue ich mich über eine Bewertung auf meinem ProvenExpert-Profil.

👉 Bewertung abgeben

Lust auf noch mehr Wissen?

In meinem Blog findest du viele Artikel zu Java, Softwarearchitektur und Performance von grundlegenden Konzepten bis hin zu fortgeschrittenen Patterns.

Wenn du tiefer einsteigen willst, schau dir meine Trainings an: praxisnah, verständlich und direkt auf euren Projektalltag übertragbar. Statt Theorie vermittle ich Prinzipien, die euch helfen, Code langfristig besser, wartbarer und performanter zu schreiben.

Zu den Java-Schulungen

Werde ein:e bessere:r Java-Entwickler:in

Mit meinem kostenlosen Newsletter bleibst du vorn. Modernes Java: neue Versionen & Features, Performance und JVM-Insights – 1x im Monat.

Suche