Skip to content

Split View: 모던 Java 완전 정복: Java 17/21 핵심 문법 가이드

|

모던 Java 완전 정복: Java 17/21 핵심 문법 가이드

모던 Java 완전 정복: Java 17/21 핵심 문법 가이드

Java는 꾸준히 진화하고 있습니다. Java 21 LTS는 Project Loom(Virtual Threads), Pattern Matching, Record, Sealed Classes 등 혁신적인 기능을 포함합니다. 이 가이드에서는 현업에서 반드시 알아야 할 모던 Java 문법을 완전 정복합니다.


1. Java 21 핵심 신기능 (LTS 기준)

1.1 Record 클래스 (Java 16+)

Record는 불변(immutable) 데이터 클래스를 간결하게 선언하는 방법입니다. 컴파일러가 자동으로 생성자, getter, equals, hashCode, toString을 생성합니다.

// 기존 방식 (boilerplate 가득)
public class PersonOld {
    private final String name;
    private final int age;

    public PersonOld(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String getName() { return name; }
    public int getAge() { return age; }

    @Override
    public boolean equals(Object o) { /* ... */ }
    @Override
    public int hashCode() { /* ... */ }
    @Override
    public String toString() { /* ... */ }
}

// Record 방식 (한 줄!)
public record Person(String name, int age) {}

// 사용
Person p = new Person("Alice", 30);
System.out.println(p.name()); // "Alice"  (getter 이름은 필드명 그대로)
System.out.println(p);        // Person[name=Alice, age=30]

// 커스텀 생성자로 유효성 검사
public record Person(String name, int age) {
    public Person {
        if (name == null || name.isBlank()) throw new IllegalArgumentException("name cannot be blank");
        if (age < 0 || age > 150) throw new IllegalArgumentException("invalid age: " + age);
    }

    // 추가 메서드 정의 가능
    public String greeting() {
        return "Hello, I'm " + name + " and I'm " + age + " years old.";
    }
}

// Record는 인터페이스 구현 가능
public record Point(double x, double y) implements Comparable<Point> {
    @Override
    public int compareTo(Point other) {
        return Double.compare(this.distance(), other.distance());
    }

    private double distance() {
        return Math.sqrt(x * x + y * y);
    }
}

Record의 제약사항:

  • 상속 불가 (implicitly final)
  • 필드는 모두 private final
  • 인스턴스 필드 추가 불가 (static 필드는 가능)

1.2 Sealed Classes (Java 17+)

Sealed Class는 어떤 클래스/인터페이스가 해당 타입을 상속/구현할 수 있는지 명시적으로 제한합니다. 이를 통해 타입 계층을 폐쇄적으로 설계할 수 있습니다.

// Shape는 오직 Circle, Rectangle, Triangle만 상속 가능
public sealed class Shape permits Circle, Rectangle, Triangle {}

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

public final class Rectangle extends Shape {
    private final double width, height;
    public Rectangle(double width, double height) {
        this.width = width;
        this.height = height;
    }
    public double width() { return width; }
    public double height() { return height; }
}

public non-sealed class Triangle extends Shape {
    // non-sealed: Triangle을 상속한 클래스는 제한 없음
    private final double base, height;
    public Triangle(double base, double height) {
        this.base = base;
        this.height = height;
    }
}

// 인터페이스에도 sealed 적용 가능
public sealed interface Result<T> permits Result.Success, Result.Failure {
    record Success<T>(T value) implements Result<T> {}
    record Failure<T>(String error) implements Result<T> {}
}

1.3 Pattern Matching for instanceof (Java 16+)

기존 instanceof 후 명시적 캐스팅 없이 바로 사용 가능합니다.

// 기존 방식
if (obj instanceof String) {
    String s = (String) obj;
    System.out.println(s.length());
}

// 모던 방식 (Pattern Matching)
if (obj instanceof String s) {
    System.out.println(s.length()); // s는 String으로 자동 캐스팅
}

// 조건과 결합
if (obj instanceof String s && s.length() > 5) {
    System.out.println("Long string: " + s);
}

// 부정
if (!(obj instanceof String s)) {
    System.out.println("Not a string");
    return;
}
// 이 시점부터 s 사용 가능
System.out.println(s.toUpperCase());

1.4 Pattern Matching for switch (Java 21)

switch 표현식에서 타입 패턴 매칭을 지원합니다. Sealed Class와 결합하면 강력합니다.

// 타입 기반 switch
public double calculateArea(Shape shape) {
    return switch (shape) {
        case Circle c -> Math.PI * c.radius() * c.radius();
        case Rectangle r -> r.width() * r.height();
        case Triangle t -> 0.5 * t.base() * t.height();
        // sealed class이므로 default 불필요 (모든 케이스 커버)
    };
}

// Guarded Pattern (조건부 패턴)
public String classify(Object obj) {
    return switch (obj) {
        case Integer i when i < 0 -> "Negative integer: " + i;
        case Integer i when i == 0 -> "Zero";
        case Integer i -> "Positive integer: " + i;
        case String s when s.isEmpty() -> "Empty string";
        case String s -> "String: " + s;
        case null -> "Null value";
        default -> "Other: " + obj.getClass().getSimpleName();
    };
}

// Record Pattern
public String describePoint(Object obj) {
    return switch (obj) {
        case Point(double x, double y) when x == 0 && y == 0 -> "Origin";
        case Point(double x, double y) when x == 0 -> "On Y-axis at " + y;
        case Point(double x, double y) when y == 0 -> "On X-axis at " + x;
        case Point(double x, double y) -> "Point at (" + x + ", " + y + ")";
        default -> "Not a point";
    };
}

1.5 Text Blocks (Java 15+)

여러 줄 문자열을 가독성 있게 작성합니다.

// 기존 방식
String json = "{\n" +
    "  \"name\": \"Alice\",\n" +
    "  \"age\": 30\n" +
    "}";

// Text Block
String json = """
        {
          "name": "Alice",
          "age": 30
        }
        """;

// SQL 예시
String sql = """
        SELECT u.id, u.name, o.total
        FROM users u
        JOIN orders o ON u.id = o.user_id
        WHERE u.active = true
        ORDER BY o.total DESC
        """;

// HTML
String html = """
        <html>
            <body>
                <h1>Hello, World!</h1>
            </body>
        </html>
        """;

// 들여쓰기 제어: \s (trailing whitespace 유지), \ (줄바꿈 없애기)
String oneLine = """
        Hello \
        World\
        """; // "Hello World"

1.6 Virtual Threads (Java 21, Project Loom)

Virtual Thread는 JVM이 관리하는 경량 스레드입니다. OS 스레드와 1:1 매핑 대신 M:N 방식으로 수백만 개의 동시 작업이 가능합니다.

// 기존 플랫폼 스레드
Thread platformThread = new Thread(() -> {
    System.out.println("Platform thread: " + Thread.currentThread());
});
platformThread.start();

// Virtual Thread 생성
Thread virtualThread = Thread.ofVirtual()
    .name("my-virtual-thread")
    .start(() -> {
        System.out.println("Virtual thread: " + Thread.currentThread());
    });

// ExecutorService로 Virtual Thread 사용
try (ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor()) {
    List<Future<String>> futures = new ArrayList<>();
    for (int i = 0; i < 10_000; i++) {
        int taskId = i;
        futures.add(executor.submit(() -> {
            // I/O 차단 작업도 OK - Virtual Thread는 블로킹 시 마운트 해제
            Thread.sleep(100);
            return "Task " + taskId + " completed";
        }));
    }
    // 모든 작업 완료 대기
    for (Future<String> f : futures) {
        System.out.println(f.get());
    }
}

// Spring Boot와 함께 사용 (application.properties)
// spring.threads.virtual.enabled=true

// 구조적 동시성 (Structured Concurrency, preview)
try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
    Future<User> user = scope.fork(() -> fetchUser(userId));
    Future<List<Order>> orders = scope.fork(() -> fetchOrders(userId));
    scope.join().throwIfFailed();
    return new UserProfile(user.resultNow(), orders.resultNow());
}

1.7 Sequenced Collections (Java 21)

컬렉션에 순서 개념을 추가하는 새 인터페이스입니다.

