Table of contents
Text Link

Multidimensional arrays in Java

What's an array? It is a collection that contains and structures different kinds of data in individual cells—a one-dimensional array stores other elements: numbers, text, or items. A multidimensional (or n-dimensional array) contains arrays of arrays (matrices). To understand multidimensional arrays better, let's look at one-dimensional ones and stream first.

In Java programming, the simplest arrays are one-dimensional (1d) or two-dimensional arrays (2d), but they can have more dimensions. Arrays can be three-dimensional (3d), 4d, 5d, or more. Multidimensional can be used for vectors or table-oriented data. You can also use it for methods returning more than one array or a board in the tic-tac-toe game.

In Java, each field can contain only one type of data. You can’t make an array containing numbers and text simultaneously. However, each subarray can have different sizes.

We would focus primarily on 1-, 2-, and 3d arrays in the following examples.

Creating arrays

You can create an array by specifying all sizes without data. 1D would have only one size. In that situation, fields are filled with default values. Each data type has its default values:

  • 0 for int
  • 0,0 for double
  • ‘\u0000’ for chars
  • null for String, custom objects, nullable objects

You don’t have to write the whole type (for instance, int[][]). Instead, you can use the var keyword. This keyword was introduced in Java 10. It can be helpful for an array with a long name data type, for instance, custom objects or classes.

int[] simpleNumbers = new int[3];
int[][] numbers = new int[3][4];
var anotherNumbers = new double[1][10];
var texts = new String[4][6];
char[][][] signs = new char[2][2][2];

In the example above, we have created one-dimensional, three 2d arrays, and one 3d arrays. You can see below what it contains.


For a one-dimensional empty array, you must indicate the size. However, you don’t have to write all sizes for multidimensional ones. Especially when you would like to have subarrays in different sizes. You have to specify at least one size, but not all. In that situation, fields would be filled with nulls. It doesn’t matter which type of data. Even a double array would get null.

double[][] nullNumbers = new double[1][];
char[][][] nullSigns = new char[1][1][];

In the example above, we are creating two arrays that would contain only nulls. If you try to get “nullNumbers[0][0]”, you will get NullPointerException. It’s because nullNumbers contains a null array.

You can also create an array with specified data. Specify the type, for example, int[][]. However, you can use the var keyword, but in this case, add a type next to data, for example, new int[][]. And also, subarrays can have different sizes. As you can see in the numbers array below, the first has two array elements, the second has three, and the last contains four.

var texts = new String[]{"test", "sth", "here"};
var a = new int[][]{{0, 1, 2}, {3, 4, 5}, {6, 7, 8}};
int[][] numbers = {{0, 1}, {0, 1, 2}, {0, 1, 2, 3}};
char[][][] signs = {{{'a'}}, {{'b'}}};

Accessing data

You can access every field or whole subarray. You need indexes to get a specific field. Remember that indexes start on 0 in Java. The table below displays fields from the texts array. It is a 1D, so we need only one index to access the field.

texts[0]= ”test”

texts[1] = “sth”

texts[2] = “here”

The next table displays what the a array contains. 

a[0][0] = 0

a[0][1] = 1

a[0][2] = 2

a[1][0] = 3

a[1][1] = 4

a[1][2] = 5

a[2][0] = 6

a[2][1] = 7

a[2][2] = 8

We need two indexes to get the field. The bigger the array dimension, the more complex the array indexing you need. For a 3-dimensional array, you would need 3, and 4d would require 4. If we use only one index for the a array, we will get the row size of a one-dimensional array. That’s why “a[0]” equals {0, 1, 2}.

int[][] numbers = {{0, 1, 2}, {3, 4}, {5, 6, 7, 8}};
System.out.println(numbers[1][0]);
System.out.println(numbers[1].length);

The above code will output the following numbers. 3 is the value from the field. And 2 is the subarray’s size.

3
2

You must be very careful when accessing the field or subarrays. You will get an ArrayIndexOutOfBoundsException when you use an incorrect index. It can be a number less than 0, more prominent, or equal to the array size.

Loops and streams

You would need only one loop or stream to access one-dimensional array values.

For 2d arrays, you need two loops. For 3d one, more loops are required. The more dimensions an array contains, the more loops are needed. However, a stream could also be helpful. This will shorten the code.

In loops, we have used the length method. The code is more transparent and will work for different size subarrays.

int[] negatives = {-1, -2, -3, -4};
System.out.println("---- negatives (a loop):");
for (int field: negatives) {
    System.out.print(field + " ");
}
int[][] numbers = {{0, 1, 2}, {3, 4}, {5, 6, 7, 8}};
System.out.println("\n\n---- numbers (2 loops):");
for (int y = 0; y < numbers.length; y++) {
    for (int x = 0; x < numbers[y].length; x++) {
        System.out.print(numbers[y][x] + " ");
    }
 }

