← Tech Guides

01

Quick Reference

The essential Java cheat sheet. Data types, operators, control flow, and common patterns all in one place.

Primitive Data Types

Type Size Range Default Example
byte 8-bit -128 to 127 0 byte b = 100;
short 16-bit -32,768 to 32,767 0 short s = 10000;
int 32-bit -231 to 231-1 0 int i = 42;
long 64-bit -263 to 263-1 0L long l = 100000L;
float 32-bit IEEE 754 single-precision 0.0f float f = 3.14f;
double 64-bit IEEE 754 double-precision 0.0d double d = 3.14159;
boolean 1-bit* true or false false boolean ok = true;
char 16-bit '\u0000' to '\uffff' '\u0000' char c = 'A';

Variable Declaration

// Explicit type declaration
int count = 10;
String name = "Java";
double[] prices = {9.99, 19.99, 29.99};

// Type inference (Java 10+)
var message = "Hello, World!";     // inferred as String
var numbers = List.of(1, 2, 3);    // inferred as List<Integer>
var map = new HashMap<String, Integer>();  // inferred type

// Constants
final int MAX_SIZE = 100;
final String APP_NAME = "CoffeeApp";
static final double PI = 3.14159265358979;

Operator Reference

Category Operators Example
Arithmetic + - * / % int r = 10 % 3; // 1
Assignment = += -= *= /= %= x += 5; // x = x + 5
Comparison == != < > <= >= if (a >= b) { ... }
Logical && || ! if (a && !b) { ... }
Bitwise & | ^ ~ << >> >>> int mask = flags & 0xFF;
Ternary ? : String s = x > 0 ? "pos" : "neg";
instanceof instanceof if (obj instanceof String s) { ... }
Increment ++ -- i++; --j;

Control Flow Quick Reference

if / else if / else

if (score >= 90) {
    grade = "A";
} else if (score >= 80) {
    grade = "B";
} else {
    grade = "C";
}

switch (classic)

switch (day) {
    case "MON":
    case "TUE":
        System.out.println("Early week");
        break;
    case "FRI":
        System.out.println("TGIF!");
        break;
    default:
        System.out.println("Other day");
}

for loop

for (int i = 0; i < 10; i++) {
    System.out.println(i);
}

enhanced for-each

String[] names = {"Ada", "Bob"};
for (String name : names) {
    System.out.println(name);
}

while / do-while

while (running) {
    process();
}

do {
    retry();
} while (!success);

try / catch / finally

try {
    readFile("data.txt");
} catch (IOException e) {
    log.error("Read failed", e);
} finally {
    cleanup();
}

Common String Methods

Method Returns Example
length() int "Java".length() // 4
charAt(int) char "Java".charAt(0) // 'J'
substring(int, int) String "Hello".substring(1, 4) // "ell"
indexOf(String) int "coffee".indexOf("fee") // 3
contains(CharSequence) boolean "latte".contains("att") // true
replace(old, new) String "aab".replace("a", "b") // "bbb"
split(String regex) String[] "a,b,c".split(",") // ["a","b","c"]
trim() String " hi ".trim() // "hi"
toLowerCase() String "JAVA".toLowerCase() // "java"
toUpperCase() String "java".toUpperCase() // "JAVA"
equals(Object) boolean "abc".equals("abc") // true
format(String, ...) String String.format("Hi %s", name)

Array Syntax

// Declaration and initialization
int[] numbers = new int[5];              // default zeros
int[] primes = {2, 3, 5, 7, 11};        // literal initialization
String[] names = new String[]{"Ada", "Bob", "Cal"};

// Multidimensional arrays
int[][] matrix = {
    {1, 2, 3},
    {4, 5, 6},
    {7, 8, 9}
};

// Access and length
int first = primes[0];         // 2
int len = primes.length;       // 5
matrix[1][2] = 99;             // set row 1, col 2

// Common operations (java.util.Arrays)
Arrays.sort(numbers);
Arrays.fill(numbers, 0);
int[] copy = Arrays.copyOf(primes, primes.length);
boolean eq = Arrays.equals(primes, copy);
String repr = Arrays.toString(primes);  // "[2, 3, 5, 7, 11]"

Common Patterns

Main Method

Entry point for every Java application

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

Class Declaration

Fields, constructor, and methods

public class Coffee {
    private String roast;
    private int strength;

    public Coffee(String roast, int strength) {
        this.roast = roast;
        this.strength = strength;
    }

    public String getRoast() {
        return roast;
    }
}

Interface

Contract with default methods

public interface Brewable {
    void brew();
    int getStrength();

    default String describe() {
        return "Strength: " + getStrength();
    }
}

Enum

Type-safe constants with behavior

public enum Roast {
    LIGHT("Blonde"),
    MEDIUM("House"),
    DARK("Espresso");

    private final String label;
    Roast(String label) {
        this.label = label;
    }
    public String getLabel() {
        return label;
    }
}

02

Setup & Installation

Getting your Java development environment ready. From choosing a JDK to writing your first program.

JDK LTS Versions

Long-Term Support releases are the backbone of production Java. Choose the newest LTS your project supports.

Version Released Status Key Features
Java 8 Legacy Mar 2014 Extended support Lambdas, Streams API, Optional, Date/Time API, default methods
Java 11 LTS Sep 2018 Active support HTTP Client, var in lambdas, String methods, single-file execution
Java 17 LTS Sep 2021 Active support Sealed classes, pattern matching, records, text blocks, switch expressions
Java 21 Latest LTS Sep 2023 Active support Virtual threads, record patterns, sequenced collections, string templates (preview)

Recommendation

For new projects, use Java 21. For existing projects, Java 17 is the safest migration target. Avoid starting new work on Java 8 unless legacy constraints demand it.

Installation Methods

SDKMAN! (Recommended)

Manages multiple JDK versions side by side on Linux and macOS

# Install SDKMAN
curl -s "https://get.sdkman.io" | bash
source "$HOME/.sdkman/bin/sdkman-init.sh"

# List available JDKs
sdk list java

# Install a specific version
sdk install java 21.0.2-tem

# Switch between versions
sdk use java 17.0.10-tem
sdk default java 21.0.2-tem

Package Manager

Quick install via system package managers

# Ubuntu / Debian
sudo apt update
sudo apt install openjdk-21-jdk

# macOS (Homebrew)
brew install openjdk@21

# Fedora / RHEL
sudo dnf install java-21-openjdk-devel

# Windows (winget)
winget install EclipseAdoptium.Temurin.21.JDK

Manual Install

Download from Adoptium, Oracle, or Amazon Corretto

# Download from adoptium.net
# Extract to /opt or /usr/local
tar -xzf OpenJDK21U-jdk_x64_linux.tar.gz
sudo mv jdk-21.0.2+13 /opt/java-21

# Set environment variables
export JAVA_HOME=/opt/java-21
export PATH=$JAVA_HOME/bin:$PATH

JAVA_HOME & PATH Configuration

# Check current Java version
java -version
javac -version

# Find where Java is installed
which java
readlink -f $(which java)

# Set JAVA_HOME permanently (add to ~/.bashrc or ~/.zshrc)
export JAVA_HOME=/usr/lib/jvm/java-21-openjdk-amd64
export PATH=$JAVA_HOME/bin:$PATH

# Verify setup
echo $JAVA_HOME
java -version

On macOS, use /usr/libexec/java_home -V to list all installed JVMs and /usr/libexec/java_home -v 21 to get the path for a specific version.

IDE Comparison

Feature IntelliJ IDEA Eclipse VS Code
Best for Professional Java development Enterprise / legacy projects Lightweight, polyglot work
Cost Free (Community) / Paid (Ultimate) Free Free
Refactoring Excellent Very good Good (via extensions)
Debugger Best-in-class Very good Good
Build tools Maven, Gradle built-in Maven, Gradle via plugins Maven, Gradle via extensions
Startup speed Slow Moderate Fast
Key extensions Built-in plugins Marketplace Extension Pack for Java (Red Hat)

First Java Program

Create, compile, and run your first Java application.

// File: HelloWorld.java
public class HelloWorld {
    public static void main(String[] args) {
        System.out.println("Hello, World!");
        System.out.println("Java version: " + System.getProperty("java.version"));
    }
}
# Compile to bytecode
javac HelloWorld.java

# Run the compiled class
java HelloWorld
# Output: Hello, World!
# Output: Java version: 21.0.2

# Single-file execution (Java 11+) — no separate compile step
java HelloWorld.java

Single-File Execution

Since Java 11, you can run java MyFile.java directly without a separate javac step. The file is compiled in memory and executed. Great for quick scripts and learning.

JShell REPL

Interactive Java shell for quick experimentation, available since Java 9.

# Start JShell
jshell

# Inside JShell:
jshell> int x = 42
x ==> 42

jshell> String greeting = "Hello, " + "Java!"
greeting ==> "Hello, Java!"

jshell> List.of(1, 2, 3).stream().map(n -> n * n).toList()
$3 ==> [1, 4, 9]

jshell> /methods     // list declared methods
jshell> /vars        // list declared variables
jshell> /imports     // list active imports
jshell> /exit        // quit

Project Structure Conventions

my-project/
├── src/
│   ├── main/
│   │   ├── java/                 # Application source
│   │   │   └── com/
│   │   │       └── example/
│   │   │           ├── App.java
│   │   │           ├── model/
│   │   │           ├── service/
│   │   │           └── util/
│   │   └── resources/            # Config files, templates
│   │       └── application.properties
│   └── test/
│       ├── java/                 # Test source (mirrors main)
│       │   └── com/
│       │       └── example/
│       │           └── AppTest.java
│       └── resources/            # Test-specific resources
├── pom.xml                       # Maven build file
├── build.gradle                  # OR Gradle build file
├── .gitignore
└── README.md

The src/main/java and src/test/java layout is the Maven Standard Directory Layout. Both Maven and Gradle use it by default, and all major IDEs expect it.

03

Language Fundamentals

The building blocks of Java. Types, variables, operators, control flow, strings, and arrays in depth.

Primitive Types vs Reference Types

Java has two categories of types. Primitives hold values directly on the stack; reference types hold a pointer to an object on the heap.

Primitive Types

Stored directly in memory. Fixed size. No methods. Cannot be null. Eight types: byte, short, int, long, float, double, boolean, char.

int count = 42;       // value on stack
double pi = 3.14;     // value on stack
boolean ok = true;    // value on stack

// Primitives are copied by value
int a = 10;
int b = a;  // b is independent copy
b = 20;     // a is still 10

Reference Types

Stored on the heap. Variable holds a reference (pointer). Can be null. Classes, interfaces, arrays, enums, records.

String name = "Java";     // ref to heap
int[] nums = {1, 2, 3};  // ref to heap
Coffee c = new Coffee();  // ref to heap

// References are copied by value
int[] x = {1, 2, 3};
int[] y = x;  // y points to same array
y[0] = 99;    // x[0] is also 99!
Primitive Wrapper Class Autoboxing Example
int Integer Integer n = 42; // autoboxed
double Double Double d = 3.14; // autoboxed
boolean Boolean Boolean b = true; // autoboxed
char Character Character c = 'A'; // autoboxed
long Long Long l = 100L; // autoboxed

Autoboxing Pitfall

Never compare wrapper types with ==. Use .equals() instead. The == operator compares references, not values. Integer a = 200; Integer b = 200; a == b is false because values above 127 are not cached.

Variables and Constants

// Local variables (must be initialized before use)
int count = 0;
String name;         // OK to declare without init
// System.out.println(name);  // COMPILE ERROR - not initialized

// var keyword (Java 10+) - local variable type inference
var message = "Hello";            // String
var numbers = new ArrayList<Integer>();  // ArrayList<Integer>
var stream = list.stream();       // Stream<T>

// var restrictions: cannot use with...
// var x;                  // no initializer
// var y = null;           // cannot infer type
// var z = {1, 2, 3};     // array initializer
// var as field type       // only local variables
// var as parameter type   // only local variables

// Constants with final
final int MAX_RETRIES = 3;
final String DB_URL = "jdbc:postgresql://localhost/mydb";

// Static constants (class-level)
public static final int BUFFER_SIZE = 8192;
public static final String VERSION = "2.1.0";

Type Casting

Widening (implicit) conversions are safe. Narrowing (explicit) conversions can lose data and require a cast.

// Widening (implicit) - no data loss
// byte -> short -> int -> long -> float -> double
int i = 42;
long l = i;           // implicit widening
double d = l;         // implicit widening
float f = i;          // implicit widening

// Narrowing (explicit) - potential data loss, requires cast
double pi = 3.14159;
int truncated = (int) pi;        // 3 (decimal lost)
byte small = (byte) 300;         // 44 (overflow!)
short s = (short) 100_000;       // -31072 (overflow!)

// Reference type casting
Object obj = "Hello";
String str = (String) obj;       // downcast (may throw ClassCastException)

// Safe casting with instanceof (Java 16+ pattern matching)
if (obj instanceof String s) {
    System.out.println(s.toUpperCase());  // s is already cast
}

// Numeric parsing (String to number)
int n = Integer.parseInt("42");
double d2 = Double.parseDouble("3.14");
long l2 = Long.parseLong("1000000");

// Number to String
String s1 = String.valueOf(42);
String s2 = Integer.toString(42);
String s3 = "" + 42;  // concatenation (less efficient)

Use underscore separators in numeric literals for readability: 1_000_000, 0xFF_EC_DE_5E, 3.141_592_653. The compiler ignores them.

Operators in Depth

Arithmetic & Assignment

int a = 10, b = 3;

a + b    // 13  addition
a - b    // 7   subtraction
a * b    // 30  multiplication
a / b    // 3   integer division
a % b    // 1   modulo (remainder)

a += 5;  // a = a + 5  (15)
a -= 3;  // a = a - 3  (12)
a *= 2;  // a = a * 2  (24)
a /= 4;  // a = a / 4  (6)
a %= 4;  // a = a % 4  (2)

Logical & Ternary

boolean x = true, y = false;

x && y   // false (AND, short-circuit)
x || y   // true  (OR, short-circuit)
!x       // false (NOT)

// Short-circuit: second operand not
// evaluated if result is determined
obj != null && obj.isValid()  // safe

// Ternary operator
String msg = age >= 18
    ? "adult"
    : "minor";

// Nested ternary (avoid for readability)
String grade = score >= 90 ? "A"
    : score >= 80 ? "B" : "C";

Bitwise Operations

int a = 0b1010, b = 0b1100;

a & b    // 0b1000 (AND)
a | b    // 0b1110 (OR)
a ^ b    // 0b0110 (XOR)
~a       // inverts all bits (NOT)

a << 2   // 0b101000 (left shift)
a >> 1   // 0b0101   (signed right)
a >>> 1  // 0b0101   (unsigned right)

// Common uses
int flags = READ | WRITE;        // set
boolean canRead = (flags & READ) != 0; // test
flags &= ~WRITE;                 // clear

instanceof (Pattern Matching)

// Traditional instanceof
if (shape instanceof Circle) {
    Circle c = (Circle) shape;
    System.out.println(c.radius());
}

// Pattern matching (Java 16+)
if (shape instanceof Circle c) {
    System.out.println(c.radius());
    // c is already cast and in scope
}

// Guarded pattern
if (shape instanceof Circle c
        && c.radius() > 5) {
    System.out.println("Big circle");
}

// In switch (Java 21+)
String desc = switch (shape) {
    case Circle c   -> "r=" + c.radius();
    case Square s   -> "side=" + s.side();
    default         -> "unknown";
};

Control Flow

if / else

// Standard if-else
if (temperature > 95) {
    System.out.println("Too hot for coffee");
} else if (temperature > 65) {
    System.out.println("Iced coffee weather");
} else {
    System.out.println("Hot coffee time!");
}

// Null-safe checks
if (name != null && !name.isEmpty()) {
    greet(name);
}

// Java 14+ helpful NullPointerExceptions
// Shows exactly which variable was null in chain:
// a.b.c.d  ->  "Cannot invoke ... because 'a.b' is null"

Switch Expressions (Java 14+)

// Classic switch (statement)
switch (day) {
    case MONDAY:
    case FRIDAY:
        System.out.println("Busy day");
        break;
    case SATURDAY:
    case SUNDAY:
        System.out.println("Weekend!");
        break;
    default:
        System.out.println("Midweek");
}

// Modern switch expression with arrow syntax (Java 14+)
String type = switch (day) {
    case MONDAY, FRIDAY     -> "Busy day";
    case SATURDAY, SUNDAY   -> "Weekend!";
    default                 -> "Midweek";
};