// SequencedCollection: getFirst(), getLast(), addFirst(), addLast(), reversed()
List<String> list = new ArrayList<>(List.of("a", "b", "c"));
System.out.println(list.getFirst()); // "a"
System.out.println(list.getLast());  // "c"
list.addFirst("z");                  // ["z", "a", "b", "c"]
list.addLast("x");                   // ["z", "a", "b", "c", "x"]
List<String> reversed = list.reversed(); // ["x", "c", "b", "a", "z"]

// SequencedMap: firstEntry(), lastEntry(), putFirst(), putLast(), reversed()
LinkedHashMap<String, Integer> map = new LinkedHashMap<>();
map.put("one", 1);
map.put("two", 2);
map.put("three", 3);
System.out.println(map.firstEntry()); // one=1
System.out.println(map.lastEntry());  // three=3

2. 람다와 스트림 API 완전 정복

2.1 람다 표현식 문법

// 기본 형태
Runnable r = () -> System.out.println("Hello");
Comparator<String> comp = (a, b) -> a.compareTo(b);

// 블록 람다 (여러 줄)
Function<Integer, Integer> factorial = n -> {
    int result = 1;
    for (int i = 1; i <= n; i++) result *= i;
    return result;
};

// 타입 추론
List<String> names = List.of("Charlie", "Alice", "Bob");
names.sort((a, b) -> a.compareTo(b));
// 또는 메서드 참조
names.sort(String::compareTo);

2.2 함수형 인터페이스

// Function<T, R>: T -> R
Function<String, Integer> strLen = String::length;
Function<Integer, Integer> doubler = x -> x * 2;
Function<String, Integer> lenThenDouble = strLen.andThen(doubler);
System.out.println(lenThenDouble.apply("Hello")); // 10

// BiFunction<T, U, R>: T, U -> R
BiFunction<String, Integer, String> repeat = (s, n) -> s.repeat(n);
System.out.println(repeat.apply("Java", 3)); // "JavaJavaJava"

// Predicate<T>: T -> boolean
Predicate<String> isLong = s -> s.length() > 5;
Predicate<String> startsWithA = s -> s.startsWith("A");
Predicate<String> combined = isLong.and(startsWithA);
Predicate<String> either = isLong.or(startsWithA);
Predicate<String> notLong = isLong.negate();

// Supplier<T>: () -> T
Supplier<LocalDate> today = LocalDate::now;
Supplier<List<String>> listFactory = ArrayList::new;

// Consumer<T>: T -> void
Consumer<String> printer = System.out::println;
Consumer<String> logger = s -> log.info("Processing: {}", s);
Consumer<String> both = printer.andThen(logger);

// UnaryOperator<T>: T -> T (Function의 특수화)
UnaryOperator<String> toUpper = String::toUpperCase;
UnaryOperator<Integer> increment = x -> x + 1;

// BinaryOperator<T>: T, T -> T
BinaryOperator<Integer> add = Integer::sum;
BinaryOperator<String> concat = String::concat;

2.3 메서드 참조 (4가지 유형)

// 1. 정적 메서드 참조: ClassName::staticMethod
Function<String, Integer> parseInt = Integer::parseInt;
Comparator<String> comp = String::compareTo; // 아니, 이건 인스턴스 메서드

// 2. 특정 인스턴스의 메서드 참조: instance::instanceMethod
String prefix = "Hello, ";
Function<String, String> greeter = prefix::concat;
System.out.println(greeter.apply("World")); // "Hello, World"

// 3. 임의 인스턴스의 메서드 참조: ClassName::instanceMethod
Function<String, String> upper = String::toUpperCase;
Comparator<String> byLength = Comparator.comparingInt(String::length);

// 4. 생성자 참조: ClassName::new
Supplier<ArrayList<String>> listCreator = ArrayList::new;
Function<String, StringBuilder> sbCreator = StringBuilder::new;
BiFunction<String, Integer, String> repeater = String::new; // 아님, 예시용

2.4 Stream API 핵심 연산

List<Employee> employees = List.of(
    new Employee("Alice", "Engineering", 90_000),
    new Employee("Bob", "Engineering", 80_000),
    new Employee("Charlie", "Marketing", 60_000),
    new Employee("Diana", "Marketing", 70_000),
    new Employee("Eve", "Engineering", 95_000)
);

// filter + map + collect
List<String> highEarnerNames = employees.stream()
    .filter(e -> e.salary() > 75_000)
    .map(Employee::name)
    .sorted()
    .collect(Collectors.toList());
// Java 16+: .toList() (불변 리스트)

// flatMap: Stream<Stream<T>> -> Stream<T>
List<List<Integer>> nested = List.of(List.of(1, 2, 3), List.of(4, 5), List.of(6));
List<Integer> flat = nested.stream()
    .flatMap(Collection::stream)
    .collect(Collectors.toList());
// [1, 2, 3, 4, 5, 6]

// reduce
int totalSalary = employees.stream()
    .mapToInt(Employee::salary)
    .sum(); // 또는 reduce(0, Integer::sum)

OptionalDouble avgSalary = employees.stream()
    .mapToInt(Employee::salary)
    .average();

// groupingBy
Map<String, List<Employee>> byDept = employees.stream()
    .collect(Collectors.groupingBy(Employee::department));

Map<String, Double> avgSalaryByDept = employees.stream()
    .collect(Collectors.groupingBy(
        Employee::department,
        Collectors.averagingInt(Employee::salary)
    ));

Map<String, Long> countByDept = employees.stream()
    .collect(Collectors.groupingBy(Employee::department, Collectors.counting()));

// partitioningBy
Map<Boolean, List<Employee>> partition = employees.stream()
    .collect(Collectors.partitioningBy(e -> e.salary() > 75_000));
// true -> 고연봉자, false -> 나머지

// joining
String names = employees.stream()
    .map(Employee::name)
    .collect(Collectors.joining(", ", "[", "]"));
// "[Alice, Bob, Charlie, Diana, Eve]"

// toMap
Map<String, Integer> nameSalaryMap = employees.stream()
    .collect(Collectors.toMap(Employee::name, Employee::salary));

// findFirst, findAny, anyMatch, allMatch, noneMatch
Optional<Employee> first = employees.stream()
    .filter(e -> e.department().equals("Engineering"))
    .findFirst();

boolean anyHigh = employees.stream().anyMatch(e -> e.salary() > 100_000);
boolean allAbove50k = employees.stream().allMatch(e -> e.salary() > 50_000);

2.5 Optional

// Optional 생성
Optional<String> opt1 = Optional.of("value");      // null 불가
Optional<String> opt2 = Optional.ofNullable(null); // null 허용 -> empty
Optional<String> opt3 = Optional.empty();

// 값 추출
String val1 = opt1.get();                          // NoSuchElementException 위험
String val2 = opt1.orElse("default");              // 기본값 (항상 평가됨)
String val3 = opt1.orElseGet(() -> computeDefault()); // lazy 평가
String val4 = opt1.orElseThrow(() -> new NotFoundException("not found"));

// 변환
Optional<Integer> length = opt1.map(String::length);
Optional<String> upper = opt1.map(String::toUpperCase);

// flatMap: Optional<Optional<T>> 방지
Optional<Address> address = Optional.of(user)
    .flatMap(u -> Optional.ofNullable(u.getAddress()));

// 조건부 처리
opt1.ifPresent(System.out::println);
opt1.ifPresentOrElse(
    v -> System.out.println("Found: " + v),
    () -> System.out.println("Not found")
);

// filter
Optional<String> longVal = opt1.filter(s -> s.length() > 3);

// 안티패턴: Optional.get() without isPresent() 체크
// 안티패턴: 메서드 파라미터로 Optional 사용 (권장하지 않음)
// 권장: 반환값에만 Optional 사용

2.6 병렬 스트림 주의사항

// 병렬 스트림 - CPU 바운드 작업에 적합
List<Integer> numbers = IntStream.rangeClosed(1, 1_000_000)
    .boxed().collect(Collectors.toList());

long sum = numbers.parallelStream()
    .mapToLong(Integer::longValue)
    .sum(); // 안전: 상태 없는 연산

// 주의: 공유 상태 변경 (스레드 불안전)
List<Integer> results = new ArrayList<>();
numbers.parallelStream()
    .filter(n -> n % 2 == 0)
    .forEach(results::add); // 위험! ArrayList는 thread-safe 하지 않음

// 올바른 방법: collect 사용
List<Integer> safeResults = numbers.parallelStream()
    .filter(n -> n % 2 == 0)
    .collect(Collectors.toList()); // thread-safe