Output

---- negatives (a loop):
-1 -2 -3 -4 

---- numbers (2 loops):
0 1 2 3 4 5 6 7 8 

For streams, we use the forEach method to execute code for every element/subarray in the array. Many methods can help you map or filter data in it.

int[] negatives = {-1, -2, -3, -4};
System.out.println("---- negatives (a stream):");
Arrays.stream(negatives).forEach(n -> System.out.print(n + " "));

int[][] numbers = {{0, 1, 2}, {3, 4}, {5, 6, 7, 8}};
System.out.println("\n---- numbers (loop and stream):");
for (var row : numbers) {
    Arrays.stream(row).forEach(n -> System.out.print(n + " "));
}

Output

---- negatives (a loop):
-1 -2 -3 -4 
---- numbers (loop and stream):
0 1 2 3 4 5 6 7 8 

You can transfer arrays to a stream when you want more methods to manipulate arrays. For the example above, we use the forEach method to execute code for every element/subarray in the array.

Updating data

You can update a specific field, a subarray, or the whole array. Updated ones can have different sizes.

int[] negatives = {-1, -2, -3, -4};
negatives[1] = -22;
System.out.println("\n---- negatives (updated):");
Arrays.stream(negatives).forEach(n -> System.out.print(n + " "));
negatives = new int[]{-10, -20, -30, -40, -50};
System.out.println("\n---- negatives (replaced all):");
Arrays.stream(negatives).forEach(n -> System.out.print(n + " "));

Output

---- negatives (updated):
-1 -22 -3 -4 
---- negatives (replaced all):
-10 -20 -30 -40 -50 

Now, we can update the 2d array. It looks similar to a 1d array. Updated arrays can have different sizes.

private final int[][] table = {{0, 1, 2}, {3, 4}, {5, 6, 7, 8}};

private void updateData() {
    var numbers = table;
    table[0][2] = -2;
    table[2] = new int[]{-5, -6, -7};
    System.out.println(numbers[0][2]);
    System.out.println(numbers[2][1]);
}

Output

-2
-6

In the example above, you can see the final keyword. The whole array can be final even if you update a field or subarray. You can’t replace the whole final one, though. You must delete the final keyword to replace the entire array.

Copy array

Arrays in Java are reference types. It means that the variable is not an actual array but an address to it. That’s why assigning wouldn’t be enough to copy the whole array. As you can see in the previous example, updates to the table variable were also applied to the numbers variable. It works similarly to a 1d array.

int[] simpleNumbers = {1, 2, 3, 4};
var newNumbers = simpleNumbers;
var copiedNumbers = Arrays.copyOf(simpleNumbers, simpleNumbers.length);
var lessNumbers = Arrays.copyOf(simpleNumbers, simpleNumbers.length - 1);
var moreNumbers = Arrays.copyOf(simpleNumbers, simpleNumbers.length + 3);
simpleNumbers[0] = -1;
System.out.println("\n---- newNumbers:");
Arrays.stream(newNumbers).forEach(n -> System.out.print(n + " "));
System.out.println("\n---- copiedNumbers:");
Arrays.stream(copiedNumbers).forEach(n -> System.out.print(n + " "));
System.out.println("\n---- lessNumbers:");
Arrays.stream(lessNumbers).forEach(n -> System.out.print(n + " "));
System.out.println("\n---- moreNumbers:");
Arrays.stream(moreNumbers).forEach(n -> System.out.print(n + " "));

Output

---- newNumbers:
-1 2 3 4 
---- copiedNumbers:
1 2 3 4 
---- lessNumbers:
1 2 3 
---- moreNumbers:
1 2 3 4 0 0 0 

As you can see in newNumbers, we have updated the field with Arrays.copyOf. This method has two arguments. The first is an array that we want to copy. The second one is the length of our new array. That’s what we can do when we want to change the size of our array. We have to copy it to the array with a different size. The lessNumber is shorter and doesn’t contain the last field. MoreNumbers has two extra fields at the end filled by default values.

Let’s try to copy the 2d array. Use two different methods.

int[][] numbers = {{0, 1, 2}, {3, 4}, {5, 6, 7, 8}};
var clone = numbers.clone();
var copy = Arrays.copyOf(numbers, numbers.length);
numbers[0][2] = -2;

System.out.println("numbers[0][2]: " + numbers[0][2]);
System.out.println("clone[0][2]: " + clone[0][2]);
System.out.println("copy[0][2]: " + copy[0][2]);

As you can see, all arrays have been updated. That’s why we need to copy each subarray separately.

