---
title: Arrays
source: 'https://academia.sh/en/courses/java-fundamentals/arrays'
course: 'Java Fundamentals'
language: en
updated: '2026-08-17T18:09:39+00:00'
license: 'CC BY-SA 4.0'
---

# Arrays

An array in Java is an object, and the measurement shows this in three places: production leaves a single instruction, d.length is not a field read but an instruction of its own, and bound checking and array covariance's type check are left to run time without ever showing up in the class file.

The previous lesson measured which instruction flow forms compile to: the same selection
turned into two separate lookup instructions, the same enhanced `for` loop turned into
separate steps over an array and a list. What is left is the thing walked over itself.

The array was established in the Data Structures course as a data structure: fixed size,
contiguous layout, arithmetic that goes from a position to an address. This lesson does not
repeat that. The question here is: how many instructions does the line `new int[3]` leave in
the class file, is the form `d.length` a field read, and where in the source does the step
that checks the array's bound sit? All three answers come from the array being an **object**
in Java.

## An Array Is an Object

- **BS21** — The measured source text is written inside the lesson. Since we know what we
  wrote, every extra found in the class file is what the compiler added.
- **BS22** — The `Gauge` core comes from the shared definition. This lesson adds only one
  measurement to it: `instructionNames`, which returns a method's instruction names in
  order. The behavior of the existing measurements does not change.
- **BS23** — Instruction names are the names in the class file and are printed lowercased
  with `Locale.ROOT`; locale does not affect the result.
- **BS24** — If the compiler returns a nonzero result, the core throws an exception. The
  lesson catches this exception and prints only the "did not compile" line; the compiler's
  message text is not printed.

```java
// Gauge.java — shared measurement core: reads what the compiler puts in the class file
import java.io.PrintWriter;
import java.io.Writer;
import java.lang.classfile.*;
import java.lang.classfile.instruction.*;
import java.nio.file.*;
import java.util.*;
import java.util.spi.ToolProvider;

class Gauge {
    record Method(String name, int instructions, List<String> added) {}
    record ClassInfo(List<Method> methods, List<String> fields) {}

    static ClassModel compile(String source, String className) throws Exception {
        Path d = Files.createTempDirectory("gauge");
        Path k = d.resolve(className + ".java");
        Files.writeString(k, source);
        PrintWriter sink = new PrintWriter(Writer.nullWriter());
        if (ToolProvider.findFirst("javac").orElseThrow()
                .run(sink, sink, "-d", d.toString(), k.toString()) != 0)
            throw new IllegalStateException("did not compile: " + className);
        return ClassFile.of().parse(d.resolve(className + ".class"));
    }

    static ClassInfo read(String source, String className) throws Exception {
        ClassModel cm = compile(source, className);
        List<Method> methods = new ArrayList<>();
        for (MethodModel m : cm.methods()) {
            if (m.code().isEmpty()) continue;
            List<String> added = new ArrayList<>();
            int instructions = 0;
            for (CodeElement e : m.code().get()) {
                if (e instanceof Instruction) instructions++;
                if (e instanceof InvokeInstruction iv)
                    added.add(shortName(iv.owner().asInternalName()) + "." + iv.name().stringValue());
                else if (e instanceof NewPrimitiveArrayInstruction a)
                    added.add("new array " + a.typeKind().name().toLowerCase(Locale.ROOT));
                else if (e instanceof NewReferenceArrayInstruction a)
                    added.add("new array " + shortName(a.componentType().asInternalName()));
            }
            methods.add(new Method(m.methodName().stringValue(), instructions, added));
        }
        List<String> fields = new ArrayList<>();
        for (FieldModel f : cm.fields()) fields.add(f.fieldName().stringValue());
        return new ClassInfo(methods, fields);
    }

    static List<String> instructionNames(String source, String className, String method) throws Exception {
        List<String> names = new ArrayList<>();
        for (MethodModel m : compile(source, className).methods())
            if (m.methodName().stringValue().equals(method))
                for (CodeElement e : m.code().orElseThrow())
                    if (e instanceof Instruction i)
                        names.add(i.opcode().name().toLowerCase(Locale.ROOT));
        return names;
    }

    static String shortName(String internal) { return internal.substring(internal.lastIndexOf('/') + 1); }
}
```

