Objects and collections
Classes, constructors, generics, interfaces, Comparable and Comparator, Iterator and Iterable, boxing and null handling: the object-oriented features that custom data structures and collection code rely on.
Learning goals
- Define classes and records for algorithmic data
- Write generic classes and methods
- Order objects with Comparable and Comparator
- Make a class iterable
- Avoid boxing and null pitfalls
Prerequisites
Classes
A class bundles fields (state) and methods (behaviour). For data holders, a record (Java 16+) generates the constructor, accessors, equals, hashCode and toString, and its fields are final.
Prefer records for small immutable values such as points, edges and intervals.
1class Counter {2private int count;3void inc() { count++; }4int get() { return count; }5}6record Edge(int from, int to, int weight) {}7Edge e = new Edge(0, 1, 7);8int w = e.weight(); // accessor, not a field access
Constructors
A constructor initialises a new object. It has the class name and no return type. Chain constructors with this(...) and validate arguments there so an object is never created in an invalid state.
A record's compact constructor is a good place to validate.
1class Interval {2final int lo, hi;3Interval(int lo, int hi) {4if (lo > hi) throw new IllegalArgumentException("lo > hi");5this.lo = lo; this.hi = hi;6}7Interval(int point) { this(point, point); }8}9record Range(int lo, int hi) { Range { if (lo > hi) throw new IllegalArgumentException(); } }
Generics
Generics let a class or method work with any type while keeping type safety: Box<T>, List<String>. The type parameter is erased at runtime, so you cannot create new T[] or new ArrayList<int>. Use bounds like <T extends Comparable<T>> to require capabilities.
Generic arrays need an unchecked cast: (T[]) new Object[n].
1static <T extends Comparable<T>> T maxOf(List<T> xs) {2T best = xs.get(0);3for (T x : xs) if (x.compareTo(best) > 0) best = x;4return best;5}6class Stack<T> {7@SuppressWarnings("unchecked")8private T[] data = (T[]) new Object[8];9private int size;10void push(T v) { if (size == data.length) data = Arrays.copyOf(data, size * 2); data[size++] = v; }11T pop() { return data[--size]; }12}
Interfaces
An interface declares behaviour without storing state (default and static methods are allowed). Program against interfaces: declare List<Integer> xs = new ArrayList<>() so the implementation can change. A class can implement several interfaces.
A functional interface has exactly one abstract method and can be written as a lambda, for example Comparator.
1interface Shape { double area(); }2record Circle(double r) implements Shape { public double area() { return Math.PI * r * r; } }3record Square(double s) implements Shape { public double area() { return s * s; } }4Shape s = new Circle(1);5double a = s.area();
Comparable
Comparable<T> gives a class a natural order through compareTo, which returns a negative number, zero or a positive number. Sorted collections (TreeSet, TreeMap), Arrays.sort on objects and Collections.sort use it.
Keep compareTo consistent with equals, and avoid a - b for ints because it can overflow. Use Integer.compare(a, b).
1record Version(int major, int minor) implements Comparable<Version> {2public int compareTo(Version o) {3int c = Integer.compare(major, o.major);4return c != 0 ? c : Integer.compare(minor, o.minor);5}6}7List<Version> vs = new ArrayList<>(List.of(new Version(1, 2), new Version(1, 0)));8Collections.sort(vs); // [1.0, 1.2]
Comparator
A Comparator<T> is an external ordering, used when you need a different order from the natural one or the class is not yours. Build it with Comparator.comparing, thenComparing and reversed.
Sort by a primary key then a tie-breaker to get deterministic results.
1record P(String name, int age) {}2List<P> ps = new ArrayList<>(List.of(new P("Bo", 30), new P("Al", 30), new P("Cy", 25)));3ps.sort(Comparator.comparingInt(P::age).thenComparing(P::name)); // Cy, Al, Bo4ps.sort(Comparator.comparingInt(P::age).reversed());5PriorityQueue<int[]> pq = new PriorityQueue<>((x, y) -> Integer.compare(x[1], y[1]));
Iterator
An Iterator<T> walks a collection with hasNext() and next() and can remove the last returned element with remove(). Removing through the collection while iterating throws ConcurrentModificationException; remove through the iterator instead.
removeIf is simpler for filtering.
1List<Integer> xs = new ArrayList<>(List.of(1, 2, 3, 4, 5, 6));2Iterator<Integer> it = xs.iterator();3while (it.hasNext()) if (it.next() % 2 == 0) it.remove(); // [1, 3, 5]4// xs.removeIf(x -> x % 2 == 0); // same result in one line
Iterable
A class that implements Iterable<T> provides an iterator() and can be used in the enhanced for loop. Custom linked lists, trees and ranges become easy to use this way.
Keep the iterator's state in the iterator object, not the collection, so several loops can run independently.
1class IntRange implements Iterable<Integer> {2private final int lo, hi;3IntRange(int lo, int hi) { this.lo = lo; this.hi = hi; }4public Iterator<Integer> iterator() {5return new Iterator<>() {6int cur = lo;7public boolean hasNext() { return cur < hi; }8public Integer next() { return cur++; }9};10}11}12for (int x : new IntRange(0, 3)) System.out.print(x); // 012
Autoboxing and unboxing
Java converts between primitives and wrappers automatically (int ↔ Integer). It is convenient but allocates objects, uses more memory and can throw NullPointerException when unboxing null. Comparing two Integer with == compares references, and only values from -128 to 127 are cached.
Use equals for wrappers and primitive arrays in hot loops.
1Integer a = 127, b = 127;2Integer c = 1000, d = 1000;3boolean x = a == b; // true (cached)4boolean y = c == d; // false (different objects)5boolean z = c.equals(d); // true6Map<String, Integer> m = new HashMap<>();7// int v = m.get("missing"); // NullPointerException: unboxing null
Null handling
null means no object. Calling a method on it throws NullPointerException. Design to avoid null: return empty collections instead of null, use Optional for values that may be absent, and validate arguments with Objects.requireNonNull.
Remember that ArrayDeque, TreeMap keys and List.of do not accept null.
1static String describe(String s) {2Objects.requireNonNull(s, "s");3return s.isEmpty() ? "empty" : s;4}5Optional<Integer> first = Stream.of(5, 8).filter(x -> x > 6).findFirst();6int v = first.orElse(-1); // 87Map<String, Integer> m = new HashMap<>();8int count = m.getOrDefault("k", 0);
Practice ladder
Now try it yourself
Easy
- Write a record for a 2D point and sort a list of points by x
- Explain why
Integer a = 1000, b = 1000; a == bis false
Medium
- Write a generic
Pair<A, B>and aminBymethod using a Comparator - Make a singly linked list class implement Iterable
Advanced
- Implement a generic min-stack that supports push, pop and getMin in O(1)
Key takeaways
- Use records for values, interfaces for abstractions and generics for reusable structures.
- Comparable defines natural order, Comparator an external one; use Integer.compare, not subtraction.
- Boxing allocates and null unboxing throws; prefer primitives in hot paths.