Arrays and Collections
Storing groups of values: arrays, the List, Set and Map interfaces with ArrayList, HashSet and HashMap, immutable List.of and Map.of, sorting and sequenced collections.
Programs rarely deal with one value at a time: a month of meter
readings, a queue of tickets, the tariff for each customer. Java
offers two families for this. Arrays are built into the language,
fast and fixed in size. Collections are ordinary classes in
java.util that grow and shrink and come in many shapes.
The general
data structures
behind them, such as dynamic arrays and hash tables, are explained
in Programming Concepts. The examples here are compact source files
with
void main(), which import java.util
automatically.
Arrays
An array holds a fixed number of values of one type, side by side in
memory, numbered from index 0. You choose the length when you create
it, and it never changes. The type is written with square brackets:
int[] is an array of int,
String[] an array of strings. Arrays can hold primitive
values directly, which makes them compact and fast for large amounts
of numbers.
void main() {
int[] monthlyKwh = { 412, 388, 395 }; // values known in advance
double[] temperatures = new double[4]; // four zeros
temperatures[0] = 18.5;
temperatures[3] = 21.0;
IO.println(monthlyKwh.length);
IO.println(monthlyKwh[0] + monthlyKwh[2]);
IO.println(Arrays.toString(temperatures));
IO.println(Arrays.toString(new String[2]));
}
3 807 [18.5, 0.0, 0.0, 21.0] [null, null]
A new array is filled with default values: 0 for
numbers, false for boolean, and
null (no object) for reference types such as
String. length is a field, not a method,
so it has no parentheses (unlike String.length() and
List.size()). An index outside 0…length - 1
stops the program:
void main() {
int[] monthlyKwh = { 412, 388, 395 };
IO.println(monthlyKwh[3]);
}
Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: Index 3 out of bounds for length 3
Arrays are objects
An array variable holds a reference to the array, not the values
themselves. Assigning it to another variable copies the reference,
so both names refer to the same array. To get an independent copy,
use Arrays.copyOf or clone(). Comparing
arrays with == or equals only checks
whether they are the same array; Arrays.equals compares
the contents.
void main() {
int[] readings = { 412, 388, 395 };
int[] alias = readings;
int[] copy = Arrays.copyOf(readings, readings.length);
alias[0] = 999;
IO.println(Arrays.toString(readings));
IO.println(Arrays.toString(copy));
int[] same = { 412, 388, 395 };
IO.println(copy.equals(same));
IO.println(Arrays.equals(copy, same));
}
[999, 388, 395] [412, 388, 395] false true
Sorting and searching arrays
The java.util.Arrays class has static helpers for
arrays: sort, binarySearch (on a sorted
array), fill, copyOfRange and
stream, which gives you the
stream
operations such as sum and max.
void main() {
int[] readings = { 412, 388, 395, 351, 298 };
Arrays.sort(readings);
IO.println(Arrays.toString(readings));
IO.println("395 is at index " + Arrays.binarySearch(readings, 395));
IO.println("Highest: " + Arrays.stream(readings).max().getAsInt());
String[] meters = { "G-007", "E-002", "E-001" };
Arrays.sort(meters);
IO.println(String.join(", ", meters));
}
[298, 351, 388, 395, 412] 395 is at index 3 Highest: 412 E-001, E-002, G-007
Arrays of arrays
Java has no true two-dimensional arrays; it has arrays whose
elements are arrays.
int[][] grid = new int[3][24] gives three rows of 24
numbers, for example hourly loads for three substations. Because
each row is its own array, rows can even have different lengths.
void main() {
int[][] loadPerShift = {
{ 120, 340, 210 }, // Delft
{ 95, 180 } // Gouda has two shifts
};
IO.println(loadPerShift[0][1]);
IO.println(loadPerShift[1].length);
IO.println(Arrays.deepToString(loadPerShift));
}
340 2 [[120, 340, 210], [95, 180]]
The Collections Framework
Arrays cannot grow, cannot hold anything but their declared type,
and have few methods. For everyday work Java code uses the
Java Collections Framework: a set of interfaces in java.util that describe kinds
of collection, plus classes that implement them. The interfaces say
what you can do; the classes decide how, with
different trade-offs in speed and ordering.
-
List: an ordered sequence with positions, duplicates allowed. -
Set: no duplicates; mainly for "is it in there?" questions. -
QueueandDeque(double-ended queue): take elements from the front or the back. -
Map: pairs of a key and a value, each key at most once. AMapis not aCollection, but it belongs to the framework.
The usual style is to declare a variable with the interface type and
create it with a class:
List<String> names = new ArrayList<>();.
The code that uses names then depends only on what a
List can do, and switching to another implementation
later means changing one line. The type in angle brackets is a type
argument (see generics); the empty
<> on the right lets the compiler repeat it for
you.
Lists
An
ArrayList is the
standard List: a
dynamic array
that grows automatically when you add elements. Reading or replacing
an element by index is fast; inserting or removing near the front
means shifting everything after it.
void main() {
List<String> onCall = new ArrayList<>();
onCall.add("Amara");
onCall.add("Mei");
onCall.add("Hamid");
onCall.add(1, "Joost"); // insert at index 1
IO.println(onCall);
IO.println(onCall.size() + " people, first is " + onCall.get(0));
IO.println(onCall.contains("Mei") + " " + onCall.indexOf("Hamid"));
onCall.set(3, "Tomasz"); // replace
onCall.remove("Amara"); // remove by value
IO.println(onCall);
}
[Amara, Joost, Mei, Hamid] 4 people, first is Amara true 3 [Joost, Mei, Tomasz]
Unlike arrays, lists print nicely: every standard collection has a
readable toString(). LinkedList also
implements List, but is rarely faster in practice;
ArrayList is the default choice.
Collections hold objects
A collection can only hold objects, so a list of numbers is a
List<Integer>, using the wrapper class
Integer. Java converts between int and
Integer automatically (autoboxing, see
variables, types and operators). Writing a primitive type in angle brackets is a compile error:
void main() {
List<int> readings = new ArrayList<>();
}
Main.java:2: error: unexpected type
Remove by index or by value
List has two remove methods:
remove(int index) and
remove(Object value). With a
List<Integer>, an int
argument picks the index version, because that needs no boxing.
void main() {
List<Integer> readings = new ArrayList<>(List.of(10, 20, 30));
readings.remove(1); // removes the element at index 1
IO.println(readings);
readings.remove(Integer.valueOf(10)); // removes the value 10
IO.println(readings);
}
[10, 30] [30]
Sets
A Set keeps each value at most once; adding a duplicate
simply returns false. Its strength is a fast
contains. Java has three standard sets that differ in
the order in which they return their elements:
-
HashSet: the fastest, based on a hash table; the order is unpredictable. LinkedHashSet: keeps insertion order.TreeSet: keeps the elements sorted.
void main() {
List<String> alarms = List.of("G-007", "E-002", "E-001", "E-002", "G-007");
Set<String> seen = new HashSet<>(alarms);
IO.println(seen.size() + " distinct meters, E-001 among them: " + seen.contains("E-001"));
IO.println(seen.add("E-001"));
IO.println(new LinkedHashSet<>(alarms));
IO.println(new TreeSet<>(alarms));
}
3 distinct meters, E-001 among them: true false [G-007, E-002, E-001] [E-001, E-002, G-007]
The example does not print the HashSet itself. Its
order depends on the hash codes of the elements and on the set's
internal size; it is neither insertion order nor sorted, and code
must never rely on it. When order matters, choose
LinkedHashSet or TreeSet. Sets of your own
classes need correct equals and
hashCode methods (see
classes and objects).
Maps
A
HashMap is the standard
Map: it stores key-value pairs in a hash table, so
looking up a value by its key takes about the same time however
large the map grows. Like HashSet, it makes no promise
about order; LinkedHashMap keeps insertion order and
TreeMap keeps the keys sorted. The concept is the same
as a Python
dictionary.
void main() {
Map<String, Double> tariffs = new HashMap<>();
tariffs.put("Day", 0.32);
tariffs.put("Night", 0.24);
tariffs.put("Day", 0.31); // replaces the old value
IO.println(tariffs.get("Day"));
IO.println(tariffs.size() + " tariffs, has Weekend: " + tariffs.containsKey("Weekend"));
Map<String, Double> sorted = new TreeMap<>(tariffs);
IO.println(sorted);
for (var entry : sorted.entrySet()) {
IO.println(entry.getKey() + " costs " + entry.getValue());
}
}
0.31
2 tariffs, has Weekend: false
{Day=0.31, Night=0.24}
Day costs 0.31
Night costs 0.24
Missing keys
get returns null for a key that is not in
the map. That is easy to miss, and it turns into a
NullPointerException as soon as the result is unboxed
to a primitive. getOrDefault gives a fallback instead.
void main() {
Map<String, Integer> usage = new HashMap<>();
usage.put("E-001", 350);
IO.println(usage.get("E-999"));
IO.println(usage.getOrDefault("E-999", 0));
int kwh = usage.get("E-999");
IO.println(kwh);
}
null 0 Exception in thread "main" java.lang.NullPointerException: Cannot invoke "java.lang.Integer.intValue()" because the return value of "java.util.Map.get(Object)" is null
The message names exactly what was null: the return value of
Map.get, which Java tried to turn into an
int with Integer.intValue(). More about
these messages on
null, Optional and NullPointerException.
Counting and grouping
Two methods make the common "update or insert" patterns short.
merge(key, value, function) stores the value for a new
key and otherwise combines the old and new values, which is ideal
for counting and summing.
computeIfAbsent(key, function)
creates a value the first time a key is seen and returns it, which
is ideal for building lists per key.
void main() {
List<String> tickets = List.of(
"INC-10041 NOC", "INC-10042 Billing", "INC-10043 NOC", "INC-10044 Portal");
Map<String, Integer> perTeam = new TreeMap<>();
Map<String, List<String>> idsPerTeam = new TreeMap<>();
for (String line : tickets) {
String id = line.substring(0, 9);
String team = line.substring(10);
perTeam.merge(team, 1, Integer::sum);
idsPerTeam.computeIfAbsent(team, t -> new ArrayList<>()).add(id);
}
IO.println(perTeam);
IO.println(idsPerTeam);
}
{Billing=1, NOC=2, Portal=1}
{Billing=[INC-10042], NOC=[INC-10041, INC-10043], Portal=[INC-10044]}
Integer::sum and
t -> new ArrayList<>()
are a method reference and a lambda: small pieces of code passed as
arguments (see
lambdas, functional interfaces and streams). The example uses TreeMap so that the printed order
is sorted and the same on every run.
Unmodifiable collections
List.of(…), Set.of(…) and
Map.of(…) (Java 9) create an
unmodifiable collection: every method that would change it throws
UnsupportedOperationException. That makes them safe to
share and to return from methods, because nobody can change them
behind your back. They also reject null elements, and
Map.of and Set.of reject duplicates.
void main() {
List<String> priorities = List.of("P1", "P2", "P3", "P4");
IO.println(priorities.get(0));
priorities.add("P5");
}
P1 Exception in thread "main" java.lang.UnsupportedOperationException
To get a changeable list from fixed values, wrap them:
new ArrayList<>(List.of(…)), as in several
examples on this page. Two related tools behave differently and are
worth knowing:
void main() {
List<Integer> readings = new ArrayList<>(List.of(412, 388));
List<Integer> view = Collections.unmodifiableList(readings);
List<Integer> snapshot = List.copyOf(readings);
readings.add(395);
IO.println("view: " + view + ", snapshot: " + snapshot);
List<String> fixedSize = Arrays.asList("E-001", "E-002");
fixedSize.set(1, "G-007");
IO.println(fixedSize);
}
view: [412, 388, 395], snapshot: [412, 388] [E-001, G-007]
Collections.unmodifiableList returns a read-only
view: you cannot change the list through it, but changes to
the original show through. List.copyOf makes a real
unmodifiable copy. Arrays.asList wraps an array in a
list of fixed size: set works, add and
remove throw
UnsupportedOperationException.
Sorting
list.sort(null) or
Collections.sort(list) sorts a list in its
natural order: numbers ascending, strings alphabetically
(by character code, so uppercase before lowercase), dates
chronologically. Classes with a natural order implement the
interface Comparable and its
compareTo method. For any other order, pass a
Comparator: an object that compares two elements and returns a negative
number, zero or a positive number. The factory methods
Comparator.comparing, thenComparing and
reversed build one from the fields you want to sort on.
void main() {
List<Ticket> tickets = new ArrayList<>(List.of(
new Ticket("INC-10044", "P2", 35),
new Ticket("INC-10041", "P1", 50),
new Ticket("INC-10043", "P2", 10),
new Ticket("INC-10042", "P1", 20)));
tickets.sort(Comparator.comparing(Ticket::priority).thenComparing(Ticket::id));
tickets.forEach(t -> IO.println(t.priority() + " " + t.id()));
tickets.sort(Comparator.comparingInt(Ticket::minutesOpen).reversed());
IO.println("Oldest: " + tickets.getFirst().id());
List<Integer> minutes = new ArrayList<>(List.of(35, 50, 10, 20));
minutes.sort(Comparator.reverseOrder());
IO.println(minutes + ", max " + Collections.max(minutes));
}
record Ticket(String id, String priority, int minutesOpen) {}
P1 INC-10041 P1 INC-10042 P2 INC-10043 P2 INC-10044 Oldest: INC-10041 [50, 35, 20, 10], max 50
Ticket::priority is a method reference to the record's
accessor method; Ticket is a
record declared
below main. Java's list sort is stable:
elements that compare as equal keep their earlier order. A
TreeSet or TreeMap can take a
Comparator in its constructor to sort its contents in a
custom order.
Sequenced collections
For years, getting the last element of a list meant
list.get(list.size() - 1), and every collection type
had its own way to do it, or none. Java 21 added the interfaces
SequencedCollection, SequencedSet and
SequencedMap. A
sequenced collection is
one with a defined encounter order, and it offers the same methods
everywhere: getFirst(), getLast(),
addFirst, addLast,
removeFirst, removeLast and
reversed(). Lists, deques, LinkedHashSet,
TreeSet, LinkedHashMap and
TreeMap are sequenced; HashSet and
HashMap are not, because they have no defined order.
void main() {
List<String> shift = new ArrayList<>(List.of("Amara", "Mei", "Hamid"));
IO.println(shift.getFirst() + " to " + shift.getLast());
IO.println(shift.reversed());
shift.addFirst("Sanne");
shift.removeLast();
IO.println(shift);
LinkedHashMap<String, Integer> readingsInOrder = new LinkedHashMap<>();
readingsInOrder.put("2026-07", 298);
readingsInOrder.put("2026-08", 260);
readingsInOrder.put("2026-09", 351);
IO.println(readingsInOrder.firstEntry() + " ... " + readingsInOrder.lastEntry());
IO.println(readingsInOrder.sequencedKeySet().reversed());
}
Amara to Hamid [Hamid, Mei, Amara] [Sanne, Amara, Mei] 2026-07=298 ... 2026-09=351 [2026-09, 2026-08, 2026-07]
reversed() returns a reversed view, not a
copy, so it is cheap even for large collections. On an unmodifiable
list, getFirst() and getLast() work, but
addFirst and removeLast throw
UnsupportedOperationException, like every other change.
Choosing a collection
| You need | Use | Notes |
|---|---|---|
| an ordered list that grows | ArrayList |
the default collection |
| a fixed list of values that never changes | List.of(…) |
unmodifiable, no nulls |
| many numbers, fixed count, maximum speed | int[], double[] |
no boxing, but no methods either |
unique values, fast contains |
HashSet (any order),
LinkedHashSet (insertion order),
TreeSet (sorted)
|
needs correct equals/hashCode
|
| look up a value by a key |
HashMap, LinkedHashMap,
TreeMap
|
same order choice as for sets |
| a queue or stack | ArrayDeque |
offer/poll for a queue,
push/pop for a stack
|
| sharing between threads |
ConcurrentHashMap and friends in
java.util.concurrent
|
see concurrency |
How fast each operation is, for example contains on a
list (it checks every element) versus on a set (one hash lookup), is
the subject of
algorithmic complexity.