```java
// ArrayDemo.java — what array production leaves in the class file
import java.util.Arrays;

class Item {
    String name;
    int count;
    long weight;
}

public class ArrayDemo {
    static final String K = """
        class ArraySample {
            static class Item { String name; int count; long weight; }
            static int[] empty() { return new int[3]; }
            static Item[] items() { return new Item[3]; }
            static int[] withConstants() { return new int[] {12, 18, 7}; }
            static int length(int[] d) { return d.length; }
            static int count(Item k) { return k.count; }
        }
        """;

    public static void main(String[] args) throws Exception {
        System.out.printf("%-10s %6s  %s%n", "method", "instr", "step not written in source");
        for (Gauge.Method y : Gauge.read(K, "ArraySample").methods())
            if (!y.name().equals("<init>"))
                System.out.printf("%-10s %6d  %s%n", y.name(), y.instructions(),
                        y.added().isEmpty() ? "-" : String.join(", ", y.added()));

        System.out.println();
        System.out.println("d.length read: " + Gauge.instructionNames(K, "ArraySample", "length"));
        System.out.println("k.count read:  " + Gauge.instructionNames(K, "ArraySample", "count"));

        int[] numbers = new int[3];
        Item[] items = new Item[3];
        System.out.println();
        System.out.println("int[] class:       " + numbers.getClass().getName());
        System.out.println("Item[] class:      " + items.getClass().getName());
        System.out.println("array's superclass: " + numbers.getClass().getSuperclass().getName());
        System.out.println("initial values: " + Arrays.toString(numbers)
                + " " + Arrays.toString(items));

        try {
            Gauge.read(K.replace("return d.length;", "d.length = 5; return 0;"), "ArraySample");
        } catch (IllegalStateException e) {
            System.out.println("d.length = 5   -> " + e.getMessage());
        }
    }
}
```

```
method      instr  step not written in source
empty           3  new array int
items           3  new array ArraySample$Item
withConstants     15  new array int
length          3  -
count           3  -

d.length read: [aload_0, arraylength, ireturn]
k.count read:  [aload_0, getfield, ireturn]

int[] class:       [I
Item[] class:      [LItem;
array's superclass: java.lang.Object
initial values: [0, 0, 0] [null, null, null]
d.length = 5   -> did not compile: ArraySample
```

The first two rows say array production is **three instructions**: load the size, produce
the array, return. The gauge records this production as `new array`, because in the class
file array production falls into the same class as object production. In the `Item[3]` row's
record, the class's **full name** stands; the element type is written into the array.

The third row makes a difference visible. The `withConstants` method, which writes three
values inside square brackets, produces **15** instructions; the empty array of the same size
produces **3.** The twelve instructions in between are the three elements being written one
by one: for every element, duplicate the array, load the position, load the value, write it.
The form `{12, 18, 7}`, which stands like a single constant in the source, has no constant
counterpart in the class file. Constant folding, measured in the first lesson, works on
numbers; it does not work on an array, because what would need folding is not a value, it is
an object produced at run time.

The last three rows put this into a name. The array has a class — `[I` for `int[]`, `[LItem;`
for `Item[]` — and that class's superclass is `java.lang.Object`. An array is an object: it
is produced on the heap, held by a reference, and passes into any spot expecting an `Object`.
Production does not produce elements; a number array starts with zeros, a record array with
`null`. Building a record array does not build three records, it builds three empty spots.

The length rows give the distinction's second half. The form `d.length` compiles to the
`arraylength` instruction, the form `k.count` to `getfield`. The syntax looks the same in
both — a dot and a name — but their counterparts are separate instructions. `length` is not a
field; it is not in the field list in the class file, it has its own instruction. This has a
visible consequence in the last line: source writing `d.length = 5` **does not compile.**
Growing an array is impossible not because of a prohibition, but because no instruction exists
to be its counterpart.

## The Bound-Checking Step Is Not in the Source

- **BS25** — In the bound measurement, only the exception's **class name** is printed. The
  exception message contains the position and the length; that text is not printed, because
  what is measured is the check's existence.

```java
// AccessDemo.java — which instruction bound checking sits inside
public class AccessDemo {
    static final String K = """
        class AccessSample {
            static int read(int[] d, int i) { return d[i]; }
            static void write(int[] d, int i, int v) { d[i] = v; }
        }
        """;

    public static void main(String[] args) throws Exception {
        System.out.println("d[i] read:  " + Gauge.instructionNames(K, "AccessSample", "read"));
        System.out.println("d[i] write: " + Gauge.instructionNames(K, "AccessSample", "write"));

        int[] d = {12, 18, 7};
        System.out.println();
        for (int i : new int[] {0, 2, 3, -1}) {
            try {
                System.out.printf("d[%d] -> %d%n", i, d[i]);
            } catch (RuntimeException e) {
                System.out.printf("d[%d] -> %s%n", i, e.getClass().getSimpleName());
            }
        }
        int size = -1;
        try {
            System.out.println("new int[size] -> " + new int[size].length);
        } catch (RuntimeException e) {
            System.out.println("new int[size] -> " + e.getClass().getSimpleName() + " (size = " + size + ")");
        }
    }
}
```

