AlgoMaster Logo

Java Crash Course for DSA

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.

Imports You Will Use Everywhere

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.

Variables, Types, and the Overflow Trap

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.

TypeSizeRangeDSA Use Case
int4 bytes-2.1B to 2.1BArray indices, counters, most values
long8 bytes-9.2 x 10^18 to 9.2 x 10^18Prefix sums, large products, overflow-prone math
char2 bytes0 to 65,535String characters, frequency arrays
boolean1 bit*true / falseVisited arrays, flags
double8 bytes±1.7 x 10^308Rarely 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:

  1. Prefix sums of arrays with large values (sum of 10^5 elements each up to 10^9 gives 10^14)
  2. Multiplication of two large ints (e.g., n * (n - 1) where n is near 10^5)
  3. Factorial or combination calculations
  4. Modulo problems where intermediate sums can exceed int range before taking mod

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:

Operators and Control Flow

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:

OperatorSymbolDSA 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:

Methods and Pass-by-Value

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:

  • Primitive parameters are copies. Changing them inside a method does not affect the caller.
  • Object/array parameters pass a copy of the reference. You can modify the contents (like 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.

Autoboxing and Unboxing

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

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 TypeDefault ValueDSA Significance
int[]0Distance arrays, DP tables start at 0
long[]0LSame, for large value computations
boolean[]falseVisited arrays start as unvisited
char[]'\0'Null character
Object[] (including String[], Integer[])nullMust 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

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:

MethodReturnsExampleDSA Use
charAt(i)chars.charAt(0)Access individual characters
length()ints.length()Loop bounds (note: parentheses, unlike arrays)
substring(start, end)Strings.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)ints.indexOf("ab")Find substrings (-1 if not found)
equals(other)booleans.equals(t)Content comparison
compareTo(other)ints.compareTo(t)Lexicographic comparison
trim()Strings.trim()Remove leading/trailing whitespace
isEmpty()booleans.isEmpty()Check if length is 0
startsWith(prefix)booleans.startsWith("ab")Prefix check
contains(seq)booleans.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).

Collections Framework

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.

ArrayList -- Dynamic Arrays

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 and HashSet -- O(1) Lookups

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 and LinkedHashSet -- Insertion Order

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.

TreeMap and TreeSet -- Sorted Collections

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.

Queue with LinkedList -- BFS Foundation

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.

Deque and ArrayDeque -- The Better Stack

Never use Stack in Java. It extends Vector, which is synchronized (slow) and a legacy class. Use ArrayDeque instead:

PriorityQueue -- Heaps

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

Collections Quick Reference

CollectionInterfaceKey MethodsTime ComplexityDSA Use Case
ArrayListListadd, get, set, sizeO(1) get, O(1) addResult lists, dynamic arrays
HashMapMapput, get, containsKey, getOrDefaultO(1) averageFrequency counting, lookups
HashSetSetadd, contains, removeO(1) averageVisited tracking, duplicates
LinkedHashMapMapSame as HashMap + insertion orderO(1) averageLRU cache, ordered maps
LinkedHashSetSetSame as HashSet + insertion orderO(1) averageOrdered unique elements
TreeMapMapput, floorKey, ceilingKey, firstKeyO(log n)Sorted access, range queries
TreeSetSetadd, floor, ceiling, firstO(log n)Sorted unique elements
LinkedListQueueoffer, poll, peekO(1)BFS queues
ArrayDequeDequepush, pop, offerLast, pollFirstO(1)Stacks, double-ended queues
PriorityQueueQueueoffer, poll, peekO(log n) insert/removeHeaps, top-K, Dijkstra

Sorting and Comparators

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:

Functional Patterns for DSA

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 Handling

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.

Iterating and Modifying Collections Safely

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 and the Call Stack

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:

ScenarioTypical DepthRisk
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.

Graph Representation Patterns

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:

ApproachProsConsUse When
List<List<Integer>>Fast index access, no hashing overheadWastes space if node IDs are sparseNodes are 0 to n-1
HashMap<Integer, List<Integer>>Handles any node IDs, no wasted spaceSlightly slower due to hashingNode IDs are large, sparse, or non-numeric

Pair and Tuple Alternatives

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.

Deduplication Patterns

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.

Common DSA Idioms in Java

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:

OOP Basics for Design Problems

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: