Java basics, part 2: collections, null and errors
Part of the free Low-Level Design course on LogicWiz, in the chapter: Start here: your machine and Java. The class-diagram exercises run in the app.
Java basics, part 2: collections, null and errors
"The cart crashed at checkout, but only for customers with no coupon. Where did the null come from?"
Priya, at the start of a bug review with Arjun
Lists: a cart that grows
An array has a fixed size, but a cart grows and shrinks. Java's answer is the List.
import java.util.ArrayList;
import java.util.List;
public class CartDemo {
public static void main(String[] args) {
List<String> cart = new ArrayList<>();
cart.add("Milk 1L");
cart.add("Bread");
cart.remove("Bread");
System.out.println(cart.size() + " " + cart.get(0)); // 1 Milk 1L
for (String item : cart) {
System.out.println(item); // Milk 1L
}
}
}
An import line brings a class from Java's standard library into your file. Later snippets leave imports out. Put import java.util.*; at the top of your file, and the star brings in every class of that library package.
The <String> in List<String> says what the list holds. A type with such a slot is generic: one List class serves every element type, and you fill the slot. The compiler then rejects cart.add(42). A Python list accepts anything, and a wrong value is found later, at runtime. Generics hold objects, so you write List<Integer>, never List<int>. Java converts between int and Integer for you.
List is the promise and ArrayList is one way to keep it. Declare the variable as List, and create it with new ArrayList<>(). The empty <> lets the compiler fill in the element type.
| Task | Java | Python | JavaScript | C++ |
|---|---|---|---|---|
| Make | new ArrayList<>() |
[] |
[] |
std::vector<int> v |
| Add | list.add(x) |
list.append(x) |
list.push(x) |
v.push_back(x) |
| Read at i | list.get(i) |
list[i] |
list[i] |
v[i] |
| Size | list.size() |
len(list) |
list.length |
v.size() |
| Remove at i | list.remove(i) |
list.pop(i) |
list.splice(i, 1) |
v.erase(v.begin() + i) |
Maps and sets
A Map stores values by key, like a Python dict. A Set stores each value once.
Map<String, Integer> stock = new LinkedHashMap<>();
stock.put("MILK", 40);
stock.put("BREAD", 0);
stock.put("MILK", stock.get("MILK") - 2);
System.out.println(stock.getOrDefault("EGGS", 0)); // 0
for (Map.Entry<String, Integer> e : stock.entrySet()) {
System.out.println(e.getKey() + " -> " + e.getValue());
}
// MILK -> 38
// BREAD -> 0
Set<String> usedCoupons = new HashSet<>();
System.out.println(usedCoupons.add("SAVE10")); // true
System.out.println(usedCoupons.add("SAVE10")); // false
HashMap is the usual map, and it makes no promise about the order of its entries. The code above uses LinkedHashMap, which returns entries in the order you added them. TreeMap returns them sorted by key. When a problem's expected output lists entries, pick one of these two so the order is fixed. HashSet has the same three variants.
| Task | Java | Python | JavaScript | C++ |
|---|---|---|---|---|
| Put | m.put(k, v) |
m[k] = v |
m.set(k, v) |
m[k] = v |
| Missing key | m.get(k) gives null |
m[k] raises KeyError |
m.get(k) gives undefined |
m[k] adds the key with 0 |
| Is it there | m.containsKey(k) |
k in m |
m.has(k) |
m.count(k) |
| A set | new HashSet<>() |
set() |
new Set() |
std::unordered_set<int> |
Queues, deques and stacks
A queue hands things back first in, first out, like orders waiting for a rider. A deque (double-ended queue) works at both ends. ArrayDeque is the usual choice for both.
Deque<String> orders = new ArrayDeque<>();
orders.offer("order-1");
orders.offer("order-2");
System.out.println(orders.poll()); // order-1
System.out.println(orders.peek()); // order-2
Deque<Integer> stack = new ArrayDeque<>();
stack.push(1);
stack.push(2);
System.out.println(stack.pop()); // 2
offer adds at the back, poll removes from the front, and peek looks without removing. push and pop work at the front, so the same class acts as a stack. Use ArrayDeque for stacks. Avoid the old Stack class: its own documentation points to Deque instead. PriorityQueue hands back the smallest item first, as Python's heapq does. In Python you would use collections.deque. In C++ you would use std::queue and std::stack.
null and Optional
Here is the bug Priya was chasing.
Map<String, Integer> percentOff = new HashMap<>();
percentOff.put("SAVE10", 10);
Integer off = percentOff.get("NEW50");
int discount = off + 5;
Exception in thread "main" java.lang.NullPointerException:
Cannot invoke "java.lang.Integer.intValue()" because "off" is null
The code NEW50 is not in the map, so get returned null. Null means "no object here". Using a null as if it were an object throws a NullPointerException. Python has None, JavaScript has null and undefined, and C++ has nullptr for pointers.
Why is this a bug factory? Every object variable in Java may hold null, and the compiler does not warn. The null is made in one place and crashes in another. The line that crashes is rarely the line that is wrong.
Arjun asked, "So I check if (x != null) everywhere?" That works but is easy to forget. A better habit is to make absence visible in the type. A method returns Optional<Integer>, a box that is either empty or holds one value.
Optional<Integer> found = Optional.ofNullable(percentOff.get("NEW50"));
System.out.println(found.isPresent()); // false
System.out.println(found.orElse(0)); // 0
System.out.println(Optional.of(10).orElse(0)); // 10
The caller sees Optional in the signature, and must decide what to do about an empty one. orElse(0) supplies a default. orElseThrow() throws an exception if the box is empty. Use Optional for return values. Do not use it for fields or parameters, and do not return null where an Optional or an empty list would do. It does not remove null from Java. It makes absence impossible to miss.
Exceptions: when something goes wrong
An exception is an object that Java throws when code cannot carry on. It jumps out of the method, and out of each caller, until a try and catch block handles it.
class OutOfStockException extends RuntimeException {
OutOfStockException(String sku) {
super("Out of stock: " + sku);
}
}
The word extends says the new class is a special kind of an existing one and gets all its behaviour. The method that throws it checks the stock first.
static void reserve(Map<String, Integer> stock, String sku, int qty) {
if (qty <= 0) {
throw new IllegalArgumentException("qty must be positive: " + qty);
}
int have = stock.getOrDefault(sku, 0);
if (have < qty) {
throw new OutOfStockException(sku);
}
stock.put(sku, have - qty);
}
The caller handles the problem like this.
try {
reserve(stock, "BREAD", 1);
} catch (OutOfStockException e) {
System.out.println("Sorry: " + e.getMessage()); // Sorry: Out of stock: BREAD
} finally {
System.out.println("done"); // done
}
The finally block runs whether or not an exception happened. Python uses raise, except and finally. JavaScript uses throw, catch and finally. C++ has throw and catch, and no finally.
Java has two kinds. A checked exception extends Exception but not RuntimeException, for example IOException. The compiler forces you to catch it or to list it in the method header after the word throws. An unchecked exception extends RuntimeException, for example IllegalArgumentException, IllegalStateException and NullPointerException. The compiler asks nothing. Checked exceptions were meant to force callers to handle failures they can repair, such as a missing file. Many Java teams now find them noisy, and Python, Kotlin and C# have none. In this course, use unchecked exceptions for broken rules and misuse, such as a bad argument or an impossible state. You will rarely meet a checked one, because machine-coding problems keep everything in memory.
Also remember that a refused request is not always an error. A coupon that does not apply is an ordinary result. Return an Optional, a boolean or a result value, and keep exceptions for broken rules.
Things that cannot change: final, List.of and records
The word final on a variable means it cannot be given a new value. It does not freeze what the variable points at. A final List can still have items added.
List.of("MILK", "BREAD") makes a list that cannot change at all. Calling add on it throws UnsupportedOperationException. Set.of and Map.of work the same way.
A record is a short way to write a class that only carries data.
record CartLine(String sku, int quantity) { }
CartLine line = new CartLine("MILK", 2);
System.out.println(line.sku()); // MILK
System.out.println(line); // CartLine[sku=MILK, quantity=2]
System.out.println(line.equals(new CartLine("MILK", 2))); // true
Java writes the constructor, the getters, equals, hashCode and toString for you. The getter is sku(), not getSku(). Every field is final. Python has @dataclass(frozen=True) for this, JavaScript has Object.freeze, and C++ has const.
Why bother? When many parts of a program share one object, any of them can change it, and the bug shows up far from the cause. An object that cannot change cannot be broken that way. Prefer records and final for values such as money and cart lines.
Now draw the design for stock errors. The cart holds lines. An inventory refuses a request when stock runs out, and it does so by throwing a special exception that you define.
📌 Summary: Use List, Map, Set and ArrayDeque for collections, and pick an ordered map when output order matters. Make absence visible with Optional, use unchecked exceptions for broken rules, and prefer final and records.
What Arjun learns next
Arjun now knows enough Java to write a whole problem. Next, Priya shows him how every problem on this site becomes a small project on his laptop, and how to run it, debug it and finish it on time.
Check your understanding
- Map<String, Integer> m holds only SAVE10. The code int x = m.get("NEW50") + 5; crashes. What happened?
- A problem's expected output lists the stock entries in the order they were added. Which map keeps your output stable?
- A customer types a coupon code that does not exist. How should Arjun's method report it?
- Arjun declares final List<String> items = new ArrayList<>(); and later calls items.add("Milk"). What happens?