Skip to content
academia.sh

Lesson 08 / 10

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.

Contents

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

To keep your progress and take notes, Log in

My notes

Log in to take notes.

Start typing to search.

↑↓ Esc navigate · open · close