Intermediate~20 min read

Java

A comprehensive guide to modern Java (21 LTS) covering OOP, records, sealed classes, generics, collections, streams, and concurrency including virtual threads.

OOPGenericsCollectionsConcurrency

Java is a statically typed, object-oriented, class-based language that runs on the Java Virtual Machine (JVM). Code compiles to platform-independent bytecode, giving Java its famous "write once, run anywhere" portability. This guide targets Java 21, the current Long-Term Support (LTS) release, and covers modern features like records, sealed classes, switch expressions, and virtual threads.

Class and main Structure

Every Java program lives inside a class. Execution starts at the main method. A source file typically holds one public top-level class matching the file name.

public class Hello {
    public static void main(String[] args) {
        System.out.println("Hello, world!");
    }
}

Compile with javac Hello.java and run with java Hello. Since Java 11 you can run a single file directly: java Hello.java.

Primitives vs Objects

Java has eight primitive types stored by value, and everything else is an object (a reference). Each primitive has a wrapper class (e.g. int ↔ Integer) used by generics and collections. Autoboxing converts between them automatically.

Primitive Size Wrapper
byte, short, int, long8–64 bit integersByte, Short, Integer, Long
float, double32/64 bit floatsFloat, Double
char16 bit UnicodeCharacter
booleantrue/falseBoolean

Variables and var

Variables are declared with an explicit type or, for local variables, with var (Java 10+) for local type inference. Use final to make a binding immutable.

int count = 42;
final double PI = 3.14159;
var name = "Ada";               // inferred as String
var list = new ArrayList<String>(); // inferred as ArrayList<String>
long big = 1_000_000L;          // underscores for readability

Operators

Java supports arithmetic (+ - * / %), comparison (== != < > <= >=), logical (&& || !), and bitwise (& | ^ ~ << >> >>>) operators. Use == to compare references and .equals() to compare object contents.

Gotcha

For Strings and other objects, == tests identity (same object), not value. Always use .equals() to compare values.

Control Flow

Java has the usual if/else, for, enhanced for-each, while, and do/while loops.

for (int i = 0; i < 5; i++) System.out.println(i);
for (String s : List.of("a", "b", "c")) System.out.println(s);
while (count > 0) count--;

Switch Expressions and Patterns

Modern switch expressions (Java 14+) use arrow syntax, return values, and require no break. Java 21 finalized pattern matching for switch, allowing type patterns and guards.

String day = switch (num) {
    case 1, 7 -> "Weekend";
    case 2, 3, 4, 5, 6 -> "Weekday";
    default -> "Unknown";
};

// Pattern matching (Java 21)
String describe(Object obj) {
    return switch (obj) {
        case Integer i when i > 100 -> "big int";
        case Integer i -> "int " + i;
        case String s -> "string of length " + s.length();
        case null -> "null";
        default -> "other";
    };
}

Methods

Methods declare a return type, name, and parameters. Overloading allows multiple methods with the same name but different parameter lists. Varargs (...) accept a variable number of arguments.

int add(int a, int b) { return a + b; }
double add(double a, double b) { return a + b; }   // overload

int sum(int... nums) {                              // varargs
    int total = 0;
    for (int n : nums) total += n;
    return total;
}
sum(1, 2, 3, 4);   // -> 10

Object-Oriented Programming

Java's core paradigm is OOP. A class bundles state (fields) and behavior (methods). Constructors initialize new instances. Encapsulation hides internal state behind access modifiers and getters/setters.

public class Account {
    private double balance;          // encapsulated field

    public Account(double initial) { // constructor
        this.balance = initial;
    }

    public double getBalance() { return balance; }
    public void deposit(double amt) { balance += amt; }
}

Access Modifiers

Modifier Visibility
publicEverywhere
protectedSame package + subclasses
(default)Same package only
privateSame class only

Inheritance, Interfaces, Polymorphism

