You closed module 4 with a promise and a confession. The promise: BiblioTech's provisional arrays will become collections. The confession: every time the project needs to store many things, it falls back on a Material[] catalog = new Material[10] that has to be copied whole to add an element and walked end to end to find one. Before replacing them it is worth understanding them properly, because the array does not go away: ArrayList is an array inside, HashMap is an array of buckets, ArrayDeque is a circular array, and String is an array of bytes. The whole Collections Framework you will see in the next eight lessons is built on arrays.
An array (the term we will use almost always, because it is the one you will see in all real code) is Java's most primitive and fastest data structure: a contiguous block of memory, of fixed size, that holds elements of the same type and lets you reach any one of them by position in constant time. Those four words —contiguous, fixed, homogeneous, constant— explain at once all its power and all its limitations. In this lesson you will see them in detail, you will finally learn the for-each loop left pending in module 2, you will master the Arrays utility class and you will finish with a clear list of the things an array cannot do: exactly the list of reasons why the Collections Framework exists.
Contents
- What an array really is
- Declaring, creating and initialising
- Default values
- Indices,
lengthand the bounds of the array - Traversal with a classic
for - The
for-eachloop - Arrays of primitives versus arrays of objects
- Multidimensional and jagged arrays
- The
Arraysclass - Copying arrays
- Varargs is an array underneath
- Application: the BiblioTech catalogue
- The limitations that motivate collections
- Common Mistakes and Tips
- Exercises
- What an array really is
An array is a contiguous block of memory reserved in one go, divided into cells of the same size. When you write:
the JVM reserves enough space on the heap for five integers (five times four bytes) plus a small header with the type and the length. All the cells sit one right behind the other:
flowchart LR
subgraph heap["Array object on the heap"]
direction LR
H["header<br/>type: int[]<br/>length: 5"]
C0["[0]<br/>0"]
C1["[1]<br/>0"]
C2["[2]<br/>0"]
C3["[3]<br/>0"]
C4["[4]<br/>0"]
end
ref["daysLate<br/>(reference on the stack)"] --> H
That contiguity is the key to everything. To read daysLate[3], the JVM does not search for anything: it computes base_address + 3 * 4 bytes and reads. One multiplication and one addition, always the same work, whether the array has five elements or five million. We call that constant-time access, or O(1).
O() notation: how to read it. Throughout this module you will compare data structures by their cost. O() notation describes how the time of an operation grows as the number of elements
ngrows, ignoring constants and machine details. O(1) means constant time: whether there are 10 or 10 million elements, it costs the same. O(n) means linear: twice the elements, twice the time (walking a whole array). O(log n) means that doubling the elements adds just one more step (binary search: with a million elements, about 20 steps). O(n²) means that doubling the elements quadruples the time (two nested loops; that is what BiblioTech'sreportByTypedoes today). It is not a measure of seconds, it is a measure of how it scales.
And there is a second, less obvious advantage: cache locality. Modern processors do not read memory byte by byte, but in blocks (cache lines, typically 64 bytes). When reading daysLate[0], the processor brings in the following elements "for free" as well. That is why walking an array is dramatically faster than walking a structure whose elements are scattered around memory: you will see it with numbers in 05-04, when you compare ArrayList with LinkedList.
The three properties that define an array in Java:
| Property | What it means | Consequence |
|---|---|---|
| Fixed size | Decided when it is created and cannot be changed | To "add" you have to create another array and copy |
| Homogeneous type | All elements are of the same declared type | The compiler guarantees what is inside |
| It is an object | It lives on the heap; the variable holds a reference | It is passed by reference, accepts null, inherits from Object |
That third point surprises a lot of people: an array is an object, even though there is no Array class you can look at. int[], String[] and Material[] are real types that inherit from Object, so an array has hashCode(), toString() and getClass(). That will have important consequences in section 9.
- Declaring, creating and initialising
These are three distinct operations and it pays not to confuse them.
Declaring only creates the variable that will hold the reference. There is no array yet:
Material[] catalog; // recommended form: the brackets next to the TYPE
Material catalog2[]; // legal, inherited from C, DISCOURAGEDAlways use the first form. The second compiles, but hides information: in Material catalog[], other;, catalog is an array and other is not, which is a classic source of confusion.
Creating reserves the memory with new, and this is where the size is fixed forever:
Material[] catalog = new Material[10]; // 10 cells, all null
int[] daysLate = new int[5]; // 5 cells, all 0The size can be a variable computed at run time, which is flexible enough:
What it cannot do is change afterwards. selection will be howMany long forever.
Initialising with a literal creates and fills in a single expression, and the size is inferred from the elements:
String[] employees = { "Marta Ruiz", "Diego Alonso", "Nuria Vidal" }; // length = 3
double[] rates = { 0.25, 0.10, 0.50 };This shorthand only works in the declaration. If you want to assign an array literal to an already declared variable, or pass it directly as an argument, you need the full form with new:
String[] employees;
// employees = { "Marta Ruiz" }; // COMPILATION error
employees = new String[] { "Marta Ruiz" }; // correct
register(new String[] { "Marta Ruiz", "Diego Alonso" }); // correct as an argument
- Default values
When you create an array with new, Java fills every cell with the default value for the type. This is no minor detail: it means a freshly created array never contains garbage, unlike what happens in C.
| Element type | Default value |
|---|---|
byte, short, int, long |
0 |
float, double |
0.0 |
char |
'\u0000' (null character, printed as a blank or a square) |
boolean |
false |
Any reference type (String, Material, Integer...) |
null |
The most important practical consequence is the last row. A new Material[10] does not contain ten materials: it contains ten null references. Filling them is still your job:
Material[] catalog = new Material[3];
System.out.println(catalog[0]); // null
// catalog[0].getTitle(); // NullPointerException
catalog[0] = new Book("Effective Java", "Joshua Bloch", "978-0000000001", 2018);
System.out.println(catalog[0].getTitle()); // Effective JavaIt also explains the classic trap of counting elements: if you create new Material[10] and only fill three, catalog.length is 10, not 3. The array does not know how many cells you have used; that is why the whole BiblioTech of module 4 drags around helper variables such as int n = 0 to keep the count by hand. Collections remove exactly that problem.
- Indices,
length and the bounds of the array
length and the bounds of the arrayArray indices run from 0 to length - 1. Starting at 0 is not a whim: the index is literally the offset from the start of the block. The first element is at distance zero from the base.
String[] employees = { "Marta Ruiz", "Diego Alonso", "Nuria Vidal" };
System.out.println(employees.length); // 3
System.out.println(employees[0]); // Marta Ruiz (the first)
System.out.println(employees[2]); // Nuria Vidal (the last)
System.out.println(employees[employees.length - 1]); // Nuria Vidal, the standard idiomlength is a field, not a method. You write array.length, without parentheses. It is one of Java's historical inconsistencies that causes the most confusion, because for String it is a method (text.length()) and for collections it is yet another method (list.size()). Memorise all three:
| Type | How you get the size |
|---|---|
| Array | array.length — field, no parentheses |
String |
text.length() — method |
Collection (List, Set, Map...) |
collection.size() — method |
If you go out of range, the JVM detects it and throws ArrayIndexOutOfBoundsException:
System.out.println(employees[3]); // ArrayIndexOutOfBoundsException: Index 3 out of bounds for length 3
System.out.println(employees[-1]); // ArrayIndexOutOfBoundsException: Index -1 out of bounds for length 3This is a virtue, not a defect: Java checks every access and stops the program at the exact point of the error, instead of silently reading somebody else's memory the way C would. The cost of that check is minimal and the JIT compiler removes it when it can prove the index is safe. How to catch and handle that exception is the subject of module 6; for now, avoid it by checking the bounds before you access.
- Traversal with a classic
for
forThe for loop you have known since 02-02 is the complete way to walk an array, and it is still the right one when you need the index:
double[] fines = { 3.75, 0.0, 12.50, 1.25 };
for (int i = 0; i < fines.length; i++) {
System.out.printf("Loan %d -> fine %.2f EUR%n", i + 1, fines[i]);
}Three details that prevent most traversal bugs:
i < fines.length, neveri <= fines.length. The last valid index islength - 1.- Use
fines.length, not a literal constant such as4. If the array grows tomorrow, the loop adapts on its own. - Start at 0, unless you have an explicit reason not to.
The classic for is essential when the traversal needs more than the values:
// Backwards traversal
for (int i = fines.length - 1; i >= 0; i--) { ... }
// Two at a time
for (int i = 0; i < fines.length; i += 2) { ... }
// Compare each element with the next one (mind the bound)
for (int i = 0; i < fines.length - 1; i++) {
if (fines[i] > fines[i + 1]) { ... }
}
// MODIFY the contents of the array
for (int i = 0; i < fines.length; i++) {
fines[i] = Math.min(fines[i], 20.0); // apply the fine cap
}That last one is the most important to remember for the next section.
- The
for-each loop
for-each loopIn module 2 the for-each was postponed because there was no point explaining it without something to walk. Now you have it.
The enhanced for loop or for-each (Java 5) walks every element of an array or of any Iterable object —which includes all the collections you will see in this module— without managing indices:
It reads literally as "for each element of type Type in collectionOrArray". The colon is pronounced "in".
Compare the two versions of the same traversal:
String[] employees = { "Marta Ruiz", "Diego Alonso", "Nuria Vidal" };
// classic for: 3 pieces that can go wrong (start, condition, increment)
for (int i = 0; i < employees.length; i++) {
System.out.println("Employee: " + employees[i]);
}
// for-each: no index, no condition, no increment
for (String name : employees) {
System.out.println("Employee: " + name);
}And with the project's objects, where it really shines:
Material[] catalog = {
new Book("Effective Java", "Joshua Bloch", "978-0000000001", 2018),
new Book("Design Patterns", "Erich Gamma", "978-0000000002", 1994),
new Magazine("Java Magazine", "REV-2024-03", 42, "Monthly"),
new Dvd("Refactoring Live", "DVD-0007", 95)
};
for (Material m : catalog) {
System.out.println(m.describe()); // polymorphism from 03-06, without a single index
}Advantages
- Less code and fewer mistakes. The three classic sources of failure disappear: starting at 1, ending with
<=, forgetting thei++(infinite loop). - Impossible to go out of range. There is no index to get wrong:
ArrayIndexOutOfBoundsExceptioncannot happen. - It expresses the intent. "I walk every element, one by one, forwards." Whoever reads your code knows it instantly.
- It works the same with arrays and with collections. The same loop serves
Material[]andList<Material>, which will make the refactoring in section 12 painless.
Limitations
And now the three things it cannot do, which are the reason the classic for still exists:
1. It does not give you the index. If you need to number, compare with the neighbour or write at position i, you need the classic for (or keep a separate counter, which is exactly what the for-each was trying to avoid).
int position = 1;
for (Material m : catalog) {
System.out.println(position + ". " + m.getTitle());
position++; // works, but betrays that you wanted a classic for
}2. It cannot modify the array cell. This is the limitation that is most often misunderstood, so it is worth seeing precisely:
int[] numbers = { 1, 2, 3 };
for (int n : numbers) {
n = n * 10; // does NOTHING to the array
}
System.out.println(Arrays.toString(numbers)); // [1, 2, 3]The variable n is a copy of the cell's value (pass by value, 03-03). Reassigning it changes the copy, not the array. The same happens with objects:
for (Material m : catalog) {
m = new Book("Other", "Nobody", "REF-X", 2020); // does NOT change catalog[i]
}m is a copy of the reference. Reassigning it points the copy at another object; the array cell still points at the original.
But you can modify the object it points to, because there you reassign nothing, you invoke methods on the same object:
The rule, in one line: the for-each cannot change what each cell points at, but it can change the state of the object pointed at.
3. It only goes forwards and one at a time. There is no reverse traversal, no jumps, no partial traversals.
When to use each one
| Situation | Recommended loop |
|---|---|
| Read every element, in order | for-each |
| You need the index (numbering, position, comparing neighbours) | classic for |
Writing into the cells (array[i] = ...) |
classic for |
| Traversing backwards or with jumps | classic for |
| Traversing only part of it | classic for |
| Traversing a collection and removing as you go | Neither: Iterator or removeIf (05-02) |
Use for-each by default and drop down to the classic for only when you need what the for-each does not give you. In 05-02 you will see that the for-each is not a magic construct: the compiler translates it into a classic for when it walks an array, and into an Iterator when it walks a collection.
- Arrays of primitives versus arrays of objects
Here is the structural difference that explains half of the surprises with arrays: what each cell really holds.
In an array of primitives, the cell contains the value:
In an array of objects, the cell contains a reference to the object, which lives elsewhere on the heap:
Material[] catalog = new Material[3];
catalog[0] = new Book("Effective Java", "Joshua Bloch", "978-0000000001", 2018);
catalog[1] = new Magazine("Java Magazine", "REV-2024-03", 42, "Monthly");
// catalog[2] is still nullflowchart LR
subgraph prim["int[] years — the values are INSIDE"]
direction LR
P0["[0] 2018"]
P1["[1] 1994"]
P2["[2] 1999"]
end
subgraph obj["Material[] catalog — references"]
direction LR
O0["[0] ref"]
O1["[1] ref"]
O2["[2] null"]
end
L["Book<br/>Effective Java<br/>978-0000000001"]
R["Magazine<br/>Java Magazine<br/>REV-2024-03"]
O0 --> L
O1 --> R
Four consequences follow from that diagram, and they are worth keeping very clear:
- An array of objects is smaller than it looks but more expensive to walk. The cells only hold references (4 or 8 bytes), but following each reference means a jump to another area of the heap, which breaks the cache locality of section 1.
- Two cells can point at the same object (aliasing, 03-02).
catalog[2] = catalog[0];does not copy the book: it creates a second path to it. Modifying it through one route is visible through the other. nullis a legitimate value in the cells, and every access tocatalog[i].something()can throwNullPointerExceptionif that cell was never filled.- The array is covariant, and that has a trap.
Material[]accepts any subclass ofMaterial(Book,Magazine,Dvd), which is convenient and is what makes the polymorphic catalogue possible. But it also allows this:
Object[] things = new Book[2]; // COMPILES: Book[] is an Object[]
things[0] = new Magazine("Java Magazine", "REV-1", 1, "Monthly"); // ArrayStoreException at RUN TIMEThe compiler accepts it because Book[] is a subtype of Object[], but the JVM checks the real type on every write and throws ArrayStoreException. It is a type-safety hole that Java 1.0 accepted out of pragmatism and that generics fixed: List<Book> is not a List<Object>, precisely so that this error is caught at compile time. The full theory (invariance, wildcards, type erasure) is lesson 10-01.
- Multidimensional and jagged arrays
In Java there are no true two-dimensional arrays: what exists are arrays whose elements are, in turn, arrays. Understanding that clears up every doubt at once.
This creates an array of 3 elements, each of which is a reference to an array of 12 integers:
flowchart LR
M["loansPerMonth<br/>int[][] length=3"]
F0["[0] → int[12]"]
F1["[1] → int[12]"]
F2["[2] → int[12]"]
M --> F0
M --> F1
M --> F2
F0 --> A0["0 0 0 0 0 0 0 0 0 0 0 0"]
F1 --> A1["0 0 0 0 0 0 0 0 0 0 0 0"]
F2 --> A2["0 0 0 0 0 0 0 0 0 0 0 0"]
Access takes two indices, in row-column order:
loansPerMonth[0][3] = 12; // type 0 (books), month 3 (April)
System.out.println(loansPerMonth.length); // 3 -> number of rows
System.out.println(loansPerMonth[0].length); // 12 -> length of row 0The natural traversal is two nested loops, one per dimension:
String[] types = { "Books", "Magazines", "DVDs" };
for (int t = 0; t < loansPerMonth.length; t++) {
int total = 0;
for (int month = 0; month < loansPerMonth[t].length; month++) {
total += loansPerMonth[t][month];
}
System.out.printf("%-10s %3d loans a year%n", types[t], total);
}Or, if you do not need the indices, with a nested for-each:
for (int[] row : loansPerMonth) {
int total = 0;
for (int value : row) { total += value; }
System.out.println("Row total: " + total);
}Notice the type of the outer loop variable: int[] row, not int. Every element of an int[][] is an int[].
There is literal initialisation too, and here it is plain to see that these are arrays of arrays:
Jagged arrays
Since each row is an independent array, the rows can have different lengths. That is called a jagged array:
String[][] loansPerEmployee = new String[3][]; // 3 rows, no columns yet
loansPerEmployee[0] = new String[] { "Effective Java", "Refactoring" }; // Marta: 2
loansPerEmployee[1] = new String[] { "Design Patterns" }; // Diego: 1
loansPerEmployee[2] = new String[0]; // Nuria: 0
String[] names = { "Marta Ruiz", "Diego Alonso", "Nuria Vidal" };
for (int i = 0; i < loansPerEmployee.length; i++) {
System.out.print(names[i] + ": ");
if (loansPerEmployee[i].length == 0) {
System.out.println("(no loans)");
} else {
System.out.println(String.join(", ", loansPerEmployee[i]));
}
}Notice the syntax new String[3][]: you state the number of rows and leave the second dimension empty, because each row will be created later with its own size. If you leave the rows uncreated, they are null, and accessing loansPerEmployee[0][0] would give a NullPointerException.
This structure —"for each employee, a list of titles of varying length"— is exactly the one you will replace in 05-05 with a Map<Employee, List<Loan>>, far more expressive and with no numeric indices to keep in sync with another array of names.
- The
Arrays class
Arrays classjava.util.Arrays is a utility class with static methods that solve almost everything you need to do with arrays. It is one of the first things to import in any serious program:
toString and deepToString
An array is an object that does not override toString(), so printing it directly shows the Object representation from 03-09 (type name, at sign, hash in hexadecimal):
int[] years = { 2018, 1994, 1999 };
System.out.println(years); // [I@1b6d3586 <- useless
System.out.println(Arrays.toString(years)); // [2018, 1994, 1999]For arrays of more than one dimension, toString is not enough —it would print the reference of each row— and you need deepToString, which descends recursively:
int[][] matrix = { { 1, 2 }, { 3, 4 } };
System.out.println(Arrays.toString(matrix)); // [[I@4554617c, [I@74a14482]
System.out.println(Arrays.deepToString(matrix)); // [[1, 2], [3, 4]]Arrays.toString is your number one tool for debugging arrays. Use it in every trace.
sort and sort with a Comparator
Arrays.sort sorts the array in place: it modifies the array it receives and returns nothing.
double[] fines = { 12.50, 0.0, 3.75, 1.25 };
Arrays.sort(fines);
System.out.println(Arrays.toString(fines)); // [0.0, 1.25, 3.75, 12.5]With objects there are two variants. Without a Comparator, it requires the elements to implement Comparable —which Card does, as you saw in 04-07— and uses their natural order:
Card[] cards = {
new Card("Refactoring", "Martin Fowler", 1999),
new Card("Effective Java", "Joshua Bloch", 2018),
new Card("Design Patterns", "Erich Gamma", 1994)
};
Arrays.sort(cards); // uses Card's compareTo (by title)And with a Comparator, which is where the whole of module 4 connects:
Arrays.sort(cards, Comparator.comparingInt(Card::year)); // by year
Arrays.sort(cards, Comparator.comparing(Card::author).thenComparing(Card::title));
Arrays.sort(cards, Comparator.comparingInt(Card::year).reversed()); // newest firstThere is also a variant that sorts only a range, useful when the array has unused cells at the end:
The full theory —the compareTo contract, stability, which algorithm Java uses— is this module's closing lesson, 05-09.
binarySearch
It looks for an element with a binary search: it checks the middle element, discards half and repeats. Cost O(log n): in an array of a million elements, about twenty checks instead of a million.
int[] references = { 101, 205, 307, 412, 588 }; // ALREADY SORTED
System.out.println(Arrays.binarySearch(references, 307)); // 2 -> index where it is
System.out.println(Arrays.binarySearch(references, 400)); // -4 -> not thereTwo essential warnings:
- The array must be sorted beforehand, by the same criterion you are searching with. On an unsorted array the result is garbage, and there is no warning whatsoever.
- The negative value is not "-1 and that's it": it is
-(insertion_point) - 1. In the example,-4means 400 would go at index 3. If you want the insertion point:int pos = -result - 1;.
fill
Fills the whole array (or a range) with the same value:
double[] fines = new double[5];
Arrays.fill(fines, -1.0); // marks "not calculated" in every cell
Arrays.fill(fines, 0, 2, 0.0); // only indices 0 and 1 (2 is excluded)copyOf and copyOfRange
copyOf creates a new array of the given size, copying what fits and filling the rest with the default value:
Material[] catalog = new Material[3];
// ... the 3 cells get filled ...
Material[] expanded = Arrays.copyOf(catalog, 6); // 3 originals + 3 nulls
Material[] trimmed = Arrays.copyOf(catalog, 2); // only the first 2This is exactly the trick BiblioTech has been using since module 3 to "grow" an array, and also, literally, what ArrayList does internally (05-03).
copyOfRange copies a stretch, with the lower bound included and the upper one excluded:
int[] numbers = { 10, 20, 30, 40, 50 };
int[] middle = Arrays.copyOfRange(numbers, 1, 4); // [20, 30, 40]equals versus deepEquals
Here is a trap that costs many people hours. Since an array does not override equals, comparing two arrays with == or with .equals() compares references, not contents:
int[] a = { 1, 2, 3 };
int[] b = { 1, 2, 3 };
System.out.println(a == b); // false
System.out.println(a.equals(b)); // false <- the equals inherited from Object
System.out.println(Arrays.equals(a, b)); // true <- what you almost always wantedAnd for arrays of more than one dimension, Arrays.equals compares the rows by reference and also fails; you need deepEquals:
int[][] m1 = { { 1, 2 }, { 3, 4 } };
int[][] m2 = { { 1, 2 }, { 3, 4 } };
System.out.println(Arrays.equals(m1, m2)); // false
System.out.println(Arrays.deepEquals(m1, m2)); // trueThe same goes for the hash: use Arrays.hashCode and Arrays.deepHashCode, never array.hashCode(), if you are going to put arrays into hash-based structures (which is, moreover, an idea 05-05 will argue you out of).
asList and its trap
Arrays.asList creates a List from an array, and it is tempting because it looks like the obvious conversion. But it returns a fixed-size view backed by the original array, not a normal list:
String[] names = { "Marta Ruiz", "Diego Alonso", "Nuria Vidal" };
List<String> list = Arrays.asList(names);
System.out.println(list.get(0)); // Marta Ruiz
list.set(0, "Marta R."); // allowed: it ALSO changes names[0]
System.out.println(names[0]); // Marta R.
list.add("New"); // UnsupportedOperationException
list.remove(0); // UnsupportedOperationExceptionOperation on Arrays.asList(array) |
Result |
|---|---|
get, size, contains, indexOf, traversing |
Works normally |
set(i, value) |
Works and modifies the original array |
add, remove, clear |
UnsupportedOperationException |
If you want a real list, independent and modifiable, wrap the view:
List<String> modifiable = new ArrayList<>(Arrays.asList(names));
modifiable.add("New"); // now it worksAnd one extra trap that shows up with primitives: Arrays.asList is generic and generics do not accept primitive types, so an int[] is interpreted as a single element:
int[] numbers = { 1, 2, 3 };
List<int[]> odd = Arrays.asList(numbers); // list of ONE element (the whole array)
System.out.println(odd.size()); // 1, not 3With Integer[] it would work the way you expect. The deep cause —generics only operate on reference types— is explained in 10-01.
- Copying arrays
There are four ways to copy, and choosing badly is a common source of subtle bugs.
Material[] original = { book1, book2, book3 };
Material[] a = original; // NOT a copy: it is an alias
Material[] b = original.clone(); // shallow copy
Material[] c = Arrays.copyOf(original, original.length); // shallow copy
Material[] d = new Material[3];
System.arraycopy(original, 0, d, 0, 3); // shallow copy, with control| Form | What it does | When to use it |
|---|---|---|
b = a |
Copies nothing. Two variables, one single array | Never, if your intention was to copy |
a.clone() |
Shallow copy of the same size | A quick identical copy |
Arrays.copyOf(a, n) |
Shallow copy with a new size | Growing, trimming, defensive copy |
System.arraycopy(src, iSrc, dst, iDst, n) |
Copies n elements from one array into another existing one |
Inserting, shifting, fine control |
The first row is the blunder: Material[] a = original; creates a second name for the same array. Writing a[0] = other; also changes original[0]. It is the aliasing of 03-02 applied to arrays, and that is why the constructor of Catalog does Arrays.copyOf(materials, materials.length): a defensive copy (03-07) so that whoever built the array cannot alter the catalogue through the back door.
All copies are shallow. They copy the references, not the objects:
Material[] copy = original.clone();
copy[0].lend(); // affects THE SAME Book that 'original' sees
System.out.println(original[0].isAvailable()); // falseFor a deep copy you have to clone element by element, and that is why the immutability of 03-07 is so convenient: if the objects do not change, a shallow copy is enough and there is no risk.
System.arraycopy is the most awkward of the four but the most powerful, because it copies into an array that already exists, at whatever position you want, and it allows overlap with the same array as source and destination. It is what ArrayList.remove(int) uses to close the gap:
// Remove the element at position 1 by shifting the following ones left
Material[] c = { m0, m1, m2, m3 };
System.arraycopy(c, 2, c, 1, c.length - 2); // copies [2..3] over [1..2]
c[c.length - 1] = null; // frees the last cellIt is an O(n) operation: every element after it has to be moved. Remember this detail, because it is exactly the cost ArrayList.remove(0) will pay in 05-03 and the one LinkedList avoids in 05-04.
- Varargs is an array underneath
In 03-03 you used varargs to write methods with a variable number of arguments:
public static double sumFines(double... fines) {
double total = 0;
for (double f : fines) { total += f; } // walked like an array... because IT IS one
return total;
}The secret is that double... fines is exactly double[] fines with syntactic sugar at the call site: the compiler packs the loose arguments into an array before invoking the method.
sumFines(3.75, 1.25, 12.50); // the compiler creates new double[]{3.75, 1.25, 12.50}
sumFines(); // creates new double[0]: an EMPTY array, not null
sumFines(new double[] { 3.75, 1.25 }); // also valid: you pass it the array directlyThree practical rules follow from that:
- A varargs parameter is never
nullif it is called with the normal syntax: with no arguments a zero-length array arrives. You can walk it without checking anything. - There can be only one and it must come last:
method(String label, double... values)is valid; the other way round, it is not. - You can pass it an already built array, which is very useful for forwarding arguments between methods.
- Application: the BiblioTech catalogue
Let us bring it all together in the project's catalogue, still in its array version, and sort it with the Comparators from module 4.
package com.nexussoftware.bibliotech.service;
import java.util.Arrays;
import java.util.Comparator;
import com.nexussoftware.bibliotech.domain.Material;
/** BiblioTech's catalogue in its final array-based version. */
public class ArrayCatalog {
private Material[] materials; // internal array
private int n; // how many cells are actually occupied
public ArrayCatalog(int initialCapacity) {
this.materials = new Material[Math.max(initialCapacity, 1)];
this.n = 0;
}
/** Adds a material, growing the array if it is full. */
public void add(Material m) {
if (m == null) { return; }
if (n == materials.length) {
// the array is full: we create one twice the size and copy
materials = Arrays.copyOf(materials, materials.length * 2);
}
materials[n] = m;
n++;
}
/** Removes by reference, closing the gap with System.arraycopy. */
public boolean remove(String reference) {
for (int i = 0; i < n; i++) {
if (materials[i].getReference().equals(reference)) {
System.arraycopy(materials, i + 1, materials, i, n - i - 1);
materials[n - 1] = null; // avoids a memory leak
n--;
return true;
}
}
return false;
}
/** Returns a COPY with only the occupied cells: a defensive copy. */
public Material[] list() {
return Arrays.copyOf(materials, n);
}
/** Sorts the catalogue by any criterion, only the occupied stretch. */
public void sort(Comparator<Material> criteria) {
Arrays.sort(materials, 0, n, criteria);
}
public int size() { return n; }
}And its use, applying the comparators from 04-06:
ArrayCatalog catalog = new ArrayCatalog(4);
catalog.add(new Book("Effective Java", "Joshua Bloch", "978-0000000001", 2018));
catalog.add(new Book("Design Patterns", "Erich Gamma", "978-0000000002", 1994));
catalog.add(new Book("Refactoring", "Martin Fowler", "978-0000000003", 1999));
catalog.add(new Magazine("Java Magazine", "REV-2024-03", 42, "Monthly"));
catalog.add(new Dvd("Refactoring Live", "DVD-0007", 95)); // it grows on its own here
catalog.sort(Comparator.comparing(Material::getType)
.thenComparing(Material::getTitle));
for (Material m : catalog.list()) {
System.out.printf("%-10s %-24s %s%n", m.getType(), m.getTitle(), m.getReference());
}Book Design Patterns 978-0000000002 Book Effective Java 978-0000000001 Book Refactoring 978-0000000003 DVD Refactoring Live DVD-0007 Magazine Java Magazine REV-2024-03
It works. And it contains, written by hand, three pieces that in the next lesson the JDK will hand you ready-made: automatic growth, the shift on removal and the distinction between capacity and occupied size.
- The limitations that motivate collections
Look at ArrayCatalog with a critical eye. Everything it has beyond a List is infrastructure, not business logic:
| Array limitation | What it forces you to write | What the collection does |
|---|---|---|
| Fixed size | Keep a counter n, check n == length, double and copy |
add and nothing else: it grows on its own |
| No removal | System.arraycopy to close the gap and set null at the end |
remove |
| No search by criterion | A loop with an if for every new criterion |
contains, indexOf, removeIf |
| Capacity ≠ size | Remembering that length is not "how many there are" and doing copyOf(a, n) on return |
size() is the truth |
| No uniqueness guarantee | Checking by hand whether an ISBN already exists, with an O(n) loop | Set (05-06) |
| No access by key | An O(n) loop over the reference; worse, nested O(n²) loops to group | Map (05-05) |
| No semantics at all | A "queue" is an array plus two indices you maintain yourself | Queue, Deque (05-07, 05-08) |
None of those limitations makes the array obsolete. An array is still the right choice when:
- The size is known and fixed (a 3×12 matrix, a board, a 4096-byte buffer).
- You work with primitives and performance matters: an
int[]of a million elements takes 4 MB; aList<Integer>can take five times more because of autoboxing (05-02). - You need maximum traversal speed and cache locality is decisive.
- You are implementing a data structure, as
ArrayList,HashMapandArrayDequedo internally.
For everything else —which in a business application is practically everything— the answer is the Collections Framework.
Common Mistakes and Tips
length with parentheses. array.length() does not compile. It is a field on arrays, a method on String and size() on collections. Keep it in your head as a three-row table.
Confusing capacity with content. new Material[10] has length == 10 but zero materials. If you have only filled three cells, walking all ten will give you seven NullPointerExceptions. Always keep a counter or trim with Arrays.copyOf(array, n) before returning.
Printing an array directly. System.out.println(array) shows [I@1b6d3586. Use Arrays.toString(array) and, with more than one dimension, Arrays.deepToString(array).
Comparing arrays with equals or ==. Both compare references. Use Arrays.equals and, with more dimensions, Arrays.deepEquals.
Believing the for-each modifies the array. for (int n : numbers) { n = 0; } changes nothing: n is a copy. If you write into the cells, use a classic for. If you only call methods on the object, the for-each is perfect.
Assigning instead of copying. Material[] copy = original; does not copy; it creates an alias. Use original.clone() or Arrays.copyOf(original, original.length). And remember that both are shallow: the objects pointed at are shared.
Treating Arrays.asList as a normal list. It is a fixed-size view backed by the array: set yes, add/remove throw UnsupportedOperationException. If you need to modify it, wrap it in new ArrayList<>(...).
binarySearch on an unsorted array. It returns meaningless results, with no warning at all. Sort first, by the same criterion you are searching with. And remember that the negative returned is -(insertion point) - 1, not a plain "not found".
Forgetting to null out the freed cell on removal. In remove, after the arraycopy, the last cell still points at the object that is no longer part of the catalogue. As long as the array lives, that object cannot be collected: it is a silent memory leak. ArrayList does exactly that elementData[--size] = null for the same reason.
Style tip: for-each by default. If the loop does not need the index, write for (Material m : catalog). It is shorter, clearer and impossible to break on the bounds. Drop down to the classic for only when the index is genuinely necessary.
Exercises
Exercise 1: catalogue statistics
Write a class CatalogStatistics with static methods that take a Material[]:
int countByType(Material[] catalog, String type): how many materials of that type there are.Material mostExpensive(Material[] catalog): the one with the highest daily rate, ornullif the array is empty ornull.double averageRate(Material[] catalog): the average of the daily rates, 0.0 if there are no elements.String[] titles(Material[] catalog): a new array with the titles only.
All of them must ignore null cells and use for-each whenever possible.
Exercise 2: monthly report with a two-dimensional array
Create MonthlyReport managing an int[][] loans of 3 rows (Book, Magazine, DVD) by 12 columns (months). Implement:
void register(int type, int month): increments the corresponding cell.int totalByType(int type)andint totalByMonth(int month).int busiestMonth(): the index of the month with the most loans overall.String table(): a table formatted withprintfshowing rows, columns and totals.
Exercise 3: a toolbox with Arrays
Write CatalogUtils with static methods that apply the Arrays class:
Material[] add(Material[] catalog, Material item): returns a new array with one more element (without modifying the original).Material[] removeAt(Material[] catalog, int index): returns a new array without that element.Material[] sortedByTitle(Material[] catalog): returns a sorted copy, leaving the original untouched.int findByReference(Material[] catalog, String reference): useArrays.sort+Arrays.binarySearchon a copy and explain in a comment why the returned index is not usable on the original array.
Solutions
Solution 1
package com.nexussoftware.bibliotech.service;
import com.nexussoftware.bibliotech.domain.Material;
public final class CatalogStatistics {
private CatalogStatistics() { } // utility class: not instantiated
public static int countByType(Material[] catalog, String type) {
if (catalog == null || type == null) { return 0; }
int n = 0;
for (Material m : catalog) { // for-each: we do not need the index
if (m != null && m.getType().equals(type)) { // equals, not ==, for strings (01-05)
n++;
}
}
return n;
}
public static Material mostExpensive(Material[] catalog) {
if (catalog == null) { return null; }
Material best = null;
for (Material m : catalog) {
if (m == null) { continue; }
// the first time round 'best' is null: it has to be handled separately
if (best == null || m.getDailyRate() > best.getDailyRate()) {
best = m;
}
}
return best;
}
public static double averageRate(Material[] catalog) {
if (catalog == null) { return 0.0; }
double sum = 0.0;
int n = 0; // we count ONLY the non-null ones
for (Material m : catalog) {
if (m != null) { sum += m.getDailyRate(); n++; }
}
return (n == 0) ? 0.0 : sum / n; // protection against division by zero
}
public static String[] titles(Material[] catalog) {
if (catalog == null) { return new String[0]; } // never return null: empty array
String[] result = new String[catalog.length]; // MAXIMUM possible size
int n = 0;
for (Material m : catalog) {
if (m != null) { result[n++] = m.getTitle(); }
}
// we trim to what was actually used: the pattern ArrayList will make unnecessary
return java.util.Arrays.copyOf(result, n);
}
}The four methods share the same defensive skeleton: check null on the array, skip null elements and return a neutral value (0, a documented null, an empty array) when there is no data. The important detail in titles is the "create at maximum size, count, trim" pattern: it is the only way to return an array of the exact size when you do not know in advance how many elements there will be. It is also, exactly, what ArrayList.toArray() does for you.
Solution 2
package com.nexussoftware.bibliotech.presentation;
public class MonthlyReport {
private static final String[] TYPES = { "Books", "Magazines", "DVDs" };
private static final String[] MONTHS = { "Jan", "Feb", "Mar", "Apr", "May", "Jun",
"Jul", "Aug", "Sep", "Oct", "Nov", "Dec" };
private final int[][] loans = new int[TYPES.length][MONTHS.length]; // 3 x 12, all 0
public void register(int type, int month) {
// we check the bounds by hand: module 6 will teach us to signal it with an exception
if (type < 0 || type >= TYPES.length || month < 0 || month >= MONTHS.length) {
System.out.println("WARNING: index out of range, entry ignored");
return;
}
loans[type][month]++;
}
public int totalByType(int type) {
int total = 0;
for (int value : loans[type]) { // we walk a whole ROW: it is an int[]
total += value;
}
return total;
}
public int totalByMonth(int month) {
int total = 0;
for (int[] row : loans) { // we walk the rows and take one column
total += row[month];
}
return total;
}
public int busiestMonth() {
int bestMonth = 0;
int bestTotal = totalByMonth(0);
for (int month = 1; month < MONTHS.length; month++) { // classic for: we need the index
int total = totalByMonth(month);
if (total > bestTotal) { bestTotal = total; bestMonth = month; }
}
return bestMonth;
}
public String table() {
StringBuilder sb = new StringBuilder();
sb.append(String.format("%-10s", ""));
for (String month : MONTHS) { sb.append(String.format("%5s", month)); }
sb.append(String.format("%8s%n", "TOTAL"));
for (int t = 0; t < TYPES.length; t++) {
sb.append(String.format("%-10s", TYPES[t]));
for (int value : loans[t]) { sb.append(String.format("%5d", value)); }
sb.append(String.format("%8d%n", totalByType(t)));
}
sb.append(String.format("%-10s", "TOTAL"));
for (int month = 0; month < MONTHS.length; month++) {
sb.append(String.format("%5d", totalByMonth(month)));
}
sb.append(String.format("%8d%n", grandTotal()));
sb.append("Busiest month: ").append(MONTHS[busiestMonth()]).append('\n');
return sb.toString();
}
private int grandTotal() {
int total = 0;
for (int[] row : loans) {
for (int value : row) { total += value; }
}
return total;
}
}Notice the deliberate alternation between the two loops. totalByType walks a row with for-each because it does not need to know which month each value belongs to. totalByMonth walks the rows with for-each but indexes the column, because the month is given. And busiestMonth needs the classic for because what it returns is the index. Each loop uses the tool that belongs to it.
Solution 3
package com.nexussoftware.bibliotech.service;
import java.util.Arrays;
import java.util.Comparator;
import com.nexussoftware.bibliotech.domain.Material;
public final class CatalogUtils {
private CatalogUtils() { }
/** Returns a NEW array with one more element. The original is untouched. */
public static Material[] add(Material[] catalog, Material item) {
if (catalog == null) { return new Material[] { item }; }
// copyOf with length+1 creates the expanded array with the last cell at null
Material[] expanded = Arrays.copyOf(catalog, catalog.length + 1);
expanded[catalog.length] = item;
return expanded;
}
/** Returns a NEW array without the element at that position. */
public static Material[] removeAt(Material[] catalog, int index) {
if (catalog == null || index < 0 || index >= catalog.length) {
return catalog; // nothing to do
}
Material[] result = new Material[catalog.length - 1];
// two copies: the stretch before the index and the one after
System.arraycopy(catalog, 0, result, 0, index);
System.arraycopy(catalog, index + 1, result, index,
catalog.length - index - 1);
return result;
}
/** A copy sorted by title. The original keeps its order. */
public static Material[] sortedByTitle(Material[] catalog) {
if (catalog == null) { return new Material[0]; }
Material[] copy = catalog.clone(); // clone: shallow copy of the same size
Arrays.sort(copy, Comparator.comparing(Material::getTitle));
return copy; // the Materials are the SAME objects
}
/**
* Searches by reference with a binary search over a sorted copy.
*
* IMPORTANT: the index returned by binarySearch is the position in the SORTED
* COPY, not in the original array. That is why we do not return it: we retrieve
* the material found and look for ITS real position in the original. Confusing
* the two indices is one of the subtlest mistakes when working with arrays.
*/
public static int findByReference(Material[] catalog, String reference) {
if (catalog == null || reference == null) { return -1; }
Comparator<Material> byReference = Comparator.comparing(Material::getReference);
Material[] copy = catalog.clone();
Arrays.sort(copy, byReference);
// binarySearch needs a "probe element" carrying the reference we are after
Material probe = new Book("", "", reference, 2000);
int posInCopy = Arrays.binarySearch(copy, probe, byReference);
if (posInCopy < 0) { return -1; } // negative = insertion point, not there
Material found = copy[posInCopy];
for (int i = 0; i < catalog.length; i++) { // we translate to the real position
if (catalog[i] == found) { return i; }
}
return -1;
}
}This exercise lays bare how awkward the array is for everyday operations. add builds a whole array to insert one element: O(n) every time. removeAt needs two arraycopy calls. And findByReference costs more than the linear search it meant to avoid, because sorting the copy is O(n log n) and then the index has to be translated. The honest conclusion is that for searching by key, the array is the wrong structure: the right answer is a Map<String, Material>, with O(1) lookup and no copies or probes. That is lesson 05-05.
Conclusion
You now master the structure everything else is built on. You know that an array is a contiguous block of memory of fixed size and homogeneous type, that it is a heap object even though it has no visible class, and that its contiguity gives it O(1) access by index and a cache locality no other structure matches. You distinguish declaring, creating with new and initialising with a literal; you know the default values and you know that new Material[10] contains ten nulls, not ten materials; and you are clear that length is a field, that indices run from 0 to length - 1 and that going outside throws ArrayIndexOutOfBoundsException —an exception that module 6 will teach you to handle.
You have finally learned the for-each that was postponed in module 2: its syntax for (Type e : source), its advantages —less code, impossible to go out of range, the same shape for arrays and collections— and its three exact limitations: it does not give the index, it cannot reassign the cell (though it can modify the object pointed at) and it only goes forwards. And you have the criterion: for-each by default, classic for when the index is indispensable.
You know what each cell really holds —values in arrays of primitives, references in arrays of objects—, with everything that follows: aliasing, null, worse locality and the covariance that can throw ArrayStoreException at run time, the hole that the generics of 10-01 came along to plug. You handle multidimensional arrays understanding them as arrays of arrays, including jagged ones with rows of different lengths. And you have the Arrays class in your hands: toString/deepToString for debugging, sort with and without a Comparator, binarySearch with its two warnings, fill, copyOf, copyOfRange, equals/deepEquals versus the inherited equals that compares references, and asList with its fixed-size-view trap. You know how to copy in four ways, and that all of them are shallow. And you know that a varargs parameter is literally an array, never null, always last.
BiblioTech now has an ArrayCatalog that grows on its own, removes by closing the gap, returns defensive copies and sorts itself with any Comparator from module 4. It works. And three quarters of its code is infrastructure that should not be there: a counter n running parallel to length, an Arrays.copyOf to double the capacity, a System.arraycopy to shift, a manual null to avoid leaking memory. None of that talks about libraries or loans. On top of that, it still cannot guarantee there are no duplicate ISBNs without an O(n) loop, nor search by reference in less than O(n), nor group loans by employee without nested O(n²) loops.
In the next lesson, The Collections Framework, you will see the full map of the structures the JDK already has solved: the Iterable → Collection → List/Set/Queue hierarchy, the Map that stands apart and why, the golden rule "declare by the interface, instantiate the implementation", the master table comparing the ten implementations you will use for the rest of your professional life, how the for-each you have just learned works inside —with the Iterator underneath and the dreaded ConcurrentModificationException— and a decision tree for choosing the right collection first time. From there on, each lesson in the module will develop one row of that table, and the ArrayCatalog you have just written will shrink to half the lines.
Java Programming Course
Module 1: Introduction to Java
- Introduction to Java
- Setting Up the Development Environment
- Basic Syntax and Structure
- Variables and Data Types
- Operators
- Console Input and Output
- Your First Complete Program: BiblioTech
Module 2: Control Flow
- Conditional Statements
- Loops
- Switch Statements
- Break and Continue
- Debugging and Execution Traces
- Project: The BiblioTech Interactive Menu
Module 3: Object-Oriented Programming
- Introduction to OOP
- Classes and Objects
- Methods
- Constructors
- Inheritance
- Polymorphism
- Encapsulation
- Abstraction
- The Object Class: equals, hashCode and toString
Module 4: Advanced Object-Oriented Programming
- Interfaces
- Abstract Classes
- Inner Classes
- Anonymous Classes
- Lambda Expressions
- Functional Interfaces and Method References
- Enums and Records
Module 5: Data Structures and Collections
- Arrays
- The Collections Framework
- ArrayList
- LinkedList
- HashMap
- HashSet
- Queue and Deque
- Stack
- Sorting and Searching Collections
Module 6: Exception Handling
- Introduction to Exceptions
- The Try-Catch Block
- Throw and Throws
- Custom Exceptions
- The Finally Block
- Try-with-resources and AutoCloseable
- Error Handling Strategies and Logging
Module 7: File Input/Output
- Reading Files
- Writing Files
- File Streams
- BufferedReader and BufferedWriter
- Serialization
- The NIO.2 API: Path and Files
- Interchange Formats: CSV and Properties
Module 8: Multithreading and Concurrency
- Introduction to Multithreading
- Creating Threads
- Thread Lifecycle
- Synchronization
- Concurrency Utilities
- Concurrent Collections and Atomic Variables
- Asynchronous Tasks with CompletableFuture
Module 9: Networking
- Introduction to Networking
- Sockets
- ServerSocket
- DatagramSocket and DatagramPacket
- URL and HttpURLConnection
- The Modern HTTP Client
Module 10: Advanced Topics
- Generics
- Annotations
- Reflection
- Java 8 Features: Streams and Optional
- Dates and Times with java.time
- Java 9 and Beyond
- Memory, Garbage Collection and Performance
Module 11: Java Frameworks and Libraries
- Introduction to Java Frameworks
- Spring Framework
- Hibernate
- JUnit
- Maven
- Advanced Testing with Mockito
- Essential Ecosystem Libraries