// Switch with blocks and yield
int numLetters = switch (day) {
    case MONDAY, FRIDAY, SUNDAY -> 6;
    case TUESDAY                -> 7;
    default -> {
        String name = day.toString();
        yield name.length();
    }
};

Loops

// Standard for loop
for (int i = 0; i < 10; i++) {
    System.out.println("Iteration: " + i);
}

// Enhanced for-each (arrays and Iterables)
List<String> coffees = List.of("Espresso", "Latte", "Mocha");
for (String coffee : coffees) {
    System.out.println(coffee);
}

// while loop
int retries = 0;
while (retries < 3 && !connected) {
    connect();
    retries++;
}

// do-while (executes at least once)
Scanner scanner = new Scanner(System.in);
String input;
do {
    System.out.print("Enter command (quit to exit): ");
    input = scanner.nextLine();
    process(input);
} while (!"quit".equals(input));

// Loop control: break and continue
for (int i = 0; i < 100; i++) {
    if (i % 2 == 0) continue;  // skip even numbers
    if (i > 50) break;          // stop at 50
    System.out.println(i);
}

// Labeled break (nested loops)
outer:
for (int i = 0; i < 10; i++) {
    for (int j = 0; j < 10; j++) {
        if (i * j > 50) break outer;
        System.out.println(i + " * " + j + " = " + (i * j));
    }
}

Strings

Strings are immutable in Java. Every modification creates a new String object. Use StringBuilder for frequent concatenation.

// String creation
String s1 = "Hello";               // string literal (interned)
String s2 = new String("Hello");   // new object on heap
String s3 = "Hello";               // same reference as s1 (interned)

// IMPORTANT: String comparison
s1 == s3          // true  (same interned reference)
s1 == s2          // false (different objects!)
s1.equals(s2)     // true  (ALWAYS use .equals() for content comparison)
s1.equalsIgnoreCase("hello")  // true

// String concatenation
String full = "Hello" + " " + "World";     // compiler optimizes
String msg = "Count: " + 42;                // auto-converts int

// String.format and formatted (Java 15+)
String formatted = String.format("Name: %s, Age: %d", name, age);
String formatted2 = "Name: %s, Age: %d".formatted(name, age);  // Java 15+

// StringBuilder (mutable, for loops and concatenation)
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 1000; i++) {
    sb.append("item ").append(i).append("\n");
}
String result = sb.toString();

// StringBuffer (thread-safe StringBuilder, slower)
StringBuffer threadSafe = new StringBuffer();
threadSafe.append("synchronized").append(" append");

Text Blocks (Java 15+)

// Multi-line strings with text blocks
String json = """
        {
            "name": "Java",
            "version": 21,
            "features": [
                "virtual threads",
                "pattern matching"
            ]
        }
        """;

String html = """
        <html>
            <body>
                <p>Hello, %s!</p>
            </body>
        </html>
        """.formatted(name);

String sql = """
        SELECT u.name, u.email
        FROM users u
        WHERE u.active = true
          AND u.created_at > ?
        ORDER BY u.name
        """;

Common String Operations

String s = "  Hello, Java World!  ";

s.trim()                    // "Hello, Java World!"
s.strip()                   // "Hello, Java World!" (Unicode-aware, Java 11+)
s.stripLeading()            // "Hello, Java World!  "
s.stripTrailing()           // "  Hello, Java World!"
s.isBlank()                 // false (Java 11+, checks whitespace-only too)
s.isEmpty()                 // false

"hello".repeat(3)           // "hellohellohello" (Java 11+)
"a\nb\nc".lines().toList()  // ["a", "b", "c"] (Java 11+)

// Checking and searching
"Hello".startsWith("He")    // true
"Hello".endsWith("lo")      // true
"Hello".matches("[A-Za-z]+") // true (regex match)

// Joining strings
String csv = String.join(", ", "a", "b", "c");  // "a, b, c"
String joined = List.of("x", "y").stream()
    .collect(Collectors.joining(", "));           // "x, y"

Arrays

Fixed-size, zero-indexed containers. For dynamic sizing, use ArrayList instead.

// Single-dimensional arrays
int[] nums = new int[5];                  // [0, 0, 0, 0, 0]
int[] primes = {2, 3, 5, 7, 11};         // literal init
String[] names = new String[]{"A", "B"}; // explicit init

// Multi-dimensional arrays
int[][] grid = new int[3][4];            // 3 rows, 4 cols
int[][] jagged = {                       // ragged array
    {1, 2},
    {3, 4, 5},
    {6}
};

// Access
int first = primes[0];          // 2
int last = primes[primes.length - 1];  // 11
grid[0][1] = 42;

// Iteration
for (int i = 0; i < primes.length; i++) {
    System.out.println(primes[i]);
}
for (int p : primes) {
    System.out.println(p);
}

// java.util.Arrays utility class
import java.util.Arrays;

Arrays.sort(nums);                      // in-place sort
Arrays.sort(names, Comparator.reverseOrder());  // reverse sort
int idx = Arrays.binarySearch(primes, 7);       // 3 (must be sorted)
int[] copy = Arrays.copyOf(primes, 10);         // pad with zeros
int[] range = Arrays.copyOfRange(primes, 1, 4); // [3, 5, 7]
boolean eq = Arrays.equals(primes, copy);       // false
Arrays.fill(nums, -1);                          // all elements = -1
String str = Arrays.toString(primes);           // "[2, 3, 5, 7, 11]"
String deep = Arrays.deepToString(grid);        // nested arrays

// Convert array to List
List<String> list = Arrays.asList(names);          // fixed-size list
List<String> mutable = new ArrayList<>(Arrays.asList(names));

// Convert List to array
String[] arr = list.toArray(new String[0]);

Comments and Javadoc

// Single-line comment

/*
 * Multi-line comment.
 * Used for longer explanations.
 */

/**
 * Javadoc comment - generates API documentation.
 *
 * <p>Describes the purpose and behavior of a class,
 * method, or field. Supports HTML tags.</p>
 *
 * @param roast   the type of roast (LIGHT, MEDIUM, DARK)
 * @param ounces  the size of the cup in fluid ounces
 * @return a freshly brewed Coffee instance
 * @throws IllegalArgumentException if ounces is negative
 * @since 2.0
 * @see Roast
 * @author CoffeeTeam
 */
public Coffee brew(Roast roast, int ounces) {
    if (ounces < 0) {
        throw new IllegalArgumentException("ounces must be positive");
    }
    return new Coffee(roast, ounces);
}
# Generate Javadoc HTML from source
javadoc -d docs/ -sourcepath src/main/java com.example

# Common Javadoc tags:
# @param name    - describes a parameter
# @return        - describes the return value
# @throws type   - documents an exception
# @see reference - cross-reference to another element
# @since version - when this API was introduced
# @deprecated    - marks as deprecated with explanation
# {@code text}   - inline code formatting
# {@link Class}  - inline hyperlink to another class

Javadoc Best Practice

Write Javadoc for all public classes and methods. Focus on what the method does and why, not how. Document parameters, return values, exceptions, and edge cases. Use {@code} for inline code and {@link} for cross-references.

04

Object-Oriented Programming

Classes, inheritance, interfaces, polymorphism, and composition. The pillars of Java's design philosophy.

Classes & Objects

A class is a blueprint; an object is an instance of that blueprint. Every Java program is built from classes.

public class Coffee {
    // Fields (instance variables)
    private String roast;
    private int strengthMl;
    private boolean decaf;

    // Static field (shared across all instances)
    private static int totalBrewCount = 0;

    // Static initializer block (runs once when class loads)
    static {
        System.out.println("Coffee class loaded");
    }

    // Instance initializer block (runs before every constructor)
    {
        this.decaf = false;  // default for all constructors
    }

    // Default constructor
    public Coffee() {
        this("Medium", 250);  // delegates to parameterized constructor
    }

    // Parameterized constructor
    public Coffee(String roast, int strengthMl) {
        this.roast = roast;
        this.strengthMl = strengthMl;
        totalBrewCount++;
    }

    // Copy constructor
    public Coffee(Coffee other) {
        this.roast = other.roast;
        this.strengthMl = other.strengthMl;
        this.decaf = other.decaf;
        totalBrewCount++;
    }

    // Instance method
    public String describe() {
        return roast + " roast, " + strengthMl + "ml";
    }

    // Static method
    public static int getTotalBrewCount() {
        return totalBrewCount;
    }

    // The 'this' keyword refers to the current instance
    public Coffee withRoast(String roast) {
        this.roast = roast;   // distinguishes field from parameter
        return this;          // enables method chaining
    }
}
// Creating objects
Coffee espresso = new Coffee("Dark", 60);
Coffee latte = new Coffee();              // uses default constructor
Coffee copy = new Coffee(espresso);       // uses copy constructor

// Method chaining with 'this'
Coffee custom = new Coffee().withRoast("Light");

// Static access (no instance needed)
int count = Coffee.getTotalBrewCount();

Access Modifiers

Control visibility of classes, fields, methods, and constructors.

Modifier Class Package Subclass World
public
protected
default (no modifier)
private

Rule of Thumb

Start with the most restrictive access (private) and only widen when necessary. Fields should almost always be private. Use protected sparingly for extension points.

Encapsulation

Hide internal state and expose controlled access through getters and setters.

// Traditional encapsulation with getters/setters
public class BankAccount {
    private String owner;
    private double balance;

    public BankAccount(String owner, double initialBalance) {
        this.owner = owner;
        this.balance = initialBalance;
    }

    // Getter
    public double getBalance() {
        return balance;
    }

    // Setter with validation
    public void deposit(double amount) {
        if (amount <= 0) {
            throw new IllegalArgumentException("Amount must be positive");
        }
        this.balance += amount;
    }

    // No setBalance() - balance can only change through deposit/withdraw
}

// Immutable class (no setters, all fields final)
public final class Money {
    private final String currency;
    private final long cents;

    public Money(String currency, long cents) {
        this.currency = currency;
        this.cents = cents;
    }

    public String getCurrency() { return currency; }
    public long getCents() { return cents; }

    // Returns a NEW instance instead of mutating
    public Money add(Money other) {
        if (!this.currency.equals(other.currency)) {
            throw new IllegalArgumentException("Currency mismatch");
        }
        return new Money(this.currency, this.cents + other.cents);
    }
}
// Record types (Java 16+) — immutable data carriers with less boilerplate
// Automatically generates: constructor, getters, equals, hashCode, toString
public record Point(double x, double y) {
    // Compact constructor for validation
    public Point {
        if (Double.isNaN(x) || Double.isNaN(y)) {
            throw new IllegalArgumentException("Coordinates must not be NaN");
        }
    }

    // Custom method
    public double distanceTo(Point other) {
        return Math.sqrt(Math.pow(this.x - other.x, 2)
                       + Math.pow(this.y - other.y, 2));
    }
}

// Usage
Point p = new Point(3.0, 4.0);
double x = p.x();          // accessor (not getX!)
System.out.println(p);     // Point[x=3.0, y=4.0]

Inheritance

Java supports single inheritance via extends. A subclass inherits all non-private members of its parent.

// Superclass
public class Beverage {
    protected String name;
    protected int temperatureC;

    public Beverage(String name, int temperatureC) {
        this.name = name;
        this.temperatureC = temperatureC;
    }

    public String describe() {
        return name + " at " + temperatureC + "C";
    }
}

// Subclass
public class Coffee extends Beverage {
    private String roast;

    public Coffee(String name, int temperatureC, String roast) {
        super(name, temperatureC);  // must be first statement
        this.roast = roast;
    }

    // Method overriding
    @Override
    public String describe() {
        return super.describe() + " (" + roast + " roast)";
    }
}

// Constructor chaining
public class Espresso extends Coffee {
    public Espresso() {
        super("Espresso", 93, "Dark");  // chains to Coffee -> Beverage
    }
}

final Classes

Cannot be extended (subclassed)

// No class can extend String
public final class String { ... }

// Your own final class
public final class Constants {
    public static final int MAX = 100;
}

final Methods

Cannot be overridden in subclasses

public class Payment {
    // Subclasses cannot change this logic
    public final double calculateTax(
            double amount) {
        return amount * 0.08;
    }
}

Interfaces

Interfaces define contracts. A class can implement multiple interfaces, enabling a form of multiple inheritance for behavior.

// Interface declaration
public interface Brewable {
    // Abstract method (no body) - must be implemented
    void brew();
    int getStrength();

    // Default method (Java 8+) - provides implementation
    default String describe() {
        return "Brewable drink, strength: " + getStrength();
    }

    // Static method (Java 8+) - called on the interface itself
    static Brewable strongest(Brewable a, Brewable b) {
        return a.getStrength() >= b.getStrength() ? a : b;
    }
}

// Implementing an interface
public class Tea implements Brewable {
    private int steepMinutes;

    @Override
    public void brew() {
        System.out.println("Steeping for " + steepMinutes + " min");
    }

    @Override
    public int getStrength() {
        return steepMinutes * 2;
    }
    // describe() is inherited from default method
}

// Implementing multiple interfaces
public class Chai extends Tea implements Serializable, Comparable<Chai> {
    @Override
    public int compareTo(Chai other) {
        return Integer.compare(this.getStrength(), other.getStrength());
    }
}

Functional Interfaces & Lambdas

// A functional interface has exactly ONE abstract method
@FunctionalInterface
public interface CoffeeFilter {
    boolean test(Coffee coffee);

    // Default and static methods don't count
    default CoffeeFilter and(CoffeeFilter other) {
        return c -> this.test(c) && other.test(c);
    }
}

// Lambda expression replaces anonymous inner class
CoffeeFilter isStrong = coffee -> coffee.getStrength() > 8;
CoffeeFilter isDark = coffee -> coffee.getRoast().equals("Dark");

// Combine functional interfaces
CoffeeFilter strongAndDark = isStrong.and(isDark);

// Built-in functional interfaces (java.util.function)
Predicate<String>   notEmpty = s -> !s.isEmpty();
Function<String, Integer> len = String::length;   // method reference
Consumer<String>    printer = System.out::println;
Supplier<String>    uuid = () -> UUID.randomUUID().toString();
BiFunction<Integer, Integer, Integer> add = (a, b) -> a + b;

Abstract Classes vs Interfaces

Feature Abstract Class Interface
Inheritance Single (extends one class) Multiple (implements many)
Constructors Yes No
Instance fields Yes (any access modifier) No (only public static final)
Method bodies Abstract and concrete methods Abstract, default, and static
Access modifiers Any Public only (methods)
When to use Shared state/code among related classes Contract for unrelated classes
// Abstract class - shares state and partial implementation
public abstract class HotDrink {
    protected int temperatureC;

    public HotDrink(int temperatureC) {
        this.temperatureC = temperatureC;
    }

    // Concrete method (shared behavior)
    public boolean isTooHot() {
        return temperatureC > 80;
    }

    // Abstract method (subclass must implement)
    public abstract String getIngredients();
}

// Interface - defines capability, no state
public interface Sweetened {
    void addSugar(int spoons);
    int getSugarSpoons();
}

Polymorphism

Method Overloading (Compile-time)

Same name, different parameter lists within the same class

public class Barista {
    // Overloaded methods
    public Coffee make() {
        return new Coffee("Medium", 250);
    }
    public Coffee make(String roast) {
        return new Coffee(roast, 250);
    }
    public Coffee make(String roast, int ml) {
        return new Coffee(roast, ml);
    }
}

Method Overriding (Runtime)

Subclass provides specific implementation of a superclass method

Beverage drink = new Coffee(
    "Espresso", 93, "Dark");

// Calls Coffee.describe(), not
// Beverage.describe()
// Resolved at runtime based on
// actual object type
System.out.println(drink.describe());

// Covariant return type
class Animal {
    Animal create() { return new Animal(); }
}
class Dog extends Animal {
    @Override
    Dog create() { return new Dog(); } // OK
}

Composition vs Inheritance

Favor composition over inheritance. Inheritance creates tight coupling between parent and child. Composition gives you flexible, reusable components that are easier to test and change independently.

Inheritance Approach

Tightly coupled. Changes to CoffeeMachine break subclass.

// Fragile: locked into a hierarchy
class CoffeeMachine {
    void grindBeans() { ... }
    void heatWater() { ... }
    void brew() {
        grindBeans();
        heatWater();
    }
}

