Arrays in Java — from Python lists to fixed size, indexes and references

I still remember the first time one of my Java programs stopped dead with a red message saying ArrayIndexOutOfBoundsException. It was a loop going through an array of grades, and I was convinced the code was perfect: I had written i <= grades.length. I came from Python, where a mistake like that would have given me an IndexError and that would have been it, but Java made me stop and understand why the last index is length - 1. It took me a good while to realise the problem wasn’t the loop, it was that I hadn’t understood what an array is underneath. This article is so it doesn’t happen to you.

We’ll see what an array is, how to create it, how to loop over it, why two “equal” arrays sometimes aren’t, and what the Arrays class does. If you’re coming from Python, we’ll compare with lists and tuples along the way, because that’s where half the confusion comes from. And if you haven’t yet seen how methods work, it’s worth going through Methods in Java first: we’ll use them at the end.

What an array is (and how it compares to a Python list)

An array is a collection of values of the same type, stored one after another, with a fixed size that is decided when you create it. Think of an egg carton: it has a set number of slots, they all hold the same kind of thing (eggs), and every slot has a position.

Compared with Python, keep these three differences in mind:

  • All elements have the same type. An int[] only holds integers. In a Python list you can mix numbers and text; here you can’t.
  • The size is fixed. There’s no append or remove. If you need more slots, you have to create a bigger array. (For a variable size, Java has ArrayList, which you’ll meet later in collections.)
  • Elements can be modified. In that sense it’s like a list, not like a tuple.

A summary to keep handy:

Python listPython tupleJava array
Mixed typesYesYesNo, a single type
Changes sizeYesNoNo
Elements can be modifiedYesNoYes

Declaring and creating an array

In Java you have to tell apart two steps that you don’t see in Python: declaring the variable (saying what kind of array it is) and creating the array (reserving the slots). The type is followed by square brackets:

int[] grades;   // declaration: "grades" will be an array of integers (it doesn't exist yet)

There are two ways to create it. The first is to give the values directly, between curly braces:

int[] grades = {7, 5, 9, 4, 10};   // 5 slots, already filled in

The second is to give only the size with new, and fill it in afterwards:

int[] grades = new int[5];   // 5 slots, all of them are 0
grades[0] = 7;
grades[1] = 5;

And what do the slots hold if I don’t fill them?

Java never leaves a slot “without a value”: when you create the array with new, every element gets the default value of its type:

Element typeDefault value
int, long, short, byte0
double, float0.0
booleanfalse
charthe null character ('\u0000')
String and any objectnull (“points to nothing”)
int[] integers = new int[3];
boolean[] flags = new boolean[3];
String[] names = new String[3];

System.out.println(integers[0]);   // 0
System.out.println(flags[0]);      // false
System.out.println(names[0]);      // null

A freshly created String array doesn’t contain empty strings: it contains null. If you try names[0].length() before assigning anything to it, you’ll get a NullPointerException. It’s a classic mistake.

Accessing elements: indexes and length

Just like in Python, indexes start at 0. In an array of 5 elements, the valid positions are 0, 1, 2, 3 and 4.

int[] grades = {7, 5, 9, 4, 10};

int first = grades[0];        // 7
grades[3] = 6;                // now the array is {7, 5, 9, 6, 10}

int howMany = grades.length;  // 5
int last = grades[grades.length - 1];   // 10

Notice that length has no parentheses: in Python you wrote len(list), here it’s grades.length. It’s an attribute of the array, not a method. (With String it’s the other way round: text.length() does have parentheses. One of Java’s little traps.)

And an important difference from Python: negative indexes don’t exist. In Python list[-1] is the last element; in Java, grades[-1] is an error. For the last one you have to write grades[grades.length - 1].

ArrayIndexOutOfBoundsException. If you ask for a position that doesn’t exist, the program stops:

grades[5] → Index 5 out of bounds for length 5

With 5 elements, the last index is 4, not 5. The rule: the last index is always length - 1. It’s exactly what happened to me with that loop; you’ll see it calmly in the next section.

Looping over an array

There are two ways, just like in Python: by index or by value.

By index: the classic for

int[] grades = {7, 5, 9, 4, 10};

for (int i = 0; i < grades.length; i++) {
    System.out.println("Position " + i + ": " + grades[i]);
}

What matters is the condition: i < grades.length, with strictly less than. The last iteration happens with i = 4, and after adding one, i is 5 and the loop stops. If you write i <= grades.length, there’s an extra iteration with i = 5 and the exception from before appears.

