Java Collections
Cheat sheet for the Java collections framework plus strings and arrays, with the Big-O of every structure and a table for picking the right one.
Fast lookup for the Java APIs that show up in coding rounds. The code carries most of it, and there is a line of explanation only where an API is easy to get wrong.
Pick the right one
| Need | Use |
|---|---|
| Index lookup, mostly reads | ArrayList |
| FIFO queue | ArrayDeque |
| LIFO stack | ArrayDeque, not Stack |
| Add and remove at both ends | ArrayDeque |
| Key to value, order does not matter | HashMap |
| Key to value, sorted keys | TreeMap |
| Key to value, insertion order kept | LinkedHashMap |
| Unique items, order does not matter | HashSet |
| Unique items, sorted | TreeSet |
| Always pull the smallest or largest | PriorityQueue |
| Count frequency of lowercase letters | int[26] |
| Count frequency of anything else | HashMap<K, Integer> with merge |
| A fixed set of known values, never modified | List.of, Set.of, Map.of |
Complexity at a glance
| Structure | Access | Contains | Insert | Remove | Notes |
|---|---|---|---|---|---|
ArrayList | O(1) | O(n) | O(1) amortized at end, O(n) in middle | O(n) | Array backed, capacity doubles on growth |
LinkedList | O(n) | O(n) | O(1) at either end | O(1) at either end, O(n) by index | Doubly linked |
ArrayDeque | Not indexable | O(n) | O(1) both ends | O(1) both ends | Fastest stack and queue |
HashMap | O(1) avg | O(1) avg | O(1) avg | O(1) avg | O(log n) worst case per bucket since Java 8 |
LinkedHashMap | O(1) avg | O(1) avg | O(1) avg | O(1) avg | Keeps insertion order |
TreeMap | O(log n) | O(log n) | O(log n) | O(log n) | Red black tree, keys sorted |
HashSet | n/a | O(1) avg | O(1) avg | O(1) avg | HashMap underneath |
TreeSet | n/a | O(log n) | O(log n) | O(log n) | Sorted |
PriorityQueue | peek O(1) | O(n) | O(log n) | poll O(log n), remove(Object) O(n) | Binary heap, iteration order is not sorted |
Strings
String is immutable. Every concat builds a new object, so loop with StringBuilder.
String s = "hello";
s.length();
s.charAt(i);
s.isEmpty();
s.trim();
s.substring(start, end); // end is exclusive
s.indexOf("lo"); // index or -1
s.contains("ell");
s.equals(other); // never use == on strings
s.equalsIgnoreCase(other);
s.compareTo(other); // <0, 0, >0
s.toLowerCase();
s.split(","); // takes a regex, not a plain string
s.repeat(3);
String.join(",", list);StringBuilder
StringBuilder sb = new StringBuilder();
StringBuilder sb = new StringBuilder("Hello world");
sb.append('c'); // char, String, int, double all work
sb.insert(0, "x");
sb.deleteCharAt(i);
sb.setCharAt(i, 'a');
sb.charAt(i);
sb.indexOf("lo");
sb.length();
sb.reverse();
sb.toString();
// Two builders are only equal as strings
sb.toString().equals(other.toString());String vs StringBuffer vs StringBuilder
| Feature | String | StringBuffer | StringBuilder |
|---|---|---|---|
| Storage | String pool if a literal, else heap | Heap | Heap |
| Object creation | New object on every change (immutable) | Changes the same object (mutable) | Changes the same object (mutable) |
| Memory usage | High, one object per change | Moderate, no new objects but synchronization overhead | Low, no new objects and no synchronization |
| Thread safety | Thread safe because immutable | Thread safe, methods are synchronized | Not thread safe |
| Performance | Slow for heavy modification | Slower than StringBuilder because of synchronization | Fastest for modification on one thread |
| Use case | Constant strings and light work | String edits shared across threads | Hot string edits on one thread |
Default to StringBuilder. Reach for StringBuffer only when several threads
edit the same buffer.
Conversions
Everything in one place, since these are the lines you forget under pressure.
// String and char[]
char[] chars = s.toCharArray();
String back = String.valueOf(chars);
String back = new String(chars);
// String and int
int n = Integer.parseInt("200");
String s = String.valueOf(200);
String s = Integer.toString(200);
// char and String
String s = Character.toString('a');
String s = String.valueOf('a');
// char and int digit
int d = c - '0'; // '7' becomes 7
int idx = c - 'a'; // 'c' becomes 2, for int[26] buckets
// List and array
List<Integer> list = new ArrayList<>(Arrays.asList(arr)); // arr is Integer[]
Integer[] arr = list.toArray(new Integer[0]);
int[][] grid = list.toArray(new int[0][]); // List<int[]>char
Character.isLetter(c);
Character.isDigit(c);
Character.isLetterOrDigit(c);
Character.isUpperCase(c);
Character.isWhitespace(c);
Character.toUpperCase(c);
Character.toLowerCase(c);Integer
Integer.MIN_VALUE;
Integer.MAX_VALUE;
Integer.parseInt(s);
Integer.parseInt(s, 2); // binary string to int
Integer.toBinaryString(n);
Integer.bitCount(n); // number of set bits
Integer.compare(a, b); // overflow safe, unlike a - b
Integer.valueOf(s); // boxed Integer, parseInt gives a primitive intInteger caches boxed values from -128 to 127, so == on Integer gives the right
answer for small numbers and the wrong one for large ones. Use .equals() or unbox to
int.
Number limits
Every primitive has a fixed range, and the constants are the readable way to say it. Reach for them when you need a sentinel for a min or max search, or when you are checking whether a value fits.
| Type | Bits | Min | Max |
|---|---|---|---|
byte | 8 | -128 | 127 |
short | 16 | -32,768 | 32,767 |
char | 16 | 0 | 65,535 |
int | 32 | -2,147,483,648 | 2,147,483,647, about 2.1 billion |
long | 64 | -9,223,372,036,854,775,808 | 9,223,372,036,854,775,807, about 9.2 quintillion |
float | 32 | about -3.4e38 | about 3.4e38, 7 digits of precision |
double | 64 | about -1.8e308 | about 1.8e308, 15 digits of precision |
Integer.MIN_VALUE; // -2147483648
Integer.MAX_VALUE; // 2147483647
Long.MIN_VALUE; // -9223372036854775808
Long.MAX_VALUE; // 9223372036854775807
Byte.MIN_VALUE; Byte.MAX_VALUE;
Short.MIN_VALUE; Short.MAX_VALUE;
Character.MIN_VALUE; Character.MAX_VALUE;
Integer.SIZE; // 32 bits
Integer.BYTES; // 4 bytes
Double.MAX_VALUE; // largest positive double
Double.MIN_VALUE; // smallest positive double, 4.9e-324, not the most negative
Double.POSITIVE_INFINITY;
Double.NEGATIVE_INFINITY;
Double.NaN;Double.MIN_VALUE is the trap. It is the smallest positive value, not the lowest
one. For the lowest possible double write -Double.MAX_VALUE.
NaN is not equal to itself, so d == Double.NaN is always false. Use
Double.isNaN(d), and Double.isInfinite(d) for the other two.
Long
long is the escape hatch when int is too small: sums of a large array,
products of two ints, timestamps in milliseconds, anything counting past 2.1
billion.
long x = 10; // widening from int is automatic
long big = 10000000000L; // literal past int range needs the L
int back = (int) big; // narrowing needs a cast and can silently truncate
int safe = Math.toIntExact(big); // throws ArithmeticException instead
Long.MIN_VALUE; Long.MAX_VALUE;
Long.SIZE; // 64 bits
Long.BYTES; // 8 bytes
Long.parseLong(s);
Long.compare(a, b);
Long.toBinaryString(n);
Long.bitCount(n);The suffix is the part that bites. A numeric literal is an int until you write
L, so the arithmetic happens in int and overflows before the value is ever
widened to long.
long ms = 24 * 60 * 60 * 1000; // fine, fits in int
long us = 24 * 60 * 60 * 1000 * 1000; // overflows, all int math
long us = 24L * 60 * 60 * 1000 * 1000; // one L on the left fixes the whole expression
long sum = 0;
for (int n : nums) sum += n; // sum is long, each n widens, safeLong.MIN_VALUE is one further from zero than Long.MAX_VALUE, so it has no
positive twin. Negating or taking the absolute value of it gives you the same
number back.
Math.abs(Long.MIN_VALUE); // still Long.MIN_VALUE, negative
-Long.MIN_VALUE; // same, wraps to itself
Long.MIN_VALUE / -1; // Long.MIN_VALUE, no exception thrown
Math.negateExact(Long.MIN_VALUE); // throws ArithmeticExceptionJava will not let you write the constant as a plain literal either.
-9223372036854775808L compiles only because the minus is part of the
expression; 9223372036854775808L on its own is a compile error. Use
Long.MIN_VALUE.
Boxing rules match Integer. Long caches -128 to 127, so == on two Long
objects is right for small values and wrong for big ones. Compare with
.equals() or unbox.
Overflow
int arithmetic wraps around in silence. Nothing throws, you get a negative
number and a wrong answer far from the line that caused it.
Integer.MAX_VALUE + 1; // -2147483648
Math.abs(Integer.MIN_VALUE); // still -2147483648, abs has no positive answer here
int big = 100000 * 100000; // overflows before the assignment
long ok = 100000L * 100000; // one L makes the whole product long
int mid = (low + high) / 2; // overflows on large indexes
int mid = low + (high - low) / 2; // safe binary search midpointThree ways out. Compute in long and cast back once you know it fits. Use
Math.addExact, Math.subtractExact or Math.multiplyExact, which throw
ArithmeticException instead of wrapping. Or use Math.toIntExact(longValue)
when narrowing.
Sentinels overflow too. Integer.MAX_VALUE is the usual "infinity" for a
shortest path or a min search, but the moment you add a weight to it the value
wraps negative and the algorithm picks the broken path.
int[] dist = new int[n];
Arrays.fill(dist, Integer.MAX_VALUE);
if (dist[u] != Integer.MAX_VALUE && dist[u] + w < dist[v]) { } // guard first
// or give yourself headroom
final int INF = Integer.MAX_VALUE / 2;
// or hold distances in longMath
Math.abs(n);
Math.max(a, b); Math.min(a, b);
Math.pow(2, 10); // returns double, cast for int
Math.sqrt(n); // double
Math.cbrt(n);
Math.floor(x); Math.ceil(x); // double in, double out
Math.round(x); // long for double, int for float
Math.hypot(a, b);
Math.floorDiv(-7, 2); // -4, rounds down
Math.floorMod(-7, 3); // 2, never negative
-7 % 3; // -1, the sign follows the left operand
Math.addExact(a, b); // throws on overflow
Math.multiplyExact(a, b);
Math.toIntExact(longValue);Integer division truncates toward zero, so 7 / 2 is 3 and -7 / 2 is -3. For
a ceiling divide of positive numbers use (a + b - 1) / b.
% keeps the sign of the left side, which breaks index wrapping on a negative
step. Math.floorMod(i, n) gives the answer you wanted.
Frequency count
int[] freq = new int[26];
for (char c : s.toCharArray()) {
freq[c - 'a']++;
}
Map<Character, Integer> freq = new HashMap<>();
for (char c : s.toCharArray()) {
freq.merge(c, 1, Integer::sum);
}Arrays
int[] a = new int[20]; // filled with 0
int[] a = {1, 2, 3, 4, 5};
int[][] grid = new int[10][10];
a.length; // field, no parentheses
Arrays.fill(a, 10);
Arrays.fill(grid[0], -1);
Arrays.sort(a); // dual pivot quicksort, O(n log n)
Arrays.sort(a, from, to);
Arrays.binarySearch(a, key); // array must be sorted first
Arrays.copyOf(a, newLength);
Arrays.copyOfRange(a, from, to);
Arrays.equals(a, b);
Arrays.toString(a);
Arrays.deepToString(grid); // for 2D
Arrays.stream(a).sum();
Arrays.stream(a).max().getAsInt();Descending sort needs boxed types. Arrays.sort(int[], Comparator) does not compile.
Integer[] a = {3, 1, 2};
Arrays.sort(a, Collections.reverseOrder());
// For int[], sort ascending then reverse by hand
int[] b = {3, 1, 2};
Arrays.sort(b);
for (int i = 0, j = b.length - 1; i < j; i++, j--) {
int tmp = b[i]; b[i] = b[j]; b[j] = tmp;
}
// Sort 2D by a column
int[][] intervals = {{1, 3}, {0, 2}};
Arrays.sort(intervals, (x, y) -> x[0] - y[0]);
Arrays.sort(intervals, Comparator.comparingInt(x -> x[0]));List
ArrayList
List<Integer> list = new ArrayList<>();
List<Integer> list = new ArrayList<>(Arrays.asList(60, 25, 12));
List<Integer> list = new ArrayList<>(other); // copy
list.size();
list.isEmpty();
list.get(i);
list.set(i, value);
list.add(10); // append
list.add(i, 10); // insert at i, shifts right
list.addAll(other);
list.remove(2); // by index for List<Integer>
list.remove(Integer.valueOf(2)); // by value
list.contains(120);
list.indexOf(o); // first match or -1
list.clear();
list.subList(from, to); // view, not a copy
list.toArray(new Integer[0]);Arrays.asList(...) returns a fixed size view of the array. add and remove on it
throw UnsupportedOperationException, though set works and writes straight through to
the backing array. Wrap it in new ArrayList<>(...) when you need to mutate. For a list
that cannot be changed at all, see the immutable factories below.
Nested lists:
List<List<String>> grid = new ArrayList<>();
for (int i = 0; i < 100; i++) {
grid.add(new ArrayList<>());
}Filtering and transforming
removeIf edits in place. Everything else here reads the list and builds a new
one, leaving the original alone.
list.removeIf(n -> n % 2 == 0); // in place, no iterator needed
// Filter and map
List<Integer> evens = list.stream().filter(n -> n % 2 == 0).toList();
List<String> names = people.stream().map(Person::getName).toList();
List<Integer> flat = nested.stream().flatMap(List::stream).toList();
// Group, partition, join
Map<String, List<Person>> byCity =
people.stream().collect(Collectors.groupingBy(Person::getCity));
Map<Boolean, List<Integer>> split =
list.stream().collect(Collectors.partitioningBy(n -> n > 10));
String csv = String.join(", ", names);
// Reduce to one value
int sum = list.stream().mapToInt(Integer::intValue).sum();
double avg = list.stream().mapToInt(Integer::intValue).average().orElse(0);
int max = list.stream().mapToInt(Integer::intValue).max().orElseThrow();
// Ask a question
boolean any = list.stream().anyMatch(n -> n > 100);
boolean all = list.stream().allMatch(n -> n > 0);
long count = list.stream().filter(n -> n > 10).count();
// Dedupe and slice
List<Integer> unique = list.stream().distinct().toList();
List<Integer> firstFive = list.stream().limit(5).toList();
// Back to an array
Integer[] arr = list.stream().toArray(Integer[]::new);
int[] prim = list.stream().mapToInt(Integer::intValue).toArray();Two toList calls exist and they are not the same. stream().toList() (Java 16
and up) returns an unmodifiable list, while collect(Collectors.toList())
returns a mutable one. Choosing the first and then calling add on it throws
UnsupportedOperationException somewhere far from the cause.
subList returns a view, not a copy. Writes go through to the backing list, and
structurally changing the backing list invalidates the view.
List<Integer> slice = list.subList(2, 5); // indexes 2, 3, 4
slice.clear(); // deletes them from list
List<Integer> copy = new ArrayList<>(list.subList(2, 5)); // real copylist.sort(...) reorders in place. list.stream().sorted() leaves the original
alone and gives you a new sequence, which is the one to use when the caller owns
the list.
new ArrayList<>(100) sets capacity, not size. The list is still empty, and the
argument only saves the array copies that growth would otherwise cost.
LinkedList
Use it when you need Deque behaviour on a List. For a plain stack or queue,
ArrayDeque is faster.
LinkedList<String> ll = new LinkedList<>();
ll.add(e); ll.add(i, e);
ll.addFirst(e); ll.addLast(e);
ll.getFirst(); ll.getLast();
ll.peekFirst(); ll.peekLast(); // null if empty
ll.removeFirst(); ll.removeLast(); // throw if empty
ll.pollFirst(); ll.pollLast(); // null if empty
ll.size();Stack
Stack extends Vector and every method is synchronized, so it is slow. Interviewers
expect ArrayDeque.
Deque<Integer> stack = new ArrayDeque<>();
stack.push(10); // addFirst
stack.pop(); // removeFirst, throws if empty
stack.peek(); // null if empty
stack.isEmpty();
stack.size();Legacy form, still fine to read:
Stack<Integer> stack = new Stack<>();
stack.push(10);
stack.pop();
stack.peek();
stack.isEmpty();Queue
Queue<Integer> queue = new ArrayDeque<>(); // LinkedList also works, slower
queue.offer(10); // add at tail, returns false when full
queue.add(10); // add at tail, throws when full
queue.poll(); // remove head, null if empty
queue.remove(); // remove head, throws if empty
queue.peek(); // head without removing, null if empty
queue.isEmpty();
queue.size();The pattern is the same everywhere: offer / poll / peek return a special value,
add / remove / element throw. Prefer the first set.
ArrayDeque rejects null elements. LinkedList allows them, which is why poll
returning null is ambiguous there.
BFS template
Queue<int[]> q = new ArrayDeque<>();
q.offer(new int[]{0, 0});
boolean[][] seen = new boolean[rows][cols];
seen[0][0] = true;
while (!q.isEmpty()) {
int size = q.size(); // one level at a time
for (int i = 0; i < size; i++) {
int[] cur = q.poll();
// visit neighbours, mark seen, offer
}
}PriorityQueue
A binary heap. peek and poll give the smallest by default. Iterating it does not
give sorted order, only poll does.
PriorityQueue<Integer> minHeap = new PriorityQueue<>();
PriorityQueue<Integer> maxHeap = new PriorityQueue<>(Collections.reverseOrder());
PriorityQueue<Integer> maxHeap = new PriorityQueue<>((a, b) -> b - a);
// By a field
PriorityQueue<int[]> pq = new PriorityQueue<>((a, b) -> a[1] - b[1]);
PriorityQueue<int[]> pq = new PriorityQueue<>(Comparator.comparingInt(a -> a[1]));
pq.offer(10);
pq.poll(); // smallest, null if empty
pq.peek();
pq.size();
pq.isEmpty();Top K pattern: keep a min heap of size k, evict the head once it overflows.
PriorityQueue<Integer> pq = new PriorityQueue<>();
for (int n : nums) {
pq.offer(n);
if (pq.size() > k) pq.poll();
}
// pq.peek() is the kth largestMap
HashMap
Map<String, Integer> map = new HashMap<>();
Map<Integer, List<String>> map = new HashMap<>();
map.size();
map.isEmpty();
map.containsKey(key);
map.containsValue(value); // O(n)
map.get(key); // null if absent
map.getOrDefault(key, 0);
map.put(key, value); // returns the old value or null
map.putIfAbsent(key, value);
map.remove(key);
map.clear();
map.keySet();
map.values();
map.entrySet();
// Counting and grouping without null checks
map.merge(key, 1, Integer::sum);
map.computeIfAbsent(key, k -> new ArrayList<>()).add(item);getOrDefault, merge, and computeIfAbsent replace the if (map.get(k) == null)
dance. Use them.
A key's hashCode and equals must agree. Mutating an object after using it as a key
makes the entry unreachable.
TreeMap
Sorted keys, plus navigation methods that HashMap does not have.
TreeMap<String, Integer> tm = new TreeMap<>(); // ascending
TreeMap<String, Integer> tm = new TreeMap<>(Collections.reverseOrder());
tm.put(key, value);
tm.get(key);
tm.getOrDefault(key, 0);
tm.remove(key);
tm.firstKey(); tm.lastKey();
tm.firstEntry(); tm.lastEntry();
tm.pollFirstEntry(); tm.pollLastEntry();
tm.floorKey(k); // greatest key <= k
tm.ceilingKey(k); // smallest key >= k
tm.lowerKey(k); // greatest key < k
tm.higherKey(k); // smallest key > k
tm.headMap(k); tm.tailMap(k); tm.subMap(from, to);Iterating a map
for (Map.Entry<String, Integer> e : map.entrySet()) {
String key = e.getKey();
int value = e.getValue();
}
for (String key : map.keySet()) { }
for (int value : map.values()) { }
map.forEach((k, v) -> System.out.println(k + " " + v));
List<String> keys = new ArrayList<>(map.keySet());
List<Integer> values = new ArrayList<>(map.values());Removing while looping throws ConcurrentModificationException. Remove through the
iterator instead.
Iterator<Map.Entry<String, Integer>> it = map.entrySet().iterator();
while (it.hasNext()) {
if (it.next().getValue() == 0) it.remove();
}Set
HashSet
Set<Integer> set = new HashSet<>();
Set<Integer> set = new HashSet<>(list); // dedupes the list
set.add(10); // false if already present
set.contains(10);
set.remove(10);
set.size();
set.isEmpty();
set.clear();
set.addAll(other); // union
set.retainAll(other); // intersection
set.removeAll(other); // differenceLinkedHashSet is the same with insertion order preserved.
TreeSet
Sorted, with the same navigation methods as TreeMap.
TreeSet<Integer> ts = new TreeSet<>(); // ascending
TreeSet<Integer> ts = new TreeSet<>((a, b) -> b - a); // descending
ts.add(10);
ts.contains(10);
ts.remove(10);
ts.first(); ts.last();
ts.pollFirst(); ts.pollLast();
ts.floor(k); ts.ceiling(k);
ts.lower(k); ts.higher(k);
ts.headSet(k); ts.tailSet(k); ts.subSet(from, to);Immutable factories
Java 9 added of factories to List, Set and Map. They build a small
collection in one expression, and the result cannot be changed afterwards.
List<String> list = List.of("a", "b", "c");
Set<Integer> set = Set.of(1, 2, 3);
Map<String, Integer> map = Map.of("a", 1, "b", 2);
// Map.of tops out at 10 pairs, so past that use entries
Map<String, Integer> bigger = Map.ofEntries(
Map.entry("a", 1),
Map.entry("b", 2)
);
// unmodifiable snapshot of a collection you were handed
List<String> snapshot = List.copyOf(other);Four things bite here.
Every mutator throws UnsupportedOperationException, not only add and
remove. That includes set, sort, clear and removeIf.
null is rejected everywhere: as an element, as a key, as a value, and even as
the argument to contains. List.of("a", null) throws NullPointerException
at construction rather than later.
Duplicates are an error, not a silent merge. Set.of(1, 1) and a Map.of with
a repeated key both throw IllegalArgumentException.
List.of keeps insertion order, but the iteration order of Set.of and
Map.of is unspecified and deliberately varies between JVM runs. Never write
code, or a test, that depends on it.
When you want a literal to start from and then edit, wrap it:
List<String> editable = new ArrayList<>(List.of("a", "b"));The three one-line ways to build a list differ more than they look:
List.of(...) | Arrays.asList(...) | new ArrayList<>(...) | |
|---|---|---|---|
| Size | Fixed | Fixed | Grows |
set(i, v) | Throws | Allowed, writes through to the array | Allowed |
add, remove | Throws | Throws | Allowed |
null elements | Rejected | Allowed | Allowed |
| Backed by | Its own storage | The array you passed | A copy |
Sorting and comparators
Collections.sort(list); // natural order
Collections.sort(list, Collections.reverseOrder());
list.sort(Comparator.naturalOrder());
list.sort(Comparator.reverseOrder());
// By a field
people.sort(Comparator.comparing(Person::getName));
people.sort(Comparator.comparingInt(Person::getAge));
// Descending, then a tie breaker
people.sort(Comparator.comparingInt(Person::getAge).reversed()
.thenComparing(Person::getName));
Collections.reverse(list);
Collections.shuffle(list);
Collections.max(list);
Collections.min(list);
Collections.frequency(list, item);
Collections.swap(list, i, j);Comparable lives on the class through compareTo, so it defines one natural order.
Comparator is passed in at the call site, so you can have as many orders as you need.
Subtraction comparators like (a, b) -> a - b overflow when the values can be near
Integer.MIN_VALUE or Integer.MAX_VALUE. Integer.compare(a, b) is always safe.