You can use a loop for it. First, you must create an empty array with a specified first size. Then you would go through every subarray and copy it. You can use the clone or copyOf method. This would prevent updating the array assigned to the copy variable.

int[][] numbers = {{0, 1, 2}, {3, 4}, {5, 6, 7, 8}};
var clone = new int[numbers.length][];
for (int i = 0; i < numbers.length; i++) {
   clone[i] = numbers[i].clone();
}
var copy = new int[numbers.length][];
for (int i = 0; i < numbers.length; i++) {
   copy[i] = Arrays.copyOf(numbers[i], numbers.length);
}
numbers[0][2] = -2;
System.out.println("numbers[0][2]: " + numbers[0][2]);
System.out.println("clone[0][2]: " + clone[0][2]);
System.out.println("copy[0][2]: " + copy[0][2]);

As you can see, only the first array has been updated. That means that we were able to copy the array successfully.

numbers[0][2]: -2
clone[0][2]: -2
copy[0][2]: -2

You can also use stream instead of loop. It will shorten the code, but you can't ignore the readability.

int[][] numbers = {{0, 1, 2}, {3, 4}, {5, 6, 7, 8}};
var clone = Arrays.stream(numbers)
   .map(arr -> arr.clone()).toArray(n -> numbers.clone());

numbers[0][2] = -2;
System.out.println("numbers[0][2]: " + numbers[0][2]);
System.out.println("clone[0][2]: " + clone[0][2]);

Output

numbers[0][2]: -2
clone[0][2]: 2

This works perfectly for primitive data, like numbers or text. However, it wouldn’t work so well for custom objects. In that situation, you must go through every object and copy them.

class Sample {
            private int x;

            public Sample(int size) {
                this.x = size;
            }

            public Sample(Sample sample) {
                this.x = sample.x;
            }

            public int getX() {
                return x;
            }

            public void setX(int x) {
                this.x = x;
            }
        }

        Sample[][] samples = {{new Sample(1), new Sample(2)}, {new Sample(3)}};

        var cloneSamples = new Sample[samples.length][];
        for (int i = 0; i < samples.length; i++) {
            cloneSamples[i] = samples[i].clone();
        }

        var cloneBSamples = new Sample[samples.length][];
        for (int i = 0; i < samples.length; i++) {
            cloneBSamples[i] = Arrays.stream(samples[i]).map(Sample::new).toArray(Sample[]::new);
        }

        samples[0][0].setX(-5);
        System.out.println("samples[0][0]: x = " + samples[0][0].getX());
        System.out.println("cloneSamples[0][0]: x = " + cloneSamples[0][0].getX());
        System.out.println("cloneBSamples[0][0]: x = " + cloneBSamples[0][0].getX());

Output

samples[0][0]: x = -5
cloneSamples[0][0]: x = -5
cloneBSamples[0][0]: x = 1

As you can see in cloneSamples, we haven't copied each object separately. As a result, updating the object affects both arrays: samples and cloneSamples. In cloneBSamples, we have created a perfect copy. We copied each object separately and didn’t have the updated object.

Sorting data

First, let’s look at how we can sort a 1d array.

int[] simpleNumbers = {3, -1, 7, 4, 0};
Arrays.sort(simpleNumbers);
System.out.println("\n---- simpleNumbers (sorted):");
Arrays.stream(simpleNumbers).forEach(n -> System.out.print(n + " "));

Output

---- simpleNumbers (sorted):
-1 0 3 4 7

Sorting a multidimensional array would look very similar. You can easily sort by subarrays. You can use an existing comparator, as in the example below, or write your comparator.

int[][] numbers = {{7, 6, 2}, {1, 3}, {0, 1, -4, 2}};
Arrays.sort(numbers[0]);
Arrays.sort(numbers, Comparator.comparingInt(a -> a.length));
Arrays.sort(numbers, 
        Comparator.comparingInt(a -> Arrays.stream(a).sum()));

This is how the array would look after first sorting. We sorted only the first subarray, not the whole array.

Then, we sorted the whole array by the subarray size.

The array would be updated after the subsequent sorting. We sorted by a sum of all subarrays’ elements.

Conclusion

In this article, we have shown you how to create, update, and copy arrays. Many operations on multidimensional arrays are similar to 1d arrays. Remember, you should be very careful when you try to copy an array. It can be very tricky because of the reference nature of arrays. Loops and streams can be beneficial.

Related Hyperskill topics

Share this article
Get more articles
like this
Thank you! Your submission has been received!
Oops! Something went wrong.

Create a free account to access the full topic

Wide range of learning tracks for beginners and experienced developers
Study at your own pace with your personal study plan
Focus on practice and real-world experience
Andrei Maftei
It has all the necessary theory, lots of practice, and projects of different levels. I haven't skipped any of the 3000+ coding exercises.