class FancyMachine extends CoffeeMachine {
    void frothMilk() { ... }
    @Override
    void brew() {
        super.brew();
        frothMilk();
    }
}

Composition Approach

Loosely coupled. Swap components independently.

// Flexible: compose from parts
class Grinder { void grind() { ... } }
class Heater { void heat() { ... } }
class Frother { void froth() { ... } }

class FancyMachine {
    private final Grinder grinder;
    private final Heater heater;
    private final Frother frother;

    FancyMachine(Grinder g, Heater h,
                 Frother f) {
        this.grinder = g;
        this.heater = h;
        this.frother = f;
    }
    void brew() {
        grinder.grind();
        heater.heat();
        frother.froth();
    }
}

The final Keyword

🔒

final Variables

Can be assigned exactly once. For primitives, the value cannot change. For objects, the reference cannot change (but the object's contents can).

final int x = 10;
// x = 20;  // COMPILE ERROR

final List<String> list = new ArrayList<>();
list.add("ok");   // contents can change
// list = new ArrayList<>();  // ERROR
🛡

final Methods

Cannot be overridden by subclasses. Use when a method's behavior must remain consistent across the hierarchy.

public class Security {
    public final boolean validate(
            String token) {
        // This logic cannot be changed
        // by any subclass
        return token != null
            && token.length() > 20;
    }
}
🛏

final Classes

Cannot be extended. Use for immutable value types, security-critical classes, and utility classes.

// String, Integer, etc. are final
public final class ApiKey {
    private final String value;
    public ApiKey(String value) {
        this.value = value;
    }
    public String getValue() {
        return value;
    }
}

Enums

Type-safe constants that can carry fields, methods, and constructors. Much more powerful than integer constants.

// Basic enum
public enum Season { SPRING, SUMMER, AUTUMN, WINTER }

// Enum with fields, constructor, and methods
public enum Roast {
    LIGHT("Blonde", 196),
    MEDIUM("House", 210),
    MEDIUM_DARK("Full City", 225),
    DARK("Espresso", 240);

    private final String label;
    private final int tempCelsius;

    // Constructor is implicitly private
    Roast(String label, int tempCelsius) {
        this.label = label;
        this.tempCelsius = tempCelsius;
    }

    public String getLabel() { return label; }
    public int getTempCelsius() { return tempCelsius; }

    // Custom method
    public boolean isHighTemp() {
        return tempCelsius > 220;
    }
}

// Enum with abstract method
public enum Operation {
    ADD  { public double apply(double a, double b) { return a + b; } },
    SUB  { public double apply(double a, double b) { return a - b; } },
    MUL  { public double apply(double a, double b) { return a * b; } },
    DIV  { public double apply(double a, double b) { return a / b; } };

    public abstract double apply(double a, double b);
}
// Using enums
Roast r = Roast.DARK;
String name = r.name();            // "DARK"
int ordinal = r.ordinal();        // 3
Roast parsed = Roast.valueOf("LIGHT");  // Roast.LIGHT
Roast[] all = Roast.values();     // iterate all constants

// Switch on enum
switch (r) {
    case LIGHT  -> System.out.println("Mild");
    case DARK   -> System.out.println("Bold");
    default     -> System.out.println("Balanced");
}

// EnumSet and EnumMap (highly efficient for enum keys)
EnumSet<Roast> lightRoasts = EnumSet.of(Roast.LIGHT, Roast.MEDIUM);
EnumSet<Roast> allRoasts = EnumSet.allOf(Roast.class);

EnumMap<Roast, String> descriptions = new EnumMap<>(Roast.class);
descriptions.put(Roast.DARK, "Rich and smoky");
descriptions.put(Roast.LIGHT, "Bright and fruity");

05

Collections & Generics

Java's collection framework, generic types, the Streams API, and Optional. The toolkit for working with groups of objects.

Collection Hierarchy

The Java Collections Framework provides a unified architecture for storing and manipulating groups of objects.

📋

List

Ordered, indexed, allows duplicates. Implementations: ArrayList, LinkedList, CopyOnWriteArrayList.

🎲

Set

No duplicates. Implementations: HashSet (unordered), TreeSet (sorted), LinkedHashSet (insertion order).

📜

Queue / Deque

FIFO or priority ordering. Implementations: PriorityQueue, ArrayDeque, LinkedList.

🗝

Map (separate hierarchy)

Key-value pairs. Not part of Collection interface. Implementations: HashMap, TreeMap, LinkedHashMap, ConcurrentHashMap.

Immutable Collections (Java 9+)

Use factory methods for unmodifiable collections: List.of(), Set.of(), Map.of(). These throw UnsupportedOperationException on any mutating operation.

List Implementations

Feature ArrayList LinkedList
Backing structure Resizable array Doubly-linked list
get(index) O(1) O(n)
add(element) O(1) amortized O(1)
add(index, element) O(n) O(1)*
remove(index) O(n) O(1)*
Memory Compact (array) Higher (node overhead)
Best for Random access, iteration Frequent insert/remove at ends
// ArrayList (most common, default choice)
List<String> coffees = new ArrayList<>();
coffees.add("Espresso");
coffees.add("Latte");
coffees.add(0, "Americano");       // insert at index
String first = coffees.get(0);     // "Americano"
coffees.remove("Latte");           // remove by value
coffees.set(0, "Cappuccino");      // replace at index
int size = coffees.size();         // 1

// Immutable list (Java 9+)
List<String> fixed = List.of("Espresso", "Latte", "Mocha");
// fixed.add("Tea");  // throws UnsupportedOperationException

// Useful operations
Collections.sort(coffees);
Collections.shuffle(coffees);
Collections.reverse(coffees);
int idx = Collections.binarySearch(coffees, "Latte");  // must be sorted
List<String> synced = Collections.synchronizedList(coffees);
List<String> unmod = Collections.unmodifiableList(coffees);

Set Implementations

Feature HashSet TreeSet LinkedHashSet
Order None Sorted (natural/comparator) Insertion order
add/remove/contains O(1) O(log n) O(1)
Null elements One allowed No One allowed
Backed by HashMap TreeMap (Red-Black tree) LinkedHashMap
// HashSet (most common, fastest)
Set<String> tags = new HashSet<>();
tags.add("java");
tags.add("coffee");
tags.add("java");            // duplicate ignored
boolean has = tags.contains("java");  // true

// TreeSet (sorted)
Set<Integer> sorted = new TreeSet<>();
sorted.addAll(List.of(5, 1, 9, 3));
System.out.println(sorted);  // [1, 3, 5, 9]

// LinkedHashSet (maintains insertion order)
Set<String> ordered = new LinkedHashSet<>();
ordered.add("first");
ordered.add("second");
ordered.add("third");
// Iteration order: first, second, third

// Immutable set (Java 9+)
Set<String> immutable = Set.of("a", "b", "c");

// Set operations
Set<Integer> a = new HashSet<>(Set.of(1, 2, 3, 4));
Set<Integer> b = Set.of(3, 4, 5, 6);
a.retainAll(b);  // intersection: {3, 4}
a.addAll(b);     // union
a.removeAll(b);  // difference

Map Implementations

Feature HashMap TreeMap LinkedHashMap ConcurrentHashMap
Order None Key sorted Insertion order None
get/put O(1) O(log n) O(1) O(1)
Null keys One allowed No One allowed No
Thread-safe No No No Yes
// HashMap (default choice for key-value storage)
Map<String, Integer> prices = new HashMap<>();
prices.put("Espresso", 350);
prices.put("Latte", 500);
prices.put("Mocha", 550);

int price = prices.get("Latte");           // 500
int safe = prices.getOrDefault("Tea", 0);  // 0 (key not found)
prices.putIfAbsent("Espresso", 999);       // no change (key exists)

// Iteration
for (Map.Entry<String, Integer> entry : prices.entrySet()) {
    System.out.println(entry.getKey() + ": " + entry.getValue());
}
prices.forEach((k, v) -> System.out.println(k + ": " + v));

// Compute methods
prices.compute("Latte", (k, v) -> v + 50);       // Latte: 550
prices.computeIfAbsent("Tea", k -> 300);          // adds Tea: 300
prices.merge("Espresso", 100, Integer::sum);       // Espresso: 450

// Immutable map (Java 9+)
Map<String, Integer> fixed = Map.of("a", 1, "b", 2, "c", 3);

// Map.ofEntries for more than 10 entries
Map<String, Integer> big = Map.ofEntries(
    Map.entry("Espresso", 350),
    Map.entry("Latte", 500),
    Map.entry("Mocha", 550)
);

Queue & Deque

PriorityQueue

Elements ordered by natural ordering or a Comparator. Not FIFO.

// Min-heap by default
Queue<Integer> minHeap = new PriorityQueue<>();
minHeap.offer(5);
minHeap.offer(1);
minHeap.offer(3);
minHeap.poll();  // 1 (smallest)
minHeap.peek();  // 3

// Max-heap
Queue<Integer> maxHeap = new PriorityQueue<>(
    Comparator.reverseOrder()
);

ArrayDeque

Double-ended queue. Use as stack or queue. Faster than Stack and LinkedList.

// As a Stack (LIFO)
Deque<String> stack = new ArrayDeque<>();
stack.push("first");
stack.push("second");
stack.pop();   // "second"
stack.peek();  // "first"

// As a Queue (FIFO)
Deque<String> queue = new ArrayDeque<>();
queue.offer("first");
queue.offer("second");
queue.poll();  // "first"
queue.peek();  // "second"

Generics

Enable type-safe collections and reusable classes/methods without casting. Checked at compile time, erased at runtime.

// Generic class
public class Box<T> {
    private T content;

    public void put(T item) { this.content = item; }
    public T get() { return content; }
}

Box<String> stringBox = new Box<>();
stringBox.put("Coffee");
String s = stringBox.get();  // no cast needed

// Generic method
public static <T> T firstOrNull(List<T> list) {
    return list.isEmpty() ? null : list.get(0);
}

String first = firstOrNull(List.of("a", "b"));  // type inferred

// Bounded type parameter
public static <T extends Comparable<T>> T max(T a, T b) {
    return a.compareTo(b) >= 0 ? a : b;
}

// Multiple bounds
public static <T extends Serializable & Comparable<T>> void process(T item) {
    // T must implement BOTH Serializable and Comparable
}

Wildcards

// Upper bounded wildcard: ? extends T
// "Anything that IS-A Number" - read-only (producer)
public static double sum(List<? extends Number> numbers) {
    double total = 0;
    for (Number n : numbers) {
        total += n.doubleValue();
    }
    return total;
}
sum(List.of(1, 2, 3));          // List<Integer> works
sum(List.of(1.5, 2.5));         // List<Double> works

// Lower bounded wildcard: ? super T
// "Anything that is a supertype of Integer" - write-only (consumer)
public static void addNumbers(List<? super Integer> list) {
    list.add(1);
    list.add(2);
}
List<Number> nums = new ArrayList<>();
addNumbers(nums);   // works: Number is supertype of Integer

// Unbounded wildcard: ?
// "Anything" - useful for methods that don't care about type
public static void printAll(List<?> list) {
    for (Object item : list) {
        System.out.println(item);
    }
}

Type Erasure

Generics are a compile-time feature. At runtime, List<String> and List<Integer> are both just List. You cannot use instanceof with generic types or create generic arrays directly.

Streams API

Declarative, functional-style operations on collections. Lazy evaluation with intermediate and terminal operations.

// Creating streams
Stream<String> fromList = List.of("a", "b", "c").stream();
Stream<String> fromArray = Arrays.stream(new String[]{"x", "y"});
Stream<Integer> fromOf = Stream.of(1, 2, 3);
Stream<Integer> generated = Stream.iterate(0, n -> n + 2).limit(5);
IntStream range = IntStream.rangeClosed(1, 10);  // 1 to 10

Intermediate Operations (lazy, return a Stream)

List<String> coffees = List.of(
    "Espresso", "Latte", "Mocha", "Espresso", "Cappuccino", "Latte"
);

coffees.stream()
    .filter(c -> c.length() > 5)          // keep matching elements
    .map(String::toUpperCase)               // transform each element
    .distinct()                             // remove duplicates
    .sorted()                               // natural order sort
    .peek(System.out::println)              // debug: see elements pass through
    .limit(3)                               // take first 3
    .skip(1)                                // skip first 1
    .collect(Collectors.toList());          // terminal: collect results

// flatMap: flatten nested structures
List<List<String>> nested = List.of(
    List.of("a", "b"), List.of("c", "d")
);
List<String> flat = nested.stream()
    .flatMap(Collection::stream)            // flatten to single stream
    .collect(Collectors.toList());          // ["a", "b", "c", "d"]

Terminal Operations (eager, produce a result)

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

// collect - gather results into a collection
List<Integer> evens = nums.stream()
    .filter(n -> n % 2 == 0)
    .collect(Collectors.toList());

// forEach - perform action on each element
nums.stream().forEach(System.out::println);

// reduce - combine elements into a single value
int sum = nums.stream().reduce(0, Integer::sum);          // 31
Optional<Integer> max = nums.stream().reduce(Integer::max); // 9

// count, min, max
long count = nums.stream().filter(n -> n > 3).count();   // 3

// findFirst, findAny
Optional<Integer> first = nums.stream()
    .filter(n -> n > 5).findFirst();                      // Optional[9]

// anyMatch, allMatch, noneMatch
boolean hasNeg = nums.stream().anyMatch(n -> n < 0);     // false
boolean allPos = nums.stream().allMatch(n -> n > 0);     // true

Collectors

List<String> names = List.of("Alice", "Bob", "Charlie", "Alice", "Dave");

// toList, toSet, toMap
List<String> list = names.stream().collect(Collectors.toList());
Set<String> set = names.stream().collect(Collectors.toSet());
Map<String, Integer> nameLen = names.stream().distinct()
    .collect(Collectors.toMap(n -> n, String::length));

// joining
String joined = names.stream()
    .collect(Collectors.joining(", ", "[", "]"));  // [Alice, Bob, ...]

// groupingBy
Map<Integer, List<String>> byLength = names.stream()
    .collect(Collectors.groupingBy(String::length));
// {3=[Bob], 4=[Dave], 5=[Alice, Alice], 7=[Charlie]}

// partitioningBy
Map<Boolean, List<String>> parts = names.stream()
    .collect(Collectors.partitioningBy(n -> n.length() > 4));

// counting, summarizing
Map<Integer, Long> countByLen = names.stream()
    .collect(Collectors.groupingBy(String::length, Collectors.counting()));

Use .parallelStream() or .parallel() for CPU-intensive operations on large datasets. Avoid parallel streams for I/O-bound work or small collections where threading overhead outweighs the benefit.

Optional

A container that may or may not contain a value. Eliminates null checks and makes "absence" explicit in the API.

// Creating Optional
Optional<String> present = Optional.of("Coffee");      // must not be null
Optional<String> nullable = Optional.ofNullable(null);  // may be null
Optional<String> empty = Optional.empty();               // always empty

// Checking and extracting values
present.isPresent()            // true
present.isEmpty()              // false (Java 11+)
present.get()                  // "Coffee" (throws if empty!)

// Safe extraction
present.orElse("Tea")                          // "Coffee"
empty.orElse("Tea")                            // "Tea"
empty.orElseGet(() -> fetchDefault())          // lazy evaluation
empty.orElseThrow()                            // NoSuchElementException
empty.orElseThrow(() -> new CustomException()) // custom exception

// Conditional actions
present.ifPresent(System.out::println);        // prints "Coffee"
present.ifPresentOrElse(                       // Java 9+
    System.out::println,
    () -> System.out.println("Nothing")
);

// Transformations (functional chaining)
Optional<String> upper = present.map(String::toUpperCase);  // Optional["COFFEE"]
Optional<String> filtered = present.filter(s -> s.length() > 3);

// flatMap for nested Optionals
Optional<Optional<String>> nested = Optional.of(Optional.of("inner"));
Optional<String> flat = nested.flatMap(o -> o);  // Optional["inner"]

Optional Best Practices

Use Optional as a return type for methods that may not have a result. Do not use it for fields, method parameters, or collections. Never call .get() without checking .isPresent() first — prefer .orElse(), .map(), or .ifPresent() instead.

06

Exception Handling & I/O

Graceful error handling, checked and unchecked exceptions, and file I/O with both classic java.io and modern java.nio.

Exception Hierarchy

All exceptions descend from Throwable. Understanding the hierarchy determines how and when you must handle errors.

🚨

Error

Serious JVM problems you should NOT catch. OutOfMemoryError, StackOverflowError, etc.

Exception (checked)

Must be caught or declared with throws. IOException, SQLException, ClassNotFoundException.

💥

RuntimeException (unchecked)

Not required to catch. Programming errors. NullPointerException, IllegalArgumentException, IndexOutOfBoundsException.

Try / Catch / Finally

// Basic try-catch-finally
try {
    FileReader reader = new FileReader("config.properties");
    // read file...
} catch (FileNotFoundException e) {
    System.err.println("File not found: " + e.getMessage());
} catch (IOException e) {
    System.err.println("IO error: " + e.getMessage());
} finally {
    // ALWAYS executes (cleanup code)
    System.out.println("Cleanup complete");
}

// Multi-catch (Java 7+) — single block for multiple exception types
try {
    processData(input);
} catch (NumberFormatException | IllegalArgumentException e) {
    System.err.println("Bad input: " + e.getMessage());
}

// Try-with-resources (Java 7+) — auto-closes AutoCloseable resources
try (FileReader reader = new FileReader("data.txt");
     BufferedReader br = new BufferedReader(reader)) {
    String line;
    while ((line = br.readLine()) != null) {
        System.out.println(line);
    }
}  // reader and br are automatically closed here, even on exception

// Try-with-resources with existing variable (Java 9+)
FileReader reader = new FileReader("data.txt");
try (reader) {
    // use reader...
}  // auto-closed

Checked vs Unchecked Exceptions

Aspect Checked Exceptions Unchecked Exceptions
Extends Exception RuntimeException
Compiler enforces Yes (must catch or declare) No
Common examples IOException, SQLException, ClassNotFoundException, InterruptedException NullPointerException, IllegalArgumentException, IndexOutOfBoundsException, ClassCastException
When to use Recoverable conditions (file not found, network timeout) Programming errors (null access, bad argument)

Custom Exceptions

// Custom checked exception
public class BrewException extends Exception {
    private final String beanType;

    public BrewException(String message, String beanType) {
        super(message);
        this.beanType = beanType;
    }

    public BrewException(String message, Throwable cause) {
        super(message, cause);  // wrap the original exception
        this.beanType = "unknown";
    }

    public String getBeanType() { return beanType; }
}

// Custom unchecked exception
public class InvalidOrderException extends RuntimeException {
    public InvalidOrderException(String message) {
        super(message);
    }
}

// Usage
public Coffee brew(String bean) throws BrewException {
    if (bean == null) {
        throw new InvalidOrderException("Bean type required");  // unchecked
    }
    try {
        return grindAndBrew(bean);
    } catch (MachineException e) {
        throw new BrewException("Brewing failed for " + bean, e);  // checked, wraps cause
    }
}

throw vs throws

throw — throws an exception

Used inside a method body to actually throw an exception object.

public void setAge(int age) {
    if (age < 0) {
        throw new IllegalArgumentException(
            "Age cannot be negative: " + age
        );
    }
    this.age = age;
}

throws — declares exceptions

Used in a method signature to declare which checked exceptions may be thrown.

// Caller must handle or propagate
public String readFile(String path)
        throws IOException,
               FileNotFoundException {
    return Files.readString(Path.of(path));
}

// Calling code
try {
    String data = readFile("config.txt");
} catch (IOException e) {
    log.error("Read failed", e);
}

File I/O with java.io

The classic I/O library. Stream-based, blocking. Still widely used for byte/character streams.

// Reading text with BufferedReader
try (BufferedReader br = new BufferedReader(new FileReader("data.txt"))) {
    String line;
    while ((line = br.readLine()) != null) {
        System.out.println(line);
    }
}

// Writing text with BufferedWriter
try (BufferedWriter bw = new BufferedWriter(new FileWriter("output.txt"))) {
    bw.write("Line 1");
    bw.newLine();
    bw.write("Line 2");
}

// Appending to a file
try (FileWriter fw = new FileWriter("log.txt", true)) {  // true = append
    fw.write("New log entry\n");
}

// Reading/writing bytes
try (FileInputStream fis = new FileInputStream("image.png");
     FileOutputStream fos = new FileOutputStream("copy.png")) {
    byte[] buffer = new byte[8192];
    int bytesRead;
    while ((bytesRead = fis.read(buffer)) != -1) {
        fos.write(buffer, 0, bytesRead);
    }
}

// Object serialization (java.io)
try (ObjectOutputStream oos = new ObjectOutputStream(
        new FileOutputStream("data.ser"))) {
    oos.writeObject(myObject);
}

try (ObjectInputStream ois = new ObjectInputStream(
        new FileInputStream("data.ser"))) {
    MyClass obj = (MyClass) ois.readObject();
}

File I/O with java.nio

The modern I/O library (Java 7+). Path-based, with powerful utility methods in the Files class. Preferred for new code.

import java.nio.file.*;

// Path creation
Path path = Path.of("src", "main", "data.txt");    // Java 11+
Path path2 = Paths.get("/home/user/docs/file.txt"); // Java 7+
Path home = Path.of(System.getProperty("user.home"));

// Reading files (simple convenience methods)
String content = Files.readString(Path.of("config.txt"));           // Java 11+
List<String> lines = Files.readAllLines(Path.of("data.csv"));      // Java 7+
byte[] bytes = Files.readAllBytes(Path.of("image.png"));            // Java 7+

// Writing files
Files.writeString(Path.of("output.txt"), "Hello, NIO!");            // Java 11+
Files.write(Path.of("lines.txt"), List.of("Line 1", "Line 2"));    // Java 7+
Files.write(Path.of("log.txt"), "append\n".getBytes(),
    StandardOpenOption.APPEND, StandardOpenOption.CREATE);           // append

// Listing and walking directories
try (Stream<Path> entries = Files.list(Path.of("."))) {
    entries.filter(Files::isRegularFile)
           .forEach(System.out::println);
}

try (Stream<Path> tree = Files.walk(Path.of("src"), 3)) {  // max depth 3
    tree.filter(p -> p.toString().endsWith(".java"))
        .forEach(System.out::println);
}

// File operations
Files.copy(source, target, StandardCopyOption.REPLACE_EXISTING);
Files.move(source, target, StandardCopyOption.ATOMIC_MOVE);
Files.delete(path);                  // throws if not exists
Files.deleteIfExists(path);          // returns boolean

// File attributes
boolean exists = Files.exists(path);
boolean isDir = Files.isDirectory(path);
long size = Files.size(path);
FileTime modified = Files.getLastModifiedTime(path);

// Creating directories and temp files
Files.createDirectories(Path.of("a/b/c"));   // creates all parents
Path temp = Files.createTempFile("prefix-", ".tmp");
Path tempDir = Files.createTempDirectory("myapp-");
Feature java.io java.nio
File representation File Path
Read/write Streams (byte/char) Files utility class
Directory listing File.listFiles() (eager, returns array) Files.list() (lazy, returns Stream)
Symbolic links Limited support Full support
File watching Not supported WatchService
Recommended Legacy code only All new code

Serialization

Converting objects to byte streams for storage or transmission. Implement Serializable to opt in.

import java.io.*;

public class UserProfile implements Serializable {
    // Unique ID for version control of serialized class
    private static final long serialVersionUID = 1L;

    private String username;
    private String email;
    private transient String password;  // transient = excluded from serialization

    public UserProfile(String username, String email, String password) {
        this.username = username;
        this.email = email;
        this.password = password;
    }

    @Override
    public String toString() {
        return username + " (" + email + ")";
    }
}

// Serialize (write object to file)
UserProfile user = new UserProfile("duke", "duke@java.com", "secret123");
try (ObjectOutputStream oos = new ObjectOutputStream(
        new FileOutputStream("user.ser"))) {
    oos.writeObject(user);
}

// Deserialize (read object from file)
try (ObjectInputStream ois = new ObjectInputStream(
        new FileInputStream("user.ser"))) {
    UserProfile loaded = (UserProfile) ois.readObject();
    System.out.println(loaded);           // duke (duke@java.com)
    System.out.println(loaded.password);  // null (transient field)
}

Serialization Caution

Java serialization has known security vulnerabilities. For new projects, prefer JSON (Jackson, Gson) or Protocol Buffers. Always define serialVersionUID to prevent InvalidClassException when class structure changes. Mark sensitive fields transient.

07

Concurrency & Threads

Multithreaded programming in Java. From basic thread creation to lock-free atomic operations and the modern CompletableFuture API.

Thread Basics

Two ways to create threads: extending Thread or implementing Runnable. The Runnable approach is preferred because Java only allows single inheritance.

// Approach 1: Extend Thread (less flexible)
class BrewThread extends Thread {
    @Override
    public void run() {
        System.out.println(getName() + " is brewing coffee...");
    }
}

// Approach 2: Implement Runnable (preferred)
class BrewTask implements Runnable {
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + " is grinding beans...");
    }
}