The most common mistake when looping over arrays: writing <= instead of < in the for condition. The program compiles without any problem and fails when it runs, right on the last iteration.

By value: the for-each

If you only need to read each value and you don’t care about its position, Java has a shorter version, equivalent to Python’s for value in list:

int sum = 0;
for (int grade : grades) {      // "for each grade in grades"
    sum += grade;
}
System.out.println("Sum: " + sum);

You read it as: “for each int called grade inside grades“. The colon : replaces Python’s word in.

Which one do I use? If you need the index (to know the position, to walk two arrays at once, or to modify elements), use the classic for. If you’re only going to read the values, use the for-each. It has a catch: in a for-each over int, the variable grade is a copy of each value, so doing grade = 0 inside the loop doesn’t change the array.

The point that confuses people most: an array is a reference

This is the hardest thing at first and, at the same time, what will save you the most mistakes if you understand it well. Look at this code and think about what you expect it to print:

int[] grades = {7, 5, 9};
int[] copy = grades;      // does this copy the array?
copy[0] = 99;

System.out.println(grades[0]);   // 7 or 99?

It prints 99. And it isn’t a bug: it’s how arrays work. A variable of type int[] doesn’t hold the numbers: it holds the address where the array is in memory. It’s like the address of a house. When you write int[] copy = grades;, you copy the address, not the house. Now there are two variables pointing to the same house, and if someone moves the furniture, both of them see it.

With plain numbers it doesn’t happen: int b = a; copies the value and from then on they’re independent. Arrays (and all objects) behave differently. If you want a real copy, you have to create another array:

import java.util.Arrays;

int[] grades = {7, 5, 9};
int[] copy = Arrays.copyOf(grades, grades.length);   // new, independent array
copy[0] = 99;

System.out.println(grades[0]);   // 7  (the original doesn't change)
System.out.println(copy[0]);     // 99

Why == doesn’t work to compare arrays

The same idea explains another stumble. The == operator compares addresses, not contents:

int[] a = {1, 2, 3};
int[] b = {1, 2, 3};

System.out.println(a == b);                 // false  (they are two different arrays)
System.out.println(Arrays.equals(a, b));    // true   (same contents)

It’s the same story as == versus .equals() with String: to compare contents, you use a method. For arrays, Arrays.equals(a, b).

And if I print an array directly, what comes out?

int[] grades = {7, 5, 9};
System.out.println(grades);    // [I@49476842  (not the numbers!)

