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[] or IntStream for 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 + 3 becomes 5 at 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: public inside a non-exported package is inaccessible to other modules β€” even via reflection (unless opens)
  • Reliable configuration: missing modules are detected at startup, not when the missing class is first used at runtime
  • Smaller deploys: jlink creates 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).
  • HashMap is NOT thread-safe β€” concurrent reads+writes cause data corruption and infinite loops. Use ConcurrentHashMap instead.
  • final nuance β€” final on a variable means the reference cannot be reassigned; the object itself CAN still be mutated. final List<String> list β€” you cannot do list = new ArrayList<>() but list.add("x") is perfectly legal.
  • Memory leaks despite GC β€” GC collects unreachable objects. Leaks happen via: static collections, unclosed streams, ThreadLocal not removed, listeners never deregistered.
  • Checked vs unchecked β€” prefer RuntimeException for 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() vs Stream.toList() β€” Stream.toList() (Java 16+) returns an unmodifiable list. If downstream code tries to add/remove elements it throws UnsupportedOperationException. Use Collectors.toList() for a mutable result.
  • Optional.get() without checking β€” throws NoSuchElementException. Use map, orElse, or orElseThrow instead. Never call .get() on an Optional.
  • Integer overflow β€” int max is ~2.1 billion. Timestamps, database IDs, and arithmetic on large numbers should use long. Use Math.addExact(), Math.multiplyExact() to get an exception instead of silent overflow.
  • synchronized on virtual threads (Java 21) β€” synchronized pins a virtual thread to a platform OS thread, destroying the scalability benefit. Use ReentrantLock inside virtual thread code.
  • Failing to awaitTermination on ExecutorService β€” calling only shutdown() doesn’t wait for in-flight tasks to finish. Always pair with awaitTermination or use try-with-resources on virtual thread executors.
  • @Transactional on private methods β€” Spring proxies work by subclassing; private methods bypass the proxy entirely, so @Transactional on a private method is silently ignored.
  • Self-invocation breaks @Transactional β€” calling an @Transactional method from another method in the same class also bypasses the proxy. Extract to a separate bean, or inject self reference.