// Approach 3: Lambda (cleanest for simple tasks)
Runnable pourTask = () -> System.out.println("Pouring...");

// Starting threads
BrewThread t1 = new BrewThread();
t1.setName("Barista-1");
t1.start();  // start() creates a new OS thread; never call run() directly

Thread t2 = new Thread(new BrewTask(), "Barista-2");
t2.start();

Thread t3 = new Thread(pourTask, "Barista-3");
t3.start();

Thread lifecycle states — a thread moves through these states during its life:

NEW

Thread object created but start() not yet called. No OS thread allocated.

RUNNABLE

After start(). Running or ready to run. The OS scheduler decides when it actually executes.

BLOCKED

Waiting to acquire a monitor lock (e.g., trying to enter a synchronized block held by another thread).

WAITING

Indefinitely waiting for another thread. Caused by wait(), join(), or LockSupport.park().

TIMED_WAITING

Waiting with a timeout. Caused by sleep(ms), wait(ms), or join(ms).

TERMINATED

The run() method completed (normally or via exception). Thread cannot be restarted.

// Joining threads — wait for completion
Thread worker = new Thread(() -> {
    try {
        Thread.sleep(2000);  // simulate work
        System.out.println("Work done!");
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();  // restore interrupt flag
    }
});

worker.start();
System.out.println("Waiting for worker...");
worker.join();  // blocks until worker finishes (or is interrupted)
System.out.println("Worker finished!");

// Daemon threads — background threads that don't prevent JVM shutdown
Thread daemon = new Thread(() -> {
    while (true) {
        System.out.println("Background cleanup...");
        Thread.sleep(5000);
    }
});
daemon.setDaemon(true);  // must set BEFORE start()
daemon.start();

Synchronization

When multiple threads access shared mutable state, you must synchronize access. Java uses intrinsic locks (monitors) via the synchronized keyword.

// Synchronized method — locks on 'this' (the instance)
public class CoffeeCounter {
    private int count = 0;

    public synchronized void brew() {
        count++;  // thread-safe: only one thread at a time
    }

    public synchronized int getCount() {
        return count;
    }
}

// Synchronized block — more granular locking
public class CoffeeShop {
    private final List<String> orders = new ArrayList<>();
    private final Object lock = new Object();  // dedicated lock object

    public void addOrder(String order) {
        // Only the critical section is locked
        synchronized (lock) {
            orders.add(order);
        }
        // Non-critical code runs without holding the lock
        System.out.println("Order placed: " + order);
    }

    public List<String> getOrders() {
        synchronized (lock) {
            return new ArrayList<>(orders);  // return a copy
        }
    }
}
// volatile — ensures visibility across threads (no caching)
public class ShutdownFlag {
    private volatile boolean running = true;  // visible to all threads

    public void stop() {
        running = false;  // write is immediately visible to readers
    }

    public void doWork() {
        while (running) {           // reads latest value every iteration
            processNextOrder();
        }
    }
}

// volatile guarantees visibility but NOT atomicity
// Use synchronized or Atomic* for compound operations (check-then-act, read-modify-write)

Deadlock

Deadlock occurs when two or more threads are each waiting for a lock held by the other. Thread A holds Lock 1 and waits for Lock 2, while Thread B holds Lock 2 and waits for Lock 1. Neither can proceed. Prevent deadlocks by always acquiring locks in a consistent, predefined order.

ExecutorService

Thread pools manage a reusable set of threads. Instead of creating threads manually, submit tasks to an executor. This improves performance and resource management.

import java.util.concurrent.*;

// Fixed pool — bounded number of threads
ExecutorService fixed = Executors.newFixedThreadPool(4);

// Cached pool — creates threads as needed, reuses idle ones
ExecutorService cached = Executors.newCachedThreadPool();

// Single thread — guarantees sequential execution
ExecutorService single = Executors.newSingleThreadExecutor();

// Scheduled — for delayed and periodic tasks
ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(2);

// Submitting tasks
fixed.execute(() -> System.out.println("Fire and forget"));

Future<String> future = fixed.submit(() -> {
    Thread.sleep(1000);
    return "Espresso ready!";
});

// Scheduled tasks
scheduler.schedule(() -> System.out.println("Delayed!"), 5, TimeUnit.SECONDS);
scheduler.scheduleAtFixedRate(
    () -> System.out.println("Every 10s"), 0, 10, TimeUnit.SECONDS
);
// Proper shutdown pattern
ExecutorService pool = Executors.newFixedThreadPool(4);
try {
    // submit tasks...
    pool.submit(() -> brewCoffee("Latte"));
    pool.submit(() -> brewCoffee("Mocha"));
} finally {
    pool.shutdown();  // no new tasks accepted, existing tasks finish
    try {
        if (!pool.awaitTermination(60, TimeUnit.SECONDS)) {
            pool.shutdownNow();  // forcefully interrupt running tasks
            if (!pool.awaitTermination(60, TimeUnit.SECONDS)) {
                System.err.println("Pool did not terminate!");
            }
        }
    } catch (InterruptedException e) {
        pool.shutdownNow();
        Thread.currentThread().interrupt();
    }
}

// Java 19+ try-with-resources (auto-closes on scope exit)
try (var pool2 = Executors.newFixedThreadPool(4)) {
    pool2.submit(() -> brewCoffee("Americano"));
}  // automatically shuts down and awaits termination

Callable & Future

Callable is like Runnable but returns a result and can throw checked exceptions. Future represents the pending result.

Runnable

No return value. Cannot throw checked exceptions. Use for fire-and-forget tasks.

Runnable task = () -> {
    System.out.println("Working");
};
executor.execute(task);

Callable<T>

Returns a value of type T. Can throw checked exceptions. Use when you need a result.

Callable<Double> task = () -> {
    return calculatePrice();
};
Future<Double> f = executor.submit(task);
// Future methods
ExecutorService pool = Executors.newFixedThreadPool(2);

Future<String> order = pool.submit(() -> {
    Thread.sleep(2000);
    return "Cappuccino";
});

order.isDone();               // false — still running
order.cancel(true);           // attempt to cancel (true = may interrupt)
order.isCancelled();          // check if cancelled

// get() blocks until result is available
String result = order.get();  // blocks here...
System.out.println(result);   // "Cappuccino"

// get() with timeout — throws TimeoutException if too slow
String result2 = order.get(5, TimeUnit.SECONDS);

// Handling exceptions from Callable
Future<String> failing = pool.submit(() -> {
    throw new RuntimeException("Machine broken!");
});

try {
    failing.get();  // ExecutionException wraps the original exception
} catch (ExecutionException e) {
    Throwable cause = e.getCause();  // RuntimeException: Machine broken!
    System.err.println("Task failed: " + cause.getMessage());
}

CompletableFuture

Java 8 introduced CompletableFuture for composable, non-blocking asynchronous programming. Chain operations, combine results, and handle errors declaratively.

import java.util.concurrent.CompletableFuture;

// Creating — runs on ForkJoinPool.commonPool() by default
CompletableFuture<String> cf1 = CompletableFuture.supplyAsync(() -> {
    return fetchBeans("Colombian");  // returns a value
});

CompletableFuture<Void> cf2 = CompletableFuture.runAsync(() -> {
    cleanMachine();  // no return value
});

// Chaining — thenApply transforms the result (like map)
CompletableFuture<String> order = CompletableFuture
    .supplyAsync(() -> "beans")
    .thenApply(beans -> grind(beans))           // String -> String
    .thenApply(grounds -> brew(grounds))         // String -> String
    .thenApply(coffee -> addMilk(coffee));       // String -> String

// thenCompose — for chaining futures (like flatMap)
CompletableFuture<String> pipeline = CompletableFuture
    .supplyAsync(() -> selectBeans())
    .thenCompose(beans -> roastAsync(beans))     // returns CompletableFuture
    .thenCompose(roasted -> brewAsync(roasted)); // returns CompletableFuture

// thenCombine — combine two independent futures
CompletableFuture<String> coffee = brewAsync("espresso");
CompletableFuture<String> milk = steamAsync("oat milk");
CompletableFuture<String> latte = coffee.thenCombine(milk,
    (c, m) -> c + " + " + m);  // "espresso + oat milk"
// Error handling
CompletableFuture<String> safe = CompletableFuture
    .supplyAsync(() -> riskyOperation())
    .exceptionally(ex -> {
        System.err.println("Failed: " + ex.getMessage());
        return "default value";  // fallback
    });

// handle — receives both result and exception
CompletableFuture<String> handled = CompletableFuture
    .supplyAsync(() -> riskyOperation())
    .handle((result, ex) -> {
        if (ex != null) return "fallback";
        return result.toUpperCase();
    });

// Combining multiple futures
CompletableFuture<String> f1 = CompletableFuture.supplyAsync(() -> "Latte");
CompletableFuture<String> f2 = CompletableFuture.supplyAsync(() -> "Mocha");
CompletableFuture<String> f3 = CompletableFuture.supplyAsync(() -> "Flat White");

// allOf — wait for ALL to complete
CompletableFuture<Void> all = CompletableFuture.allOf(f1, f2, f3);
all.thenRun(() -> System.out.println("All orders ready!"));

// anyOf — wait for FIRST to complete
CompletableFuture<Object> fastest = CompletableFuture.anyOf(f1, f2, f3);
fastest.thenAccept(result -> System.out.println("First ready: " + result));

Concurrent Collections

Thread-safe collections from java.util.concurrent. These avoid the coarse-grained locking of Collections.synchronizedList() and deliver better throughput.

Collection Thread-Safe Alternative To Strategy
ConcurrentHashMap HashMap Segment-level locking; high read concurrency
CopyOnWriteArrayList ArrayList Copies entire array on write; ideal for read-heavy use
BlockingQueue Queue Blocks on take/put; producer-consumer pattern
ConcurrentLinkedQueue LinkedList Lock-free CAS-based; non-blocking
import java.util.concurrent.*;

// ConcurrentHashMap — safe concurrent reads and writes
ConcurrentHashMap<String, Integer> menu = new ConcurrentHashMap<>();
menu.put("Espresso", 3);
menu.put("Latte", 5);
menu.computeIfAbsent("Mocha", k -> 4);      // atomic check-and-insert
menu.merge("Espresso", 1, Integer::sum);      // atomic read-modify-write

// CopyOnWriteArrayList — thread-safe iteration without ConcurrentModificationException
CopyOnWriteArrayList<String> subscribers = new CopyOnWriteArrayList<>();
subscribers.add("alice@coffee.com");
subscribers.add("bob@coffee.com");
for (String s : subscribers) {
    // safe to iterate even if another thread adds/removes
}

// BlockingQueue — producer-consumer pattern
BlockingQueue<String> orderQueue = new LinkedBlockingQueue<>(100);

// Producer thread
orderQueue.put("Latte");       // blocks if queue is full

// Consumer thread
String order = orderQueue.take();  // blocks if queue is empty

Atomic Variables

Lock-free thread-safe operations on single variables using CPU-level compare-and-swap (CAS). Found in java.util.concurrent.atomic.

import java.util.concurrent.atomic.*;

// AtomicInteger — thread-safe counter without synchronized
AtomicInteger orderCount = new AtomicInteger(0);
orderCount.incrementAndGet();         // atomic i++, returns new value
orderCount.getAndIncrement();         // atomic i++, returns old value
orderCount.addAndGet(5);              // atomic i += 5
orderCount.compareAndSet(6, 0);       // set to 0 only if current value is 6

// AtomicLong — same API, 64-bit
AtomicLong totalRevenue = new AtomicLong(0L);
totalRevenue.addAndGet(450L);

// AtomicReference — for reference types
AtomicReference<String> currentSpecial = new AtomicReference<>("Mocha");
currentSpecial.set("Pumpkin Spice Latte");
currentSpecial.compareAndSet("Pumpkin Spice Latte", "Gingerbread Latte");

// AtomicBoolean — for flags
AtomicBoolean shopOpen = new AtomicBoolean(true);
if (shopOpen.compareAndSet(true, false)) {
    System.out.println("Shop closed for the day.");
}

// Use Atomic* when you need thread-safe single-variable operations
// without the overhead of synchronized blocks

Common Pitfalls

Concurrency bugs are notoriously difficult to reproduce and debug. Know these patterns to avoid them.

Race Condition

When the correctness of a program depends on thread timing. Classic example: two threads doing count++ on a shared int. The read-modify-write is not atomic — both threads may read the same value and write the same incremented value, losing an update. Fix: use synchronized, AtomicInteger, or a Lock.

Thread Starvation

A thread is perpetually denied access to resources it needs. Can happen when high-priority threads always run first, or when a thread pool is too small and long-running tasks block shorter ones. Fix: use fair locks (new ReentrantLock(true)), properly size thread pools, and avoid holding locks during I/O.

Visibility Issue

Without synchronization or volatile, changes made by one thread may never be visible to another thread. The JVM is allowed to cache variable values in CPU registers or reorder instructions. Fix: use volatile for simple flags, synchronized for compound operations, or classes from java.util.concurrent.

Prefer higher-level concurrency utilities (ExecutorService, CompletableFuture, concurrent collections) over raw threads and synchronized. They are harder to misuse and easier to reason about. When you do need low-level control, consider java.util.concurrent.locks.ReentrantLock over intrinsic locks for its tryLock, fairness, and multiple-condition support.

08

Modern Java (17–21+)

Java has evolved rapidly since the six-month release cadence began. Records, sealed classes, pattern matching, virtual threads, and more.

Version Timeline

Key features introduced in each major release. LTS versions receive long-term security patches and are the standard choice for production.

Version Released Type Key Features
Java 8 Mar 2014 Legacy LTS Lambdas, Streams, Optional, default methods, java.time
Java 9 Sep 2017 Feature Modules (JPMS), JShell, private interface methods
Java 10 Mar 2018 Feature var local variable type inference
Java 11 Sep 2018 LTS HTTP Client, single-file execution, String methods
Java 14 Mar 2020 Feature Switch expressions, helpful NPEs, Records (preview)
Java 15 Sep 2020 Feature Text blocks, sealed classes (preview), hidden classes
Java 16 Mar 2021 Feature Records (final), pattern matching instanceof, Stream.toList()
Java 17 Sep 2021 LTS Sealed classes (final), pattern matching switch (preview)
Java 21 Sep 2023 Latest LTS Virtual threads, pattern matching switch (final), sequenced collections, record patterns
Java 22+ Mar 2024+ Feature Unnamed variables, statements before super(), stream gatherers

Records (Java 16+)

Compact syntax for immutable data carriers. The compiler generates equals(), hashCode(), toString(), and accessor methods automatically.

// Before records — verbose boilerplate
public class CoffeeOrderOld {
    private final String drink;
    private final int size;
    private final double price;

    public CoffeeOrderOld(String drink, int size, double price) {
        this.drink = drink;
        this.size = size;
        this.price = price;
    }
    // ... equals(), hashCode(), toString(), getters — ~50 lines
}

// With records — one line!
public record CoffeeOrder(String drink, int size, double price) {}

// Usage
CoffeeOrder order = new CoffeeOrder("Latte", 16, 5.50);
order.drink();     // "Latte" — accessor method (no 'get' prefix)
order.size();      // 16
order.price();     // 5.50
order.toString();  // CoffeeOrder[drink=Latte, size=16, price=5.5]
// Compact canonical constructor — validation without repeating params
public record CoffeeOrder(String drink, int size, double price) {
    public CoffeeOrder {
        if (drink == null || drink.isBlank()) throw new IllegalArgumentException("Drink required");
        if (price < 0) throw new IllegalArgumentException("Price must be non-negative");
        drink = drink.trim();  // can reassign parameters before final assignment
    }
}

// Custom methods on records
public record CoffeeOrder(String drink, int size, double price) {
    public double totalWithTax(double taxRate) {
        return price * (1 + taxRate);
    }

    public static CoffeeOrder espresso() {
        return new CoffeeOrder("Espresso", 8, 3.00);
    }
}

// Records can implement interfaces (but cannot extend classes)
public record CoffeeOrder(String drink, int size, double price)
    implements Comparable<CoffeeOrder> {

    @Override
    public int compareTo(CoffeeOrder other) {
        return Double.compare(this.price, other.price);
    }
}

Use records for DTOs, API responses, value objects, and any class whose identity is defined entirely by its data. Do not use records for entities with mutable state, JPA entities (which need a no-arg constructor), or classes that need inheritance.

Sealed Classes (Java 17+)

Restrict which classes can extend a class or implement an interface. Enables exhaustive pattern matching in switch expressions.

// sealed — only permitted subclasses can extend
public sealed interface Beverage permits Coffee, Tea, Juice {}

// Each subclass must be final, sealed, or non-sealed
public final class Coffee implements Beverage {
    private final String roast;
    public Coffee(String roast) { this.roast = roast; }
    public String roast() { return roast; }
}

public final class Tea implements Beverage {
    private final String variety;
    public Tea(String variety) { this.variety = variety; }
    public String variety() { return variety; }
}

// non-sealed opens the hierarchy again
public non-sealed class Juice implements Beverage {
    // anyone can extend Juice
}

// Exhaustive switch — compiler verifies all cases covered
public String describe(Beverage b) {
    return switch (b) {
        case Coffee c -> "Coffee: " + c.roast() + " roast";
        case Tea t    -> "Tea: " + t.variety();
        case Juice j  -> "Fresh juice";
        // no default needed — all permitted subtypes covered
    };
}
// Sealed classes with records — powerful combination
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 {}

public double area(Shape shape) {
    return 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

Eliminate verbose casting and type-checking boilerplate. Java progressively added pattern matching across multiple versions.

// Pattern matching for instanceof (Java 16+)
// Before — manual cast
if (obj instanceof String) {
    String s = (String) obj;
    System.out.println(s.length());
}

// After — binding variable in one step
if (obj instanceof String s) {
    System.out.println(s.length());  // s is already cast
}

// Works with && (but not ||)
if (obj instanceof String s && s.length() > 5) {
    System.out.println("Long string: " + s);
}
// Pattern matching for switch (Java 21)
static String formatObj(Object obj) {
    return switch (obj) {
        case Integer i    -> "int: " + i;
        case Long l       -> "long: " + l;
        case Double d     -> "double: " + d;
        case String s     -> "string: " + s;
        case int[] arr    -> "array of length " + arr.length;
        case null         -> "null value";
        default           -> "unknown: " + obj.getClass().getName();
    };
}

// Guarded patterns — add conditions with 'when'
static String categorize(Object obj) {
    return switch (obj) {
        case Integer i when i < 0  -> "negative int";
        case Integer i when i == 0 -> "zero";
        case Integer i             -> "positive int: " + i;
        case String s when s.isBlank() -> "blank string";
        case String s              -> "string: " + s;
        default                    -> "other";
    };
}

// Record patterns — destructure records in one step (Java 21)
record Point(int x, int y) {}
record Line(Point start, Point end) {}

static String describePoint(Object obj) {
    return switch (obj) {
        case Point(int x, int y) -> "Point at (" + x + ", " + y + ")";
        case Line(Point(int x1, int y1), Point(int x2, int y2)) ->
            "Line from (" + x1 + "," + y1 + ") to (" + x2 + "," + y2 + ")";
        default -> "not a shape";
    };
}

Switch Expressions (Java 14+)

Switch is now an expression that returns a value. Arrow syntax eliminates fall-through bugs and the yield keyword handles complex cases.

// Traditional switch statement (still valid)
switch (size) {
    case "S": price = 3.00; break;
    case "M": price = 4.00; break;
    case "L": price = 5.00; break;
    default:  price = 4.00; break;
}

// Switch expression with arrow syntax — no break, no fall-through
double price = switch (size) {
    case "S" -> 3.00;
    case "M" -> 4.00;
    case "L" -> 5.00;
    default  -> 4.00;
};  // note the semicolon — it's an expression assigned to a variable

// Multiple case labels
String category = switch (day) {
    case MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY -> "Weekday";
    case SATURDAY, SUNDAY -> "Weekend";
};

// yield — for multi-line case blocks
String description = switch (roast) {
    case "light" -> "Bright and acidic";
    case "medium" -> "Balanced flavor";
    case "dark" -> {
        String base = "Bold and smoky";
        yield base + " — our signature";  // yield returns from the block
    }
    default -> "Unknown roast";
};

// Exhaustiveness — enums and sealed types don't need a default
enum Size { SMALL, MEDIUM, LARGE }
String label = switch (size) {
    case SMALL  -> "S";
    case MEDIUM -> "M";
    case LARGE  -> "L";
    // no default needed — all cases covered
};

Text Blocks (Java 15+)

Multi-line string literals using triple quotes. Indentation is managed automatically, and common escape sequences work as expected.

// Before text blocks — painful concatenation
String json = "{\n" +
    "  \"drink\": \"Latte\",\n" +
    "  \"size\": 16,\n" +
    "  \"price\": 5.50\n" +
    "}";

// With text blocks — natural formatting
String json = """
        {
          "drink": "Latte",
          "size": 16,
          "price": 5.50
        }
        """;

// SQL queries
String sql = """
        SELECT c.name, c.price
        FROM coffees c
        WHERE c.roast = 'dark'
          AND c.price < 6.00
        ORDER BY c.price DESC
        """;

// HTML templates
String html = """
        <div class="order">
            <h2>%s</h2>
            <p>Price: $%.2f</p>
        </div>
        """.formatted("Mocha", 4.75);

// Escape sequences in text blocks
String escaped = """
        Line 1\
        continues here    \s
        "quotes work fine"
        """;
// \  at end of line = no newline (join lines)
// \s = preserve trailing space (space is significant)

Virtual Threads (Java 21)

Lightweight threads managed by the JVM, not the OS. Create millions of threads with minimal overhead. Ideal for I/O-bound workloads like web servers and database calls.

// Creating virtual threads
Thread vt = Thread.ofVirtual()
    .name("barista-", 1)       // naming pattern: barista-1, barista-2, ...
    .start(() -> {
        System.out.println(Thread.currentThread());
    });

// Simple one-liner
Thread.startVirtualThread(() -> processOrder());

// ExecutorService with virtual threads (preferred for production)
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
    // Each task gets its own virtual thread — no pool limit!
    for (int i = 0; i < 100_000; i++) {
        executor.submit(() -> {
            // I/O-bound work: HTTP call, DB query, file read
            String result = fetchFromDatabase();
            return result;
        });
    }
}  // waits for all tasks to complete

Platform Threads

1:1 mapping to OS threads. Heavy (~1MB stack). Limited to thousands. Good for CPU-bound work.

// Expensive — OS allocates real thread
Thread.ofPlatform()
    .name("worker")
    .start(task);

Virtual Threads

Many-to-one on carrier threads. Lightweight (~1KB). Scale to millions. Ideal for I/O-bound work.

// Cheap — JVM manages scheduling
Thread.ofVirtual()
    .name("handler")
    .start(task);

Migration Tips

Virtual threads are best for I/O-bound tasks (HTTP requests, DB queries, file I/O). Avoid them for CPU-heavy computation — use platform thread pools for that. Do not pool virtual threads; create a new one per task. Avoid synchronized blocks with I/O inside them (use ReentrantLock instead) as they can pin the carrier thread.

Helpful NullPointerExceptions (Java 14+)

The JVM now tells you exactly which variable was null in a chain, instead of leaving you guessing.

Before Java 14

order.getCustomer().getAddress().getCity()

// Exception:
// NullPointerException
//   at App.main(App.java:42)
// Which was null? order? customer?
// address? You have to debug it.

Java 14+

order.getCustomer().getAddress().getCity()

// Exception:
// NullPointerException: Cannot invoke
// "Address.getCity()" because the return
// value of "Customer.getAddress()"
// is null

Enabled by default since Java 14. Enable explicitly on older builds with -XX:+ShowCodeDetailsInExceptionMessages.

Unnamed Variables (Java 22)

Use _ (underscore) for variables you must declare but never use. Communicates intent clearly and avoids compiler warnings.

