Getting Started
Hello World
Every Java program starts from main(). The file name must match the public class name (Main.java → Main.class). javac compiles to bytecode (.class), java runs it on the JVM. System.out.println prints to stdout; printf supports format specifiers (%s, %d, %f, %n for newline).
// Main.java
public class Main {
public static void main(String[] args) {
System.out.println("Hello, World!");
System.out.printf("Name: %s, Age: %d%n", "Alice", 30);
}
}
// Compile: javac Main.java -> Main.class
// Run: java Main
// Package: java -cp . com.example.Main
// Every Java program needs:
// 1. A class (public class, file name must match)
// 2. A main method: public static void main(String[] args)Variables & Primitive Types
Java has 8 primitive types (int, long, double, float, boolean, char, byte, short) and reference types (String, arrays, objects). Use 'final' for constants. 'var' (Java 10+) infers the type at compile time — use for local variables with obvious types. Underscores in numbers (100_000) improve readability.
// Primitive types (8 total)
int age = 30; // 32-bit integer
long bigNum = 100_000L; // 64-bit integer
double price = 19.99; // 64-bit float (default for decimals)
float pi = 3.14f; // 32-bit float
boolean active = true; // true/false
char grade = 'A'; // 16-bit Unicode character
byte b = 127; // 8-bit signed
short s = 32767; // 16-bit signed
// Reference types
String name = "Alice"; // Object (not primitive)
int[] nums = {1, 2, 3}; // Array object
// Constants
final double PI = 3.14159; // can't be reassigned
// var (Java 10+, local type inference)
var count = 42; // inferred as int
var list = new ArrayList<String>(); // inferred as ArrayList<String>Wrapper Classes & Boxing
Wrapper classes (Integer, Double, Boolean, etc.) are object versions of primitives. Autoboxing/unboxing converts automatically. Integer caches values -128 to 127, so == works for small numbers but fails for larger ones — always use .equals(). Wrappers are needed for Collections (which can't hold primitives).
// Wrapper classes (Object versions of primitives)
Integer wrapped = Integer.valueOf(42); // explicit
Integer auto = 42; // autoboxing
int unboxed = auto; // unboxing
// Useful methods
int max = Integer.MAX_VALUE; // 2147483647
String bin = Integer.toBinaryString(42);
int parsed = Integer.parseInt("42");
String s = String.valueOf(42);
// Other wrappers: Double, Boolean, Character, Long, Float
Double d = 3.14;
Boolean b = Boolean.TRUE;
Character c = 'A';
// Be careful with == on wrappers
Integer a = 127, b2 = 127; // a == b2: true (cached)
Integer x = 128, y = 128; // x == y: false (not cached)
// Always use .equals() for Integer comparisonPackages & Imports
Packages organize classes and prevent naming conflicts. Convention: reverse domain name (com.example.app). Import specific classes or use wildcards (*). Static imports bring in constants and methods (Math.PI, Math.sqrt). Fully qualified names work without imports but are verbose. The java.lang package is auto-imported.
// Package declaration (must be first line)
package com.example.app;
// Import specific class
import java.util.List;
import java.util.ArrayList;
// Import all classes in a package
import java.util.*;
// Static import (for static members)
import static java.lang.Math.PI;
import static java.lang.Math.sqrt;
// Usage
double area = PI * 5 * 5;
double root = sqrt(16);
// Fully qualified name (no import needed)
java.time.LocalDate today = java.time.LocalDate.now();
// Package naming convention: reverse domain
// com.google.gson, org.apache.commons, io.netty.channelInput & Output
System.out (stdout), System.err (stderr), System.in (stdin). Scanner is the easiest way to read console input — it parses tokens (nextInt, nextDouble, nextLine). Always close Scanner to release resources. Command-line arguments are in args[] (args[0] is the first argument, not the program name like in C).
import java.util.Scanner;
// Console output
System.out.println("Hello"); // with newline
System.out.print("No newline"); // without newline
System.out.printf("Pi: %.2f%n", 3.14159); // formatted
// Console input with Scanner
Scanner scanner = new Scanner(System.in);
System.out.print("Enter name: ");
String name = scanner.nextLine();
System.out.print("Enter age: ");
int age = scanner.nextInt();
System.out.printf("Hi %s, age %d%n", name, age);
scanner.close(); // always close
// Command-line arguments
// java Main arg1 arg2
// args[0] = "arg1", args[1] = "arg2"Strings & Formatting
String Methods
Strings are immutable — methods return new strings. Always use .equals() for content comparison (== compares references). compareTo() returns negative/zero/positive for ordering (useful for sorting). split() returns a String[]. For mutable strings, use StringBuilder.
String s = "Hello, World";
// Length and access
int len = s.length(); // 12
char c = s.charAt(0); // 'H'
// Comparison
s.equals("Hello, World"); // true (content comparison)
s.equalsIgnoreCase("hello, world"); // true
s.compareTo("Apple"); // positive (s > "Apple")
"abc".compareTo("abd"); // negative
// Search
s.indexOf("World"); // 7 (-1 if not found)
s.lastIndexOf("l"); // 10
s.contains("World"); // true
s.startsWith("Hello"); // true
s.endsWith("World"); // true
// Extract
s.substring(7); // "World"
s.substring(0, 5); // "Hello"
// Transform
s.toUpperCase(); // "HELLO, WORLD"
s.toLowerCase(); // "hello, world"
s.replace("o", "0"); // "Hell0, W0rld"
s.trim(); // remove whitespace
s.split(", "); // ["Hello", "World"]StringBuilder & Concatenation
String concatenation with + creates a new String each time (inefficient in loops). StringBuilder is mutable and efficient for building strings incrementally. StringBuffer is the thread-safe version (rarely needed). String.join() combines with a delimiter. Java 11+ adds repeat() for string multiplication.
// String concatenation (creates new String each time)
String s = "Hello" + ", " + "World";
String formatted = String.format("%s is %d", "Alice", 30);
// StringBuilder (mutable, efficient for many concatenations)
StringBuilder sb = new StringBuilder();
sb.append("Hello");
sb.append(", ");
sb.append("World");
sb.insert(5, " there");
sb.delete(5, 11);
sb.reverse();
String result = sb.toString();
// StringBuffer (thread-safe, slower than StringBuilder)
StringBuffer sbf = new StringBuffer("thread-safe");
// Join strings
String joined = String.join(", ", "a", "b", "c"); // "a, b, c"
// Repeat (Java 11+)
String repeated = "ab".repeat(3); // "ababab"String Formatting
printf/format uses C-style format specifiers: %d (int), %f (float), %s (string), %c (char), %b (boolean), %x (hex). Width (%5d), left-align (%-5d), zero-pad (%05d), precision (%.2f). %n is the platform newline. Text blocks (Java 15+) with triple quotes enable multiline strings without escaping.
// printf / format specifiers
System.out.printf("Int: %d%n", 42);
System.out.printf("Float: %.2f%n", 3.14159); // 3.14
System.out.printf("String: %s%n", "hello");
System.out.printf("Char: %c%n", 'A');
System.out.printf("Bool: %b%n", true);
System.out.printf("Hex: %x%n", 255); // ff
System.out.printf("Octal: %o%n", 8); // 10
// Width and padding
System.out.printf("[%5d]%n", 42); // [ 42]
System.out.printf("[%-5d]%n", 42); // [42 ]
System.out.printf("[%05d]%n", 42); // [00042]
System.out.printf("[%8.2f]%n", 3.14); // [ 3.14]
// String.format returns a String
String s = String.format("Name: %s, Age: %d", "Alice", 30);
// Text blocks (Java 15+)
String json = """
{
"name": "Alice",
"age": 30
}
""";Regular Expressions
Java regex uses Pattern (compiled) and Matcher (applied to input). String methods (matches, split, replaceAll) are convenient shortcuts. Backslashes must be doubled in Java string literals (\\d for \d). Groups are captured with parentheses and referenced as $1, $2 in replacements. Always compile patterns once if used repeatedly.
import java.util.regex.*;
// String methods
"hello123".matches("[a-z]+\d+"); // true
"a,b,c".split(","); // ["a", "b", "c"]
"hello".replaceAll("l", "L"); // "heLLo"
// Pattern and Matcher
Pattern p = Pattern.compile("\\d{4}-\\d{2}-\\d{2}");
Matcher m = p.matcher("Date: 2024-01-15");
if (m.find()) {
System.out.println(m.group()); // "2024-01-15"
}
// Find all matches
while (m.find()) {
System.out.println(m.group());
}
// Replace with regex
String result = "2024-01-15".replaceAll(
"(\\d{4})-(\\d{2})-(\\d{2})",
"$3/$2/$1"); // "15/01/2024"
// Common patterns
String email = "^[\\w.]+@[\\w.]+\\.\\w+$";
String phone = "^\\d{3}-\\d{4}$";Numbers & Math
Math class provides static math functions. Math.random() returns 0.0-1.0. For more control, use java.util.Random (seedable) or java.security.SecureRandom (cryptographic). Integer/Double have static utility methods. Be careful with floating-point precision — use BigDecimal for financial calculations.
// Math class
double sqrt = Math.sqrt(16); // 4.0
double pow = Math.pow(2, 10); // 1024.0
int abs = Math.abs(-5); // 5
int max = Math.max(3, 7); // 7
int min = Math.min(3, 7); // 3
double rounded = Math.round(3.7); // 4
double ceil = Math.ceil(3.1); // 4.0
double floor = Math.floor(3.9); // 3.0
double random = Math.random(); // 0.0 to 1.0
// Constants
double pi = Math.PI; // 3.14159...
double e = Math.E; // 2.71828...
// Integer/Long methods
int sum = Integer.sum(3, 4); // 7
int max2 = Integer.max(3, 7); // 7
// Rounding modes
double r = Math.round(3.5); // 4 (round half up)
double r2 = Math.floor(3.5 + 0.5); // alternative
// Random (java.util.Random)
import java.util.Random;
Random rand = new Random();
int n = rand.nextInt(100); // 0-99
double d = rand.nextDouble(); // 0.0-1.0
boolean b = rand.nextBoolean();Control Flow
If / Else
Java if/else works like C/C++. Conditions must be boolean — no truthy/falsy like JavaScript (0 and non-empty strings are NOT truthy). The ternary operator (cond ? a : b) is an expression, not a statement. Use braces even for single-line bodies (code style best practice).
int score = 85;
if (score >= 90) {
System.out.println("A");
} else if (score >= 80) {
System.out.println("B");
} else if (score >= 70) {
System.out.println("C");
} else {
System.out.println("F");
}
// Ternary operator
String grade = score >= 60 ? "Pass" : "Fail";
// Nested if
if (score >= 60) {
if (score >= 90) {
System.out.println("Excellent");
}
}
// Note: conditions must be boolean (no truthy/falsy)
// if (score) { } // Error: int is not booleanSwitch & Expressions
Traditional switch has fall-through (use break). Java 14+ switch expressions (->) don't fall through and can return values. Use commas for multiple case labels (case 1, 2, 3). 'yield' returns a value from a complex block. Switch expressions are exhaustive — you need a default or all cases for enums.
// Traditional switch (fall-through)
int day = 3;
switch (day) {
case 1:
System.out.println("Mon");
break;
case 2:
case 3:
case 4:
System.out.println("Midweek");
break;
case 6:
case 7:
System.out.println("Weekend");
break;
default:
System.out.println("Invalid");
}
// Switch expression (Java 14+, no fall-through)
String type = switch (day) {
case 1, 2, 3, 4, 5 -> "Weekday";
case 6, 7 -> "Weekend";
default -> "Invalid";
};
// Switch with yield (for complex blocks)
int result = switch (day) {
case 1, 2, 3, 4, 5 -> {
int hours = 8;
yield hours * 5;
}
case 6, 7 -> 0;
default -> -1;
};Loops
Java has for, while, and do-while loops. The enhanced for (for-each) works with arrays and any Iterable. break exits the loop; continue skips to the next iteration. For collections, prefer for-each or streams over indexed loops. do-while runs at least once (rarely used).
// For loop
for (int i = 0; i < 5; i++) {
System.out.println(i);
}
// Enhanced for (for-each)
int[] nums = {1, 2, 3, 4, 5};
for (int n : nums) {
System.out.println(n);
}
List<String> names = List.of("Alice", "Bob");
for (String name : names) {
System.out.println(name);
}
// While loop
int count = 0;
while (count < 3) {
System.out.println(count);
count++;
}
// Do-while (runs at least once)
int i = 0;
do {
System.out.println(i);
i++;
} while (i < 3);
// Break and continue
for (int j = 0; j < 10; j++) {
if (j == 5) break; // exit loop
if (j % 2 == 0) continue; // skip iteration
System.out.println(j);
}Labeled Break & Continue
Labels (outer:) allow breaking/continuing outer loops from nested loops. This is rarely needed — extracting to a method with return is usually cleaner. Labels are placed before the loop, followed by a colon. break label exits the labeled loop; continue label skips to its next iteration.
// Labels for breaking out of nested loops
outer:
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 3; j++) {
if (i == 1 && j == 1) {
break outer; // exits both loops
}
System.out.println(i + "," + j);
}
}
// Labeled continue
outer:
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 3; j++) {
if (j == 1) {
continue outer; // skip to next i
}
System.out.println(i + "," + j);
}
}
// Alternative: extract to method and use return
void findPair(int[][] matrix, int target) {
for (int i = 0; i < matrix.length; i++) {
for (int j = 0; j < matrix[i].length; j++) {
if (matrix[i][j] == target) return; // exit method
}
}
}Arrays
Arrays have fixed length (use ArrayList for dynamic). Arrays.sort() sorts in place. Arrays.toString() gives a readable representation. Arrays.copyOf() creates a copy with a new length. For multidimensional arrays, each row can have different length (jagged arrays). Use Arrays for utility methods on arrays.
// Declare and initialize
int[] nums = {1, 2, 3, 4, 5};
int[] empty = new int[5]; // [0, 0, 0, 0, 0]
String[] names = new String[3]; // [null, null, null]
// Access and modify
nums[0] = 10;
int first = nums[0]; // 10
int length = nums.length; // 5
// Multidimensional
int[][] matrix = {{1, 2}, {3, 4}};
int val = matrix[0][1]; // 2
// Arrays utility class
import java.util.Arrays;
int[] sorted = {3, 1, 2};
Arrays.sort(sorted); // [1, 2, 3]
int[] copy = Arrays.copyOf(nums, 3);
String str = Arrays.toString(nums); // "[10, 2, 3, 4, 5]"
boolean eq = Arrays.equals(nums, copy);
// Fill
int[] filled = new int[5];
Arrays.fill(filled, 42); // [42, 42, 42, 42, 42]
// Binary search (sorted array only)
int idx = Arrays.binarySearch(sorted, 2); // index of 2Methods & Functions
Method Definition
Java methods are always inside a class. 'static' means the method belongs to the class (call without an instance). The return type (int, String, void) is declared before the name. Parameters are typed. Java has no default parameter values — use method overloading instead.
public class Calculator {
// Method with return type
public static int add(int a, int b) {
return a + b;
}
// Void method (no return)
public static void printResult(int result) {
System.out.println("Result: " + result);
}
// Method with default (no overloading needed)
public static String greet(String name, String greeting) {
return greeting + ", " + name + "!";
}
public static void main(String[] args) {
int sum = add(3, 4);
printResult(sum);
String msg = greet("Alice", "Hello");
System.out.println(msg);
}
}Method Overloading
Method overloading allows multiple methods with the same name but different parameter lists (type, count, or order). Java resolves overloads at compile time based on argument types. Overloading is common for constructors and utility methods. It's different from overriding (which involves inheritance and runtime dispatch).
public class MathUtils {
// Overloaded methods (same name, different params)
public static int add(int a, int b) {
return a + b;
}
public static double add(double a, double b) {
return a + b;
}
public static int add(int a, int b, int c) {
return a + b + c;
}
public static String add(String a, String b) {
return a + b;
}
}
// Java picks the most specific match
MathUtils.add(1, 2); // int version -> 3
MathUtils.add(1.5, 2.5); // double version -> 4.0
MathUtils.add(1, 2, 3); // 3-param version -> 6
MathUtils.add("Hello", "!"); // String version -> "Hello!"Varargs & Pass-by-Value
Varargs (Type... name) allow variable arguments, received as an array. Java is always pass-by-value: primitives are copied, object references are copied (but point to the same object). So modifying a parameter inside a method doesn't affect the caller's variable, but modifying the object it points to does.
// Varargs: variable number of arguments
public static int sum(int... nums) {
int total = 0;
for (int n : nums) {
total += n;
}
return total;
}
sum(1, 2, 3); // 6
sum(1, 2, 3, 4, 5); // 15
sum(); // 0 (empty array)
int[] arr = {1, 2, 3};
sum(arr); // 6 (pass array to varargs)
// Java is ALWAYS pass-by-value
public static void modify(int x) {
x = 100; // doesn't affect the caller's variable
}
int n = 5;
modify(n);
System.out.println(n); // still 5
// For objects, the reference is passed by value
public static void addItem(List<String> list) {
list.add("new"); // modifies the same list object
}Recursion
Recursion is when a method calls itself. Always have a base case to stop. Java doesn't optimize tail recursion (unlike some languages), so deep recursion can cause StackOverflowError. For performance-critical or deep recursion, convert to iteration. Memoization (caching results) can speed up recursive solutions like Fibonacci.
// Factorial
public static int factorial(int n) {
if (n <= 1) return 1; // base case
return n * factorial(n - 1); // recursive case
}
// factorial(5) = 5 * 4 * 3 * 2 * 1 = 120
// Fibonacci
public static int fib(int n) {
if (n < 2) return n;
return fib(n - 1) + fib(n - 2);
}
// Tail recursion (Java doesn't optimize this)
public static int factorialTail(int n, int acc) {
if (n <= 1) return acc;
return factorialTail(n - 1, n * acc);
}
// Call: factorialTail(5, 1)
// Be careful: deep recursion causes StackOverflowError
// For deep recursion, use iteration or a loop insteadLambda Expressions
Lambdas (Java 8+) are anonymous functions. The type is a functional interface (one abstract method). Common ones: Function<T,R> (input→output), Predicate<T> (boolean test), Consumer<T> (consume, no return), Supplier<T> (produce, no input). Method references (String::length) are shorthand for lambdas that call a single method.
import java.util.function.*;
// Lambda syntax: (params) -> expression
Function<Integer, Integer> square = x -> x * x;
Function<String, Integer> length = s -> s.length();
BiFunction<Integer, Integer, Integer> add = (a, b) -> a + b;
// With type annotations
BinaryOperator<Integer> multiply = (Integer a, Integer b) -> a * b;
// Multi-line lambda
Function<String, String> process = s -> {
String upper = s.toUpperCase();
return upper.substring(0, 3);
};
// Predicate (boolean test)
Predicate<Integer> isEven = n -> n % 2 == 0;
Predicate<String> isEmpty = String::isEmpty; // method reference
// Consumer (no return)
Consumer<String> printer = s -> System.out.println(s);
Consumer<String> printer2 = System.out::println; // method reference
// Supplier (no input, produces value)
Supplier<Double> random = () -> Math.random();
// Usage
int result = square.apply(5); // 25
boolean even = isEven.test(4); // true
printer.accept("Hello"); // prints "Hello"Classes & OOP
Class & Constructor
Classes are templates for objects. Fields hold state, methods define behavior. Constructors initialize new objects (use 'this' to distinguish fields from parameters). @Override indicates a method overrides a superclass method (toString is from Object). Encapsulation: private fields, public getters/setters.
public class Person {
// Fields (instance variables)
private String name;
private int age;
// Constructor
public Person(String name, int age) {
this.name = name; // 'this' refers to the current instance
this.age = age;
}
// Methods
public String getName() { return name; }
public int getAge() { return age; }
public void setAge(int age) {
if (age >= 0) this.age = age;
}
public String greet() {
return "Hi, I'm " + name;
}
@Override
public String toString() {
return "Person{name='" + name + "', age=" + age + "}";
}
}
// Usage
Person p = new Person("Alice", 30);
System.out.println(p.getName()); // "Alice"
System.out.println(p); // uses toString()Access Modifiers & Static
Access modifiers: public (everywhere), private (class only), protected (class + subclasses + package), default/package-private (same package). Static members belong to the class, not instances — shared across all objects. Static initializers run once when the class loads. Use static for constants (static final), utility methods, and counters.
public class BankAccount {
// Access modifiers:
public String owner; // accessible everywhere
private double balance; // class only
protected String type; // class + subclasses + same package
String id; // package-private (default)
// Static field (shared by all instances)
private static int accountCount = 0;
// Static constant
public static final double MIN_BALANCE = 100.0;
// Static method (call without instance)
public static int getAccountCount() {
return accountCount;
}
// Static initializer (runs once when class loads)
static {
System.out.println("BankAccount class loaded");
}
public BankAccount(String owner) {
this.owner = owner;
this.balance = MIN_BALANCE;
accountCount++; // increment shared counter
}
}
int count = BankAccount.getAccountCount(); // static method callInheritance & super
Java uses 'extends' for class inheritance (single inheritance only). super() calls the parent constructor (must be first line). @Override indicates method overriding (runtime polymorphism). A Dog IS-A Animal. Use inheritance for 'is-a' relationships; use composition (has-a) for code reuse. Java 17+ supports sealed classes to restrict inheritance.
// Parent class
class Animal {
protected String name;
public Animal(String name) {
this.name = name;
System.out.println("Animal constructor");
}
public void eat() {
System.out.println(name + " is eating");
}
}
// Child class (extends)
class Dog extends Animal {
private String breed;
public Dog(String name, String breed) {
super(name); // must be first line — call parent constructor
this.breed = breed;
}
// Override parent method
@Override
public void eat() {
super.eat(); // call parent's eat()
System.out.println(name + " the " + breed + " eats dog food");
}
public void bark() {
System.out.println("Woof!");
}
}
Dog dog = new Dog("Rex", "Labrador");
dog.eat(); // calls Dog's eat()
dog.bark(); // Dog-specific methodAbstract Classes & Interfaces
Abstract classes can't be instantiated and can have both abstract (no body) and concrete methods. Interfaces define contracts — all methods are public abstract by default. Java 8+ allows default methods (with body) and static methods in interfaces. A class extends one abstract class but can implement multiple interfaces. Use abstract classes for shared code, interfaces for contracts.
// Abstract class (can't be instantiated)
abstract class Shape {
protected String color;
public Shape(String color) {
this.color = color;
}
// Abstract method (must be implemented by subclasses)
public abstract double area();
// Concrete method (inherited)
public String describe() {
return color + " " + this.getClass().getSimpleName();
}
}
// Interface (pure contract, Java 8+ can have default methods)
interface Drawable {
void draw(); // abstract by default
// Default method (Java 8+)
default void drawTwice() {
draw();
draw();
}
// Static method in interface
static Drawable empty() {
return () -> System.out.println("nothing");
}
}
// A class can extend one class and implement multiple interfaces
class Circle extends Shape implements Drawable {
private double radius;
public Circle(String color, double radius) {
super(color);
this.radius = radius;
}
@Override
public double area() {
return Math.PI * radius * radius;
}
@Override
public void draw() {
System.out.println("Drawing " + describe());
}
}Polymorphism & Casting
Polymorphism: a parent reference can hold a child object. Method calls dispatch to the actual object's implementation (runtime polymorphism). Use instanceof before downcasting to avoid ClassCastException. Java 16+ pattern matching (instanceof Circle c) combines check and cast. Override equals() and hashCode() together for correct behavior in collections.
// Polymorphism: one interface, many forms
Shape s1 = new Circle("red", 5);
Shape s2 = new Square("blue", 3);
// Calls the overridden method (runtime dispatch)
System.out.println(s1.area()); // Circle's area
System.out.println(s2.area()); // Square's area
// instanceof check
if (s1 instanceof Circle) {
Circle c = (Circle) s1; // downcast
System.out.println("Radius: " + c.radius);
}
// Pattern matching (Java 16+)
if (s1 instanceof Circle c) {
System.out.println("Radius: " + c.radius); // c is already cast
}
// Upcasting (automatic)
Circle circle = new Circle("green", 2);
Shape shape = circle; // upcast (no explicit cast needed)
// Object class methods (all classes inherit from Object)
circle.equals(circle); // reference equality by default
circle.hashCode(); // hash code
circle.getClass(); // Class<Circle>
circle.toString(); // string representationRecords & Enums
Records (Java 16+) are immutable data classes — the compiler generates constructor, getters, equals, hashCode, and toString. Use for DTOs and value objects. Enums are type-safe constants that can have fields, methods, and constructors. Enums implement Comparable and have values() and valueOf() methods. Both are essential for modern Java.
// Record (Java 16+): concise data class
public record Point(int x, int y) {}
// Equivalent to a class with:
// - final fields x, y
// - constructor
// - getters x(), y()
// - equals, hashCode, toString
Point p = new Point(3, 4);
System.out.println(p.x()); // 3
System.out.println(p.y()); // 4
System.out.println(p); // Point[x=3, y=4]
// Compact constructor (validation)
public record Age(int value) {
public Age {
if (value < 0 || value > 150) {
throw new IllegalArgumentException("Invalid age");
}
}
}
// Enum (named constants)
public enum Direction {
UP, DOWN, LEFT, RIGHT;
public Direction opposite() {
return switch (this) {
case UP -> DOWN;
case DOWN -> UP;
case LEFT -> RIGHT;
case RIGHT -> LEFT;
};
}
}
Direction d = Direction.UP;
Direction opp = d.opposite(); // DOWNCollections & Generics
List (ArrayList & LinkedList)
ArrayList is backed by an array (fast get/set, slow insert/delete in middle). LinkedList is backed by a doubly-linked list (fast insert/delete at ends, slow random access). List.of() creates immutable lists. Use ArrayList for most cases; LinkedList only for frequent end operations. Both implement the List interface.
import java.util.*;
// ArrayList (fast random access, slow insert/delete in middle)
List<String> list = new ArrayList<>();
list.add("Alice");
list.add("Bob");
list.add(0, "Carol"); // insert at index
list.set(1, "Dave"); // replace at index
String name = list.get(0); // "Carol"
list.remove(0); // remove by index
list.remove("Dave"); // remove by value
int size = list.size(); // 1
boolean has = list.contains("Bob");
// LinkedList (fast insert/delete at ends)
LinkedList<Integer> linked = new LinkedList<>();
linked.addFirst(1);
linked.addLast(2);
linked.removeFirst();
linked.peek(); // see first element
// Immutable list (Java 9+)
List<String> immutable = List.of("a", "b", "c");
// immutable.add("d"); // UnsupportedOperationException
// Iterate
for (String s : list) {
System.out.println(s);
}
list.forEach(System.out::println); // method referenceSet (HashSet & TreeSet)
Set stores unique elements. HashSet is fastest but unordered. TreeSet keeps elements sorted (natural order or Comparator). LinkedHashSet maintains insertion order. Set operations: addAll (union), retainAll (intersection), removeAll (difference). For custom objects in a HashSet, override equals() and hashCode().
import java.util.*;
// HashSet (fast, unordered)
Set<String> set = new HashSet<>();
set.add("apple");
set.add("banana");
set.add("apple"); // duplicate ignored
System.out.println(set.size()); // 2
System.out.println(set.contains("apple")); // true
set.remove("banana");
// TreeSet (sorted, slower)
Set<Integer> sorted = new TreeSet<>();
sorted.add(3);
sorted.add(1);
sorted.add(2);
System.out.println(sorted); // [1, 2, 3]
// LinkedHashSet (maintains insertion order)
Set<String> ordered = new LinkedHashSet<>();
ordered.add("c");
ordered.add("a");
ordered.add("b");
System.out.println(ordered); // [c, a, b]
// Set operations
Set<Integer> a = new HashSet<>(Set.of(1, 2, 3));
Set<Integer> b = new HashSet<>(Set.of(2, 3, 4));
a.addAll(b); // union: [1, 2, 3, 4]
a.retainAll(b); // intersection: [2, 3]
a.removeAll(b); // difference: [1]
// Immutable set
Set<String> immutable = Set.of("x", "y", "z");Map (HashMap & TreeMap)
Map stores key-value pairs. HashMap is fastest (unordered). TreeMap sorts by keys. LinkedHashMap maintains insertion order. getOrDefault avoids null checks. compute/merge are powerful for updating values. For custom keys, override equals() and hashCode(). Map.of() creates immutable maps (Java 9+).
import java.util.*;
// HashMap (fast, unordered)
Map<String, Integer> ages = new HashMap<>();
ages.put("Alice", 30);
ages.put("Bob", 25);
ages.put("Alice", 31); // overwrite
// Access
int age = ages.get("Alice"); // 31
int defaultAge = ages.getOrDefault("Eve", 0); // 0
// Check
boolean has = ages.containsKey("Alice");
boolean hasVal = ages.containsValue(25);
// Remove
ages.remove("Bob");
// Iterate
for (Map.Entry<String, Integer> entry : ages.entrySet()) {
System.out.println(entry.getKey() + ": " + entry.getValue());
}
ages.forEach((key, val) -> System.out.println(key + "=" + val));
// Compute (Java 8+)
ages.compute("Alice", (k, v) -> v + 1); // increment
ages.putIfAbsent("Carol", 28);
ages.merge("Alice", 1, Integer::sum); // add 1
// TreeMap (sorted by keys)
Map<String, Integer> sorted = new TreeMap<>();
// LinkedHashMap (maintains insertion order)
Map<String, Integer> ordered = new LinkedHashMap<>();Queue & Deque
Queue is FIFO (first-in-first-out). Deque is double-ended (can add/remove from both ends). PriorityQueue orders elements by natural order or a Comparator (min-heap by default). For a stack, use ArrayDeque (push/pop) instead of the legacy Stack class. ArrayDeque is faster than LinkedList for queue/deque operations.
import java.util.*;
// Queue (FIFO)
Queue<String> queue = new LinkedList<>();
queue.add("first"); // throws if full (capacity-restricted)
queue.offer("second"); // returns false if full
String head = queue.peek(); // see head (null if empty)
String removed = queue.poll(); // remove and return head
// Deque (double-ended)
Deque<Integer> deque = new ArrayDeque<>();
deque.addFirst(1);
deque.addLast(2);
deque.peekFirst(); // 1
deque.peekLast(); // 2
deque.pollFirst(); // 1
deque.pollLast(); // 2
// PriorityQueue (min-heap by default)
PriorityQueue<Integer> pq = new PriorityQueue<>();
pq.add(3);
pq.add(1);
pq.add(2);
System.out.println(pq.poll()); // 1 (smallest first)
// Max-heap (reverse order)
PriorityQueue<Integer> maxHeap = new PriorityQueue<>(Collections.reverseOrder());
maxHeap.add(1);
maxHeap.add(3);
System.out.println(maxHeap.poll()); // 3 (largest first)
// Stack (legacy, prefer Deque)
Deque<Integer> stack = new ArrayDeque<>();
stack.push(1); // add to front
stack.push(2);
stack.pop(); // 2 (remove from front)Generics
Generics enable type-safe collections and classes. <T> is a type parameter. Bounded types (<T extends Comparable<T>>) restrict to types with certain behavior. Wildcards: ? (any), ? extends T (covariant, read-only), ? super T (contravariant, write-only). Generics use type erasure — types are checked at compile time, erased at runtime.
// Generic class
public class Box<T> {
private T value;
public void set(T value) { this.value = value; }
public T get() { return value; }
}
Box<String> stringBox = new Box<>();
stringBox.set("hello");
String s = stringBox.get();
Box<Integer> intBox = new Box<>();
intBox.set(42);
// Generic method
public static <T> T firstOf(List<T> list) {
return list.get(0);
}
String first = firstOf(List.of("a", "b"));
// Bounded type parameter
public static <T extends Comparable<T>> T max(List<T> list) {
T result = list.get(0);
for (T item : list) {
if (item.compareTo(result) > 0) {
result = item;
}
}
return result;
}
// Wildcards
void process(List<?> list) { } // any type
void processNums(List<? extends Number> list) { } // Number or subclass
void addNums(List<? super Integer> list) { } // Integer or superclassIterators & Comparable
Iterator allows safe removal during iteration (it.remove()). ListIterator adds bidirectional traversal and set/add. Comparable defines natural ordering (compareTo). Comparator defines custom ordering (comparing, comparingInt, reversed, thenComparing). Use Comparator.comparing() for fluent sorting. Collections.sort() uses the natural order.
import java.util.*;
// Iterator
List<String> list = List.of("a", "b", "c");
Iterator<String> it = list.iterator();
while (it.hasNext()) {
String s = it.next();
System.out.println(s);
// it.remove(); // safe removal during iteration
}
// ListIterator (bidirectional)
ListIterator<String> lit = list.listIterator();
while (lit.hasNext()) {
lit.set(lit.next().toUpperCase()); // replace
}
// Comparable (natural ordering)
public class Person implements Comparable<Person> {
String name;
int age;
@Override
public int compareTo(Person other) {
return Integer.compare(this.age, other.age);
}
}
// Comparator (custom ordering)
Comparator<Person> byName = Comparator.comparing(p -> p.name);
Comparator<Person> byAgeDesc = Comparator.comparingInt((Person p) -> p.age).reversed();
List<Person> people = new ArrayList<>();
people.sort(byName);
people.sort(byAgeDesc);
Collections.sort(people); // uses ComparableStreams & Functional
Stream Basics
Streams (Java 8+) provide declarative data processing. Create with .stream() (collections) or Stream.of(). Intermediate operations (filter, map, sorted) are lazy — they execute only when a terminal operation (collect, reduce, count, forEach) is called. toList() (Java 16+) is a concise alternative to collect(Collectors.toList()).
import java.util.*;
import java.util.stream.*;
List<Integer> nums = List.of(1, 2, 3, 4, 5, 6);
// Filter and collect
List<Integer> evens = nums.stream()
.filter(n -> n % 2 == 0)
.collect(Collectors.toList()); // [2, 4, 6]
// Map (transform)
List<String> doubled = nums.stream()
.map(n -> "num" + n)
.collect(Collectors.toList());
// Reduce
int sum = nums.stream().reduce(0, Integer::sum); // 21
int product = nums.stream().reduce(1, (a, b) -> a * b);
// Count
long count = nums.stream().filter(n -> n > 3).count(); // 3
// Find
Optional<Integer> first = nums.stream().filter(n -> n > 3).findFirst();
boolean anyMatch = nums.stream().anyMatch(n -> n > 5);
boolean allMatch = nums.stream().allMatch(n -> n > 0);
// ForEach
nums.stream().forEach(System.out::println);
// toList() shortcut (Java 16+)
List<Integer> result = nums.stream().filter(n -> n > 3).toList();Stream Operations
sorted() orders elements (natural or with Comparator). distinct() removes duplicates. limit(n)/skip(n) paginate. flatMap flattens nested streams — essential for one-to-many transformations. peek() is for debugging (side effects). groupingBy creates maps grouping elements by a classifier. Streams are lazy — operations chain efficiently.
List<String> names = List.of("Alice", "Bob", "Charlie", "David");
// Sorted
List<String> sorted = names.stream()
.sorted()
.toList();
// Sorted by length
List<String> byLength = names.stream()
.sorted(Comparator.comparing(String::length))
.toList();
// Distinct
List<Integer> distinct = List.of(1, 2, 2, 3, 3, 3).stream()
.distinct()
.toList(); // [1, 2, 3]
// Limit and Skip
List<Integer> limited = nums.stream()
.skip(2) // skip first 2
.limit(3) // take next 3
.toList();
// FlatMap (flatten nested structures)
List<List<Integer>> nested = List.of(List.of(1, 2), List.of(3, 4));
List<Integer> flat = nested.stream()
.flatMap(List::stream)
.toList(); // [1, 2, 3, 4]
// Peek (debug, side-effect)
nums.stream()
.peek(n -> System.out.println("before: " + n))
.filter(n -> n > 2)
.peek(n -> System.out.println("after: " + n))
.toList();
// Grouping
Map<Integer, List<String>> byLength = names.stream()
.collect(Collectors.groupingBy(String::length));Collectors & Reduction
Collectors provide rich reduction operations: joining (concatenate strings), groupingBy (group by key), partitioningBy (split by boolean), toMap (create a map), summarizingInt (stats: count, sum, min, max, average). Collectors can be composed (groupingBy with downstream collector). These replace verbose loops with declarative one-liners.
import java.util.stream.*;
List<Person> people = List.of(
new Person("Alice", 30),
new Person("Bob", 25),
new Person("Charlie", 35)
);
// Join strings
String joined = people.stream()
.map(Person::getName)
.collect(Collectors.joining(", ")); // "Alice, Bob, Charlie"
// Group by
Map<Integer, List<Person>> byAge = people.stream()
.collect(Collectors.groupingBy(Person::getAge));
// Partition (boolean)
Map<Boolean, List<Person>> partition = people.stream()
.collect(Collectors.partitioningBy(p -> p.getAge() > 28));
// Count by group
Map<Integer, Long> countByAge = people.stream()
.collect(Collectors.groupingBy(Person::getAge, Collectors.counting()));
// Summarizing
IntSummaryStatistics stats = people.stream()
.collect(Collectors.summarizingInt(Person::getAge));
System.out.println(stats.getAverage()); // 30.0
System.out.println(stats.getMax()); // 35
// To map
Map<String, Integer> nameToAge = people.stream()
.collect(Collectors.toMap(Person::getName, Person::getAge));
// Reducing
int totalAge = people.stream()
.collect(Collectors.reducing(0, Person::getAge, Integer::sum));Optional
Optional<T> is a container that may or may not contain a value. It forces explicit handling of absence — no more NullPointerException. Use of() for non-null values, ofNullable() for possibly-null. Chain with map/flatMap/filter. Never use get() without isPresent() — prefer orElse/orElseThrow. Optional is designed for return types, not fields.
import java.util.Optional;
// Creating Optional
Optional<String> present = Optional.of("hello");
Optional<String> empty = Optional.empty();
Optional<String> nullable = Optional.ofNullable(null); // empty if null
// Checking
present.isPresent(); // true
empty.isEmpty(); // true (Java 11+)
// Getting values
String val = present.get(); // throws if empty (avoid!)
String safe = present.orElse("default");
String computed = present.orElseGet(() -> computeDefault());
String orThrow = present.orElseThrow(() -> new RuntimeException("missing"));
// Transform (map/flatMap)
Optional<Integer> length = present.map(String::length); // Optional[5]
Optional<String> upper = present.map(s -> s.toUpperCase());
// Filter
Optional<String> filtered = present.filter(s -> s.length() > 3);
// ifPresent
present.ifPresent(s -> System.out.println(s));
present.ifPresentOrElse(
s -> System.out.println("Got: " + s),
() -> System.out.println("Empty")
);
// Chaining (avoid null checks)
String result = getUser(1)
.map(User::getProfile)
.map(Profile::getEmail)
.orElse("no email");Functional Interfaces
Functional interfaces have exactly one abstract method (can have multiple default methods). @FunctionalInterface is optional but documents intent. Java provides many in java.util.function: Function, Predicate, Consumer, Supplier, plus Bi- and primitive variants. Use these instead of creating custom interfaces when possible. They enable lambda expressions and method references.
import java.util.function.*;
// Built-in functional interfaces
Function<String, Integer> strToInt = Integer::parseInt;
BiFunction<String, String, String> concat = String::concat;
Predicate<String> isEmpty = String::isEmpty;
BiPredicate<String, String> contains = String::contains;
Consumer<String> printer = System.out::println;
BiConsumer<String, Integer> printPair = (s, i) -> System.out.println(s + ":" + i);
Supplier<List<String>> listFactory = ArrayList::new;
// Primitive specializations
IntFunction<String> intToStr = String::valueOf;
ToIntFunction<String> length = String::length;
IntPredicate isPositive = n -> n > 0;
IntConsumer intPrinter = System.out::println;
IntSupplier randomInt = () -> (int)(Math.random() * 100);
// Binary operators
BinaryOperator<Integer> max = Integer::max;
IntBinaryOperator sum = Integer::sum;
// Unary operators
UnaryOperator<String> trim = String::trim;
IntUnaryOperator negate = n -> -n;
// Custom functional interface
@FunctionalInterface
interface StringProcessor {
String process(String input);
// Can have default methods
default StringProcessor andThen(StringProcessor after) {
return input -> after.process(process(input));
}
}Exceptions & I/O
Try / Catch / Finally
try/catch/finally handles exceptions. finally always runs (use for cleanup). Multi-catch (catch A | B) handles multiple exceptions together. Try-with-resources auto-closes any AutoCloseable (files, connections, streams) — preferred over manual finally cleanup. Resources are closed in reverse order of declaration.
try {
int result = 10 / 0;
} catch (ArithmeticException e) {
System.out.println("Math error: " + e.getMessage());
} catch (Exception e) {
System.out.println("General error: " + e);
} finally {
// Always runs (even if return/throw in try/catch)
System.out.println("Cleanup");
}
// Multi-catch (Java 7+)
try {
riskyOperation();
} catch (IOException | SQLException e) {
// Handle both exceptions the same way
log.error(e);
}
// Try-with-resources (auto-close, Java 7+)
try (BufferedReader br = new BufferedReader(new FileReader("file.txt"));
PrintWriter pw = new PrintWriter("output.txt")) {
String line = br.readLine();
pw.println(line);
} catch (IOException e) {
e.printStackTrace();
}
// br and pw are auto-closed (in reverse order)Checked vs Unchecked
Checked exceptions (extends Exception) must be declared with 'throws' or caught — the compiler enforces this. Use for recoverable conditions (file not found, network error). Unchecked exceptions (extends RuntimeException) don't need declaration — use for programming errors (null pointer, invalid argument). The debate: checked exceptions force handling but can clutter code; many frameworks prefer unchecked.
// Checked exceptions (must be declared or caught)
public void readFile(String path) throws IOException {
BufferedReader br = new BufferedReader(new FileReader(path));
// IOException is checked — compiler enforces handling
}
// Unchecked exceptions (RuntimeException, no need to declare)
public int divide(int a, int b) {
if (b == 0) {
throw new IllegalArgumentException("Divisor cannot be zero");
// RuntimeException — no 'throws' needed
}
return a / b;
}
// Common checked exceptions:
// IOException, SQLException, ClassNotFoundException
// Common unchecked exceptions:
// NullPointerException, IllegalArgumentException,
// IndexOutOfBoundsException, ArithmeticException,
// ClassCastException, IllegalStateException
// Custom checked exception
class DataException extends Exception {
public DataException(String msg) { super(msg); }
}
// Custom unchecked exception
class ValidationException extends RuntimeException {
public ValidationException(String msg) { super(msg); }
}File I/O (NIO.2)
NIO.2 (java.nio.file) is the modern file API. Files.readString/writeString (Java 11+) are convenient for text. Files.lines() returns a lazy Stream — efficient for large files (must close with try-with-resources). Path.of() replaces the old File class. Files.createDirectories() creates the full path. Always handle IOException.
import java.nio.file.*;
import java.io.*;
// Read entire file (small files)
List<String> lines = Files.readAllLines(Path.of("input.txt"));
String content = Files.readString(Path.of("config.json")); // Java 11+
byte[] bytes = Files.readAllBytes(Path.of("image.png"));
// Write file
Files.writeString(Path.of("output.txt"), "Hello, World!");
Files.write(Path.of("data.bin"), bytes);
// Append
Files.writeString(Path.of("log.txt"), "entry\n",
StandardOpenOption.APPEND, StandardOpenOption.CREATE);
// Stream lines (large files, lazy)
try (Stream<String> lineStream = Files.lines(Path.of("large.txt"))) {
lineStream.filter(l -> l.contains("ERROR"))
.forEach(System.out::println);
}
// Copy, move, delete
Files.copy(Path.of("src.txt"), Path.of("dest.txt"));
Files.move(Path.of("old.txt"), Path.of("new.txt"));
Files.delete(Path.of("temp.txt"));
// Check existence
boolean exists = Files.exists(Path.of("file.txt"));
// Create directories
Files.createDirectories(Path.of("a/b/c"));Reader & Writer (Text)
BufferedReader/Writer are efficient for text I/O (buffering reduces system calls). PrintWriter offers printf-style formatting. Scanner parses input (nextInt, nextDouble, nextLine). InputStreamReader bridges byte streams to character streams (specify charset for non-UTF-8). Always use try-with-resources to ensure streams are closed.
import java.io.*;
import java.nio.file.*;
// BufferedReader (efficient text reading)
try (BufferedReader br = Files.newBufferedReader(Path.of("input.txt"))) {
String line;
while ((line = br.readLine()) != null) {
System.out.println(line);
}
}
// BufferedWriter (efficient text writing)
try (BufferedWriter bw = Files.newBufferedWriter(Path.of("output.txt"))) {
bw.write("First line");
bw.newLine();
bw.write("Second line");
}
// PrintWriter (convenient formatting)
try (PrintWriter pw = new PrintWriter("output.txt")) {
pw.println("Hello");
pw.printf("Name: %s, Age: %d%n", "Alice", 30);
}
// Scanner (parsing input)
try (Scanner sc = new Scanner(Path.of("data.txt"))) {
while (sc.hasNextLine()) {
String line = sc.nextLine();
// Parse tokens
Scanner lineSc = new Scanner(line);
if (lineSc.hasNextInt()) {
int n = lineSc.nextInt();
}
}
}
// InputStreamReader (bytes to chars, e.g., from InputStream)
Reader reader = new InputStreamReader(System.in);Date & Time (java.time)
java.time (Java 8+) is the modern date/time API, replacing the old Date/Calendar. LocalDate (date only), LocalTime (time only), LocalDateTime (both), ZonedDateTime (with timezone). All are immutable and thread-safe. Use Period for date differences, Duration for time differences. DateTimeFormatter for parsing/formatting. Instant for machine timestamps (UTC).
import java.time.*;
import java.time.format.*;
import java.time.temporal.*;
// Current date/time
LocalDate today = LocalDate.now(); // 2024-01-15
LocalTime now = LocalTime.now(); // 14:30:45.123
LocalDateTime dt = LocalDateTime.now(); // both
ZonedDateTime zdt = ZonedDateTime.now(); // with timezone
// Create specific
LocalDate date = LocalDate.of(2024, 1, 15);
LocalTime time = LocalTime.of(14, 30, 0);
LocalDateTime specific = LocalDateTime.of(2024, 1, 15, 14, 30);
// Parsing and formatting
LocalDate parsed = LocalDate.parse("2024-01-15");
String formatted = date.format(DateTimeFormatter.ofPattern("dd/MM/yyyy"));
// "15/01/2024"
// Manipulation (immutable, returns new)
LocalDate tomorrow = today.plusDays(1);
LocalDate lastMonth = today.minusMonths(1);
LocalDate nextYear = today.plusYears(1);
// Period (date-based)
Period age = Period.between(LocalDate.of(1990, 1, 1), today);
System.out.println(age.getYears()); // 34
// Duration (time-based)
Duration dur = Duration.between(time, LocalTime.now());
System.out.println(dur.toMinutes());
// Instant (machine time, UTC)
Instant instant = Instant.now();
Instant epoch = Instant.ofEpochSecond(0);Concurrency Basics
Java concurrency: Thread (low-level), ExecutorService (thread pools — preferred), CompletableFuture (async composition, like Promises). parallelStream() uses the ForkJoinPool for parallel processing. synchronized blocks protect shared state. Atomic variables (AtomicInteger, etc.) provide lock-free thread-safe operations. For complex concurrency, use java.util.concurrent collections (ConcurrentHashMap, BlockingQueue).
import java.util.concurrent.*;
// Create a thread
Thread thread = new Thread(() -> {
System.out.println("Running in: " + Thread.currentThread().getName());
});
thread.start();
thread.join(); // wait for completion
// ExecutorService (thread pool)
ExecutorService executor = Executors.newFixedThreadPool(4);
Future<Integer> future = executor.submit(() -> {
Thread.sleep(1000);
return 42;
});
Integer result = future.get(); // blocks until done
executor.shutdown();
// CompletableFuture (async, Java 8+)
CompletableFuture<String> cf = CompletableFuture
.supplyAsync(() -> "Hello")
.thenApply(s -> s + " World")
.thenApply(String::toUpperCase);
String asyncResult = cf.join(); // "HELLO WORLD"
// Parallel stream
List<Integer> nums = List.of(1, 2, 3, 4, 5);
int sum = nums.parallelStream().mapToInt(Integer::intValue).sum();
// Synchronized
synchronized (this) {
// only one thread at a time
}
// Atomic variables
AtomicInteger counter = new AtomicInteger(0);
counter.incrementAndGet();
counter.compareAndSet(0, 1);Lambda Expressions
Lambda Syntax Basics
Lambdas (Java 8+) are concise implementations of functional interfaces. Syntax: (params) -> expression or (params) -> { statements; }. The compiler infers parameter types from the target type. Single-param lambdas can omit parentheses; zero-param need empty parens. Lambdas enable functional programming and are the backbone of the Streams API.
// Anonymous class (verbose, pre-Java 8)
Runnable r1 = new Runnable() {
public void run() {
System.out.println("Old way");
}
};
// Lambda expression (Java 8+)
Runnable r2 = () -> System.out.println("Lambda");
// With parameters and body
Comparator<Integer> cmp = (a, b) -> {
int diff = a - b;
return diff;
};
// Type inference (omit types)
Comparator<Integer> cmp2 = (a, b) -> a - b;
// Single param, no parentheses needed
Consumer<String> printer = s -> System.out.println(s);
// Zero params need empty parens
Runnable noop = () -> {};Functional Interfaces
A functional interface has exactly one abstract method (SAM type). @FunctionalInterface annotation makes the compiler enforce this. Lambdas can only target functional interfaces. Default and static methods are allowed and don't break the single-method rule. This is the foundation that makes lambdas work in Java's type system.
// Functional interface: exactly one abstract method
@FunctionalInterface
interface MathOperation {
int operate(int a, int b);
}
// Implement with lambda
MathOperation add = (a, b) -> a + b;
MathOperation mul = (a, b) -> a * b;
MathOperation max = (a, b) -> Math.max(a, b);
int result = add.operate(3, 4); // 7
int m = mul.operate(3, 4); // 12
// @FunctionalInterface is optional but recommended
// It prevents accidentally adding a second abstract method
// Default and static methods don't count toward the limit
interface StringProcessor {
String process(String s);
default StringProcessor andThen(StringProcessor next) {
return s -> next.process(this.process(s));
}
}Built-in Functional Interfaces
java.util.function provides ~40 ready-made functional interfaces so you rarely write your own. The core four: Function (transform), Predicate (test), Consumer (consume), Supplier (produce). Bi- variants take two args. Primitive variants (IntFunction, ToIntFunction, etc.) avoid autoboxing overhead. Use these instead of creating custom interfaces.
import java.util.function.*;
// Function<T,R>: input -> output
Function<String, Integer> len = String::length;
int n = len.apply("hello"); // 5
// Predicate<T>: input -> boolean (for filtering)
Predicate<String> isEmpty = String::isEmpty;
boolean e = isEmpty.test(""); // true
// Consumer<T>: input -> void (side effects)
Consumer<String> print = System.out::println;
print.accept("hi");
// Supplier<T>: no input -> output (factories, lazy)
Supplier<Double> random = Math::random;
double r = random.get();
// BiFunction<T,U,R>: two inputs -> output
BiFunction<String, Integer, String> repeat =
(s, i) -> s.repeat(i);
// Primitives variants avoid boxing
IntFunction<String> f = i -> "n=" + i;
IntPredicate positive = i -> i > 0;
ToIntFunction<String> length = String::length;
IntBinaryOperator sum = (a, b) -> a + b;Method References
Method references (::) are shorthand for lambdas that just call a single method. Four kinds: static (Class::static), bound instance (obj::method), unbound instance (Class::method — first param becomes receiver), and constructor (Class::new). Use them when a lambda only forwards to one method — they're more readable. Otherwise stick with explicit lambdas.
import java.util.*;
List<String> names = List.of("alice", "bob", "charlie");
// Lambda form
names.forEach(s -> System.out.println(s));
// Method reference (shorthand)
names.forEach(System.out::println);
// Four kinds of method references:
// 1. Static method: ClassName::staticMethod
names.stream().map(String::toUpperCase);
// 2. Instance method of particular object: instance::method
var printer = System.out;
names.forEach(printer::println);
// 3. Instance method of arbitrary object: ClassName::instanceMethod
List<String> upper = names.stream()
.map(String::toUpperCase)
.toList();
// 4. Constructor: ClassName::new
Supplier<ArrayList<String>> factory = ArrayList::new;
ArrayList<String> list = factory.get();Capturing Variables (Effectively Final)
Lambdas can capture local variables, but they must be final or 'effectively final' (never reassigned). This is because lambdas may outlive the stack frame. To work around it, use a single-element array or an AtomicInteger/holder object. Instance and static fields have no such restriction. 'this' inside a lambda refers to the enclosing class instance, not the lambda itself.
import java.util.function.*;
int x = 10;
// Capturing a local variable — must be final or effectively final
Supplier<Integer> getter = () -> x * 2;
System.out.println(getter.get()); // 20
// x = 20; // ERROR: would break the lambda capture
// Local variables captured by lambdas must be final/effectively final
// Workaround: use an array or wrapper (mutable container)
int[] counter = {0};
Runnable inc = () -> counter[0]++;
inc.run();
inc.run();
System.out.println(counter[0]); // 2
// Instance/static fields CAN be modified (no restriction)
class Holder {
int value = 0;
Runnable bump = () -> value++; // OK, field access
}
// 'this' inside a lambda refers to the enclosing instance
class Outer {
String name = "Outer";
Runnable r = () -> System.out.println(this.name); // "Outer"
}Constructor References
Constructor references (ClassName::new) create new instances concisely. Useful with Collectors.toCollection() to choose the result type, with array creation (Type[]::new), and in factory patterns. For records and immutable objects, constructor references are the idiomatic way to build copies. They pair naturally with Function/Supplier targets.
import java.util.*;
import java.util.function.*;
import java.util.stream.*;
// Supplier constructor reference
Supplier<StringBuilder> sbFactory = StringBuilder::new;
StringBuilder sb = sbFactory.get();
// Function constructor reference (with one arg)
Function<String, StringBuilder> sbFromString = StringBuilder::new;
StringBuilder named = sbFromString.apply("Hello");
// In streams: collect into a specific collection
List<String> names = List.of("a", "b", "c");
ArrayList<String> copy = names.stream()
.collect(Collectors.toCollection(ArrayList::new));
// Array constructor reference
IntFunction<String[]> arrayFactory = String[]::new;
String[] arr = arrayFactory.apply(5); // new String[5]
// Copying via constructor
record Point(int x, int y) {}
Function<Point, Point> copyCtor = Point::new;
Point p = copyCtor.apply(new Point(1, 2));Optional & Null Safety
Creating Optional
Optional is a container that may or may not hold a value. Use empty() for no value, of() when you're certain the value is non-null (throws NPE otherwise), and ofNullable() when null is possible. Optional forces callers to handle the absent case explicitly. Never return null where Optional is expected — that defeats the purpose.
import java.util.Optional;
// empty() — no value
Optional<String> empty = Optional.empty();
// of() — value must be non-null (throws NPE if null)
Optional<String> present = Optional.of("hello");
// ofNullable() — accepts null safely
Optional<String> maybe = Optional.ofNullable(getName());
// From a stream that may produce 0 or 1 elements
Optional<Integer> first = List.of(1, 2, 3).stream().findFirst();
// Common helper pattern
public Optional<User> findUser(long id) {
User u = db.lookup(id);
return Optional.ofNullable(u);
}
String getName() { return Math.random() > 0.5 ? "Alice" : null; }
class User {}
class Db { User lookup(long id) { return null; } }
Db db = new Db();Consuming Values Safely
Prefer ifPresent/ifPresentOrElse over isPresent+get. orElse returns a constant default; orElseGet takes a Supplier so the default is computed lazily (important when the default is expensive). orElseThrow converts absence to an exception. The goal is to never call .get() blindly — that reintroduces the NPE risk Optional was meant to eliminate.
import java.util.Optional;
Optional<String> name = Optional.of("Alice");
// isPresent / isEmpty (Java 11+)
if (name.isPresent()) {
System.out.println(name.get()); // "Alice"
}
// Avoid .get() without checking — throws NoSuchElementException
// ifPresent: run action only if value exists
name.ifPresent(System.out::println);
// ifPresentOrElse (Java 9+)
name.ifPresentOrElse(
System.out::println,
() -> System.out.println("No name")
);
// orElse: provide default
String s1 = name.orElse("Anonymous");
// orElseGet: lazy default (computed only if needed)
String s2 = name.orElseGet(() -> expensiveDefault());
// orElseThrow: throw if absent
String s3 = name.orElseThrow(() -> new IllegalStateException("missing"));
String expensiveDefault() { return "computed"; }Transforming with map & flatMap
map transforms the contained value (Optional<T> -> Optional<R>). flatMap is used when the mapping function itself returns an Optional, preventing nested Optionals. filter keeps the value only if a predicate matches. Chaining map/filter/flatMap lets you build pipelines that short-circuit on the first empty — much cleaner than nested null checks.
import java.util.Optional;
Optional<String> name = Optional.of("Alice");
// map: transform the value if present
Optional<Integer> length = name.map(String::length); // Optional[5]
Optional<String> upper = name.map(String::toUpperCase);
// flatMap: when the transform itself returns Optional
// (avoids Optional<Optional<T>>)
public Optional<String> findEmail(long id) {
return Optional.ofNullable(db.get(id));
}
Optional<String> email = Optional.of(1L)
.flatMap(this::findEmail); // Optional<email> not Optional<Optional<email>>
// filter: keep only if predicate matches
Optional<Integer> adultAge = Optional.of(25)
.filter(a -> a >= 18); // Optional[25]
Optional<Integer> kid = Optional.of(10)
.filter(a -> a >= 18); // Optional.empty
// Chain transformations
String label = Optional.of("alice")
.map(String::strip)
.filter(s -> !s.isEmpty())
.map(s -> s.substring(0, 1).toUpperCase() + s.substring(1))
.orElse("unknown"); // "Alice"
class Db { String get(long id) { return "[email protected]"; } }
Db db = new Db();
Optional<String> findEmail(long id) { return Optional.ofNullable(db.get(id)); }Anti-patterns to Avoid
Optional is designed for return types, not fields or parameters. It's not Serializable and adds overhead as a field. Don't use .get() without checking, and don't use isPresent()+get() — that's just verbose null-checking. Collections already express emptiness, so don't wrap them in Optional. Use Optional as a return-type signal that a value may be absent.
import java.util.Optional;
// BAD: using Optional for fields (not serializable, wastes memory)
class Bad {
private Optional<String> name; // DON'T
}
// GOOD: use plain field, return Optional from accessor
class Good {
private String name;
public Optional<String> getName() { return Optional.ofNullable(name); }
}
// BAD: Optional as method parameter (clutters API)
public void process(Optional<String> input) {} // DON'T
// GOOD: method overloading or nullable param
public void process(String input) {}
public void process() { process(null); }
// BAD: .get() without check
String x = findName().get(); // throws if empty
// BAD: .isPresent() + .get() — defeats the purpose
Optional<String> opt = findName();
if (opt.isPresent()) {
use(opt.get()); // just use ifPresent or map instead
}
// BAD: returning Optional from collections
public Optional<Item> find(...) {
// Collections already express emptiness — return empty List, not Optional<List>
return Optional.ofNullable(items);
}Optional with Streams
Optional.stream() (Java 9+) yields a Stream of 0 or 1 element, which lets you flatMap Optionals out of a stream elegantly. This is the cleanest way to skip absent values during stream processing. It avoids the verbose filter(isPresent).map(get) pattern and keeps the pipeline declarative.
import java.util.*;
import java.util.stream.*;
// stream() on Optional: 0 or 1 element stream
Optional<String> opt = Optional.of("hi");
opt.stream().forEach(System.out::println);
// Useful: flatMap Optional out of a stream
class User {
String email; // may be null
User(String e) { email = e; }
Optional<String> getEmail() { return Optional.ofNullable(email); }
}
List<User> users = List.of(
new User("[email protected]"),
new User(null),
new User("[email protected]")
);
// Extract emails, skipping nulls — clean with Optional::stream
List<String> emails = users.stream()
.flatMap(u -> u.getEmail().stream())
.toList(); // [[email protected], [email protected]]
// Without Optional::stream you'd need filter+map
List<String> emails2 = users.stream()
.map(User::getEmail)
.filter(Optional::isPresent)
.map(Optional::get)
.toList();Streams API Deep Dive
Creating Streams
Streams can be created from collections, arrays, or static factories. iterate() and generate() produce infinite streams — always follow with limit(). iterate with a predicate (Java 9+) is safer than bare iterate. IntStream/LongStream/DoubleStream avoid boxing for numeric work. Streams are single-use: once a terminal operation runs, the stream is consumed.
import java.util.*;
import java.util.stream.*;
// From collections
Stream<String> s1 = List.of("a", "b").stream();
Stream<String> s2 = Set.of("x").stream();
// From arrays
int[] nums = {1, 2, 3};
IntStream s3 = Arrays.stream(nums);
Stream<String> s4 = Arrays.stream(new String[]{"a", "b"});
// Static factory methods
Stream<Integer> s5 = Stream.of(1, 2, 3);
Stream<Integer> s6 = Stream.empty();
Stream<Integer> s7 = Stream.iterate(1, n -> n * 2); // infinite
Stream<Integer> s8 = Stream.iterate(1, n -> n < 100, n -> n + 1); // bounded (Java 9+)
Stream<Double> s9 = Stream.generate(Math::random); // infinite
Stream<String> s10 = Stream.ofNullable(null); // 0 or 1 element (Java 9+)
// From functions (infinite, must limit)
List<Integer> powers = Stream.iterate(1, n -> n * 2)
.limit(10)
.toList();
// Numeric ranges
IntStream range = IntStream.range(0, 5); // 0,1,2,3,4
IntStream closed = IntStream.rangeClosed(1, 5); // 1,2,3,4,5Intermediate Operations
Intermediate operations are lazy — they don't run until a terminal operation is invoked. filter keeps elements, map transforms 1:1, flatMap transforms 1:many. distinct/sorted/limit/skip are stateful. takeWhile/dropWhile (Java 9+) stop at the first non-matching element (unlike filter, which scans everything). Use peek for debugging, not for side effects in production.
import java.util.*;
import java.util.stream.*;
List<Integer> nums = List.of(3, 1, 4, 1, 5, 9, 2, 6, 5);
// filter: keep matching
List<Integer> evens = nums.stream()
.filter(n -> n % 2 == 0).toList();
// map: transform
List<String> labels = nums.stream()
.map(n -> "n=" + n).toList();
// flatMap: one-to-many
List<Integer> expanded = List.of(List.of(1, 2), List.of(3))
.stream().flatMap(List::stream).toList(); // [1,2,3]
// distinct: remove duplicates
List<Integer> uniq = nums.stream().distinct().toList();
// sorted
List<Integer> asc = nums.stream().sorted().toList();
List<Integer> desc = nums.stream().sorted(Comparator.reverseOrder()).toList();
// peek: inspect (mainly for debugging)
nums.stream().peek(n -> System.out.println("seen " + n)).count();
// limit / skip
List<Integer> first3 = nums.stream().limit(3).toList();
List<Integer> after2 = nums.stream().skip(2).toList();
// takeWhile / dropWhile (Java 9+)
List<Integer> lt5 = nums.stream().takeWhile(n -> n < 5).toList();Collectors: grouping & partitioning
Collectors.groupingBy is the SQL GROUP BY of Java streams. The classifier function defines the key; an optional downstream collector processes each group (counting, summing, mapping, etc.). partitioningBy is a special case with a boolean predicate (exactly two buckets). Pass a TreeMap supplier for sorted keys. These compose powerfully — you can build multi-level groupings.
import java.util.*;
import java.util.stream.*;
record Person(String name, String city, int age) {}
List<Person> people = List.of(
new Person("Alice", "NYC", 30),
new Person("Bob", "LA", 25),
new Person("Carol", "NYC", 35),
new Person("Dave", "LA", 40)
);
// groupingBy: Map<key, List<item>>
Map<String, List<Person>> byCity = people.stream()
.collect(Collectors.groupingBy(Person::city));
// {NYC=[Alice,Carol], LA=[Bob,Dave]}
// groupingBy with downstream collector
Map<String, Long> countByCity = people.stream()
.collect(Collectors.groupingBy(Person::city, Collectors.counting()));
Map<String, Integer> sumAgeByCity = people.stream()
.collect(Collectors.groupingBy(Person::city,
Collectors.summingInt(Person::age)));
Map<String, List<String>> namesByCity = people.stream()
.collect(Collectors.groupingBy(Person::city,
Collectors.mapping(Person::name, Collectors.toList())));
// partitioningBy: Map<Boolean, List> (2 buckets)
Map<Boolean, List<Person>> byAge = people.stream()
.collect(Collectors.partitioningBy(p -> p.age() >= 30));
// groupingBy with TreeMap for sorted keys
Map<String, List<Person>> sorted = people.stream()
.collect(Collectors.groupingBy(Person::city, TreeMap::new, Collectors.toList()));Reducing & Statistics
reduce combines all elements into a single value — provide an identity for safety with empty streams. summaryStatistics gives count/sum/min/max/avg in one pass. Collectors.joining is handy for building delimited strings. teeing (Java 12+) runs two collectors in parallel and merges their results — useful when you need two aggregates (like min and max) in one pass.
import java.util.*;
import java.util.stream.*;
List<Integer> nums = List.of(1, 2, 3, 4, 5);
// reduce: combine all into one
int sum = nums.stream().reduce(0, Integer::sum); // 15
Optional<Integer> product = nums.stream().reduce((a, b) -> a * b); // Optional[120]
// Built-in summary collectors
IntSummaryStatistics stats = nums.stream()
.mapToInt(Integer::intValue)
.summaryStatistics();
// stats.getCount(), getSum(), getMin(), getMax(), getAverage()
// Common terminal collectors
long count = nums.stream().collect(Collectors.counting());
double avg = nums.stream().collect(Collectors.averagingInt(Integer::intValue));
int max = nums.stream().collect(Collectors.maxBy(Comparator.naturalOrder())).orElse(0);
// join strings
String joined = List.of("a", "b", "c").stream()
.collect(Collectors.joining(", ", "[", "]")); // "[a, b, c]"
// teeing (Java 12+): two collectors, merge results
record Range(int min, int max) {}
Range range = nums.stream().collect(Collectors.teeing(
Collectors.minBy(Comparator.naturalOrder()),
Collectors.maxBy(Comparator.naturalOrder()),
(mn, mx) -> new Range(mn.orElse(0), mx.orElse(0))
));Numeric Streams
IntStream/LongStream/DoubleStream are primitive specializations that avoid autoboxing overhead — use them for numeric work. mapToInt/mapToLong/mapToDouble convert object streams to primitive streams; boxed() goes back. Primitive streams have specialized terminal ops (sum, average, max) that return OptionalInt/Double to handle empty streams. Great for performance-sensitive numeric pipelines.
import java.util.stream.*;
import java.util.*;
// IntStream / LongStream / DoubleStream avoid boxing
IntStream range = IntStream.rangeClosed(1, 100);
int sum = range.sum(); // 5050
double avg = IntStream.of(1, 2, 3).average().orElse(0);
// mapToInt / mapToLong / mapToDouble from object stream
int totalAge = people().stream().mapToInt(Person::age).sum();
// boxed: convert primitive stream back to object stream
List<Integer> list = IntStream.range(0, 5).boxed().toList();
// mapToObj: primitive -> objects
List<String> labels = IntStream.range(1, 4)
.mapToObj(i -> "item-" + i).toList();
// asLongStream / asDoubleStream
LongStream longs = IntStream.range(0, 5).asLongStream();
// Common numeric operations
int max = IntStream.of(3, 1, 4, 1, 5).max().orElse(Integer.MIN_VALUE);
boolean anyEven = IntStream.of(1, 3, 5).anyMatch(n -> n % 2 == 0);
// Iterate to build numeric sequences
List<Integer> fib = Stream.iterate(new int[]{0, 1}, a -> new int[]{a[1], a[0] + a[1]})
.limit(10).mapToInt(a -> a[0]).boxed().toList();
List<Person> people() { return List.of(new Person("a", "c", 30)); }
record Person(String name, String city, int age) {}Parallel Streams
parallelStream splits work across the common ForkJoinPool (sized to CPU cores). Only use it for large datasets with CPU-intensive, stateless, order-independent operations — for small data the overhead exceeds the benefit. Avoid shared mutable state (causes races). I/O in parallel streams blocks the shared pool — use a custom ForkJoinPool for blocking work. Measure before assuming parallel is faster.
import java.util.*;
import java.util.stream.*;
// parallelStream: uses common ForkJoinPool
long sum = List.of(1, 2, 3, 4, 5).parallelStream()
.mapToInt(Integer::intValue).sum();
// Convert sequential to parallel
long count = IntStream.range(0, 1_000_000).parallel()
.filter(n -> n % 2 == 0).count();
// Order may differ — use forEachOrdered if order matters
List.of(1, 2, 3, 4).parallelStream()
.forEachOrdered(System.out::println);
// Collecting preserves encounter order (but work is parallel)
List<Integer> doubled = IntStream.range(0, 1000).parallel()
.map(n -> n * 2).boxed().toList();
// Custom thread pool (avoid blocking the common pool)
import java.util.concurrent.ForkJoinPool;
ForkJoinPool pool = new ForkJoinPool(8);
int result = pool.submit(() ->
IntStream.range(0, 1000).parallel().sum()
).get();
// WHEN to use parallel: large dataset, CPU-heavy per element,
// order-independent, stateless operations
// WHEN NOT: small data, I/O-bound, shared mutable state, ordered opsGenerics Deep Dive
Generic Classes & Methods
Generics enable type-safe reusable code. Classes declare type parameters (<T>); methods can too (<T> before the return type). The diamond operator <> infers the type at construction. Generics are checked at compile time — they make collections and APIs safer by catching type errors early instead of at runtime via ClassCastException.
// Generic class
public class Box<T> {
private T value;
public void set(T v) { value = v; }
public T get() { return value; }
}
Box<String> strBox = new Box<>();
strBox.set("hello");
String s = strBox.get(); // no cast needed
// Multiple type parameters
public class Pair<K, V> {
private final K key;
private final V value;
public Pair(K k, V v) { key = k; value = v; }
public K key() { return key; }
public V value() { return value; }
}
Pair<String, Integer> p = new Pair<>("age", 30);
// Generic method (independent of class type params)
public static <T> T first(List<T> list) {
return list.get(0);
}
// Generic method with multiple type params
public static <K, V> Map<K, V> zip(List<K> keys, List<V> values) {
Map<K, V> m = new HashMap<>();
for (int i = 0; i < keys.size(); i++) m.put(keys.get(i), values.get(i));
return m;
}Bounded Type Parameters
Bounded type parameters (<T extends Bound>) restrict what types can be used and let you call methods of the bound. <T extends Number> means T must be a Number or subtype. Multiple bounds use & — at most one class (must be first), the rest interfaces. Bounds are essential for writing algorithms that need specific capabilities (comparability, numeric operations).
// Upper bound: T must be a subtype of Number
public static <T extends Number> double sum(List<T> nums) {
double total = 0;
for (Number n : nums) total += n.doubleValue();
return total;
}
sum(List.of(1, 2, 3)); // Integer is a Number
sum(List.of(1.0, 2.5)); // Double is a Number
// sum(List.of("a")); // compile error
// Multiple bounds: T must extend all (first is class, rest interfaces)
interface Comparable<T> { int compareTo(T o); }
interface Serializable {}
public static <T extends Number & Comparable<T> & Serializable>
T max(List<T> list) {
T best = list.get(0);
for (T t : list) if (t.compareTo(best) > 0) best = t;
return best;
}
// Bound lets you call methods of the bound
public static <T extends CharSequence> int totalLength(List<T> items) {
int len = 0;
for (CharSequence c : items) len += c.length(); // can call .length()
return len;
}Wildcards: ?, extends, super
Wildcards make generic types flexible. ? extends T (covariant) lets you read T but not write — use for producers. ? super T (contravariant) lets you write T but only read Object — use for consumers. The PECS rule (Producer Extends, Consumer Super) guides which to use. copy(dest, src) is the classic example: dest is a consumer (super), src is a producer (extends).
import java.util.*;
// ? (unbounded wildcard) — any type
void printAll(List<?> list) {
for (Object o : list) System.out.println(o);
}
// ? extends T (upper-bounded / covariant) — producer (PECS: Producer Extends)
double sum(List<? extends Number> nums) {
double total = 0;
for (Number n : nums) total += n.doubleValue();
return total;
}
sum(List.of(1, 2, 3)); // List<Integer> OK
sum(List.of(1.0, 2.0)); // List<Double> OK
// nums.add(5); // ERROR: can't add (don't know exact type)
// ? super T (lower-bounded / contravariant) — consumer (PECS: Consumer Super)
void addNumbers(List<? super Integer> list) {
list.add(1); list.add(2); list.add(3); // OK to add Integer
}
addNumbers(new ArrayList<Number>()); // OK
addNumbers(new ArrayList<Object>()); // OK
// Number n = list.get(0); // only safe to read as Object
// PECS rule: Producer Extends, Consumer Super
// If you read from a collection, use ? extends T
// If you write to a collection, use ? super T
public static <T> void copy(List<? super T> dest, List<? extends T> src) {
for (T t : src) dest.add(t);
}Type Erasure
Java generics use type erasure — generic types exist only at compile time; at runtime, List<String> and List<Integer> are both just List. This enables backward compatibility but has limits: you can't do new T(), create generic arrays, use instanceof with generics, or have overloaded methods with same-erased signatures. Heap pollution occurs when unchecked casts put wrong types into generics, deferring errors to runtime.
import java.util.*;
// At runtime, generic types are erased to their bounds (or Object)
// List<String>, List<Integer>, List<?> all become List at runtime
List<String> strings = new ArrayList<>();
List<Integer> ints = new ArrayList<>();
// Runtime: both are just ArrayList
// You CANNOT do these due to erasure:
// new T() — can't instantiate type param
// new T[] — can't create generic array
// instanceof List<String> — only instanceof List (raw)
// class MyException<T> extends Exception — can't extend Throwable generically
// static T field — no static generic fields
// Erasure means overloads clash:
// void process(List<String> list) {}
// void process(List<Integer> list) {} // ERROR: same erasure
// Checking types at runtime requires Class<T>
public static <T> List<T> filter(List<?> items, Class<T> type) {
List<T> result = new ArrayList<>();
for (Object o : items) {
if (type.isInstance(o)) result.add(type.cast(o));
}
return result;
}
// Heap pollution: when unchecked warnings lead to runtime ClassCastException
List<String> polluted = (List<String>)(List) List.of(1, 2); // unchecked
// String s = polluted.get(0); // ClassCastException at runtimeGeneric Methods & Inference
Type inference lets the compiler determine type arguments from context (arguments and target type), so you rarely write them explicitly. The diamond operator <> is inference for constructors. Target typing uses the variable's expected type. Use an explicit type witness (Class.<T>method()) only when inference can't resolve ambiguity. Inference makes generic code read as cleanly as non-generic code.
import java.util.*;
// Type inference: compiler figures out T from arguments
public static <T> T pick(T a, T b) { return Math.random() > 0.5 ? a : b; }
String s = pick("hello", "world"); // T inferred as String
Number n = pick(1, 2.0); // T inferred as Number (common supertype)
// Target typing: inference uses the expected type
List<String> list = Collections.emptyList(); // T inferred from target
// Inference with method chains
List<Integer> nums = List.of(1, 2, 3);
String joined = nums.stream()
.map(Object::toString) // Stream<String>
.collect(Collectors.joining(",")); // inferred
// Explicit type witness (rarely needed)
Collections.<String>emptyList();
// Generic constructor
class Holder<T> {
private T value;
<U extends T> Holder(U init) { value = init; } // constructor type param
T get() { return value; }
}
Holder<Number> h = new Holder<>(42); // U=Integer, T=Number
import java.util.stream.Collectors;
class Math { static double random() { return 0.5; } }Generic Interfaces & Patterns
Generic interfaces (like Repository<T,ID>) define reusable contracts. The self-referencing bound pattern (class X implements Comparable<X>) ensures compareTo only accepts the same type. The type token pattern (using Class<T> as a key) works around erasure to provide runtime type safety in heterogeneous containers. These patterns are the backbone of frameworks like Spring Data.
// Generic interface
interface Repository<T, ID> {
Optional<T> findById(ID id);
List<T> findAll();
void save(T entity);
}
// Implement with concrete types
class UserRepository implements Repository<User, Long> {
public Optional<User> findById(Long id) { /* ... */ return Optional.empty(); }
public List<User> findAll() { return List.of(); }
public void save(User entity) {}
}
// Generic interface with self-referencing bound (Comparable pattern)
interface Comparable<T> {
int compareTo(T other);
}
class Temperature implements Comparable<Temperature> {
private final double celsius;
Temperature(double c) { celsius = c; }
public int compareTo(Temperature other) {
return Double.compare(celsius, other.celsius);
}
}
// Generic builder pattern
class Builder<T> {
private T value;
public Builder<T> set(T v) { value = v; return this; }
public T build() { return value; }
}
// Type token pattern for runtime type safety
class TypeSafeMap {
private final Map<Class<?>, Object> map = new HashMap<>();
public <T> void put(Class<T> type, T value) { map.put(type, value); }
public <T> T get(Class<T> type) { return type.cast(map.get(type)); }
}
record User(String name) {}
import java.util.Optional;Annotations
Built-in Annotations
Java's built-in annotations: @Override (catches typos in overriding — always use it), @Deprecated (signals API shouldn't be used, with since/forRemoval metadata), @SuppressWarnings (silences specific warnings — use narrowly), @FunctionalInterface (enforces the SAM rule). These are the everyday annotations that improve compile-time safety and documentation.
import java.util.*;
// @Override: declares intent to override (compiler checks)
class Animal {
public void sound() { System.out.println("..."); }
}
class Dog extends Animal {
@Override
public void sound() { System.out.println("Woof"); }
}
// @Deprecated: marks API as outdated
class OldApi {
@Deprecated(since = "1.5", forRemoval = true)
public void legacy() {}
}
// @SuppressWarnings: silence compiler warnings
@SuppressWarnings("unchecked")
List<String> list = (List<String>) new ArrayList();
// @FunctionalInterface: enforces single abstract method
@FunctionalInterface
interface Op { int apply(int a, int b); }
// Common warning keys: unchecked, deprecation, rawtypes, null
// Java 17+ sealed-related
@Deprecated
class ToRemove {}Custom Annotations
Custom annotations are declared with @interface. Members look like methods but are annotation attributes — they can have default values. Use @Target to restrict where it applies (TYPE, METHOD, FIELD, etc.) and @Retention to control availability. Marker annotations (no members) just tag elements. Annotations themselves carry no behavior — processors (reflection, annotation tools) read them and act.
import java.lang.annotation.*;
// Define an annotation
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Test {
String value() default "";
long timeout() default 0L;
}
// Use it
class MyTests {
@Test
public void quickCheck() {}
@Test(timeout = 5000)
public void slowCheck() {}
@Test("custom-name")
public void named() {}
}
// Annotation with default values
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.FIELD})
public @interface Entity {
String table() default "";
String[] columns() default {};
}
@Entity(table = "users", columns = {"id", "name"})
class User {}
// Marker annotation (no members)
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Service {}Retention & Target
@Retention controls how long an annotation survives: SOURCE (compile-time only, like @Override), CLASS (in bytecode but not visible at runtime — the default), RUNTIME (accessible via reflection). @Target restricts where an annotation can appear. Java 8+ added TYPE_USE and TYPE_PARAMETER, letting you annotate generics and casts (List<@NonNull String>). Choose RUNTIME only if you need reflection access.
import java.lang.annotation.*;
// RetentionPolicy.SOURCE: discarded by compiler (e.g., @Override)
@Retention(RetentionPolicy.SOURCE)
@interface CompileOnly {}
// RetentionPolicy.CLASS: kept in .class but not loaded (default)
@Retention(RetentionPolicy.CLASS)
@interface BytecodeOnly {}
// RetentionPolicy.RUNTIME: available via reflection at runtime
@Retention(RetentionPolicy.RUNTIME)
@interface RuntimeVisible {}
// ElementType targets
@Target(ElementType.TYPE) // classes, interfaces, enums
@interface ForType {}
@Target(ElementType.METHOD)
@interface ForMethod {}
@Target(ElementType.FIELD)
@interface ForField {}
@Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER})
@interface ForTypeUse {}
// Java 8+ type-use annotations (annotate any type occurrence)
@RuntimeVisible String[] names; // example usage
List<@RuntimeVisible String> typed;Reading Annotations via Reflection
Annotations with RUNTIME retention can be read via reflection: isAnnotationPresent() checks existence, getAnnotation() retrieves it. This is how frameworks (Spring, JUnit, JAX-RS) wire up behavior declaratively — you tag methods/classes, the framework scans and dispatches. Annotation processing at compile time (annotation processors) is an alternative for code generation without runtime reflection cost.
import java.lang.annotation.*;
import java.lang.reflect.*;
import java.util.*;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@interface Route {
String path();
String method() default "GET";
}
class Api {
@Route(path = "/users", method = "GET")
public void listUsers() {}
@Route(path = "/users", method = "POST")
public void createUser() {}
}
// Scan methods for @Route at runtime
for (Method m : Api.class.getDeclaredMethods()) {
if (m.isAnnotationPresent(Route.class)) {
Route r = m.getAnnotation(Route.class);
System.out.println(r.method() + " " + r.path() + " -> " + m.getName());
}
}
// Output:
// GET /users -> listUsers
// POST /users -> createUser
// Reading annotations on a class
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@interface Table { String name(); }
@Table(name = "orders")
class Order {}
Table t = Order.class.getAnnotation(Table.class);
System.out.println(t.name()); // "orders"Repeatable & Meta-Annotations
@Repeatable (Java 8+) lets you apply the same annotation multiple times by defining a container annotation. @Inherited makes annotations propagate to subclasses (only for class-level annotations). @Documented includes the annotation in Javadoc. @Target with ANNOTATION_TYPE creates meta-annotations (annotations that annotate other annotations) — this is how Spring builds composable annotation stereotypes like @RestController = @Controller + @ResponseBody.
import java.lang.annotation.*;
// Repeatable: allow same annotation multiple times
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@interface Schedule {
String cron();
}
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@interface Schedules {
Schedule[] value(); // container annotation
}
// Make Schedule repeatable
@Repeatable(Schedules.class)
@interface Schedule2 {
String cron();
}
// Now you can repeat it (Java 8+)
class Job {
@Schedule2(cron = "0 0 * * *")
@Schedule2(cron = "0 30 * * *")
public void run() {}
}
// Meta-annotations: annotations on annotations
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE) // can only annotate other annotations
@interface TestCategory {}
@TestCategory
@Retention(RetentionPolicy.RUNTIME)
@interface UnitTest {}
// Inherited: subclass inherits the annotation
@Inherited
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@interface Persistent {}
@Persistent class Base {}
class Child extends Base {} // Child also has @Persistent
// Documented: appears in Javadoc
@Documented
@interface PublicApi {}Reflection
Class Object & Getting Classes
Every loaded type has a unique Class object — the entry point to reflection. Get it via class literal (Type.class), instance.getClass(), or Class.forName() (dynamic loading, throws ClassNotFoundException). The Class object exposes name, modifiers, superclass, interfaces, and type checks (isInterface, isArray, isEnum, isRecord). isAssignableFrom checks polymorphic relationships.
import java.lang.reflect.*;
// Three ways to get a Class object
Class<String> c1 = String.class; // class literal
Class<?> c2 = "hello".getClass(); // from instance
Class<?> c3 = Class.forName("java.lang.String"); // by name (throws checked)
// Basic introspection
Class<?> c = String.class;
System.out.println(c.getName()); // "java.lang.String"
System.out.println(c.getSimpleName()); // "String"
System.out.println(c.getPackage()); // "package java.lang"
System.out.println(c.getSuperclass()); // "class java.lang.Object"
System.out.println(Modifier.toString(c.getModifiers())); // "public final"
// Check type relationships
System.out.println(c.isInterface()); // false
System.out.println(c.isArray()); // false
System.out.println(c.isEnum()); // false
System.out.println(c.isRecord()); // false (Java 16+)
System.out.println(CharSequence.class.isAssignableFrom(c)); // true
// Primitive class objects
Class<?> intClass = int.class;
Class<?> intArrayClass = int[].class;
System.out.println(intClass.isPrimitive()); // trueInspecting Fields, Methods, Constructors
getDeclaredFields/Methods/Constructors return ALL members (including private) declared in this class only. getFields/getMethods return only public members but include inherited ones. To find a specific member, use getDeclaredField(name) or getDeclaredMethod(name, paramTypes...) — parameter types are needed to disambiguate overloads. Reflection bypasses access control unless you call setAccessible(true).
import java.lang.reflect.*;
import java.util.*;
class Sample {
public String name;
private int count;
public Sample() {}
public Sample(String n) { name = n; }
private void secret() {}
public int compute(int x) { return x * 2; }
}
Class<?> c = Sample.class;
// Fields: getDeclaredFields includes private; getFields only public
for (Field f : c.getDeclaredFields()) {
System.out.println(f.getName() + " : " + f.getType().getSimpleName()
+ " (" + Modifier.toString(f.getModifiers()) + ")");
}
// Methods
for (Method m : c.getDeclaredMethods()) {
System.out.println(m.getName()
+ " params=" + Arrays.toString(m.getParameterTypes())
+ " returns=" + m.getReturnType().getSimpleName());
}
// Constructors
for (Constructor<?> ctor : c.getConstructors()) {
System.out.println("ctor params=" + Arrays.toString(ctor.getParameterTypes()));
}
// Lookup specific member
Field nameField = c.getDeclaredField("name");
Method compute = c.getDeclaredMethod("compute", int.class);
Constructor<?> ctor = c.getDeclaredConstructor(String.class);Invoking Methods & Creating Instances
Method.invoke(obj, args...) calls a method reflectively — always returns Object, so cast the result. setAccessible(true) bypasses Java access checks (private members become reachable; may require --add-opens on modules). Constructor.newInstance() creates objects — the reflective equivalent of new. Array.newInstance creates arrays of a runtime-known component type. Reflection is slower than direct calls and bypasses compile-time safety.
import java.lang.reflect.*;
class Greeter {
public String greet(String name) { return "Hello, " + name; }
private String secret() { return "hidden"; }
}
Object obj = new Greeter();
Class<?> c = obj.getClass();
// Invoke public method
Method greet = c.getMethod("greet", String.class);
String result = (String) greet.invoke(obj, "Alice"); // "Hello, Alice"
// Invoke private method
Method sec = c.getDeclaredMethod("secret");
sec.setAccessible(true); // bypass access check
String s = (String) sec.invoke(obj); // "hidden"
// Create instances via constructor
Constructor<?> noArg = c.getConstructor();
Object o1 = noArg.newInstance();
// With args
class Person {
String name;
public Person(String n) { name = n; }
public String toString() { return "Person(" + name + ")"; }
}
Constructor<?> ctor = Person.class.getConstructor(String.class);
Object p = ctor.newInstance("Bob");
System.out.println(p); // "Person(Bob)"
// Array creation via reflection
Object strArray = Array.newInstance(String.class, 5);
Array.set(strArray, 0, "first");
String v = (String) Array.get(strArray, 0);Reading & Modifying Fields
Field.get(instance) reads a field's value; Field.set(instance, value) writes it. For primitives, use type-specific accessors (getInt/setInt) to avoid boxing. Static fields take null as the instance argument. setAccessible(true) is required for private fields. Reflection field access is how serialization libraries (Jackson, Gson) and ORM frameworks read/write object state generically.
import java.lang.reflect.*;
class Config {
public String env = "dev";
private int retries = 3;
public static String VERSION = "1.0";
}
Config cfg = new Config();
Class<?> c = cfg.getClass();
// Read public field
Field env = c.getField("env");
String e = (String) env.get(cfg); // "dev"
// Read private field
Field retries = c.getDeclaredField("retries");
retries.setAccessible(true);
int r = retries.getInt(cfg); // 3
// For objects: Object val = field.get(instance);
// Modify fields
env.set(cfg, "prod");
retries.setInt(cfg, 5);
System.out.println(cfg.env); // "prod"
// Static fields: pass null as the instance
Field version = c.getField("VERSION");
String v = (String) version.get(null); // "1.0"
version.set(null, "2.0");
// Type-specific getters/setters avoid boxing
// getInt/setInt, getLong/setLong, getBoolean/setBoolean, etc.
// For reference types, use get()/set()Dynamic Proxies & Use Cases
java.lang.reflect.Proxy creates dynamic proxies that implement interfaces at runtime — an InvocationHandler intercepts every call. This is how Spring AOP, Hibernate lazy loading, and Mockito mocks work. Reflection powers most Java frameworks (DI, ORM, serialization, testing) but has costs: slower than direct calls, weaker type safety, and module-system restrictions. Use it when you need runtime flexibility, not for ordinary code.
import java.lang.reflect.*;
import java.util.*;
// JDK dynamic proxy: implements interfaces at runtime
interface UserService {
String getUser(long id);
void deleteUser(long id);
}
// InvocationHandler intercepts every method call
class LoggingHandler implements InvocationHandler {
private final Object target;
LoggingHandler(Object t) { target = t; }
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("Calling " + method.getName() + "(" + Arrays.toString(args) + ")");
long start = System.nanoTime();
Object result = method.invoke(target, args);
System.out.println(" -> " + result + " (" + (System.nanoTime() - start) + "ns)");
return result;
}
}
UserService real = id -> "user-" + id;
UserService proxied = (UserService) Proxy.newProxyInstance(
UserService.class.getClassLoader(),
new Class<?>[]{UserService.class},
new LoggingHandler(real)
);
proxied.getUser(42); // logs the call and result
// Common reflection use cases:
// - Frameworks: Spring DI, JPA entities, Jackson serialization
// - AOP/proxies: transactions, logging, security
// - Annotation processing at runtime
// - Test frameworks: JUnit discovers @Test methods
// - Code generation tools
// Caution: reflection is slower, bypasses compile-time checks,
// and can break under modules (--add-opens). Prefer alternatives when possible.JDBC & Database Access
Connection & DriverManager
DriverManager.getConnection() opens a database connection — always wrap it in try-with-resources to avoid leaks. Since JDBC 4, drivers auto-register via ServiceLoader, so Class.forName() is rarely needed. URL format varies by vendor. Use Properties for connection options (SSL, timeouts). In production, prefer a connection pool (HikariCP) over direct DriverManager calls.
import java.sql.*;
// Basic connection (try-with-resources auto-closes)
String url = "jdbc:postgresql://localhost:5432/mydb";
try (Connection conn = DriverManager.getConnection(url, "user", "pass")) {
System.out.println("Connected: " + conn.getSchema());
// ... use connection
}
// Modern: no need for Class.forName() with JDBC 4+ (auto-discovery)
// Legacy: Class.forName("org.postgresql.Driver");
// Common URL patterns:
// jdbc:postgresql://host:5432/db
// jdbc:mysql://host:3306/db
// jdbc:oracle:thin:@host:1521:db
// jdbc:sqlite:/path/to/db.sqlite
// jdbc:h2:mem:test (in-memory H2)
// Connection properties
import java.util.Properties;
Properties props = new Properties();
props.setProperty("user", "user");
props.setProperty("password", "pass");
props.setProperty("ssl", "true");
try (Connection c = DriverManager.getConnection(url, props)) {
// ...
}Statement vs PreparedStatement
Always use PreparedStatement over Statement for any query with parameters — it prevents SQL injection by separating SQL structure from data. Parameters are set by 1-based index with type-specific setters. PreparedStatements can be reused (re-set params and execute again) and support batching (addBatch/executeBatch) for bulk operations. Statement is fine only for static, trusted SQL like DDL.
import java.sql.*;
// Statement: plain SQL (vulnerable to injection — avoid for user input)
try (Connection c = DriverManager.getConnection("jdbc:h2:mem:", "sa", "");
Statement st = c.createStatement()) {
st.execute("CREATE TABLE users (id INT PRIMARY KEY, name VARCHAR(100))");
st.execute("INSERT INTO users VALUES (1, 'Alice')");
// NEVER do this with user input — SQL injection!
// st.execute("SELECT * FROM users WHERE name = '" + userInput + "'");
}
// PreparedStatement: parameterized, safe from injection, can be reused
String sql = "INSERT INTO users (id, name) VALUES (?, ?)";
try (Connection c = DriverManager.getConnection("jdbc:h2:mem:", "sa", "");
PreparedStatement ps = c.prepareStatement(sql)) {
// Set parameters by index (1-based!)
ps.setInt(1, 1);
ps.setString(2, "Alice");
ps.executeUpdate();
// Reuse with different params
ps.setInt(1, 2);
ps.setString(2, "Bob");
ps.executeUpdate();
// Batch inserts
for (int i = 3; i <= 100; i++) {
ps.setInt(1, i);
ps.setString(2, "user" + i);
ps.addBatch();
}
ps.executeBatch();
}ResultSet & Querying
ResultSet is a cursor over query rows — call next() to advance (returns false at end). Read columns by name (readable) or 1-based index. wasNull() distinguishes SQL NULL from a primitive default (e.g., getInt returns 0 for NULL). Default ResultSets are forward-only; TYPE_SCROLL_INSENSITIVE + CONCUR_UPDATABLE enables random access and in-place updates, though this is rarely used in modern apps.
import java.sql.*;
import java.util.*;
record User(int id, String name, String email) {}
// Execute query and map rows
String sql = "SELECT id, name, email FROM users WHERE active = ?";
List<User> users = new ArrayList<>();
try (Connection c = getConnection();
PreparedStatement ps = c.prepareStatement(sql)) {
ps.setBoolean(1, true);
try (ResultSet rs = ps.executeQuery()) {
while (rs.next()) { // advance to next row
users.add(new User(
rs.getInt("id"), // by column name
rs.getString("name"),
rs.getString("email") // by column name (preferred)
));
}
}
}
// Column access by index (1-based) or name (more readable)
// rs.getInt(1), rs.getString(2), rs.getBoolean("active")
// Handle NULLs
String nick = rs.getString("nickname");
if (rs.wasNull()) nick = "anonymous"; // distinguish NULL from real null
// Scrollable/updatable ResultSet (needs specific flags)
Statement st = c.createStatement(
ResultSet.TYPE_SCROLL_INSENSITIVE,
ResultSet.CONCUR_UPDATABLE
);
ResultSet rs = st.executeQuery("SELECT * FROM users");
rs.absolute(5); // jump to row 5
rs.updateString("name", "newname");
rs.updateRow(); // persist change
Connection getConnection() throws SQLException {
return DriverManager.getConnection("jdbc:h2:mem:", "sa", "");
}Transactions & Batch
JDBC auto-commits every statement by default — set autoCommit(false) to group statements into a transaction. Commit on success, rollback on failure. Savepoints allow partial rollback within a transaction. Isolation levels control visibility of concurrent changes (READ_COMMITTED is the common default; SERIALIZABLE is safest but slowest). Always restore autoCommit or close the connection to avoid leaking transaction state.
import java.sql.*;
// Transactions: disabled by default (auto-commit = true)
try (Connection c = getConnection()) {
c.setAutoCommit(false); // start transaction
try (PreparedStatement ps = c.prepareStatement(
"UPDATE accounts SET balance = balance - ? WHERE id = ?")) {
ps.setInt(1, 100); ps.setInt(2, 1); ps.executeUpdate(); // debit
ps.setInt(1, -100); ps.setInt(2, 2); ps.executeUpdate(); // credit
}
c.commit(); // commit both
// If any step fails, rollback
} catch (SQLException e) {
// connection auto-closed; rollback happens implicitly on close if not committed
}
// Explicit rollback pattern
try (Connection c = getConnection()) {
c.setAutoCommit(false);
try {
// ... multiple statements
c.commit();
} catch (SQLException e) {
c.rollback(); // undo all changes in this transaction
throw e;
}
}
// Savepoints: partial rollback
Statement st = c.createStatement();
st.execute("INSERT INTO log VALUES (1)");
Savepoint sp = c.setSavepoint("before-risky");
st.execute("INSERT INTO log VALUES (2)");
c.rollback(sp); // undo only after savepoint
c.commit();
// Transaction isolation levels
c.setTransactionIsolation(Connection.TRANSACTION_READ_COMMITTED);
// Levels: NONE, READ_UNCOMMITTED, READ_COMMITTED, REPEATABLE_READ, SERIALIZABLE
Connection getConnection() throws SQLException {
return DriverManager.getConnection("jdbc:h2:mem:", "sa", "");
}Connection Pooling (HikariCP)
Connection pools (HikariCP is the de-facto standard) keep connections warm and reuse them, avoiding the 10-100ms cost of opening a new TCP+auth connection per request. Configure max pool size (bounded by DB capacity), timeouts, and lifetime. Borrow with getConnection(), return by closing (it goes back to the pool, not closed). Always close the DataSource on shutdown. In Spring Boot, HikariCP is auto-configured.
import com.zaxxer.hikari.*;
import java.sql.*;
import java.util.*;
// HikariCP: high-performance JDBC connection pool
HikariConfig config = new HikariConfig();
config.setJdbcUrl("jdbc:postgresql://localhost:5432/mydb");
config.setUsername("user");
config.setPassword("pass");
config.setMaximumPoolSize(10);
config.setMinimumIdle(2);
config.setConnectionTimeout(30_000); // ms to wait for a connection
config.setIdleTimeout(600_000); // ms before idle connections close
config.setMaxLifetime(1_800_000); // ms max connection lifetime
config.setPoolName("app-pool");
HikariDataSource ds = new HikariDataSource(config);
// Borrow a connection, use it, return it (auto via try-with-resources)
try (Connection c = ds.getConnection();
PreparedStatement ps = c.prepareStatement("SELECT * FROM users")) {
try (ResultSet rs = ps.executeQuery()) {
while (rs.next()) System.out.println(rs.getString("name"));
}
} // connection returns to pool here
// Shutdown pool when app stops
ds.close();
// Why pool? Opening a TCP+auth connection is expensive (~10-100ms).
// A pool keeps connections warm and reuses them across requests,
// dramatically reducing latency under load.
// Alternative pools: Apache DBCP, c3p0, Tomcat JDBC, AgroalI/O & NIO Deep Dive
InputStream & OutputStream (Bytes)
InputStream/OutputStream handle raw bytes. Always wrap in Buffered* variants — unbuffered I/O does a system call per byte, which is catastrophic for performance. read() returns -1 at end-of-stream. transferTo() (Java 9+) does an efficient bulk copy. readAllBytes() is convenient but loads the entire stream into memory — only for small files. Always close streams (try-with-resources).
import java.io.*;
// Read bytes from a file
try (InputStream in = new FileInputStream("input.bin")) {
int b;
while ((b = in.read()) != -1) { // -1 = end of stream
// process byte
}
}
// Buffered for performance (8KB default buffer)
try (InputStream in = new BufferedInputStream(new FileInputStream("big.bin"))) {
byte[] buffer = new byte[8192];
int read;
while ((read = in.read(buffer)) != -1) {
// process buffer[0..read]
}
}
// readAllBytes (small files only — loads everything into memory)
byte[] all = new FileInputStream("small.bin").readAllBytes();
// Write bytes
try (OutputStream out = new BufferedOutputStream(new FileOutputStream("out.bin"))) {
out.write(65); // single byte
out.write(new byte[]{66, 67, 68});
out.flush(); // force buffered data to disk
}
// Copy streams (Java 9+)
try (InputStream in = new FileInputStream("src.bin");
OutputStream out = new FileOutputStream("dst.bin")) {
in.transferTo(out); // efficient bulk copy
}
// Standard streams
System.in.read(); // stdin (InputStream)
System.out.write(65); // stdout (PrintStream)Channels & Buffers (NIO)
NIO Channels + ByteBuffers are the high-performance alternative to streams. Buffers have position/limit/capacity; flip() switches from write to read mode, clear() resets for writing, compact() preserves unread data. Direct buffers (allocateDirect) live outside the JVM heap and avoid a copy step for large I/O. transferTo between channels can use zero-copy on supported OSes. Use NIO when streaming performance matters.
import java.nio.*;
import java.nio.channels.*;
import java.nio.file.*;
import java.io.*;
// Channel: high-performance, block-oriented I/O
try (FileChannel ch = FileChannel.open(Path.of("data.bin"),
StandardOpenOption.READ, StandardOpenOption.WRITE)) {
// ByteBuffer: fixed-capacity block of bytes
ByteBuffer buf = ByteBuffer.allocate(1024);
int read = ch.read(buf); // fill buffer from channel
buf.flip(); // switch from write mode to read mode
while (buf.hasRemaining()) {
byte b = buf.get();
}
buf.clear(); // reset for next read (or compact() to preserve unread)
}
// Direct buffer: outside JVM heap, faster for large I/O (no copy)
ByteBuffer direct = ByteBuffer.allocateDirect(64 * 1024);
// Scatter/gather: read into multiple buffers / write from multiple
ByteBuffer header = ByteBuffer.allocate(128);
ByteBuffer body = ByteBuffer.allocate(1024);
ch.read(new ByteBuffer[]{header, body}); // scatter
// Transfer between channels (zero-copy on some OSes)
try (FileChannel src = FileChannel.open(Path.of("a.bin"));
FileChannel dst = FileChannel.open(Path.of("b.bin"), StandardOpenOption.WRITE)) {
src.transferTo(0, src.size(), dst);
}
// ByteOrder
buf.order(ByteOrder.LITTLE_ENDIAN);
int value = buf.getInt();Path Operations (NIO.2)
Path (NIO.2) replaces the old File class. Path operations (getFileName, getParent, resolve, normalize, relativize) are pure string math — they don't touch the disk. Files.* methods interact with the filesystem: size, timestamps, permissions, symlinks. normalize() cleans up . and .. segments. resolveSibling is handy for renaming (same directory, different name). Prefer Path over File in modern code.
import java.nio.file.*;
import java.nio.file.attribute.*;
import java.io.IOException;
// Creating Path objects
Path p1 = Path.of("a/b/c.txt"); // relative
Path p2 = Path.of("/usr/local/bin"); // absolute
Path p3 = Paths.get("config.json"); // older API, same thing
// Path manipulation (doesn't touch filesystem)
System.out.println(p1.getFileName()); // "c.txt"
System.out.println(p1.getParent()); // "a/b"
System.out.println(p1.getRoot()); // "" (relative)
System.out.println(p2.getRoot()); // "/"
System.out.println(p1.toAbsolutePath()); // "/cwd/a/b/c.txt"
System.out.println(p1.normalize()); // removes . and ..
System.out.println(p1.resolve("d.txt")); // "a/b/c.txt/d.txt"
System.out.println(p1.resolveSibling("x.txt")); // "a/b/x.txt"
System.out.println(p1.relativize(Path.of("a/b"))); // "../.."
// File metadata
Path file = Path.of("notes.txt");
System.out.println(Files.size(file)); // bytes
System.out.println(Files.getLastModifiedTime(file));
System.out.println(Files.isReadable(file));
System.out.println(Files.isWritable(file));
// PosixFilePermissions (Linux/macOS)
String perms = "rwxr-xr--";
Set<PosixFilePermission> set = PosixFilePermissions.fromString(perms);
Files.setPosixFilePermissions(file, set);
// Symbolic links
Path link = Path.of("link.txt");
Files.createSymbolicLink(link, Path.of("target.txt"));
Path target = Files.readSymbolicLink(link);Directory Walking & File Trees
Files.list() lists one level; Files.walk() recursively walks the tree (returns a Stream, must close). Files.find() filters by path and attributes during the walk. For full control, walkFileTree with a FileVisitor lets you skip subtrees, handle errors, and act before/after visiting directories. All return Streams that hold file handles — always use try-with-resources. Use max depth to bound expensive walks.
import java.nio.file.*;
import java.io.IOException;
import java.util.stream.*;
// List directory entries (one level)
try (Stream<Path> entries = Files.list(Path.of("."))) {
entries.filter(Files::isRegularFile)
.map(Path::getFileName)
.forEach(System.out::println);
}
// Walk file tree recursively (depth-first)
try (Stream<Path> walk = Files.walk(Path.of("/project"))) {
walk.filter(Files::isRegularFile)
.filter(p -> p.toString().endsWith(".java"))
.forEach(System.out::println);
}
// Walk with depth limit
try (Stream<Path> walk = Files.walk(Path.of("/project"), 3)) {
walk.forEach(System.out::println);
}
// Find with BiPredicate (path + attributes)
try (Stream<Path> found = Files.find(Path.of("/logs"), 10,
(path, attrs) -> attrs.isRegularFile()
&& attrs.size() > 1_000_000
&& path.toString().endsWith(".log"))) {
found.forEach(p -> System.out.println("Large log: " + p));
}
// FileVisitor for full control (pre/post visit, skip subtrees)
Files.walkFileTree(Path.of("/project"), new SimpleFileVisitor<>() {
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
System.out.println("Visiting " + file);
return FileVisitResult.CONTINUE;
}
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) {
if (dir.getFileName().toString().equals("target")) {
return FileVisitResult.SKIP_SUBTREE; // skip target dirs
}
return FileVisitResult.CONTINUE;
}
});
import java.nio.file.attribute.BasicFileAttributes;
import java.nio.file.attribute.BasicFileAttributes;WatchService & File Events
WatchService receives filesystem events (create, modify, delete) for registered directories. take() blocks until an event arrives; pollEvents() drains them. Watch the parent directory and filter by context() to track a specific file. Events can be coalesced or lost (OVERFLOW). Recursive watching requires registering every subdirectory. WatchService is OS-native (inotify on Linux, FSEvents on macOS) but its API is low-level — consider libraries for complex needs.
import java.nio.file.*;
import static java.nio.file.StandardWatchEventKinds.*;
import java.io.IOException;
// Watch a directory for changes
try (WatchService watcher = FileSystems.getDefault().newWatchService()) {
Path dir = Path.of("/var/log");
dir.register(watcher,
ENTRY_CREATE,
ENTRY_DELETE,
ENTRY_MODIFY);
System.out.println("Watching " + dir + "...");
while (true) {
WatchKey key = watcher.take(); // blocks until event
for (WatchEvent<?> event : key.pollEvents()) {
Path changed = dir.resolve((Path) event.context());
System.out.println(event.kind() + " -> " + changed);
if (event.kind() == OVERFLOW) continue; // events lost
// React to change
if (event.kind() == ENTRY_CREATE) {
System.out.println("New file: " + changed);
}
}
if (!key.reset()) break; // key no longer valid (dir deleted)
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
// Note: WatchService watches directories, not individual files.
// To watch a file, watch its parent dir and filter by context().
// Events may be coalesced (rapid modifications may yield one event).
// For recursive watching, register each subdirectory manually
// or use a library like Apache Commons IO's FileAlterationMonitor.Records & Pattern Matching
Records Basics
Records (Java 16+) are transparent immutable data carriers. The header `record Name(Type1 f1, Type2 f2)` generates a constructor, accessors (f1(), f2() — not getF1()), equals, hashCode, and toString. They're ideal for DTOs, value objects, and function results. The compact constructor (just `{ ... }`) validates or normalizes without reassigning fields. Records can implement interfaces but can't extend classes.
// Record: concise immutable data carrier (Java 16+)
public record Point(int x, int y) {}
// Equivalent to writing:
// - final class with private final int x, y
// - constructor Point(int x, int y)
// - accessors x(), y()
// - equals, hashCode, toString (all auto-generated)
Point p = new Point(3, 4);
System.out.println(p.x()); // 3 (accessor, not getX())
System.out.println(p.y()); // 4
System.out.println(p); // "Point[x=3, y=4]"
System.out.println(p.equals(new Point(3, 4))); // true
// Immutable: no setters
// p.setX(5); // no such method
// Custom record with validation
public record Age(int value) {
public Age {
if (value < 0 || value > 150) {
throw new IllegalArgumentException("Invalid age: " + value);
}
}
}
// Record with multiple components
public record Employee(String name, int id, double salary, String dept) {}
// Records implement interfaces
public interface Named { String name(); }
public record Customer(String name, long id) implements Named {}Compact Constructors & Custom Methods
The compact constructor (`public Name { ... }`) runs before field assignment — assign to the parameter to normalize, and the compiler assigns it to the field. Records can have additional methods and static factories but no instance fields beyond the header. Non-canonical constructors must delegate to the canonical one via this(...). Use static factories for clearer construction (Point.origin()) and to cache common instances.
// Compact constructor: validation without reassigning
public record Email(String address) {
public Email {
if (!address.contains("@")) {
throw new IllegalArgumentException("Bad email: " + address);
}
address = address.toLowerCase().strip(); // normalize
}
}
// Add custom methods (but not mutable fields)
public record Money(double amount, String currency) {
public Money plus(Money other) {
if (!currency.equals(other.currency)) {
throw new IllegalArgumentException("Currency mismatch");
}
return new Money(amount + other.amount, currency);
}
public Money times(int factor) {
return new Money(amount * factor, currency);
}
public static Money usd(double amt) { return new Money(amt, "USD"); }
}
// Canonical + non-canonical constructors
public record Range(int start, int end) {
// Compact canonical (validation)
public Range {
if (start > end) throw new IllegalArgumentException();
}
// Non-canonical convenience constructor
public Range(int end) {
this(0, end);
}
}
// Static factories are common on records
public record Point(int x, int y) {
public static Point origin() { return new Point(0, 0); }
public static Point of(int x, int y) { return new Point(x, y); }
}Sealed Classes
Sealed classes/interfaces (Java 17+) restrict which types can extend them via the `permits` clause. Each permitted subtype must be final, sealed, or non-sealed. Combined with records, they form algebraic data types (closed inheritance + immutable data). The key benefit: the compiler knows all subtypes, so switch expressions can be exhaustive without a default branch — if you add a new subtype, the compiler flags every switch that needs updating.
// Sealed class: restricts which classes can extend it (Java 17+)
public sealed interface Shape
permits Circle, Square, Triangle {}
public record Circle(double radius) implements Shape {}
public record Square(double side) implements Shape {}
public record Triangle(double base, double height) implements Shape {}
// Every permitted subtype must be final, sealed, or non-sealed
public non-sealed class WeirdShape implements Shape {} // open again
// Why sealed? Enables exhaustive pattern matching
public double area(Shape s) {
return switch (s) {
case Circle c -> Math.PI * c.radius() * c.radius();
case Square sq -> sq.side() * sq.side();
case Triangle t -> 0.5 * t.base() * t.height();
// No default needed — compiler knows all subtypes
};
}
// Sealed class hierarchy with records = algebraic data types
// Combines: closed inheritance (sealed) + immutable data (record)
// Sealed classes also work with classes (not just interfaces)
public sealed abstract class Result permits Success, Failure {}
public final class Success<T>(T value) extends Result {}
public final class Failure(String error) extends Result {}Pattern Matching for instanceof
Pattern matching for instanceof (Java 16+) declares a variable that's bound only when the test succeeds, eliminating the explicit cast. The variable's scope flows from the pattern's truth — usable in && continuations and after early returns. Java 21 added pattern matching in switch (case Type var when guard), enabling type-based dispatch with guards. This makes type-checking code far more concise and less error-prone.
// Old way: cast after instanceof
Object obj = "hello";
if (obj instanceof String) {
String s = (String) obj; // redundant cast
System.out.println(s.length());
}
// Java 16+: pattern variable
if (obj instanceof String s) {
System.out.println(s.length()); // s is in scope, no cast
}
// The variable is scoped by the pattern's truth
if (obj instanceof String s && s.length() > 3) {
System.out.println("Long string: " + s);
}
// s is NOT in scope here if the && short-circuited
// Negation scoping
if (!(obj instanceof String s)) {
// s NOT in scope here
return;
}
// s IS in scope here (the instanceof must have been true)
// Combining with other patterns
Object data = 42;
String desc = switch (data) {
case Integer i when i > 0 -> "positive int: " + i;
case Integer i -> "non-positive int: " + i;
case String s -> "string: " + s;
case null -> "null";
default -> "other";
};Switch Pattern Matching (Java 21)
Switch pattern matching (Java 21, final) lets you switch on types, destructure records, and add guards (when). Combined with sealed types, the compiler verifies exhaustiveness — no default needed if all subtypes are covered. Record patterns (case Point(int x, int y)) destructure in one step. null case is explicit (no NPE). This brings Java close to ML/Scala's pattern matching for modeling domains declaratively.
// Java 21: switch pattern matching with type patterns and guards
sealed interface Event permits Login, Logout, Message, Error {}
record Login(String user) implements Event {}
record Logout(String user) implements Event {}
record Message(String from, String text) implements Event {}
record Error(int code, String detail) implements Event {}
String describe(Event e) {
return switch (e) {
case Login l -> l.user() + " logged in";
case Logout l -> l.user() + " logged out";
case Message m when m.from().equals("system") -> "[system] " + m.text();
case Message m -> m.from() + ": " + m.text();
case Error(int code, String detail) when code >= 500 -> "SERVER ERROR " + code;
case Error(int code, String detail) -> "error " + code + ": " + detail;
case null -> "no event"; // explicit null handling
};
}
// Record patterns: destructure records in one go
record Point(int x, int y) {}
String classify(Object o) {
return switch (o) {
case Point(int x, int y) when x == y -> "diagonal";
case Point(int x, int y) -> "point at (" + x + "," + y + ")";
default -> "not a point";
};
}
// Nested patterns
record Box(Point p) {}
String describe2(Object o) {
return switch (o) {
case Box(Point(int x, int y)) -> "box at " + x + "," + y;
default -> "unknown";
};
}Modules (JPMS)
module-info.java Basics
module-info.java declares a module (Java 9+ JPMS). requires adds a dependency; exports makes packages accessible; opens allows reflective access (needed by serialization/DI frameworks); uses/provides wire up ServiceLoader. Without this file, code lives on the classpath as an 'unnamed module' with legacy behavior. Modules give strong encapsulation (only exported packages are public) and reliable configuration (explicit dependencies).
// File: src/com.example.app/module-info.java
module com.example.app {
requires java.sql; // depends on java.sql module
requires transitive com.example.lib; // re-export (consumers see it too)
requires static java.annotation; // compile-time only (optional at runtime)
exports com.example.app.api; // public API visible to all
exports com.example.app.internal to com.example.test; // qualified export
opens com.example.app.model to com.fasterxml.jackson.databind; // reflection only
opens com.example.app.dynamic; // open to all for reflection
uses com.example.app.spi.Plugin; // service consumer
provides com.example.app.spi.Plugin with com.example.app.plugins.DefaultPlugin; // service provider
}
// Key directives:
// requires: depends on another module
// exports: makes packages public to other modules
// opens: allows deep reflection (for frameworks like Jackson, Hibernate)
// uses/provides: service loader integration
// A module without module-info.java is an "unnamed module" (classpath behavior)requires, exports, opens
requires declares a dependency; requires transitive propagates it (use when your public API exposes types from that module). exports makes packages public; exports to restricts to named modules (qualified exports). opens grants reflective access (deep reflection) — essential for frameworks that setAccessible(true). The distinction between exports (public API) and opens (reflection) is key: strong encapsulation is the default, and you opt in per package.
// Module A: com.example.library
module com.example.library {
// Public API anyone can use
exports com.example.library.api;
// Internal package: only visible to specific modules
exports com.example.library.internal to com.example.app;
// Allow reflection for frameworks (Jackson, JPA)
opens com.example.library.model;
// Only specific module can reflect
opens com.example.library.config to com.example.app;
// Dependencies
requires java.logging;
requires transitive java.sql; // consumers of A also get java.sql
}
// Module B: com.example.app (consumer)
module com.example.app {
requires com.example.library; // use A's exported packages
// Note: transitive means java.sql is also available here
requires com.fasterxml.jackson.databind;
}
// 'requires transitive X' means: any module requiring this one
// also reads X. Use when your exported API exposes X's types.
// 'opens' vs 'exports':
// exports: compile-time + runtime access to public members
// opens: runtime reflective access to ALL members (including private)
// Reflective access without 'opens' fails with InaccessibleObjectException
// in Java 16+ (strong encapsulation enforced by default).ServiceLoader & Services
ServiceLoader implements the SPI pattern: an interface in one module, implementations discovered at runtime. The API module exports the interface; provider modules declare `provides X with Y`; consumer modules declare `uses X`. ServiceLoader.load(X.class) finds all providers on the module path. This decouples interface from implementation — JDBC drivers, logging backends (SLF4J), and Charset providers all work this way. No compile-time dependency on the implementation.
// SPI (Service Provider Interface) pattern with modules
// 1. Define the service interface in an API module
module com.example.spi {
exports com.example.spi;
}
package com.example.spi;
public interface Plugin {
String name();
void run();
}
// 2. Implement in a provider module
module com.example.plugin.impl {
requires com.example.spi;
provides com.example.spi.Plugin with com.example.plugin.impl.DefaultPlugin;
}
package com.example.plugin.impl;
import com.example.spi.Plugin;
public class DefaultPlugin implements Plugin {
public String name() { return "default"; }
public void run() { System.out.println("running"); }
}
// 3. Consume in an app module
module com.example.app {
requires com.example.spi;
uses com.example.spi.Plugin; // declares intent to load services
}
// Loading services at runtime
import java.util.ServiceLoader;
ServiceLoader<Plugin> loader = ServiceLoader.load(Plugin.class);
for (Plugin p : loader) {
System.out.println("Found: " + p.name());
p.run();
}
// ServiceLoader is how JDBC drivers, SLF4J backends, and many
// plugin systems are discovered without compile-time dependencies.Module Path vs Classpath
The module path (--module-path) holds modular JARs with strong encapsulation; the classpath (-cp) holds legacy JARs as unnamed modules with no encapsulation. Modular JARs work on both. Automatic modules are JARs without module-info placed on the module path — their name comes from the filename or Automatic-Module-Name manifest attribute. --add-opens is an escape hatch for legacy reflection that breaks under strong encapsulation.
// Compile a module
// javac -d out/com.example.app \
// --module-source-path src \
// --module com.example.app
// Run a modular app
// java --module-path out --module com.example.app/com.example.app.Main
// Package as a modular JAR (includes module-info.class)
// jar --create --file app.jar --main-class com.example.app.Main -C out/com.example.app .
// Module path vs classpath:
// --module-path: modules with module-info, strong encapsulation enforced
// --class-path (or -cp): legacy "unnamed module", everything public, no encapsulation
// Mixing: modular JARs can be used on the classpath too (automatic module)
// java -cp lib/app.jar:lib/dep.jar com.example.app.Main
// An automatic module: a JAR without module-info on the module path.
// Its name is derived from the JAR filename (Automatic-Module-Name in MANIFEST.MF
// gives an explicit name).
// Inspect a module JAR
// jar --describe-module --file app.jar
// List observable modules
// java --list-modules
// java --describe-module java.sql
// Add opens at runtime for legacy reflection (escape hatch)
// java --add-opens com.example.app/com.example.app.internal=ALL-UNNAMEDjdeps & jlink (Custom Runtimes)
jdeps analyzes bytecode to list module dependencies — useful for migrating to modules and finding unused deps. jlink creates a custom JRE containing only the modules your app needs, producing a self-contained, smaller, faster-starting runtime. This is ideal for Docker images and installers: ship the app plus a 30-50MB JRE instead of requiring a 300MB JDK install. Together, jdeps + jlink enable lean, self-contained Java deployments.
# jdeps: analyze dependencies (find unused, list required modules)
# List dependencies of a JAR
jdeps --module-path lib app.jar
# Generate module-info.java for an existing JAR
jdeps --generate-module-info ./out app.jar
# Show which JDK modules a JAR uses
jdeps --print-module-deps --ignore-missing-deps app.jar
# Output: java.base,java.logging,java.sql
# jlink: create a custom stripped-down JRE containing only needed modules
jlink \
--module-path "$JAVA_HOME/jmods:./out" \
--add-modules com.example.app \
--output ./custom-jre \
--strip-debug \
--compress=zip-6 \
--no-header-files \
--no-man-pages \
--launcher app=com.example.app/com.example.app.Main
# The custom JRE is self-contained:
# ./custom-jre/bin/app # launches the app
# ./custom-jre/bin/java # the stripped JVM
# Benefits of jlink:
# - Smaller distribution (only needed modules)
# - Faster startup (less to load)
# - No need to install Java on target machine
# - Can cross-target (different OS/arch) with matching jmods
# Common workflow:
# 1. jdeps to find required modules
# 2. jlink to build a custom runtime
# 3. Package app + custom JRE together (Docker image, installer)Concurrency Deep Dive
Locks: ReentrantLock & ReadWriteLock
ReentrantLock offers more control than synchronized: tryLock (non-blocking/timed), fairness, interruptibility, and lock state inspection. Always unlock in finally. ReadWriteLock allows many concurrent readers but exclusive writers — great for read-heavy caches. StampedLock (Java 8+) adds optimistic reads for even better read throughput. Prefer synchronized for simple cases; use Lock when you need its advanced features.
import java.util.concurrent.locks.*;
// ReentrantLock: more flexible than synchronized
class Counter {
private final ReentrantLock lock = new ReentrantLock();
private int count = 0;
public void increment() {
lock.lock();
try {
count++;
} finally {
lock.unlock(); // MUST be in finally
}
}
public int get() {
lock.lock();
try { return count; }
finally { lock.unlock(); }
}
}
// tryLock with timeout (avoids deadlock-induced hangs)
if (lock.tryLock(1, java.util.concurrent.TimeUnit.SECONDS)) {
try { /* work */ } finally { lock.unlock(); }
}
// Fair lock (FIFO ordering, slower)
ReentrantLock fair = new ReentrantLock(true);
// ReadWriteLock: many readers OR one writer
class Cache {
private final ReentrantReadWriteLock rw = new ReentrantReadWriteLock();
private final Lock read = rw.readLock();
private final Lock write = rw.writeLock();
private java.util.Map<String, String> data = new java.util.HashMap<>();
public String get(String key) {
read.lock();
try { return data.get(key); }
finally { read.unlock(); }
}
public void put(String key, String val) {
write.lock();
try { data.put(key, val); }
finally { write.unlock(); }
}
}Concurrent Collections
ConcurrentHashMap is the workhorse thread-safe map — use compute/merge for atomic updates instead of check-then-act. CopyOnWriteArrayList is best for read-heavy, write-rare lists (event listeners) — writes copy the array. BlockingQueue is the backbone of producer-consumer pipelines (put blocks when full, take blocks when empty). ConcurrentLinkedQueue is non-blocking and unbounded. These replace synchronized wrappers (Collections.synchronizedX) which are coarse-grained and slower.
import java.util.concurrent.*;
import java.util.*;
// ConcurrentHashMap: thread-safe HashMap (no null keys/values)
ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
map.put("a", 1);
map.putIfAbsent("a", 2); // only if absent
map.compute("a", (k, v) -> v == null ? 1 : v + 1); // atomic update
map.merge("a", 1, Integer::sum); // add 1 atomically
Integer val = map.getOrDefault("a", 0);
// CopyOnWriteArrayList: snapshot semantics, fast reads, slow writes
CopyOnWriteArrayList<String> listeners = new CopyOnWriteArrayList<>();
listeners.add("x"); // copies entire array
for (String l : listeners) { /* safe iteration, no ConcurrentModificationException */ }
// BlockingQueue: producer-consumer pattern
BlockingQueue<String> queue = new ArrayBlockingQueue<>(100);
// Producer
queue.put("task"); // blocks if full
boolean added = queue.offer("task", 1, TimeUnit.SECONDS); // timed
// Consumer
String task = queue.take(); // blocks if empty
String polled = queue.poll(1, TimeUnit.SECONDS);
// ConcurrentLinkedQueue: non-blocking, unbounded
Queue<String> q = new ConcurrentLinkedQueue<>();
q.offer("a");
// SkipListMap / SkipListSet: concurrent sorted collections
ConcurrentNavigableMap<Integer, String> sorted = new ConcurrentSkipListMap<>();
import java.util.concurrent.TimeUnit;CountDownLatch & CyclicBarrier
CountDownLatch is a one-shot gate — N threads count down, others await; not resettable. CyclicBarrier is reusable — threads wait for each other at a rendezvous point, with an optional action when all arrive. Phaser is the most flexible: variable parties, multiple phases, tree structure. Use latch for startup coordination, barrier for parallel multi-phase algorithms, phaser for dynamic participant counts.
import java.util.concurrent.*;
import java.util.*;
// CountDownLatch: one-shot gate, N threads must arrive before proceeding
CountDownLatch ready = new CountDownLatch(3);
List<String> results = Collections.synchronizedList(new ArrayList<>());
for (int i = 0; i < 3; i++) {
final int id = i;
new Thread(() -> {
try { Thread.sleep(id * 100); } catch (InterruptedException e) {}
results.add("worker-" + id);
ready.countDown(); // signal done
}).start();
}
ready.await(); // main thread blocks until count reaches 0
System.out.println("All done: " + results);
// CyclicBarrier: reusable barrier, threads wait for each other
CyclicBarrier barrier = new CyclicBarrier(3, () ->
System.out.println("--- phase complete ---"));
Runnable worker = () -> {
try {
System.out.println(Thread.currentThread().getName() + " phase 1");
barrier.await(); // wait for all 3
System.out.println(Thread.currentThread().getName() + " phase 2");
barrier.await(); // reusable for next phase
} catch (Exception e) {}
};
for (int i = 0; i < 3; i++) new Thread(worker).start();
// Phaser: more flexible (variable parties, multiple phases)
Phaser phaser = new Phaser(3);
phaser.register(); // dynamically add a party
phaser.arriveAndAwaitAdvance();Semaphore & Exchanger
Semaphore controls access to N permits — acquire blocks until one is available, release returns it. Use for rate limiting, connection pools, or any bounded-resource scenario. tryAcquire offers non-blocking and timed variants. Exchanger lets two threads swap values at a rendezvous point — niche but elegant for pipeline designs where two threads trade buffers. Both are in java.util.concurrent and are lower-level than locks for some coordination patterns.
import java.util.concurrent.*;
// Semaphore: limit concurrent access to N permits
Semaphore pool = new Semaphore(5); // 5 concurrent allowed
pool.acquire(); // blocks until a permit is available
try {
// critical section (at most 5 threads here at once)
System.out.println("Working, permits left: " + pool.availablePermits());
} finally {
pool.release(); // return the permit
}
// Try-acquire (non-blocking)
if (pool.tryAcquire()) {
try { /* work */ } finally { pool.release(); }
} else {
System.out.println("Too busy, try later");
}
// Timed acquire
if (pool.tryAcquire(1, TimeUnit.SECONDS)) {
try { /* work */ } finally { pool.release(); }
}
// Use case: rate limiting, connection pools, parking lots
// Exchanger: two threads swap values
Exchanger<String> ex = new Exchanger<>();
new Thread(() -> {
try {
String got = ex.exchange("from-A"); // gives "from-A", receives "from-B"
System.out.println("A got: " + got);
} catch (InterruptedException e) {}
}).start();
String got = ex.exchange("from-B"); // gives "from-B", receives "from-A"
System.out.println("B got: " + got);
import java.util.concurrent.TimeUnit;CompletableFuture Advanced
CompletableFuture is Java's Promise — compose async work with thenApply (map), thenCompose (flatMap), thenCombine (zip two). allOf waits for all, anyOf for the first. exceptionally recovers from errors; handle covers both. orTimeout (Java 9+) cancels if it takes too long. Always pass an explicit executor — the default commonPool can starve under blocking work. This is the foundation of reactive-style async code in Java.
import java.util.concurrent.*;
import java.util.*;
ExecutorService pool = Executors.newFixedThreadPool(4);
// Async composition (like JS Promises)
CompletableFuture<String> future = CompletableFuture
.supplyAsync(() -> "hello", pool) // async supplier
.thenApply(String::toUpperCase) // transform
.thenCompose(s -> CompletableFuture.supplyAsync( // flatMap
() -> s + " world"))
.thenApply(s -> s + "!");
System.out.println(future.join()); // "HELLO world!"
// Combine two independent futures
CompletableFuture<Integer> a = CompletableFuture.supplyAsync(() -> 10);
CompletableFuture<Integer> b = CompletableFuture.supplyAsync(() -> 20);
CompletableFuture<Integer> sum = a.thenCombine(b, Integer::sum);
System.out.println(sum.join()); // 30
// Run multiple and wait for all (or any)
List<CompletableFuture<Integer>> futures = List.of(
CompletableFuture.supplyAsync(() -> 1, pool),
CompletableFuture.supplyAsync(() -> 2, pool),
CompletableFuture.supplyAsync(() -> 3, pool)
);
CompletableFuture<Void> all = CompletableFuture.allOf(
futures.toArray(new CompletableFuture[0]));
all.join();
List<Integer> results = futures.stream().map(CompletableFuture::join).toList();
// Error handling
CompletableFuture<String> safe = CompletableFuture
.supplyAsync(() -> { throw new RuntimeException("boom"); })
.exceptionally(ex -> "fallback: " + ex.getMessage());
System.out.println(safe.join()); // "fallback: boom"
// handle: both success and failure
CompletableFuture<String> handled = CompletableFuture
.supplyAsync(() -> "ok")
.handle((val, ex) -> ex == null ? val : "error");
// Timeout (Java 9+)
CompletableFuture<String> timed = CompletableFuture
.supplyAsync(() -> { Thread.sleep(5000); return "slow"; })
.orTimeout(1, TimeUnit.SECONDS)
.exceptionally(ex -> "timed out");Virtual Threads (Java 21)
Virtual threads (Java 21) are lightweight threads scheduled by the JVM on a small pool of carrier (platform) threads. When a virtual thread blocks on I/O, it's suspended and the carrier runs another — so you can have millions of concurrent blocking operations. This lets you write simple blocking code instead of complex reactive/async chains. Use them for I/O-bound workloads (HTTP handlers, DB calls); for CPU-bound work, platform threads or parallelStream are still appropriate.
import java.util.concurrent.*;
import java.util.*;
// Virtual threads: lightweight, cheap, millions possible
// Java 21 LTS feature — "Project Loom"
// Start a virtual thread
Thread vt = Thread.ofVirtual().start(() -> {
System.out.println("Running on: " + Thread);
});
// Builder pattern
Thread vt2 = Thread.ofVirtual().name("worker-1").start(() -> {
// blocking I/O here is fine — virtual thread yields, not the OS thread
});
// Per-thread factory
ThreadFactory factory = Thread.ofVirtual().factory();
// ExecutorService for virtual threads (Java 21+)
try (ExecutorService es = Executors.newVirtualThreadPerTaskExecutor()) {
// Submit a million tasks — each gets its own virtual thread
List<Future<String>> futures = new ArrayList<>();
for (int i = 0; i < 1_000_000; i++) {
final int id = i;
futures.add(es.submit(() -> {
Thread.sleep(100); // blocking call is cheap on virtual threads
return "done-" + id;
}));
}
// virtual threads yield when blocking, so 1M concurrent is feasible
}
// Why virtual threads?
// - Platform threads (OS threads) are heavy (~1MB stack, kernel scheduling)
// - Virtual threads are user-mode, ~KB, scheduled by JVM on a small carrier pool
// - Blocking I/O on a virtual thread doesn't block a platform thread
// - Lets you write straightforward blocking code at scale (no reactive complexity)
// Best practice: use virtual threads for I/O-bound work,
// NOT for CPU-bound work (use platform threads / parallelStream).Collections Framework Deep Dive
Comparators & Sorting
Comparator.comparing(keyExtractor) builds comparators from a key function — far cleaner than writing raw compare logic. thenComparing chains secondary sort keys. nullsFirst/nullsLast handle nulls safely. Use comparingInt/comparingLong/comparingDouble to avoid autoboxing. List.sort() sorts in place (mutable lists only); Stream.sorted() returns a new sorted stream. Comparators power sorting, TreeSet/TreeMap ordering, and stream operations.
import java.util.*;
import java.util.stream.*;
record Person(String name, int age) {}
List<Person> people = List.of(
new Person("Alice", 30),
new Person("Bob", 25),
new Person("Carol", 30),
new Person("Dave", 25)
);
// Comparator.comparing (key extractor)
people.stream().sorted(Comparator.comparing(Person::name)).toList();
people.stream().sorted(Comparator.comparingInt(Person::age)).toList();
// Reversed
people.stream().sorted(Comparator.comparing(Person::age).reversed()).toList();
// Chained (thenComparing): sort by age, then by name
List<Person> sorted = people.stream().sorted(
Comparator.comparingInt(Person::age)
.thenComparing(Person::name)
).toList();
// [Bob(25), Dave(25), Alice(30), Carol(30)]
// Nulls handling
Comparator<String> cmp = Comparator.nullsFirst(Comparator.naturalOrder());
List.of("b", null, "a").stream().sorted(cmp).toList(); // [null, a, b]
// Custom comparator
Comparator<Person> byNameLen = (a, b) -> a.name().length() - b.name().length();
// Mutable list sort
List<String> names = new ArrayList<>(List.of("charlie", "alice", "bob"));
names.sort(Comparator.naturalOrder());
// names = [alice, bob, charlie]
// Comparing with primitive specializations avoids boxing
Comparator<Person> byAge = Comparator.comparingInt(Person::age);Unmodifiable & Immutable Collections
List.of/Set.of/Map.of (Java 9+) create truly immutable collections — no nulls, no mutations. Collections.unmodifiableX creates a read-only view that still reflects changes to the backing collection. List.copyOf (Java 10+) makes an independent immutable copy. Arrays.asList is a fixed-size view of an array (set works, add/remove don't). Choose based on need: immutable factories for constants, unmodifiable views to expose internals safely, copyOf for defensive copies.
import java.util.*;
// Java 9+ immutable factories (List.of, Set.of, Map.of)
List<String> immutable = List.of("a", "b", "c");
Set<Integer> set = Set.of(1, 2, 3);
Map<String, Integer> map = Map.of("a", 1, "b", 2);
Map<String, Integer> bigMap = Map.ofEntries(
Map.entry("x", 1), Map.entry("y", 2), Map.entry("z", 3)
);
// immutable.add("d"); // throws UnsupportedOperationException
// immutable.set(0, "z"); // throws
// nulls not allowed in these immutable collections
// Unmodifiable view (wraps an existing collection)
List<String> mutable = new ArrayList<>(List.of("a", "b"));
List<String> view = Collections.unmodifiableList(mutable);
// view.add("c"); // throws
mutable.add("c"); // but changes to backing list ARE visible in view
System.out.println(view); // [a, b, c]
// CopyOf (Java 10+): creates immutable copy
List<String> copy = List.copyOf(mutable); // independent immutable copy
// Arrays.asList: fixed-size view of an array
String[] arr = {"a", "b"};
List<String> fixed = Arrays.asList(arr);
fixed.set(0, "x"); // OK (writes through to array)
// fixed.add("c"); // throws (size fixed)
// To make a truly mutable copy:
List<String> mut = new ArrayList<>(Arrays.asList(arr));Queue & Deque Implementations
ArrayDeque is the preferred stack and queue implementation — faster than the legacy Stack (which is synchronized) and LinkedList. PriorityQueue orders elements by a Comparator (min-heap by default) — use for scheduling, top-K problems. Deque supports both ends; use addFirst/removeFirst for stack semantics, addLast/removeFirst for queue semantics. For concurrent queues, use the java.util.concurrent implementations (LinkedBlockingQueue, etc.).
import java.util.*;
// Queue: FIFO (offer/poll/peek)
Queue<String> queue = new LinkedList<>();
queue.offer("a"); queue.offer("b");
System.out.println(queue.peek()); // "a" (head)
System.out.println(queue.poll()); // "a" (remove head)
// Deque: double-ended (add/remove at both ends)
Deque<String> deque = new ArrayDeque<>();
deque.addFirst("a"); deque.addLast("b");
System.out.println(deque.peekFirst()); // "a"
System.out.println(deque.peekLast()); // "b"
deque.removeFirst(); deque.removeLast();
// ArrayDeque as a stack (push/pop/peek)
Deque<String> stack = new ArrayDeque<>();
stack.push("first"); // addFirst
stack.push("second");
System.out.println(stack.pop()); // "second" (LIFO)
System.out.println(stack.peek()); // "first"
// PriorityQueue: orders by Comparator (not insertion order)
PriorityQueue<Integer> pq = new PriorityQueue<>();
pq.offer(5); pq.offer(1); pq.offer(3);
System.out.println(pq.poll()); // 1 (smallest first)
System.out.println(pq.poll()); // 3
// With custom comparator
PriorityQueue<String> byLen = new PriorityQueue<>(Comparator.comparingInt(String::length));
byLen.offer("aaa"); byLen.offer("a"); byLen.offer("aa");
System.out.println(byLen.poll()); // "a"
// BlockingQueue implementations (concurrency): see concurrency-deep section
// ArrayDeque is faster than Stack/LinkedList for stack/queue useMap merge, compute, getOrDefault
These Map methods make common patterns atomic and concise. merge is the word-counting idiom — combine existing and new value, removing the entry if the function returns null. computeIfAbsent is the lazy-cache pattern (memoization). getOrDefault avoids null checks. replaceAll transforms all values. These are far cleaner than the check-then-act get/put dance, and they're the building blocks for ConcurrentHashMap's atomic updates in concurrent code.
import java.util.*;
Map<String, Integer> counts = new HashMap<>();
// getOrDefault: safe read with default
int n = counts.getOrDefault("missing", 0); // 0, no null
// putIfAbsent: only set if not present
counts.putIfAbsent("a", 1); // sets to 1
counts.putIfAbsent("a", 2); // no change (already present)
// compute: recompute value for a key
counts.compute("a", (k, v) -> v == null ? 1 : v + 1); // increment
// computeIfAbsent: lazy initialization (cache pattern)
Map<String, List<String>> groups = new HashMap<>();
groups.computeIfAbsent("key", k -> new ArrayList<>()).add("item");
// creates list if absent, then adds — atomic, no race in single thread
// computeIfPresent: update only if present
counts.computeIfPresent("a", (k, v) -> v > 0 ? v - 1 : null); // decrement, remove at 0
// merge: combine existing and new value (great for counting)
counts.merge("word", 1, Integer::sum); // word count pattern
counts.merge("word", 1, Integer::sum); // now 2
counts.merge("word", 1, Integer::sum); // now 3
// merge with removal: function returns null -> entry removed
counts.merge("word", 1, (old, v) -> old > 1 ? old - 1 : null);
// replaceAll: transform all values
Map<String, Integer> doubled = new HashMap<>(counts);
doubled.replaceAll((k, v) -> v * 2);
// Word frequency counter (idiomatic)
String text = "the cat the dog the bird";
Map<String, Integer> freq = new HashMap<>();
for (String w : text.split(" ")) {
freq.merge(w, 1, Integer::sum);
}Collections Utility Methods
Collections.* provides classic utility: sort, binarySearch (requires sorted input), shuffle, reverse, frequency, min/max. singleton/empty factories return immutable single-element or empty collections — prefer emptyList() over returning null. nCopies is memory-efficient (one element shared). Synchronized wrappers exist for legacy code but prefer java.util.concurrent collections. Checked wrappers catch generic-type violations at runtime, useful for interop with raw types.
import java.util.*;
import java.util.stream.*;
List<Integer> nums = new ArrayList<>(List.of(3, 1, 4, 1, 5, 9, 2, 6));
// Sorting
Collections.sort(nums); // in-place, natural order
Collections.sort(nums, Comparator.reverseOrder());
nums.sort(Comparator.naturalOrder()); // List.sort (preferred)
// Searching (list must be sorted first)
int idx = Collections.binarySearch(nums, 4); // index, or negative if absent
// Shuffling & reversing
Collections.shuffle(nums);
Collections.reverse(nums);
// Frequency & disjoint
int freq = Collections.frequency(nums, 1); // count occurrences
boolean dis = Collections.disjoint(List.of(1, 2), List.of(3, 4)); // true
// Min/max
int min = Collections.min(nums);
int max = Collections.max(nums, Comparator.reverseOrder());
// Singleton collections (immutable, single element)
Set<String> one = Collections.singleton("only");
List<Integer> oneList = Collections.singletonList(42);
Map<String, Integer> oneMap = Collections.singletonMap("k", 1);
// Empty collections (prefer over returning null)
List<Object> empty = Collections.emptyList();
Set<Object> emptySet = Collections.emptySet();
// nCopies (immutable list of n copies)
List<String> padding = Collections.nCopies(5, "x"); // [x,x,x,x,x]
// Synchronized wrappers (legacy — prefer concurrent collections)
List<String> sync = Collections.synchronizedList(new ArrayList<>());
// Checked wrappers (catch heap pollution at runtime)
List<String> checked = Collections.checkedList(new ArrayList<>(), String.class);
// checked.add(123); // throws ClassCastException at the add siteTesting (JUnit 5 & Mockito)
JUnit 5 Basics
JUnit 5 (Jupiter) annotations: @Test marks a test; @BeforeEach/@AfterEach run around each test; @BeforeAll/@AfterAll run once for the class (must be static). @DisplayName customizes test names. @Disabled skips tests. assertThrows verifies exceptions. Tests should be independent — use @BeforeEach to reset state, not static fields. JUnit 5 lives in org.junit.jupiter.api (different from JUnit 4's org.junit).
import org.junit.jupiter.api.*;
import static org.junit.jupiter.api.Assertions.*;
class CalculatorTest {
private Calculator calc;
@BeforeAll
static void setUpOnce() {
// runs once before all tests (must be static)
System.out.println("Starting CalculatorTest");
}
@BeforeEach
void setUp() {
// runs before each test
calc = new Calculator();
}
@Test
@DisplayName("2 + 2 should equal 4")
void addsTwoNumbers() {
assertEquals(4, calc.add(2, 2));
}
@Test
void dividesByZeroThrows() {
ArithmeticException ex = assertThrows(
ArithmeticException.class,
() -> calc.divide(1, 0)
);
assertEquals("Division by zero", ex.getMessage());
}
@AfterEach
void tearDown() {
// runs after each test (cleanup)
calc = null;
}
@AfterAll
static void tearDownOnce() {
// runs once after all tests
}
@Disabled("until bug #42 is fixed")
@Test
void skippedTest() {}
}
class Calculator {
int add(int a, int b) { return a + b; }
int divide(int a, int b) {
if (b == 0) throw new ArithmeticException("Division by zero");
return a / b;
}
}Assertions
JUnit 5 assertions: assertEquals/assertNotEquals, assertTrue/False, assertNull/NotNull, assertSame (identity). assertAll groups checks so all run even if some fail. assertTimeout fails slow tests. Messages can be strings or Suppliers (lazy — built only on failure, avoiding string concatenation when tests pass). assertIterableEquals compares ordered collections. These come from org.junit.jupiter.api.Assertions.
import org.junit.jupiter.api.*;
import static org.junit.jupiter.api.Assertions.*;
class AssertionsTest {
@Test
void equality() {
assertEquals(4, 2 + 2);
assertEquals(4, 2 + 2, "math is broken");
assertNotEquals(5, 2 + 2);
}
@Test
void booleans() {
assertTrue(5 > 3);
assertFalse(5 < 3, "5 should not be less than 3");
}
@Test
void nullness() {
assertNull(null);
assertNotNull(new Object());
}
@Test
void sameInstance() {
String a = "x";
assertSame(a, a); // ==
assertNotSame(a, new String("x"));
}
@Test
void collections() {
assertIterableEquals(List.of(1, 2, 3), List.of(1, 2, 3));
assertLinesMatch(List.of("a.*", "b"), List.of("abc", "b"));
}
@Test
void groupedAssertions() {
// All run even if one fails — reports all failures
assertAll("person",
() -> assertEquals("Alice", "Alice"),
() -> assertEquals(30, 30),
() -> assertNotNull("x")
);
}
@Test
void timeout() {
// Fails if it takes longer than 100ms
assertTimeout(Duration.ofMillis(100), () -> {
Thread.sleep(10);
});
// assertTimeoutPreemptively: stops the task early (in another thread)
}
@Test
void customMessage() {
int result = 5;
assertEquals(4, result, () -> "expected 4 but got " + result);
// Supplier<String> — message built lazily only on failure
}
import java.time.Duration;
import java.util.List;
}Parameterized Tests
Parameterized tests run the same test logic with multiple inputs. @ValueSource provides a single-arg array. @CsvSource maps CSV rows to multiple params. @MethodSource (most flexible) uses a static Stream<Arguments>. @EnumSource iterates enum values. @NullAndEmptySource adds null/empty cases. This eliminates copy-paste test methods and makes data-driven testing clean. Display names auto-include the parameters for easy diagnosis.
import org.junit.jupiter.params.*;
import org.junit.jupiter.params.provider.*;
import static org.junit.jupiter.api.Assertions.*;
class ParameterizedTests {
@ParameterizedTest
@ValueSource(ints = {1, 2, 3, 4, 5})
void positiveNumbersArePositive(int n) {
assertTrue(n > 0);
}
@ParameterizedTest
@ValueSource(strings = {"", " ", "\t"})
void blankStrings(String s) {
assertTrue(s.isBlank());
}
@ParameterizedTest
@NullAndEmptySource
@ValueSource(strings = {" ", "\t"})
void blankOrNull(String s) {
assertTrue(s == null || s.isBlank());
}
@ParameterizedTest
@CsvSource({
"1, 1, 2",
"2, 3, 5",
"10, -5, 5"
})
void addition(int a, int b, int expected) {
assertEquals(expected, a + b);
}
@ParameterizedTest
@CsvFileSource(resources = "/testdata.csv", numLinesToSkip = 1)
void fromCsv(int a, int b, int expected) {
assertEquals(expected, a + b);
}
@ParameterizedTest
@MethodSource("additionProvider")
void fromMethod(int a, int b, int expected) {
assertEquals(expected, a + b);
}
static java.util.stream.Stream<Arguments> additionProvider() {
return java.util.stream.Stream.of(
Arguments.of(1, 1, 2),
Arguments.of(2, 3, 5)
);
}
@ParameterizedTest
@EnumSource(TimeUnit.class)
void allEnums(TimeUnit unit) {
assertNotNull(unit);
}
}Lifecycle, Nested & Conditional
@Nested creates inner test classes that share lifecycle — great for BDD-style 'when X then Y' structures where outer setup applies to inner tests. Conditional annotations (@EnabledOnOs, @EnabledIfSystemProperty, @EnabledIfEnvironmentVariable) skip tests based on environment. @Tag groups tests for selective execution (e.g., fast vs slow, unit vs integration). Nested classes can't have @BeforeAll (they're non-static). These features make test organization expressive.
import org.junit.jupiter.api.*;
import static org.junit.jupiter.api.Assertions.*;
import org.junit.jupiter.api.condition.*;
class LifecycleTest {
@Test
void topLevel() {}
@Nested
@DisplayName("when stack is empty")
class WhenEmpty {
@BeforeEach
void setUp() { /* stack is empty here */ }
@Test
void throwsOnPop() {
assertThrows(Exception.class, () -> {});
}
@Nested
@DisplayName("after pushing one element")
class AfterPush {
@BeforeEach
void push() { /* push one */ }
@Test
void popReturnsElement() {
// ...
}
}
}
// Conditional execution
@Test
@EnabledOnOs(OS.LINUX)
void onlyOnLinux() {}
@Test
@EnabledIfSystemProperty(named = "env", matches = "ci")
void onlyInCi() {}
@Test
@EnabledIfEnvironmentVariable(named = "DATABASE", matches = "postgres")
void onlyWithPostgres() {}
@Test
@DisabledIf("customCondition")
void conditional() {}
static boolean customCondition() { return java.time.LocalTime.now().getHour() < 9; }
// Tagging for selective runs
@Test
@Tag("slow")
void slowIntegrationTest() {}
@Test
@Tag("fast")
void fastUnitTest() {}
// Run only fast: mvn test -Dgroups=fast
import org.junit.jupiter.api.condition.OS;
import java.util.concurrent.TimeUnit;Mockito Mocking
Mockito creates test doubles for dependencies. @Mock creates a mock; @InjectMocks builds a real object with mocks injected. when(...).thenReturn(...) stubs return values; verify(...) checks interactions. Argument matchers (any(), eq(), argThat()) flexibly match calls. Spies wrap real objects (partial mocking). The Arrange-Act-Assert pattern keeps tests readable. Mocking isolates the unit under test from its dependencies (database, network, time).
import org.junit.jupiter.api.*;
import org.mockito.*;
import static org.mockito.Mockito.*;
import static org.junit.jupiter.api.Assertions.*;
class UserServiceTest {
@Mock
UserRepository repo; // mock dependency
@InjectMocks
UserService service; // real service with mocks injected
@BeforeEach
void setUp() {
MockitoAnnotations.openMocks(this);
}
@Test
void returnsUserWhenFound() {
// Arrange: stub the mock
User alice = new User(1, "Alice");
when(repo.findById(1)).thenReturn(alice);
// Act
String name = service.getUserName(1);
// Assert
assertEquals("Alice", name);
verify(repo).findById(1); // called once
verify(repo, never()).findById(2);
}
@Test
void throwsWhenNotFound() {
when(repo.findById(99)).thenReturn(null);
// or: when(repo.findById(99)).thenThrow(new RuntimeException());
assertThrows(RuntimeException.class, () -> service.getUserName(99));
}
@Test
void argumentMatchers() {
when(repo.findById(anyInt())).thenReturn(new User(0, "default"));
// matchers: eq(), any(), anyInt(), contains(), argThat()
service.getUserName(42);
verify(repo).findById(intThat(n -> n > 0));
}
@Test
void verifyInteractionDetails() {
service.getUserName(1);
verify(repo, times(1)).findById(1);
verify(repo, atLeastOnce()).findById(anyInt());
verifyNoMoreInteractions(repo);
}
@Test
void spy_partialMock() {
List<String> spy = spy(new ArrayList<>());
spy.add("real");
when(spy.size()).thenReturn(100); // stub one method
assertEquals(100, spy.size()); // stubbed
assertEquals(1, spy.size()); // wait, this would be 100 too
}
}
interface UserRepository { User findById(int id); }
record User(int id, String name) {}
class UserService {
UserRepository repo;
UserService(UserRepository r) { repo = r; }
String getUserName(int id) {
User u = repo.findById(id);
if (u == null) throw new RuntimeException("not found");
return u.name();
}
}JUnit Testing
Basic Test
JUnit 5 uses @Test from org.junit.jupiter.api. assertEquals verifies expected equals actual. Other assertions: assertTrue, assertThrows, assertAll.
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
class CalcTest {
@Test
void testAdd() {
assertEquals(5, calc.add(2, 3));
}
}Parameterized Tests
Parameterized tests run the same test with different inputs. @ValueSource provides single arguments. @CsvSource provides multiple arguments. Reduces test duplication.
@ParameterizedTest
@ValueSource(ints = {1, 2, 3, 4})
void testPositive(int n) { assertTrue(n > 0); }
@ParameterizedTest
@CsvSource({"1,2,3", "4,5,9"})
void testAdd(int a, int b, int expected) {
assertEquals(expected, calc.add(a, b));
}Lifecycle Methods
@BeforeAll/@AfterAll run once per class (must be static). @BeforeEach/@AfterEach run around each test. Use for database connections and mock setup.
class DbTest {
@BeforeAll static void setupAll() { /* once before all */ }
@AfterAll static void tearDownAll() { /* once after all */ }
@BeforeEach void setup() { /* before each test */ }
@AfterEach void tearDown() { /* after each test */ }
}Assertions
assertAll runs all assertions even if some fail. assertThrows verifies code throws a specific exception. Use assertTimeout for time-bounded tests.
@Test
void testAll() {
assertAll("person",
() -> assertEquals("Alice", p.getName()),
() -> assertEquals(30, p.getAge())
);
}
@Test
void testException() {
assertThrows(IllegalArgumentException.class, () -> service.process(-1));
}Mockito
@Mock creates mock objects, @InjectMocks injects them. when().thenReturn() stubs calls. verify() checks method was called. Mockito is the standard mocking framework.
@ExtendWith(MockitoExtension.class)
class UserServiceTest {
@Mock UserRepository repo;
@InjectMocks UserService service;
@Test
void testFind() {
when(repo.findById(1)).thenReturn(Optional.of(new User("Alice")));
assertEquals("Alice", service.findUser(1).getName());
verify(repo).findById(1);
}
}Maven/Gradle
Maven POM
Maven uses pom.xml. groupId/artifactId/version identify the project. Dependencies have scope (compile, test, provided). Maven enforces standard directory structure.
<project>
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>myapp</artifactId>
<version>1.0.0</version>
<dependencies>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.10.0</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>Maven Commands
Maven lifecycle: clean, compile, test, package, install, deploy. Each phase runs preceding phases. Use -DskipTests to skip tests.
mvn clean # Remove target/
mvn compile # Compile sources
mvn test # Run tests
mvn package # Build JAR
mvn install # Install to local repo
mvn dependency:tree # Show dependency treeGradle Build
Gradle uses build.gradle (Groovy) or build.gradle.kts (Kotlin). implementation for compile deps, testImplementation for test. Gradle is faster than Maven.
plugins { id 'java'; id 'application' }
repositories { mavenCentral() }
dependencies {
implementation 'com.google.guava:guava:32.1.3-jre'
testImplementation 'org.junit.jupiter:junit-jupiter:5.10.0'
}
application { mainClass = 'com.example.App' }Gradle Commands
Gradle tasks: build, test, run, clean. The wrapper (gradlew) ensures consistent versions. Use --parallel for parallel module builds.
gradle clean # Clean build
gradle build # Build + test
gradle test # Run tests
gradle run # Run application
gradle bootRun # Run Spring Boot
gradle dependencies # Show dependency treeMulti-Module Project
Multi-module projects split large apps. settings.gradle lists modules. project(:core) creates inter-module dependencies. Each module has its own build.gradle.
// settings.gradle
include 'core', 'web', 'api'
// build.gradle (root)
subprojects {
apply plugin: 'java'
repositories { mavenCentral() }
}
// In web/build.gradle
dependencies { implementation project(':core') }Spring Basics
Spring Boot App
@SpringBootApplication enables auto-configuration, component scanning, and configuration. SpringApplication.run starts the embedded server. Eliminates XML configuration.
@SpringBootApplication
public class App {
public static void main(String[] args) {
SpringApplication.run(App.class, args);
}
}REST Controller
@RestController combines @Controller and @ResponseBody. @GetMapping, @PostMapping are shortcuts. @PathVariable extracts URL params, @RequestBody binds JSON.
@RestController
@RequestMapping("/api/users")
public class UserController {
@GetMapping("/{id}")
public User getUser(@PathVariable Long id) {
return userService.findById(id);
}
@PostMapping
public User create(@RequestBody User user) {
return userService.save(user);
}
}Dependency Injection
@Autowired injects dependencies. Constructor injection is recommended (testable, immutable). @Service, @Repository, @Component are stereotypes for injection.
@Service
public class UserService {
private final UserRepository repo;
@Autowired // Constructor injection (recommended)
public UserService(UserRepository repo) {
this.repo = repo;
}
}Configuration
@Configuration marks config classes. @Bean declares beans managed by Spring. @Primary makes a bean preferred. Use for third-party classes.
@Configuration
public class AppConfig {
@Bean
public RestTemplate restTemplate() {
return new RestTemplate();
}
@Bean @Primary
public DataSource primaryDataSource() {
return DataSourceBuilder.create().build();
}
}Application Properties
application.properties configures Spring Boot. Profiles enable environment-specific config. Activate with spring.profiles.active=dev. Use @Value or @ConfigurationProperties.
# application.properties
server.port=8080
spring.datasource.url=jdbc:postgresql://localhost/mydb
spring.jpa.hibernate.ddl-auto=update
# Profile-specific
# application-dev.properties
server.port=9090JDBC Deep Dive
Connection & Statement
DriverManager.getConnection establishes a connection. Statement executes static SQL. ResultSet iterates results. Always close resources or use try-with-resources.
Connection conn = DriverManager.getConnection(url, user, pass);
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery("SELECT * FROM users");
while (rs.next()) {
System.out.println(rs.getString("name"));
}
rs.close(); stmt.close(); conn.close();PreparedStatement
PreparedStatement prevents SQL injection by parameterizing queries. Set values by index (1-based). try-with-resources auto-closes. Improves performance through pre-compilation.
String sql = "INSERT INTO users (name, email) VALUES (?, ?)";
try (PreparedStatement ps = conn.prepareStatement(sql)) {
ps.setString(1, "Alice");
ps.setString(2, "[email protected]");
ps.executeUpdate();
}Transaction Management
setAutoCommit(false) starts a transaction. commit persists, rollback undoes. If any statement fails, rollback to maintain integrity. @Transactional handles this in Spring.
conn.setAutoCommit(false);
try {
stmt.executeUpdate("UPDATE accounts SET bal = bal - 100 WHERE id = 1");
stmt.executeUpdate("UPDATE accounts SET bal = bal + 100 WHERE id = 2");
conn.commit();
} catch (SQLException e) {
conn.rollback();
}Connection Pooling
Connection pooling reuses connections. HikariCP is the fastest pool. maximumPoolSize limits concurrent connections. Always close (returns to pool). Spring Boot configures HikariCP automatically.
HikariConfig config = new HikariConfig();
config.setJdbcUrl(url);
config.setMaximumPoolSize(10);
HikariDataSource ds = new HikariDataSource(config);
Connection conn = ds.getConnection();ResultSet Metadata
ResultSetMetaData describes result structure: column names, types, properties. Useful for generic data access. Column indices are 1-based.
ResultSet rs = stmt.executeQuery("SELECT * FROM users");
ResultSetMetaData meta = rs.getMetaData();
int cols = meta.getColumnCount();
for (int i = 1; i <= cols; i++)
System.out.println(meta.getColumnName(i) + ": " + meta.getColumnTypeName(i));Concurrency Utilities
ExecutorService
ExecutorService manages thread pools. submit returns a Future for async results. get blocks until done (with timeout). Always shutdown the executor.
ExecutorService executor = Executors.newFixedThreadPool(4);
Future<String> future = executor.submit(() -> {
Thread.sleep(1000);
return "Result";
});
String result = future.get(5, TimeUnit.SECONDS);
executor.shutdown();CompletableFuture
CompletableFuture enables functional async programming. supplyAsync runs in ForkJoinPool. thenApply transforms, thenAccept consumes, exceptionally handles errors.
CompletableFuture.supplyAsync(() -> fetchData())
.thenApply(data -> process(data))
.thenAccept(result -> System.out.println(result))
.exceptionally(ex -> { ex.printStackTrace(); return null; });Concurrent Collections
ConcurrentHashMap is thread-safe without full locking. CopyOnWriteArrayList copies on write (read-heavy). BlockingQueue supports producer-consumer patterns.
ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
map.computeIfAbsent("b", k -> k.length());
CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();
BlockingQueue<Task> queue = new LinkedBlockingQueue<>(100);CountDownLatch & CyclicBarrier
CountDownLatch waits for N threads (one-shot). CyclicBarrier waits for N threads then resets (reusable). Use latch for startup, barrier for phased computation.
CountDownLatch latch = new CountDownLatch(3);
for (int i = 0; i < 3; i++)
new Thread(() -> { work(); latch.countDown(); }).start();
latch.await(); // Wait for all
CyclicBarrier barrier = new CyclicBarrier(3, () -> System.out.println("All ready"));Atomic Variables
Atomic variables provide lock-free thread-safe operations. compareAndSet enables optimistic locking. LongAdder is faster than AtomicLong for high-contention counters.
AtomicInteger counter = new AtomicInteger(0);
counter.incrementAndGet();
counter.compareAndSet(0, 1);
LongAdder adder = new LongAdder();
adder.increment();JVM Internals
Memory Areas
JVM memory: Heap (objects, GC-managed), Stack (method calls, per-thread), Metaspace (class metadata). Young Gen uses copying GC, Old Gen uses mark-sweep-compact.
// Heap: objects and arrays (shared)
// - Young Gen: Eden, S0, S1
// - Old Gen: long-lived objects
// Stack: method frames (per thread)
// Metaspace: class metadata (Java 8+)
// JVM flags: -Xms512m -Xmx2gClass Loading
Class loading is lazy. Bootstrap loads core Java, Extension loads extensions, Application loads classpath. Static initializers run once. Custom classloaders enable hot-reload.
// Bootstrap -> Extension -> Application classloaders
class MyClass {
static { System.out.println("Static init"); }
}
// Class.forName("MyClass") triggers loading
// -verbose:class shows class loadingBytecode
Java compiles to bytecode (stack-based). javap -c disassembles class files. Each instruction pushes/pops the operand stack. Java agents can modify bytecode at load time.
// javap -c MyClass.class
// Method int add(int, int):
// iload_1 // Load local var 1
// iload_2 // Load local var 2
// iadd // Add
// ireturn // Return intJIT Compilation
JIT compiles frequently-executed bytecode to native code. Tiered compilation balances startup and peak performance. Hot methods are inlined and optimized.
// JIT compiles hot methods to native code
// -XX:+PrintCompilation // Show JIT activity
// -XX:CompileThreshold=10000 // Method call count
// Tiered: Interpreter -> C1 -> C2Thread Dump
Thread dumps show all thread states and stack traces. Essential for debugging deadlocks and hangs. jstack is the command-line tool. Look for BLOCKED and WAITING threads.
// Get thread dump
jstack <pid>
// Or: kill -3 <pid>
// Deadlock detection
jstack -l <pid> | grep -A 20 "Found deadlock"Garbage Collection
GC Algorithms
Serial GC for small apps. Parallel GC maximizes throughput. G1 GC balances throughput and latency (default). ZGC provides sub-millisecond pauses for large heaps.
# Serial GC (single-threaded)
-XX:+UseSerialGC
# Parallel GC (throughput)
-XX:+UseParallelGC
# G1 GC (balanced, default in Java 9+)
-XX:+UseG1GC
# ZGC (low-latency)
-XX:+UseZGCG1 GC Tuning
G1 divides heap into regions. MaxGCPauseMillis sets a soft pause target. G1 prioritizes regions with most garbage. Use GCViewer or GCEasy to analyze logs.
# Set heap
-Xms4g -Xmx4g
# Max GC pause target
-XX:MaxGCPauseMillis=200
# Region size (1-32MB)
-XX:G1HeapRegionSize=16m
# Enable GC logging
-Xlog:gc*:file=gc.log:time,uptimeMemory Leaks
Memory leaks are caused by unintended object retention. Static collections, unclosed resources, and listener registrations are common. jmap shows object counts. Analyze hprof with MAT.
// Common leak: static collections
static Map<String, Object> cache = new HashMap<>();
// Objects never removed -> leak
// Detect with:
jmap -histo <pid> | head -20
// Heap dump:
jmap -dump:format=b,file=heap.hprof <pid>Weak References
WeakReference allows GC when no strong references exist. SoftReference survives until memory pressure. WeakHashMap keys are weak. Use for caches that should not prevent GC.
WeakReference<Object> weakRef = new WeakReference<>(new Object());
SoftReference<byte[]> softRef = new SoftReference<>(new byte[1024]);
WeakHashMap<Object, String> map = new WeakHashMap<>();
map.put(key, "value"); // Entry removed when key is GC'dFinalization
finalize() is deprecated (unpredictable, slow). Cleaner API (Java 9+) provides better cleanup. try-with-resources is preferred for deterministic cleanup.
// Cleaner API (Java 9+)
class Resource implements AutoCloseable {
private final Cleaner.Cleanable cleanable;
Resource() {
cleanable = Cleaner.create().register(this, () -> cleanup());
}
public void close() { cleanable.clean(); }
}Stream Collectors
Grouping By
groupingBy partitions elements by a classifier. The second argument is a downstream collector for aggregation. counting, averaging, summing are common downstream collectors.
Map<String, List<Person>> byCity =
people.stream().collect(Collectors.groupingBy(Person::getCity));
Map<String, Long> countByCity =
people.stream().collect(Collectors.groupingBy(
Person::getCity, Collectors.counting()));Partitioning
partitioningBy splits into two groups (true/false). More efficient than groupingBy for boolean keys. Result always has both keys. Downstream collectors aggregate each partition.
Map<Boolean, List<Person>> partition =
people.stream().collect(Collectors.partitioningBy(p -> p.getAge() >= 18));
// {false=[minors], true=[adults]}
Map<Boolean, Long> count =
people.stream().collect(Collectors.partitioningBy(
p -> p.getAge() >= 18, Collectors.counting()));Joining
joining concatenates strings with optional delimiter, prefix, and suffix. Elements must be strings; use map first. Uses StringBuilder internally.
String names = people.stream()
.map(Person::getName)
.collect(Collectors.joining(", "));
// "Alice, Bob, Charlie"
String csv = people.stream()
.map(p -> p.getName() + "=" + p.getAge())
.collect(Collectors.joining("\n", "[", "]"));Reducing
reducing performs a fold operation. Three-arg version takes identity, mapper, reducer. Two-arg version returns Optional. Use when standard collectors are insufficient.
int totalAge = people.stream()
.collect(Collectors.reducing(0, Person::getAge, Integer::sum));
Optional<Person> oldest = people.stream()
.collect(Collectors.reducing((p1, p2) ->
p1.getAge() > p2.getAge() ? p1 : p2));Custom Collector
Collector.of creates custom collectors: supplier, accumulator, combiner, finisher. The combiner merges partial results for parallel streams. Useful for specialized output formats.
Collector<Person, ?, String> toJson = Collector.of(
StringBuilder::new,
(sb, p) -> sb.append(`{"name":"${p.getName()}"}`),
StringBuilder::append,
StringBuilder::toString
);Common Pitfalls
Integer Caching
Java caches Integer values from -128 to 127. == compares references, not values. For Integer outside cache range, == returns false. Always use .equals() for Integer.
Integer a = 127; Integer b = 127;
System.out.println(a == b); // true (cached)
Integer c = 128; Integer d = 128;
System.out.println(c == d); // false (not cached)
System.out.println(c.equals(d)); // trueString Immutability
Strings are immutable: methods like concat return new strings. Forgetting to assign the result is a common bug. Use StringBuilder for repeated concatenation.
String s = "Hello";
s.concat(" World"); // Returns new string, s unchanged
System.out.println(s); // "Hello"
// Use StringBuilder for mutation
StringBuilder sb = new StringBuilder("Hello");
sb.append(" World"); // Mutates in placeEquals & HashCode
Equal objects must have equal hash codes. HashMap and HashSet use hashCode for bucketing and equals for comparison. If you override equals, you MUST override hashCode.
class Person {
String name;
public boolean equals(Object o) {
if (!(o instanceof Person)) return false;
return name.equals(((Person)o).name);
}
public int hashCode() { return name.hashCode(); }
}Checked vs Unchecked
Checked exceptions must be declared or caught (IOException, SQLException). Unchecked (RuntimeException) do not require handling. Avoid catching broad Exception.
// Checked: must catch or declare
try { Thread.sleep(1000); }
catch (InterruptedException e) { /* handle */ }
// Unchecked: RuntimeException
throw new IllegalArgumentException("bad input");Resource Leaks
Resources must be closed. try-with-resources (Java 7+) auto-closes AutoCloseable. Without it, exceptions cause leaks. Never leave a resource unclosed.
// BAD: resource leak
FileInputStream fis = new FileInputStream("file.txt");
// If exception here, fis never closed
// GOOD: try-with-resources
try (FileInputStream fis = new FileInputStream("file.txt")) {
// Use resource
} // Auto-closed even on exceptionDesign Patterns
Singleton
Double-checked locking with volatile ensures thread-safe lazy initialization. volatile prevents instruction reordering. Enum singleton is simpler: public enum Singleton { INSTANCE; }.
public class Singleton {
private static volatile Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) instance = new Singleton();
}
}
return instance;
}
}Builder
Builder pattern handles objects with many optional parameters. Avoids telescoping constructors. The inner Builder collects parameters fluently. build() creates the immutable object.
public class Pizza {
private final String size;
private final boolean cheese;
private Pizza(Builder b) { size = b.size; cheese = b.cheese; }
public static class Builder {
private String size; private boolean cheese;
public Builder size(String s) { size = s; return this; }
public Builder cheese(boolean c) { cheese = c; return this; }
public Pizza build() { return new Pizza(this); }
}
}Strategy
Strategy pattern encapsulates interchangeable algorithms. The context delegates to the strategy interface. Avoids large if-else chains. Follows open-closed principle.
interface PaymentStrategy { void pay(double amount); }
class CreditCard implements PaymentStrategy {
public void pay(double amount) { System.out.println("Card: " + amount); }
}
class Cart { private PaymentStrategy strategy; void checkout() { strategy.pay(100); } }Observer
Observer pattern defines a one-to-many dependency. When the subject changes, all observers are notified. Used in MVC, event systems, and reactive programming.
interface Observer { void update(String event); }
class Subject {
private List<Observer> observers = new ArrayList<>();
void subscribe(Observer o) { observers.add(o); }
void notify(String event) { observers.forEach(o -> o.update(event)); }
}Factory Method
Factory Method defines an interface for creating objects but lets subclasses decide which class to instantiate. Decouples client code from concrete classes.
abstract class Document { abstract void open(); }
class PDF extends Document { void open() { /* ... */ } }
abstract class DocFactory { abstract Document create(); }
class PDFFactory extends DocFactory { Document create() { return new PDF(); } }Related Java snippets
Copy-paste ready code for common tasks.
String Operations and StringBuilder
Manipulate strings with split, join, substring, and StringBuilder in Java.
Stream API
Process collections using the Stream API.
Lambda Expressions
Simplify code with Lambda expressions.
Optional
Handle null values elegantly.
Collection Operations
Common operations on List, Set, and Map.
Exception Handling
try-catch-finally and custom exceptions.
File IO
Read and write file operations.
Threads
Create and manage threads.
Concurrency Utilities
Common utilities in the concurrency package.
Annotations
Custom annotations and usage.
Generics
Generic classes and methods.
Reflection
Get class information at runtime.
Serialization
Object serialization and deserialization.
Date and Time
Java 8+ Date-Time API.
Regular Expressions
Pattern and Matcher.
JDBC
Database connection and operations.
HTTP Client
Java 11+ HTTP client.
Record
Java 14+ record classes.
Pattern Matching
instanceof pattern matching.
Sealed Classes
Java 17+ sealed classes.
Text Blocks
Java 15+ multi-line strings.
Was this helpful?