A class extends one superclass and implements any number of interfaces. Abstract classes cannot be instantiated and may declare abstract methods. Interfaces define contracts and can include default methods. Polymorphism lets a subtype be used wherever the supertype is expected.

abstract class Shape {
    abstract double area();          // must be overridden
}

interface Drawable {
    void draw();
    default void render() { draw(); } // default method
}

class Circle extends Shape implements Drawable {
    private final double r;
    Circle(double r) { this.r = r; }

    @Override double area() { return Math.PI * r * r; }
    @Override public void draw() { System.out.println("circle"); }
}

Shape s = new Circle(2.0);   // polymorphism
System.out.println(s.area());

Records (Java 16+)

Records are immutable data carriers. The compiler generates the constructor, accessors, equals, hashCode, and toString.

public record Point(int x, int y) {
    // compact constructor for validation
    public Point {
        if (x < 0 || y < 0) throw new IllegalArgumentException();
    }
}

var p = new Point(3, 4);
System.out.println(p.x() + ", " + p.y());

Sealed Classes (Java 17+)

Sealed types restrict which classes may extend or implement them, enabling exhaustive pattern matching.

public sealed interface Expr permits Num, Add {}
public record Num(double value) implements Expr {}
public record Add(Expr left, Expr right) implements Expr {}

double eval(Expr e) {
    return switch (e) {
        case Num n -> n.value();
        case Add a -> eval(a.left()) + eval(a.right());
    }; // no default needed — compiler knows it's exhaustive
}

Enums

Enums define a fixed set of constants and can carry fields and methods.

enum Planet {
    EARTH(5.97e24), MARS(6.42e23);

    private final double mass;
    Planet(double mass) { this.mass = mass; }
    double mass() { return mass; }
}
Planet.EARTH.mass();

Generics

Generics provide compile-time type safety for classes and methods. Type parameters like <T> are placeholders filled in at use.

// Generic class
class Box<T> {
    private final T value;
    Box(T value) { this.value = value; }
    T get() { return value; }
}
Box<String> b = new Box<>("hi");

// Generic method
static <T> T firstOf(List<T> list) { return list.get(0); }

Bounded Wildcards

Wildcards express flexible bounds. Remember PECS: Producer extends, Consumer super.

// reads (produces) from the list -> ? extends
double sumAll(List<? extends Number> nums) {
    double total = 0;
    for (Number n : nums) total += n.doubleValue();
    return total;
}

// writes (consumes) into the list -> ? super
void addInts(List<? super Integer> list) {
    list.add(1);
    list.add(2);
}

// bounded type parameter
<T extends Comparable<? super T>> T max(List<T> list) {
    return list.stream().max(Comparator.naturalOrder()).orElseThrow();
}

Collections Framework

The java.util collections framework provides interfaces (List, Set, Map, Deque) and implementations.

Type Common Impl Use Case
List<E>ArrayList, LinkedListOrdered, indexed, duplicates
Set<E>HashSet, TreeSetUnique elements
Map<K, V>HashMap, TreeMapKey-value lookup
Deque<E>ArrayDequeStack / queue
List<String> names = new ArrayList<>();
names.add("Ada");

Set<Integer> ids = new HashSet<>(List.of(1, 2, 2, 3)); // {1,2,3}

Map<String, Integer> ages = new HashMap<>();
ages.put("Ada", 36);
ages.getOrDefault("Bob", 0);
ages.computeIfAbsent("Cara", k -> 0);

Deque<Integer> stack = new ArrayDeque<>();
stack.push(1); stack.pop();

Streams API

The Streams API enables declarative, functional-style processing of collections with operations like map, filter, reduce, and collect.

List<Integer> nums = List.of(1, 2, 3, 4, 5, 6);

List<Integer> evensDoubled = nums.stream()
    .filter(n -> n % 2 == 0)
    .map(n -> n * 2)
    .collect(Collectors.toList());   // [4, 8, 12]

int total = nums.stream().reduce(0, Integer::sum);   // 21

