Chapter 2: Java Prerequisites for DSA
2.03Beginner10 topics20 min

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

Java language essentialsArrays and types
2.03.1

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.

Java
1class Counter {
2 private int count;
3 void inc() { count++; }
4 int 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
2.03.2

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.

Java
1class Interval {
2 final int lo, hi;
3 Interval(int lo, int hi) {
4 if (lo > hi) throw new IllegalArgumentException("lo > hi");
5 this.lo = lo; this.hi = hi;
6 }
7 Interval(int point) { this(point, point); }
8}
9record Range(int lo, int hi) { Range { if (lo > hi) throw new IllegalArgumentException(); } }
2.03.3

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].

Java
1static <T extends Comparable<T>> T maxOf(List<T> xs) {
2 T best = xs.get(0);
3 for (T x : xs) if (x.compareTo(best) > 0) best = x;
4 return best;
5}
6class Stack<T> {
7 @SuppressWarnings("unchecked")
8 private T[] data = (T[]) new Object[8];
9 private int size;
10 void push(T v) { if (size == data.length) data = Arrays.copyOf(data, size * 2); data[size++] = v; }
11 T pop() { return data[--size]; }
12}
2.03.4

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.

Java
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();
2.03.5

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).

Java
1record Version(int major, int minor) implements Comparable<Version> {
2 public int compareTo(Version o) {
3 int c = Integer.compare(major, o.major);
4 return 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]
2.03.6

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.

Java
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, Bo
4ps.sort(Comparator.comparingInt(P::age).reversed());
5PriorityQueue<int[]> pq = new PriorityQueue<>((x, y) -> Integer.compare(x[1], y[1]));
2.03.7

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.

Java
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
2.03.8

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.

Java
1class IntRange implements Iterable<Integer> {
2 private final int lo, hi;
3 IntRange(int lo, int hi) { this.lo = lo; this.hi = hi; }
4 public Iterator<Integer> iterator() {
5 return new Iterator<>() {
6 int cur = lo;
7 public boolean hasNext() { return cur < hi; }
8 public Integer next() { return cur++; }
9 };
10 }
11}
12for (int x : new IntRange(0, 3)) System.out.print(x); // 012
2.03.9

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.

Java
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); // true
6Map<String, Integer> m = new HashMap<>();
7// int v = m.get("missing"); // NullPointerException: unboxing null
2.03.10

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.

Java
1static String describe(String s) {
2 Objects.requireNonNull(s, "s");
3 return s.isEmpty() ? "empty" : s;
4}
5Optional<Integer> first = Stream.of(5, 8).filter(x -> x > 6).findFirst();
6int v = first.orElse(-1); // 8
7Map<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 == b is false

Medium

  • Write a generic Pair<A, B> and a minBy method 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.