// I/O 바운드 작업에는 Virtual Threads가 더 적합
// 병렬 스트림은 ForkJoinPool.commonPool() 사용 -> 풀 고갈 주의

3. 제네릭 심화

3.1 와일드카드와 PECS 원칙

PECS: Producer Extends, Consumer Super

// 공변(Covariant): extends - 읽기(Producer)에 사용
public double sumList(List<? extends Number> list) {
    double sum = 0;
    for (Number n : list) {
        sum += n.doubleValue(); // 읽기 OK
    }
    // list.add(1.0); // 컴파일 에러! 쓰기 불가
    return sum;
}
// 호출: sumList(List<Integer>), sumList(List<Double>) 모두 가능

// 반공변(Contravariant): super - 쓰기(Consumer)에 사용
public void addNumbers(List<? super Integer> list) {
    list.add(1);
    list.add(2); // 쓰기 OK
    // Integer n = list.get(0); // 컴파일 에러! 타입 보장 불가 (Object로만 읽기)
    Object obj = list.get(0); // OK
}
// 호출: addNumbers(List<Integer>), addNumbers(List<Number>), addNumbers(List<Object>) 가능

// 복사 예시 (PECS 완전 적용)
public static <T> void copy(List<? super T> dest, List<? extends T> src) {
    for (T item : src) {  // src는 Producer -> extends
        dest.add(item);   // dest는 Consumer -> super
    }
}

// 비한정 와일드카드: ? (모든 타입)
public void printList(List<?> list) {
    for (Object o : list) {
        System.out.println(o);
    }
}

3.2 제네릭 메서드와 타입 소거

// 제네릭 메서드
public static <T extends Comparable<T>> T max(T a, T b) {
    return a.compareTo(b) >= 0 ? a : b;
}

// 재귀 타입 바운드
public static <T extends Comparable<T>> T findMax(List<T> list) {
    return list.stream().max(Comparator.naturalOrder()).orElseThrow();
}

// 타입 소거 (Type Erasure)
// 컴파일 후 List<String>은 List가 됨 (런타임에 타입 정보 없음)
List<String> strings = new ArrayList<>();
List<Integer> integers = new ArrayList<>();
System.out.println(strings.getClass() == integers.getClass()); // true!

// instanceof에 제네릭 타입 사용 불가
// if (obj instanceof List<String>) {} // 컴파일 에러

// 올바른 방법
if (obj instanceof List<?> list && !list.isEmpty() && list.get(0) instanceof String) {
    List<String> stringList = (List<String>) list; // unchecked warning
}

// 제네릭 배열 생성 불가
// T[] arr = new T[10]; // 컴파일 에러
// 해결책:
@SuppressWarnings("unchecked")
T[] arr = (T[]) new Object[10];
// 또는 리스트 사용
List<T> list = new ArrayList<>();

4. 컬렉션 프레임워크

4.1 불변 컬렉션 (Java 9+)

// 불변 List (null 불가, 순서 유지)
List<String> immutableList = List.of("a", "b", "c");
// immutableList.add("d"); // UnsupportedOperationException

// 불변 Set
Set<Integer> immutableSet = Set.of(1, 2, 3, 4, 5);
// Set.of는 순서 미보장, 중복 불가

// 불변 Map
Map<String, Integer> immutableMap = Map.of(
    "one", 1,
    "two", 2,
    "three", 3
);

// 10개 이상: Map.ofEntries
Map<String, Integer> bigMap = Map.ofEntries(
    Map.entry("a", 1),
    Map.entry("b", 2),
    Map.entry("c", 3)
    // ...
);

// 복사본 (불변)
List<String> original = new ArrayList<>(List.of("a", "b"));
List<String> copy = List.copyOf(original);
Map<String, Integer> mapCopy = Map.copyOf(immutableMap);

4.2 Map 구현체 선택 기준

// HashMap: 순서 없음, O(1) 평균 (일반적인 경우)
Map<String, Integer> hashMap = new HashMap<>();

// LinkedHashMap: 삽입 순서 유지, O(1) 평균
Map<String, Integer> linkedMap = new LinkedHashMap<>();

// TreeMap: 키 기준 정렬, O(log n)
Map<String, Integer> treeMap = new TreeMap<>();
Map<String, Integer> reverseMap = new TreeMap<>(Comparator.reverseOrder());

// ConcurrentHashMap: 스레드 안전, 세그먼트 잠금
ConcurrentHashMap<String, Integer> concurrentMap = new ConcurrentHashMap<>();
concurrentMap.computeIfAbsent("key", k -> 0);
concurrentMap.merge("key", 1, Integer::sum); // 원자적 카운터

// EnumMap: Enum 키, 매우 빠름
EnumMap<DayOfWeek, String> schedule = new EnumMap<>(DayOfWeek.class);

4.3 List/Deque 구현체

// ArrayList: 랜덤 접근 O(1), 삽입/삭제 O(n)
List<String> arrayList = new ArrayList<>();

// LinkedList: 삽입/삭제 O(1), 랜덤 접근 O(n) (실제로는 캐시 미스로 느림)
// 권장하지 않음 - ArrayDeque가 대부분의 경우 더 빠름

// ArrayDeque: 양방향 큐, 스택/큐 역할 모두 가능, 빠름
Deque<String> deque = new ArrayDeque<>();
deque.push("first");      // 스택: 앞에 추가
deque.offerLast("last");  // 큐: 뒤에 추가
String top = deque.peek(); // 첫 번째 확인 (제거 없음)
String removed = deque.poll(); // 첫 번째 제거

// CopyOnWriteArrayList: 읽기 많고 쓰기 적은 경우, 스레드 안전
CopyOnWriteArrayList<String> cowList = new CopyOnWriteArrayList<>();

5. 예외 처리 패턴

5.1 Checked vs Unchecked Exception

// Checked Exception: 컴파일러가 처리 강제 (IOException, SQLException 등)
public String readFile(String path) throws IOException {
    return Files.readString(Path.of(path));
}

// Unchecked Exception: RuntimeException 하위 (처리 선택)
public int divide(int a, int b) {
    if (b == 0) throw new ArithmeticException("Division by zero");
    return a / b;
}

// 커스텀 예외 계층 설계
public class AppException extends RuntimeException {
    private final String errorCode;

    public AppException(String errorCode, String message) {
        super(message);
        this.errorCode = errorCode;
    }

    public AppException(String errorCode, String message, Throwable cause) {
        super(message, cause);
        this.errorCode = errorCode;
    }

    public String getErrorCode() { return errorCode; }
}

public class ResourceNotFoundException extends AppException {
    public ResourceNotFoundException(String resourceType, Object id) {
        super("RESOURCE_NOT_FOUND",
              resourceType + " not found with id: " + id);
    }
}

public class ValidationException extends AppException {
    private final Map<String, String> fieldErrors;

    public ValidationException(Map<String, String> fieldErrors) {
        super("VALIDATION_FAILED", "Validation failed");
        this.fieldErrors = Map.copyOf(fieldErrors);
    }

    public Map<String, String> getFieldErrors() { return fieldErrors; }
}

5.2 try-with-resources & Multi-catch

// try-with-resources (AutoCloseable 구현체)
public String readAndClose(String path) throws IOException {
    try (BufferedReader reader = new BufferedReader(new FileReader(path))) {
        return reader.lines().collect(Collectors.joining("\n"));
    } // reader.close() 자동 호출
}

// 여러 리소스 (역순으로 닫힘)
public void copyFile(String src, String dest) throws IOException {
    try (
        InputStream in = new FileInputStream(src);
        OutputStream out = new FileOutputStream(dest)
    ) {
        in.transferTo(out);
    }
}

// Multi-catch
public void process(String input) {
    try {
        int value = Integer.parseInt(input);
        String result = riskyOperation(value);
        System.out.println(result);
    } catch (NumberFormatException | IllegalArgumentException e) {
        System.err.println("Invalid input: " + e.getMessage());
    } catch (RuntimeException e) {
        log.error("Unexpected error", e);
        throw e; // 재발생
    }
}

// Effective Java: 예외를 삼키지 말 것
// Bad:
try { riskyOperation(); } catch (Exception e) { /* 무시 */ }

// Good:
try { riskyOperation(); } catch (Exception e) {
    log.error("Operation failed", e);
    throw new AppException("OPERATION_FAILED", "Failed to complete operation", e);
}

