Java Deep Dive – Interactive Handbook
For intermediate developers who want more than tutorials. Built on JDK 21 LTS, this handbook teaches tradeoffs, internals, and production patterns—not just syntax.
01 Setup & Anatomy
Why JDK 21 LTS? It's the current long-term support release (supported until 2031), and it's where modern Java stabilized: virtual threads, record patterns, pattern matching for switch, and sequenced collections. If you're on 17, you can follow along—most features exist—but 21 removes preview flags and gives you the best performance.
Install via SDKMAN: sdk install java 21.0.2-tem or Homebrew: brew install openjdk@21. Verify with java -version.
Project structure: Use Maven/Gradle standard layout. It matters because tools expect it:
my-app/ ├─ src/main/java/com/example/ │ └─ App.java ├─ src/test/java/ └─ pom.xml
Compile: javac -d out src/main/java/com/example/*.java Run: java -cp out com.example.App
Main method evolution
Traditional (still required for public APIs): public static void main(String[] args). Java 21 introduced JEP 445 (preview) allowing simplified launch: void main() in an unnamed class, and instance main methods. Java 25 makes this stable. For learning, use the classic form—then drop ceremony in scripts.
public class HelloWorld {
public static void main(String[] args) {
System.out.println("Hello, Stephen! JDK " + Runtime.version());
}
}
module com.example.hello {
// no exports needed for simple app
requires java.base; // implicit
}
02 Core Syntax Deep Dive
Primitives vs Wrappers: Primitives (int, long, double etc) live on stack, no identity. Wrappers (Integer) are objects, nullable, live on heap. Autoboxing is convenient but in hot loops creates garbage. Integer cache covers -128 to 127—== works there by accident, fails outside. Always use .equals() for wrappers.
var: Local type inference (Java 10). The compiler infers from RHS. It's not dynamic—type is fixed at compile time. Use for generics noise: var map = new HashMap<String, List<User>>(); Don't use when type isn't obvious from initializer.
Text Blocks ("""): Java 15. Preserve formatting, strip incidental indent. Perfect for SQL, JSON, HTML. No more + "\n" concat.
Switch expressions: Java 14. Use arrow -> to avoid fall-through, yield a value, and be exhaustive with sealed types. Combined with pattern matching (Java 21), switch becomes a powerful type-safe dispatcher.
public class DataTypesDemo {
public static void main(String[] args) {
// primitives vs wrappers
int a = 127;
Integer b = 127;
Integer c = 127;
System.out.println(b == c); // true (cached)
Integer d = 128;
Integer e = 128;
System.out.println(d == e); // false! different objects
// var - type inferred as ArrayList
var names = new java.util.ArrayList();
names.add("Stephen");
// text block - perfect for JSON
String json = """
{
"name": "%s",
"jdk": 21,
"features": ["records","virtual-threads"]
}
""".formatted(names.get(0));
System.out.println(json);
}
}
public class SwitchPatternDemo {
static String describe(Object obj) {
// pattern matching for switch - Java 21
return switch (obj) {
case null -> "null";
case String s when s.isBlank() -> "empty string";
case String s -> "String length " + s.length();
case Integer i -> "int " + i;
case Long l -> "long " + l;
default -> "unknown " + obj.getClass().getSimpleName();
};
}
public static void main(String[] args) {
System.out.println(describe("Hi"));
System.out.println(describe(42));
System.out.println(describe(null));
}
}
03 Object-Oriented Mastery
Java is OOP-first but modernized. Encapsulation: private fields, public behavior. Inheritance: use sparingly—prefer composition. Polymorphism: dynamic dispatch on overridden methods.
Interfaces now have: default methods (evolution without breaking), static methods, private methods (Java 9). Use them for capability traits.
Records (Java 16): immutable data carriers. Compiler generates constructor, accessors, equals, hashCode, toString. Perfect for DTOs, keys, tuples. You can add validation in compact constructor.
Sealed classes (Java 17): sealed interface X permits A,B,C restricts who can implement. Enables exhaustive switch—compiler warns if you miss a case. Critical for domain modeling.
abstract class Shape {
abstract double area();
String name() { return getClass().getSimpleName(); }
}
final class Circle extends Shape {
private final double radius;
Circle(double radius) { this.radius = radius; }
@Override double area() { return Math.PI * radius * radius; }
}
final class Rectangle extends Shape {
private final double w, h;
Rectangle(double w, double h) { this.w = w; this.h = h; }
@Override double area() { return w * h; }
}
public record Point(int x, int y) {
// compact constructor for validation
public Point {
if (x < 0 || y < 0) throw new IllegalArgumentException("positive only");
}
// additional method
public double distanceFromOrigin() {
return Math.hypot(x, y);
}
}
public sealed interface Vehicle permits Car, Truck, Bike { }
final class Car implements Vehicle { int seats() { return 5; } }
final class Truck implements Vehicle { int payloadKg() { return 2000; } }
final class Bike implements Vehicle { }
class VehicleDemo {
static String inspect(Vehicle v) {
return switch(v) {
case Car c -> "Car with " + c.seats() + " seats";
case Truck t -> "Truck payload " + t.payloadKg();
case Bike b -> "Bike";
// exhaustive - no default needed
};
}
}
04 Exception Handling & Best Practices
Checked vs Unchecked: Checked (IOException) must be declared or caught—forces handling of recoverable conditions. Unchecked (RuntimeException) for programming bugs—don't catch broadly.
Try-with-resources: Anything implementing AutoCloseable is closed automatically, even on exception. Suppressed exceptions are attached—use e.getSuppressed() to debug.
Custom exceptions: Extend Exception for checked business errors, or RuntimeException for validation. Keep hierarchy shallow, include context.
import java.io.*;
import java.nio.file.*;
public class FileReaderSafe {
public static String readFirstLine(Path p) throws FileReadException {
// try-with-resources guarantees close
try (BufferedReader br = Files.newBufferedReader(p)) {
return br.readLine();
} catch (IOException e) {
throw new FileReadException("Cannot read " + p, e);
}
}
public static void main(String[] args) {
try {
System.out.println(readFirstLine(Path.of("README.md")));
} catch (FileReadException e) {
System.err.println(e.getMessage());
e.printStackTrace();
}
}
}
// custom checked exception
class FileReadException extends Exception {
FileReadException(String msg, Throwable cause) { super(msg, cause); }
}
05 Collections Framework In Depth
Lists: ArrayList backed by array—O(1) get, amortized O(1) add, O(n) insert middle. LinkedList—O(n) get, O(1) insert if you have iterator at position. In practice, ArrayList wins 99% of time due to cache locality.
Maps: HashMap O(1) average, buckets array length power of 2, hash spread via XOR. Load factor 0.75 triggers resize. When bucket >8 entries, treeifies to red-black tree (Java 8+) to prevent hash-DoS. LinkedHashMap maintains insertion order, TreeMap sorted O(log n).
Comparator vs Comparable: Comparable defines natural order (class User implements Comparable). Comparator is external strategy—prefer for multiple orderings.
import java.util.*;
public class CollectionsLab {
public static void main(String[] args) {
// ArrayList vs LinkedList microbenchmark concept
List array = new ArrayList<>();
List linked = new LinkedList<>();
for (int i = 0; i < 100_000; i++) {
array.add(i);
linked.add(i);
}
long t1 = System.nanoTime();
array.get(50_000);
long t2 = System.nanoTime();
linked.get(50_000);
long t3 = System.nanoTime();
System.out.println("ArrayList get: " + (t2-t1)/1000 + "µs");
System.out.println("LinkedList get: " + (t3-t2)/1000 + "µs");
// HashMap internals demo
Map counts = new HashMap<>(16, 0.75f);
counts.merge("java", 1, Integer::sum); // atomic merge
counts.merge("java", 1, Integer::sum);
System.out.println(counts); // {java=2}
// Comparator - sort by length then alphabetically
List words = new ArrayList<>(List.of("stream","lambda","var","record"));
words.sort(Comparator.comparingInt(String::length).thenComparing(Comparator.naturalOrder()));
System.out.println(words);
}
}
06 Lambdas, Streams, and Functional Java
Lambdas are syntactic sugar for functional interfaces. Method references (String::toUpperCase) improve readability.
Streams are lazy pipelines: intermediate ops (filter, map) build plan; terminal op (collect, forEach) triggers. Never mutate external state in streams—use collectors.
Collectors: groupingBy, partitioningBy, summarizingDouble. Parallel streams use ForkJoinPool.commonPool—only for CPU-bound, large data; otherwise overhead hurts.
import java.util.*;
import java.util.stream.*;
public class StreamAnalytics {
record Employee(String name, String dept, double salary) {}
public static void main(String[] args) {
List staff = List.of(
new Employee("Ada","Eng",120_000),
new Employee("Grace","Eng",135_000),
new Employee("Linus","Ops",95_000),
new Employee("Stephen","Eng",110_000),
new Employee("Margaret","HR",85_000)
);
// filter + map + collect
var highEarners = staff.stream()
.filter(e -> e.salary() > 100_000)
.map(Employee::name)
.toList(); // Java 16
// grouping and averaging
Map avgByDept = staff.stream()
.collect(Collectors.groupingBy(
Employee::dept,
Collectors.averagingDouble(Employee::salary)
));
// summarizing
DoubleSummaryStatistics stats = staff.stream()
.collect(Collectors.summarizingDouble(Employee::salary));
System.out.println("High earners: " + highEarners);
System.out.println("Avg by dept: " + avgByDept);
System.out.println("Salary stats: " + stats);
}
}
07 File I/O and NIO.2
Forget java.io.File. Use java.nio.file.Path and Files. For small files: Files.readString()/writeString() (Java 11). For large: Files.lines(path) returns lazy Stream—must close via try-with-resources.
Files.walk() traverses directories depth-first. Use with filter for finding files. Always handle IOException properly—it's checked for a reason.
import java.io.IOException;
import java.nio.file.*;
public class FileWalker {
public static void main(String[] args) throws IOException {
Path start = Path.of(".");
try (var stream = Files.walk(start, 3)) {
long javaFiles = stream
.filter(p -> p.toString().endsWith(".java"))
.peek(System.out::println)
.count();
System.out.println("Found " + javaFiles + " java files");
}
}
}
import java.nio.file.*;
import java.io.IOException;
public class CsvProcessor {
public static void main(String[] args) throws IOException {
Path csv = Path.of("data.csv");
// create sample
Files.writeString(csv, "id,name,score\n1,Ada,95\n2,Grace,98\n");
try (var lines = Files.lines(csv)) {
double avg = lines.skip(1) // header
.map(l -> l.split(","))
.mapToInt(arr -> Integer.parseInt(arr[2]))
.average()
.orElse(0);
System.out.println("Average score: " + avg);
}
}
}
08 Concurrency Essentials
Platform threads are OS threads—expensive (~1MB stack). Virtual threads (JDK 21) are JVM-managed, cheap (KBs). You can spawn millions.
Rule: For I/O-bound tasks (HTTP, DB), use virtual threads. For CPU-bound, use platform thread pool sized to cores.
Executors.newVirtualThreadPerTaskExecutor() creates a new virtual thread per task—no pooling needed. CompletableFuture composes async steps without blocking.
import java.net.URI;
import java.net.http.*;
import java.util.List;
import java.util.concurrent.*;
public class VirtualThreadWebFetcher {
public static void main(String[] args) throws Exception {
List urls = List.of(
"https://example.com",
"https://httpbin.org/delay/1",
"https://httpbin.org/get"
);
HttpClient client = HttpClient.newHttpClient();
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
List> futures = urls.stream()
.map(url -> CompletableFuture.supplyAsync(() -> {
try {
var req = HttpRequest.newBuilder(URI.create(url)).GET().build();
return client.send(req, HttpResponse.BodyHandlers.ofString()).statusCode() + " " + url;
} catch (Exception e) { return "ERR " + url; }
}, executor))
.toList();
futures.forEach(f -> System.out.println(f.join()));
}
}
}
09 Networking and Modern HTTP Client
java.net.http.HttpClient (Java 11) supports HTTP/1.1 and HTTP/2, sync and async, WebSocket. It's immutable and thread-safe—create one per app.
For JSON, JDK has no built-in parser. For production use Jackson or Gson, but for learning, parse minimally or treat as string. Pattern: build request, send, handle BodyHandler.
import java.net.URI;
import java.net.http.*;
import java.time.Duration;
public class ApiClient {
public static void main(String[] args) throws Exception {
HttpClient client = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(5))
.version(HttpClient.Version.HTTP_2)
.build();
HttpRequest req = HttpRequest.newBuilder()
.uri(URI.create("https://api.github.com/repos/openjdk/jdk"))
.header("User-Agent", "JavaDeepDive")
.GET()
.build();
HttpResponse res = client.send(req, HttpResponse.BodyHandlers.ofString());
System.out.println("Status: " + res.statusCode());
// crude JSON field extract
String body = res.body();
int idx = body.indexOf("\"stargazers_count\":");
System.out.println(body.substring(idx, idx+30));
}
}
10 Practical Projects
Five complete, runnable programs using only JDK 21. Copy, compile, run.
Project A: To-Do CLI with persistence
import java.nio.file.*;
import java.util.*;
public class TodoCLI {
static final Path FILE = Path.of("todos.txt");
record Todo(int id, String task, boolean done) {}
public static void main(String[] args) throws Exception {
if (args.length == 0) { System.out.println("Usage: add/list/done "); return; }
List lines = Files.exists(FILE) ? Files.readAllLines(FILE) : new ArrayList<>();
List todos = new ArrayList<>();
for (String l: lines) { var p=l.split("\\|",3); todos.add(new Todo(Integer.parseInt(p[0]), p[1], Boolean.parseBoolean(p[2]))); }
switch(args[0]) {
case "add" -> { int id = todos.stream().mapToInt(Todo::id).max().orElse(0)+1; todos.add(new Todo(id, String.join(" ", Arrays.copyOfRange(args,1,args.length)), false)); }
case "list" -> todos.forEach(t -> System.out.printf("%d [%s] %s%n", t.id, t.done?"x":" ", t.task));
case "done" -> { int id = Integer.parseInt(args[1]); todos.replaceAll(t -> t.id==id? new Todo(t.id,t.task,true):t); }
}
List out = todos.stream().map(t -> t.id+"|"+t.task+"|"+t.done).toList();
Files.write(FILE, out);
}
}
Project B: CSV Sales Analyzer
import java.nio.file.*;
import java.util.*;
import java.util.stream.*;
public class CsvSalesAnalyzer {
record Sale(String product, int qty, double price) { double total(){ return qty*price; } }
public static void main(String[] args) throws Exception {
Path csv = Path.of("sales.csv");
if (!Files.exists(csv)) Files.writeString(csv, "product,qty,price\nCoffee,3,4.5\nTea,5,2.0\nCoffee,2,4.5\n");
try (var lines = Files.lines(csv)) {
Map revenue = lines.skip(1)
.map(l -> l.split(","))
.map(a -> new Sale(a[0], Integer.parseInt(a[1]), Double.parseDouble(a[2])))
.collect(Collectors.groupingBy(Sale::product, Collectors.summingDouble(Sale::total)));
revenue.forEach((k,v) -> System.out.printf("%s: $%.2f%n", k, v));
}
}
}
Project C: Chat Server & Client (virtual threads)
import java.io.*;
import java.net.*;
import java.util.concurrent.*;
public class ChatServer {
static final ConcurrentHashMap clients = new ConcurrentHashMap<>();
public static void main(String[] args) throws IOException {
try (ServerSocket ss = new ServerSocket(8080)) {
System.out.println("Chat on 8080");
while (true) {
Socket s = ss.accept();
Thread.startVirtualThread(() -> handle(s));
}
}
}
static void handle(Socket s) {
try (s; var in = new BufferedReader(new InputStreamReader(s.getInputStream()));
var out = new PrintWriter(s.getOutputStream(), true)) {
clients.put(s, out);
out.println("Welcome Stephen!");
String line;
while ((line = in.readLine()) != null) {
for (var w : clients.values()) w.println(line);
}
} catch (IOException ignored) {} finally { clients.remove(s); }
}
}
import java.io.*;
import java.net.*;
public class ChatClient {
public static void main(String[] args) throws IOException {
try (Socket s = new Socket("localhost",8080);
var in = new BufferedReader(new InputStreamReader(s.getInputStream()));
var out = new PrintWriter(s.getOutputStream(), true);
var sysIn = new BufferedReader(new InputStreamReader(System.in))) {
Thread.startVirtualThread(() -> in.lines().forEach(System.out::println));
String line;
while ((line = sysIn.readLine()) != null) out.println(line);
}
}
}
Project D: Swing GUI Calculator
import javax.swing.*;
import java.awt.*;
public class SwingCalculator {
public static void main(String[] args) {
SwingUtilities.invokeLater(() -> {
JFrame f = new JFrame("Calc");
f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
JTextField display = new JTextField(); display.setEditable(false); display.setFont(new Font("Monospaced",Font.BOLD,24));
f.add(display, BorderLayout.NORTH);
JPanel p = new JPanel(new GridLayout(4,4,5,5));
String[] keys = {"7","8","9","/","4","5","6","*","1","2","3","-","0",".","=","+"};
final double[] acc = {0}; final String[] op = {""};
for (String k: keys) {
JButton b = new JButton(k);
b.addActionListener(e -> {
String t = e.getActionCommand();
if (t.matches("[0-9.]")) display.setText(display.getText()+t);
else if (t.equals("=")) { double v=Double.parseDouble(display.getText()); acc[0]=switch(op[0]){case "+"->acc[0]+v;case"-"->acc[0]-v;case"*"->acc[0]*v;case"/"->acc[0]/v;default->v;}; display.setText(String.valueOf(acc[0])); op[0]=""; }
else { acc[0]=Double.parseDouble(display.getText()); op[0]=t; display.setText(""); }
});
p.add(b);
}
f.add(p); f.setSize(300,400); f.setVisible(true);
});
}
}
Project E: Directory Watcher Auto-Backup
import java.nio.file.*;
import static java.nio.file.StandardWatchEventKinds.*;
public class DirectoryWatcher {
public static void main(String[] args) throws Exception {
Path dir = Path.of("watch"); Path backup = Path.of("backup");
Files.createDirectories(dir); Files.createDirectories(backup);
WatchService ws = FileSystems.getDefault().newWatchService();
dir.register(ws, ENTRY_CREATE, ENTRY_MODIFY);
System.out.println("Watching " + dir.toAbsolutePath());
while (true) {
WatchKey key = ws.take();
for (WatchEvent> ev : key.pollEvents()) {
Path src = dir.resolve((Path)ev.context());
Path dst = backup.resolve(src.getFileName() + ".bak");
Files.copy(src, dst, StandardCopyOption.REPLACE_EXISTING);
System.out.println("Backed up: " + src);
}
key.reset();
}
}
}
11 Interactive Playground
Quiz: Test Your Depth
Concept Mapper
Click a term to highlight related code examples in the handbook.
Simulated Code Runner
These run in-browser with precomputed JDK 21 output—edit safely, then hit Run.