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
Gaugecore 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 withnullvalues. d.lengthis not a field read; it compiles to thearraylengthinstruction,k.counttogetfield. 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 ofint[]:d[i].lengthis 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 withArrayStoreException; 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.