Java Interview Cheatsheet

Published on June 16, 2026

Quick reference for Java built-in data structures — the things that always trip you up mid-interview.


Arrays

Array length is an instance variable, not a method call.

int[] arr = {3, 1, 4, 1, 5};
int n = arr.length;   // no parentheses

Sort an array in ascending order with Arrays.sort():

Arrays.sort(arr);  // [1, 1, 3, 4, 5]

Pass a custom comparator to sort in a different order. Comparators only work on object arrays (use Integer[], not int[]):

Integer[] arr = {3, 1, 4, 1, 5};

// Sort descending
Arrays.sort(arr, (a, b) -> b - a);  // [5, 4, 3, 1, 1]

// Sort by absolute value
Integer[] nums = {-3, 1, -4, 2};
Arrays.sort(nums, (a, b) -> Math.abs(a) - Math.abs(b));  // [1, 2, -3, -4]

Priority Queue (Heap)

PriorityQueue is a min-heap by default — poll() returns the smallest element.

PriorityQueue<Integer> minHeap = new PriorityQueue<>();
minHeap.add(5);
minHeap.add(1);
minHeap.add(3);

minHeap.peek();   // 1  — view top without removing
minHeap.poll();   // 1  — remove and return top
minHeap.isEmpty(); // false

Pass a comparator to the constructor to get a max-heap or custom ordering:

// Max-heap
PriorityQueue<Integer> maxHeap = new PriorityQueue<>((a, b) -> b - a);

// Min-heap by string length
PriorityQueue<String> byLength = new PriorityQueue<>((a, b) -> a.length() - b.length());

Comparator — how to tune ordering

A comparator must return:

  • negativea comes before b
  • zero → equal
  • positiveb comes before a

Since PriorityQueue is a min-heap, the element the comparator considers “smallest” (most negative) pops first.

Ascending (smallest first):

(a, b) -> Integer.compare(a, b)   // 2 pops before 5

Descending (largest first — max-heap):

(a, b) -> Integer.compare(b, a)   // 5 pops before 2

Custom objects — sort Tweets by timestamp:

// Oldest first
PriorityQueue<Tweet> pq = new PriorityQueue<>((a, b) -> Integer.compare(a.createdAt, b.createdAt));

// Newest first
PriorityQueue<Tweet> pq = new PriorityQueue<>((a, b) -> Integer.compare(b.createdAt, a.createdAt));

Multi-level sort — age ascending, salary descending:

Comparator<Employee> cmp =
    Comparator.comparingInt((Employee e) -> e.age)
              .thenComparing((a, b) -> Integer.compare(b.salary, a.salary));

The one rule to remember:

Integer.compare(X, Y) means X pops before Y.

So if you want the largest value out first, put the larger-valued expression first:

Integer.compare(b.field, a.field)  // b (larger) comes before a (smaller)

If you want the smallest first:

Integer.compare(a.field, b.field)  // a (smaller) comes before b (larger)

Avoid a - b style subtraction — it overflows on large integers. Always use Integer.compare.


Strings

Length is a method on strings (unlike arrays where it’s a field):

String s = "hello";
int n = s.length();   // parentheses required

Strings are immutable — every operation returns a new string.

Use .equals() for value comparison, not ==. The == operator only checks reference equality.

String a = new String("hello");
String b = new String("hello");

a == b        // false — different objects in memory
a.equals(b)   // true  — same content

hashCode() and the contract with equals()

By default, hashCode() returns a value derived from the object’s memory address — so two new objects will have different hashcodes even if their contents are identical.

The contract Java requires:

  • If a.equals(b) is truea.hashCode() == b.hashCode() must be true.
  • If a.hashCode() == b.hashCode()a.equals(b) is not necessarily true (hash collision).

This matters because HashMap and HashSet use hashCode() first to find the bucket, then equals() to confirm the match. If you override equals() without overriding hashCode(), two logically equal objects will land in different buckets and the map/set will treat them as distinct keys.