6. 동시성 (Concurrency)

6.1 CompletableFuture

// 비동기 작업 체인
CompletableFuture<String> future = CompletableFuture
    .supplyAsync(() -> fetchUserId())          // 비동기 시작
    .thenApplyAsync(id -> fetchUser(id))       // 변환 (새 스레드)
    .thenApply(user -> user.getName())         // 변환 (같은 스레드)
    .exceptionally(ex -> "Unknown User");      // 예외 처리

// thenCompose: flatMap 역할 (비동기 체인)
CompletableFuture<Order> orderFuture = CompletableFuture
    .supplyAsync(() -> fetchUser(userId))
    .thenCompose(user -> CompletableFuture.supplyAsync(() -> fetchLatestOrder(user)));

// thenCombine: 두 비동기 결과 합치기
CompletableFuture<User> userFuture = CompletableFuture.supplyAsync(() -> fetchUser(id));
CompletableFuture<List<Order>> ordersFuture = CompletableFuture.supplyAsync(() -> fetchOrders(id));

CompletableFuture<UserProfile> profileFuture = userFuture.thenCombine(
    ordersFuture,
    (user, orders) -> new UserProfile(user, orders)
);

// allOf: 모두 완료까지 대기
CompletableFuture<Void> allDone = CompletableFuture.allOf(
    task1, task2, task3
);
allDone.thenRun(() -> System.out.println("All done!"));

// anyOf: 하나라도 완료
CompletableFuture<Object> firstDone = CompletableFuture.anyOf(task1, task2, task3);

// 타임아웃
CompletableFuture<String> withTimeout = future
    .orTimeout(5, TimeUnit.SECONDS)
    .completeOnTimeout("default", 3, TimeUnit.SECONDS);

// 커스텀 Executor 지정
Executor executor = Executors.newFixedThreadPool(10);
CompletableFuture.supplyAsync(() -> heavyTask(), executor);

6.2 Virtual Threads와 구조적 동시성

// Virtual Thread 활용 - I/O 집약적 작업
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
    List<CompletableFuture<String>> tasks = urls.stream()
        .map(url -> CompletableFuture.supplyAsync(() -> httpGet(url), executor))
        .collect(Collectors.toList());

    List<String> results = tasks.stream()
        .map(CompletableFuture::join)
        .collect(Collectors.toList());
}

// ReentrantLock
ReentrantLock lock = new ReentrantLock(true); // fair lock
lock.lock();
try {
    // critical section
} finally {
    lock.unlock(); // finally에서 반드시 해제
}

// tryLock으로 타임아웃
if (lock.tryLock(1, TimeUnit.SECONDS)) {
    try {
        // critical section
    } finally {
        lock.unlock();
    }
} else {
    // 잠금 획득 실패 처리
}

// ReadWriteLock: 다수 읽기, 단일 쓰기
ReadWriteLock rwLock = new ReentrantReadWriteLock();
Map<String, String> cache = new HashMap<>();

public String get(String key) {
    rwLock.readLock().lock();
    try {
        return cache.get(key);
    } finally {
        rwLock.readLock().unlock();
    }
}

public void put(String key, String value) {
    rwLock.writeLock().lock();
    try {
        cache.put(key, value);
    } finally {
        rwLock.writeLock().unlock();
    }
}

7. 모던 Java 패턴

7.1 Builder Pattern

// 수동 Builder
public class HttpRequest {
    private final String url;
    private final String method;
    private final Map<String, String> headers;
    private final String body;
    private final int timeoutMs;

    private HttpRequest(Builder builder) {
        this.url = builder.url;
        this.method = builder.method;
        this.headers = Map.copyOf(builder.headers);
        this.body = builder.body;
        this.timeoutMs = builder.timeoutMs;
    }

    public static Builder builder(String url) {
        return new Builder(url);
    }

    public static class Builder {
        private final String url;
        private String method = "GET";
        private Map<String, String> headers = new HashMap<>();
        private String body;
        private int timeoutMs = 5000;

        private Builder(String url) { this.url = url; }

        public Builder method(String method) { this.method = method; return this; }
        public Builder header(String key, String value) { headers.put(key, value); return this; }
        public Builder body(String body) { this.body = body; return this; }
        public Builder timeout(int ms) { this.timeoutMs = ms; return this; }
        public HttpRequest build() { return new HttpRequest(this); }
    }
}

// 사용
HttpRequest request = HttpRequest.builder("https://api.example.com/users")
    .method("POST")
    .header("Content-Type", "application/json")
    .header("Authorization", "Bearer token123")
    .body("{\"name\": \"Alice\"}")
    .timeout(3000)
    .build();

7.2 Sealed Class + Pattern Matching으로 ADT 구현

// Result 타입 (Either와 유사)
public sealed interface Result<T> permits Result.Ok, Result.Err {

    record Ok<T>(T value) implements Result<T> {}
    record Err<T>(String message, Throwable cause) implements Result<T> {
        public Err(String message) { this(message, null); }
    }

    static <T> Result<T> ok(T value) { return new Ok<>(value); }
    static <T> Result<T> err(String message) { return new Err<>(message); }
    static <T> Result<T> of(Supplier<T> supplier) {
        try {
            return ok(supplier.get());
        } catch (Exception e) {
            return err(e.getMessage());
        }
    }

    default <U> Result<U> map(Function<T, U> mapper) {
        return switch (this) {
            case Ok<T> ok -> Result.ok(mapper.apply(ok.value()));
            case Err<T> err -> Result.err(err.message());
        };
    }

    default T getOrElse(T defaultValue) {
        return switch (this) {
            case Ok<T> ok -> ok.value();
            case Err<T> ignored -> defaultValue;
        };
    }
}

// 사용 예시
Result<Integer> result = Result.of(() -> Integer.parseInt("123"));
Result<String> mapped = result.map(n -> "Number: " + n);
String output = mapped.getOrElse("Parse failed");
System.out.println(output); // "Number: 123"

// Command 패턴 (ADT)
public sealed interface Command permits Command.CreateUser, Command.DeleteUser, Command.UpdateEmail {
    record CreateUser(String name, String email) implements Command {}
    record DeleteUser(String userId) implements Command {}
    record UpdateEmail(String userId, String newEmail) implements Command {}
}

public void handle(Command command) {
    switch (command) {
        case Command.CreateUser(String name, String email) ->
            userService.create(name, email);
        case Command.DeleteUser(String userId) ->
            userService.delete(userId);
        case Command.UpdateEmail(String userId, String newEmail) ->
            userService.updateEmail(userId, newEmail);
    }
}

7.3 Spring Boot와의 연계

// Record를 DTO로 사용
public record CreateUserRequest(
    @NotBlank String name,
    @Email String email,
    @Min(0) @Max(150) int age
) {}

public record UserResponse(
    String id,
    String name,
    String email,
    LocalDateTime createdAt
) {
    public static UserResponse from(User user) {
        return new UserResponse(
            user.getId(),
            user.getName(),
            user.getEmail(),
            user.getCreatedAt()
        );
    }
}

// Controller
@RestController
@RequestMapping("/api/users")
public class UserController {

    @PostMapping
    public ResponseEntity<UserResponse> createUser(
            @RequestBody @Valid CreateUserRequest request) {
        User user = userService.create(request);
        return ResponseEntity.status(HttpStatus.CREATED)
                .body(UserResponse.from(user));
    }
}

// Sealed class로 API 응답 통일
public sealed interface ApiResponse<T> permits ApiResponse.Success, ApiResponse.Error {
    record Success<T>(T data, String message) implements ApiResponse<T> {}
    record Error<T>(String code, String message, List<String> details) implements ApiResponse<T> {}
}

8. 퀴즈

퀴즈 1: Record의 특징으로 올바르지 않은 것은?

보기:

  • A) 컴파일러가 equals, hashCode, toString을 자동 생성한다
  • B) 인스턴스 필드를 추가로 선언할 수 있다
  • C) 인터페이스를 구현할 수 있다
  • D) 다른 클래스를 상속받을 수 없다

정답: B

설명: Record는 헤더에 정의된 컴포넌트 외에 인스턴스 필드를 추가로 선언할 수 없습니다. static 필드는 가능합니다. Record는 java.lang.Record를 암묵적으로 상속하므로 다른 클래스를 상속받을 수 없으며, 인터페이스는 구현 가능합니다.