```
d[i] read:  [aload_0, iload_1, iaload, ireturn]
d[i] write: [aload_0, iload_1, iload_2, iastore, return]

d[0] -> 12
d[2] -> 7
d[3] -> ArrayIndexOutOfBoundsException
d[-1] -> ArrayIndexOutOfBoundsException
new int[size] -> NegativeArraySizeException (size = -1)
```

Reading is four instructions, writing is five. In neither is there a comparison, a branch, or
a call: load the array, load the position, read or write. Bound checking is not written in
the source, and it does not stand as **a separate instruction** in the class file either. The
check sits inside the **definition** of `iaload` and `iastore`; the virtual machine does it on
every access, and if the position is out of range, it throws an exception.

This is the added step's third form. The first is an instruction placed in the class file —
conversion, call, production. The second is a member never written in the source at all. The
third is embedded in the instruction's own definition and **never changes instruction count
at all**; the gauge cannot count it, it can only be read from its run-time result. Of the
measurement's four accesses, two give a value, two give an exception; the negative position is
not a separate rule, it is the same rule's result.

The last line shows the check's second location: a production request with a negative size
also falls at run time and gives a separate exception, because the size does not have to be
known at compile time.

## A Multidimensional Array Is an Array of Arrays

- **BS26** — Row lengths are written one by one in the measurement; none is produced
  randomly, none is read from the environment.

```java
// MultiDim.java — a multidimensional array is an array of arrays
public class MultiDim {
    static final String K = """
        class DimSample {
            static int[][] regular() { return new int[2][3]; }
            static int[][] jagged() { return new int[2][]; }
            static int rowLength(int[][] d, int i) { return d[i].length; }
        }
        """;

    public static void main(String[] args) throws Exception {
        for (String y : new String[] {"regular", "jagged", "rowLength"})
            System.out.printf("%-14s %s%n", y, Gauge.instructionNames(K, "DimSample", y));

        int[][] t = new int[3][];
        t[0] = new int[] {12};
        t[1] = new int[] {18, 7, 4};
        t[2] = new int[0];
        System.out.println();
        System.out.println("outer array class: " + t.getClass().getName());
        System.out.println("one row's class:   " + t[0].getClass().getName());
        System.out.printf("outer length %d, row lengths %d %d %d%n",
                t.length, t[0].length, t[1].length, t[2].length);

        int[][] d = new int[2][3];
        System.out.println("in a regular array, are rows the same object: " + (d[0] == d[1]));
        d[0][0] = 12;
        System.out.println("after d[0][0] = 12, d[1][0]: " + d[1][0]);
    }
}
```

```
regular        [iconst_2, iconst_3, multianewarray, areturn]
jagged         [iconst_2, anewarray, areturn]
rowLength      [aload_0, iload_1, aaload, arraylength, ireturn]

outer array class: [[I
one row's class:   [I
outer length 3, row lengths 1 3 0
in a regular array, are rows the same object: false
after d[0][0] = 12, d[1][0]: 0
```

The third row writes the lesson's sentence: reading `d[i].length` is **two** steps. First the
row is loaded with `aaload` — because a row is a reference, an object — then that object's
length is read with `arraylength`. A two-dimensional array is not a single contiguous block;
the outer array's elements are references to inner arrays. The class name writes this too:
the outer array's class is `[[I`, a row's is `[I`.

The first two rows separate two different productions. `new int[2][3]`, which gives every
dimension, is a single `multianewarray` instruction and produces the rows along with the
outer array. `new int[2][]`, which gives only the outer dimension, is an `anewarray`
instruction; the outer array is produced, its rows stay `null`. The second lets rows be built
at **separate lengths**: the three rows in the measurement have lengths 1, 3, and 0. The last
two lines confirm that even in a regular array, rows are separate objects — `d[0]` and `d[1]`
are not the same object, and writing to the first row does not change the second. Java has no
separate multidimensional array type.

## One Type Check Per Write

- **BS27** — The same write is tried on three separate sources. Two compile and their
  instructions are printed, one does not compile and passes with the "did not compile" line;
  which one falls at which time is the measurement itself.

```java
// Covariance.java — array covariance: which write compiles, which write falls
public class Covariance {
    static final String STRING_ARR = "class CovSample { static void write(String[] d) { d[0] = %s; } }";
    static final String OBJECT_ARR = "class CovSample { static void write(Object[] d) { d[0] = %s; } }";

    static void tryIt(String name, String source) {
        try {
            System.out.printf("%-28s -> %s%n", name, Gauge.instructionNames(source, "CovSample", "write"));
        } catch (Exception e) {
            System.out.printf("%-28s -> %s%n", name, e.getMessage());
        }
    }

    public static void main(String[] args) throws Exception {
        tryIt("String[] d; d[0] = \"north\"", STRING_ARR.formatted("\"north\""));
        tryIt("String[] d; d[0] = 12", STRING_ARR.formatted("12"));
        tryIt("Object[] d; d[0] = 12", OBJECT_ARR.formatted("12"));

        String[] names = new String[2];
        Object[] view = names;
        System.out.println();
        System.out.println("Object[] view = names;  same object: " + (view == names));
        System.out.println("view's class: " + view.getClass().getName());
        view[0] = "north";
        System.out.println("view[0] = \"north\" -> names[0] = " + names[0]);
        try {
            view[1] = 12;
            System.out.println("view[1] = 12 -> " + names[1]);
        } catch (RuntimeException e) {
            System.out.println("view[1] = 12 -> " + e.getClass().getSimpleName()
                    + ", names[1] = " + names[1]);
        }
        int[] numbers = new int[2];
        numbers[1] = 12;
        System.out.println("same write on the int[] side -> " + numbers[1]);
    }
}
```

```
String[] d; d[0] = "north"   -> [aload_0, iconst_0, ldc, aastore, return]
String[] d; d[0] = 12        -> did not compile: CovSample
Object[] d; d[0] = 12        -> [aload_0, iconst_0, bipush, invokestatic, aastore, return]

Object[] view = names;  same object: true
view's class: [Ljava.lang.String;
view[0] = "north" -> names[0] = north
view[1] = 12 -> ArrayStoreException, names[1] = null
same write on the int[] side -> 12
```

Arrays in Java are **covariant** (array covariance): if the element type is beneath a
supertype, the array too substitutes for an array of that supertype. `String[]` substitutes
for an `Object[]`, and the measurement's fourth line confirms this — the two names hold the
same object, its class is still `[Ljava.lang.String;`.

The first three lines write covariance's cost. Source writing a number into an array named
`String[]` **does not compile**: the compiler looks at the declared type and gives the error
at compile time. When the same array is named `Object[]`, **the same write compiles** — with
a call added that converts the number to a wrapper, too. The write is valid at the syntax
level, because any object can be written into an `Object[]`.

Run time makes the decision: the string write passes and is visible to the caller, the number
write falls with `ArrayStoreException` and `names[1]` is **still `null`** — the write was not
done, and it was not left half-done either. As the virtual machine executes the `aastore`
instruction, it checks whether the value matches the array's **real** element type; if it does
not, it does not write.

The bounding measurement here runs two ways. First: this check leaves no trace at all in
instruction count. `aastore` is a single instruction and `iastore` is a single instruction
too; looking at the class file, we cannot count the check's existence. Second: in the last
line's `int[]` write, no such check **exists.** If the element type is primitive, no question
of array covariance arises, because an array with a primitive element type never substitutes
for another array type. The added step disappears here — and where it disappears is exactly
where the subtype relationship ends.

## Summary

- An array is an object: it has its own class, its superclass is `java.lang.Object`, and its
  production is **3** instructions in the class file; the bracketed form `{12, 18, 7}` is
  **15** instructions, because elements are written one by one. Production does not produce
  elements: a number array starts with zeros, a reference array with `null` values.
- `d.length` is not a field read; it compiles to the `arraylength` instruction, `k.count` to
  `getfield`. Source that writes to the length does not compile.
- Bound checking sits neither in the source nor in a separate instruction; it is inside the
  access instruction's own definition, does not change instruction count, and throws an
  exception on an out-of-range access.
- `int[][]` is not a separate type, it is an array of `int[]`: `d[i].length` is two
  instructions, rows are separate objects, and they can be separate lengths.
- Because of array covariance, writing a number into an array named `Object[]` that is really
  a string array compiles but falls at run time with `ArrayStoreException`; the same check
  does not exist at all for a primitive-typed array.

## Next Step

This lesson measured that writing to a `String[]` array is checked at run time; the value
written was, every time, an already-ready string object. The next lesson looks at that object
itself: where does a string literal sit in the class file, how many objects does writing the
same literal twice produce, and how many objects does text concatenated inside a loop leave
along the way? A string's immutability, like an array's fixed size, decides steps invisible in
the source.