class Point {
    int x, y;

    @Override
    public boolean equals(Object o) {
        Point p = (Point) o;
        return x == p.x && y == p.y;
    }

    @Override
    public int hashCode() {
        return Objects.hash(x, y); // must include same fields as equals()
    }
}

Set<Point> set = new HashSet<>();
set.add(new Point(1, 2));
set.contains(new Point(1, 2)); // true — only works because both methods are overridden

Set

All collections in Java use .size() (not .length) to get their size.

Set<Integer> s = new HashSet<>();

s.add(1);
s.add(2);
s.add(1);       // duplicate, ignored

s.size();        // 2
s.contains(1);   // true
s.contains(99);  // false
s.remove(1);     // removes 1, returns true; returns false if not present

HashMap

Map<String, Integer> map = new HashMap<>();

map.put("apple", 1);
map.put("banana", 2);

map.get("apple");               // 1
map.get("missing");             // null
map.getOrDefault("missing", 0); // 0 — safe fallback
map.remove("apple");            // removes entry, returns the value (1) or null
map.containsKey("banana");      // true

getOrDefault also accepts a lambda or expression as the default value:

// Default to the result of some computation
map.getOrDefault(key, computeDefault());

Use computeIfAbsent when the default value is a new collection — it only computes and inserts when the key is absent, then returns the value. This lets you chain the update on the same line:

Map<String, List<Integer>> groups = new HashMap<>();

// Without computeIfAbsent — verbose
groups.putIfAbsent("a", new ArrayList<>());
groups.get("a").add(1);

// With computeIfAbsent — idiomatic
groups.computeIfAbsent("a", k -> new ArrayList<>()).add(1);

The lambda k -> new ArrayList<>() receives the missing key k and returns the value to insert. The whole expression evaluates to the list, so .add(1) works directly on the returned list.


TreeSet

TreeSet stores elements in sorted order (natural ordering by default). All operations are O(log n). Use it when you need sorted membership and range queries — things HashSet can’t do.

TreeSet<Integer> ts = new TreeSet<>();
ts.add(10);
ts.add(30);
ts.add(20);
ts.add(50);
// internal order: [10, 20, 30, 50]

ts.first();   // 10 — smallest element
ts.last();    // 50 — largest element
ts.size();    // 4
ts.contains(20); // true

Range queries — the four key methods:

ts.floor(25);    // 20 — largest element <= 25
ts.ceiling(25);  // 30 — smallest element >= 25
ts.lower(20);    // 10 — largest element strictly < 20
ts.higher(20);   // 30 — smallest element strictly > 20

All four return null if no such element exists, so null-check before using the result.

Cross-language equivalents — given a sorted list a and query value x:

Java (TreeSet) C++ Python (bisect)
ceiling(x) — smallest ≥ x lower_bound(x) a[bisect.bisect_left(a, x)]
higher(x) — smallest > x upper_bound(x) a[bisect.bisect_right(a, x)]
floor(x) — largest ≤ x *prev(upper_bound(x)) a[bisect.bisect_right(a, x) - 1]
lower(x) — largest < x *prev(lower_bound(x)) a[bisect.bisect_left(a, x) - 1]

Python’s bisect_left and bisect_right return indices, not values — index into the list to get the element. Always bounds-check the index before using it (an index of -1 or >= len(a) means no such element exists).

import bisect

a = [10, 20, 30, 50]

# ceiling(25) — smallest >= 25
i = bisect.bisect_left(a, 25)   # i = 2
a[i]                             # 30

# higher(20) — smallest > 20
i = bisect.bisect_right(a, 20)  # i = 2
a[i]                             # 30

# floor(25) — largest <= 25
i = bisect.bisect_right(a, 25) - 1  # i = 1
a[i]                                 # 20

# lower(20) — largest < 20
i = bisect.bisect_left(a, 20) - 1   # i = 0
a[i]                                 # 10