퀴즈 2: PECS 원칙에서 다음 코드의 올바른 와일드카드 유형은? public void addToList(List<___> list, T element)

보기:

  • A) extends T
  • B) super T
  • C) ?
  • D) 와일드카드 불필요

정답: B (super T)

설명: PECS(Producer Extends, Consumer Super)에서 list가 element를 받아들이는 Consumer 역할을 하므로 ? super T를 사용합니다. ? extends T는 읽기(Producer)에 사용합니다. List<? super T>는 T 타입 또는 T의 상위 타입 리스트를 받을 수 있어 T 요소를 안전하게 추가할 수 있습니다.

퀴즈 3: Virtual Thread에 대한 설명으로 올바른 것은?

보기:

  • A) Virtual Thread는 OS 스레드와 1:1 매핑된다
  • B) CPU 집약적 작업에 특히 효과적이다
  • C) 블로킹 I/O 시 캐리어 스레드에서 마운트 해제된다
  • D) synchronized 블록에서도 항상 최적의 성능을 발휘한다

정답: C

설명: Virtual Thread는 블로킹 I/O 발생 시 OS 스레드(캐리어 스레드)에서 언마운트되어 다른 Virtual Thread가 해당 OS 스레드를 사용할 수 있습니다. CPU 집약적 작업보다 I/O 집약적 작업에 적합하며, synchronized 블록에서는 pinning이 발생할 수 있어 성능 저하가 있을 수 있습니다(ReentrantLock 권장).

퀴즈 4: 다음 Stream 코드의 결과는? List.of(1, 2, 3, 4, 5).stream().filter(n -> n % 2 == 0).map(n -> n * n).reduce(0, Integer::sum)

보기:

  • A) 20
  • B) 29
  • C) 25
  • D) 4

정답: A (20)

설명: filter로 짝수 선별 [2, 4], map으로 제곱 [4, 16], reduce로 합산 4 + 16 = 20입니다.

퀴즈 5: Sealed Class에서 permits에 나열된 하위 클래스가 가질 수 없는 특징은?

보기:

  • A) final로 선언되어 더 이상 상속 불가
  • B) sealed로 선언되어 자신의 하위 타입을 제한
  • C) non-sealed로 선언되어 자유롭게 상속 허용
  • D) abstract로 선언되어 직접 인스턴스화 불가

정답: D

설명: Sealed Class의 직접 하위 클래스는 반드시 final, sealed, 또는 non-sealed 중 하나로 선언되어야 합니다. abstract만으로는 선언할 수 없습니다(abstract는 sealed나 final과 함께 사용 가능하지만 단독으로 permits 조건을 충족하지 않습니다). Java 컴파일러는 Sealed 계층의 모든 하위 클래스를 알고 있어야 하기 때문입니다.


마무리

모던 Java는 단순한 문법 개선을 넘어 함수형 프로그래밍, 타입 안전성, 고성능 동시성을 언어 차원에서 지원합니다. Java 21 LTS를 기반으로:

  • Record + Sealed Class + Pattern Matching: 타입 안전한 도메인 모델
  • Stream API + Optional: 선언적 데이터 처리
  • Virtual Threads: 고성능 I/O 동시성
  • CompletableFuture: 비동기 파이프라인

이 기능들을 적절히 조합하면 간결하고 안전하며 고성능인 Java 코드를 작성할 수 있습니다.

Modern Java Complete Syntax Guide: Java 17/21 Edition

Modern Java Complete Syntax Guide: Java 17/21 Edition

Java continues to evolve rapidly. Java 21 LTS includes revolutionary features like Project Loom (Virtual Threads), Pattern Matching, Records, and Sealed Classes. This guide gives you a complete mastery of the modern Java syntax you must know for professional development.


1. Java 21 Core New Features (LTS)

1.1 Record Classes (Java 16+)

Records provide a concise way to declare immutable data classes. The compiler automatically generates the constructor, getters, equals, hashCode, and toString.

// Old approach (full of boilerplate)
public class PersonOld {
    private final String name;
    private final int age;

    public PersonOld(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String getName() { return name; }
    public int getAge() { return age; }

    @Override public boolean equals(Object o) { /* ... */ }
    @Override public int hashCode() { /* ... */ }
    @Override public String toString() { /* ... */ }
}

// Record approach (one line!)
public record Person(String name, int age) {}

// Usage
Person p = new Person("Alice", 30);
System.out.println(p.name()); // "Alice"  (getter names match field names)
System.out.println(p);        // Person[name=Alice, age=30]

// Custom constructor for validation
public record Person(String name, int age) {
    public Person {
        if (name == null || name.isBlank()) throw new IllegalArgumentException("name cannot be blank");
        if (age < 0 || age > 150) throw new IllegalArgumentException("invalid age: " + age);
    }

    // Additional methods can be defined
    public String greeting() {
        return "Hello, I'm " + name + " and I'm " + age + " years old.";
    }
}

// Records can implement interfaces
public record Point(double x, double y) implements Comparable<Point> {
    @Override
    public int compareTo(Point other) {
        return Double.compare(this.distance(), other.distance());
    }

    private double distance() {
        return Math.sqrt(x * x + y * y);
    }
}

Record constraints:

  • Cannot be extended (implicitly final)
  • All fields are private final
  • Cannot add instance fields (static fields are allowed)

1.2 Sealed Classes (Java 17+)

Sealed classes explicitly restrict which classes or interfaces can inherit from or implement a given type, enabling closed type hierarchies.

// Shape can only be extended by Circle, Rectangle, Triangle
public sealed class Shape permits Circle, Rectangle, Triangle {}

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

public final class Rectangle extends Shape {
    private final double width, height;
    public Rectangle(double width, double height) {
        this.width = width;
        this.height = height;
    }
    public double width() { return width; }
    public double height() { return height; }
}

public non-sealed class Triangle extends Shape {
    // non-sealed: classes extending Triangle are unrestricted
    private final double base, height;
    public Triangle(double base, double height) {
        this.base = base;
        this.height = height;
    }
}

// Sealed interfaces work too
public sealed interface Result<T> permits Result.Success, Result.Failure {
    record Success<T>(T value) implements Result<T> {}
    record Failure<T>(String error) implements Result<T> {}
}

1.3 Pattern Matching for instanceof (Java 16+)

No more explicit casting after instanceof checks.

// Old approach
if (obj instanceof String) {
    String s = (String) obj;
    System.out.println(s.length());
}

// Modern approach (Pattern Matching)
if (obj instanceof String s) {
    System.out.println(s.length()); // s is automatically cast to String
}

// Combined with conditions
if (obj instanceof String s && s.length() > 5) {
    System.out.println("Long string: " + s);
}

// Negation pattern
if (!(obj instanceof String s)) {
    System.out.println("Not a string");
    return;
}
// From here s is available
System.out.println(s.toUpperCase());

1.4 Pattern Matching for switch (Java 21)

Switch expressions support type pattern matching, especially powerful combined with Sealed Classes.

// Type-based switch
public double calculateArea(Shape shape) {
    return switch (shape) {
        case Circle c -> Math.PI * c.radius() * c.radius();
        case Rectangle r -> r.width() * r.height();
        case Triangle t -> 0.5 * t.base() * t.height();
        // No default needed since Shape is sealed (all cases covered)
    };
}

// Guarded patterns (conditional patterns)
public String classify(Object obj) {
    return switch (obj) {
        case Integer i when i < 0 -> "Negative integer: " + i;
        case Integer i when i == 0 -> "Zero";
        case Integer i -> "Positive integer: " + i;
        case String s when s.isEmpty() -> "Empty string";
        case String s -> "String: " + s;
        case null -> "Null value";
        default -> "Other: " + obj.getClass().getSimpleName();
    };
}

// Record Pattern deconstruction
public String describePoint(Object obj) {
    return switch (obj) {
        case Point(double x, double y) when x == 0 && y == 0 -> "Origin";
        case Point(double x, double y) when x == 0 -> "On Y-axis at " + y;
        case Point(double x, double y) when y == 0 -> "On X-axis at " + x;
        case Point(double x, double y) -> "Point at (" + x + ", " + y + ")";
        default -> "Not a point";
    };
}

1.5 Text Blocks (Java 15+)

Write multi-line strings with improved readability.

// Old approach
String json = "{\n" +
    "  \"name\": \"Alice\",\n" +
    "  \"age\": 30\n" +
    "}";

// Text Block
String json = """
        {
          "name": "Alice",
          "age": 30
        }
        """;

// SQL example
String sql = """
        SELECT u.id, u.name, o.total
        FROM users u
        JOIN orders o ON u.id = o.user_id
        WHERE u.active = true
        ORDER BY o.total DESC
        """;

// Escape sequences: \s (preserve trailing whitespace), \ (suppress newline)
String oneLine = """
        Hello \
        World\
        """; // "Hello World"

1.6 Virtual Threads (Java 21, Project Loom)

Virtual Threads are lightweight threads managed by the JVM. Instead of 1:1 mapping to OS threads, they use M:N multiplexing allowing millions of concurrent tasks.

// Traditional platform thread
Thread platformThread = new Thread(() -> {
    System.out.println("Platform thread: " + Thread.currentThread());
});
platformThread.start();

// Virtual Thread creation
Thread virtualThread = Thread.ofVirtual()
    .name("my-virtual-thread")
    .start(() -> {
        System.out.println("Virtual thread: " + Thread.currentThread());
    });

// ExecutorService with Virtual Threads
try (ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor()) {
    List<Future<String>> futures = new ArrayList<>();
    for (int i = 0; i < 10_000; i++) {
        int taskId = i;
        futures.add(executor.submit(() -> {
            // Blocking I/O is fine - Virtual Thread unmounts from carrier thread
            Thread.sleep(100);
            return "Task " + taskId + " completed";
        }));
    }
    for (Future<String> f : futures) {
        System.out.println(f.get());
    }
}

// With Spring Boot (application.properties)
// spring.threads.virtual.enabled=true

// Structured Concurrency (preview feature)
try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
    Future<User> user = scope.fork(() -> fetchUser(userId));
    Future<List<Order>> orders = scope.fork(() -> fetchOrders(userId));
    scope.join().throwIfFailed();
    return new UserProfile(user.resultNow(), orders.resultNow());
}

