Java
Java has been the enterprise backend language for 30 years, and itβs still the language most large financial, healthcare, and government systems are written in. The verbosity people complain about is real, but so is the payoff: a well-written Java codebase is extremely readable, the tooling is unmatched, and the JVMβs performance characteristics are well understood and tunable in ways most runtimes arenβt.
Modern Java (17+) has addressed most of the historical complaints β records eliminate boilerplate data classes, sealed types bring exhaustive pattern matching, text blocks make multiline strings tolerable. Java 21βs virtual threads make blocking I/O viable at scale without the async/callback complexity of Node. Itβs a different language from the Java of 2010.
π’ Junior
Primitive Types & Autoboxing
Java has 8 primitive types. They are NOT objects β they live on the stack, not the heap, making them fast and memory-efficient.
byte b = 127; // 8-bit signed, -128 to 127
short s = 32_767; // 16-bit signed
int i = 2_147_483_647; // 32-bit signed (default integer type)
long l = 9_223_372_036L; // 64-bit signed, suffix L required
float f = 3.14f; // 32-bit float, suffix f required
double d = 3.14159265; // 64-bit float (default decimal type)
boolean flag = true; // true or false only
char c = 'A'; // 16-bit Unicode character
Autoboxing β Java silently converts between primitives and their wrapper objects (Integer, Long, Double, etc.):
Integer boxed = 42; // autoboxing: int β Integer
int unboxed = boxed; // unboxing: Integer β int
List<Integer> list = new ArrayList<>();
list.add(99); // 99 is autoboxed to new Integer(99)
int val = list.get(0); // unboxed back to int automatically
Autoboxing in tight loops creates many short-lived heap objects β use
int[]orIntStreamfor performance-critical number crunching.
Integer cache trap β always use .equals() for object comparison:
Integer a = 127;
Integer b = 127;
System.out.println(a == b); // true β JVM caches Integer -128 to 127
Integer x = 128;
Integer y = 128;
System.out.println(x == y); // false β different objects on heap!
System.out.println(x.equals(y)); // true β always use .equals() on objects
String Handling
String is immutable β every modification creates a new object. Strings are stored in the String Pool (heap area) and can be reused.
String s = "hello";
String upper = s.toUpperCase(); // "HELLO" β s is unchanged
String trimmed = " hi ".strip(); // "hi" (Java 11+ β Unicode-aware, prefer over trim())
String sub = s.substring(1, 3); // "el" (start inclusive, end exclusive)
// Checking content
s.contains("ell"); // true
s.startsWith("hel"); // true
s.endsWith("lo"); // true
s.isEmpty(); // false (length == 0)
s.isBlank(); // false (Java 11+, checks whitespace too)
s.indexOf("ll"); // 2
// Splitting and joining
String[] parts = "a,b,c".split(","); // ["a", "b", "c"]
String joined = String.join("-", "a", "b", "c"); // "a-b-c"
String joined2 = String.join(", ", parts); // "a, b, c"
// Formatting (Java 15+)
String msg = "Hello %s, you are %d years old".formatted("Alice", 30);
// Text blocks β multiline strings without escape characters (Java 15+)
String json = """
{
"name": "Alice",
"age": 30
}
""";
String concatenation in loops β use StringBuilder:
// BAD β creates thousands of intermediate String objects
String result = "";
for (int i = 0; i < 1000; i++) {
result += i; // allocates a new String each time
}
// GOOD
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 1000; i++) {
sb.append(i);
}
String result = sb.toString();
// Or with streams
String joined = IntStream.range(0, 1000)
.mapToObj(Integer::toString)
.collect(Collectors.joining(", "));
Access Modifiers
| Modifier | Same Class | Same Package | Subclass | Everywhere |
|---|---|---|---|---|
public |
β | β | β | β |
protected |
β | β | β | β |
| (package) | β | β | β | β |
private |
β | β | β | β |
public class User {
private String password; // only this class can read/write
protected String email; // subclasses and same-package can access
String username; // package-private (no keyword)
public String displayName; // anyone can access
// Expose internal state safely through methods
public boolean checkPassword(String input) {
return password.equals(input); // logic lives in the class
}
}
Exception Handling
Java exceptions split into two families:
| Type | Examples | Must handle? |
|---|---|---|
| Checked | IOException, SQLException, ParseException |
Yes β declare in throws or catch |
Unchecked (RuntimeException) |
NullPointerException, IllegalArgumentException, IndexOutOfBoundsException |
No |
// Basic try-catch-finally
try {
String text = Files.readString(Path.of("data.txt")); // declares throws IOException
process(text);
} catch (IOException e) {
System.err.println("File error: " + e.getMessage());
} catch (RuntimeException e) {
System.err.println("Logic error: " + e.getMessage());
} finally {
System.out.println("Always runs β use for guaranteed cleanup");
}
// try-with-resources β auto-closes anything implementing AutoCloseable
try (
Connection conn = dataSource.getConnection();
PreparedStatement ps = conn.prepareStatement("SELECT * FROM users")
) {
ResultSet rs = ps.executeQuery();
// conn and ps are closed automatically when the block exits (even on exception)
} catch (SQLException e) {
log.error("DB error", e);
}
// Multi-catch β handle multiple types in one block
try {
riskyOp();
} catch (IOException | SQLException e) {
log.error("Operation failed", e);
}
// Custom exception β prefer unchecked for application-level errors
public class UserNotFoundException extends RuntimeException {
private final Long userId;
public UserNotFoundException(Long id) {
super("User not found: " + id);
this.userId = id;
}
public Long getUserId() { return userId; }
}
// Throw custom exception
User user = userRepo.findById(id)
.orElseThrow(() -> new UserNotFoundException(id));
Never swallow exceptions silently (
catch (Exception e) {}). Either handle meaningfully or re-throw. Always preserve the original cause:throw new AppException("msg", e).
OOP Pillars
Encapsulation
Keep state private; expose behavior through methods. Callers cannot put the object in an invalid state.
public class BankAccount {
private double balance;
private final String owner;
public BankAccount(String owner, double initialBalance) {
if (initialBalance < 0) throw new IllegalArgumentException("Negative balance");
this.owner = owner;
this.balance = initialBalance;
}
public void deposit(double amount) {
if (amount <= 0) throw new IllegalArgumentException("Amount must be positive");
balance += amount;
}
public void withdraw(double amount) {
if (amount <= 0) throw new IllegalArgumentException("Amount must be positive");
if (amount > balance) throw new IllegalStateException("Insufficient funds");
balance -= amount;
}
public double getBalance() { return balance; }
public String getOwner() { return owner; }
}
Inheritance & Polymorphism
A subclass is-a superclass. The JVM decides at runtime which method to call based on the actual object type, not the variable type.
public abstract class Animal {
protected final String name;
public Animal(String name) { this.name = name; }
public abstract String speak(); // each subclass must implement
@Override
public String toString() { return name + " says: " + speak(); }
}
public class Dog extends Animal {
public Dog(String name) { super(name); }
@Override public String speak() { return "Woof!"; }
}
public class Cat extends Animal {
public Cat(String name) { super(name); }
@Override public String speak() { return "Meow!"; }
}
// Polymorphism in action β the loop doesn't know or care which subtype it has
List<Animal> animals = List.of(new Dog("Rex"), new Cat("Luna"), new Dog("Buddy"));
animals.forEach(System.out::println);
// Rex says: Woof!
// Luna says: Meow!
// Buddy says: Woof!
Abstraction
Hide implementation details. Expose only what the caller needs.
// Abstract class β partial implementation, forces subclasses to fill in the rest
public abstract class DataExporter {
// Template method β orchestrates the steps, calls abstract ones
public final void export(List<?> data) {
byte[] formatted = format(data); // abstract
String destination = getDestination(); // abstract
writeToDestination(formatted, destination); // implemented here
log("Exported " + data.size() + " records to " + destination);
}
protected abstract byte[] format(List<?> data);
protected abstract String getDestination();
private void writeToDestination(byte[] data, String dest) { /* ... */ }
private void log(String msg) { System.out.println(msg); }
}
// Interface β defines a capability contract; unrelated classes can both implement it
public interface Auditable {
AuditEntry getLastModified();
default String auditSummary() { // optional to override (Java 8+)
return "Modified: " + getLastModified();
}
}
// A class can implement multiple interfaces
public class Order extends DomainEntity implements Auditable, Exportable { /* ... */ }
Interfaces vs Abstract Classes
| Β | Interface | Abstract Class |
|---|---|---|
| Multiple inheritance | Yes β a class can implements many |
No β single extends only |
| State (instance fields) | No β only static final constants |
Yes |
| Constructor | No | Yes |
| Default method body | Yes (Java 8+) | Yes |
| Access modifiers | public or private (Java 9+) |
Any |
| When to use | Define a capability (βis printableβ, βis comparableβ) | Share implementation among related classes |
Collections Framework
Always declare with the interface type on the left:
// List β ordered, duplicates allowed
List<String> list = new ArrayList<>(); // O(1) get by index, O(n) insert in middle
List<String> linked = new LinkedList<>(); // O(1) insert at head/tail, O(n) random access
// Set β no duplicates
Set<String> hash = new HashSet<>(); // O(1) average, no order
Set<String> tree = new TreeSet<>(); // O(log n), natural sorted order
Set<String> linked = new LinkedHashSet(); // O(1) average, insertion order preserved
// Map β keyβvalue pairs, no duplicate keys
Map<String, Integer> hash = new HashMap<>(); // O(1) average, no order
Map<String, Integer> tree = new TreeMap<>(); // O(log n), sorted keys
Map<String, Integer> linked = new LinkedHashMap<>(); // insertion order
Map<String, Integer> concurrent = new ConcurrentHashMap<>(); // thread-safe
// Queue and Deque
Queue<String> queue = new LinkedList<>(); // FIFO β add to tail, poll from head
Deque<Integer> deque = new ArrayDeque<>(); // double-ended, faster than LinkedList
PriorityQueue<Integer> pq = new PriorityQueue<>(); // min-heap by default; pass Comparator for max
// Immutable collections (Java 9+) β cannot add, remove, or modify
List<String> fixed = List.of("a", "b", "c");
Set<Integer> fixedSet = Set.of(1, 2, 3);
Map<String, Integer> fixedMap = Map.of("x", 1, "y", 2);
Common operations:
List<String> list = new ArrayList<>(List.of("banana", "apple", "cherry", "apple"));
list.remove("apple"); // removes first occurrence
list.removeIf(s -> s.length() > 5); // remove all matching
Collections.sort(list); // ["cherry"]? depends on removes
list.sort(Comparator.comparingInt(String::length).reversed()); // by length desc
Map<String, Integer> map = new HashMap<>();
map.put("a", 1);
map.getOrDefault("missing", 0); // safe default
map.putIfAbsent("a", 99); // only inserts if key absent
map.computeIfAbsent("new", k -> k.length()); // compute and store
map.merge("a", 1, Integer::sum); // existing value + 1
π‘ Medior
Functional Interfaces & Lambdas
A functional interface has exactly one abstract method. Java uses them as the target type for lambdas and method references.
Built-in functional interfaces in java.util.function:
| Interface | Method signature | Purpose |
|---|---|---|
Function<T,R> |
R apply(T t) |
Transform a value |
BiFunction<T,U,R> |
R apply(T t, U u) |
Transform two inputs |
Predicate<T> |
boolean test(T t) |
Test a condition |
Consumer<T> |
void accept(T t) |
Consume a value (side effect) |
Supplier<T> |
T get() |
Produce a value |
UnaryOperator<T> |
T apply(T t) |
Transform where input type = output type |
BinaryOperator<T> |
T apply(T t1, T t2) |
Combine two values of same type |
// Function β transform
Function<String, Integer> len = s -> s.length();
Function<String, String> upper = String::toUpperCase;
Function<String, String> pipe = upper.andThen(s -> "[" + s + "]");
// pipe.apply("hi") β "[HI]"
// Predicate β filter
Predicate<String> isLong = s -> s.length() > 5;
Predicate<String> isUpper = s -> s.equals(s.toUpperCase());
Predicate<String> both = isLong.and(isUpper);
Predicate<String> either = isLong.or(isUpper);
Predicate<String> notLong = isLong.negate();
// Consumer β side effects
Consumer<String> print = System.out::println;
Consumer<String> upper2 = s -> System.out.println(s.toUpperCase());
Consumer<String> both2 = print.andThen(upper2); // runs both in sequence
// Supplier β lazy factory
Supplier<List<String>> listFactory = ArrayList::new;
Supplier<LocalDateTime> now = LocalDateTime::now; // called lazily when needed
// Practical chaining
List<String> names = List.of("alice", "bob", "charlie", "diana");
names.stream()
.filter(isLong) // keep names > 5 chars
.map(upper) // uppercase
.forEach(print); // print each
Method References
Cleaner shorthand for lambdas that just call an existing method:
// 1. Static method reference: Type::staticMethod
Function<String, Integer> parse = Integer::parseInt; // s -> Integer.parseInt(s)
// 2. Bound instance method: instance::method
String prefix = "Hello, ";
Function<String, String> greet = prefix::concat; // s -> prefix.concat(s)
// 3. Unbound instance method: Type::instanceMethod
Function<String, String> toLower = String::toLowerCase; // s -> s.toLowerCase()
Function<String, Integer> length = String::length;
// 4. Constructor reference: Type::new
Supplier<ArrayList<String>> makeList = ArrayList::new;
Function<String, StringBuilder> makeSb = StringBuilder::new;
// Real-world example
List<String> emails = users.stream()
.map(User::getEmail) // unbound β calls email() on each User
.filter(Objects::nonNull) // static β filters nulls
.map(String::toLowerCase) // unbound β lowercase each email
.collect(Collectors.toList());
Generics
Bounded wildcards β the PECS rule (Producer Extends, Consumer Super):
// Upper bound: <? extends T> β read from (producer), cannot write
public double sumAll(List<? extends Number> numbers) {
// Safe to READ: every element is at least a Number
return numbers.stream().mapToDouble(Number::doubleValue).sum();
// NOT safe: numbers.add(new Integer(1)); β type unknown, could be List<Double>
}
sumAll(List.of(1, 2, 3)); // accepts List<Integer>
sumAll(List.of(1.0, 2.0)); // accepts List<Double>
// Lower bound: <? super T> β write to (consumer), reading returns Object
public void fillWithValue(List<? super Integer> list, int count, int value) {
// Safe to WRITE: we know the list accepts at least Integers
for (int i = 0; i < count; i++) list.add(value);
// Reading: Object obj = list.get(0); β can only read as Object
}
fillWithValue(new ArrayList<Integer>(), 3, 0); // OK
fillWithValue(new ArrayList<Number>(), 3, 0); // OK
fillWithValue(new ArrayList<Object>(), 3, 0); // OK
// Generic class
public class Pair<A, B> {
private final A first;
private final B second;
public Pair(A first, B second) { this.first = first; this.second = second; }
public static <X, Y> Pair<X, Y> of(X x, Y y) { return new Pair<>(x, y); }
}
Pair<String, Integer> p = Pair.of("score", 42);
Type erasure β generic type parameters are removed at compile time. At runtime List<String> and List<Integer> are both just List.
// These are the same type at runtime (can't distinguish):
List<String> strings = new ArrayList<>();
List<Integer> ints = new ArrayList<>();
// Cannot do at runtime:
// if (list instanceof List<String>) { } // compile error
// T obj = new T(); // compile error β T unknown at runtime
// T[] arr = new T[10]; // compile error
// Workaround β pass Class<T> token:
public <T> T deserialize(String json, Class<T> type) {
return objectMapper.readValue(json, type);
}
User user = deserialize(json, User.class);
Java 8+ β Streams
Streams are lazy, single-pass sequences of operations over data sources. They never modify the source collection.
List<Employee> employees = getEmployees();
// --- Intermediate operations (lazy, build the pipeline) ---
employees.stream()
.filter(e -> e.getSalary() > 50_000) // keep matching
.map(Employee::getName) // transform
.sorted() // natural order (allocates)
.distinct() // deduplicate
.limit(10) // take first 10
.skip(2) // skip first 2
.peek(name -> log.debug("Processing: {}", name)) // side-effect, debugging only
// --- Terminal operations (eager, trigger evaluation) ---
.collect(Collectors.toList()); // collect to mutable list
// .toList() // Java 16+ β unmodifiable list
// .count()
// .findFirst() // Optional<T>
// .anyMatch(predicate) // boolean
// .allMatch(predicate)
// .noneMatch(predicate)
// .min(comparator) // Optional<T>
// .max(comparator)
// .reduce(identity, accumulator) // fold
// --- Grouping and collecting ---
Map<String, List<Employee>> byDept = employees.stream()
.collect(Collectors.groupingBy(Employee::getDepartment));
Map<String, Double> avgSalaryByDept = employees.stream()
.collect(Collectors.groupingBy(
Employee::getDepartment,
Collectors.averagingDouble(Employee::getSalary)
));
Map<Boolean, List<Employee>> partitioned = employees.stream()
.collect(Collectors.partitioningBy(e -> e.getSalary() > 60_000));
// partitioned.get(true) β high earners, .get(false) β the rest
// --- flatMap β flatten one level ---
List<String> allSkills = employees.stream()
.flatMap(e -> e.getSkills().stream()) // each employee has List<String> skills
.distinct()
.sorted()
.toList();
// --- Numeric streams (no boxing overhead) ---
IntStream.rangeClosed(1, 100).sum(); // 5050
OptionalDouble avg = employees.stream()
.mapToDouble(Employee::getSalary).average();
// --- Joining ---
String csv = employees.stream()
.map(Employee::getName)
.collect(Collectors.joining(", ", "[", "]")); // [Alice, Bob, Charlie]
// --- Parallel stream (use with care) ---
long count = veryLargeList.parallelStream()
.filter(s -> expensiveCheck(s))
.count();
// Parallel is NOT always faster. Overhead + coordination can make it slower for small lists.
// Avoid stateful operations (sorting, distinct) in parallel streams.
Java 8+ β Optional
Optional<T> is a container that may or may not hold a non-null value. Use it as a return type to make βmight not existβ explicit. Never use it as a method parameter or field.
// Creating
Optional<String> present = Optional.of("value"); // throws NPE if null
Optional<String> nullable = Optional.ofNullable(getValue()); // null β empty
Optional<String> empty = Optional.empty();
// Safe consumption β no isPresent() checks needed
optional.ifPresent(System.out::println);
optional.ifPresentOrElse( // Java 9+
v -> System.out.println("Found: " + v),
() -> System.out.println("Not found")
);
// Extracting values
String value = optional.orElse("default");
String lazy = optional.orElseGet(() -> buildDefault()); // computed only if empty
String strict = optional.orElseThrow(() -> new NotFoundException("missing"));
// Transforming without unpacking
Optional<Integer> length = optional.map(String::length);
Optional<User> user = optional
.filter(s -> s.length() > 3) // keep if condition met
.map(email -> findUserByEmail(email))
.flatMap(u -> u.getProfile()); // flatMap when transform itself returns Optional
// Chaining across multiple steps
Optional<String> contactEmail = findUser(userId) // Optional<User>
.map(User::getProfile) // Optional<Profile>
.flatMap(Profile::getContactEmail) // Optional<String>
.filter(email -> !email.isBlank());
Modern Java Features
Records (Java 16+) β immutable data classes; auto-generates constructor, accessors, equals, hashCode, toString:
public record Point(int x, int y) {
// Compact constructor β runs before storage, used for validation
Point {
if (x < 0 || y < 0) throw new IllegalArgumentException("Negative coordinate");
}
// Custom methods are allowed
public double distanceTo(Point other) {
return Math.hypot(x - other.x, y - other.y);
}
// Static factory
public static Point origin() { return new Point(0, 0); }
}
Point p = new Point(3, 4);
p.x(); // 3 β accessor is field name, not getX()
p.distanceTo(new Point(0, 0)); // 5.0
Sealed classes (Java 17+) β the compiler knows every possible subtype at compile time:
public sealed interface Shape permits Circle, Rectangle, Triangle {}
public record Circle(double radius) implements Shape {}
public record Rectangle(double width, double height) implements Shape {}
public record Triangle(double base, double height) implements Shape {}
// Exhaustive switch β no default needed!
double area = switch (shape) {
case Circle c -> Math.PI * c.radius() * c.radius();
case Rectangle r -> r.width() * r.height();
case Triangle t -> 0.5 * t.base() * t.height();
};
Pattern matching for instanceof (Java 16+):
// Old way
if (obj instanceof String) {
String s = (String) obj; // redundant cast
System.out.println(s.length());
}
// Pattern matching β binds variable in one step
if (obj instanceof String s && s.length() > 5) {
System.out.println(s.toUpperCase()); // s is String here, compiler knows
}
Switch expressions (Java 14+) and pattern switch (Java 21):
// Switch expression β returns a value, no fall-through
String label = switch (day) {
case MONDAY, TUESDAY -> "Early week";
case WEDNESDAY -> "Midweek";
case THURSDAY, FRIDAY -> "Late week";
default -> "Weekend";
};
// Pattern matching in switch (Java 21) β works on any type
String describe = switch (obj) {
case Integer i when i < 0 -> "negative int: " + i;
case Integer i -> "positive int: " + i;
case String s when s.isBlank() -> "blank string";
case String s -> "string: " + s;
case null -> "null";
default -> obj.getClass().getSimpleName();
};
Text blocks (Java 15+):
// Indentation is stripped automatically up to the closing """
String sql = """
SELECT u.id, u.name, o.total
FROM users u
JOIN orders o ON o.user_id = u.id
WHERE u.active = true
ORDER BY o.total DESC
""";
String html = """
<html>
<body>
<h1>Hello, %s!</h1>
</body>
</html>
""".formatted(name);
ExecutorService & Thread Pools
// Fixed pool β N threads, tasks queue up when all busy. Good for CPU-bound work.
int cores = Runtime.getRuntime().availableProcessors();
ExecutorService cpuPool = Executors.newFixedThreadPool(cores);
// Cached pool β creates threads on demand, reuses idle ones. Good for short I/O tasks.
ExecutorService ioPool = Executors.newCachedThreadPool();
// Virtual thread executor (Java 21) β one virtual thread per task, millions possible
ExecutorService vPool = Executors.newVirtualThreadPerTaskExecutor();
// Scheduled pool β delays and periodic tasks
ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(2);
scheduler.schedule(() -> sendReminder(), 1, TimeUnit.HOURS); // once after delay
scheduler.scheduleAtFixedRate(() -> collectMetrics(), 0, 30, TimeUnit.SECONDS); // fixed rate
scheduler.scheduleWithFixedDelay(() -> poll(), 0, 5, TimeUnit.SECONDS); // delay between end and start
// Submitting work
Future<String> future = pool.submit(() -> fetchFromApi());
String result = future.get(10, TimeUnit.SECONDS); // blocks with timeout
// Invoke all β submit batch, wait for all to complete
List<Callable<String>> tasks = List.of(
() -> fetchFromServiceA(),
() -> fetchFromServiceB(),
() -> fetchFromServiceC()
);
List<Future<String>> results = pool.invokeAll(tasks, 30, TimeUnit.SECONDS);
// Always shut down cleanly
pool.shutdown(); // stop accepting new tasks
boolean done = pool.awaitTermination(30, TimeUnit.SECONDS);
if (!done) pool.shutdownNow(); // interrupt running tasks
π΄ Senior
JVM Architecture
Source (.java)
βββΆ javac compiler
βββΆ Bytecode (.class files)
βββΆ ClassLoader subsystem
ββ Bootstrap ClassLoader β core Java (java.lang, java.util)
ββ Platform ClassLoader β java.se modules
ββ Application ClassLoader β your classpath
βββΆ Bytecode Verifier (security + type correctness)
βββΆ Execution Engine
ββ Interpreter β executes bytecode directly
ββ JIT Compiler β compiles HOT methods to native
ββ C1 (client): fast compile, basic opts
ββ C2 (server): aggressive opts after profiling
JIT optimizations (happen transparently after enough executions):
- Inlining β replaces method call with the method body at the call site; eliminates call overhead
- Escape analysis β if an object doesnβt escape a method, allocate it on the stack instead of heap (no GC pressure)
- Devirtualization β if a virtual method is only ever called on one concrete type, replace with a direct call
- Loop unrolling β replicate loop body to reduce loop overhead
- Constant folding β
2 + 3becomes5at compile/JIT time
GraalVM Native Image β compiles ahead-of-time to a native binary:
native-image -jar app.jar -o app-native
./app-native # starts in ~10ms, uses 50-80% less RAM than JVM
Limitation: dynamic features (reflection, proxies, class loading) require extra configuration (native-image.properties, reflect-config.json).
Class loading and initialization:
// Classes are loaded lazily β only when first referenced
// Static initializers run exactly once, at class initialization time
public class Config {
static {
System.out.println("Config loaded"); // prints once ever
}
private static final Map<String, String> VALUES = loadFromFile();
}
// Custom class loader β used for hot-reload, plugin systems, isolation
public class PluginClassLoader extends URLClassLoader {
public PluginClassLoader(URL[] urls) {
super(urls, null); // null parent = parent-last (load ourselves first)
}
}
Advanced Concurrency
CompletableFuture β async pipelines:
// Async supply + transform chain
CompletableFuture<UserDto> result = CompletableFuture
.supplyAsync(() -> userRepo.findById(id), ioExecutor) // runs on ioExecutor
.thenApplyAsync(user -> enrichWithRoles(user), ioExecutor)
.thenApply(UserDto::from); // lightweight, stays on same thread
// Combining independent futures (fan-out β fan-in)
CompletableFuture<User> userFuture = CompletableFuture.supplyAsync(() -> fetchUser(id));
CompletableFuture<Orders> ordersFuture = CompletableFuture.supplyAsync(() -> fetchOrders(id));
CompletableFuture<ProfilePage> page = userFuture.thenCombine(
ordersFuture,
(user, orders) -> new ProfilePage(user, orders)
);
// Wait for all (fan-out β all must succeed)
CompletableFuture.allOf(f1, f2, f3)
.thenRun(() -> System.out.println("All done"));
// Race β first to succeed wins
CompletableFuture.anyOf(regionA, regionB)
.thenApply(result -> (String) result);
// Error handling
CompletableFuture<User> safe = CompletableFuture
.supplyAsync(() -> fetchUser(id))
.exceptionally(ex -> {
log.warn("Falling back for user {}: {}", id, ex.getMessage());
return User.anonymous();
})
.handle((user, ex) -> ex != null ? User.anonymous() : user) // alternative: handle both paths
.whenComplete((user, ex) -> audit.log(id, ex)); // always runs, like finally
Low-level synchronization:
// synchronized β coarse implicit lock, simple but can cause deadlocks
synchronized (lock) { counter++; }
synchronized void increment() { counter++; } // locks on 'this'
// ReentrantLock β explicit, more control
ReentrantLock lock = new ReentrantLock(true); // fair=true: FIFO ordering
lock.lock();
try {
doWork();
} finally {
lock.unlock(); // ALWAYS in finally β never leave a lock acquired
}
// tryLock β non-blocking; good for avoiding deadlocks
if (lock.tryLock(100, TimeUnit.MILLISECONDS)) {
try { doWork(); } finally { lock.unlock(); }
} else {
handleTimeout(); // graceful degradation
}
// ReadWriteLock β multiple readers OR one exclusive writer
ReadWriteLock rwLock = new ReentrantReadWriteLock();
// Reading (many threads simultaneously)
rwLock.readLock().lock();
try { return data; } finally { rwLock.readLock().unlock(); }
// Writing (exclusive)
rwLock.writeLock().lock();
try { data = newData; } finally { rwLock.writeLock().unlock(); }
// StampedLock (Java 8+) β adds optimistic reads for maximum throughput
StampedLock sl = new StampedLock();
long stamp = sl.tryOptimisticRead();
double x = this.x, y = this.y; // read without locking
if (!sl.validate(stamp)) { // a write happened β our read is stale
stamp = sl.readLock();
try { x = this.x; y = this.y; }
finally { sl.unlockRead(stamp); }
}
// Atomic variables β lock-free CAS (compare-and-swap)
AtomicInteger counter = new AtomicInteger(0);
AtomicLong version = new AtomicLong(0);
AtomicReference<State> state = new AtomicReference<>(State.IDLE);
counter.incrementAndGet(); // atomic read-increment-write
counter.compareAndSet(expected, newVal); // sets only if current == expected
state.updateAndGet(s -> s.transition()); // apply function atomically
LongAdder adder = new LongAdder(); // better than AtomicLong under high contention
adder.increment();
long total = adder.sum(); // approximate read (stripes internally)
// volatile β visibility guarantee; NOT atomicity
// Ensures all threads see the most recent write; prevents CPU register caching
volatile boolean shutdown = false;
// Thread A: shutdown = true; β guaranteed visible to thread B
// Thread B: while (!shutdown) {} β will observe the update
Concurrency data structures:
// Producer-Consumer pattern β BackPressure via bounded queue
BlockingQueue<Task> queue = new LinkedBlockingQueue<>(500); // blocks at 500
// Producer:
queue.put(task); // blocks if full β natural backpressure
queue.offer(task, 1, SECONDS); // timeout version
// Consumer:
Task t = queue.take(); // blocks until item available
// CopyOnWriteArrayList β reads are lock-free; writes copy entire array
// Good for lists that are mostly read, rarely written (event listeners, etc.)
CopyOnWriteArrayList<EventListener> listeners = new CopyOnWriteArrayList<>();
// ConcurrentSkipListMap β sorted, thread-safe, lock-free reads
NavigableMap<Long, Session> sessions = new ConcurrentSkipListMap<>();
// Phaser β flexible synchronization barrier (Java 7+, more flexible than CountDownLatch)
Phaser phaser = new Phaser(3); // 3 parties
// Each thread: phaser.arriveAndAwaitAdvance(); β waits until all 3 arrive
Virtual Threads (Java 21 β Project Loom):
// Platform threads: ~1MB stack, OS thread, thousands max before OOM
// Virtual threads: few KB stack, JVM-scheduled, millions possible
// Create virtual thread
Thread.ofVirtual().name("request-handler").start(() -> handleRequest(req));
// Virtual thread executor β one VT per task (I/O scales massively)
try (ExecutorService ex = Executors.newVirtualThreadPerTaskExecutor()) {
for (Request req : requests) {
ex.submit(() -> processRequest(req)); // blocking I/O is fine here
}
} // auto-closes, waits for all tasks
// KEY RULES for virtual threads:
// 1. Avoid synchronized blocks β they PIN the VT to a platform thread
// Use ReentrantLock instead
// 2. Avoid ThreadLocal for large objects β one VT per task = many ThreadLocals
// Consider ScopedValue (Java 21 preview) instead
// 3. Blocking I/O inside VTs is the whole point β don't try to make it non-blocking
// 4. Don't pool virtual threads β they're cheap to create, just make new ones
Garbage Collection Deep Dive
JVM Heap layout:
βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Young Generation β
β ββ Eden Space (new objects start here) β
β ββ Survivor S0 (survive 1st GC β here) β
β ββ Survivor S1 (survive 2nd GC β here) β
β β
β Old Generation (Tenured) β
β ββ Objects surviving N minor GCs get promoted hereβ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Metaspace (NOT on heap β native OS memory)
ββ Class metadata, method bytecode, string pool
Most objects die young β this is the generational hypothesis that makes GC efficient.
| GC | Stop-the-World | Throughput | Best for |
|---|---|---|---|
| G1 (Java 9+ default) | Lowβmedium, incremental | High | General purpose apps |
| ZGC (Java 15+ prod-ready) | <1ms even on 16TB heap | Medium | Low-latency (trading, gaming) |
| Shenandoah | <1ms | Medium | Low-latency |
| Parallel GC | Medium | Very high | Batch/throughput (pauses OK) |
| Serial GC | High | Low | Single-core, embedded |
# Common JVM flags
java -Xms512m -Xmx4g # initial and max heap
java -XX:+UseZGC -Xmx16g app.jar # ZGC for latency-critical
java -XX:+UseG1GC -XX:MaxGCPauseMillis=100 # G1 with 100ms pause target
java -Xlog:gc*:file=gc.log:time,uptime # GC logging (Java 9+)
java -XX:+HeapDumpOnOutOfMemoryError \
-XX:HeapDumpPath=/tmp/heap.hprof # auto heap dump on OOM
Memory leak patterns:
// 1. Static collection that grows without bound
static final Map<String, byte[]> cache = new HashMap<>(); // LEAK
// Fix: use bounded cache (Caffeine), WeakHashMap, or expiry
// 2. Listeners never removed
eventBus.subscribe(this::handleEvent);
// Fix: eventBus.unsubscribe(this::handleEvent) when done
// 3. ThreadLocal in thread pool β values survive task boundaries
ThreadLocal<Connection> holder = new ThreadLocal<>();
holder.set(getConnection());
try { work(); }
finally { holder.remove(); } // CRITICAL β without this, connection leaks across tasks
// 4. Inner class holds reference to outer
button.addActionListener(new ActionListener() {
void actionPerformed(ActionEvent e) { ... }
// This anonymous class holds a reference to the enclosing class!
// Fix: use static nested class or lambda
});
Java Module System (Java 9+)
// module-info.java at the root of your source directory
module com.example.users {
// Declare what we need
requires java.sql; // explicit dependency
requires transitive com.example.common; // transitive: our consumers also get it
// Declare what we expose β everything else is private to this module
exports com.example.users.api; // public packages
exports com.example.users.model to com.example.reporting; // targeted export
// Allow reflection (e.g., for frameworks that use it)
opens com.example.users.model to com.example.orm;
// Service provider pattern
provides UserService with UserServiceImpl;
uses AuditLogger; // we'll consume a service
}
Why it matters:
- Strong encapsulation:
publicinside a non-exported package is inaccessible to other modules β even via reflection (unlessopens) - Reliable configuration: missing modules are detected at startup, not when the missing class is first used at runtime
- Smaller deploys:
jlinkcreates a custom JDK containing only the modules your app actually needs
jlink --module-path $JAVA_HOME/jmods:build/modules \
--add-modules com.example.app \
--output dist/runtime
# dist/runtime is a minimal JDK image β can be ~30MB instead of 200MB+
Reflection & Annotations
// Inspect a class at runtime
Class<?> clazz = User.class;
// Alternative: Class.forName("com.example.User") β used when class name is dynamic
// Access fields (even private ones)
Field nameField = clazz.getDeclaredField("name");
nameField.setAccessible(true); // bypass private modifier
nameField.set(userInstance, "Updated"); // write
String val = (String) nameField.get(userInstance); // read
// Access methods
Method method = clazz.getDeclaredMethod("validate", String.class);
method.setAccessible(true);
Object result = method.invoke(userInstance, "input");
// Read annotations at runtime
for (Field field : clazz.getDeclaredFields()) {
if (field.isAnnotationPresent(Validate.class)) {
Validate ann = field.getAnnotation(Validate.class);
// act based on annotation attributes
}
}
Custom annotations:
// Define an annotation
@Retention(RetentionPolicy.RUNTIME) // survives until runtime (SOURCE=compile only, CLASS=bytecode)
@Target({ElementType.FIELD, ElementType.METHOD})
public @interface Required {
String message() default "This field is required";
int minLength() default 0;
}
// Apply it
public class CreateUserRequest {
@Required(message = "Name is mandatory", minLength = 2)
private String name;
@Required
private String email;
}
// Process at runtime (simple validator example)
public void validate(Object obj) throws ValidationException {
for (Field field : obj.getClass().getDeclaredFields()) {
if (!field.isAnnotationPresent(Required.class)) continue;
field.setAccessible(true);
Required ann = field.getAnnotation(Required.class);
Object val = field.get(obj);
if (val == null || val.toString().isBlank()) {
throw new ValidationException(field.getName() + ": " + ann.message());
}
if (val.toString().length() < ann.minLength()) {
throw new ValidationException(field.getName() + " too short (min " + ann.minLength() + ")");
}
}
}
Spring Boot
// Entry point
@SpringBootApplication // = @Configuration + @EnableAutoConfiguration + @ComponentScan
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
// REST Controller
@RestController
@RequestMapping("/api/v1/users")
public class UserController {
private final UserService userService;
// Constructor injection β always prefer over @Autowired on fields
public UserController(UserService userService) {
this.userService = userService;
}
@GetMapping
public Page<UserDto> list(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "20") int size
) {
return userService.list(PageRequest.of(page, size));
}
@GetMapping("/{id}")
public ResponseEntity<UserDto> get(@PathVariable Long id) {
return ResponseEntity.ok(userService.findById(id));
}
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public UserDto create(@Valid @RequestBody CreateUserRequest req) {
return userService.create(req);
}
@PatchMapping("/{id}")
public UserDto update(@PathVariable Long id, @Valid @RequestBody UpdateUserRequest req) {
return userService.update(id, req);
}
@DeleteMapping("/{id}")
@ResponseStatus(HttpStatus.NO_CONTENT)
public void delete(@PathVariable Long id) {
userService.delete(id);
}
}
// Service with transaction management
@Service
@Transactional(readOnly = true) // all methods read-only by default β prevents accidental writes
public class UserService {
private final UserRepository repo;
private final ApplicationEventPublisher events;
public UserService(UserRepository repo, ApplicationEventPublisher events) {
this.repo = repo;
this.events = events;
}
public UserDto findById(Long id) {
return repo.findById(id).map(UserDto::from)
.orElseThrow(() -> new UserNotFoundException(id));
}
@Transactional // writable β creates a new transaction (or joins existing)
public UserDto create(CreateUserRequest req) {
if (repo.existsByEmail(req.getEmail())) {
throw new ConflictException("Email already registered");
}
User saved = repo.save(new User(req));
events.publishEvent(new UserCreatedEvent(saved)); // transactional event
return UserDto.from(saved);
}
}
// Spring Data JPA repository
public interface UserRepository extends JpaRepository<User, Long> {
// Derived queries β Spring generates JPQL from the method name
Optional<User> findByEmail(String email);
boolean existsByEmail(String email);
List<User> findByActiveTrue();
List<User> findByNameContainingIgnoreCaseOrderByCreatedAtDesc(String name);
// Custom JPQL
@Query("SELECT u FROM User u WHERE u.department = :dept AND u.salary > :min")
List<User> findHighEarnersInDept(@Param("dept") String dept, @Param("min") double min);
// Pagination
Page<UserDto> findAllProjectedBy(Pageable pageable); // projections avoid loading full entity
// Modifying queries
@Modifying
@Query("UPDATE User u SET u.active = false WHERE u.lastLoginAt < :cutoff")
int deactivateInactive(@Param("cutoff") LocalDateTime cutoff);
}
Configuration best practices:
# application.yml
spring:
datasource:
url: jdbc:postgresql://localhost:5432/mydb
username: ${DB_USER} # from environment variable β never hardcode credentials
password: ${DB_PASS}
hikari:
maximum-pool-size: 10 # match your DB connection limit
connection-timeout: 3000
jpa:
hibernate:
ddl-auto: validate # prod: validate. never create-drop in prod!
open-in-view: false # prevents N+1 queries from lazy loading in views
profiles:
active: ${SPRING_PROFILES_ACTIVE:local}
Design Patterns
Builder β complex objects with many optional parameters:
// Records handle simple immutable DTOs. Builder is for complex construction.
public class HttpRequest {
private final String method, url, body;
private final Map<String, String> headers;
private final Duration timeout;
private HttpRequest(Builder b) {
this.method = b.method;
this.url = b.url;
this.body = b.body;
this.headers = Map.copyOf(b.headers);
this.timeout = b.timeout;
}
public static class Builder {
private final String method, url; // required
private String body = null;
private Map<String, String> headers = new HashMap<>();
private Duration timeout = Duration.ofSeconds(30);
public Builder(String method, String url) {
this.method = Objects.requireNonNull(method);
this.url = Objects.requireNonNull(url);
}
public Builder body(String body) { this.body = body; return this; }
public Builder header(String k, String v) { headers.put(k, v); return this; }
public Builder timeout(Duration t) { this.timeout = t; return this; }
public HttpRequest build() { return new HttpRequest(this); }
}
}
HttpRequest req = new HttpRequest.Builder("POST", "https://api.example.com/data")
.header("Authorization", "Bearer " + token)
.header("Content-Type", "application/json")
.body(json)
.timeout(Duration.ofSeconds(5))
.build();
Strategy β swap algorithms at runtime:
@FunctionalInterface
public interface PricingStrategy {
double calculatePrice(double basePrice, User user);
}
public class PricingService {
private PricingStrategy strategy;
public void setStrategy(PricingStrategy s) { this.strategy = s; }
public double price(double base, User user) { return strategy.calculatePrice(base, user); }
}
// Strategies as lambdas
PricingService pricing = new PricingService();
pricing.setStrategy((base, user) -> base); // regular
pricing.setStrategy((base, user) -> base * 0.8); // 20% discount
pricing.setStrategy((base, user) -> user.isPremium() ? base * 0.7 : base); // conditional
Observer via Spring events:
// Event
public record UserCreatedEvent(User user) {}
// Publisher
@Service
public class UserService {
private final ApplicationEventPublisher events;
@Transactional
public User create(CreateUserRequest req) {
User user = repo.save(new User(req));
events.publishEvent(new UserCreatedEvent(user));
return user;
}
}
// Multiple independent listeners β decoupled from the service
@EventListener
@Async // runs in a separate thread β don't block the transaction
public void sendWelcomeEmail(UserCreatedEvent e) {
emailService.sendWelcome(e.user().getEmail());
}
@EventListener
@Async
public void createDefaultSettings(UserCreatedEvent e) {
settingsService.initDefaults(e.user().getId());
}
Singleton β best approach in Java is enum:
public enum AppRegistry {
INSTANCE;
private final Map<String, Object> data = new ConcurrentHashMap<>();
public void register(String key, Object value) { data.put(key, value); }
public Object lookup(String key) { return data.get(key); }
}
// AppRegistry.INSTANCE.register("key", value);
// JVM guarantees enum instances are initialized exactly once β no double-checked locking needed.
Performance Tips
// 1. Use primitive streams for numbers β no boxing, ~2-5x faster than Stream<Integer>
int sum = IntStream.rangeClosed(1, 1_000_000).sum(); // no boxing
// vs:
int boxed = Stream.iterate(1, n -> n + 1).limit(1_000_000)
.mapToInt(Integer::intValue).sum(); // unboxes each time
// 2. Pre-size collections when you know the capacity
new ArrayList<>(expectedSize);
new HashMap<>((int)(expectedSize / 0.75) + 1); // account for load factor
// 3. Use ArrayDeque instead of Stack or LinkedList as a stack/queue
Deque<String> stack = new ArrayDeque<>(); // no sync overhead, better cache locality
// 4. String.intern() for string deduplication (rarely needed β JVM does it for literals)
String s = longString.intern(); // points to pool copy
// 5. Lazy initialization with double-checked locking
private volatile ExpensiveResource resource;
public ExpensiveResource getResource() {
if (resource == null) { // first check (no lock)
synchronized (this) {
if (resource == null) { // second check (with lock)
resource = new ExpensiveResource();
}
}
}
return resource;
}
// Or simpler: use a Holder class (guaranteed by JVM class loading)
private static class Holder {
static final ExpensiveResource INSTANCE = new ExpensiveResource();
}
public ExpensiveResource getResource() { return Holder.INSTANCE; }
// 6. Avoid reflection in hot paths β reflection is 10-100x slower than direct calls
// Cache Method/Field objects if you must use reflection repeatedly
// 7. Use JMH for accurate benchmarks β System.currentTimeMillis() is not enough
@Benchmark
@BenchmarkMode(Mode.Throughput)
public void benchmarkStringOp(Blackhole bh) {
bh.consume("hello".toUpperCase()); // Blackhole prevents dead-code elimination
}
Senior Gotchas
==vs.equals()β==compares references;.equals()compares content. Use.equals()for objects. For null-safety:Objects.equals(a, b).HashMapis NOT thread-safe β concurrent reads+writes cause data corruption and infinite loops. UseConcurrentHashMapinstead.finalnuance βfinalon a variable means the reference cannot be reassigned; the object itself CAN still be mutated.final List<String> listβ you cannot dolist = new ArrayList<>()butlist.add("x")is perfectly legal.- Memory leaks despite GC β GC collects unreachable objects. Leaks happen via: static collections, unclosed streams,
ThreadLocalnot removed, listeners never deregistered. - Checked vs unchecked β prefer
RuntimeExceptionfor application-level errors (not recoverable by the caller). Reserve checked exceptions for recoverable I/O failures where callers genuinely can do something useful. Collectors.toList()vsStream.toList()βStream.toList()(Java 16+) returns an unmodifiable list. If downstream code tries to add/remove elements it throwsUnsupportedOperationException. UseCollectors.toList()for a mutable result.Optional.get()without checking β throwsNoSuchElementException. Usemap,orElse, ororElseThrowinstead. Never call.get()on anOptional.- Integer overflow β
intmax is ~2.1 billion. Timestamps, database IDs, and arithmetic on large numbers should uselong. UseMath.addExact(),Math.multiplyExact()to get an exception instead of silent overflow. synchronizedon virtual threads (Java 21) βsynchronizedpins a virtual thread to a platform OS thread, destroying the scalability benefit. UseReentrantLockinside virtual thread code.- Failing to
awaitTerminationonExecutorServiceβ calling onlyshutdown()doesnβt wait for in-flight tasks to finish. Always pair withawaitTerminationor use try-with-resources on virtual thread executors. @Transactionalon private methods β Spring proxies work by subclassing; private methods bypass the proxy entirely, so@Transactionalon a private method is silently ignored.- Self-invocation breaks
@Transactionalβ calling an@Transactionalmethod from another method in the same class also bypasses the proxy. Extract to a separate bean, or injectselfreference.