TreeMap

TreeMap is a sorted map — keys are kept in natural order (or a custom comparator). It gives you all the HashMap operations plus the same range query methods on keys.

TreeMap<Integer, String> tm = new TreeMap<>();
tm.put(10, "ten");
tm.put(30, "thirty");
tm.put(20, "twenty");
tm.put(50, "fifty");
// key order: 10, 20, 30, 50

tm.firstKey();  // 10
tm.lastKey();   // 50

Key-only range queries:

tm.floorKey(25);    // 20 — largest key <= 25
tm.ceilingKey(25);  // 30 — smallest key >= 25
tm.lowerKey(20);    // 10 — largest key strictly < 20
tm.higherKey(20);   // 30 — smallest key strictly > 20

Entry range queries — same methods but return the full key-value pair:

Map.Entry<Integer, String> e = tm.floorEntry(25);
e.getKey();    // 20
e.getValue();  // "twenty"

Example — find the closest appointment:

// Keys are timestamps, values are appointment names
TreeMap<Integer, String> calendar = new TreeMap<>();
calendar.put(900,  "standup");
calendar.put(1200, "lunch");
calendar.put(1500, "review");

int now = 1100;

// What's the next appointment from now?
Map.Entry<Integer, String> next = calendar.ceilingEntry(now);
// next.getKey() = 1200, next.getValue() = "lunch"

// What was the last appointment before now?
Map.Entry<Integer, String> prev = calendar.lowerEntry(now);
// prev.getKey() = 900, prev.getValue() = "standup"

Submap — get all keys in a range:

// Keys from 1000 to 1400 (inclusive on both ends)
SortedMap<Integer, String> window = calendar.subMap(1000, true, 1400, true);
// {1200 -> "lunch"}

ArrayList

ArrayList is a resizable array. Use it when you need indexed access and don’t care about fast insertions at the front.

List<Integer> list = new ArrayList<>();

list.add(10);       // append to end
list.add(20);
list.add(1, 99);    // insert 99 at index 1 — shifts elements right

list.get(0);        // 10  — O(1) random access
list.set(0, 55);    // replace element at index 0
list.remove(0);     // remove by index — shifts elements left
list.remove(Integer.valueOf(99)); // remove by value (need Integer, not int)

list.size();        // number of elements
list.contains(20);  // true
list.isEmpty();     // false

Iterate over elements:

// Index-based
for (int i = 0; i < list.size(); i++) {
    System.out.println(list.get(i));
}

// Enhanced for-loop (preferred when index isn't needed)
for (int val : list) {
    System.out.println(val);
}

Sort an ArrayList with Collections.sort() or list.sort():

Collections.sort(list);                        // ascending
list.sort((a, b) -> b - a);                   // descending via comparator

LinkedList

LinkedList is a doubly-linked list. It implements both List and Deque, so it’s commonly used as a queue or stack in interviews.

LinkedList<Integer> ll = new LinkedList<>();

// Add elements
ll.add(1);           // append to tail
ll.addFirst(0);      // prepend to head
ll.addLast(2);       // append to tail (same as add)

// Access elements
ll.get(0);           // O(n) — traverses from head, avoid in hot loops
ll.getFirst();       // O(1) — peek at head
ll.getLast();        // O(1) — peek at tail

// Remove elements
ll.removeFirst();    // remove and return head
ll.removeLast();     // remove and return tail
ll.remove(1);        // remove by index

ll.size();
ll.isEmpty();

Used as a queue (FIFO):

Queue<Integer> queue = new LinkedList<>();

queue.offer(1);   // enqueue — add to tail (prefer over add(), won't throw on capacity)
queue.offer(2);
queue.offer(3);

queue.peek();     // 1 — view front without removing
queue.poll();     // 1 — remove and return front

Used as a stack (LIFO) — though ArrayDeque is faster in practice:

Deque<Integer> stack = new LinkedList<>();

stack.push(1);    // push to front
stack.push(2);
stack.push(3);

stack.peek();     // 3 — view top
stack.pop();      // 3 — remove and return top


Commonly Asked Questions

Abstract Class vs Interface — what’s the difference and which is preferred?

  Abstract Class Interface
Instantiation Cannot be instantiated Cannot be instantiated
Methods Can have abstract and concrete methods All methods abstract by default; default methods allowed since Java 8
Fields Can have instance variables (state) Only public static final constants
Constructors Yes No
Inheritance A class can extend only one abstract class A class can implement multiple interfaces
Access modifiers Methods can be any visibility Methods are public by default

Which is preferred?

Prefer interfaces in most cases. They let you:

  • Implement multiple interfaces (avoiding the single-inheritance limit)
  • Program to an abstraction without committing to a class hierarchy
  • Add default method implementations without breaking existing code

Use an abstract class when:

  • You need to share state (instance variables) across subclasses
  • You need a constructor to enforce initialization
  • Subclasses are tightly related and share a lot of common behavior that isn’t just a contract
// Interface — defines a contract
interface Flyable {
    void fly();
    default String describe() { return "I can fly"; }
}

// Abstract class — shares state and partial implementation
abstract class Animal {
    private String name;  // shared state

    Animal(String name) { this.name = name; }

    public String getName() { return name; }
    abstract void speak();  // subclasses must implement
}

How Java avoids the diamond problem

The diamond problem: if class D inherits from both B and C, and both B and C override a method from their common parent A, which version does D get?

Java sidesteps this entirely for classes — you can only extend one class, so the diamond can never form in the class hierarchy.

class A { void hello() { System.out.println("A"); } }
class B extends A { void hello() { System.out.println("B"); } }
class C extends A { void hello() { System.out.println("C"); } }

// This is a compile error — Java doesn't allow it
class D extends B, C { }  // ERROR: cannot extend multiple classes

With interfaces it’s trickier, because default methods (added in Java 8) can have implementations. If two interfaces both define a default method with the same signature, the implementing class must override it — the compiler forces you to resolve the ambiguity explicitly.

interface B {
    default String hello() { return "Hello from B"; }
}

interface C {
    default String hello() { return "Hello from C"; }
}

// Compile error if you don't override hello()
class D implements B, C {
    @Override
    public String hello() {
        return B.super.hello(); // explicitly pick one, or write your own
    }
}

If only one of the interfaces provides a default implementation and the other declares the method as abstract, there’s no conflict — the concrete default wins automatically.

interface B {
    default String hello() { return "Hello from B"; }
}

interface C {
    String hello(); // abstract — no implementation
}

class D implements B, C {
    // No override needed — B's default satisfies C's contract too
}

How do you make an object immutable?

An immutable object cannot be modified after construction. The pattern:

  1. Make the class final so it can’t be subclassed.
  2. Make all fields private final.
  3. Set all fields only in the constructor.
  4. Provide no setters.
  5. In getters for mutable fields (collections, arrays, other objects), return a deep copy, not the original reference.
import java.util.ArrayList;
import java.util.List;

public final class Student {
    private final String name;
    private final List<String> courses;

    public Student(String name, List<String> courses) {
        this.name = name;
        this.courses = new ArrayList<>(courses); // defensive copy on the way in
    }

    public String getName() {
        return name; // String is already immutable, safe to return directly
    }

    public List<String> getCourses() {
        return new ArrayList<>(courses); // deep copy on the way out
    }
}

Why the deep copy in the getter matters:

Student s = new Student("Alice", List.of("Math", "CS"));
List<String> c = s.getCourses();
c.add("Physics");           // modifies the copy, not the internal list
s.getCourses().size();      // still 2 — object is unaffected

If you returned courses directly, the caller could mutate the internal list and break immutability even without a setter.

Tags: java, interview, data-structures