// try-catch — you don't use the exception variable
try {
    int port = Integer.parseInt(input);
} catch (NumberFormatException _) {
    System.out.println("Invalid port number, using default");
}

// Enhanced for-each — you only care about the count
int count = 0;
for (String _ : orderList) {
    count++;
}

// Lambda parameters you don't need
BiFunction<String, Integer, String> format = (name, _) ->
    "Customer: " + name;

// Map iteration — only need the value
for (var _ : map.entrySet()) {
    // process without naming the variable
}

// Pattern matching — ignore some components
if (obj instanceof Point(int x, int _)) {
    System.out.println("x = " + x);  // y is irrelevant
}

// Switch with unused binding
switch (shape) {
    case Circle _   -> "round";
    case Rectangle _ -> "rectangular";
}

09

Build Tools

Maven and Gradle automate compiling, testing, packaging, and dependency management. Every production Java project uses one or the other.

Maven Basics

Maven uses an XML-based pom.xml (Project Object Model) and a convention-over-configuration philosophy. Everything follows a lifecycle of phases.

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0">
    <modelVersion>4.0.0</modelVersion>

    <!-- Project coordinates (the unique ID) -->
    <groupId>com.coffeeshop</groupId>
    <artifactId>barista-api</artifactId>
    <version>1.0.0</version>
    <packaging>jar</packaging>

    <properties>
        <java.version>21</java.version>
        <maven.compiler.source>${java.version}</maven.compiler.source>
        <maven.compiler.target>${java.version}</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencies>
        <!-- Compile dependency -->
        <dependency>
            <groupId>com.google.guava</groupId>
            <artifactId>guava</artifactId>
            <version>33.0.0-jre</version>
        </dependency>
        <!-- Test dependency -->
        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter</artifactId>
            <version>5.10.2</version>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.12.1</version>
            </plugin>
        </plugins>
    </build>
</project>

Maven Lifecycle Phases — each phase executes all preceding phases automatically:

Phase Description
clean Deletes target/ directory
validate Validates project structure and POM
compile Compiles main source code to target/classes/
test Runs unit tests via Surefire plugin
package Packages compiled code into JAR/WAR
verify Runs integration tests and quality checks
install Installs artifact to local ~/.m2/repository
deploy Uploads artifact to remote repository
# Common Maven commands
mvn clean install              # Clean, compile, test, package, install to local repo
mvn test                       # Compile and run tests only
mvn package -DskipTests        # Package without running tests
mvn dependency:tree             # Show dependency hierarchy
mvn versions:display-dependency-updates  # Check for newer dependencies
mvn clean install -U           # Force update snapshots from remote
mvn help:effective-pom         # Show the fully resolved POM

Maven Dependency Management

Dependencies are declared with a scope that controls when they are available and whether they are included in the final artifact.

Scope Compile Classpath Test Classpath Runtime Classpath Packaged
compile (default)
provided
test
runtime
system
<!-- BOM (Bill of Materials) — centralize dependency versions -->
<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-dependencies</artifactId>
            <version>3.2.2</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

<!-- Now child dependencies don't need <version> -->
<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
        <!-- version inherited from BOM -->
    </dependency>
</dependencies>

<!-- Exclusions — remove transitive dependencies -->
<dependency>
    <groupId>com.example</groupId>
    <artifactId>some-library</artifactId>
    <version>2.0.0</version>
    <exclusions>
        <exclusion>
            <groupId>commons-logging</groupId>
            <artifactId>commons-logging</artifactId>
        </exclusion>
    </exclusions>
</dependency>

Gradle Basics

Gradle uses a Groovy or Kotlin DSL instead of XML. It is faster than Maven (incremental builds, build cache) and more flexible.

Groovy DSL (build.gradle)

plugins {
    id 'java'
    id 'application'
}

group = 'com.coffeeshop'
version = '1.0.0'

java {
    toolchain {
        languageVersion =
            JavaLanguageVersion.of(21)
    }
}

repositories {
    mavenCentral()
}

dependencies {
    implementation 'com.google.guava:guava:33.0.0-jre'
    testImplementation 'org.junit.jupiter:junit-jupiter:5.10.2'
}

application {
    mainClass = 'com.coffeeshop.App'
}

test {
    useJUnitPlatform()
}

Kotlin DSL (build.gradle.kts)

plugins {
    java
    application
}

group = "com.coffeeshop"
version = "1.0.0"

java {
    toolchain {
        languageVersion.set(
            JavaLanguageVersion.of(21)
        )
    }
}

repositories {
    mavenCentral()
}

dependencies {
    implementation("com.google.guava:guava:33.0.0-jre")
    testImplementation("org.junit.jupiter:junit-jupiter:5.10.2")
}

application {
    mainClass.set("com.coffeeshop.App")
}

tasks.test {
    useJUnitPlatform()
}
# Common Gradle commands
gradle build                   # Compile, test, and package
gradle test                    # Run tests only
gradle clean build             # Delete build/ then rebuild
gradle dependencies            # Show dependency tree
gradle tasks                   # List all available tasks
gradle build -x test           # Build without running tests
gradle --refresh-dependencies  # Force re-download dependencies
gradle wrapper --gradle-version 8.5  # Update wrapper version

Gradle Dependency Management

Gradle uses named configurations instead of Maven scopes. The most common ones map closely to Maven equivalents.

Gradle Configuration Maven Equivalent Description
implementation compile Internal dependency; not exposed to consumers
api compile Exposed to consumers (requires java-library plugin)
compileOnly provided Available at compile time only (e.g., annotations)
runtimeOnly runtime Available at runtime only (e.g., JDBC drivers)
testImplementation test Available during test compilation and execution
testRuntimeOnly test + runtime Available at test runtime only (e.g., test engines)
// Gradle dependency declarations
dependencies {
    // Compile + runtime (hidden from consumers)
    implementation 'com.google.guava:guava:33.0.0-jre'

    // Compile + runtime (exposed to consumers of this library)
    api 'org.apache.commons:commons-lang3:3.14.0'

    // Compile only (provided at runtime by container)
    compileOnly 'jakarta.servlet:jakarta.servlet-api:6.0.0'

    // Runtime only (not needed at compile time)
    runtimeOnly 'org.postgresql:postgresql:42.7.1'

    // Test dependencies
    testImplementation 'org.junit.jupiter:junit-jupiter:5.10.2'
    testImplementation 'org.mockito:mockito-core:5.10.0'
    testRuntimeOnly 'org.junit.platform:junit-platform-launcher'

    // Platform (BOM) — version alignment
    implementation platform('org.springframework.boot:spring-boot-dependencies:3.2.2')
    implementation 'org.springframework.boot:spring-boot-starter-web'  // no version needed
}

Maven vs Gradle

Both are mature, well-supported build tools. Choose based on your team's experience and project requirements.

Aspect Maven Gradle
Config Format XML (pom.xml) Groovy/Kotlin DSL
Build Speed Slower (no incremental build by default) Faster (incremental, build cache, daemon)
Flexibility Rigid convention-over-configuration Highly customizable with scripting
Learning Curve Lower (strict conventions) Steeper (flexibility = more decisions)
Market Share ~60% of Java projects ~35% of Java projects (growing)
IDE Support Excellent in all IDEs Excellent in all IDEs
Multi-module Parent POM + modules settings.gradle + subprojects
Dependency Mgmt Scopes (compile, test, provided) Configurations (implementation, api, compileOnly)
Best For Enterprise, Spring Boot, standardized teams Android, multi-module, performance-critical builds

If you are starting a new project and have no strong preference, Spring Initializr defaults to Gradle with Kotlin DSL. For enterprise environments, Maven is still the safe default. Either way, always commit the wrapper (mvnw / gradlew) so everyone builds with the same version.

Standard Project Structure

Both Maven and Gradle follow the same directory layout by default. This is the standard that all Java IDEs, CI tools, and frameworks expect.

my-project/
├── pom.xml                       # Maven config (or build.gradle for Gradle)
├── mvnw / gradlew                # Wrapper scripts (commit these!)
├── src/
│   ├── main/
│   │   ├── java/                 # Application source code
│   │   │   └── com/coffeeshop/
│   │   │       ├── App.java
│   │   │       ├── model/
│   │   │       │   └── Coffee.java
│   │   │       ├── service/
│   │   │       │   └── BrewService.java
│   │   │       └── controller/
│   │   │           └── OrderController.java
│   │   └── resources/            # Configuration and static files
│   │       ├── application.yml
│   │       ├── logback.xml
│   │       └── db/migration/
│   └── test/
│       ├── java/                 # Test source code (mirrors main/java structure)
│       │   └── com/coffeeshop/
│       │       ├── model/
│       │       │   └── CoffeeTest.java
│       │       └── service/
│       │           └── BrewServiceTest.java
│       └── resources/            # Test-specific config
│           └── application-test.yml
└── target/ (Maven) / build/ (Gradle)   # Generated output (never commit)

Package Naming

Java convention is reverse domain name: com.company.project. Package names match directory structure. Test packages should mirror main packages so tests can access package-private members.

Useful Plugins

Common plugins for code quality, packaging, and framework support. Both Maven and Gradle have equivalents for most tasks.

Purpose Maven Plugin Gradle Plugin
Compiler maven-compiler-plugin java (built-in)
Unit Tests maven-surefire-plugin test task (built-in)
Integration Tests maven-failsafe-plugin jvm-test-suite
Code Formatting spotless-maven-plugin com.diffplug.spotless
Static Analysis maven-checkstyle-plugin checkstyle
Fat JAR maven-shade-plugin com.github.johnrengelman.shadow
Spring Boot spring-boot-maven-plugin org.springframework.boot
Code Coverage jacoco-maven-plugin jacoco
<!-- Maven: Fat JAR with shade plugin -->
<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-shade-plugin</artifactId>
    <version>3.5.1</version>
    <executions>
        <execution>
            <phase>package</phase>
            <goals><goal>shade</goal></goals>
            <configuration>
                <transformers>
                    <transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
                        <mainClass>com.coffeeshop.App</mainClass>
                    </transformer>
                </transformers>
            </configuration>
        </execution>
    </executions>
</plugin>
// Gradle: Fat JAR with shadow plugin
plugins {
    id 'java'
    id 'com.github.johnrengelman.shadow' version '8.1.1'
}

jar {
    manifest {
        attributes 'Main-Class': 'com.coffeeshop.App'
    }
}

// Run: gradle shadowJar
// Output: build/libs/barista-api-1.0.0-all.jar

10

Testing

JUnit 5, Mockito, parameterized tests, and integration testing. Write tests that catch bugs before your users do.

JUnit 5 Basics

JUnit 5 (Jupiter) is the standard testing framework for Java. Test classes and methods don't need to be public. Each test method gets a fresh instance of the test class.

import org.junit.jupiter.api.*;
import static org.junit.jupiter.api.Assertions.*;

class CoffeeServiceTest {

    private CoffeeService service;

    @BeforeAll
    static void setupOnce() {
        // Runs once before ALL tests in this class
        // Must be static (shared setup like DB connections)
        System.out.println("Starting test suite");
    }

    @BeforeEach
    void setUp() {
        // Runs before EACH test method (fresh state)
        service = new CoffeeService(new InMemoryRepo());
    }

    @Test
    @DisplayName("should brew a valid espresso order")
    void brewValidOrder() {
        Coffee result = service.brew("espresso", Size.SMALL);

        assertNotNull(result);
        assertEquals("espresso", result.getType());
        assertEquals(Size.SMALL, result.getSize());
    }

    @Test
    @DisplayName("should reject null bean type")
    void rejectNullBean() {
        assertThrows(IllegalArgumentException.class, () -> {
            service.brew(null, Size.MEDIUM);
        });
    }

    @Test
    @Disabled("Pending barista API integration")
    void shouldConnectToBaristaApi() {
        // Skipped — not yet implemented
    }

    @AfterEach
    void tearDown() {
        // Runs after EACH test method (cleanup)
        service.shutdown();
    }

    @AfterAll
    static void cleanupOnce() {
        // Runs once after ALL tests in this class
        System.out.println("Test suite complete");
    }
}
Annotation Scope Purpose
@Test Method Marks a test method
@DisplayName Class / Method Human-readable name in test reports
@BeforeEach Method Runs before each test (setup)
@AfterEach Method Runs after each test (teardown)
@BeforeAll Static method Runs once before all tests
@AfterAll Static method Runs once after all tests
@Disabled Class / Method Skip this test or class
@Nested Inner class Group related tests hierarchically
@Tag("slow") Class / Method Categorize tests for filtering
@TestMethodOrder Class Control execution order

Assertions

JUnit 5 provides rich assertion methods in org.junit.jupiter.api.Assertions. All accept an optional last parameter for a custom failure message.

import static org.junit.jupiter.api.Assertions.*;

@Test
void assertionShowcase() {
    Coffee latte = new Coffee("latte", 4.50, Size.MEDIUM);

    // Equality
    assertEquals("latte", latte.getType());
    assertEquals(4.50, latte.getPrice(), 0.01);  // delta for doubles
    assertNotEquals("mocha", latte.getType());

    // Boolean
    assertTrue(latte.getPrice() > 0, "Price must be positive");
    assertFalse(latte.isDecaf());

    // Null checks
    assertNotNull(latte);
    assertNull(latte.getDiscount());

    // Same reference
    Coffee same = latte;
    assertSame(same, latte);

    // Exception testing
    Exception ex = assertThrows(
        InvalidOrderException.class,
        () -> service.order(null)
    );
    assertEquals("Bean type required", ex.getMessage());

    // Timeout — fails if block takes longer than 2 seconds
    assertTimeout(Duration.ofSeconds(2), () -> {
        service.brew("espresso", Size.SMALL);
    });

    // Grouped assertions — ALL are evaluated, even if some fail
    assertAll("latte properties",
        () -> assertEquals("latte", latte.getType()),
        () -> assertEquals(Size.MEDIUM, latte.getSize()),
        () -> assertTrue(latte.getPrice() > 0)
    );
}

@Test
void iterableAndArrayAssertions() {
    List<String> menu = List.of("Espresso", "Latte", "Mocha");

    assertIterableEquals(
        List.of("Espresso", "Latte", "Mocha"),
        menu
    );

    assertArrayEquals(
        new int[]{1, 2, 3},
        new int[]{1, 2, 3}
    );

    // Lines comparison (useful for multi-line strings)
    assertLinesMatch(
        List.of("Espresso", "\\w+", "Mocha"),  // regex supported
        menu
    );
}

Parameterized Tests

Run the same test logic with different inputs. Each set of arguments creates a separate test invocation in reports.

import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.*;

class PricingServiceTest {

    // @ValueSource — simple single-argument tests
    @ParameterizedTest
    @ValueSource(strings = {"espresso", "latte", "cappuccino", "mocha"})
    void allDrinksHavePositivePrice(String drink) {
        double price = pricingService.getPrice(drink);
        assertTrue(price > 0, drink + " should have positive price");
    }

    @ParameterizedTest
    @ValueSource(ints = {1, 5, 10, 50, 100})
    void bulkDiscountApplied(int quantity) {
        Order order = new Order("latte", quantity);
        assertTrue(order.getDiscount() >= 0);
    }

    // @NullAndEmptySource — tests null and "" automatically
    @ParameterizedTest
    @NullAndEmptySource
    @ValueSource(strings = {"  ", "\t", "\n"})
    void rejectBlankDrinkNames(String drink) {
        assertThrows(IllegalArgumentException.class,
            () -> pricingService.getPrice(drink));
    }

    // @CsvSource — multiple arguments per test case
    @ParameterizedTest(name = "{0} size {1} = ${2}")
    @CsvSource({
        "espresso, SMALL,  3.00",
        "espresso, LARGE,  4.50",
        "latte,    MEDIUM, 4.75",
        "mocha,    LARGE,  6.00"
    })
    void correctPricing(String drink, Size size, double expected) {
        assertEquals(expected, pricingService.getPrice(drink, size), 0.01);
    }

    // @MethodSource — complex objects from a factory method
    @ParameterizedTest
    @MethodSource("orderProvider")
    void ordersAreValid(Order order) {
        assertTrue(order.isValid());
    }

    static Stream<Order> orderProvider() {
        return Stream.of(
            new Order("espresso", 1, Size.SMALL),
            new Order("latte", 2, Size.MEDIUM),
            new Order("mocha", 1, Size.LARGE)
        );
    }

    // @EnumSource — iterate over enum values
    @ParameterizedTest
    @EnumSource(Size.class)
    void allSizesSupported(Size size) {
        assertNotNull(pricingService.getSizeMultiplier(size));
    }

    @ParameterizedTest
    @EnumSource(value = Size.class, names = {"SMALL", "MEDIUM"})
    void standardSizesUnder5Dollars(Size size) {
        assertTrue(pricingService.getPrice("drip", size) < 5.00);
    }
}

Mockito Fundamentals

Mockito creates mock objects to isolate the unit under test from its dependencies. Stub return values, verify interactions, and control behavior.

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import static org.mockito.Mockito.*;
import static org.mockito.ArgumentMatchers.*;

@ExtendWith(MockitoExtension.class)  // enables @Mock and @InjectMocks
class OrderServiceTest {

    @Mock
    CoffeeRepository coffeeRepo;          // mock dependency

    @Mock
    PaymentGateway paymentGateway;         // mock dependency

    @InjectMocks
    OrderService orderService;             // injects mocks automatically

    @Test
    void shouldPlaceOrderSuccessfully() {
        // Arrange — stub return values
        Coffee latte = new Coffee("latte", 4.50);
        when(coffeeRepo.findByName("latte")).thenReturn(Optional.of(latte));
        when(paymentGateway.charge(anyDouble())).thenReturn(true);

        // Act
        OrderResult result = orderService.placeOrder("latte", 2);

        // Assert
        assertTrue(result.isSuccess());
        assertEquals(9.00, result.getTotal(), 0.01);

        // Verify interactions
        verify(coffeeRepo).findByName("latte");
        verify(paymentGateway).charge(9.00);
        verify(paymentGateway, never()).refund(anyDouble());
    }

    @Test
    void shouldThrowWhenDrinkNotFound() {
        when(coffeeRepo.findByName(anyString())).thenReturn(Optional.empty());

        assertThrows(DrinkNotFoundException.class,
            () -> orderService.placeOrder("unicorn-frapp", 1));

        verify(paymentGateway, never()).charge(anyDouble());
    }

    @Test
    void shouldHandlePaymentFailure() {
        Coffee espresso = new Coffee("espresso", 3.00);
        when(coffeeRepo.findByName("espresso")).thenReturn(Optional.of(espresso));
        when(paymentGateway.charge(anyDouble()))
            .thenThrow(new PaymentException("Card declined"));

        assertThrows(PaymentException.class,
            () -> orderService.placeOrder("espresso", 1));
    }
}

// Common argument matchers
verify(repo).save(any(Coffee.class));       // any instance of Coffee
verify(repo).findByName(eq("latte"));       // exact match
verify(repo).findByPrice(anyDouble());      // any double
verify(repo).search(anyString());           // any string
verify(repo).findAll(anyList());            // any list
verify(repo, times(2)).findByName("latte"); // called exactly 2 times
verify(repo, atLeastOnce()).findAll();      // called 1+ times
verify(repo, atMost(3)).save(any());        // called at most 3 times

Test Patterns

Well-structured tests are readable, maintainable, and trustworthy. Follow these patterns for consistency.

AAA Pattern (Arrange-Act-Assert)

The most common test structure. Set up inputs, execute the action, verify the result.

@Test
void shouldApplyBulkDiscount() {
    // Arrange
    var service = new PricingService();
    var order = new Order("latte", 10);

    // Act
    double total = service.calculate(order);

    // Assert
    assertEquals(40.50, total, 0.01);
}

Given-When-Then (BDD Style)

Behavior-driven style. Reads like a specification.

@Test
void loyalCustomerGetsDiscount() {
    // Given a loyal customer with 50+ orders
    Customer customer = new Customer("Alice");
    customer.setOrderCount(55);

    // When they place a new order
    Order order = service.createOrder(
        customer, "cappuccino"
    );

    // Then a 10% discount is applied
    assertEquals(0.10, order.getDiscount());
}

Test Naming: Name tests to describe behavior, not implementation. Prefer shouldRejectExpiredCoupon() over testCoupon3(). The name should tell you what is broken when it fails. Combine with @DisplayName for even clearer reports.

Avoid Testing Private Methods Directly

If you feel the need to test a private method, it usually means the class is doing too much. Extract the logic into a separate class with a public API and test that instead. Using reflection to access private methods makes tests brittle and tightly coupled to implementation details.

Integration Testing

Integration tests verify that components work together. Spring Boot provides excellent test infrastructure for loading application contexts and testing REST endpoints.

import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.*;

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class CoffeeApiIntegrationTest {

    @Autowired
    TestRestTemplate restTemplate;         // auto-configured HTTP client

    @MockBean
    PaymentGateway paymentGateway;         // replace real bean with mock

    @Test
    void shouldReturnMenuItems() {
        ResponseEntity<Coffee[]> response = restTemplate
            .getForEntity("/api/coffees", Coffee[].class);

        assertEquals(HttpStatus.OK, response.getStatusCode());
        assertTrue(response.getBody().length > 0);
    }

    @Test
    void shouldCreateOrder() {
        when(paymentGateway.charge(anyDouble())).thenReturn(true);

        OrderRequest request = new OrderRequest("latte", 2, Size.MEDIUM);
        ResponseEntity<OrderResult> response = restTemplate
            .postForEntity("/api/orders", request, OrderResult.class);

        assertEquals(HttpStatus.CREATED, response.getStatusCode());
        assertTrue(response.getBody().isSuccess());
    }

    @Test
    void shouldReturn404ForUnknownDrink() {
        ResponseEntity<String> response = restTemplate
            .getForEntity("/api/coffees/unicorn-frapp", String.class);

        assertEquals(HttpStatus.NOT_FOUND, response.getStatusCode());
    }
}
// Testcontainers — real databases in Docker for integration tests
import org.testcontainers.containers.PostgreSQLContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;

@Testcontainers
@SpringBootTest
class CoffeeRepositoryIT {

    @Container
    static PostgreSQLContainer<?> postgres =
        new PostgreSQLContainer<>("postgres:16-alpine")
            .withDatabaseName("coffeedb")
            .withUsername("barista")
            .withPassword("brew123");

    @DynamicPropertySource
    static void configureProperties(DynamicPropertyRegistry registry) {
        registry.add("spring.datasource.url", postgres::getJdbcUrl);
        registry.add("spring.datasource.username", postgres::getUsername);
        registry.add("spring.datasource.password", postgres::getPassword);
    }

    @Autowired
    CoffeeRepository coffeeRepo;

    @Test
    void shouldPersistAndRetrieveCoffee() {
        Coffee saved = coffeeRepo.save(new Coffee("espresso", 3.00));
        Optional<Coffee> found = coffeeRepo.findById(saved.getId());

        assertTrue(found.isPresent());
        assertEquals("espresso", found.get().getType());
    }
}

Test Slicing

Spring Boot offers slice annotations that load only part of the context: @WebMvcTest for controllers, @DataJpaTest for repositories, @JsonTest for serialization. These are faster than @SpringBootTest and should be preferred when you only need a specific layer.

11

Best Practices & Patterns

Naming conventions, SOLID principles, design patterns, and clean code habits. Write Java that your future self will thank you for.

Naming Conventions

Java has strong conventions enforced by community consensus and tooling. Follow them consistently for readable, idiomatic code.

Element Convention Example
Classes PascalCase, noun CoffeeOrder, PaymentService
Interfaces PascalCase, adjective or noun Serializable, CoffeeRepository
Methods camelCase, verb calculateTotal(), isValid()
Variables camelCase, descriptive orderCount, currentUser
Constants UPPER_SNAKE_CASE MAX_RETRIES, DEFAULT_SIZE
Packages all lowercase, reverse domain com.coffeeshop.orders
Enums PascalCase type, UPPER_SNAKE values Size.LARGE, Status.IN_PROGRESS
Type Parameters Single uppercase letter T (type), E (element), K/V (key/value)
Test Classes ClassNameTest CoffeeServiceTest
Test Methods shouldDescribeBehavior shouldRejectExpiredCoupon()

SOLID Principles

Five principles of object-oriented design that make software easier to understand, maintain, and extend.

S — Single Responsibility

A class should have only one reason to change. Each class handles one job.

// BAD — one class does everything
class OrderManager {
    void createOrder() { ... }
    void sendEmail() { ... }
    void generatePdf() { ... }
}

// GOOD — separate responsibilities
class OrderService { void create() { ... } }
class EmailService { void send() { ... } }
class PdfGenerator { void generate() { ... } }

O — Open/Closed

Open for extension, closed for modification. Add new behavior without changing existing code.

// Open for extension via new implementations
interface DiscountStrategy {
    double apply(double price);
}

class LoyaltyDiscount implements DiscountStrategy {
    public double apply(double price) {
        return price * 0.90;
    }
}

class SeasonalDiscount implements DiscountStrategy {
    public double apply(double price) {
        return price * 0.85;
    }
}
// Add new discounts without touching existing ones

L — Liskov Substitution

Subtypes must be substitutable for their base types without breaking behavior.

// GOOD — substitutable
class Coffee {
    double getPrice() { return 3.00; }
}

class PremiumCoffee extends Coffee {
    double getPrice() { return 5.00; } // OK
}

// BAD — breaks substitution
class FreeSample extends Coffee {
    double getPrice() {
        throw new UnsupportedOperationException();
        // Violates contract! Callers expect a price.
    }
}

I — Interface Segregation

No client should be forced to depend on methods it does not use. Prefer small, focused interfaces.

// BAD — fat interface
interface CoffeeMachine {
    void brew();
    void frothMilk();
    void grindBeans();
    void selfClean();
}

// GOOD — segregated interfaces
interface Brewer { void brew(); }
interface MilkFrother { void frothMilk(); }
interface Grinder { void grindBeans(); }

class EspressoMachine
    implements Brewer, Grinder {
    // Only implements what it needs
}

D — Dependency Inversion

High-level modules should not depend on low-level modules. Both should depend on abstractions.

// BAD — depends on concrete class
class OrderService {
    private MySqlOrderRepo repo =
        new MySqlOrderRepo();  // tightly coupled
}

// GOOD — depends on abstraction
class OrderService {
    private final OrderRepository repo;

    OrderService(OrderRepository repo) {
        this.repo = repo;  // injected
    }
}

// Easy to swap implementations
new OrderService(new MySqlOrderRepo());
new OrderService(new MongoOrderRepo());
new OrderService(new InMemoryOrderRepo());

Common Design Patterns

Battle-tested solutions to recurring design problems. Know when to reach for them and when to keep it simple.

Singleton (Enum-Based)

Ensure a class has exactly one instance. The enum approach is thread-safe, serialization-proof, and reflection-proof.

public enum AppConfig {
    INSTANCE;

    private final Properties props = new Properties();

    AppConfig() {
        try (var is = getClass().getResourceAsStream(
                "/config.properties")) {
            props.load(is);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    public String get(String key) {
        return props.getProperty(key);
    }
}
// Usage: AppConfig.INSTANCE.get("db.url")

Builder

Construct complex objects step by step. Essential when constructors have many parameters.

public class CoffeeOrder {
    private final String drink;
    private final Size size;
    private final boolean iced;
    private final List<String> extras;

    private CoffeeOrder(Builder b) {
        this.drink = b.drink;
        this.size = b.size;
        this.iced = b.iced;
        this.extras = List.copyOf(b.extras);
    }

    public static class Builder {
        private final String drink;       // required
        private Size size = Size.MEDIUM;  // default
        private boolean iced = false;
        private List<String> extras = new ArrayList<>();

        public Builder(String drink) {
            this.drink = drink;
        }
        public Builder size(Size s) {
            this.size = s; return this;
        }
        public Builder iced() {
            this.iced = true; return this;
        }
        public Builder extra(String e) {
            this.extras.add(e); return this;
        }
        public CoffeeOrder build() {
            return new CoffeeOrder(this);
        }
    }
}

// Usage
var order = new CoffeeOrder.Builder("latte")
    .size(Size.LARGE)
    .iced()
    .extra("oat milk")
    .extra("vanilla")
    .build();

Factory Method

Create objects without specifying the exact class. Let subclasses or a method decide which type to instantiate.

public interface Coffee {
    String describe();
    double getPrice();
}

public class CoffeeFactory {
    public static Coffee create(String type) {
        return switch (type.toLowerCase()) {
            case "espresso" -> new Espresso();
            case "latte"    -> new Latte();
            case "mocha"    -> new Mocha();
            default -> throw new IllegalArgumentException(
                "Unknown type: " + type);
        };
    }
}

// Usage — caller doesn't know concrete class
Coffee drink = CoffeeFactory.create("latte");

Strategy

Define a family of algorithms, encapsulate each one, and make them interchangeable at runtime.

interface BrewStrategy {
    Coffee brew(String bean, int ml);
}

class PourOver implements BrewStrategy {
    public Coffee brew(String bean, int ml) {
        // slow, precise extraction
        return new Coffee(bean, ml, "pour-over");
    }
}

class FrenchPress implements BrewStrategy {
    public Coffee brew(String bean, int ml) {
        // full immersion, bold flavor
        return new Coffee(bean, ml, "french-press");
    }
}

class Barista {
    private BrewStrategy strategy;

    void setStrategy(BrewStrategy s) {
        this.strategy = s;
    }

    Coffee makeCoffee(String bean) {
        return strategy.brew(bean, 250);
    }
}

// Swap algorithms at runtime
barista.setStrategy(new PourOver());
barista.setStrategy(new FrenchPress());

Observer

Define a one-to-many dependency so that when one object changes state, all dependents are notified.

interface OrderListener {
    void onOrderPlaced(Order order);
}

class OrderService {
    private final List<OrderListener> listeners
        = new ArrayList<>();

    void addListener(OrderListener l) {
        listeners.add(l);
    }

    void placeOrder(Order order) {
        // ... process order ...
        listeners.forEach(l ->
            l.onOrderPlaced(order));
    }
}

// Register observers
orderService.addListener(emailNotifier);
orderService.addListener(inventoryUpdater);
orderService.addListener(analyticsTracker);

Adapter

Bridge between two incompatible interfaces. Wrap an existing class to match the interface your code expects.

// Existing legacy API
class OldPaymentSystem {
    boolean processPayment(
        String cardNum, int cents) {
        // legacy implementation
        return true;
    }
}

// Your interface
interface PaymentGateway {
    boolean charge(BigDecimal amount);
}

// Adapter bridges the gap
class LegacyPaymentAdapter
        implements PaymentGateway {
    private final OldPaymentSystem legacy;
    private final String cardNumber;

    LegacyPaymentAdapter(
        OldPaymentSystem legacy, String card) {
        this.legacy = legacy;
        this.cardNumber = card;
    }

    public boolean charge(BigDecimal amount) {
        int cents = amount
            .multiply(BigDecimal.valueOf(100))
            .intValue();
        return legacy.processPayment(
            cardNumber, cents);
    }
}

Clean Code Tips

Practical habits that make Java code easier to read, test, and maintain.

// 1. Prefer immutability — use final, unmodifiable collections, records
final var menu = List.of("Espresso", "Latte", "Mocha");         // unmodifiable
final var prices = Map.of("Espresso", 3.0, "Latte", 4.5);      // unmodifiable
record Order(String drink, int qty) {}                           // immutable by default

// 2. Return empty collections, not null
public List<Coffee> findByRoast(String roast) {
    List<Coffee> results = repo.query(roast);
    return results != null ? results : List.of();   // never return null
}

// 3. Use Optional for return types, NEVER for parameters or fields
public Optional<Coffee> findById(long id) {
    return Optional.ofNullable(repo.get(id));
}

// BAD — don't use Optional as a parameter
void process(Optional<String> name) { ... }         // never do this

// 4. Prefer composition over inheritance
class CoffeeShop {
    private final BrewStrategy brewer;               // HAS-A, not IS-A
    private final PaymentGateway payments;

    CoffeeShop(BrewStrategy brewer, PaymentGateway payments) {
        this.brewer = brewer;
        this.payments = payments;
    }
}

// 5. Keep methods short — single level of abstraction
public OrderResult processOrder(OrderRequest req) {
    validate(req);                       // one thing
    Coffee coffee = brew(req);           // one thing
    double total = calculateTotal(req);  // one thing
    charge(req.customer(), total);       // one thing
    return new OrderResult(coffee, total);
}

// 6. Use meaningful names
// BAD
int d;                    // days? distance? duration?
List<String> list1;       // what's in the list?
void process(Object o);   // process what? how?

// GOOD
int daysSinceLastOrder;
List<String> activeCustomerEmails;
void sendOrderConfirmation(Order order);

Code Style

Formatting conventions, import ordering, and Javadoc guidelines followed by most Java teams.

// Import ordering (Google Java Style)
// 1. All static imports in a single block
// 2. All non-static imports in a single block
// Separated by a blank line, each group alphabetical

import static org.junit.jupiter.api.Assertions.*;    // static first
import static org.mockito.Mockito.*;

import java.util.List;                                // java.*
import java.util.Optional;
import java.util.stream.Collectors;

import org.springframework.stereotype.Service;        // third-party

import com.coffeeshop.model.Coffee;                   // project imports
import com.coffeeshop.repository.CoffeeRepository;
// Javadoc — write for public APIs
/**
 * Brews a coffee with the specified parameters.
 *
 * <p>The brewing process grinds the beans, heats water to the
 * optimal temperature, and extracts for the configured duration.
 *
 * @param beanType   the coffee bean variety (e.g., "arabica")
 * @param size       the cup size, must not be {@code null}
 * @return           a freshly brewed {@link Coffee} instance
 * @throws BrewException         if the machine encounters an error
 * @throws IllegalArgumentException if beanType is blank
 * @since 2.1.0
 * @see Coffee
 * @see BrewStrategy
 */
public Coffee brew(String beanType, Size size) throws BrewException {
    // ...
}

// Inline tags
/** Uses {@code Map<String, List<Coffee>>} internally. */
/** See {@link CoffeeFactory#create(String)} for alternatives. */
/** @deprecated Use {@link #brew(String, Size)} instead. */

Formatting tools: Use google-java-format or spotless to enforce style automatically. Configure in Maven/Gradle so formatting runs on every build. This eliminates style debates in code reviews and keeps diffs clean.

12

Ecosystem & Frameworks

Spring Boot, Jakarta EE, popular libraries, HTTP clients, database access, and essential tools. The Java ecosystem in one place.

Spring Boot

The dominant framework for building Java applications. Spring Boot auto-configures Spring based on your dependencies, so you can focus on business logic instead of boilerplate.

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.bind.annotation.*;
import org.springframework.stereotype.Service;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;

// Main application class — scans for components in this package and below
@SpringBootApplication
public class CoffeeShopApplication {
    public static void main(String[] args) {
        SpringApplication.run(CoffeeShopApplication.class, args);
    }
}

// REST controller — handles HTTP requests
@RestController
@RequestMapping("/api/coffees")
public class CoffeeController {

    private final CoffeeService service;

    @Autowired  // constructor injection (preferred)
    public CoffeeController(CoffeeService service) {
        this.service = service;
    }

    @GetMapping
    public List<Coffee> findAll() {
        return service.findAll();
    }

    @GetMapping("/{id}")
    public Coffee findById(@PathVariable Long id) {
        return service.findById(id);
    }

    @PostMapping
    @ResponseStatus(HttpStatus.CREATED)
    public Coffee create(@RequestBody @Valid CoffeeRequest req) {
        return service.create(req);
    }

    @DeleteMapping("/{id}")
    @ResponseStatus(HttpStatus.NO_CONTENT)
    public void delete(@PathVariable Long id) {
        service.delete(id);
    }
}

// Service layer — business logic
@Service
public class CoffeeService {

    private final CoffeeRepository repo;

    @Value("${coffee.default-size:MEDIUM}")   // reads from application.properties
    private String defaultSize;

    public CoffeeService(CoffeeRepository repo) {
        this.repo = repo;
    }

    public List<Coffee> findAll() {
        return repo.findAll();
    }

    public Coffee findById(Long id) {
        return repo.findById(id)
            .orElseThrow(() -> new NotFoundException("Coffee not found: " + id));
    }
}

// Repository — data access (Spring Data JPA generates the implementation)
@Repository
public interface CoffeeRepository extends JpaRepository<Coffee, Long> {
    List<Coffee> findByRoast(String roast);                   // derived query
    Optional<Coffee> findByNameIgnoreCase(String name);        // derived query

    @Query("SELECT c FROM Coffee c WHERE c.price < :max")
    List<Coffee> findAffordable(@Param("max") double max);    // custom JPQL
}
Annotation Layer Purpose
@SpringBootApplication App entry Enables auto-config, component scanning, configuration
@RestController Web Combines @Controller + @ResponseBody (JSON by default)
@Service Business Marks a service component for DI scanning
@Repository Data Marks a data access component, adds exception translation
@Autowired Any Injects a dependency (prefer constructor injection)
@Value("${key}") Any Injects a value from application.properties/yml
@Component Any Generic Spring-managed bean (parent of @Service, @Repository)
# application.properties
server.port=8080
spring.application.name=coffee-shop

# Database
spring.datasource.url=jdbc:postgresql://localhost:5432/coffeedb
spring.datasource.username=barista
spring.datasource.password=brew123
spring.jpa.hibernate.ddl-auto=validate

# Logging
logging.level.com.coffeeshop=DEBUG
logging.level.org.springframework.web=INFO
# application.yml (alternative format — same settings)
server:
  port: 8080

spring:
  application:
    name: coffee-shop
  datasource:
    url: jdbc:postgresql://localhost:5432/coffeedb
    username: barista
    password: brew123
  jpa:
    hibernate:
      ddl-auto: validate

logging:
  level:
    com.coffeeshop: DEBUG
    org.springframework.web: INFO

Jakarta EE

The official enterprise Java platform (formerly Java EE). A set of specifications implemented by application servers like WildFly, Payara, and Open Liberty.

Spec Purpose Spring Equivalent
Jakarta Servlet HTTP request/response handling Spring MVC (DispatcherServlet)
Jakarta Persistence (JPA) ORM / database mapping Spring Data JPA (uses JPA under the hood)
Jakarta CDI Dependency injection Spring IoC container
Jakarta RESTful Web Services REST APIs (JAX-RS) Spring MVC @RestController
Jakarta Bean Validation @NotNull, @Size, @Email, etc. Spring uses Bean Validation directly
Jakarta JSON Processing JSON parsing and generation Jackson (default in Spring Boot)

Spring vs Jakarta EE: Spring Boot dominates the job market and startup scene with its opinionated, rapid-development approach. Jakarta EE is common in large enterprises and government. Both use JPA for persistence, Bean Validation for constraints, and similar annotation-driven models. Skills transfer well between them.

Popular Libraries

Essential libraries that most Java projects depend on. Know what they do and when to reach for them.

Lombok

Reduces boilerplate with compile-time annotations. Generates getters, setters, constructors, builders, and more.

@Data               // getters + setters + toString + equals + hashCode
@Builder             // fluent builder pattern
@Slf4j               // private static final Logger log = ...
@NoArgsConstructor
@AllArgsConstructor
public class Coffee {
    private String name;
    private double price;
    private Size size;
}

// Usage with builder
Coffee c = Coffee.builder()
    .name("Latte")
    .price(4.50)
    .size(Size.MEDIUM)
    .build();

Jackson

The standard JSON library for Java. Used by Spring Boot by default. Serializes/deserializes Java objects to/from JSON.

ObjectMapper mapper = new ObjectMapper();

// Object to JSON
String json = mapper.writeValueAsString(coffee);

// JSON to Object
Coffee c = mapper.readValue(json, Coffee.class);

// Annotations
@JsonProperty("drink_name")    // custom field name
@JsonIgnore                    // exclude from JSON
@JsonFormat(pattern = "yyyy-MM-dd")
private LocalDate createdAt;

SLF4J + Logback

The standard logging stack. SLF4J is the API facade; Logback is the default implementation in Spring Boot.

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

class OrderService {
    private static final Logger log =
        LoggerFactory.getLogger(OrderService.class);

    void process(Order order) {
        log.info("Processing order: {}", order.getId());
        log.debug("Order details: {}", order);
        log.warn("Low stock for: {}", order.getDrink());
        log.error("Payment failed", exception);
    }
}
// With Lombok: just add @Slf4j to the class

Apache Commons

Battle-tested utility libraries for strings, collections, I/O, and more.

// commons-lang3
StringUtils.isBlank(str);          // null-safe
StringUtils.capitalize("latte");   // "Latte"
StringUtils.abbreviate(text, 50);  // truncate

// commons-collections4
CollectionUtils.isEmpty(list);     // null-safe
CollectionUtils.union(a, b);

// commons-io
FileUtils.readFileToString(file, UTF_8);
IOUtils.copy(inputStream, outputStream);

MapStruct

Compile-time object mapper. Generates type-safe mapping code between DTOs and entities. No reflection overhead.

@Mapper(componentModel = "spring")
public interface CoffeeMapper {

    CoffeeDto toDto(Coffee entity);
    Coffee toEntity(CoffeeDto dto);

    @Mapping(source = "name", target = "drinkName")
    @Mapping(target = "id", ignore = true)
    Coffee fromRequest(CoffeeRequest req);
}
// MapStruct generates the implementation at compile time

Google Guava

Google's core libraries for Java. Immutable collections, caching, hashing, string utilities, and more.

// Immutable collections
ImmutableList<String> menu =
    ImmutableList.of("Espresso", "Latte");

// Multimap — one key, multiple values
Multimap<String, Coffee> byRoast =
    ArrayListMultimap.create();
byRoast.put("dark", espresso);
byRoast.put("dark", mocha);

// Cache with auto-loading
LoadingCache<String, Coffee> cache =
    CacheBuilder.newBuilder()
        .maximumSize(100)
        .expireAfterWrite(Duration.ofMinutes(10))
        .build(CacheLoader.from(repo::findByName));

HTTP Clients

Java offers multiple HTTP client options depending on your framework and requirements.

// java.net.http.HttpClient (Java 11+) — no dependencies needed
HttpClient client = HttpClient.newHttpClient();

HttpRequest request = HttpRequest.newBuilder()
    .uri(URI.create("https://api.coffeeshop.com/menu"))
    .header("Accept", "application/json")
    .GET()
    .build();

HttpResponse<String> response = client.send(
    request, HttpResponse.BodyHandlers.ofString()
);

System.out.println(response.statusCode());  // 200
System.out.println(response.body());        // JSON string

// Async request
CompletableFuture<HttpResponse<String>> future = client.sendAsync(
    request, HttpResponse.BodyHandlers.ofString()
);
future.thenAccept(r -> System.out.println(r.body()));
// Spring RestClient (Spring 6.1+ / Boot 3.2+) — modern, fluent, synchronous
@Service
class MenuClient {
    private final RestClient restClient;

    MenuClient(RestClient.Builder builder) {
        this.restClient = builder
            .baseUrl("https://api.coffeeshop.com")
            .build();
    }

    List<Coffee> getMenu() {
        return restClient.get()
            .uri("/menu")
            .retrieve()
            .body(new ParameterizedTypeReference<>() {});
    }

    Coffee getById(Long id) {
        return restClient.get()
            .uri("/menu/{id}", id)
            .retrieve()
            .body(Coffee.class);
    }
}
Client Since Style Best For
java.net.http.HttpClient Java 11 Sync + async No-dependency projects, scripts
Spring RestClient Spring 6.1 Sync, fluent Spring Boot apps (recommended)
Spring WebClient Spring 5 Reactive (non-blocking) WebFlux / reactive apps
Spring RestTemplate Spring 3 Sync, template Legacy code (in maintenance mode)

Database Access

From raw JDBC to Spring Data JPA. Each layer adds convenience while the one below it handles the heavy lifting.

// JDBC — low-level, full control
try (Connection conn = DriverManager.getConnection(
        "jdbc:postgresql://localhost:5432/coffeedb", "barista", "brew123");
     PreparedStatement ps = conn.prepareStatement(
        "SELECT name, price FROM coffees WHERE roast = ?")) {

    ps.setString(1, "dark");
    try (ResultSet rs = ps.executeQuery()) {
        while (rs.next()) {
            String name = rs.getString("name");
            double price = rs.getDouble("price");
            System.out.println(name + ": $" + price);
        }
    }
}
// Verbose but gives full control over SQL and connection lifecycle
// JPA Entity — maps a Java class to a database table
import jakarta.persistence.*;

@Entity
@Table(name = "coffees")
public class Coffee {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false, length = 100)
    private String name;

    @Column(nullable = false)
    private double price;

    @Enumerated(EnumType.STRING)
    private Roast roast;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "shop_id")
    private Shop shop;

    // constructors, getters, setters
}
// Spring Data JPA Repository — generates queries from method names
@Repository
public interface CoffeeRepository extends JpaRepository<Coffee, Long> {

    // Derived queries — Spring generates SQL from the method name
    List<Coffee> findByRoast(Roast roast);
    List<Coffee> findByPriceLessThan(double maxPrice);
    Optional<Coffee> findByNameIgnoreCase(String name);
    List<Coffee> findByRoastAndPriceBetween(Roast roast, double min, double max);
    boolean existsByName(String name);
    long countByRoast(Roast roast);

    // Custom JPQL query
    @Query("SELECT c FROM Coffee c WHERE c.price = (SELECT MIN(c2.price) FROM Coffee c2)")
    Optional<Coffee> findCheapest();

    // Native SQL query
    @Query(value = "SELECT * FROM coffees WHERE name ILIKE %:search%", nativeQuery = true)
    List<Coffee> search(@Param("search") String search);
}

// Usage — Spring auto-implements the interface
@Service
class CoffeeService {
    private final CoffeeRepository repo;

    CoffeeService(CoffeeRepository repo) { this.repo = repo; }

    public List<Coffee> findDarkRoasts() {
        return repo.findByRoast(Roast.DARK);     // no SQL to write
    }

    public Coffee save(Coffee coffee) {
        return repo.save(coffee);                 // insert or update
    }

    public void delete(Long id) {
        repo.deleteById(id);
    }
}

Useful Tools

Essential tools for packaging, performance analysis, and native compilation in the Java ecosystem.

Tool Since Purpose
jlink Java 9 Creates custom minimal JRE with only the modules your app needs. Reduces image size from ~300MB to ~30MB.
jpackage Java 14 Creates native installers (.msi, .dmg, .deb, .rpm) for Java applications. Bundles a custom JRE.
GraalVM native-image GraalVM Ahead-of-time compilation to native executables. Instant startup (~10ms), low memory footprint. Ideal for CLIs, serverless, containers.
VisualVM External Visual monitoring and profiling. CPU/memory usage, thread dumps, heap analysis, GC monitoring. Connect to running JVMs.
JMH OpenJDK Java Microbenchmark Harness. Accurate benchmarking with warmup, JIT-aware measurement. The only reliable way to benchmark Java code.
jconsole Java 5 Built-in JMX monitoring tool. Memory, threads, classes, MBeans. Ships with the JDK.
# jlink — build a minimal custom JRE
jlink --module-path $JAVA_HOME/jmods \
      --add-modules java.base,java.sql,java.net.http \
      --output custom-jre \
      --strip-debug \
      --compress zip-6

# Result: a ~30MB JRE instead of the full ~300MB JDK
./custom-jre/bin/java -jar barista-api.jar
# jpackage — create a native installer
jpackage --type dmg \
         --input target/ \
         --main-jar barista-api-1.0.0.jar \
         --main-class com.coffeeshop.App \
         --name "Barista" \
         --app-version "1.0.0" \
         --icon assets/coffee-icon.icns

# Produces: Barista-1.0.0.dmg (macOS)
# Also supports: --type msi (Windows), --type deb/rpm (Linux)
// JMH Benchmark example
import org.openjdk.jmh.annotations.*;
import java.util.concurrent.TimeUnit;

@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@State(Scope.Thread)
@Warmup(iterations = 3, time = 1)
@Measurement(iterations = 5, time = 1)
public class StringBenchmark {

    private String data = "espresso,latte,mocha,cappuccino";

    @Benchmark
    public String[] splitWithRegex() {
        return data.split(",");
    }

    @Benchmark
    public List<String> splitWithStringTokenizer() {
        var tokens = new StringTokenizer(data, ",");
        var list = new ArrayList<String>();
        while (tokens.hasMoreTokens()) {
            list.add(tokens.nextToken());
        }
        return list;
    }

    // Run: java -jar benchmarks.jar
    // Results show avg time per operation with error margins
}

GraalVM Native Image

Spring Boot 3+ has first-class support for GraalVM native compilation. Add the org.graalvm.buildtools.native Gradle plugin or use spring-boot-starter-parent with the native Maven profile. Build with ./gradlew nativeCompile or mvn -Pnative package. Startup drops from seconds to milliseconds, but build times increase significantly.