1.7 Sequenced Collections (Java 21)

New interfaces add the concept of ordering to collections.

// SequencedCollection: getFirst(), getLast(), addFirst(), addLast(), reversed()
List<String> list = new ArrayList<>(List.of("a", "b", "c"));
System.out.println(list.getFirst()); // "a"
System.out.println(list.getLast());  // "c"
list.addFirst("z");                  // ["z", "a", "b", "c"]
list.addLast("x");                   // ["z", "a", "b", "c", "x"]
List<String> reversed = list.reversed(); // ["x", "c", "b", "a", "z"]

// SequencedMap: firstEntry(), lastEntry(), putFirst(), putLast(), reversed()
LinkedHashMap<String, Integer> map = new LinkedHashMap<>();
map.put("one", 1);
map.put("two", 2);
map.put("three", 3);
System.out.println(map.firstEntry()); // one=1
System.out.println(map.lastEntry());  // three=3

2. Lambda Expressions and Stream API

2.1 Lambda Expression Syntax

// Basic form
Runnable r = () -> System.out.println("Hello");
Comparator<String> comp = (a, b) -> a.compareTo(b);

// Block lambda (multiple statements)
Function<Integer, Integer> factorial = n -> {
    int result = 1;
    for (int i = 1; i <= n; i++) result *= i;
    return result;
};

// Type inference
List<String> names = List.of("Charlie", "Alice", "Bob");
names.sort((a, b) -> a.compareTo(b));
// Or with method reference
names.sort(String::compareTo);

2.2 Functional Interfaces

// Function<T, R>: T -> R
Function<String, Integer> strLen = String::length;
Function<Integer, Integer> doubler = x -> x * 2;
Function<String, Integer> lenThenDouble = strLen.andThen(doubler);
System.out.println(lenThenDouble.apply("Hello")); // 10

// BiFunction<T, U, R>: T, U -> R
BiFunction<String, Integer, String> repeat = (s, n) -> s.repeat(n);
System.out.println(repeat.apply("Java", 3)); // "JavaJavaJava"

// Predicate<T>: T -> boolean
Predicate<String> isLong = s -> s.length() > 5;
Predicate<String> startsWithA = s -> s.startsWith("A");
Predicate<String> combined = isLong.and(startsWithA);
Predicate<String> either = isLong.or(startsWithA);
Predicate<String> notLong = isLong.negate();

// Supplier<T>: () -> T
Supplier<LocalDate> today = LocalDate::now;
Supplier<List<String>> listFactory = ArrayList::new;

// Consumer<T>: T -> void
Consumer<String> printer = System.out::println;
Consumer<String> both = printer.andThen(s -> log.info("Processed: {}", s));

// UnaryOperator<T>: T -> T (specialization of Function)
UnaryOperator<String> toUpper = String::toUpperCase;

// BinaryOperator<T>: T, T -> T
BinaryOperator<Integer> add = Integer::sum;

2.3 Method References (4 Types)

// 1. Static method reference: ClassName::staticMethod
Function<String, Integer> parseInt = Integer::parseInt;

// 2. Bound instance method reference: instance::instanceMethod
String prefix = "Hello, ";
Function<String, String> greeter = prefix::concat;
System.out.println(greeter.apply("World")); // "Hello, World"

// 3. Unbound instance method reference: ClassName::instanceMethod
Function<String, String> upper = String::toUpperCase;
Comparator<String> byLength = Comparator.comparingInt(String::length);

// 4. Constructor reference: ClassName::new
Supplier<ArrayList<String>> listCreator = ArrayList::new;
Function<String, StringBuilder> sbCreator = StringBuilder::new;

2.4 Stream API Core Operations

List<Employee> employees = List.of(
    new Employee("Alice", "Engineering", 90_000),
    new Employee("Bob", "Engineering", 80_000),
    new Employee("Charlie", "Marketing", 60_000),
    new Employee("Diana", "Marketing", 70_000),
    new Employee("Eve", "Engineering", 95_000)
);

// filter + map + collect
List<String> highEarnerNames = employees.stream()
    .filter(e -> e.salary() > 75_000)
    .map(Employee::name)
    .sorted()
    .toList(); // Java 16+ (returns unmodifiable list)

// flatMap: Stream<Stream<T>> -> Stream<T>
List<List<Integer>> nested = List.of(List.of(1, 2, 3), List.of(4, 5), List.of(6));
List<Integer> flat = nested.stream()
    .flatMap(Collection::stream)
    .toList();
// [1, 2, 3, 4, 5, 6]

// groupingBy
Map<String, List<Employee>> byDept = employees.stream()
    .collect(Collectors.groupingBy(Employee::department));

Map<String, Double> avgSalaryByDept = employees.stream()
    .collect(Collectors.groupingBy(
        Employee::department,
        Collectors.averagingInt(Employee::salary)
    ));

// partitioningBy
Map<Boolean, List<Employee>> partition = employees.stream()
    .collect(Collectors.partitioningBy(e -> e.salary() > 75_000));
// true -> high earners, false -> others

// joining
String names = employees.stream()
    .map(Employee::name)
    .collect(Collectors.joining(", ", "[", "]"));
// "[Alice, Bob, Charlie, Diana, Eve]"

// toMap
Map<String, Integer> nameSalaryMap = employees.stream()
    .collect(Collectors.toMap(Employee::name, Employee::salary));

2.5 Optional

// Creating Optional
Optional<String> opt1 = Optional.of("value");        // null disallowed
Optional<String> opt2 = Optional.ofNullable(null);   // null -> empty
Optional<String> opt3 = Optional.empty();

// Extracting values
String val1 = opt1.get();                              // throws NoSuchElementException
String val2 = opt1.orElse("default");                 // always evaluated
String val3 = opt1.orElseGet(() -> computeDefault()); // lazily evaluated
String val4 = opt1.orElseThrow(() -> new NotFoundException("not found"));

// Transformation
Optional<Integer> length = opt1.map(String::length);

// flatMap: avoids Optional<Optional<T>>
Optional<Address> address = Optional.of(user)
    .flatMap(u -> Optional.ofNullable(u.getAddress()));

// Conditional processing
opt1.ifPresent(System.out::println);
opt1.ifPresentOrElse(
    v -> System.out.println("Found: " + v),
    () -> System.out.println("Not found")
);

// Anti-patterns to avoid:
// - opt.get() without isPresent() check
// - Using Optional as method parameters (not recommended)
// - Returning Optional from getters in domain entities (use for return values only)

