Post

Java Fundamentals Cheatsheet

Personal cheatsheet for Java fundamentals. Covers OOP, classes, strings, exceptions, generics, collections, streams, concurrency, and modern Java features.

1. OOP Principles

1.1. Encapsulation

Bundles data and behaviour together, hiding internal state. External access via getters/setters.

1
2
3
4
5
6
7
8
9
public class BankAccount {
    private double balance; // hidden

    public double getBalance() { return balance; }

    public void deposit(double amount) {
        if (amount > 0) balance += amount;
    }
}

1.2. Inheritance

A subclass inherits fields and methods from a superclass. Java only supports single class inheritance but multiple interface implementation.

1
2
3
4
5
6
7
8
9
10
public class Animal {
    public void eat() { System.out.println("eating"); }
}

public class Dog extends Animal {
    @Override
    public void eat() { System.out.println("eating kibble"); }

    public void bark() { System.out.println("woof"); }
}
  • super.method() - call the parent’s version
  • super(args) - call parent constructor (must be first line)
  • A subclass constructor always calls the parent constructor (implicitly super() if not specified)

1.3. Polymorphism

Compile-time (overloading): Same method name, different parameters.

1
2
public int add(int a, int b) { return a + b; }
public double add(double a, double b) { return a + b; }

Runtime (overriding): Subclass overrides parent method. Resolved at runtime based on the actual object type, not the reference type.

1
2
Animal a = new Dog(); // reference type: Animal, object type: Dog
a.eat();              // calls Dog.eat() - resolved at runtime

1.4. Abstraction

Hides implementation details, exposes only what’s necessary. Achieved via abstract classes or interfaces.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public abstract class Shape {
    public abstract double area(); // must implement

    public void describe() {       // shared implementation
        System.out.println("Area: " + area());
    }
}

public class Circle extends Shape {
    private double radius;
    public Circle(double r) { this.radius = r; }

    @Override
    public double area() { return Math.PI * radius * radius; }
}

2. Classes & Objects

2.1. Constructors

The default no-arg constructor is not generated once any constructor is explicitly defined. Use this() to delegate to another constructor in the same class.

1
2
3
4
5
6
7
8
9
10
11
public class Person {
    private String name;
    private int age;

    public Person() { this("Unknown", 0); }          // delegates to other constructor

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }
}
  • If you define any constructor, the default no-arg constructor is not auto-generated
  • this(args) - constructor chaining, must be first line

2.2. Access Modifiers

ModifierSame ClassSame PackageSubclassEverywhere
privateyesnonono
(default)yesyesnono
protectedyesyesyesno
publicyesyesyesyes

2.3. static Members

  • Static fields - shared across all instances; belongs to the class, not any object
  • Static methods - can be called without an instance; cannot access this or instance fields
  • Static blocks - run once when the class is first loaded
1
2
3
4
5
6
7
8
9
public class Counter {
    private static int count = 0; // shared

    public Counter() { count++; }

    public static int getCount() { return count; } // no instance needed
}

Counter.getCount(); // called on the class

2.4. final

UsageMeaning
final variableValue cannot be reassigned after assignment
final methodCannot be overridden in subclasses
final classCannot be subclassed (String is final)
final referenceReference cannot point to a new object, but the object itself can be mutated
1
2
3
final List<String> list = new ArrayList<>();
list.add("ok");        // fine - mutating the object
list = new ArrayList<>(); // COMPILE ERROR - reassigning the reference

3. equals() and hashCode()

The Contract:

  1. If a.equals(b) is true, then a.hashCode() == b.hashCode() must be true
  2. The reverse is NOT required (hash collisions are allowed)
  3. Always override both together

Why it matters for Collections: HashMap/HashSet use hashCode() to find the bucket, then equals() to confirm the match. If you override only equals, two “equal” objects can land in different buckets and never be found.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Point {
    private int x, y;

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof Point p)) return false; // pattern matching
        return x == p.x && y == p.y;
    }

    @Override
    public int hashCode() {
        return Objects.hash(x, y);
    }
}

// Default Object.equals() checks reference equality (==), not value equality
new Point(1,2).equals(new Point(1,2)); // false without override, true with override

4. Enums

Enums define a fixed set of named constants. Unlike plain constants, they are type-safe and can carry fields, methods, and implement interfaces.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public enum Direction {
    NORTH, SOUTH, EAST, WEST;
}

// Enum with fields and methods
public enum Planet {
    MERCURY(3.303e+23, 2.4397e6),
    EARTH(5.976e+24, 6.37814e6);

    private final double mass;
    private final double radius;

    Planet(double mass, double radius) {
        this.mass = mass;
        this.radius = radius;
    }

    public double surfaceGravity() {
        final double G = 6.67300E-11;
        return G * mass / (radius * radius);
    }
}
MethodDescription
Direction.values()Returns array of all enum constants
Direction.valueOf("NORTH")Returns enum constant by name
direction.name()Returns the name as a String
direction.ordinal()Returns the zero-based position
  • Enums are implicitly public static final and extend java.lang.Enum
  • Enums can implement interfaces
  • Enums are safe for switch statements and can be used in EnumMap/EnumSet (highly efficient)
1
2
3
4
switch (direction) {
    case NORTH -> System.out.println("Going north");
    case SOUTH -> System.out.println("Going south");
}

5. Interfaces & Abstract Classes

 InterfaceAbstract Class
Instantiationnono
Multiple inheritanceyes (multiple)no (single)
Fieldspublic static final onlyAny type
Methodsabstract, default, staticAny type
Constructornoyes
When to useDefine a capability/contractShare common state/behaviour with a base
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public interface Flyable {
    void fly(); // abstract by default

    default void land() {               // default - optional to override
        System.out.println("landing");
    }

    static Flyable noOp() {             // static factory
        return () -> {};
    }
}

public interface Swimmable {
    void swim();
}

// A class can implement multiple interfaces
public class Duck extends Bird implements Flyable, Swimmable {
    @Override public void fly() { ... }
    @Override public void swim() { ... }
}

Diamond problem with default methods: If two interfaces have the same default method, the implementing class must override it to resolve the conflict.


6. Pass by Value vs Pass by Reference

Java is always pass by value. For objects, the value passed is the reference (memory address). The reference itself is copied.

1
2
3
4
5
6
7
8
9
10
11
void mutate(List<String> list) {
    list.add("added");     // affects original - same object via copied reference
}

void reassign(List<String> list) {
    list = new ArrayList<>(); // does NOT affect original - only local copy changes
}

List<String> names = new ArrayList<>();
mutate(names);    // names now has "added"
reassign(names);  // names unchanged

7. Strings

7.1. Immutability & String Pool

String is immutable - every “modification” creates a new object.

1
2
3
4
5
6
7
String a = "hello";       // goes into the String pool
String b = "hello";       // same object from pool
String c = new String("hello"); // new heap object - avoid this

a == b       // true  (same pool reference)
a == c       // false (different objects)
a.equals(c)  // true  (same value)

7.2. String vs StringBuilder vs StringBuffer

 StringStringBuilderStringBuffer
MutabilityImmutableMutableMutable
Thread-safeyes (immutable)noyes (synchronized)
PerformanceSlow for concatenation in loopsFastSlower than StringBuilder
1
2
3
4
5
6
7
8
// Bad - creates many intermediate String objects
String result = "";
for (int i = 0; i < 1000; i++) result += i;

// Good
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 1000; i++) sb.append(i);
String result = sb.toString();

7.3. Common String Methods

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
String s = "  Hello, World!  ";

s.trim()                        // "Hello, World!"
s.strip()                       // "Hello, World!" (Unicode-aware, prefer this)
s.toLowerCase()                 // "  hello, world!  "
s.toUpperCase()
s.contains("World")             // true
s.startsWith("Hello")           // false (leading spaces)
s.replace("World", "Java")      // "  Hello, Java!  "
s.split(", ")                   // ["  Hello", "World!  "]
s.substring(2, 7)               // "Hello"
s.charAt(2)                     // 'H'
s.indexOf("World")              // 9
s.isEmpty()                     // false
s.isBlank()                     // false (Java 11+)
s.length()                      // 17
String.format("Hi %s, you are %d", "Ryo", 25) // "Hi Ryo, you are 25"
String.join(", ", "a", "b", "c")               // "a, b, c"
"hello".repeat(3)               // "hellohellohello" (Java 11+)

8. Autoboxing, Unboxing & Gotchas

Autoboxing = automatic conversion from primitive (int) to wrapper (Integer). Unboxing is the reverse.

