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.
  • Queue and Deque (double-ended queue): take elements from the front or the back.
  • Map: pairs of a key and a value, each key at most once. A Map is not a Collection, but it belongs to the framework.
The Collection interface has the subinterfaces List, Set and (through Queue) Deque. ArrayList implements List; HashSet, LinkedHashSet and TreeSet implement Set; ArrayDeque implements Deque. Separately, HashMap, LinkedHashMap and TreeMap implement the Map interface. Collection Map List Set Deque ArrayList HashSet LinkedHashSet TreeSet ArrayDeque HashMap LinkedHashMap TreeMap Open boxes: interfaces. Shaded boxes: classes that implement them.
The most used parts of the framework. Collection itself extends Iterable, which is why every collection works in an enhanced for loop. Several interfaces in between (such as Queue, SequencedCollection, SortedSet and SortedMap) are left out.

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.