Map<Boolean, List<Integer>> parts = nums.stream()
    .collect(Collectors.partitioningBy(n -> n % 2 == 0));

Optional

Optional<T> is a container that may or may not hold a value, replacing null checks with an explicit, composable API.

Optional<String> found = names.stream()
    .filter(n -> n.startsWith("A"))
    .findFirst();

String result = found
    .map(String::toUpperCase)
    .orElse("NONE");

found.ifPresent(System.out::println);

Exceptions

Java distinguishes checked exceptions (must be declared or handled) from unchecked exceptions (runtime errors). Use try/catch/finally, and prefer try-with-resources for anything implementing AutoCloseable.

Aspect Checked Unchecked
Base typeExceptionRuntimeException
Must declare/catchYesNo
ExamplesIOException, SQLExceptionNullPointer, IllegalArgument
try (var reader = Files.newBufferedReader(path)) {
    return reader.readLine();
} catch (IOException e) {
    System.err.println("Read failed: " + e.getMessage());
    throw new RuntimeException(e);
} finally {
    System.out.println("done");
}

Concurrency

Java offers rich concurrency primitives. Prefer high-level abstractions like ExecutorService and CompletableFuture over raw threads.

// Runnable + Thread
Runnable task = () -> System.out.println("running");
new Thread(task).start();

// ExecutorService (thread pool)
try (var pool = Executors.newFixedThreadPool(4)) {
    Future<Integer> f = pool.submit(() -> 40 + 2);
    System.out.println(f.get());
}

// CompletableFuture (async composition)
CompletableFuture.supplyAsync(() -> fetch())
    .thenApply(String::trim)
    .thenAccept(System.out::println);

Use synchronized to guard critical sections, volatile for visibility of a single field, and concurrent collections like ConcurrentHashMap for thread-safe shared state.

Virtual Threads (Java 21)

Virtual threads are lightweight threads managed by the JVM, enabling millions of concurrent tasks with blocking-style code. They shine for I/O-bound workloads.

try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
    for (int i = 0; i < 10_000; i++) {
        executor.submit(() -> {
            Thread.sleep(Duration.ofSeconds(1));
            return fetchData();
        });
    }
} // waits for all tasks

// Or start one directly
Thread.startVirtualThread(() -> System.out.println("light!"));

Why virtual threads

Platform threads map 1:1 to OS threads and are expensive (~1MB each). Virtual threads are cheap, so you can write simple blocking code without a thread-pool bottleneck.

Standard Library Highlights

Useful classes ship with the JDK: String and StringBuilder, Math, java.time (LocalDate, Duration), java.nio.file (Files, Path), java.util.regex, and java.util.stream.

// Text blocks (Java 15+)
String json = """
    {
      "name": "Ada"
    }
    """;

LocalDate today = LocalDate.now();
LocalDate next = today.plusDays(7);
String greeting = "Hello, %s! It is %d.".formatted("Ada", 2026);

Tooling

The core toolchain: javac compiles sources, java runs them, and jshell is an interactive REPL. Real projects use build tools like Maven (pom.xml) or Gradle (build.gradle) for dependencies and packaging, and JUnit for testing.

javac Hello.java        # compile
java Hello              # run
jshell                  # interactive REPL
mvn clean package       # Maven build
gradle build            # Gradle build

Practice Exercises

  1. Define a record Temperature(double celsius) with a compact constructor that rejects values below -273.15, plus a method returning Fahrenheit.
  2. Write a generic method <T> List<T> reversed(List<T> in) that returns a new reversed list without mutating the input.
  3. Use the Streams API to group a List<String> of words into a Map<Integer, List<String>> keyed by word length.
  4. Model a sealed interface Shape with Circle and Rectangle records, then compute area with an exhaustive switch.
  5. Read a text file with try-with-resources and count how many lines contain a given keyword, handling IOException gracefully.
  6. Submit 1,000 sleep-and-return tasks to a virtual-thread executor and collect all results, timing how long the whole batch takes.

Section navigation