1
2
Integer a = 5;   // autoboxing
int b = a;       // unboxing

Integer Cache Gotcha: Java caches Integer values from -128 to 127. Outside this range, == will fail even for equal values.

1
2
3
4
5
6
7
8
Integer x = 127;
Integer y = 127;
x == y   // true  (cached)

Integer p = 128;
Integer q = 128;
p == q   // false (not cached - different heap objects)
p.equals(q) // true - always use .equals() for wrappers

NPE from unboxing:

1
2
Integer val = null;
int x = val; // NullPointerException at runtime - unboxing null

Performance: Autoboxing in tight loops creates many unnecessary heap objects. Prefer primitives where possible.


9. Exceptions

9.1. Hierarchy

1
2
3
4
5
Throwable
├── Error          (JVM-level, don't catch: OutOfMemoryError, StackOverflowError)
└── Exception
    ├── Checked    (must declare or handle: IOException, SQLException)
    └── RuntimeException (unchecked: NullPointerException, IllegalArgumentException)
  • Checked - compiler forces you to handle (try-catch) or declare (throws)
  • Unchecked (RuntimeException) - programming bugs; not required to handle

9.2. try-catch-finally & try-with-resources

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// Basic
try {
    int result = 10 / 0;
} catch (ArithmeticException e) {
    System.out.println("Error: " + e.getMessage());
} catch (Exception e) {
    e.printStackTrace(); // catches anything else
} finally {
    System.out.println("always runs, even if exception or return");
}

// Multi-catch
catch (IOException | SQLException e) { ... }

// try-with-resources - auto-closes anything implementing AutoCloseable
try (FileReader fr = new FileReader("file.txt");
     BufferedReader br = new BufferedReader(fr)) {
    String line = br.readLine();
} // fr and br are closed automatically, even on exception

9.3. Custom Exceptions

Extend Exception for checked exceptions, RuntimeException for unchecked. Call super(message) and add fields for any extra context.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// Checked custom exception
public class InsufficientFundsException extends Exception {
    private final double amount;

    public InsufficientFundsException(double amount) {
        super("Insufficient funds. Shortfall: " + amount);
        this.amount = amount;
    }

    public double getAmount() { return amount; }
}

// Usage
public void withdraw(double amount) throws InsufficientFundsException {
    if (amount > balance) throw new InsufficientFundsException(amount - balance);
    balance -= amount;
}

9.4. throw vs throws

 throwthrows
PurposeActually throws an exception instanceDeclares that a method may throw
LocationInside method bodyMethod signature
Examplethrow new RuntimeException("msg")void read() throws IOException

10. Generics

10.1. Generic Classes & Methods

Type parameters are declared in angle brackets and resolved at the call site. Bounds like <T extends Comparable<T>> restrict which types are accepted.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// Generic class
public class Box<T> {
    private T value;
    public Box(T value) { this.value = value; }
    public T get() { return value; }
}

Box<String> strBox = new Box<>("hello");
Box<Integer> intBox = new Box<>(42);

// Generic method
public static <T extends Comparable<T>> T max(T a, T b) {
    return a.compareTo(b) > 0 ? a : b;
}

10.2. Bounded Wildcards

WildcardMeaningUse case
<?>Unknown typeRead-only, most general
<? extends T>T or any subclass (upper bound)Producer - you read from it
<? super T>T or any superclass (lower bound)Consumer - you write into it

PECS - Producer Extends, Consumer Super:

1
2
3
4
5
6
7
8
9
10
// You can READ from an upper-bounded wildcard
public double sumList(List<? extends Number> list) {
    return list.stream().mapToDouble(Number::doubleValue).sum();
}

// You can WRITE to a lower-bounded wildcard
public void addNumbers(List<? super Integer> list) {
    list.add(1);
    list.add(2);
}

10.3. Type Erasure

Generics are a compile-time feature. At runtime, type parameters are erased to Object (or their bound). You cannot do new T(), new T[], or instanceof T at runtime.


11. Collections & Data Structures

11.1. Overview

InterfaceImplementationOrderedSortedDuplicatesNullThread-safe
ListArrayListyes (insertion)noyesyesno
ListLinkedListyes (insertion)noyesyesno
SetHashSetnonono1 nullno
SetLinkedHashSetyes (insertion)nono1 nullno
SetTreeSetyes (sorted)yesnonono
MapHashMapnonoKeys: noKey/val: yesno
MapLinkedHashMapyes (insertion)noKeys: noyesno
MapTreeMapyes (sorted)yesKeys: noKey: nono
QueueArrayDequeyes (FIFO)noyesnono
QueuePriorityQueueyes (priority)yesyesnono

Thread-safe alternatives: ConcurrentHashMap, CopyOnWriteArrayList, Collections.synchronizedList(...)

11.2. Common Operations

Reference for the most-used List, Map, and Set operations.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// List
List<String> list = new ArrayList<>(List.of("a", "b", "c")); // mutable copy
list.add("d");
list.remove("b");
list.get(0);
list.size();
list.contains("a");
Collections.sort(list);
Collections.reverse(list);

// Map
Map<String, Integer> map = new HashMap<>();
map.put("a", 1);
map.getOrDefault("z", 0);         // 0 if missing
map.putIfAbsent("a", 99);         // ignored - already present
map.computeIfAbsent("b", k -> k.length()); // compute if missing
map.merge("a", 1, Integer::sum);  // get+update in one shot
for (Map.Entry<String, Integer> e : map.entrySet()) {
    System.out.println(e.getKey() + "=" + e.getValue());
}

// Set
Set<String> set = new HashSet<>(List.of("a", "b"));
set.add("c");
set.contains("a"); // O(1) for HashSet

11.3. Comparable vs Comparator

 ComparableComparator
MethodcompareTo(T o)compare(T o1, T o2)
Defined inThe class itselfExternal / lambda
PurposeNatural orderingCustom/alternate ordering
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// Comparable - natural order
public class Student implements Comparable<Student> {
    private int gpa;
    @Override
    public int compareTo(Student other) {
        return Integer.compare(this.gpa, other.gpa);
    }
}

// Comparator - flexible, can chain
List<Student> students = ...;
students.sort(Comparator.comparing(Student::getName)
                        .thenComparingInt(Student::getGpa)
                        .reversed());

12. Anonymous Classes, Inner Classes & Nested Classes

12.1. Types of Nested Classes

Typestatic?Access outer instance?Use case
Static nested classyesnoLogically grouped helper class
Inner class (non-static)noyesNeeds access to outer instance
Local classnoyes (effectively final vars)Method-scoped helper
Anonymous classnoyes (effectively final vars)One-off implementation

12.2. Anonymous Classes

Implement an interface or extend an abstract class inline, without naming the class. Can only capture effectively final local variables from the enclosing scope.

1
2
3
4
5
6
7
8
9
10
11
12
13
// Implemented inline without naming the class
Runnable r = new Runnable() {
    @Override
    public void run() {
        System.out.println("running");
    }
};

// With an abstract class
Shape shape = new Shape() {
    @Override
    public double area() { return 0; }
};

13. Lambdas, Functional Interfaces & Method References

13.1. Lambdas

A lambda can only be assigned where a functional interface is expected (an interface with exactly one abstract method).

1
2
3
// (params) -> expression  OR  (params) -> { statements; }
Runnable r = () -> System.out.println("hi");
Comparator<String> c = (a, b) -> a.compareTo(b);

13.2. Common Functional Interfaces

InterfaceSignatureUse
Runnable() -> voidTask with no input/output
Supplier<T>() -> TProduce a value
Consumer<T>T -> voidConsume a value
BiConsumer<T,U>(T, U) -> voidConsume two values
Function<T,R>T -> RTransform a value
BiFunction<T,U,R>(T, U) -> RTransform two values to one
Predicate<T>T -> booleanTest a condition
BiPredicate<T,U>(T, U) -> booleanTest two values
UnaryOperator<T>T -> TTransform same type
BinaryOperator<T>(T, T) -> TCombine two of same type
1
2
3
4
5
6
7
8
Predicate<String> isBlank = String::isBlank;
Function<String, Integer> len = String::length;
Consumer<String> print = System.out::println;
Supplier<List<String>> listMaker = ArrayList::new;

// Composing
Predicate<String> notBlank = isBlank.negate();
Function<String, String> trimThenUpper = ((Function<String, String>) String::trim).andThen(String::toUpperCase);

13.3. Method References

KindSyntaxLambda equivalent
Static methodClassName::staticMethodx -> ClassName.staticMethod(x)
Instance method (arbitrary instance)ClassName::instanceMethodx -> x.instanceMethod()
Instance method (specific instance)instance::instanceMethodx -> instance.instanceMethod(x)
ConstructorClassName::newx -> new ClassName(x)
1
2
3
4
5
6
7
List<String> words = List.of("hello", "world");

words.stream().map(String::toUpperCase).forEach(System.out::println);
//                  ^--- instance method ref        ^--- specific instance ref

List<String> list = words.stream().collect(Collectors.toCollection(ArrayList::new));
//                                                                  ^--- constructor ref

14. Streams API

14.1. Overview

Lazy pipeline of operations on a data source. Doesn’t store data. Intermediate ops return a new stream (lazy). Terminal ops trigger evaluation and produce a result.

1
2
3
4
5
6
7
8
List<String> names = List.of("alice", "bob", "charlie", "anna");

List<String> result = names.stream()                  // source
    .filter(n -> n.startsWith("a"))                   // intermediate
    .map(String::toUpperCase)                         // intermediate
    .sorted()                                         // intermediate
    .collect(Collectors.toList());                    // terminal
// ["ALICE", "ANNA"]

14.2. Intermediate Operations

OperationDescription
filter(Predicate)Keep elements matching predicate
map(Function)Transform each element
flatMap(Function)Map then flatten (for nested collections)
distinct()Remove duplicates (uses equals)
sorted() / sorted(Comparator)Sort elements
limit(n)Take first n elements
skip(n)Skip first n elements
peek(Consumer)Debug/inspect without consuming

14.3. Terminal Operations

OperationDescription
collect(Collector)Accumulate into collection, map, string, etc.
forEach(Consumer)Consume each element
count()Count elements
findFirst() / findAny()Return Optional of first/any element
anyMatch / allMatch / noneMatchShort-circuit boolean checks
min(Comparator) / max(Comparator)Return Optional of min/max
reduce(identity, BinaryOperator)Fold elements into one value
toArray()Return array

14.4. Common Collectors

Built-in collectors for grouping, joining, partitioning, and accumulating into collections. Import java.util.stream.Collectors.* statically.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import static java.util.stream.Collectors.*;

// To collections
stream.collect(toList())
stream.collect(toSet())
stream.collect(toUnmodifiableList())

// Joining
stream.collect(joining(", ", "[", "]"))  // "[a, b, c]"

// Grouping
Map<Integer, List<String>> byLength = names.stream()
    .collect(groupingBy(String::length));

// Counting per group
Map<Integer, Long> countByLength = names.stream()
    .collect(groupingBy(String::length, counting()));

// Partitioning (splits into true/false)
Map<Boolean, List<String>> partition = names.stream()
    .collect(partitioningBy(n -> n.length() > 3));

// To map
Map<String, Integer> nameLengths = names.stream()
    .collect(toMap(n -> n, String::length));

14.5. flatMap

flatMap maps each element to a stream and flattens all resulting streams into one. Use it to process nested collections or split strings into words.

1
2
3
4
5
6
7
8
9
10
11
List<List<Integer>> nested = List.of(List.of(1, 2), List.of(3, 4));

List<Integer> flat = nested.stream()
    .flatMap(Collection::stream)
    .collect(toList()); // [1, 2, 3, 4]

// Common use: split strings into words
List<String> sentences = List.of("hello world", "foo bar");
List<String> words = sentences.stream()
    .flatMap(s -> Arrays.stream(s.split(" ")))
    .collect(toList()); // ["hello", "world", "foo", "bar"]

14.6. reduce

reduce folds all elements into a single result using an accumulator. Without an identity value, the result is an Optional because the stream may be empty.

1
2
3
4
5
6
// reduce(identity, accumulator)
int sum = IntStream.rangeClosed(1, 10).reduce(0, Integer::sum); // 55

// Without identity - returns Optional (stream could be empty)
Optional<String> longest = names.stream()
    .reduce((a, b) -> a.length() >= b.length() ? a : b);

14.7. Primitive Streams

Avoid boxing overhead for numeric work:

1
2
3
4
5
6
7
8
9
10
11
IntStream.range(0, 5)              // 0, 1, 2, 3, 4
IntStream.rangeClosed(1, 5)        // 1, 2, 3, 4, 5
IntStream.of(1, 2, 3).sum()        // 6
IntStream.of(1, 2, 3).average()    // OptionalDouble
DoubleStream.of(1.0, 2.0).max()    // OptionalDouble

// Box to stream of wrappers
IntStream.of(1,2,3).boxed()        // Stream<Integer>

// To primitive stream from object stream
names.stream().mapToInt(String::length)

14.8. Parallel Streams

Switch from .stream() to .parallelStream() to process using the common ForkJoinPool. Only beneficial for CPU-intensive, stateless, order-independent operations on large datasets.

1
2
3
long count = names.parallelStream()
    .filter(n -> n.length() > 3)
    .count();
  • Uses the ForkJoinPool.commonPool()
  • Good for CPU-intensive, stateless, order-independent operations
  • Bad for I/O, stateful operations, or small data sets (overhead > gain)
  • Avoid shared mutable state

15. Optionals

Optional wraps a potentially-null value to make the absence case explicit. Prefer orElse/orElseGet over calling get() directly.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Optional<String> opt = Optional.of("hello");        // throws NPE if null
Optional<String> safe = Optional.ofNullable(null);  // empty Optional
Optional<String> empty = Optional.empty();

opt.isPresent()          // true
opt.isEmpty()            // false (Java 11+)
opt.get()                // "hello" - throws NoSuchElementException if empty

opt.orElse("default")           // "default" if empty
opt.orElseGet(() -> compute())  // lazy - only calls supplier if empty
opt.orElseThrow(() -> new RuntimeException("missing"))

opt.ifPresent(System.out::println)          // run if present
opt.ifPresentOrElse(s -> use(s), () -> handleEmpty()); // Java 9+

// Transforming
opt.map(String::toUpperCase)         // Optional<String>
opt.filter(s -> s.length() > 3)      // Optional<String> - empty if predicate fails
opt.flatMap(s -> findByName(s))      // when the mapping function itself returns Optional

Rule: Never use Optional.get() without checking isPresent(). Prefer orElse/orElseGet.


16. Concurrency

16.1. Thread and Runnable

The low-level API for creating threads. Prefer ExecutorService for managing thread pools in production code.

1
2
3
4
5
6
// Extend Thread
Thread t = new Thread(() -> System.out.println("in thread"));
t.start();   // start - don't call run() directly

t.join();    // current thread waits for t to finish
Thread.sleep(1000); // current thread sleeps 1s (throws InterruptedException)

16.2. ExecutorService

Prefer over raw Thread for managing thread pools.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
ExecutorService exec = Executors.newFixedThreadPool(4);
ExecutorService single = Executors.newSingleThreadExecutor();
ExecutorService cached = Executors.newCachedThreadPool(); // grows as needed

// Submit Runnable (no return value)
exec.submit(() -> System.out.println("task"));

// Submit Callable (returns Future)
Future<Integer> future = exec.submit(() -> {
    Thread.sleep(100);
    return 42;
});

int result = future.get();          // blocks until done
future.get(1, TimeUnit.SECONDS);    // timeout variant

// Shutdown - always shut down
exec.shutdown();                    // graceful - waits for tasks to finish
exec.shutdownNow();                 // forceful - interrupts running tasks

16.3. synchronized and volatile

Three mechanisms for thread safety: synchronized blocks for atomic operations, volatile for memory visibility across threads, and atomic classes for lock-free single-variable updates.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// synchronized method - lock on `this`
public synchronized void increment() { count++; }

// synchronized block - more granular
public void increment() {
    synchronized(this) { count++; }
}

// volatile - guarantees visibility across threads, NOT atomicity
private volatile boolean running = true;
// Good for flags. Bad for count++ (read-modify-write is not atomic)

// Atomic classes - thread-safe, lock-free
AtomicInteger counter = new AtomicInteger(0);
counter.incrementAndGet();
counter.compareAndSet(expected, newValue);

16.4. CompletableFuture

CompletableFuture enables non-blocking async computation with composable chaining, error handling, and combination operators.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// Async task (runs in ForkJoinPool by default)
CompletableFuture<String> cf = CompletableFuture.supplyAsync(() -> {
    return fetchData(); // runs in background
});

// Chaining - non-blocking
cf.thenApply(data -> data.toUpperCase())      // transform result (same thread)
  .thenApplyAsync(data -> process(data))      // transform result (async thread)
  .thenAccept(System.out::println)            // consume result, return void
  .thenRun(() -> System.out.println("done")); // run after, no access to result

// thenCompose - flat-map for CompletableFuture (avoids nesting)
CompletableFuture<String> result = cf
    .thenCompose(data -> anotherAsyncTask(data)); // anotherAsyncTask returns CF<String>

// Error handling
cf.exceptionally(ex -> {
    System.err.println("Error: " + ex.getMessage());
    return "fallback";
});

cf.handle((result, ex) -> {           // always runs - like finally
    if (ex != null) return "error";
    return result;
});

// Combining
CompletableFuture<String> a = CompletableFuture.supplyAsync(() -> "hello");
CompletableFuture<String> b = CompletableFuture.supplyAsync(() -> "world");

CompletableFuture.allOf(a, b).join(); // wait for all
CompletableFuture.anyOf(a, b).join(); // wait for first to complete

a.thenCombine(b, (ra, rb) -> ra + " " + rb); // combine two results

16.5. Common Concurrency Problems

ProblemDescriptionFix
Race conditionTwo threads read-modify-write shared statesynchronized, AtomicInteger
DeadlockTwo threads each hold a lock the other needsConsistent lock ordering, tryLock
StarvationThread never gets CPU timeFair locks (ReentrantLock(true))
Visibility problemThread reads stale cached valuevolatile, synchronized

17. Modern Java Features (Java 8–17)

17.1. Java 8

  • Lambdas and Streams (covered above)
  • Optional (covered above)
  • Default/static interface methods (covered above)
  • java.time API (LocalDate, LocalDateTime, ZonedDateTime, Duration, Period)
1
2
3
4
LocalDate today = LocalDate.now();
LocalDate nextWeek = today.plusDays(7);
DateTimeFormatter fmt = DateTimeFormatter.ofPattern("dd/MM/yyyy");
String formatted = today.format(fmt);

17.2. Java 10 - var

1
2
3
4
var list = new ArrayList<String>();  // inferred as ArrayList<String>
var name = "hello";                  // String

// Only for local variables - cannot use in fields, params, or return types

17.3. Java 14 - Switch Expressions

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// Old switch statement (fall-through prone)
int result;
switch (day) {
    case MONDAY: result = 1; break;
    default:     result = 0;
}

// New switch expression (exhaustive, no fall-through)
int result = switch (day) {
    case MONDAY -> 1;
    case TUESDAY -> 2;
    default -> 0;
};

// With yield for blocks
int result = switch (day) {
    case MONDAY -> 1;
    default -> {
        System.out.println("other");
        yield 0;
    }
};

17.4. Java 15 - Text Blocks

1
2
3
4
5
6
String json = """
        {
            "name": "Ryo",
            "age": 25
        }
        """;

17.5. Java 16 - Records

Immutable data classes. Auto-generates constructor, accessors, equals, hashCode, toString.

1
2
3
4
5
6
7
8
9
10
11
12
13
public record Point(int x, int y) {}

Point p = new Point(1, 2);
p.x()  // 1 - accessor (not getX)
p.y()  // 2

// Can add custom methods, validate in compact constructor
public record Range(int min, int max) {
    public Range {  // compact constructor
        if (min > max) throw new IllegalArgumentException();
    }
    public int size() { return max - min; }
}

17.6. Java 16 - Pattern Matching for instanceof

1
2
3
4
5
6
7
8
9
10
// Old
if (obj instanceof String) {
    String s = (String) obj;
    System.out.println(s.length());
}

// New
if (obj instanceof String s) {
    System.out.println(s.length()); // s is already cast and scoped
}

17.7. Java 17 - Sealed Classes

Restrict which classes can extend/implement a type. All permitted subclasses must be in the same package (or module).

1
2
3
4
5
public sealed class Shape permits Circle, Rectangle, Triangle {}

public final class Circle extends Shape { ... }
public non-sealed class Rectangle extends Shape { ... } // open for further extension
public sealed class Triangle extends Shape permits EquilateralTriangle { ... }

Combines well with pattern matching in switch (preview in Java 17, standard in Java 21):

1
2
3
4
5
double area = switch (shape) {
    case Circle c    -> Math.PI * c.radius() * c.radius();
    case Rectangle r -> r.width() * r.height();
    case Triangle t  -> t.base() * t.height() / 2;
};

Comments powered by Disqus.