2.6 Parallel Streams — Caveats

// Parallel stream - best for CPU-bound stateless operations
List<Integer> numbers = IntStream.rangeClosed(1, 1_000_000)
    .boxed().collect(Collectors.toList());

long sum = numbers.parallelStream()
    .mapToLong(Integer::longValue)
    .sum(); // Safe: stateless operation

// DANGER: mutating shared state
List<Integer> results = new ArrayList<>();
numbers.parallelStream()
    .filter(n -> n % 2 == 0)
    .forEach(results::add); // Unsafe! ArrayList is not thread-safe

// Correct approach: use collect
List<Integer> safeResults = numbers.parallelStream()
    .filter(n -> n % 2 == 0)
    .collect(Collectors.toList()); // thread-safe

// For I/O-bound work, prefer Virtual Threads
// Parallel streams use ForkJoinPool.commonPool() — risk of pool starvation

3. Generics In Depth

3.1 Wildcards and the PECS Principle

PECS: Producer Extends, Consumer Super

// Covariant (extends) — for reading (Producer)
public double sumList(List<? extends Number> list) {
    double sum = 0;
    for (Number n : list) sum += n.doubleValue(); // reading OK
    // list.add(1.0); // compile error — writing forbidden
    return sum;
}
// Can call with List<Integer>, List<Double>, etc.

// Contravariant (super) — for writing (Consumer)
public void addNumbers(List<? super Integer> list) {
    list.add(1);
    list.add(2); // writing OK
    // Integer n = list.get(0); // compile error — type not guaranteed
    Object obj = list.get(0); // OK
}
// Can call with List<Integer>, List<Number>, List<Object>

// Copy method (full PECS application)
public static <T> void copy(List<? super T> dest, List<? extends T> src) {
    for (T item : src) {  // src is Producer -> extends
        dest.add(item);   // dest is Consumer -> super
    }
}

// Unbounded wildcard: ? (any type)
public void printList(List<?> list) {
    for (Object o : list) System.out.println(o);
}

3.2 Generic Methods and Type Erasure

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

// Recursive type bound
public static <T extends Comparable<T>> T findMax(List<T> list) {
    return list.stream().max(Comparator.naturalOrder()).orElseThrow();
}

// Type Erasure
// After compilation, List<String> becomes List (no type info at runtime)
List<String> strings = new ArrayList<>();
List<Integer> integers = new ArrayList<>();
System.out.println(strings.getClass() == integers.getClass()); // true!

// Cannot use generic type in instanceof
// if (obj instanceof List<String>) {} // compile error

// Correct approach
if (obj instanceof List<?> list && !list.isEmpty() && list.get(0) instanceof String) {
    @SuppressWarnings("unchecked")
    List<String> stringList = (List<String>) list;
}

// Cannot create generic array
// T[] arr = new T[10]; // compile error
// Workaround:
@SuppressWarnings("unchecked")
T[] arr = (T[]) new Object[10];

4. Collections Framework

4.1 Immutable Collections (Java 9+)

// Immutable List (no nulls, preserves order)
List<String> immutableList = List.of("a", "b", "c");
// immutableList.add("d"); // UnsupportedOperationException

// Immutable Set
Set<Integer> immutableSet = Set.of(1, 2, 3, 4, 5);
// Set.of has no guaranteed order, no duplicates

// Immutable Map
Map<String, Integer> immutableMap = Map.of(
    "one", 1,
    "two", 2,
    "three", 3
);

// More than 10 entries: Map.ofEntries
Map<String, Integer> bigMap = Map.ofEntries(
    Map.entry("a", 1),
    Map.entry("b", 2),
    Map.entry("c", 3)
);

// Immutable copies
List<String> original = new ArrayList<>(List.of("a", "b"));
List<String> copy = List.copyOf(original);
Map<String, Integer> mapCopy = Map.copyOf(immutableMap);

4.2 Choosing the Right Map

// HashMap: no ordering, O(1) average
Map<String, Integer> hashMap = new HashMap<>();

// LinkedHashMap: insertion-order preserved, O(1) average
Map<String, Integer> linkedMap = new LinkedHashMap<>();

// TreeMap: sorted by key, O(log n)
Map<String, Integer> treeMap = new TreeMap<>();
Map<String, Integer> reverseMap = new TreeMap<>(Comparator.reverseOrder());

// ConcurrentHashMap: thread-safe, segment locking
ConcurrentHashMap<String, Integer> concurrentMap = new ConcurrentHashMap<>();
concurrentMap.computeIfAbsent("key", k -> 0);
concurrentMap.merge("key", 1, Integer::sum); // atomic counter

// EnumMap: Enum keys, extremely fast
EnumMap<DayOfWeek, String> schedule = new EnumMap<>(DayOfWeek.class);

4.3 List/Deque Implementations

// ArrayList: random access O(1), insert/delete O(n)
List<String> arrayList = new ArrayList<>();

// ArrayDeque: double-ended queue, both stack and queue roles, fast
Deque<String> deque = new ArrayDeque<>();
deque.push("first");      // stack: add to front
deque.offerLast("last");  // queue: add to back
String top = deque.peek();  // peek front (no removal)
String removed = deque.poll(); // remove front

// CopyOnWriteArrayList: thread-safe, for many reads / few writes
CopyOnWriteArrayList<String> cowList = new CopyOnWriteArrayList<>();

5. Exception Handling Patterns

5.1 Checked vs Unchecked

// Checked Exception: compiler forces handling
public String readFile(String path) throws IOException {
    return Files.readString(Path.of(path));
}

// Unchecked Exception: RuntimeException subclasses
public int divide(int a, int b) {
    if (b == 0) throw new ArithmeticException("Division by zero");
    return a / b;
}

// Custom exception hierarchy
public class AppException extends RuntimeException {
    private final String errorCode;

    public AppException(String errorCode, String message) {
        super(message);
        this.errorCode = errorCode;
    }

    public AppException(String errorCode, String message, Throwable cause) {
        super(message, cause);
        this.errorCode = errorCode;
    }

    public String getErrorCode() { return errorCode; }
}

public class ResourceNotFoundException extends AppException {
    public ResourceNotFoundException(String resourceType, Object id) {
        super("RESOURCE_NOT_FOUND",
              resourceType + " not found with id: " + id);
    }
}

public class ValidationException extends AppException {
    private final Map<String, String> fieldErrors;

    public ValidationException(Map<String, String> fieldErrors) {
        super("VALIDATION_FAILED", "Validation failed");
        this.fieldErrors = Map.copyOf(fieldErrors);
    }

    public Map<String, String> getFieldErrors() { return fieldErrors; }
}

5.2 try-with-resources and Multi-catch

// try-with-resources (implements AutoCloseable)
public String readAndClose(String path) throws IOException {
    try (BufferedReader reader = new BufferedReader(new FileReader(path))) {
        return reader.lines().collect(Collectors.joining("\n"));
    } // reader.close() called automatically
}

// Multiple resources (closed in reverse order)
public void copyFile(String src, String dest) throws IOException {
    try (
        InputStream in = new FileInputStream(src);
        OutputStream out = new FileOutputStream(dest)
    ) {
        in.transferTo(out);
    }
}

// Multi-catch
public void process(String input) {
    try {
        int value = Integer.parseInt(input);
        String result = riskyOperation(value);
        System.out.println(result);
    } catch (NumberFormatException | IllegalArgumentException e) {
        System.err.println("Invalid input: " + e.getMessage());
    } catch (RuntimeException e) {
        log.error("Unexpected error", e);
        throw e;
    }
}

6. Concurrency

6.1 CompletableFuture

// Async operation chaining
CompletableFuture<String> future = CompletableFuture
    .supplyAsync(() -> fetchUserId())
    .thenApplyAsync(id -> fetchUser(id))
    .thenApply(user -> user.getName())
    .exceptionally(ex -> "Unknown User");

// thenCompose: flatMap for async (avoids CompletableFuture<CompletableFuture<T>>)
CompletableFuture<Order> orderFuture = CompletableFuture
    .supplyAsync(() -> fetchUser(userId))
    .thenCompose(user -> CompletableFuture.supplyAsync(() -> fetchLatestOrder(user)));

