Last Updated: March 31, 2026
This chapter is a focused crash course on the Java features you will use repeatedly while learning DSA and preparing for coding interviews. Instead of covering the entire language, we will concentrate only on the parts that matter for solving problems efficiently in interviews.
Before we get into the language itself, let us address something practical. On LeetCode, most imports are available automatically. But when writing Java locally or in some interview platforms, you need to know what to import. Here are the imports that cover 95% of DSA problems:
The wildcard java.util.* covers almost everything you need. In a real interview, you can write this at the top and move on. Do not waste time importing individual classes.
Java is a statically typed language, which means every variable must have a declared type. For DSA, you will use a small set of primitive types over and over.
| Type | Size | Range | DSA Use Case |
|---|---|---|---|
int | 4 bytes | -2.1B to 2.1B | Array indices, counters, most values |
long | 8 bytes | -9.2 x 10^18 to 9.2 x 10^18 | Prefix sums, large products, overflow-prone math |
char | 2 bytes | 0 to 65,535 | String characters, frequency arrays |
boolean | 1 bit* | true / false | Visited arrays, flags |
double | 8 bytes | ±1.7 x 10^308 | Rarely used, some geometry problems |
Each primitive type has a corresponding wrapper class (Integer, Long, Character, Boolean, Double) that lets you use primitives in collections. We will cover this in detail in the autoboxing section.
The most dangerous pitfall with types is integer overflow. Consider the classic binary search:
This is not a theoretical concern. When left and right are both above 1 billion, their sum exceeds int range and wraps to a negative number. The safe version avoids this by computing the difference first.
When should you use long instead of int? Four common scenarios:
n * (n - 1) where n is near 10^5)Notice the placement of the cast. (long) n * (n - 1) first promotes n to long, then performs long multiplication. But (long)(n * (n - 1)) performs int multiplication first (which overflows), then casts the already-corrupted result to long.
The ternary operator is a compact alternative to if-else that you will use often for inline decisions:
Most operators in Java work as you would expect, but a few deserve special attention for DSA.
Modular arithmetic with % appears in hashing, cyclic array problems, and math-heavy questions. Remember that Java's % can return negative values for negative operands: -7 % 3 gives -1, not 2. To get a positive result, use ((n % m) + m) % m.
Many problems ask you to return the result "modulo 10^9 + 7" to prevent overflow in the output. This is a standard pattern:
Note the underscore in 1_000_000_007. Java allows underscores in numeric literals for readability. This is the same as writing 1000000007, just easier to read and verify.
Bitwise operators unlock an entire category of problems:
The key bitwise operators you will encounter:
| Operator | Symbol | DSA Use |
|---|---|---|
| AND | & | Masking, checking if bit is set: (n & (1 << i)) != 0 |
| OR | | | Setting bits: n | (1 << i) |
| XOR | ^ | Finding unique elements, toggling bits |
| NOT | ~ | Bit inversion |
| Left shift | << | Multiply by powers of 2: 1 << n equals 2^n |
| Right shift | >> | Divide by powers of 2 (preserves sign) |
| Unsigned right shift | >>> | Divide by powers of 2 (fills with 0) |
A common bit manipulation pattern is checking and setting individual bits, which shows up in problems using bitmasks to represent subsets:
Control flow in Java offers three loop forms. Use each where it fits:
One limitation to remember: the enhanced for-each loop does not give you the index, and you cannot modify the underlying collection while iterating. If you need the index or need to modify elements, use the standard for loop.
Short-circuit evaluation with && and || is important for safety. Java evaluates left to right and stops as soon as the result is determined:
In DSA problems, you will frequently extract logic into helper methods to keep your code clean. On LeetCode, your solution lives inside a class with static or instance methods.
A critical concept to understand: Java is always pass-by-value. But for objects (including arrays), the "value" being passed is the reference. This means:
arr[i] = 5), but reassigning the reference itself (arr = new int[10]) does not affect the caller.This is why the swap method above works: it modifies the array contents through the reference. But you cannot write a method that swaps two int variables.
This distinction matters enormously in recursive and backtracking problems. When you pass a List to a recursive call and add elements to it, those additions are visible to the caller. That is why the backtracking pattern works, adding and removing from a shared list as you explore different branches:
Notice the new ArrayList<>(path) when saving results. If you wrote results.add(path) instead, every entry in results would be a reference to the same list, and they would all end up empty after backtracking unwinds. This is one of the most common bugs in backtracking solutions.
Variable scope is worth mentioning briefly. Variables declared inside a loop body exist only within that iteration:
If you need a value to persist across iterations (like a running sum or a previous element), declare it before the loop.
Java collections like ArrayList, HashMap, and HashSet cannot store primitive types directly. They require wrapper objects: Integer instead of int, Long instead of long, Character instead of char, and so on.
Java automatically converts between primitives and their wrappers. This is called autoboxing (primitive to wrapper) and unboxing (wrapper to primitive):
This conversion is convenient, but it comes with pitfalls you need to know:
Pitfall 1: `==` compares references for wrapper objects, not values.
Java caches Integer objects for values -128 to 127, so == happens to work for small numbers. For larger values, it fails. This is a nasty bug because it works in your tests with small inputs and breaks with larger ones. Always use .equals() or unbox to int before comparing.
Pitfall 2: Unboxing null throws NullPointerException.
Pitfall 3: Performance in tight loops.
Each autoboxing operation creates an object on the heap. In a tight inner loop processing millions of elements, this can cause noticeable slowdowns. When performance matters, use primitive arrays (int[]) instead of ArrayList<Integer>.
Arrays are the most fundamental data structure in Java and the starting point for almost every DSA problem.
Default values matter and they differ by type:
| Array Type | Default Value | DSA Significance |
|---|---|---|
int[] | 0 | Distance arrays, DP tables start at 0 |
long[] | 0L | Same, for large value computations |
boolean[] | false | Visited arrays start as unvisited |
char[] | '\0' | Null character |
Object[] (including String[], Integer[]) | null | Must initialize before use |
The `.length` property (no parentheses) gives the array size. This is different from String.length() (with parentheses) and List.size().
2D arrays appear in every matrix problem (BFS on grid, dynamic programming tables, etc.):
For DP problems, you often need a table with one extra row and column for the base case:
The 4-directional neighbor pattern is one of the most common patterns you will write in matrix problems:
For 8-directional movement (including diagonals), add the four diagonal pairs: {-1,-1}, {-1,1}, {1,-1}, {1,1}.
The Arrays utility class provides essential operations:
`Arrays.asList()` converts elements to a List, but has an important gotcha:
`Arrays.binarySearch()` performs binary search on a sorted array:
The return value for a missing element is -(insertion point) - 1. This is useful but confusing, so most candidates write their own binary search in interviews.
Strings in Java are immutable. Every time you modify a string, Java creates a new object. This has a critical performance implication for DSA:
Here are the string methods you will use most often:
| Method | Returns | Example | DSA Use |
|---|---|---|---|
charAt(i) | char | s.charAt(0) | Access individual characters |
length() | int | s.length() | Loop bounds (note: parentheses, unlike arrays) |
substring(start, end) | String | s.substring(0, 3) | Extract portions (start inclusive, end exclusive) |
toCharArray() | char[] | s.toCharArray() | When you need in-place modification |
split(regex) | String[] | s.split(" ") | Tokenize strings |
indexOf(str) | int | s.indexOf("ab") | Find substrings (-1 if not found) |
equals(other) | boolean | s.equals(t) | Content comparison |
compareTo(other) | int | s.compareTo(t) | Lexicographic comparison |
trim() | String | s.trim() | Remove leading/trailing whitespace |
isEmpty() | boolean | s.isEmpty() | Check if length is 0 |
startsWith(prefix) | boolean | s.startsWith("ab") | Prefix check |
contains(seq) | boolean | s.contains("ab") | Substring check |
The equals() vs == distinction is one of the most common Java bugs:
Always use `.equals()` for string comparison. The == operator compares memory addresses, not content. It sometimes works with string literals due to Java's string pool, but this behavior is unreliable and will create subtle bugs.
StringBuilder is your tool for building strings efficiently:
You can also initialize StringBuilder with a capacity hint when you know the approximate size. This avoids internal buffer resizing:
Character utilities come up in problems involving letter manipulation:
The c - 'a' pattern deserves special attention. Since characters are internally represented as numbers, subtracting 'a' from a lowercase letter gives its zero-based position. This is how you build frequency arrays without a HashMap:
This is faster and more memory-efficient than HashMap<Character, Integer> when you know the character set is limited to lowercase (or uppercase, or digits).
This is the most important section of this chapter. The Java Collections Framework provides the data structures you will reach for in virtually every DSA problem. Choosing the right collection is often the difference between an O(n) and an O(n^2) solution.
When you do not know the size upfront, or need to build a result list, ArrayList is your go-to:
Pay attention to the two remove overloads. remove(0) removes the element at index 0. remove(Integer.valueOf(42)) removes the first occurrence of the value 42. If you write remove(42) on a List<Integer>, Java treats 42 as an index, not a value. This is a common source of bugs.
A common pattern is building a list of lists for results like permutations or combinations:
Collections utility methods that work on lists:
HashMap is arguably the single most used collection in DSA. It provides O(1) average-case lookups, inserts, and deletes.
A critical null safety point: map.get(key) returns null if the key does not exist. If you auto-unbox this to int, you get a NullPointerException:
HashSet provides O(1) membership testing:
The fact that add() returns a boolean is useful. It tells you whether the element was actually added (i.e., was not already present). This lets you detect duplicates in a single operation:
LinkedHashMap maintains insertion order, which is important for specific problems like LRU Cache:
LinkedHashSet similarly maintains insertion order for sets. Use these when the order in which elements were inserted matters.
When you need keys in sorted order, TreeMap and TreeSet provide O(log n) operations backed by a red-black tree:
TreeSet offers the same navigational methods (floor, ceiling, lower, higher, first, last) for elements instead of keys.
These are invaluable for sliding window problems where you need sorted access to window elements, or for interval-based problems where you need to find the closest interval.
Every BFS implementation starts with a queue:
Use offer() and poll() instead of add() and remove(). The former return special values on failure (false / null), while the latter throw exceptions. In DSA, you almost always want the non-throwing versions.
The queue.size() capture before the inner loop is a common pattern for level-order BFS, where you need to process all nodes at the current level before moving to the next.
Never use Stack in Java. It extends Vector, which is synchronized (slow) and a legacy class. Use ArrayDeque instead:
PriorityQueue gives you a min-heap by default. It is essential for problems involving top-K elements, merge-K-sorted, and Dijkstra's algorithm:
One important limitation: PriorityQueue does not support efficient removal of arbitrary elements. pq.remove(element) is O(n) because it has to search the heap linearly. If you need to remove arbitrary elements from a heap, consider using a TreeMap instead, or use lazy deletion (mark elements as deleted and skip them when they surface via poll()).
| Collection | Interface | Key Methods | Time Complexity | DSA Use Case |
|---|---|---|---|---|
ArrayList | List | add, get, set, size | O(1) get, O(1) add | Result lists, dynamic arrays |
HashMap | Map | put, get, containsKey, getOrDefault | O(1) average | Frequency counting, lookups |
HashSet | Set | add, contains, remove | O(1) average | Visited tracking, duplicates |
LinkedHashMap | Map | Same as HashMap + insertion order | O(1) average | LRU cache, ordered maps |
LinkedHashSet | Set | Same as HashSet + insertion order | O(1) average | Ordered unique elements |
TreeMap | Map | put, floorKey, ceilingKey, firstKey | O(log n) | Sorted access, range queries |
TreeSet | Set | add, floor, ceiling, first | O(log n) | Sorted unique elements |
LinkedList | Queue | offer, poll, peek | O(1) | BFS queues |
ArrayDeque | Deque | push, pop, offerLast, pollFirst | O(1) | Stacks, double-ended queues |
PriorityQueue | Queue | offer, poll, peek | O(log n) insert/remove | Heaps, top-K, Dijkstra |
Sorting is a prerequisite for many algorithms: binary search, two pointers on sorted arrays, merge intervals, and greedy approaches.
For custom sorting, Java uses Comparator with lambda expressions. The comparator receives two elements and returns a negative number if the first should come before the second, zero if they are equal, and a positive number if the first should come after.
You might see (a, b) -> a[0] - b[0] in many tutorials, but this can overflow when values are near Integer.MAX_VALUE or Integer.MIN_VALUE. For example, Integer.MIN_VALUE - 1 wraps to Integer.MAX_VALUE, giving the wrong comparison result. Always use Integer.compare(a, b) for safe comparisons. Experienced interviewers look for this subtle bug.
For descending order:
Java 8 introduced several methods on collections that make DSA code more concise. You do not need to master functional programming, but these patterns appear constantly:
The computeIfAbsent pattern is particularly elegant for building graph adjacency lists. Without it, you would need to check if the key exists, create the list if it does not, and then add the neighbor. With it, all of that happens in one line:
null is a source of many runtime errors in Java. In DSA, you encounter it primarily in three situations: tree/linked list problems, map lookups, and uninitialized object arrays.
Rule 1: Always check for null before accessing fields or calling methods on an object.
Rule 2: Use `getOrDefault` instead of `get` for maps.
Rule 3: Combine null and empty checks.
These checks at the top of your solution handle edge cases cleanly. Many interviewers appreciate seeing these guard clauses.
One of Java's most common runtime errors is ConcurrentModificationException, thrown when you modify a collection while iterating over it with a for-each loop:
There are three safe alternatives:
For HashSet and HashMap, the same rules apply. If you need to remove entries while iterating, use the iterator's remove() method. In practice though, most DSA problems do not require removing during iteration. You are far more likely to build a new collection with the desired elements.
Recursion is the backbone of tree traversal, graph DFS, backtracking, and divide-and-conquer. Understanding how Java handles recursion is essential.
Every method call in Java goes on the call stack, which has limited space (typically a few thousand frames, depending on stack size and frame size). If your recursion goes too deep, you get a StackOverflowError:
When to worry about stack depth:
| Scenario | Typical Depth | Risk |
|---|---|---|
| Balanced binary tree (n nodes) | O(log n) | Safe for n up to 10^6 |
| Linked list / skewed tree (n nodes) | O(n) | Dangerous if n > 5,000-10,000 |
| Backtracking (k choices, depth d) | O(d) | Usually safe (d is small) |
| DFS on graph (n nodes) | O(n) | Dangerous for large n |
The fix: Convert deep recursion to an iterative approach using an explicit stack:
Note that Java does not optimize tail recursion. Unlike some functional languages, a tail-recursive method in Java still adds a frame to the call stack on every call. If your recursion depth is proportional to input size and the input can be large, convert to iteration.
Graphs appear in a large portion of DSA problems. Java does not have a built-in graph class, so you need to build representations yourself. There are two common approaches:
Approach 1: Adjacency list with `List<List<Integer>>` -- Use when nodes are numbered 0 to n-1.
Approach 2: Adjacency list with `HashMap<Integer, List<Integer>>` -- Use when node IDs are not contiguous or are very large.
Weighted graphs use int[] pairs or a simple class to store neighbor and weight:
| Approach | Pros | Cons | Use When |
|---|---|---|---|
List<List<Integer>> | Fast index access, no hashing overhead | Wastes space if node IDs are sparse | Nodes are 0 to n-1 |
HashMap<Integer, List<Integer>> | Handles any node IDs, no wasted space | Slightly slower due to hashing | Node IDs are large, sparse, or non-numeric |
Java has no built-in Pair or Tuple class (unlike Python). In DSA problems, you frequently need to store two or three values together. Here are the common alternatives:
Option 1: `int[]` array (most common in DSA)
This is the most common approach because it is fast and concise. The downside is that it is not type-safe and the meaning of each index is implicit.
Option 2: Custom class (when clarity matters)
Option 3: `Map.Entry` (quick and dirty)
In practice, int[] is the standard approach in competitive programming and interviews. Use it unless the code becomes unclear.
Many DSA problems require avoiding duplicate results (e.g., 3Sum, 4Sum, permutations with duplicates). Java offers two approaches:
Approach 1: Sort and skip duplicates (preferred for sorted array problems)
This is the standard approach for problems like 3Sum and 4Sum. It runs in O(1) extra space (beyond the sort) and produces results in sorted order.
Approach 2: Use a Set to collect unique results
This is simpler to implement but uses more memory. Note that for HashSet to detect duplicate lists, the lists must contain the same elements in the same order. So you typically sort each candidate before adding it.
These are the small patterns and utility calls you will reach for repeatedly.
Swapping elements (Java has no built-in swap for arrays):
Sentinel values for tracking min/max:
Be careful with Integer.MIN_VALUE. Negating it (-Integer.MIN_VALUE) overflows because the positive range is one less than the negative range. If you need to negate values that might be Integer.MIN_VALUE, use long.
Math utilities:
Ceiling division without floating point:
Type conversions come up when converting between arrays and collections, or between strings and numbers:
Deep copy vs shallow copy:
When you add a list to another list, you are adding a reference. If the original list changes later, the "copy" changes too:
For arrays, use Arrays.copyOf() or arr.clone():
For 2D arrays, clone() only copies the outer array. The inner arrays are still shared references. You need a manual deep copy:
DSA problems often define custom node classes. You will encounter these definitions throughout the course:
Notice that all fields are package-private (no private keyword, no getters or setters). This is standard practice in DSA because it keeps the code concise and interview-focused. You are not building production software here. You are solving problems under time pressure, and the overhead of encapsulation adds no value.
The this keyword refers to the current instance. You will see it most often in constructors to distinguish the parameter from the field (this.val = val).
Constructor overloading allows multiple constructors with different parameters:
LeetCode often provides these overloaded constructors. The two-argument constructor is handy for building linked lists concisely: