Write once, run everywhere — fueled by coffee
01
The essential Java cheat sheet. Data types, operators, control flow, and common patterns all in one place.
| 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'; |
// 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;
| 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; |
if (score >= 90) {
grade = "A";
} else if (score >= 80) {
grade = "B";
} else {
grade = "C";
}
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 (int i = 0; i < 10; i++) {
System.out.println(i);
}
String[] names = {"Ada", "Bob"};
for (String name : names) {
System.out.println(name);
}
while (running) {
process();
}
do {
retry();
} while (!success);
try {
readFile("data.txt");
} catch (IOException e) {
log.error("Read failed", e);
} finally {
cleanup();
}
| 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) |
// 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]"
Entry point for every Java application
public class App {
public static void main(String[] args) {
System.out.println("Hello, Java!");
}
}
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;
}
}
Contract with default methods
public interface Brewable {
void brew();
int getStrength();
default String describe() {
return "Strength: " + getStrength();
}
}
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
Getting your Java development environment ready. From choosing a JDK to writing your first program.
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.
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
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
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
# 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.
| 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) |
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.
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
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
The building blocks of Java. Types, variables, operators, control flow, strings, and arrays in depth.
Java has two categories of types. Primitives hold values directly on the stack; reference types hold a pointer to an object on the heap.
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
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.
// 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";
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.
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)
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";
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
// 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";
};
// 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"
// 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();
}
};
// 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 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");
// 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
""";
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"
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]);
// 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
Classes, inheritance, interfaces, polymorphism, and composition. The pillars of Java's design philosophy.
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();
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.
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]
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
}
}
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;
}
Cannot be overridden in subclasses
public class Payment {
// Subclasses cannot change this logic
public final double calculateTax(
double amount) {
return amount * 0.08;
}
}
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());
}
}
// 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;
| 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();
}
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);
}
}
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
}
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.
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();
}
}
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();
}
}
final KeywordCan 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
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;
}
}
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;
}
}
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
Java's collection framework, generic types, the Streams API, and Optional. The toolkit for working with groups of objects.
The Java Collections Framework provides a unified architecture for storing and manipulating groups of objects.
Root interface. Enables for-each loops on any collection.
Ordered, indexed, allows duplicates. Implementations: ArrayList, LinkedList, CopyOnWriteArrayList.
No duplicates. Implementations: HashSet (unordered), TreeSet (sorted), LinkedHashSet (insertion order).
FIFO or priority ordering. Implementations: PriorityQueue, ArrayDeque, LinkedList.
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.
| 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);
| 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
| 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)
);
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()
);
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"
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
}
// 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.
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
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"]
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
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.
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
Graceful error handling, checked and unchecked exceptions, and file I/O with both classic java.io and modern java.nio.
All exceptions descend from Throwable. Understanding the hierarchy determines how and when you must handle errors.
The root. Everything that can be thrown and caught extends this.
Serious JVM problems you should NOT catch. OutOfMemoryError, StackOverflowError, etc.
Must be caught or declared with throws. IOException, SQLException, ClassNotFoundException.
Not required to catch. Programming errors. NullPointerException, IllegalArgumentException, IndexOutOfBoundsException.
// 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
| 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 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 — throws an exceptionUsed 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 exceptionsUsed 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);
}
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();
}
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 |
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
Multithreaded programming in Java. From basic thread creation to lock-free atomic operations and the modern CompletableFuture API.
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:
Thread object created but start() not yet called. No OS thread allocated.
After start(). Running or ready to run. The OS scheduler decides when it actually executes.
Waiting to acquire a monitor lock (e.g., trying to enter a synchronized block held by another thread).
Indefinitely waiting for another thread. Caused by wait(), join(), or LockSupport.park().
Waiting with a timeout. Caused by sleep(ms), wait(ms), or join(ms).
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();
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.
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 is like Runnable but returns a result and can throw checked exceptions. Future represents the pending result.
No return value. Cannot throw checked exceptions. Use for fire-and-forget tasks.
Runnable task = () -> {
System.out.println("Working");
};
executor.execute(task);
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());
}
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));
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
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
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
Java has evolved rapidly since the six-month release cadence began. Records, sealed classes, pattern matching, virtual threads, and more.
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 |
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.
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();
};
}
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 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
};
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)
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
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);
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.
The JVM now tells you exactly which variable was null in a chain, instead of leaving you guessing.
order.getCustomer().getAddress().getCity()
// Exception:
// NullPointerException
// at App.main(App.java:42)
// Which was null? order? customer?
// address? You have to debug it.
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.
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
Maven and Gradle automate compiling, testing, packaging, and dependency management. Every production Java project uses one or the other.
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
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 uses a Groovy or Kotlin DSL instead of XML. It is faster than Maven (incremental builds, build cache) and more flexible.
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()
}
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 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
}
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.
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.
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
JUnit 5, Mockito, parameterized tests, and integration testing. Write tests that catch bugs before your users do.
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 |
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
);
}
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 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
Well-structured tests are readable, maintainable, and trustworthy. Follow these patterns for consistency.
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);
}
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 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
Naming conventions, SOLID principles, design patterns, and clean code habits. Write Java that your future self will thank you for.
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() |
Five principles of object-oriented design that make software easier to understand, maintain, and extend.
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() { ... } }
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
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.
}
}
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
}
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());
Battle-tested solutions to recurring design problems. Know when to reach for them and when to keep it simple.
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")
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();
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");
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());
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);
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);
}
}
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);
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
Spring Boot, Jakarta EE, popular libraries, HTTP clients, database access, and essential tools. The Java ecosystem in one place.
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
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.
Essential libraries that most Java projects depend on. Know what they do and when to reach for them.
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();
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;
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
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);
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'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));
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) |
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);
}
}
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.