// thenCombine: merge two async results
CompletableFuture<User> userFuture = CompletableFuture.supplyAsync(() -> fetchUser(id));
CompletableFuture<List<Order>> ordersFuture = CompletableFuture.supplyAsync(() -> fetchOrders(id));

CompletableFuture<UserProfile> profileFuture = userFuture.thenCombine(
    ordersFuture,
    (user, orders) -> new UserProfile(user, orders)
);

// allOf: wait for all to complete
CompletableFuture<Void> allDone = CompletableFuture.allOf(task1, task2, task3);

// anyOf: first to complete wins
CompletableFuture<Object> firstDone = CompletableFuture.anyOf(task1, task2, task3);

// Timeout
CompletableFuture<String> withTimeout = future
    .orTimeout(5, TimeUnit.SECONDS)
    .completeOnTimeout("default", 3, TimeUnit.SECONDS);

6.2 Locks

// ReentrantLock
ReentrantLock lock = new ReentrantLock(true); // fair lock
lock.lock();
try {
    // critical section
} finally {
    lock.unlock(); // always unlock in finally
}

// tryLock with timeout
if (lock.tryLock(1, TimeUnit.SECONDS)) {
    try {
        // critical section
    } finally {
        lock.unlock();
    }
}

// ReadWriteLock: many readers, single writer
ReadWriteLock rwLock = new ReentrantReadWriteLock();
Map<String, String> cache = new HashMap<>();

public String get(String key) {
    rwLock.readLock().lock();
    try { return cache.get(key); }
    finally { rwLock.readLock().unlock(); }
}

public void put(String key, String value) {
    rwLock.writeLock().lock();
    try { cache.put(key, value); }
    finally { rwLock.writeLock().unlock(); }
}

7. Modern Java Patterns

7.1 Builder Pattern

public class HttpRequest {
    private final String url;
    private final String method;
    private final Map<String, String> headers;
    private final String body;
    private final int timeoutMs;

    private HttpRequest(Builder builder) {
        this.url = builder.url;
        this.method = builder.method;
        this.headers = Map.copyOf(builder.headers);
        this.body = builder.body;
        this.timeoutMs = builder.timeoutMs;
    }

    public static Builder builder(String url) {
        return new Builder(url);
    }

    public static class Builder {
        private final String url;
        private String method = "GET";
        private Map<String, String> headers = new HashMap<>();
        private String body;
        private int timeoutMs = 5000;

        private Builder(String url) { this.url = url; }

        public Builder method(String method) { this.method = method; return this; }
        public Builder header(String key, String value) { headers.put(key, value); return this; }
        public Builder body(String body) { this.body = body; return this; }
        public Builder timeout(int ms) { this.timeoutMs = ms; return this; }
        public HttpRequest build() { return new HttpRequest(this); }
    }
}

// Usage
HttpRequest request = HttpRequest.builder("https://api.example.com/users")
    .method("POST")
    .header("Content-Type", "application/json")
    .header("Authorization", "Bearer token123")
    .body("{\"name\": \"Alice\"}")
    .timeout(3000)
    .build();

7.2 Sealed Class + Pattern Matching as ADT

// Result type (similar to Either)
public sealed interface Result<T> permits Result.Ok, Result.Err {

    record Ok<T>(T value) implements Result<T> {}
    record Err<T>(String message, Throwable cause) implements Result<T> {
        public Err(String message) { this(message, null); }
    }

    static <T> Result<T> ok(T value) { return new Ok<>(value); }
    static <T> Result<T> err(String message) { return new Err<>(message); }
    static <T> Result<T> of(Supplier<T> supplier) {
        try { return ok(supplier.get()); }
        catch (Exception e) { return err(e.getMessage()); }
    }

    default <U> Result<U> map(Function<T, U> mapper) {
        return switch (this) {
            case Ok<T> ok -> Result.ok(mapper.apply(ok.value()));
            case Err<T> err -> Result.err(err.message());
        };
    }

    default T getOrElse(T defaultValue) {
        return switch (this) {
            case Ok<T> ok -> ok.value();
            case Err<T> ignored -> defaultValue;
        };
    }
}

// Command pattern as ADT
public sealed interface Command permits Command.CreateUser, Command.DeleteUser, Command.UpdateEmail {
    record CreateUser(String name, String email) implements Command {}
    record DeleteUser(String userId) implements Command {}
    record UpdateEmail(String userId, String newEmail) implements Command {}
}

public void handle(Command command) {
    switch (command) {
        case Command.CreateUser(String name, String email) ->
            userService.create(name, email);
        case Command.DeleteUser(String userId) ->
            userService.delete(userId);
        case Command.UpdateEmail(String userId, String newEmail) ->
            userService.updateEmail(userId, newEmail);
    }
}

7.3 Integration with Spring Boot

// Records as DTOs
public record CreateUserRequest(
    @NotBlank String name,
    @Email String email,
    @Min(0) @Max(150) int age
) {}

public record UserResponse(
    String id,
    String name,
    String email,
    LocalDateTime createdAt
) {
    public static UserResponse from(User user) {
        return new UserResponse(
            user.getId(),
            user.getName(),
            user.getEmail(),
            user.getCreatedAt()
        );
    }
}

// Controller
@RestController
@RequestMapping("/api/users")
public class UserController {

    @PostMapping
    public ResponseEntity<UserResponse> createUser(
            @RequestBody @Valid CreateUserRequest request) {
        User user = userService.create(request);
        return ResponseEntity.status(HttpStatus.CREATED)
                .body(UserResponse.from(user));
    }
}

8. Quizzes

Quiz 1: Which statement about Records is INCORRECT?

Options:

  • A) The compiler automatically generates equals, hashCode, and toString
  • B) You can declare additional instance fields
  • C) Records can implement interfaces
  • D) Records cannot extend other classes

Answer: B

Explanation: Records cannot declare additional instance fields beyond those defined in the header. Static fields are allowed. Records implicitly extend java.lang.Record and therefore cannot extend any other class, but they can implement interfaces.

Quiz 2: In the PECS principle, what is the correct wildcard for: public void addToList(List<___> list, T element)?

Options:

  • A) extends T
  • B) super T
  • C) ?
  • D) No wildcard needed

Answer: B (super T)

Explanation: PECS (Producer Extends, Consumer Super) — here the list acts as a Consumer (it receives elements), so ? super T is correct. ? extends T is for Producers (reading). List<? super T> accepts a list of T or any supertype of T, allowing safe addition of T elements.

Quiz 3: Which statement about Virtual Threads is correct?

Options:

  • A) Virtual Threads map 1:1 to OS threads
  • B) They are especially effective for CPU-bound tasks
  • C) When blocking on I/O, they unmount from the carrier thread
  • D) They always deliver optimal performance inside synchronized blocks

Answer: C

Explanation: When a Virtual Thread performs a blocking I/O operation, it unmounts from its OS carrier thread, freeing that carrier for other Virtual Threads. Virtual Threads are designed for I/O-bound rather than CPU-bound work. Inside synchronized blocks, pinning can occur (the carrier thread is held), which can hurt performance — ReentrantLock is preferred in hot paths.

Quiz 4: What is the result of: List.of(1, 2, 3, 4, 5).stream().filter(n -> n % 2 == 0).map(n -> n * n).reduce(0, Integer::sum)?

Options:

  • A) 20
  • B) 29
  • C) 25
  • D) 4

Answer: A (20)

Explanation: filter selects even numbers [2, 4], map squares them [4, 16], reduce sums them: 4 + 16 = 20.

Quiz 5: What keyword CANNOT be used as the direct subclass declaration in a sealed class hierarchy?

Options:

  • A) final
  • B) sealed
  • C) non-sealed
  • D) abstract (alone, without sealed/non-sealed/final)

Answer: D

Explanation: Every direct subclass of a sealed class must be declared as exactly one of final, sealed, or non-sealed. Using abstract alone does not fulfill this requirement. An abstract class can additionally be sealed (e.g., public abstract sealed class) but cannot be only abstract without specifying one of the three required modifiers.


Summary

Modern Java goes far beyond simple syntax improvements, providing functional programming, type safety, and high-performance concurrency at the language level. With Java 21 LTS:

  • Record + Sealed Class + Pattern Matching: type-safe domain modeling
  • Stream API + Optional: declarative data processing
  • Virtual Threads: high-throughput I/O concurrency
  • CompletableFuture: asynchronous pipelines

Combining these features appropriately lets you write Java code that is concise, safe, and high-performance.