You get something like [I@49476842: the array’s type ([I means “array of int”) and a memory address. To see the contents, use Arrays.toString(grades), which gives [7, 5, 9].

Printing an array with println(array). If you see something like [I@6d06d69c, it isn’t a bug in your program: you just need Arrays.toString(...).

This idea of “the variable holds an address” will come back with classes and objects: String, and any object you create, work the same way. If you understand it here with arrays, you’re already halfway through that topic.

Arrays and methods

What we’ve just seen has a direct consequence if you already know methods: when you pass an array to a method, the address is passed, not a copy. That is, the method can modify your original array.

static void raiseByOne(int[] data) {
    for (int i = 0; i < data.length; i++) {
        data[i]++;                     // modifies the original array
    }
}

public static void main(String[] args) {
    int[] grades = {7, 5, 9};
    raiseByOne(grades);
    System.out.println(Arrays.toString(grades));   // [8, 6, 10]
}

With a single int this doesn’t happen (the method receives a copy of the value). With an array, it does. It’s very useful, but also a source of mistakes: if a method “sorts” or “changes” your array without warning, you’ll be surprised later. A good habit is to make it clear in the method name whether it modifies or only calculates (raiseByOne modifies; average only calculates).

Multi-dimensional arrays

In Python, a table is represented with a list of lists. In Java, with an array of arrays, declared with one pair of square brackets per dimension:

int[][] table = {
    {10, 15, 20, 30, 40},
    {12, 14, 16, 18},
    {11, 17, 23, 29, 31}
};

System.out.println(table[1][3]);   // 18  → row 1, column 3

The way to think about it: table is an array of 3 elements, and each element is itself an array of integers. That’s why table[1] is the whole second row, and table[1][3] is element 3 of that row.

You can also create an empty table by giving rows and columns:

int[][] table = new int[2][3];   // 2 rows, 3 columns, everything at 0
table[1][2] = 5;
System.out.println(Arrays.deepToString(table));   // [[0, 0, 0], [0, 0, 5]]

Notice that to print a table you use deepToString (“deep”), because the plain toString would only know how to show the addresses of the rows.

Since each row is an independent array, rows can have different lengths (in the example above, 5, 4 and 5 elements). That’s why, when looping, the length of each row is asked with table[i].length and not with a single common length.

Looping over a table

You need two nested loops: the outer one goes through the rows, the inner one goes through the elements of each row.

// By index
for (int row = 0; row < table.length; row++) {
    for (int col = 0; col < table[row].length; col++) {
        System.out.print(table[row][col] + " ");
    }
    System.out.println();   // new line after finishing each row
}

// By value
for (int[] row : table) {           // each "row" is an int[]
    for (int value : row) {
        System.out.print(value + " ");
    }
    System.out.println();
}

In the for-each, the outer variable must be of the type one level down: if table is int[][], each row is an int[]. It’s the only rule to remember so you don’t get the types wrong.

The Arrays class: ready-made tools

Java comes with a class called Arrays (in the java.util package, which is why you have to write import java.util.Arrays; at the top of the file) with very common operations that aren’t worth programming by hand. The main ones:

MethodWhat it does
Arrays.toString(a)Returns the contents as text: "[10, 15, 20]"
Arrays.sort(a)Sorts the array from smallest to largest (modifies the original)
Arrays.binarySearch(a, x)Looks for x in an already sorted array and returns its position
Arrays.copyOf(a, n)Creates a new array of length n with the data from a
Arrays.copyOfRange(a, from, to)Copies a slice (to is not included)
Arrays.equals(a, b)Compares the contents of two arrays
Arrays.fill(a, x)Fills the whole array with the value x
import java.util.Arrays;

public class Main {
    public static void main(String[] args) {
        int[] data = {80, 70, 60, 50, 40, 30, 20, 10};

        Arrays.sort(data);
        System.out.println(Arrays.toString(data));
        // [10, 20, 30, 40, 50, 60, 70, 80]

        System.out.println(Arrays.binarySearch(data, 30));   // 2  (it's at position 2)
        System.out.println(Arrays.binarySearch(data, 35));   // -4 (it's not there)

        int[] slice = Arrays.copyOfRange(data, 2, 5);
        System.out.println(Arrays.toString(slice));          // [30, 40, 50]

        int[] bigger = Arrays.copyOf(data, 10);              // the new slots are filled with 0
        System.out.println(Arrays.toString(bigger));
        // [10, 20, 30, 40, 50, 60, 70, 80, 0, 0]

        int[] sevens = new int[5];
        Arrays.fill(sevens, 7);
        System.out.println(Arrays.toString(sevens));         // [7, 7, 7, 7, 7]
    }
}

Why does binarySearch return -4?

When the value isn’t there, binarySearch returns -(insertion point) - 1, where the “insertion point” is the position where it should be to keep the order. Searching for 35 in [10, 20, 30, 40, ...], it would go at position 3 (between 30 and 40): -3 - 1 = -4. The practical takeaway: if the result is negative, it isn’t there; if it’s 0 or more, that’s its position.

binarySearch only works if the array is already sorted. If you use it on an unsorted array, it doesn’t give an error, but the result means nothing. Sort first with Arrays.sort.

Another catch: Arrays.equals compares only one level. If the elements are themselves arrays (a table), use Arrays.deepEquals, just like deepToString for printing.

Questions you might be asking

Can I add an element to an array?

No: the size is fixed. What you do is create a bigger array (with Arrays.copyOf(a, a.length + 1)) and put the new value in the last slot. If you need to do this often, it’s a sign you want an ArrayList.

Why do I have to say the size when I create it?

Because Java reserves a contiguous block of memory of that size. That’s what makes accessing grades[3] so fast: it knows exactly where it is without searching.

What’s the difference between an empty array and null?

new int[0] is a real array with zero elements (its length is 0 and you can loop over it with no problem). null means the variable doesn’t point to any array: if you do .length on it, you get a NullPointerException.

Can I create an array with a variable size, like new int[n]?

Yes. The size can be any expression, such as a variable read with Scanner. What can’t change is the size after it’s been created. If n is negative, it fails with NegativeArraySizeException.

A complete example

Let’s put everything together in a small program: it calculates the highest grade and the average of a group’s grades using methods, and then the average of each group in a table. Read the comments as you go; they explain the reasoning behind each decision.

import java.util.Arrays;

public class Grades {

    // Returns the highest grade. Starts from the first element and compares with the rest.
    static int maximum(int[] data) {
        int max = data[0];                   // assume the first one is the biggest
        for (int i = 1; i < data.length; i++) {   // start at 1: index 0 is already covered
            if (data[i] > max) {
                max = data[i];
            }
        }
        return max;
    }

    // Returns the average. The (double) avoids integer division: 41 / 6 would give 6, not 6.83
    static double average(int[] data) {
        int sum = 0;
        for (int n : data) {                 // for-each: we only read, we don't modify
            sum += n;
        }
        return (double) sum / data.length;
    }

    // This method DOES modify the original array (that's why it uses the classic for)
    static void raiseByOne(int[] data) {
        for (int i = 0; i < data.length; i++) {
            data[i]++;
        }
    }

    public static void main(String[] args) {
        int[] grades = {7, 5, 9, 4, 10, 6};

        System.out.println("Grades: " + Arrays.toString(grades));
        System.out.println("Highest: " + maximum(grades));
        System.out.println("Average: " + average(grades));

        raiseByOne(grades);
        System.out.println("After raising: " + Arrays.toString(grades));

        // Three groups with a different number of students (rows of different length)
        int[][] groups = {
            {7, 5, 9},
            {4, 10},
            {6, 8, 8, 3}
        };
        for (int g = 0; g < groups.length; g++) {
            // groups[g] is an int[]: we can pass it straight to the average method
            System.out.println("Group " + g + " (" + groups[g].length + " students): " + average(groups[g]));
        }
    }
}

Output:

Grades: [7, 5, 9, 4, 10, 6]
Highest: 10
Average: 6.833333333333333
After raising: [8, 6, 10, 5, 11, 7]
Group 0 (3 students): 7.0
Group 1 (2 students): 7.0
Group 2 (4 students): 6.25

See how nicely the pieces fit: average works the same with the grades array as with groups[g], because each row of the table is an array of integers. That’s what it means for a table to be “an array of arrays”.

See it with Python Tutor

References are much easier to understand by seeing them than by reading about them. Go to pythontutor.com/java.html, paste this code, and step through it with “Next”:

public class Main {
    public static void main(String[] args) {
        int[] grades = {7, 5, 9};
        int[] alias = grades;          // copy or same house?
        alias[0] = 99;

        int[] copy = new int[grades.length];
        for (int i = 0; i < grades.length; i++) {
            copy[i] = grades[i];       // real copy, element by element
        }
        copy[1] = 0;

        System.out.println(grades[0]);
        System.out.println(copy[1]);
    }
}

What to look for:

  • After int[] grades = {7, 5, 9};, on the right you’ll see one array and the variable grades with an arrow pointing to it: that arrow is the “address”.
  • After int[] alias = grades;, a second arrow appears pointing to the same array, not a new one.
  • When alias[0] = 99; runs, the 7 turns into 99 in the only array that exists: grades[0] is also 99.
  • With new int[grades.length], a second, independent array appears, and the loop copies the values one by one. Now copy[1] = 0 doesn’t affect grades.

Summary

  • An array holds values of the same type and has a fixed size; its elements can be modified.
  • It’s created with braces ({1, 2, 3}) or with new type[size]; if you don’t fill it in, the slots hold 0, 0.0, false or null depending on the type.
  • Indexes go from 0 to length - 1; length has no parentheses and there are no negative indexes.
  • You loop with the classic for (when you need the index or to modify) or with the for-each (read only). The for condition: i < array.length.
  • An array variable holds a reference: assigning (b = a) doesn’t copy, and == doesn’t compare contents. Use Arrays.copyOf and Arrays.equals.
  • When you pass an array to a method, the method can modify it.
  • A table is an array of arrays (int[][]), it’s looped over with two loops, and its rows can have different lengths.
  • The Arrays class (toString, sort, binarySearch, copyOf, copyOfRange, equals, fill) saves you from reinventing the wheel.

Checklist: do you have it clear?

  • Can you declare an array and create it both ways (braces and new)?
  • Can you say what value each slot will have if you don’t fill it in?
  • Can you explain why grades[grades.length] fails?
  • Do you know why int[] b = a; doesn’t make a copy?
  • Can you compare two arrays by contents and print one on screen?
  • Can you loop over a table with two loops, by index and by value?

If any point left you with doubts, go back to its section before moving on. In the next article we’ll put all this into practice with complete programs.

Keep goingAll Java (en) articles

Did this article help you? Here is how you can support SergioLearns →

Sigue aprendiendo

Leave a Reply

Your email address will not be published. Required fields are marked *