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:
- negative →
acomes beforeb - zero → equal
- positive →
bcomes beforea
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)istrue→a.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
defaultmethod 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:
- Make the class
finalso it can’t be subclassed. - Make all fields
private final. - Set all fields only in the constructor.
- Provide no setters.
- 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