시작하기
Hello World
모든 Java 프로그램은 main()에서 시작합니다. 파일 이름은 public 클래스 이름과 일치해야 합니다(Main.java → Main.class). javac가 바이트코드(.class)로 컴파일하고, java가 JVM에서 실행합니다. System.out.println은 stdout에 출력하고; printf는 형식 지정자를 지원합니다(%s, %d, %f, %n 줄바꿈).
// Main.java
public class Main {
public static void main(String[] args) {
System.out.println("Hello, World!");
System.out.printf("Name: %s, Age: %d%n", "Alice", 30);
}
}
// Compile: javac Main.java -> Main.class
// Run: java Main
// Package: java -cp . com.example.Main
// Every Java program needs:
// 1. A class (public class, file name must match)
// 2. A main method: public static void main(String[] args)변수 & 원시 타입
Java에는 8개의 원시 타입(int, long, double, float, boolean, char, byte, short)과 참조 타입(String, 배열, 객체)이 있습니다. 상수에는 'final'을 사용하세요. 'var'(Java 10+)는 컴파일 타임에 타입을 추론합니다 — 타입이 명확한 지역 변수에 사용하세요. 숫자의 밑줄(100_000)은 가독성을 향상합니다.
// Primitive types (8 total)
int age = 30; // 32-bit integer
long bigNum = 100_000L; // 64-bit integer
double price = 19.99; // 64-bit float (default for decimals)
float pi = 3.14f; // 32-bit float
boolean active = true; // true/false
char grade = 'A'; // 16-bit Unicode character
byte b = 127; // 8-bit signed
short s = 32767; // 16-bit signed
// Reference types
String name = "Alice"; // Object (not primitive)
int[] nums = {1, 2, 3}; // Array object
// Constants
final double PI = 3.14159; // can't be reassigned
// var (Java 10+, local type inference)
var count = 42; // inferred as int
var list = new ArrayList<String>(); // inferred as ArrayList<String>래퍼 클래스 & 박싱
래퍼 클래스(Integer, Double, Boolean 등)는 원시 타입의 객체 버전입니다. 오토박싱/언박싱이 자동 변환합니다. Integer는 -128에서 127 값을 캐시하므로, 작은 숫자는 ==가 작동하지만 큰 숫자는 실패합니다 — 항상 .equals()를 사용하세요. 래퍼는 Collections에 필요합니다(원시 타입을 가질 수 없으므로).
// Wrapper classes (Object versions of primitives)
Integer wrapped = Integer.valueOf(42); // explicit
Integer auto = 42; // autoboxing
int unboxed = auto; // unboxing
// Useful methods
int max = Integer.MAX_VALUE; // 2147483647
String bin = Integer.toBinaryString(42);
int parsed = Integer.parseInt("42");
String s = String.valueOf(42);
// Other wrappers: Double, Boolean, Character, Long, Float
Double d = 3.14;
Boolean b = Boolean.TRUE;
Character c = 'A';
// Be careful with == on wrappers
Integer a = 127, b2 = 127; // a == b2: true (cached)
Integer x = 128, y = 128; // x == y: false (not cached)
// Always use .equals() for Integer comparison패키지 & 임포트
패키지는 클래스를 조직하고 이름 충돌을 방지합니다. 관례: 역방향 도메인 이름(com.example.app). 특정 클래스를 임포트하거나 와일드카드(*)를 사용하세요. 정적 임포트는 상수와 메서드(Math.PI, Math.sqrt)를 가져옵니다. 정규화된 이름은 임포트 없이 작동하지만 장황합니다. java.lang 패키지는 자동 임포트됩니다.
// Package declaration (must be first line)
package com.example.app;
// Import specific class
import java.util.List;
import java.util.ArrayList;
// Import all classes in a package
import java.util.*;
// Static import (for static members)
import static java.lang.Math.PI;
import static java.lang.Math.sqrt;
// Usage
double area = PI * 5 * 5;
double root = sqrt(16);
// Fully qualified name (no import needed)
java.time.LocalDate today = java.time.LocalDate.now();
// Package naming convention: reverse domain
// com.google.gson, org.apache.commons, io.netty.channel입력 & 출력
System.out(stdout), System.err(stderr), System.in(stdin). Scanner는 콘솔 입력을 읽는 가장 쉬운 방법입니다 — 토큰을 파싱합니다(nextInt, nextDouble, nextLine). 리소스를 해제하려면 항상 Scanner를 닫으세요. 명령줄 인수는 args[]에 있습니다(args[0]은 C처럼 프로그램 이름이 아니라 첫 번째 인수입니다).
import java.util.Scanner;
// Console output
System.out.println("Hello"); // with newline
System.out.print("No newline"); // without newline
System.out.printf("Pi: %.2f%n", 3.14159); // formatted
// Console input with Scanner
Scanner scanner = new Scanner(System.in);
System.out.print("Enter name: ");
String name = scanner.nextLine();
System.out.print("Enter age: ");
int age = scanner.nextInt();
System.out.printf("Hi %s, age %d%n", name, age);
scanner.close(); // always close
// Command-line arguments
// java Main arg1 arg2
// args[0] = "arg1", args[1] = "arg2"문자열 & 형식화
String 메서드
문자열은 불변입니다 — 메서드는 새 문자열을 반환합니다. 내용 비교에는 항상 .equals()를 사용하세요(==는 참조를 비교). compareTo()는 정렬에 유용한 음수/영/양수를 반환합니다. split()은 String[]을 반환합니다. 가변 문자열에는 StringBuilder를 사용하세요.
String s = "Hello, World";
// Length and access
int len = s.length(); // 12
char c = s.charAt(0); // 'H'
// Comparison
s.equals("Hello, World"); // true (content comparison)
s.equalsIgnoreCase("hello, world"); // true
s.compareTo("Apple"); // positive (s > "Apple")
"abc".compareTo("abd"); // negative
// Search
s.indexOf("World"); // 7 (-1 if not found)
s.lastIndexOf("l"); // 10
s.contains("World"); // true
s.startsWith("Hello"); // true
s.endsWith("World"); // true
// Extract
s.substring(7); // "World"
s.substring(0, 5); // "Hello"
// Transform
s.toUpperCase(); // "HELLO, WORLD"
s.toLowerCase(); // "hello, world"
s.replace("o", "0"); // "Hell0, W0rld"
s.trim(); // remove whitespace
s.split(", "); // ["Hello", "World"]StringBuilder & 연결
+로 문자열 연결은 매번 새 String을 만듭니다(루프에서 비효율적). StringBuilder는 가변이고 문자열을 점진적으로 빌드하는 데 효율적입니다. StringBuffer는 스레드 안전 버전입니다(거의 필요 없음). String.join()은 구분자로 결합합니다. Java 11+는 문자열 곱셈을 위해 repeat()를 추가합니다.
// String concatenation (creates new String each time)
String s = "Hello" + ", " + "World";
String formatted = String.format("%s is %d", "Alice", 30);
// StringBuilder (mutable, efficient for many concatenations)
StringBuilder sb = new StringBuilder();
sb.append("Hello");
sb.append(", ");
sb.append("World");
sb.insert(5, " there");
sb.delete(5, 11);
sb.reverse();
String result = sb.toString();
// StringBuffer (thread-safe, slower than StringBuilder)
StringBuffer sbf = new StringBuffer("thread-safe");
// Join strings
String joined = String.join(", ", "a", "b", "c"); // "a, b, c"
// Repeat (Java 11+)
String repeated = "ab".repeat(3); // "ababab"문자열 형식화
printf/format은 C 스타일 형식 지정자를 사용합니다: %d(int), %f(float), %s(string), %c(char), %b(boolean), %x(hex). 너비(%5d), 좌측 정렬(%-5d), 제로 패드(%05d), 정밀도(%.2f). %n은 플랫폼 줄바꿈입니다. 텍스트 블록(Java 15+)의 삼중 따옴표는 이스케이프 없이 여러 줄 문자열을 가능하게 합니다.
// printf / format specifiers
System.out.printf("Int: %d%n", 42);
System.out.printf("Float: %.2f%n", 3.14159); // 3.14
System.out.printf("String: %s%n", "hello");
System.out.printf("Char: %c%n", 'A');
System.out.printf("Bool: %b%n", true);
System.out.printf("Hex: %x%n", 255); // ff
System.out.printf("Octal: %o%n", 8); // 10
// Width and padding
System.out.printf("[%5d]%n", 42); // [ 42]
System.out.printf("[%-5d]%n", 42); // [42 ]
System.out.printf("[%05d]%n", 42); // [00042]
System.out.printf("[%8.2f]%n", 3.14); // [ 3.14]
// String.format returns a String
String s = String.format("Name: %s, Age: %d", "Alice", 30);
// Text blocks (Java 15+)
String json = """
{
"name": "Alice",
"age": 30
}
""";정규 표현식
Java regex는 Pattern(컴파일된)과 Matcher(입력에 적용)을 사용합니다. String 메서드(matches, split, replaceAll)는 편리한 단축키입니다. Java 문자열 리터럴에서 백슬래시는 두 배로 해야 합니다(\\d for \d). 그룹은 괄호로 캡처되고 치환에서 $1, $2로 참조됩니다. 반복 사용하면 패턴을 한 번 컴파일하세요.
import java.util.regex.*;
// String methods
"hello123".matches("[a-z]+\d+"); // true
"a,b,c".split(","); // ["a", "b", "c"]
"hello".replaceAll("l", "L"); // "heLLo"
// Pattern and Matcher
Pattern p = Pattern.compile("\\d{4}-\\d{2}-\\d{2}");
Matcher m = p.matcher("Date: 2024-01-15");
if (m.find()) {
System.out.println(m.group()); // "2024-01-15"
}
// Find all matches
while (m.find()) {
System.out.println(m.group());
}
// Replace with regex
String result = "2024-01-15".replaceAll(
"(\\d{4})-(\\d{2})-(\\d{2})",
"$3/$2/$1"); // "15/01/2024"
// Common patterns
String email = "^[\\w.]+@[\\w.]+\\.\\w+$";
String phone = "^\\d{3}-\\d{4}$";숫자 & Math
Math 클래스는 정적 수학 함수를 제공합니다. Math.random()은 0.0-1.0을 반환합니다. 더 많은 제어를 위해 java.util.Random(시드 가능) 또는 java.security.SecureRandom(암호학적)을 사용하세요. Integer/Double은 정적 유틸리티 메서드를 가집니다. 부동소수점 정밀도에 주의하세요 — 재무 계산에는 BigDecimal을 사용하세요.
// Math class
double sqrt = Math.sqrt(16); // 4.0
double pow = Math.pow(2, 10); // 1024.0
int abs = Math.abs(-5); // 5
int max = Math.max(3, 7); // 7
int min = Math.min(3, 7); // 3
double rounded = Math.round(3.7); // 4
double ceil = Math.ceil(3.1); // 4.0
double floor = Math.floor(3.9); // 3.0
double random = Math.random(); // 0.0 to 1.0
// Constants
double pi = Math.PI; // 3.14159...
double e = Math.E; // 2.71828...
// Integer/Long methods
int sum = Integer.sum(3, 4); // 7
int max2 = Integer.max(3, 7); // 7
// Rounding modes
double r = Math.round(3.5); // 4 (round half up)
double r2 = Math.floor(3.5 + 0.5); // alternative
// Random (java.util.Random)
import java.util.Random;
Random rand = new Random();
int n = rand.nextInt(100); // 0-99
double d = rand.nextDouble(); // 0.0-1.0
boolean b = rand.nextBoolean();제어 흐름
If / Else
Java if/else는 C/C++처럼 작동합니다. 조건은 불리언이어야 합니다 — JavaScript 같은 truthy/falsy가 없습니다(0과 비어 있지 않은 문자열은 truthy가 아님). 삼항 연산자(cond ? a : b)는 문이 아닌 표현식입니다. 한 줄 본문에도 중괄호를 사용하세요(코드 스타일 모범 사례).
int score = 85;
if (score >= 90) {
System.out.println("A");
} else if (score >= 80) {
System.out.println("B");
} else if (score >= 70) {
System.out.println("C");
} else {
System.out.println("F");
}
// Ternary operator
String grade = score >= 60 ? "Pass" : "Fail";
// Nested if
if (score >= 60) {
if (score >= 90) {
System.out.println("Excellent");
}
}
// Note: conditions must be boolean (no truthy/falsy)
// if (score) { } // Error: int is not booleanSwitch & 표현식
전통적 switch는 폴스루가 있습니다(break 사용). Java 14+ switch 표현식(->)은 폴스루가 없고 값을 반환할 수 있습니다. 여러 case 레이블에 쉼표를 사용하세요(case 1, 2, 3). 'yield'는 복잡한 블록에서 값을 반환합니다. switch 표현식은 철저합니다 — 열거형에 대해 default나 모든 case가 필요합니다.
// Traditional switch (fall-through)
int day = 3;
switch (day) {
case 1:
System.out.println("Mon");
break;
case 2:
case 3:
case 4:
System.out.println("Midweek");
break;
case 6:
case 7:
System.out.println("Weekend");
break;
default:
System.out.println("Invalid");
}
// Switch expression (Java 14+, no fall-through)
String type = switch (day) {
case 1, 2, 3, 4, 5 -> "Weekday";
case 6, 7 -> "Weekend";
default -> "Invalid";
};
// Switch with yield (for complex blocks)
int result = switch (day) {
case 1, 2, 3, 4, 5 -> {
int hours = 8;
yield hours * 5;
}
case 6, 7 -> 0;
default -> -1;
};루프
Java에는 for, while, do-while 루프가 있습니다. 향상된 for(for-each)는 배열과 Iterable과 작동합니다. break는 루프를 빠져나가고; continue는 다음 반복으로 건너뜁니다. 컬렉션에는 인덱스 루프보다 for-each나 stream을 선호하세요. do-while은 적어도 한 번 실행됩니다(거의 사용 안 함).
// For loop
for (int i = 0; i < 5; i++) {
System.out.println(i);
}
// Enhanced for (for-each)
int[] nums = {1, 2, 3, 4, 5};
for (int n : nums) {
System.out.println(n);
}
List<String> names = List.of("Alice", "Bob");
for (String name : names) {
System.out.println(name);
}
// While loop
int count = 0;
while (count < 3) {
System.out.println(count);
count++;
}
// Do-while (runs at least once)
int i = 0;
do {
System.out.println(i);
i++;
} while (i < 3);
// Break and continue
for (int j = 0; j < 10; j++) {
if (j == 5) break; // exit loop
if (j % 2 == 0) continue; // skip iteration
System.out.println(j);
}레이블 break & continue
레이블(outer:)은 중첩 루프에서 외부 루프를 break/continue할 수 있게 합니다. 이는 거의 필요 없습니다 — return으로 메서드를 추출하는 것이 보통 더 깔끔합니다. 레이블은 루프 앞에 콜론이 뒤따라 배치됩니다. break label은 레이블된 루프를 빠져나가고; continue label은 다음 반복으로 건너뜁니다.
// Labels for breaking out of nested loops
outer:
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 3; j++) {
if (i == 1 && j == 1) {
break outer; // exits both loops
}
System.out.println(i + "," + j);
}
}
// Labeled continue
outer:
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 3; j++) {
if (j == 1) {
continue outer; // skip to next i
}
System.out.println(i + "," + j);
}
}
// Alternative: extract to method and use return
void findPair(int[][] matrix, int target) {
for (int i = 0; i < matrix.length; i++) {
for (int j = 0; j < matrix[i].length; j++) {
if (matrix[i][j] == target) return; // exit method
}
}
}배열
배열은 고정 길이입니다(동적에는 ArrayList 사용). Arrays.sort()는 제자리에서 정렬합니다. Arrays.toString()은 읽기 쉬운 표현을 제공합니다. Arrays.copyOf()는 새 길이로 복사본을 만듭니다. 다차원 배열의 경우, 각 행은 다른 길이를 가질 수 있습니다(가변 배열). 배열의 유틸리티 메서드를 위해 Arrays를 사용하세요.
// Declare and initialize
int[] nums = {1, 2, 3, 4, 5};
int[] empty = new int[5]; // [0, 0, 0, 0, 0]
String[] names = new String[3]; // [null, null, null]
// Access and modify
nums[0] = 10;
int first = nums[0]; // 10
int length = nums.length; // 5
// Multidimensional
int[][] matrix = {{1, 2}, {3, 4}};
int val = matrix[0][1]; // 2
// Arrays utility class
import java.util.Arrays;
int[] sorted = {3, 1, 2};
Arrays.sort(sorted); // [1, 2, 3]
int[] copy = Arrays.copyOf(nums, 3);
String str = Arrays.toString(nums); // "[10, 2, 3, 4, 5]"
boolean eq = Arrays.equals(nums, copy);
// Fill
int[] filled = new int[5];
Arrays.fill(filled, 42); // [42, 42, 42, 42, 42]
// Binary search (sorted array only)
int idx = Arrays.binarySearch(sorted, 2); // index of 2메서드 & 함수
메서드 정의
Java 메서드는 항상 클래스 내에 있습니다. 'static'은 메서드가 클래스에 속함을 의미합니다(인스턴스 없이 호출). 반환 타입(int, String, void)은 이름 앞에 선언됩니다. 매개변수는 타입화됩니다. Java에는 기본 매개변수 값이 없습니다 — 대신 메서드 오버로딩을 사용하세요.
public class Calculator {
// Method with return type
public static int add(int a, int b) {
return a + b;
}
// Void method (no return)
public static void printResult(int result) {
System.out.println("Result: " + result);
}
// Method with default (no overloading needed)
public static String greet(String name, String greeting) {
return greeting + ", " + name + "!";
}
public static void main(String[] args) {
int sum = add(3, 4);
printResult(sum);
String msg = greet("Alice", "Hello");
System.out.println(msg);
}
}메서드 오버로딩
메서드 오버로딩은 같은 이름이지만 다른 매개변수 목록(타입, 개수, 또는 순서)을 가진 여러 메서드를 허용합니다. Java는 인수 타입을 기반으로 컴파일 타임에 오버로드를 해결합니다. 오버로딩은 생성자와 유틸리티 메서드에 일반적입니다. 오버라이딩과 다릅니다(상속과 런타임 디스패치와 관련).
public class MathUtils {
// Overloaded methods (same name, different params)
public static int add(int a, int b) {
return a + b;
}
public static double add(double a, double b) {
return a + b;
}
public static int add(int a, int b, int c) {
return a + b + c;
}
public static String add(String a, String b) {
return a + b;
}
}
// Java picks the most specific match
MathUtils.add(1, 2); // int version -> 3
MathUtils.add(1.5, 2.5); // double version -> 4.0
MathUtils.add(1, 2, 3); // 3-param version -> 6
MathUtils.add("Hello", "!"); // String version -> "Hello!"Varargs & 값 전달
Varargs(Type... name)는 가변 인수를 허용하여, 배열로 받습니다. Java는 항상 값 전달입니다: 원시 타입은 복사되고, 객체 참조는 복사되지만(같은 객체를 가리킴). 따라서 메서드 내부에서 매개변수를 수정해도 호출자의 변수에는 영향을 주지 않지만, 가리키는 객체를 수정하면 영향을 줍니다.
// Varargs: variable number of arguments
public static int sum(int... nums) {
int total = 0;
for (int n : nums) {
total += n;
}
return total;
}
sum(1, 2, 3); // 6
sum(1, 2, 3, 4, 5); // 15
sum(); // 0 (empty array)
int[] arr = {1, 2, 3};
sum(arr); // 6 (pass array to varargs)
// Java is ALWAYS pass-by-value
public static void modify(int x) {
x = 100; // doesn't affect the caller's variable
}
int n = 5;
modify(n);
System.out.println(n); // still 5
// For objects, the reference is passed by value
public static void addItem(List<String> list) {
list.add("new"); // modifies the same list object
}재귀
재귀는 메서드가 자신을 호출하는 것입니다. 항상 멈출 기본 케이스를 가지세요. Java는 꼬리 재귀를 최적화하지 않으므로(일부 언어와 달리), 깊은 재귀는 StackOverflowError를 일으킬 수 있습니다. 성능 중요하거나 깊은 재귀의 경우, 반복으로 변환하세요. 메모이제이션(결과 캐싱)은 Fibonacci 같은 재귀 솔루션의 속도를 높일 수 있습니다.
// Factorial
public static int factorial(int n) {
if (n <= 1) return 1; // base case
return n * factorial(n - 1); // recursive case
}
// factorial(5) = 5 * 4 * 3 * 2 * 1 = 120
// Fibonacci
public static int fib(int n) {
if (n < 2) return n;
return fib(n - 1) + fib(n - 2);
}
// Tail recursion (Java doesn't optimize this)
public static int factorialTail(int n, int acc) {
if (n <= 1) return acc;
return factorialTail(n - 1, n * acc);
}
// Call: factorialTail(5, 1)
// Be careful: deep recursion causes StackOverflowError
// For deep recursion, use iteration or a loop insteadLambda 표현식
Lambda(Java 8+)는 익명 함수입니다. 타입은 함수형 인터페이스(하나의 추상 메서드)입니다. 일반적인 것: Function<T,R>(입력→출력), Predicate<T>(불리언 검사), Consumer<T>(소비, 반환 없음), Supplier<T>(생산, 입력 없음). 메서드 참조(String::length)는 단일 메서드를 호출하는 lambda의 약식입니다.
import java.util.function.*;
// Lambda syntax: (params) -> expression
Function<Integer, Integer> square = x -> x * x;
Function<String, Integer> length = s -> s.length();
BiFunction<Integer, Integer, Integer> add = (a, b) -> a + b;
// With type annotations
BinaryOperator<Integer> multiply = (Integer a, Integer b) -> a * b;
// Multi-line lambda
Function<String, String> process = s -> {
String upper = s.toUpperCase();
return upper.substring(0, 3);
};
// Predicate (boolean test)
Predicate<Integer> isEven = n -> n % 2 == 0;
Predicate<String> isEmpty = String::isEmpty; // method reference
// Consumer (no return)
Consumer<String> printer = s -> System.out.println(s);
Consumer<String> printer2 = System.out::println; // method reference
// Supplier (no input, produces value)
Supplier<Double> random = () -> Math.random();
// Usage
int result = square.apply(5); // 25
boolean even = isEven.test(4); // true
printer.accept("Hello"); // prints "Hello"클래스 & OOP
클래스 & 생성자
클래스는 객체의 템플릿입니다. 필드는 상태를, 메서드는 동작을 정의합니다. 생성자는 새 객체를 초기화합니다(필드와 매개변수를 구별하려면 'this' 사용). @Override는 메서드가 슈퍼클래스 메서드를 재정의함을 나타냅니다(toString은 Object에서). 캡슐화: private 필드, public getter/setter.
public class Person {
// Fields (instance variables)
private String name;
private int age;
// Constructor
public Person(String name, int age) {
this.name = name; // 'this' refers to the current instance
this.age = age;
}
// Methods
public String getName() { return name; }
public int getAge() { return age; }
public void setAge(int age) {
if (age >= 0) this.age = age;
}
public String greet() {
return "Hi, I'm " + name;
}
@Override
public String toString() {
return "Person{name='" + name + "', age=" + age + "}";
}
}
// Usage
Person p = new Person("Alice", 30);
System.out.println(p.getName()); // "Alice"
System.out.println(p); // uses toString()접근 수정자 & Static
접근 수정자: public(어디서나), private(클래스만), protected(클래스 + 서브클래스 + 패키지), default/package-private(같은 패키지). 정적 멤버는 인스턴스가 아닌 클래스에 속합니다 — 모든 객체에 공유. 정적 초기화자는 클래스 로드 시 한 번 실행됩니다. 상수(static final), 유틸리티 메서드, 카운터에 static을 사용하세요.
public class BankAccount {
// Access modifiers:
public String owner; // accessible everywhere
private double balance; // class only
protected String type; // class + subclasses + same package
String id; // package-private (default)
// Static field (shared by all instances)
private static int accountCount = 0;
// Static constant
public static final double MIN_BALANCE = 100.0;
// Static method (call without instance)
public static int getAccountCount() {
return accountCount;
}
// Static initializer (runs once when class loads)
static {
System.out.println("BankAccount class loaded");
}
public BankAccount(String owner) {
this.owner = owner;
this.balance = MIN_BALANCE;
accountCount++; // increment shared counter
}
}
int count = BankAccount.getAccountCount(); // static method call상속 & super
Java는 클래스 상속에 'extends'를 사용합니다(단일 상속만). super()는 부모 생성자를 호출합니다(첫 줄이어야 함). @Override는 메서드 재정의를 나타냅니다(런타임 다형성). Dog IS-A Animal. 'is-a' 관계에는 상속을; 코드 재사용에는 컴포지션(has-a)을 사용하세요. Java 17+는 상속을 제한하는 sealed 클래스를 지원합니다.
// Parent class
class Animal {
protected String name;
public Animal(String name) {
this.name = name;
System.out.println("Animal constructor");
}
public void eat() {
System.out.println(name + " is eating");
}
}
// Child class (extends)
class Dog extends Animal {
private String breed;
public Dog(String name, String breed) {
super(name); // must be first line — call parent constructor
this.breed = breed;
}
// Override parent method
@Override
public void eat() {
super.eat(); // call parent's eat()
System.out.println(name + " the " + breed + " eats dog food");
}
public void bark() {
System.out.println("Woof!");
}
}
Dog dog = new Dog("Rex", "Labrador");
dog.eat(); // calls Dog's eat()
dog.bark(); // Dog-specific method추상 클래스 & 인터페이스
추상 클래스는 인스턴스화할 수 없고 추상(본문 없음)과 구체 메서드를 모두 가질 수 있습니다. 인터페이스는 계약을 정의합니다 — 모든 메서드는 기본이 public abstract. Java 8+는 인터페이스의 default 메서드(본문 포함)와 static 메서드를 허용합니다. 클래스는 하나의 추상 클래스를 확장하지만 여러 인터페이스를 구현할 수 있습니다. 공유 코드에는 추상 클래스를, 계약에는 인터페이스를 사용하세요.
// Abstract class (can't be instantiated)
abstract class Shape {
protected String color;
public Shape(String color) {
this.color = color;
}
// Abstract method (must be implemented by subclasses)
public abstract double area();
// Concrete method (inherited)
public String describe() {
return color + " " + this.getClass().getSimpleName();
}
}
// Interface (pure contract, Java 8+ can have default methods)
interface Drawable {
void draw(); // abstract by default
// Default method (Java 8+)
default void drawTwice() {
draw();
draw();
}
// Static method in interface
static Drawable empty() {
return () -> System.out.println("nothing");
}
}
// A class can extend one class and implement multiple interfaces
class Circle extends Shape implements Drawable {
private double radius;
public Circle(String color, double radius) {
super(color);
this.radius = radius;
}
@Override
public double area() {
return Math.PI * radius * radius;
}
@Override
public void draw() {
System.out.println("Drawing " + describe());
}
}다형성 & 캐스팅
다형성: 부모 참조가 자식 객체를 가질 수 있습니다. 메서드 호출은 실제 객체의 구현으로 디스패치됩니다(런타임 다형성). ClassCastException을 피하려면 다운캐스팅 전에 instanceof를 사용하세요. Java 16+ 패턴 매칭(instanceof Circle c)은 검사와 캐스트를 결합합니다. 컬렉션에서 올바른 동작을 위해 equals()와 hashCode()를 함께 재정의하세요.
// Polymorphism: one interface, many forms
Shape s1 = new Circle("red", 5);
Shape s2 = new Square("blue", 3);
// Calls the overridden method (runtime dispatch)
System.out.println(s1.area()); // Circle's area
System.out.println(s2.area()); // Square's area
// instanceof check
if (s1 instanceof Circle) {
Circle c = (Circle) s1; // downcast
System.out.println("Radius: " + c.radius);
}
// Pattern matching (Java 16+)
if (s1 instanceof Circle c) {
System.out.println("Radius: " + c.radius); // c is already cast
}
// Upcasting (automatic)
Circle circle = new Circle("green", 2);
Shape shape = circle; // upcast (no explicit cast needed)
// Object class methods (all classes inherit from Object)
circle.equals(circle); // reference equality by default
circle.hashCode(); // hash code
circle.getClass(); // Class<Circle>
circle.toString(); // string representationRecord & Enum
Record(Java 16+)는 불변 데이터 클래스입니다 — 컴파일러가 생성자, getter, equals, hashCode, toString을 생성합니다. DTO와 값 객체에 사용하세요. Enum은 필드, 메서드, 생성자를 가질 수 있는 타입 안전 상수입니다. Enum은 Comparable을 구현하고 values()와 valueOf() 메서드를 가집니다. 둘 다 현대 Java에 필수적입니다.
// Record (Java 16+): concise data class
public record Point(int x, int y) {}
// Equivalent to a class with:
// - final fields x, y
// - constructor
// - getters x(), y()
// - equals, hashCode, toString
Point p = new Point(3, 4);
System.out.println(p.x()); // 3
System.out.println(p.y()); // 4
System.out.println(p); // Point[x=3, y=4]
// Compact constructor (validation)
public record Age(int value) {
public Age {
if (value < 0 || value > 150) {
throw new IllegalArgumentException("Invalid age");
}
}
}
// Enum (named constants)
public enum Direction {
UP, DOWN, LEFT, RIGHT;
public Direction opposite() {
return switch (this) {
case UP -> DOWN;
case DOWN -> UP;
case LEFT -> RIGHT;
case RIGHT -> LEFT;
};
}
}
Direction d = Direction.UP;
Direction opp = d.opposite(); // DOWN컬렉션 & 제네릭
List (ArrayList & LinkedList)
ArrayList는 배열로 백업됩니다(빠른 get/set, 중간 삽입/삭제 느림). LinkedList는 이중 연결 리스트로 백업됩니다(끝 삽입/삭제 빠름, 임의 접근 느림). List.of()는 불변 리스트를 만듭니다. 대부분의 경우 ArrayList를 사용하세요; 끝 작업이 빈번할 때만 LinkedList. 둘 다 List 인터페이스를 구현합니다.
import java.util.*;
// ArrayList (fast random access, slow insert/delete in middle)
List<String> list = new ArrayList<>();
list.add("Alice");
list.add("Bob");
list.add(0, "Carol"); // insert at index
list.set(1, "Dave"); // replace at index
String name = list.get(0); // "Carol"
list.remove(0); // remove by index
list.remove("Dave"); // remove by value
int size = list.size(); // 1
boolean has = list.contains("Bob");
// LinkedList (fast insert/delete at ends)
LinkedList<Integer> linked = new LinkedList<>();
linked.addFirst(1);
linked.addLast(2);
linked.removeFirst();
linked.peek(); // see first element
// Immutable list (Java 9+)
List<String> immutable = List.of("a", "b", "c");
// immutable.add("d"); // UnsupportedOperationException
// Iterate
for (String s : list) {
System.out.println(s);
}
list.forEach(System.out::println); // method referenceSet (HashSet & TreeSet)
Set은 고유 요소를 저장합니다. HashSet이 가장 빠르지만 순서가 없습니다. TreeSet은 요소를 정렬된 상태로 유지합니다(자연 순서나 Comparator). LinkedHashSet은 삽입 순서를 유지합니다. 집합 연산: addAll(합집합), retainAll(교집합), removeAll(차집합). HashSet의 커스텀 객체를 위해 equals()와 hashCode()를 재정의하세요.
import java.util.*;
// HashSet (fast, unordered)
Set<String> set = new HashSet<>();
set.add("apple");
set.add("banana");
set.add("apple"); // duplicate ignored
System.out.println(set.size()); // 2
System.out.println(set.contains("apple")); // true
set.remove("banana");
// TreeSet (sorted, slower)
Set<Integer> sorted = new TreeSet<>();
sorted.add(3);
sorted.add(1);
sorted.add(2);
System.out.println(sorted); // [1, 2, 3]
// LinkedHashSet (maintains insertion order)
Set<String> ordered = new LinkedHashSet<>();
ordered.add("c");
ordered.add("a");
ordered.add("b");
System.out.println(ordered); // [c, a, b]
// Set operations
Set<Integer> a = new HashSet<>(Set.of(1, 2, 3));
Set<Integer> b = new HashSet<>(Set.of(2, 3, 4));
a.addAll(b); // union: [1, 2, 3, 4]
a.retainAll(b); // intersection: [2, 3]
a.removeAll(b); // difference: [1]
// Immutable set
Set<String> immutable = Set.of("x", "y", "z");Map (HashMap & TreeMap)
Map은 키-값 쌍을 저장합니다. HashMap이 가장 빠릅니다(순서 없음). TreeMap은 키로 정렬합니다. LinkedHashMap은 삽입 순서를 유지합니다. getOrDefault는 null 검사를 피합니다. compute/merge는 값 업데이트에 강력합니다. 커스텀 키를 위해 equals()와 hashCode()를 재정의하세요. Map.of()는 불변 맵을 만듭니다(Java 9+).
import java.util.*;
// HashMap (fast, unordered)
Map<String, Integer> ages = new HashMap<>();
ages.put("Alice", 30);
ages.put("Bob", 25);
ages.put("Alice", 31); // overwrite
// Access
int age = ages.get("Alice"); // 31
int defaultAge = ages.getOrDefault("Eve", 0); // 0
// Check
boolean has = ages.containsKey("Alice");
boolean hasVal = ages.containsValue(25);
// Remove
ages.remove("Bob");
// Iterate
for (Map.Entry<String, Integer> entry : ages.entrySet()) {
System.out.println(entry.getKey() + ": " + entry.getValue());
}
ages.forEach((key, val) -> System.out.println(key + "=" + val));
// Compute (Java 8+)
ages.compute("Alice", (k, v) -> v + 1); // increment
ages.putIfAbsent("Carol", 28);
ages.merge("Alice", 1, Integer::sum); // add 1
// TreeMap (sorted by keys)
Map<String, Integer> sorted = new TreeMap<>();
// LinkedHashMap (maintains insertion order)
Map<String, Integer> ordered = new LinkedHashMap<>();Queue & Deque
Queue는 FIFO(선입선출)입니다. Deque는 양방향입니다(양 끝에서 추가/제거 가능). PriorityQueue는 자연 순서나 Comparator로 요소를 정렬합니다(기본 최소 힙). 스택에는 레거시 Stack 클래스 대신 ArrayDeque(push/pop)를 사용하세요. ArrayDeque는 queue/deque 작업에서 LinkedList보다 빠릅니다.
import java.util.*;
// Queue (FIFO)
Queue<String> queue = new LinkedList<>();
queue.add("first"); // throws if full (capacity-restricted)
queue.offer("second"); // returns false if full
String head = queue.peek(); // see head (null if empty)
String removed = queue.poll(); // remove and return head
// Deque (double-ended)
Deque<Integer> deque = new ArrayDeque<>();
deque.addFirst(1);
deque.addLast(2);
deque.peekFirst(); // 1
deque.peekLast(); // 2
deque.pollFirst(); // 1
deque.pollLast(); // 2
// PriorityQueue (min-heap by default)
PriorityQueue<Integer> pq = new PriorityQueue<>();
pq.add(3);
pq.add(1);
pq.add(2);
System.out.println(pq.poll()); // 1 (smallest first)
// Max-heap (reverse order)
PriorityQueue<Integer> maxHeap = new PriorityQueue<>(Collections.reverseOrder());
maxHeap.add(1);
maxHeap.add(3);
System.out.println(maxHeap.poll()); // 3 (largest first)
// Stack (legacy, prefer Deque)
Deque<Integer> stack = new ArrayDeque<>();
stack.push(1); // add to front
stack.push(2);
stack.pop(); // 2 (remove from front)제네릭
제네릭은 타입 안전 컬렉션과 클래스를 가능하게 합니다. <T>는 타입 매개변수입니다. 제한 타입(<T extends Comparable<T>>)은 특정 동작을 가진 타입으로 제한합니다. 와일드카드: ? (모든), ? extends T (공변, 읽기 전용), ? super T (반공변, 쓰기 전용). 제네릭은 타입 소거를 사용합니다 — 타입은 컴파일 타임에 검사되고 런타임에 지워집니다.
// Generic class
public class Box<T> {
private T value;
public void set(T value) { this.value = value; }
public T get() { return value; }
}
Box<String> stringBox = new Box<>();
stringBox.set("hello");
String s = stringBox.get();
Box<Integer> intBox = new Box<>();
intBox.set(42);
// Generic method
public static <T> T firstOf(List<T> list) {
return list.get(0);
}
String first = firstOf(List.of("a", "b"));
// Bounded type parameter
public static <T extends Comparable<T>> T max(List<T> list) {
T result = list.get(0);
for (T item : list) {
if (item.compareTo(result) > 0) {
result = item;
}
}
return result;
}
// Wildcards
void process(List<?> list) { } // any type
void processNums(List<? extends Number> list) { } // Number or subclass
void addNums(List<? super Integer> list) { } // Integer or superclassIterator & Comparable
Iterator는 순회 중 안전한 제거를 허용합니다(it.remove()). ListIterator는 양방향 순회와 set/add를 추가합니다. Comparable은 자연 순서를 정의합니다(compareTo). Comparator는 커스텀 순서를 정의합니다(comparing, comparingInt, reversed, thenComparing). 유창한 정렬을 위해 Comparator.comparing()을 사용하세요. Collections.sort()는 자연 순서를 사용합니다.
import java.util.*;
// Iterator
List<String> list = List.of("a", "b", "c");
Iterator<String> it = list.iterator();
while (it.hasNext()) {
String s = it.next();
System.out.println(s);
// it.remove(); // safe removal during iteration
}
// ListIterator (bidirectional)
ListIterator<String> lit = list.listIterator();
while (lit.hasNext()) {
lit.set(lit.next().toUpperCase()); // replace
}
// Comparable (natural ordering)
public class Person implements Comparable<Person> {
String name;
int age;
@Override
public int compareTo(Person other) {
return Integer.compare(this.age, other.age);
}
}
// Comparator (custom ordering)
Comparator<Person> byName = Comparator.comparing(p -> p.name);
Comparator<Person> byAgeDesc = Comparator.comparingInt((Person p) -> p.age).reversed();
List<Person> people = new ArrayList<>();
people.sort(byName);
people.sort(byAgeDesc);
Collections.sort(people); // uses ComparableStream & 함수형
Stream 기본
Stream(Java 8+)은 선언적 데이터 처리를 제공합니다. .stream()(컬렉션) 또는 Stream.of()로 생성. 중간 연산(filter, map, sorted)은 지연입니다 — 종단 연산(collect, reduce, count, forEach)이 호출될 때만 실행. toList()(Java 16+)는 collect(Collectors.toList())의 간결한 대안입니다.
import java.util.*;
import java.util.stream.*;
List<Integer> nums = List.of(1, 2, 3, 4, 5, 6);
// Filter and collect
List<Integer> evens = nums.stream()
.filter(n -> n % 2 == 0)
.collect(Collectors.toList()); // [2, 4, 6]
// Map (transform)
List<String> doubled = nums.stream()
.map(n -> "num" + n)
.collect(Collectors.toList());
// Reduce
int sum = nums.stream().reduce(0, Integer::sum); // 21
int product = nums.stream().reduce(1, (a, b) -> a * b);
// Count
long count = nums.stream().filter(n -> n > 3).count(); // 3
// Find
Optional<Integer> first = nums.stream().filter(n -> n > 3).findFirst();
boolean anyMatch = nums.stream().anyMatch(n -> n > 5);
boolean allMatch = nums.stream().allMatch(n -> n > 0);
// ForEach
nums.stream().forEach(System.out::println);
// toList() shortcut (Java 16+)
List<Integer> result = nums.stream().filter(n -> n > 3).toList();Stream 연산
sorted()는 요소를 정렬합니다(자연 순서나 Comparator). distinct()는 중복을 제거합니다. limit(n)/skip(n)은 페이지 매김. flatMap은 중첩된 스트림을 평탄화합니다 — 일대다 변환에 필수. peek()는 디버깅용(부작용). groupingBy는 분류자로 요소를 그룹화하여 맵을 만듭니다. Stream은 지연입니다 — 연산이 효율적으로 체인됩니다.
List<String> names = List.of("Alice", "Bob", "Charlie", "David");
// Sorted
List<String> sorted = names.stream()
.sorted()
.toList();
// Sorted by length
List<String> byLength = names.stream()
.sorted(Comparator.comparing(String::length))
.toList();
// Distinct
List<Integer> distinct = List.of(1, 2, 2, 3, 3, 3).stream()
.distinct()
.toList(); // [1, 2, 3]
// Limit and Skip
List<Integer> limited = nums.stream()
.skip(2) // skip first 2
.limit(3) // take next 3
.toList();
// FlatMap (flatten nested structures)
List<List<Integer>> nested = List.of(List.of(1, 2), List.of(3, 4));
List<Integer> flat = nested.stream()
.flatMap(List::stream)
.toList(); // [1, 2, 3, 4]
// Peek (debug, side-effect)
nums.stream()
.peek(n -> System.out.println("before: " + n))
.filter(n -> n > 2)
.peek(n -> System.out.println("after: " + n))
.toList();
// Grouping
Map<Integer, List<String>> byLength = names.stream()
.collect(Collectors.groupingBy(String::length));Collector & 축소
Collector는 풍부한 축소 연산을 제공합니다: joining(문자열 연결), groupingBy(키로 그룹화), partitioningBy(불리언으로 분할), toMap(맵 생성), summarizingInt(통계: 카운트, 합, 최소, 최대, 평균). Collector는 조합할 수 있습니다(다운스트림 collector와 groupingBy). 이들은 장황한 루프를 선언적 한 줄로 대체합니다.
import java.util.stream.*;
List<Person> people = List.of(
new Person("Alice", 30),
new Person("Bob", 25),
new Person("Charlie", 35)
);
// Join strings
String joined = people.stream()
.map(Person::getName)
.collect(Collectors.joining(", ")); // "Alice, Bob, Charlie"
// Group by
Map<Integer, List<Person>> byAge = people.stream()
.collect(Collectors.groupingBy(Person::getAge));
// Partition (boolean)
Map<Boolean, List<Person>> partition = people.stream()
.collect(Collectors.partitioningBy(p -> p.getAge() > 28));
// Count by group
Map<Integer, Long> countByAge = people.stream()
.collect(Collectors.groupingBy(Person::getAge, Collectors.counting()));
// Summarizing
IntSummaryStatistics stats = people.stream()
.collect(Collectors.summarizingInt(Person::getAge));
System.out.println(stats.getAverage()); // 30.0
System.out.println(stats.getMax()); // 35
// To map
Map<String, Integer> nameToAge = people.stream()
.collect(Collectors.toMap(Person::getName, Person::getAge));
// Reducing
int totalAge = people.stream()
.collect(Collectors.reducing(0, Person::getAge, Integer::sum));Optional
Optional<T>는 값을 포함하거나 포함하지 않을 수 있는 컨테이너입니다. 부재의 명시적 처리를 강제합니다 — 더 이상 NullPointerException 없음. 비-null 값에는 of()를, null 가능에는 ofNullable()을 사용하세요. map/flatMap/filter로 체인하세요. isPresent() 없이 get()을 사용하지 마세요 — orElse/orElseThrow를 선호하세요. Optional은 필드가 아닌 반환 타입용으로 설계되었습니다.
import java.util.Optional;
// Creating Optional
Optional<String> present = Optional.of("hello");
Optional<String> empty = Optional.empty();
Optional<String> nullable = Optional.ofNullable(null); // empty if null
// Checking
present.isPresent(); // true
empty.isEmpty(); // true (Java 11+)
// Getting values
String val = present.get(); // throws if empty (avoid!)
String safe = present.orElse("default");
String computed = present.orElseGet(() -> computeDefault());
String orThrow = present.orElseThrow(() -> new RuntimeException("missing"));
// Transform (map/flatMap)
Optional<Integer> length = present.map(String::length); // Optional[5]
Optional<String> upper = present.map(s -> s.toUpperCase());
// Filter
Optional<String> filtered = present.filter(s -> s.length() > 3);
// ifPresent
present.ifPresent(s -> System.out.println(s));
present.ifPresentOrElse(
s -> System.out.println("Got: " + s),
() -> System.out.println("Empty")
);
// Chaining (avoid null checks)
String result = getUser(1)
.map(User::getProfile)
.map(Profile::getEmail)
.orElse("no email");함수형 인터페이스
함수형 인터페이스는 정확히 하나의 추상 메서드를 가집니다(여러 default 메서드 가능). @FunctionalInterface는 선택적이지만 의도를 문서화합니다. Java는 java.util.function에 많은 것을 제공합니다: Function, Predicate, Consumer, Supplier, plus Bi-와 원시 변형. 가능하면 커스텀 인터페이스를 만드는 대신 이것들을 사용하세요. lambda 표현식과 메서드 참조를 가능하게 합니다.
import java.util.function.*;
// Built-in functional interfaces
Function<String, Integer> strToInt = Integer::parseInt;
BiFunction<String, String, String> concat = String::concat;
Predicate<String> isEmpty = String::isEmpty;
BiPredicate<String, String> contains = String::contains;
Consumer<String> printer = System.out::println;
BiConsumer<String, Integer> printPair = (s, i) -> System.out.println(s + ":" + i);
Supplier<List<String>> listFactory = ArrayList::new;
// Primitive specializations
IntFunction<String> intToStr = String::valueOf;
ToIntFunction<String> length = String::length;
IntPredicate isPositive = n -> n > 0;
IntConsumer intPrinter = System.out::println;
IntSupplier randomInt = () -> (int)(Math.random() * 100);
// Binary operators
BinaryOperator<Integer> max = Integer::max;
IntBinaryOperator sum = Integer::sum;
// Unary operators
UnaryOperator<String> trim = String::trim;
IntUnaryOperator negate = n -> -n;
// Custom functional interface
@FunctionalInterface
interface StringProcessor {
String process(String input);
// Can have default methods
default StringProcessor andThen(StringProcessor after) {
return input -> after.process(process(input));
}
}예외 & I/O
Try / Catch / Finally
try/catch/finally는 예외를 처리합니다. finally는 항상 실행됩니다(정리용). multi-catch(catch A | B)는 여러 예외를 함께 처리합니다. try-with-resources는 AutoCloseable(파일, 연결, 스트림)을 자동 닫습니다 — 수동 finally 정리보다 선호. 리소스는 선언의 역순으로 닫힙니다.
try {
int result = 10 / 0;
} catch (ArithmeticException e) {
System.out.println("Math error: " + e.getMessage());
} catch (Exception e) {
System.out.println("General error: " + e);
} finally {
// Always runs (even if return/throw in try/catch)
System.out.println("Cleanup");
}
// Multi-catch (Java 7+)
try {
riskyOperation();
} catch (IOException | SQLException e) {
// Handle both exceptions the same way
log.error(e);
}
// Try-with-resources (auto-close, Java 7+)
try (BufferedReader br = new BufferedReader(new FileReader("file.txt"));
PrintWriter pw = new PrintWriter("output.txt")) {
String line = br.readLine();
pw.println(line);
} catch (IOException e) {
e.printStackTrace();
}
// br and pw are auto-closed (in reverse order)Checked vs Unchecked
Checked 예외(Exception 확장)는 'throws'로 선언하거나 잡아야 합니다 — 컴파일러가 강제. 복구 가능한 조건(파일 없음, 네트워크 에러)에 사용. Unchecked 예외(RuntimeException 확장)는 선언이 필요 없습니다 — 프로그래밍 에러(null 포인터, 잘못된 인수)에 사용. 논쟁: checked 예외는 처리를 강제하지만 코드를 어지럽힐 수 있습니다; 많은 프레임워크가 unchecked를 선호합니다.
// Checked exceptions (must be declared or caught)
public void readFile(String path) throws IOException {
BufferedReader br = new BufferedReader(new FileReader(path));
// IOException is checked — compiler enforces handling
}
// Unchecked exceptions (RuntimeException, no need to declare)
public int divide(int a, int b) {
if (b == 0) {
throw new IllegalArgumentException("Divisor cannot be zero");
// RuntimeException — no 'throws' needed
}
return a / b;
}
// Common checked exceptions:
// IOException, SQLException, ClassNotFoundException
// Common unchecked exceptions:
// NullPointerException, IllegalArgumentException,
// IndexOutOfBoundsException, ArithmeticException,
// ClassCastException, IllegalStateException
// Custom checked exception
class DataException extends Exception {
public DataException(String msg) { super(msg); }
}
// Custom unchecked exception
class ValidationException extends RuntimeException {
public ValidationException(String msg) { super(msg); }
}파일 I/O (NIO.2)
NIO.2(java.nio.file)는 현대 파일 API입니다. Files.readString/writeString(Java 11+)는 텍스트에 편리합니다. Files.lines()는 지연 Stream을 반환합니다 — 대용량 파일에 효율적(try-with-resources로 닫아야 함). Path.of()는 구식 File 클래스를 대체합니다. Files.createDirectories()는 전체 경로를 만듭니다. 항상 IOException을 처리하세요.
import java.nio.file.*;
import java.io.*;
// Read entire file (small files)
List<String> lines = Files.readAllLines(Path.of("input.txt"));
String content = Files.readString(Path.of("config.json")); // Java 11+
byte[] bytes = Files.readAllBytes(Path.of("image.png"));
// Write file
Files.writeString(Path.of("output.txt"), "Hello, World!");
Files.write(Path.of("data.bin"), bytes);
// Append
Files.writeString(Path.of("log.txt"), "entry\n",
StandardOpenOption.APPEND, StandardOpenOption.CREATE);
// Stream lines (large files, lazy)
try (Stream<String> lineStream = Files.lines(Path.of("large.txt"))) {
lineStream.filter(l -> l.contains("ERROR"))
.forEach(System.out::println);
}
// Copy, move, delete
Files.copy(Path.of("src.txt"), Path.of("dest.txt"));
Files.move(Path.of("old.txt"), Path.of("new.txt"));
Files.delete(Path.of("temp.txt"));
// Check existence
boolean exists = Files.exists(Path.of("file.txt"));
// Create directories
Files.createDirectories(Path.of("a/b/c"));Reader & Writer (텍스트)
BufferedReader/Writer는 텍스트 I/O에 효율적입니다(버퍼링은 시스템 호출 감소). PrintWriter는 printf 스타일 형식화를 제공합니다. Scanner는 입력을 파싱합니다(nextInt, nextDouble, nextLine). InputStreamReader는 바이트 스트림을 문자 스트림으로 연결합니다(비-UTF-8을 위해 charset 지정). 스트림이 닫히도록 항상 try-with-resources를 사용하세요.
import java.io.*;
import java.nio.file.*;
// BufferedReader (efficient text reading)
try (BufferedReader br = Files.newBufferedReader(Path.of("input.txt"))) {
String line;
while ((line = br.readLine()) != null) {
System.out.println(line);
}
}
// BufferedWriter (efficient text writing)
try (BufferedWriter bw = Files.newBufferedWriter(Path.of("output.txt"))) {
bw.write("First line");
bw.newLine();
bw.write("Second line");
}
// PrintWriter (convenient formatting)
try (PrintWriter pw = new PrintWriter("output.txt")) {
pw.println("Hello");
pw.printf("Name: %s, Age: %d%n", "Alice", 30);
}
// Scanner (parsing input)
try (Scanner sc = new Scanner(Path.of("data.txt"))) {
while (sc.hasNextLine()) {
String line = sc.nextLine();
// Parse tokens
Scanner lineSc = new Scanner(line);
if (lineSc.hasNextInt()) {
int n = lineSc.nextInt();
}
}
}
// InputStreamReader (bytes to chars, e.g., from InputStream)
Reader reader = new InputStreamReader(System.in);날짜 & 시간 (java.time)
java.time(Java 8+)는 구식 Date/Calendar를 대체하는 현대 날짜/시간 API입니다. LocalDate(날짜만), LocalTime(시간만), LocalDateTime(둘 다), ZonedDateTime(시간대 포함). 모두 불변이고 스레드 안전. 날짜 차이에는 Period, 시간 차이에는 Duration을 사용하세요. 파싱/형식화에는 DateTimeFormatter. 기계 타임스탬프에는 Instant(UTC).
import java.time.*;
import java.time.format.*;
import java.time.temporal.*;
// Current date/time
LocalDate today = LocalDate.now(); // 2024-01-15
LocalTime now = LocalTime.now(); // 14:30:45.123
LocalDateTime dt = LocalDateTime.now(); // both
ZonedDateTime zdt = ZonedDateTime.now(); // with timezone
// Create specific
LocalDate date = LocalDate.of(2024, 1, 15);
LocalTime time = LocalTime.of(14, 30, 0);
LocalDateTime specific = LocalDateTime.of(2024, 1, 15, 14, 30);
// Parsing and formatting
LocalDate parsed = LocalDate.parse("2024-01-15");
String formatted = date.format(DateTimeFormatter.ofPattern("dd/MM/yyyy"));
// "15/01/2024"
// Manipulation (immutable, returns new)
LocalDate tomorrow = today.plusDays(1);
LocalDate lastMonth = today.minusMonths(1);
LocalDate nextYear = today.plusYears(1);
// Period (date-based)
Period age = Period.between(LocalDate.of(1990, 1, 1), today);
System.out.println(age.getYears()); // 34
// Duration (time-based)
Duration dur = Duration.between(time, LocalTime.now());
System.out.println(dur.toMinutes());
// Instant (machine time, UTC)
Instant instant = Instant.now();
Instant epoch = Instant.ofEpochSecond(0);동시성 기본
Java 동시성: Thread(저수준), ExecutorService(스레드 풀 — 선호), CompletableFuture(비동기 컴포지션, Promise처럼). parallelStream()은 병렬 처리를 위해 ForkJoinPool을 사용합니다. synchronized 블록은 공유 상태를 보호합니다. Atomic 변수(AtomicInteger 등)는 잠금 없는 스레드 안전 연산을 제공합니다. 복잡한 동시성을 위해 java.util.concurrent 컬렉션(ConcurrentHashMap, BlockingQueue)을 사용하세요.
import java.util.concurrent.*;
// Create a thread
Thread thread = new Thread(() -> {
System.out.println("Running in: " + Thread.currentThread().getName());
});
thread.start();
thread.join(); // wait for completion
// ExecutorService (thread pool)
ExecutorService executor = Executors.newFixedThreadPool(4);
Future<Integer> future = executor.submit(() -> {
Thread.sleep(1000);
return 42;
});
Integer result = future.get(); // blocks until done
executor.shutdown();
// CompletableFuture (async, Java 8+)
CompletableFuture<String> cf = CompletableFuture
.supplyAsync(() -> "Hello")
.thenApply(s -> s + " World")
.thenApply(String::toUpperCase);
String asyncResult = cf.join(); // "HELLO WORLD"
// Parallel stream
List<Integer> nums = List.of(1, 2, 3, 4, 5);
int sum = nums.parallelStream().mapToInt(Integer::intValue).sum();
// Synchronized
synchronized (this) {
// only one thread at a time
}
// Atomic variables
AtomicInteger counter = new AtomicInteger(0);
counter.incrementAndGet();
counter.compareAndSet(0, 1);Lambda 표현식
Lambda 구문 기본
Lambda(Java 8+)는 함수형 인터페이스의 간결한 구현입니다. 구문: (params) -> expression 또는 (params) -> { statements; }. 컴파일러는 대상 타입에서 매개변수 타입을 추론합니다. 단일 매개변수 lambda는 괄호를 생략할 수 있고; 매개변수 없는 lambda는 빈 괄호가 필요합니다. Lambda는 함수형 프로그래밍을 가능하게 하고 Streams API의 중추입니다.
// Anonymous class (verbose, pre-Java 8)
Runnable r1 = new Runnable() {
public void run() {
System.out.println("Old way");
}
};
// Lambda expression (Java 8+)
Runnable r2 = () -> System.out.println("Lambda");
// With parameters and body
Comparator<Integer> cmp = (a, b) -> {
int diff = a - b;
return diff;
};
// Type inference (omit types)
Comparator<Integer> cmp2 = (a, b) -> a - b;
// Single param, no parentheses needed
Consumer<String> printer = s -> System.out.println(s);
// Zero params need empty parens
Runnable noop = () -> {};함수형 인터페이스
함수형 인터페이스는 정확히 하나의 추상 메서드(SAM 타입)를 가집니다. @FunctionalInterface 어노테이션은 컴파일러가 이를 강제하게 만듭니다. Lambda는 함수형 인터페이스만 대상으로 할 수 있습니다. default와 static 메서드는 허용되며 단일 메서드 규칙을 깨지 않습니다. 이것이 Java 타입 시스템에서 lambda가 작동하게 하는 기반입니다.
// Functional interface: exactly one abstract method
@FunctionalInterface
interface MathOperation {
int operate(int a, int b);
}
// Implement with lambda
MathOperation add = (a, b) -> a + b;
MathOperation mul = (a, b) -> a * b;
MathOperation max = (a, b) -> Math.max(a, b);
int result = add.operate(3, 4); // 7
int m = mul.operate(3, 4); // 12
// @FunctionalInterface is optional but recommended
// It prevents accidentally adding a second abstract method
// Default and static methods don't count toward the limit
interface StringProcessor {
String process(String s);
default StringProcessor andThen(StringProcessor next) {
return s -> next.process(this.process(s));
}
}내장 함수형 인터페이스
java.util.function은 약 40개의 기성 함수형 인터페이스를 제공하여 직접 작성할 일이 거의 없습니다. 핵심 4개: Function(변환), Predicate(검사), Consumer(소비), Supplier(생산). Bi- 변형은 두 인수를 받습니다. 원시 변형(IntFunction, ToIntFunction 등)은 오토박싱 오버헤드를 피합니다. 커스텀 인터페이스를 만드는 대신 이것들을 사용하세요.
import java.util.function.*;
// Function<T,R>: input -> output
Function<String, Integer> len = String::length;
int n = len.apply("hello"); // 5
// Predicate<T>: input -> boolean (for filtering)
Predicate<String> isEmpty = String::isEmpty;
boolean e = isEmpty.test(""); // true
// Consumer<T>: input -> void (side effects)
Consumer<String> print = System.out::println;
print.accept("hi");
// Supplier<T>: no input -> output (factories, lazy)
Supplier<Double> random = Math::random;
double r = random.get();
// BiFunction<T,U,R>: two inputs -> output
BiFunction<String, Integer, String> repeat =
(s, i) -> s.repeat(i);
// Primitives variants avoid boxing
IntFunction<String> f = i -> "n=" + i;
IntPredicate positive = i -> i > 0;
ToIntFunction<String> length = String::length;
IntBinaryOperator sum = (a, b) -> a + b;메서드 참조
메서드 참조(::)는 단일 메서드를 호출하는 lambda의 약식입니다. 네 가지 종류: static(Class::static), 바운드 인스턴스(obj::method), 언바운드 인스턴스(Class::method — 첫 매개변수가 수신자), 생성자(Class::new). lambda가 하나의 메서드로만 전달할 때 사용하세요 — 더 읽기 쉽습니다. 그렇지 않으면 명시적 lambda를 사용하세요.
import java.util.*;
List<String> names = List.of("alice", "bob", "charlie");
// Lambda form
names.forEach(s -> System.out.println(s));
// Method reference (shorthand)
names.forEach(System.out::println);
// Four kinds of method references:
// 1. Static method: ClassName::staticMethod
names.stream().map(String::toUpperCase);
// 2. Instance method of particular object: instance::method
var printer = System.out;
names.forEach(printer::println);
// 3. Instance method of arbitrary object: ClassName::instanceMethod
List<String> upper = names.stream()
.map(String::toUpperCase)
.toList();
// 4. Constructor: ClassName::new
Supplier<ArrayList<String>> factory = ArrayList::new;
ArrayList<String> list = factory.get();변수 캡처 (사실상 final)
Lambda는 지역 변수를 캡처할 수 있지만, final이거나 '사실상 final'(재할당 안 됨)이어야 합니다. 이는 lambda가 스택 프레임보다 오래 살 수 있기 때문입니다. 우회하려면, 단일 요소 배열이나 AtomicInteger/holder 객체를 사용하세요. 인스턴스와 정적 필드는 그런 제한이 없습니다. lambda 내부의 'this'는 lambda 자신이 아닌 둘러싸는 클래스 인스턴스를 참조합니다.
import java.util.function.*;
int x = 10;
// Capturing a local variable — must be final or effectively final
Supplier<Integer> getter = () -> x * 2;
System.out.println(getter.get()); // 20
// x = 20; // ERROR: would break the lambda capture
// Local variables captured by lambdas must be final/effectively final
// Workaround: use an array or wrapper (mutable container)
int[] counter = {0};
Runnable inc = () -> counter[0]++;
inc.run();
inc.run();
System.out.println(counter[0]); // 2
// Instance/static fields CAN be modified (no restriction)
class Holder {
int value = 0;
Runnable bump = () -> value++; // OK, field access
}
// 'this' inside a lambda refers to the enclosing instance
class Outer {
String name = "Outer";
Runnable r = () -> System.out.println(this.name); // "Outer"
}생성자 참조
생성자 참조(ClassName::new)는 새 인스턴스를 간결하게 만듭니다. 결과 타입을 선택하기 위해 Collectors.toCollection()과 함께, 배열 생성(Type[]::new)과 함께, 팩토리 패턴에서 유용합니다. record와 불변 객체의 경우, 생성자 참조가 사본을 만드는 관용적 방법입니다. Function/Supplier 대상과 자연스럽게 짝을 이룹니다.
import java.util.*;
import java.util.function.*;
import java.util.stream.*;
// Supplier constructor reference
Supplier<StringBuilder> sbFactory = StringBuilder::new;
StringBuilder sb = sbFactory.get();
// Function constructor reference (with one arg)
Function<String, StringBuilder> sbFromString = StringBuilder::new;
StringBuilder named = sbFromString.apply("Hello");
// In streams: collect into a specific collection
List<String> names = List.of("a", "b", "c");
ArrayList<String> copy = names.stream()
.collect(Collectors.toCollection(ArrayList::new));
// Array constructor reference
IntFunction<String[]> arrayFactory = String[]::new;
String[] arr = arrayFactory.apply(5); // new String[5]
// Copying via constructor
record Point(int x, int y) {}
Function<Point, Point> copyCtor = Point::new;
Point p = copyCtor.apply(new Point(1, 2));Optional & Null 안전
Optional 생성
Optional은 값을 가질 수도 있고 가지지 않을 수도 있는 컨테이너입니다. 값이 없으면 empty(), 값이 확실히 non-null이면 of()(그렇지 않으면 NPE), null이 가능하면 ofNullable()을 사용하세요. Optional은 호출자가 부재 케이스를 명시적으로 처리하게 강제합니다. Optional이 예상되는 곳에 null을 반환하지 마세요 — 목적을 무너뜨립니다.
import java.util.Optional;
// empty() — no value
Optional<String> empty = Optional.empty();
// of() — value must be non-null (throws NPE if null)
Optional<String> present = Optional.of("hello");
// ofNullable() — accepts null safely
Optional<String> maybe = Optional.ofNullable(getName());
// From a stream that may produce 0 or 1 elements
Optional<Integer> first = List.of(1, 2, 3).stream().findFirst();
// Common helper pattern
public Optional<User> findUser(long id) {
User u = db.lookup(id);
return Optional.ofNullable(u);
}
String getName() { return Math.random() > 0.5 ? "Alice" : null; }
class User {}
class Db { User lookup(long id) { return null; } }
Db db = new Db();값 안전하게 소비
isPresent+get보다 ifPresent/ifPresentOrElse를 선호하세요. orElse는 상수 기본값을 반환하고; orElseGet은 Supplier를 받아 기본값을 지연 계산합니다(기본값이 비쌀 때 중요). orElseThrow는 부재를 예외로 변환합니다. 목표는 .get()을 맹목적으로 호출하지 않는 것입니다 — 그것이 Optional이 제거하려던 NPE 위험을 재도입합니다.
import java.util.Optional;
Optional<String> name = Optional.of("Alice");
// isPresent / isEmpty (Java 11+)
if (name.isPresent()) {
System.out.println(name.get()); // "Alice"
}
// Avoid .get() without checking — throws NoSuchElementException
// ifPresent: run action only if value exists
name.ifPresent(System.out::println);
// ifPresentOrElse (Java 9+)
name.ifPresentOrElse(
System.out::println,
() -> System.out.println("No name")
);
// orElse: provide default
String s1 = name.orElse("Anonymous");
// orElseGet: lazy default (computed only if needed)
String s2 = name.orElseGet(() -> expensiveDefault());
// orElseThrow: throw if absent
String s3 = name.orElseThrow(() -> new IllegalStateException("missing"));
String expensiveDefault() { return "computed"; }map & flatMap으로 변환
map은 포함된 값을 변환합니다(Optional<T> -> Optional<R>). flatMap은 매핑 함수 자체가 Optional을 반환할 때 사용되어 중첩 Optional을 방지합니다. filter는 술어가 일치할 때만 값을 유지합니다. map/filter/flatMap 체이닝으로 첫 번째 빈 값에서 단락하는 파이프라인을 구축할 수 있습니다 — 중첩 null 검사보다 훨씬 깔끔.
import java.util.Optional;
Optional<String> name = Optional.of("Alice");
// map: transform the value if present
Optional<Integer> length = name.map(String::length); // Optional[5]
Optional<String> upper = name.map(String::toUpperCase);
// flatMap: when the transform itself returns Optional
// (avoids Optional<Optional<T>>)
public Optional<String> findEmail(long id) {
return Optional.ofNullable(db.get(id));
}
Optional<String> email = Optional.of(1L)
.flatMap(this::findEmail); // Optional<email> not Optional<Optional<email>>
// filter: keep only if predicate matches
Optional<Integer> adultAge = Optional.of(25)
.filter(a -> a >= 18); // Optional[25]
Optional<Integer> kid = Optional.of(10)
.filter(a -> a >= 18); // Optional.empty
// Chain transformations
String label = Optional.of("alice")
.map(String::strip)
.filter(s -> !s.isEmpty())
.map(s -> s.substring(0, 1).toUpperCase() + s.substring(1))
.orElse("unknown"); // "Alice"
class Db { String get(long id) { return "[email protected]"; } }
Db db = new Db();
Optional<String> findEmail(long id) { return Optional.ofNullable(db.get(id)); }피해야 할 안티패턴
Optional은 필드나 매개변수가 아닌 반환 타입용으로 설계되었습니다. Serializable이 아니며 필드로 오버헤드를 추가합니다. 검사 없이 .get()을 사용하지 말고, isPresent()+get()도 사용하지 마세요 — 그것은 그냥 장황한 null 검사입니다. 컬렉션은 이미 빈 상태를 표현하므로 Optional로 감싸지 마세요. 값을 반환 타입 신호로 Optional을 사용하세요.
import java.util.Optional;
// BAD: using Optional for fields (not serializable, wastes memory)
class Bad {
private Optional<String> name; // DON'T
}
// GOOD: use plain field, return Optional from accessor
class Good {
private String name;
public Optional<String> getName() { return Optional.ofNullable(name); }
}
// BAD: Optional as method parameter (clutters API)
public void process(Optional<String> input) {} // DON'T
// GOOD: method overloading or nullable param
public void process(String input) {}
public void process() { process(null); }
// BAD: .get() without check
String x = findName().get(); // throws if empty
// BAD: .isPresent() + .get() — defeats the purpose
Optional<String> opt = findName();
if (opt.isPresent()) {
use(opt.get()); // just use ifPresent or map instead
}
// BAD: returning Optional from collections
public Optional<Item> find(...) {
// Collections already express emptiness — return empty List, not Optional<List>
return Optional.ofNullable(items);
}Stream과 Optional
Optional.stream()(Java 9+)는 0 또는 1 요소의 Stream을 반환하여, stream에서 Optional을 우아하게 flatMap할 수 있게 합니다. 이것이 stream 처리 중 부재 값을 건너뛰는 가장 깔끔한 방법입니다. 장황한 filter(isPresent).map(get) 패턴을 피하고 파이프라인을 선언적으로 유지합니다.
import java.util.*;
import java.util.stream.*;
// stream() on Optional: 0 or 1 element stream
Optional<String> opt = Optional.of("hi");
opt.stream().forEach(System.out::println);
// Useful: flatMap Optional out of a stream
class User {
String email; // may be null
User(String e) { email = e; }
Optional<String> getEmail() { return Optional.ofNullable(email); }
}
List<User> users = List.of(
new User("[email protected]"),
new User(null),
new User("[email protected]")
);
// Extract emails, skipping nulls — clean with Optional::stream
List<String> emails = users.stream()
.flatMap(u -> u.getEmail().stream())
.toList(); // [[email protected], [email protected]]
// Without Optional::stream you'd need filter+map
List<String> emails2 = users.stream()
.map(User::getEmail)
.filter(Optional::isPresent)
.map(Optional::get)
.toList();Stream API 심층
Stream 생성
Stream은 컬렉션, 배열, 또는 정적 팩토리에서 생성할 수 있습니다. iterate()와 generate()는 무한 스트림을 생성합니다 — 항상 limit()를 뒤에 붙이세요. 술어가 있는 iterate(Java 9+)는 순수 iterate보다 안전합니다. IntStream/LongStream/DoubleStream은 숫자 작업을 위해 박싱을 피합니다. Stream은 일회용입니다: 종단 연산이 실행되면 스트림이 소비됩니다.
import java.util.*;
import java.util.stream.*;
// From collections
Stream<String> s1 = List.of("a", "b").stream();
Stream<String> s2 = Set.of("x").stream();
// From arrays
int[] nums = {1, 2, 3};
IntStream s3 = Arrays.stream(nums);
Stream<String> s4 = Arrays.stream(new String[]{"a", "b"});
// Static factory methods
Stream<Integer> s5 = Stream.of(1, 2, 3);
Stream<Integer> s6 = Stream.empty();
Stream<Integer> s7 = Stream.iterate(1, n -> n * 2); // infinite
Stream<Integer> s8 = Stream.iterate(1, n -> n < 100, n -> n + 1); // bounded (Java 9+)
Stream<Double> s9 = Stream.generate(Math::random); // infinite
Stream<String> s10 = Stream.ofNullable(null); // 0 or 1 element (Java 9+)
// From functions (infinite, must limit)
List<Integer> powers = Stream.iterate(1, n -> n * 2)
.limit(10)
.toList();
// Numeric ranges
IntStream range = IntStream.range(0, 5); // 0,1,2,3,4
IntStream closed = IntStream.rangeClosed(1, 5); // 1,2,3,4,5중간 연산
중간 연산은 지연입니다 — 종단 연산이 호출될 때까지 실행되지 않습니다. filter는 요소를 유지하고, map은 1:1로 변환하고, flatMap은 1:다로 변환합니다. distinct/sorted/limit/skip은 상태 저장. takeWhile/dropWhile(Java 9+)은 첫 번째 비일치 요소에서 중지합니다(filter와 달리 모든 것을 스캔하지 않음). 디버깅용으로 peek을 사용하고, 프로덕션에서 부작용에는 사용하지 마세요.
import java.util.*;
import java.util.stream.*;
List<Integer> nums = List.of(3, 1, 4, 1, 5, 9, 2, 6, 5);
// filter: keep matching
List<Integer> evens = nums.stream()
.filter(n -> n % 2 == 0).toList();
// map: transform
List<String> labels = nums.stream()
.map(n -> "n=" + n).toList();
// flatMap: one-to-many
List<Integer> expanded = List.of(List.of(1, 2), List.of(3))
.stream().flatMap(List::stream).toList(); // [1,2,3]
// distinct: remove duplicates
List<Integer> uniq = nums.stream().distinct().toList();
// sorted
List<Integer> asc = nums.stream().sorted().toList();
List<Integer> desc = nums.stream().sorted(Comparator.reverseOrder()).toList();
// peek: inspect (mainly for debugging)
nums.stream().peek(n -> System.out.println("seen " + n)).count();
// limit / skip
List<Integer> first3 = nums.stream().limit(3).toList();
List<Integer> after2 = nums.stream().skip(2).toList();
// takeWhile / dropWhile (Java 9+)
List<Integer> lt5 = nums.stream().takeWhile(n -> n < 5).toList();Collector: grouping & partitioning
Collectors.groupingBy는 Java stream의 SQL GROUP BY입니다. 분류 함수가 키를 정의하고; 선택적 다운스트림 collector가 각 그룹을 처리합니다(counting, summing, mapping 등). partitioningBy는 불리언 술어가 있는 특수 케이스입니다(정확히 두 버킷). 정렬된 키를 위해 TreeMap supplier를 전달하세요. 이것들은 강력하게 조합됩니다 — 다중 수준 그룹화를 구축할 수 있습니다.
import java.util.*;
import java.util.stream.*;
record Person(String name, String city, int age) {}
List<Person> people = List.of(
new Person("Alice", "NYC", 30),
new Person("Bob", "LA", 25),
new Person("Carol", "NYC", 35),
new Person("Dave", "LA", 40)
);
// groupingBy: Map<key, List<item>>
Map<String, List<Person>> byCity = people.stream()
.collect(Collectors.groupingBy(Person::city));
// {NYC=[Alice,Carol], LA=[Bob,Dave]}
// groupingBy with downstream collector
Map<String, Long> countByCity = people.stream()
.collect(Collectors.groupingBy(Person::city, Collectors.counting()));
Map<String, Integer> sumAgeByCity = people.stream()
.collect(Collectors.groupingBy(Person::city,
Collectors.summingInt(Person::age)));
Map<String, List<String>> namesByCity = people.stream()
.collect(Collectors.groupingBy(Person::city,
Collectors.mapping(Person::name, Collectors.toList())));
// partitioningBy: Map<Boolean, List> (2 buckets)
Map<Boolean, List<Person>> byAge = people.stream()
.collect(Collectors.partitioningBy(p -> p.age() >= 30));
// groupingBy with TreeMap for sorted keys
Map<String, List<Person>> sorted = people.stream()
.collect(Collectors.groupingBy(Person::city, TreeMap::new, Collectors.toList()));축소 & 통계
reduce는 모든 요소를 단일 값으로 결합합니다 — 빈 스트림을 위해 identity를 제공하세요. summaryStatistics는 한 번에 카운트/합/최소/최대/평균을 제공합니다. Collectors.joining은 구분된 문자열을 빌드하는 데 편리합니다. teeing(Java 12+)은 두 collector를 병렬로 실행하고 결과를 병합합니다 — 두 집계(최소와 최대처럼)가 한 번에 필요할 때 유용.
import java.util.*;
import java.util.stream.*;
List<Integer> nums = List.of(1, 2, 3, 4, 5);
// reduce: combine all into one
int sum = nums.stream().reduce(0, Integer::sum); // 15
Optional<Integer> product = nums.stream().reduce((a, b) -> a * b); // Optional[120]
// Built-in summary collectors
IntSummaryStatistics stats = nums.stream()
.mapToInt(Integer::intValue)
.summaryStatistics();
// stats.getCount(), getSum(), getMin(), getMax(), getAverage()
// Common terminal collectors
long count = nums.stream().collect(Collectors.counting());
double avg = nums.stream().collect(Collectors.averagingInt(Integer::intValue));
int max = nums.stream().collect(Collectors.maxBy(Comparator.naturalOrder())).orElse(0);
// join strings
String joined = List.of("a", "b", "c").stream()
.collect(Collectors.joining(", ", "[", "]")); // "[a, b, c]"
// teeing (Java 12+): two collectors, merge results
record Range(int min, int max) {}
Range range = nums.stream().collect(Collectors.teeing(
Collectors.minBy(Comparator.naturalOrder()),
Collectors.maxBy(Comparator.naturalOrder()),
(mn, mx) -> new Range(mn.orElse(0), mx.orElse(0))
));숫자 스트림
IntStream/LongStream/DoubleStream은 오토박싱 오버헤드를 피하는 원시 특화입니다 — 숫자 작업에 사용하세요. mapToInt/mapToLong/mapToDouble은 객체 스트림을 원시 스트림으로 변환하고; boxed()는 반대로. 원시 스트림은 특수 종단 연산(sum, average, max)을 가지며 빈 스트림을 처리하기 위해 OptionalInt/Double을 반환합니다. 성능 민감 숫자 파이프라인에 훌륭.
import java.util.stream.*;
import java.util.*;
// IntStream / LongStream / DoubleStream avoid boxing
IntStream range = IntStream.rangeClosed(1, 100);
int sum = range.sum(); // 5050
double avg = IntStream.of(1, 2, 3).average().orElse(0);
// mapToInt / mapToLong / mapToDouble from object stream
int totalAge = people().stream().mapToInt(Person::age).sum();
// boxed: convert primitive stream back to object stream
List<Integer> list = IntStream.range(0, 5).boxed().toList();
// mapToObj: primitive -> objects
List<String> labels = IntStream.range(1, 4)
.mapToObj(i -> "item-" + i).toList();
// asLongStream / asDoubleStream
LongStream longs = IntStream.range(0, 5).asLongStream();
// Common numeric operations
int max = IntStream.of(3, 1, 4, 1, 5).max().orElse(Integer.MIN_VALUE);
boolean anyEven = IntStream.of(1, 3, 5).anyMatch(n -> n % 2 == 0);
// Iterate to build numeric sequences
List<Integer> fib = Stream.iterate(new int[]{0, 1}, a -> new int[]{a[1], a[0] + a[1]})
.limit(10).mapToInt(a -> a[0]).boxed().toList();
List<Person> people() { return List.of(new Person("a", "c", 30)); }
record Person(String name, String city, int age) {}병렬 스트림
parallelStream은 공용 ForkJoinPool(CPU 코어 수만큼)에 작업을 분할합니다. 큰 데이터셋에 CPU 집약적이고, 상태 없고, 순서 독립적인 연산에만 사용하세요 — 작은 데이터에는 오버헤드가 이익을 초과합니다. 공유 가변 상태를 피하세요(레이스 유발). 병렬 스트림의 I/O는 공유 풀을 차단합니다 — 차단 작업에 커스텀 ForkJoinPool을 사용하세요. 병렬이 더 빠르다고 가정하기 전에 측정하세요.
import java.util.*;
import java.util.stream.*;
// parallelStream: uses common ForkJoinPool
long sum = List.of(1, 2, 3, 4, 5).parallelStream()
.mapToInt(Integer::intValue).sum();
// Convert sequential to parallel
long count = IntStream.range(0, 1_000_000).parallel()
.filter(n -> n % 2 == 0).count();
// Order may differ — use forEachOrdered if order matters
List.of(1, 2, 3, 4).parallelStream()
.forEachOrdered(System.out::println);
// Collecting preserves encounter order (but work is parallel)
List<Integer> doubled = IntStream.range(0, 1000).parallel()
.map(n -> n * 2).boxed().toList();
// Custom thread pool (avoid blocking the common pool)
import java.util.concurrent.ForkJoinPool;
ForkJoinPool pool = new ForkJoinPool(8);
int result = pool.submit(() ->
IntStream.range(0, 1000).parallel().sum()
).get();
// WHEN to use parallel: large dataset, CPU-heavy per element,
// order-independent, stateless operations
// WHEN NOT: small data, I/O-bound, shared mutable state, ordered ops제네릭 심층
제네릭 클래스 & 메서드
제네릭은 타입 안전 재사용 가능 코드를 가능하게 합니다. 클래스는 타입 매개변수(<T>)를 선언하고; 메서드도 가능합니다(반환 타입 앞 <T>). 다이아몬드 연산자 <>는 생성 시 타입을 추론합니다. 제네릭은 컴파일 타임에 검사됩니다 — 런타임에 ClassCastException으로 잡는 대신 컴파일 타임에 타입 에러를 잡아 컬렉션과 API를 더 안전하게 만듭니다.
// Generic class
public class Box<T> {
private T value;
public void set(T v) { value = v; }
public T get() { return value; }
}
Box<String> strBox = new Box<>();
strBox.set("hello");
String s = strBox.get(); // no cast needed
// Multiple type parameters
public class Pair<K, V> {
private final K key;
private final V value;
public Pair(K k, V v) { key = k; value = v; }
public K key() { return key; }
public V value() { return value; }
}
Pair<String, Integer> p = new Pair<>("age", 30);
// Generic method (independent of class type params)
public static <T> T first(List<T> list) {
return list.get(0);
}
// Generic method with multiple type params
public static <K, V> Map<K, V> zip(List<K> keys, List<V> values) {
Map<K, V> m = new HashMap<>();
for (int i = 0; i < keys.size(); i++) m.put(keys.get(i), values.get(i));
return m;
}제한 타입 매개변수
제한 타입 매개변수(<T extends Bound>)는 사용할 수 있는 타입을 제한하고 바운드의 메서드를 호출할 수 있게 합니다. <T extends Number>는 T가 Number 또는 서브타입이어야 함을 의미합니다. 다중 바운드는 &를 사용 — 최대 하나의 클래스(첫 번째여야 함), 나머지는 인터페이스. 바운드는 특정 능력(비교 가능, 숫자 연산)이 필요한 알고리즘을 작성하는 데 필수적입니다.
// Upper bound: T must be a subtype of Number
public static <T extends Number> double sum(List<T> nums) {
double total = 0;
for (Number n : nums) total += n.doubleValue();
return total;
}
sum(List.of(1, 2, 3)); // Integer is a Number
sum(List.of(1.0, 2.5)); // Double is a Number
// sum(List.of("a")); // compile error
// Multiple bounds: T must extend all (first is class, rest interfaces)
interface Comparable<T> { int compareTo(T o); }
interface Serializable {}
public static <T extends Number & Comparable<T> & Serializable>
T max(List<T> list) {
T best = list.get(0);
for (T t : list) if (t.compareTo(best) > 0) best = t;
return best;
}
// Bound lets you call methods of the bound
public static <T extends CharSequence> int totalLength(List<T> items) {
int len = 0;
for (CharSequence c : items) len += c.length(); // can call .length()
return len;
}와일드카드: ?, extends, super
와일드카드는 제네릭 타입을 유연하게 만듭니다. ? extends T(공변)는 T를 읽을 수 있지만 쓸 수 없습니다 — 생산자용. ? super T(반공변)는 T를 쓸 수 있지만 Object만 읽을 수 있습니다 — 소비자용. PECS 규칙(Producer Extends, Consumer Super)이 무엇을 사용할지 안내합니다. copy(dest, src)가 고전적 예: dest는 소비자(super), src는 생산자(extends).
import java.util.*;
// ? (unbounded wildcard) — any type
void printAll(List<?> list) {
for (Object o : list) System.out.println(o);
}
// ? extends T (upper-bounded / covariant) — producer (PECS: Producer Extends)
double sum(List<? extends Number> nums) {
double total = 0;
for (Number n : nums) total += n.doubleValue();
return total;
}
sum(List.of(1, 2, 3)); // List<Integer> OK
sum(List.of(1.0, 2.0)); // List<Double> OK
// nums.add(5); // ERROR: can't add (don't know exact type)
// ? super T (lower-bounded / contravariant) — consumer (PECS: Consumer Super)
void addNumbers(List<? super Integer> list) {
list.add(1); list.add(2); list.add(3); // OK to add Integer
}
addNumbers(new ArrayList<Number>()); // OK
addNumbers(new ArrayList<Object>()); // OK
// Number n = list.get(0); // only safe to read as Object
// PECS rule: Producer Extends, Consumer Super
// If you read from a collection, use ? extends T
// If you write to a collection, use ? super T
public static <T> void copy(List<? super T> dest, List<? extends T> src) {
for (T t : src) dest.add(t);
}타입 소거
Java 제네릭은 타입 소거를 사용합니다 — 제네릭 타입은 컴파일 타임에만 존재하고; 런타임에 List<String>과 List<Integer>는 모두 그냥 List. 이는 하위 호환성을 가능하게 하지만 한계가 있습니다: new T(), 제네릭 배열 생성, 제네릭과 instanceof, 같이 소거된 시그니처를 가진 오버로드 메서드를 할 수 없습니다. 힙 오염은 unchecked 캐스트가 잘못된 타입을 제네릭에 넣을 때 발생하여, 에러를 런타임으로 지연시킵니다.
import java.util.*;
// At runtime, generic types are erased to their bounds (or Object)
// List<String>, List<Integer>, List<?> all become List at runtime
List<String> strings = new ArrayList<>();
List<Integer> ints = new ArrayList<>();
// Runtime: both are just ArrayList
// You CANNOT do these due to erasure:
// new T() — can't instantiate type param
// new T[] — can't create generic array
// instanceof List<String> — only instanceof List (raw)
// class MyException<T> extends Exception — can't extend Throwable generically
// static T field — no static generic fields
// Erasure means overloads clash:
// void process(List<String> list) {}
// void process(List<Integer> list) {} // ERROR: same erasure
// Checking types at runtime requires Class<T>
public static <T> List<T> filter(List<?> items, Class<T> type) {
List<T> result = new ArrayList<>();
for (Object o : items) {
if (type.isInstance(o)) result.add(type.cast(o));
}
return result;
}
// Heap pollution: when unchecked warnings lead to runtime ClassCastException
List<String> polluted = (List<String>)(List) List.of(1, 2); // unchecked
// String s = polluted.get(0); // ClassCastException at runtime제네릭 메서드 & 추론
타입 추론은 컴파일러가 컨텍스트(인수와 대상 타입)에서 타입 인수를 결정하게 하여, 거의 명시적으로 작성할 필요가 없습니다. 다이아몬드 연산자 <>는 생성자용 추론입니다. 대상 타이핑은 변수의 예상 타입을 사용합니다. 추론이 모호성을 해결할 수 없을 때만 명시적 타입 증인(Class.<T>method())을 사용하세요. 추론은 제네릭 코드를 비-제네릭 코드처럼 깔끔하게 읽히게 합니다.
import java.util.*;
// Type inference: compiler figures out T from arguments
public static <T> T pick(T a, T b) { return Math.random() > 0.5 ? a : b; }
String s = pick("hello", "world"); // T inferred as String
Number n = pick(1, 2.0); // T inferred as Number (common supertype)
// Target typing: inference uses the expected type
List<String> list = Collections.emptyList(); // T inferred from target
// Inference with method chains
List<Integer> nums = List.of(1, 2, 3);
String joined = nums.stream()
.map(Object::toString) // Stream<String>
.collect(Collectors.joining(",")); // inferred
// Explicit type witness (rarely needed)
Collections.<String>emptyList();
// Generic constructor
class Holder<T> {
private T value;
<U extends T> Holder(U init) { value = init; } // constructor type param
T get() { return value; }
}
Holder<Number> h = new Holder<>(42); // U=Integer, T=Number
import java.util.stream.Collectors;
class Math { static double random() { return 0.5; } }제네릭 인터페이스 & 패턴
제네릭 인터페이스(Repository<T,ID>처럼)는 재사용 가능한 계약을 정의합니다. 자기 참조 바운드 패턴(class X implements Comparable<X>)은 compareTo가 같은 타입만 받게 보장합니다. 타입 토큰 패턴(Class<T>를 키로 사용)은 소거를 우회하여 이질적 컨테이너에서 런타임 타입 안전을 제공합니다. 이 패턴들은 Spring Data 같은 프레임워크의 중추입니다.
// Generic interface
interface Repository<T, ID> {
Optional<T> findById(ID id);
List<T> findAll();
void save(T entity);
}
// Implement with concrete types
class UserRepository implements Repository<User, Long> {
public Optional<User> findById(Long id) { /* ... */ return Optional.empty(); }
public List<User> findAll() { return List.of(); }
public void save(User entity) {}
}
// Generic interface with self-referencing bound (Comparable pattern)
interface Comparable<T> {
int compareTo(T other);
}
class Temperature implements Comparable<Temperature> {
private final double celsius;
Temperature(double c) { celsius = c; }
public int compareTo(Temperature other) {
return Double.compare(celsius, other.celsius);
}
}
// Generic builder pattern
class Builder<T> {
private T value;
public Builder<T> set(T v) { value = v; return this; }
public T build() { return value; }
}
// Type token pattern for runtime type safety
class TypeSafeMap {
private final Map<Class<?>, Object> map = new HashMap<>();
public <T> void put(Class<T> type, T value) { map.put(type, value); }
public <T> T get(Class<T> type) { return type.cast(map.get(type)); }
}
record User(String name) {}
import java.util.Optional;어노테이션
내장 어노테이션
Java 내장 어노테이션: @Override(재정의에서 오타를 잡음 — 항상 사용), @Deprecated(API 사용을 피하라는 신호, since/forRemoval 메타데이터 포함), @SuppressWarnings(특정 경고를 무시 — 좁게 사용), @FunctionalInterface(SAM 규칙을 강제). 이것들이 컴파일 타임 안전과 문서화를 향상하는 일상적 어노테이션입니다.
import java.util.*;
// @Override: declares intent to override (compiler checks)
class Animal {
public void sound() { System.out.println("..."); }
}
class Dog extends Animal {
@Override
public void sound() { System.out.println("Woof"); }
}
// @Deprecated: marks API as outdated
class OldApi {
@Deprecated(since = "1.5", forRemoval = true)
public void legacy() {}
}
// @SuppressWarnings: silence compiler warnings
@SuppressWarnings("unchecked")
List<String> list = (List<String>) new ArrayList();
// @FunctionalInterface: enforces single abstract method
@FunctionalInterface
interface Op { int apply(int a, int b); }
// Common warning keys: unchecked, deprecation, rawtypes, null
// Java 17+ sealed-related
@Deprecated
class ToRemove {}커스텀 어노테이션
커스텀 어노테이션은 @interface로 선언됩니다. 멤버는 메서드처럼 보이지만 어노테이션 속성이며 — 기본값을 가질 수 있습니다. @Target으로 적용 위치를 제한하고(TYPE, METHOD, FIELD 등), @Retention으로 가용성을 제어하세요. 마커 어노테이션(멤버 없음)은 요소를 태그만 합니다. 어노테이션 자체는 동작을 가지지 않습니다 — 프로세서(리플렉션, 어노테이션 도구)가 읽고 행동합니다.
import java.lang.annotation.*;
// Define an annotation
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Test {
String value() default "";
long timeout() default 0L;
}
// Use it
class MyTests {
@Test
public void quickCheck() {}
@Test(timeout = 5000)
public void slowCheck() {}
@Test("custom-name")
public void named() {}
}
// Annotation with default values
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.FIELD})
public @interface Entity {
String table() default "";
String[] columns() default {};
}
@Entity(table = "users", columns = {"id", "name"})
class User {}
// Marker annotation (no members)
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Service {}Retention & Target
@Retention은 어노테이션이 얼마나 오래 살아남는지 제어합니다: SOURCE(컴파일 타임만, @Override처럼), CLASS(바이트코드에 있지만 런타임에 보이지 않음 — 기본), RUNTIME(리플렉션으로 접근 가능). @Target은 어노테이션이 나타날 수 있는 위치를 제한합니다. Java 8+는 TYPE_USE와 TYPE_PARAMETER를 추가하여, 제네릭과 캐스트에 어노테이션을 붙일 수 있게 합니다(List<@NonNull String>). 리플렉션 접근이 필요할 때만 RUNTIME을 선택하세요.
import java.lang.annotation.*;
// RetentionPolicy.SOURCE: discarded by compiler (e.g., @Override)
@Retention(RetentionPolicy.SOURCE)
@interface CompileOnly {}
// RetentionPolicy.CLASS: kept in .class but not loaded (default)
@Retention(RetentionPolicy.CLASS)
@interface BytecodeOnly {}
// RetentionPolicy.RUNTIME: available via reflection at runtime
@Retention(RetentionPolicy.RUNTIME)
@interface RuntimeVisible {}
// ElementType targets
@Target(ElementType.TYPE) // classes, interfaces, enums
@interface ForType {}
@Target(ElementType.METHOD)
@interface ForMethod {}
@Target(ElementType.FIELD)
@interface ForField {}
@Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER})
@interface ForTypeUse {}
// Java 8+ type-use annotations (annotate any type occurrence)
@RuntimeVisible String[] names; // example usage
List<@RuntimeVisible String> typed;리플렉션으로 어노테이션 읽기
RUNTIME 보존의 어노테이션은 리플렉션으로 읽을 수 있습니다: isAnnotationPresent()는 존재를 검사하고, getAnnotation()은 검색합니다. 이것이 프레임워크(Spring, JUnit, JAX-RS)가 동작을 선언적으로 연결하는 방식입니다 — 메서드/클래스에 태그하고, 프레임워크가 스캔하고 디스패치합니다. 컴파일 타임 어노테이션 처리(어노테이션 프로세서)는 런타임 리플렉션 비용 없이 코드 생성을 위한 대안입니다.
import java.lang.annotation.*;
import java.lang.reflect.*;
import java.util.*;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@interface Route {
String path();
String method() default "GET";
}
class Api {
@Route(path = "/users", method = "GET")
public void listUsers() {}
@Route(path = "/users", method = "POST")
public void createUser() {}
}
// Scan methods for @Route at runtime
for (Method m : Api.class.getDeclaredMethods()) {
if (m.isAnnotationPresent(Route.class)) {
Route r = m.getAnnotation(Route.class);
System.out.println(r.method() + " " + r.path() + " -> " + m.getName());
}
}
// Output:
// GET /users -> listUsers
// POST /users -> createUser
// Reading annotations on a class
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@interface Table { String name(); }
@Table(name = "orders")
class Order {}
Table t = Order.class.getAnnotation(Table.class);
System.out.println(t.name()); // "orders"반복 가능 & 메타 어노테이션
@Repeatable(Java 8+)는 컨테이너 어노테이션을 정의하여 같은 어노테이션을 여러 번 적용할 수 있게 합니다. @Inherited는 어노테이션이 서브클래스로 전파되게 합니다(클래스 수준 어노테이션만). @Documented는 어노테이션을 Javadoc에 포함합니다. ANNOTATION_TYPE이 있는 @Target은 메타 어노테이션(다른 어노테이션에 어노테이션을 붙이는)을 만듭니다 — 이것이 Spring이 @RestController = @Controller + @ResponseBody 같은 조합 가능한 어노테이션 스테레오타입을 구축하는 방식입니다.
import java.lang.annotation.*;
// Repeatable: allow same annotation multiple times
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@interface Schedule {
String cron();
}
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@interface Schedules {
Schedule[] value(); // container annotation
}
// Make Schedule repeatable
@Repeatable(Schedules.class)
@interface Schedule2 {
String cron();
}
// Now you can repeat it (Java 8+)
class Job {
@Schedule2(cron = "0 0 * * *")
@Schedule2(cron = "0 30 * * *")
public void run() {}
}
// Meta-annotations: annotations on annotations
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE) // can only annotate other annotations
@interface TestCategory {}
@TestCategory
@Retention(RetentionPolicy.RUNTIME)
@interface UnitTest {}
// Inherited: subclass inherits the annotation
@Inherited
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@interface Persistent {}
@Persistent class Base {}
class Child extends Base {} // Child also has @Persistent
// Documented: appears in Javadoc
@Documented
@interface PublicApi {}리플렉션
Class 객체 & 클래스 얻기
로드된 모든 타입은 고유한 Class 객체를 가집니다 — 리플렉션의 진입점. 클래스 리터럴(Type.class), instance.getClass(), 또는 Class.forName()(동적 로딩, ClassNotFoundException 발생)으로 얻으세요. Class 객체는 이름, 수정자, 슈퍼클래스, 인터페이스, 타입 검사(isInterface, isArray, isEnum, isRecord)를 노출합니다. isAssignableFrom은 다형성 관계를 검사합니다.
import java.lang.reflect.*;
// Three ways to get a Class object
Class<String> c1 = String.class; // class literal
Class<?> c2 = "hello".getClass(); // from instance
Class<?> c3 = Class.forName("java.lang.String"); // by name (throws checked)
// Basic introspection
Class<?> c = String.class;
System.out.println(c.getName()); // "java.lang.String"
System.out.println(c.getSimpleName()); // "String"
System.out.println(c.getPackage()); // "package java.lang"
System.out.println(c.getSuperclass()); // "class java.lang.Object"
System.out.println(Modifier.toString(c.getModifiers())); // "public final"
// Check type relationships
System.out.println(c.isInterface()); // false
System.out.println(c.isArray()); // false
System.out.println(c.isEnum()); // false
System.out.println(c.isRecord()); // false (Java 16+)
System.out.println(CharSequence.class.isAssignableFrom(c)); // true
// Primitive class objects
Class<?> intClass = int.class;
Class<?> intArrayClass = int[].class;
System.out.println(intClass.isPrimitive()); // true필드, 메서드, 생성자 검사
getDeclaredFields/Methods/Constructors는 이 클래스에 선언된 모든 멤버(private 포함)를 반환합니다. getFields/getMethods는 public 멤버만 반환하지만 상속된 것을 포함합니다. 특정 멤버를 찾으려면, getDeclaredField(name) 또는 getDeclaredMethod(name, paramTypes...)를 사용하세요 — 오버로드를 구별하기 위해 매개변수 타입이 필요. 리플렉션은 setAccessible(true)를 호출하지 않으면 접근 제어를 우회하지 않습니다.
import java.lang.reflect.*;
import java.util.*;
class Sample {
public String name;
private int count;
public Sample() {}
public Sample(String n) { name = n; }
private void secret() {}
public int compute(int x) { return x * 2; }
}
Class<?> c = Sample.class;
// Fields: getDeclaredFields includes private; getFields only public
for (Field f : c.getDeclaredFields()) {
System.out.println(f.getName() + " : " + f.getType().getSimpleName()
+ " (" + Modifier.toString(f.getModifiers()) + ")");
}
// Methods
for (Method m : c.getDeclaredMethods()) {
System.out.println(m.getName()
+ " params=" + Arrays.toString(m.getParameterTypes())
+ " returns=" + m.getReturnType().getSimpleName());
}
// Constructors
for (Constructor<?> ctor : c.getConstructors()) {
System.out.println("ctor params=" + Arrays.toString(ctor.getParameterTypes()));
}
// Lookup specific member
Field nameField = c.getDeclaredField("name");
Method compute = c.getDeclaredMethod("compute", int.class);
Constructor<?> ctor = c.getDeclaredConstructor(String.class);메서드 호출 & 인스턴스 생성
Method.invoke(obj, args...)는 메서드를 리플렉티브하게 호출합니다 — 항상 Object를 반환하므로, 결과를 캐스트하세요. setAccessible(true)는 Java 접근 검사를 우회합니다(private 멤버가 접근 가능해짐; 모듈에서 --add-opens 필요할 수 있음). Constructor.newInstance()는 객체를 생성합니다 — new의 리플렉티브 동등물. Array.newInstance는 런타임에 알려진 컴포넌트 타입의 배열을 만듭니다. 리플렉션은 직접 호출보다 느리고 컴파일 타임 안전을 우회합니다.
import java.lang.reflect.*;
class Greeter {
public String greet(String name) { return "Hello, " + name; }
private String secret() { return "hidden"; }
}
Object obj = new Greeter();
Class<?> c = obj.getClass();
// Invoke public method
Method greet = c.getMethod("greet", String.class);
String result = (String) greet.invoke(obj, "Alice"); // "Hello, Alice"
// Invoke private method
Method sec = c.getDeclaredMethod("secret");
sec.setAccessible(true); // bypass access check
String s = (String) sec.invoke(obj); // "hidden"
// Create instances via constructor
Constructor<?> noArg = c.getConstructor();
Object o1 = noArg.newInstance();
// With args
class Person {
String name;
public Person(String n) { name = n; }
public String toString() { return "Person(" + name + ")"; }
}
Constructor<?> ctor = Person.class.getConstructor(String.class);
Object p = ctor.newInstance("Bob");
System.out.println(p); // "Person(Bob)"
// Array creation via reflection
Object strArray = Array.newInstance(String.class, 5);
Array.set(strArray, 0, "first");
String v = (String) Array.get(strArray, 0);필드 읽기 & 수정
Field.get(instance)는 필드 값을 읽고; Field.set(instance, value)은 씁니다. 원시 타입의 경우, 박싱을 피하기 위해 타입별 접근자(getInt/setInt)를 사용하세요. 정적 필드는 instance 인수로 null을 받습니다. private 필드에는 setAccessible(true)가 필요합니다. 리플렉션 필드 접근이 직렬화 라이브러리(Jackson, Gson)와 ORM 프레임워크가 객체 상태를 일반적으로 읽고/쓰는 방식입니다.
import java.lang.reflect.*;
class Config {
public String env = "dev";
private int retries = 3;
public static String VERSION = "1.0";
}
Config cfg = new Config();
Class<?> c = cfg.getClass();
// Read public field
Field env = c.getField("env");
String e = (String) env.get(cfg); // "dev"
// Read private field
Field retries = c.getDeclaredField("retries");
retries.setAccessible(true);
int r = retries.getInt(cfg); // 3
// For objects: Object val = field.get(instance);
// Modify fields
env.set(cfg, "prod");
retries.setInt(cfg, 5);
System.out.println(cfg.env); // "prod"
// Static fields: pass null as the instance
Field version = c.getField("VERSION");
String v = (String) version.get(null); // "1.0"
version.set(null, "2.0");
// Type-specific getters/setters avoid boxing
// getInt/setInt, getLong/setLong, getBoolean/setBoolean, etc.
// For reference types, use get()/set()동적 프록시 & 사용 사례
java.lang.reflect.Proxy는 런타임에 인터페이스를 구현하는 동적 프록시를 만듭니다 — InvocationHandler가 모든 호출을 가로챕니다. 이것이 Spring AOP, Hibernate 지연 로딩, Mockito 모의가 작동하는 방식입니다. 리플렉션은 대부분의 Java 프레임워크(DI, ORM, 직렬화, 테스팅)를 구동하지만 비용이 있습니다: 직접 호출보다 느리고, 더 약한 타입 안전, 모듈 시스템 제한. 런타임 유연성이 필요할 때 사용하고, 일반 코드에는 사용하지 마세요.
import java.lang.reflect.*;
import java.util.*;
// JDK dynamic proxy: implements interfaces at runtime
interface UserService {
String getUser(long id);
void deleteUser(long id);
}
// InvocationHandler intercepts every method call
class LoggingHandler implements InvocationHandler {
private final Object target;
LoggingHandler(Object t) { target = t; }
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("Calling " + method.getName() + "(" + Arrays.toString(args) + ")");
long start = System.nanoTime();
Object result = method.invoke(target, args);
System.out.println(" -> " + result + " (" + (System.nanoTime() - start) + "ns)");
return result;
}
}
UserService real = id -> "user-" + id;
UserService proxied = (UserService) Proxy.newProxyInstance(
UserService.class.getClassLoader(),
new Class<?>[]{UserService.class},
new LoggingHandler(real)
);
proxied.getUser(42); // logs the call and result
// Common reflection use cases:
// - Frameworks: Spring DI, JPA entities, Jackson serialization
// - AOP/proxies: transactions, logging, security
// - Annotation processing at runtime
// - Test frameworks: JUnit discovers @Test methods
// - Code generation tools
// Caution: reflection is slower, bypasses compile-time checks,
// and can break under modules (--add-opens). Prefer alternatives when possible.JDBC & 데이터베이스 접근
Connection & DriverManager
DriverManager.getConnection()은 데이터베이스 연결을 엽니다 — 누수를 피하기 위해 항상 try-with-resources로 감싸세요. JDBC 4부터 드라이버는 ServiceLoader를 통해 자동 등록되므로, Class.forName()이 거의 필요 없습니다. URL 형식은 벤더마다 다릅니다. 연결 옵션(SSL, 타임아웃)을 위해 Properties를 사용하세요. 프로덕션에서는 직접 DriverManager 호출보다 연결 풀(HikariCP)을 선호하세요.
import java.sql.*;
// Basic connection (try-with-resources auto-closes)
String url = "jdbc:postgresql://localhost:5432/mydb";
try (Connection conn = DriverManager.getConnection(url, "user", "pass")) {
System.out.println("Connected: " + conn.getSchema());
// ... use connection
}
// Modern: no need for Class.forName() with JDBC 4+ (auto-discovery)
// Legacy: Class.forName("org.postgresql.Driver");
// Common URL patterns:
// jdbc:postgresql://host:5432/db
// jdbc:mysql://host:3306/db
// jdbc:oracle:thin:@host:1521:db
// jdbc:sqlite:/path/to/db.sqlite
// jdbc:h2:mem:test (in-memory H2)
// Connection properties
import java.util.Properties;
Properties props = new Properties();
props.setProperty("user", "user");
props.setProperty("password", "pass");
props.setProperty("ssl", "true");
try (Connection c = DriverManager.getConnection(url, props)) {
// ...
}Statement vs PreparedStatement
매개변수가 있는 쿼리에는 항상 Statement보다 PreparedStatement를 사용하세요 — SQL 구조와 데이터를 분리하여 SQL 인젝션을 방지합니다. 매개변수는 1 기반 인덱스로 타입별 setter로 설정됩니다. PreparedStatement는 재사용할 수 있고(매개변수 재설정 후 다시 실행), 대량 작업을 위한 배치(addBatch/executeBatch)를 지원합니다. Statement는 DDL 같은 정적이고 신뢰할 수 있는 SQL에만 적합합니다.
import java.sql.*;
// Statement: plain SQL (vulnerable to injection — avoid for user input)
try (Connection c = DriverManager.getConnection("jdbc:h2:mem:", "sa", "");
Statement st = c.createStatement()) {
st.execute("CREATE TABLE users (id INT PRIMARY KEY, name VARCHAR(100))");
st.execute("INSERT INTO users VALUES (1, 'Alice')");
// NEVER do this with user input — SQL injection!
// st.execute("SELECT * FROM users WHERE name = '" + userInput + "'");
}
// PreparedStatement: parameterized, safe from injection, can be reused
String sql = "INSERT INTO users (id, name) VALUES (?, ?)";
try (Connection c = DriverManager.getConnection("jdbc:h2:mem:", "sa", "");
PreparedStatement ps = c.prepareStatement(sql)) {
// Set parameters by index (1-based!)
ps.setInt(1, 1);
ps.setString(2, "Alice");
ps.executeUpdate();
// Reuse with different params
ps.setInt(1, 2);
ps.setString(2, "Bob");
ps.executeUpdate();
// Batch inserts
for (int i = 3; i <= 100; i++) {
ps.setInt(1, i);
ps.setString(2, "user" + i);
ps.addBatch();
}
ps.executeBatch();
}ResultSet & 쿼리
ResultSet은 쿼리 행 위의 커서입니다 — next()로 전진하세요(끝에서 false 반환). 이름(읽기 쉬움) 또는 1 기반 인덱스로 열을 읽으세요. wasNull()은 SQL NULL과 원시 기본값(예: getInt가 NULL에 0 반환)을 구분합니다. 기본 ResultSet은 전진 전용입니다; TYPE_SCROLL_INSENSITIVE + CONCUR_UPDATABLE는 임의 접근과 제자리 업데이트를 가능하게 하지만, 현대 앱에서는 거의 사용되지 않습니다.
import java.sql.*;
import java.util.*;
record User(int id, String name, String email) {}
// Execute query and map rows
String sql = "SELECT id, name, email FROM users WHERE active = ?";
List<User> users = new ArrayList<>();
try (Connection c = getConnection();
PreparedStatement ps = c.prepareStatement(sql)) {
ps.setBoolean(1, true);
try (ResultSet rs = ps.executeQuery()) {
while (rs.next()) { // advance to next row
users.add(new User(
rs.getInt("id"), // by column name
rs.getString("name"),
rs.getString("email") // by column name (preferred)
));
}
}
}
// Column access by index (1-based) or name (more readable)
// rs.getInt(1), rs.getString(2), rs.getBoolean("active")
// Handle NULLs
String nick = rs.getString("nickname");
if (rs.wasNull()) nick = "anonymous"; // distinguish NULL from real null
// Scrollable/updatable ResultSet (needs specific flags)
Statement st = c.createStatement(
ResultSet.TYPE_SCROLL_INSENSITIVE,
ResultSet.CONCUR_UPDATABLE
);
ResultSet rs = st.executeQuery("SELECT * FROM users");
rs.absolute(5); // jump to row 5
rs.updateString("name", "newname");
rs.updateRow(); // persist change
Connection getConnection() throws SQLException {
return DriverManager.getConnection("jdbc:h2:mem:", "sa", "");
}트랜잭션 & 배치
JDBC는 기본적으로 각 문을 자동 커밋합니다 — autoCommit(false)로 문을 트랜잭션으로 그룹화하세요. 성공 시 커밋, 실패 시 롤백. Savepoint는 트랜잭션 내에서 부분 롤백을 허용합니다. 격리 수준은 동시 변경의 가시성을 제어합니다(READ_COMMITTED가 일반 기본값; SERIALIZABLE이 가장 안전하지만 가장 느림). 트랜잭션 상태 누수를 피하기 위해 항상 autoCommit를 복원하거나 연결을 닫으세요.
import java.sql.*;
// Transactions: disabled by default (auto-commit = true)
try (Connection c = getConnection()) {
c.setAutoCommit(false); // start transaction
try (PreparedStatement ps = c.prepareStatement(
"UPDATE accounts SET balance = balance - ? WHERE id = ?")) {
ps.setInt(1, 100); ps.setInt(2, 1); ps.executeUpdate(); // debit
ps.setInt(1, -100); ps.setInt(2, 2); ps.executeUpdate(); // credit
}
c.commit(); // commit both
// If any step fails, rollback
} catch (SQLException e) {
// connection auto-closed; rollback happens implicitly on close if not committed
}
// Explicit rollback pattern
try (Connection c = getConnection()) {
c.setAutoCommit(false);
try {
// ... multiple statements
c.commit();
} catch (SQLException e) {
c.rollback(); // undo all changes in this transaction
throw e;
}
}
// Savepoints: partial rollback
Statement st = c.createStatement();
st.execute("INSERT INTO log VALUES (1)");
Savepoint sp = c.setSavepoint("before-risky");
st.execute("INSERT INTO log VALUES (2)");
c.rollback(sp); // undo only after savepoint
c.commit();
// Transaction isolation levels
c.setTransactionIsolation(Connection.TRANSACTION_READ_COMMITTED);
// Levels: NONE, READ_UNCOMMITTED, READ_COMMITTED, REPEATABLE_READ, SERIALIZABLE
Connection getConnection() throws SQLException {
return DriverManager.getConnection("jdbc:h2:mem:", "sa", "");
}연결 풀링 (HikariCP)
연결 풀(HikariCP가 사실상 표준)은 연결을 따뜻하게 유지하고 재사용하여, 요청당 새 TCP+인증 연결을 여는 10-100ms 비용을 피합니다. 최대 풀 크기(DB 용량으로 제한), 타임아웃, 수명을 구성하세요. getConnection()으로 빌리고, 닫아서 반환합니다(닫히지 않고 풀로 돌아감). 종료 시 항상 DataSource를 닫으세요. Spring Boot에서 HikariCP가 자동 구성됩니다.
import com.zaxxer.hikari.*;
import java.sql.*;
import java.util.*;
// HikariCP: high-performance JDBC connection pool
HikariConfig config = new HikariConfig();
config.setJdbcUrl("jdbc:postgresql://localhost:5432/mydb");
config.setUsername("user");
config.setPassword("pass");
config.setMaximumPoolSize(10);
config.setMinimumIdle(2);
config.setConnectionTimeout(30_000); // ms to wait for a connection
config.setIdleTimeout(600_000); // ms before idle connections close
config.setMaxLifetime(1_800_000); // ms max connection lifetime
config.setPoolName("app-pool");
HikariDataSource ds = new HikariDataSource(config);
// Borrow a connection, use it, return it (auto via try-with-resources)
try (Connection c = ds.getConnection();
PreparedStatement ps = c.prepareStatement("SELECT * FROM users")) {
try (ResultSet rs = ps.executeQuery()) {
while (rs.next()) System.out.println(rs.getString("name"));
}
} // connection returns to pool here
// Shutdown pool when app stops
ds.close();
// Why pool? Opening a TCP+auth connection is expensive (~10-100ms).
// A pool keeps connections warm and reuses them across requests,
// dramatically reducing latency under load.
// Alternative pools: Apache DBCP, c3p0, Tomcat JDBC, AgroalI/O & NIO 심층
InputStream & OutputStream (바이트)
InputStream/OutputStream은 원시 바이트를 처리합니다. 항상 Buffered* 변형으로 감싸세요 — 버퍼링되지 않은 I/O는 바이트당 시스템 호출을 하여 성능에 치명적. read()는 스트림 끝에서 -1 반환. transferTo()(Java 9+)는 효율적인 대량 복사를 합니다. readAllBytes()는 편리하지만 전체 스트림을 메모리에 로드합니다 — 작은 파일에만. 항상 스트림을 닫으세요(try-with-resources).
import java.io.*;
// Read bytes from a file
try (InputStream in = new FileInputStream("input.bin")) {
int b;
while ((b = in.read()) != -1) { // -1 = end of stream
// process byte
}
}
// Buffered for performance (8KB default buffer)
try (InputStream in = new BufferedInputStream(new FileInputStream("big.bin"))) {
byte[] buffer = new byte[8192];
int read;
while ((read = in.read(buffer)) != -1) {
// process buffer[0..read]
}
}
// readAllBytes (small files only — loads everything into memory)
byte[] all = new FileInputStream("small.bin").readAllBytes();
// Write bytes
try (OutputStream out = new BufferedOutputStream(new FileOutputStream("out.bin"))) {
out.write(65); // single byte
out.write(new byte[]{66, 67, 68});
out.flush(); // force buffered data to disk
}
// Copy streams (Java 9+)
try (InputStream in = new FileInputStream("src.bin");
OutputStream out = new FileOutputStream("dst.bin")) {
in.transferTo(out); // efficient bulk copy
}
// Standard streams
System.in.read(); // stdin (InputStream)
System.out.write(65); // stdout (PrintStream)Channel & Buffer (NIO)
NIO Channel + ByteBuffer는 스트림의 고성능 대안입니다. Buffer는 position/limit/capacity를 가집니다; flip()은 쓰기에서 읽기 모드로 전환하고, clear()는 쓰기를 위해 재설정하고, compact()는 읽지 않은 데이터를 보존합니다. 직접 버퍼(allocateDirect)는 JVM 힙 외부에 있어 대형 I/O의 복사 단계를 피합니다. channel 간 transferTo는 지원되는 OS에서 제로 카피를 사용할 수 있습니다. 스트리밍 성능이 중요할 때 NIO를 사용하세요.
import java.nio.*;
import java.nio.channels.*;
import java.nio.file.*;
import java.io.*;
// Channel: high-performance, block-oriented I/O
try (FileChannel ch = FileChannel.open(Path.of("data.bin"),
StandardOpenOption.READ, StandardOpenOption.WRITE)) {
// ByteBuffer: fixed-capacity block of bytes
ByteBuffer buf = ByteBuffer.allocate(1024);
int read = ch.read(buf); // fill buffer from channel
buf.flip(); // switch from write mode to read mode
while (buf.hasRemaining()) {
byte b = buf.get();
}
buf.clear(); // reset for next read (or compact() to preserve unread)
}
// Direct buffer: outside JVM heap, faster for large I/O (no copy)
ByteBuffer direct = ByteBuffer.allocateDirect(64 * 1024);
// Scatter/gather: read into multiple buffers / write from multiple
ByteBuffer header = ByteBuffer.allocate(128);
ByteBuffer body = ByteBuffer.allocate(1024);
ch.read(new ByteBuffer[]{header, body}); // scatter
// Transfer between channels (zero-copy on some OSes)
try (FileChannel src = FileChannel.open(Path.of("a.bin"));
FileChannel dst = FileChannel.open(Path.of("b.bin"), StandardOpenOption.WRITE)) {
src.transferTo(0, src.size(), dst);
}
// ByteOrder
buf.order(ByteOrder.LITTLE_ENDIAN);
int value = buf.getInt();Path 연산 (NIO.2)
Path(NIO.2)는 구식 File 클래스를 대체합니다. Path 연산(getFileName, getParent, resolve, normalize, relativize)은 순수 문자열 수학입니다 — 디스크를 건드리지 않습니다. Files.* 메서드는 파일시스템과 상호작용합니다: 크기, 타임스탬프, 권한, 심볼릭 링크. normalize()는 . 와 .. 세그먼트를 정리합니다. resolveSibling은 이름 변경에 편리합니다(같은 디렉토리, 다른 이름). 현대 코드에서 File보다 Path를 선호하세요.
import java.nio.file.*;
import java.nio.file.attribute.*;
import java.io.IOException;
// Creating Path objects
Path p1 = Path.of("a/b/c.txt"); // relative
Path p2 = Path.of("/usr/local/bin"); // absolute
Path p3 = Paths.get("config.json"); // older API, same thing
// Path manipulation (doesn't touch filesystem)
System.out.println(p1.getFileName()); // "c.txt"
System.out.println(p1.getParent()); // "a/b"
System.out.println(p1.getRoot()); // "" (relative)
System.out.println(p2.getRoot()); // "/"
System.out.println(p1.toAbsolutePath()); // "/cwd/a/b/c.txt"
System.out.println(p1.normalize()); // removes . and ..
System.out.println(p1.resolve("d.txt")); // "a/b/c.txt/d.txt"
System.out.println(p1.resolveSibling("x.txt")); // "a/b/x.txt"
System.out.println(p1.relativize(Path.of("a/b"))); // "../.."
// File metadata
Path file = Path.of("notes.txt");
System.out.println(Files.size(file)); // bytes
System.out.println(Files.getLastModifiedTime(file));
System.out.println(Files.isReadable(file));
System.out.println(Files.isWritable(file));
// PosixFilePermissions (Linux/macOS)
String perms = "rwxr-xr--";
Set<PosixFilePermission> set = PosixFilePermissions.fromString(perms);
Files.setPosixFilePermissions(file, set);
// Symbolic links
Path link = Path.of("link.txt");
Files.createSymbolicLink(link, Path.of("target.txt"));
Path target = Files.readSymbolicLink(link);디렉토리 순회 & 파일 트리
Files.list()는 한 수준을 나열하고; Files.walk()는 트리를 재귀적으로 순회합니다(Stream 반환, 닫아야 함). Files.find()는 순회 중 경로와 속성으로 필터링합니다. 완전한 제어를 위해, walkFileTree와 FileVisitor로 하위 트리를 건너뛰고, 에러를 처리하고, 디렉토리 방문 전/후에 행동할 수 있습니다. 모두 파일 핸들을 가진 Stream을 반환합니다 — 항상 try-with-resources를 사용하세요. 비용이 많이 드는 순회를 제한하려면 max depth를 사용하세요.
import java.nio.file.*;
import java.io.IOException;
import java.util.stream.*;
// List directory entries (one level)
try (Stream<Path> entries = Files.list(Path.of("."))) {
entries.filter(Files::isRegularFile)
.map(Path::getFileName)
.forEach(System.out::println);
}
// Walk file tree recursively (depth-first)
try (Stream<Path> walk = Files.walk(Path.of("/project"))) {
walk.filter(Files::isRegularFile)
.filter(p -> p.toString().endsWith(".java"))
.forEach(System.out::println);
}
// Walk with depth limit
try (Stream<Path> walk = Files.walk(Path.of("/project"), 3)) {
walk.forEach(System.out::println);
}
// Find with BiPredicate (path + attributes)
try (Stream<Path> found = Files.find(Path.of("/logs"), 10,
(path, attrs) -> attrs.isRegularFile()
&& attrs.size() > 1_000_000
&& path.toString().endsWith(".log"))) {
found.forEach(p -> System.out.println("Large log: " + p));
}
// FileVisitor for full control (pre/post visit, skip subtrees)
Files.walkFileTree(Path.of("/project"), new SimpleFileVisitor<>() {
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
System.out.println("Visiting " + file);
return FileVisitResult.CONTINUE;
}
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) {
if (dir.getFileName().toString().equals("target")) {
return FileVisitResult.SKIP_SUBTREE; // skip target dirs
}
return FileVisitResult.CONTINUE;
}
});
import java.nio.file.attribute.BasicFileAttributes;
import java.nio.file.attribute.BasicFileAttributes;WatchService & 파일 이벤트
WatchService는 등록된 디렉토리의 파일시스템 이벤트(생성, 수정, 삭제)를 받습니다. take()는 이벤트가 도착할 때까지 차단하고; pollEvents()는 배출합니다. 특정 파일을 추적하려면 부모 디렉토리를 감시하고 context()로 필터링하세요. 이벤트는 병합되거나 손실될 수 있습니다(OVERFLOW). 재귀적 감시는 모든 하위 디렉토리를 등록해야 합니다. WatchService는 OS 네이티브(Linux에서 inotify, macOS에서 FSEvents)이지만 API는 저수준입니다 — 복잡한 필요에는 라이브러리를 고려하세요.
import java.nio.file.*;
import static java.nio.file.StandardWatchEventKinds.*;
import java.io.IOException;
// Watch a directory for changes
try (WatchService watcher = FileSystems.getDefault().newWatchService()) {
Path dir = Path.of("/var/log");
dir.register(watcher,
ENTRY_CREATE,
ENTRY_DELETE,
ENTRY_MODIFY);
System.out.println("Watching " + dir + "...");
while (true) {
WatchKey key = watcher.take(); // blocks until event
for (WatchEvent<?> event : key.pollEvents()) {
Path changed = dir.resolve((Path) event.context());
System.out.println(event.kind() + " -> " + changed);
if (event.kind() == OVERFLOW) continue; // events lost
// React to change
if (event.kind() == ENTRY_CREATE) {
System.out.println("New file: " + changed);
}
}
if (!key.reset()) break; // key no longer valid (dir deleted)
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
// Note: WatchService watches directories, not individual files.
// To watch a file, watch its parent dir and filter by context().
// Events may be coalesced (rapid modifications may yield one event).
// For recursive watching, register each subdirectory manually
// or use a library like Apache Commons IO's FileAlterationMonitor.Record & 패턴 매칭
Record 기본
Record(Java 16+)는 투명한 불변 데이터 운반자입니다. 헤더 `record Name(Type1 f1, Type2 f2)`가 생성자, 접근자(f1(), f2() — getF1()이 아님), equals, hashCode, toString을 생성합니다. DTO, 값 객체, 함수 결과에 이상적입니다. 컴팩트 생성자(그냥 `{ ... }`)는 필드 재할당 없이 검증이나 정규화를 합니다. Record는 인터페이스를 구현할 수 있지만 클래스를 확장할 수 없습니다.
// Record: concise immutable data carrier (Java 16+)
public record Point(int x, int y) {}
// Equivalent to writing:
// - final class with private final int x, y
// - constructor Point(int x, int y)
// - accessors x(), y()
// - equals, hashCode, toString (all auto-generated)
Point p = new Point(3, 4);
System.out.println(p.x()); // 3 (accessor, not getX())
System.out.println(p.y()); // 4
System.out.println(p); // "Point[x=3, y=4]"
System.out.println(p.equals(new Point(3, 4))); // true
// Immutable: no setters
// p.setX(5); // no such method
// Custom record with validation
public record Age(int value) {
public Age {
if (value < 0 || value > 150) {
throw new IllegalArgumentException("Invalid age: " + value);
}
}
}
// Record with multiple components
public record Employee(String name, int id, double salary, String dept) {}
// Records implement interfaces
public interface Named { String name(); }
public record Customer(String name, long id) implements Named {}컴팩트 생성자 & 커스텀 메서드
컴팩트 생성자(`public Name { ... }`)는 필드 할당 전에 실행됩니다 — 정규화를 위해 매개변수에 할당하면, 컴파일러가 필드에 할당합니다. Record는 추가 메서드와 정적 팩토리를 가질 수 있지만 헤더 외에 인스턴스 필드는 없습니다. 비-정규 생성자는 this(...)로 정규 생성자에 위임해야 합니다. 더 명확한 구조를 위해 정적 팩토리(Point.origin())와 일반 인스턴스 캐싱을 위해 사용하세요.
// Compact constructor: validation without reassigning
public record Email(String address) {
public Email {
if (!address.contains("@")) {
throw new IllegalArgumentException("Bad email: " + address);
}
address = address.toLowerCase().strip(); // normalize
}
}
// Add custom methods (but not mutable fields)
public record Money(double amount, String currency) {
public Money plus(Money other) {
if (!currency.equals(other.currency)) {
throw new IllegalArgumentException("Currency mismatch");
}
return new Money(amount + other.amount, currency);
}
public Money times(int factor) {
return new Money(amount * factor, currency);
}
public static Money usd(double amt) { return new Money(amt, "USD"); }
}
// Canonical + non-canonical constructors
public record Range(int start, int end) {
// Compact canonical (validation)
public Range {
if (start > end) throw new IllegalArgumentException();
}
// Non-canonical convenience constructor
public Range(int end) {
this(0, end);
}
}
// Static factories are common on records
public record Point(int x, int y) {
public static Point origin() { return new Point(0, 0); }
public static Point of(int x, int y) { return new Point(x, y); }
}Sealed 클래스
Sealed 클래스/인터페이스(Java 17+)는 `permits` 절로 확장할 수 있는 타입을 제한합니다. 각 허용된 서브타입은 final, sealed, 또는 non-sealed여야 합니다. record와 결합하여, 대수적 데이터 타입(닫힌 상속 + 불변 데이터)을 형성합니다. 주요 이점: 컴파일러가 모든 서브타입을 알고 있어, switch 표현식이 default 브랜치 없이 철저할 수 있습니다 — 새 서브타입을 추가하면, 컴파일러가 업데이트가 필요한 모든 switch를 표시합니다.
// Sealed class: restricts which classes can extend it (Java 17+)
public sealed interface Shape
permits Circle, Square, Triangle {}
public record Circle(double radius) implements Shape {}
public record Square(double side) implements Shape {}
public record Triangle(double base, double height) implements Shape {}
// Every permitted subtype must be final, sealed, or non-sealed
public non-sealed class WeirdShape implements Shape {} // open again
// Why sealed? Enables exhaustive pattern matching
public double area(Shape s) {
return switch (s) {
case Circle c -> Math.PI * c.radius() * c.radius();
case Square sq -> sq.side() * sq.side();
case Triangle t -> 0.5 * t.base() * t.height();
// No default needed — compiler knows all subtypes
};
}
// Sealed class hierarchy with records = algebraic data types
// Combines: closed inheritance (sealed) + immutable data (record)
// Sealed classes also work with classes (not just interfaces)
public sealed abstract class Result permits Success, Failure {}
public final class Success<T>(T value) extends Result {}
public final class Failure(String error) extends Result {}instanceof를 위한 패턴 매칭
instanceof 패턴 매칭(Java 16+)은 검사가 성공할 때만 바인딩되는 변수를 선언하여, 명시적 캐스트를 제거합니다. 변수의 범위는 패턴의 참에서 흘러나옵니다 — && 연속과 조기 반환 후에 사용 가능. Java 21은 switch에서 패턴 매칭(case Type var when guard)을 추가하여, 가드가 있는 타입 기반 디스패치를 가능하게 합니다. 이것이 타입 검사 코드를 훨씬 간결하고 에러가 적게 만듭니다.
// Old way: cast after instanceof
Object obj = "hello";
if (obj instanceof String) {
String s = (String) obj; // redundant cast
System.out.println(s.length());
}
// Java 16+: pattern variable
if (obj instanceof String s) {
System.out.println(s.length()); // s is in scope, no cast
}
// The variable is scoped by the pattern's truth
if (obj instanceof String s && s.length() > 3) {
System.out.println("Long string: " + s);
}
// s is NOT in scope here if the && short-circuited
// Negation scoping
if (!(obj instanceof String s)) {
// s NOT in scope here
return;
}
// s IS in scope here (the instanceof must have been true)
// Combining with other patterns
Object data = 42;
String desc = switch (data) {
case Integer i when i > 0 -> "positive int: " + i;
case Integer i -> "non-positive int: " + i;
case String s -> "string: " + s;
case null -> "null";
default -> "other";
};Switch 패턴 매칭 (Java 21)
Switch 패턴 매칭(Java 21, 최종)은 타입으로 switch하고, record를 분해하고, 가드(when)를 추가할 수 있게 합니다. sealed 타입과 결합하여, 컴파일러가 철저성을 검증합니다 — 모든 서브타입이 커버되면 default가 필요 없습니다. record 패턴(case Point(int x, int y))은 한 단계로 분해합니다. null case는 명시적입니다(NPE 없음). 이것이 Java를 도메인을 선언적으로 모델링하는 ML/Scala의 패턴 매칭에 가깝게 만듭니다.
// Java 21: switch pattern matching with type patterns and guards
sealed interface Event permits Login, Logout, Message, Error {}
record Login(String user) implements Event {}
record Logout(String user) implements Event {}
record Message(String from, String text) implements Event {}
record Error(int code, String detail) implements Event {}
String describe(Event e) {
return switch (e) {
case Login l -> l.user() + " logged in";
case Logout l -> l.user() + " logged out";
case Message m when m.from().equals("system") -> "[system] " + m.text();
case Message m -> m.from() + ": " + m.text();
case Error(int code, String detail) when code >= 500 -> "SERVER ERROR " + code;
case Error(int code, String detail) -> "error " + code + ": " + detail;
case null -> "no event"; // explicit null handling
};
}
// Record patterns: destructure records in one go
record Point(int x, int y) {}
String classify(Object o) {
return switch (o) {
case Point(int x, int y) when x == y -> "diagonal";
case Point(int x, int y) -> "point at (" + x + "," + y + ")";
default -> "not a point";
};
}
// Nested patterns
record Box(Point p) {}
String describe2(Object o) {
return switch (o) {
case Box(Point(int x, int y)) -> "box at " + x + "," + y;
default -> "unknown";
};
}모듈 (JPMS)
module-info.java 기본
module-info.java는 모듈을 선언합니다(Java 9+ JPMS). requires는 종속성을 추가하고; exports는 패키지를 접근 가능하게 하고; opens는 리플렉트 접근을 허용하고(직렬화/DI 프레임워크에 필요); uses/provides는 ServiceLoader를 연결합니다. 이 파일 없이, 코드는 레거시 동작의 '이름 없는 모듈'로 classpath에 있습니다. 모듈은 강한 캡슐화(내보낸 패키지만 public)와 신뢰할 수 있는 구성(명시적 종속성)을 제공합니다.
// File: src/com.example.app/module-info.java
module com.example.app {
requires java.sql; // depends on java.sql module
requires transitive com.example.lib; // re-export (consumers see it too)
requires static java.annotation; // compile-time only (optional at runtime)
exports com.example.app.api; // public API visible to all
exports com.example.app.internal to com.example.test; // qualified export
opens com.example.app.model to com.fasterxml.jackson.databind; // reflection only
opens com.example.app.dynamic; // open to all for reflection
uses com.example.app.spi.Plugin; // service consumer
provides com.example.app.spi.Plugin with com.example.app.plugins.DefaultPlugin; // service provider
}
// Key directives:
// requires: depends on another module
// exports: makes packages public to other modules
// opens: allows deep reflection (for frameworks like Jackson, Hibernate)
// uses/provides: service loader integration
// A module without module-info.java is an "unnamed module" (classpath behavior)requires, exports, opens
requires는 종속성을 선언하고; requires transitive는 전파합니다(공개 API가 해당 모듈의 타입을 노출할 때 사용). exports는 패키지를 public으로 만들고; exports to는 명명된 모듈로 제한합니다(제한된 내보내기). opens는 리플렉트 접근(깊은 리플렉션)을 부여합니다 — setAccessible(true)을 하는 프레임워크에 필수. exports(공개 API)와 opens(리플렉션)의 구별이 핵심입니다: 강한 캡슐화가 기본이며, 패키지마다 옵트인합니다.
// Module A: com.example.library
module com.example.library {
// Public API anyone can use
exports com.example.library.api;
// Internal package: only visible to specific modules
exports com.example.library.internal to com.example.app;
// Allow reflection for frameworks (Jackson, JPA)
opens com.example.library.model;
// Only specific module can reflect
opens com.example.library.config to com.example.app;
// Dependencies
requires java.logging;
requires transitive java.sql; // consumers of A also get java.sql
}
// Module B: com.example.app (consumer)
module com.example.app {
requires com.example.library; // use A's exported packages
// Note: transitive means java.sql is also available here
requires com.fasterxml.jackson.databind;
}
// 'requires transitive X' means: any module requiring this one
// also reads X. Use when your exported API exposes X's types.
// 'opens' vs 'exports':
// exports: compile-time + runtime access to public members
// opens: runtime reflective access to ALL members (including private)
// Reflective access without 'opens' fails with InaccessibleObjectException
// in Java 16+ (strong encapsulation enforced by default).ServiceLoader & 서비스
ServiceLoader는 SPI 패턴을 구현합니다: 한 모듈의 인터페이스, 런타임에 발견된 구현. API 모듈은 인터페이스를 내보내고; 제공자 모듈은 `provides X with Y`를 선언하고; 소비자 모듈은 `uses X`를 선언합니다. ServiceLoader.load(X.class)가 모듈 경로의 모든 제공자를 찾습니다. 이것이 인터페이스와 구현을 분리합니다 — JDBC 드라이버, 로깅 백엔드(SLF4J), Charset 제공자가 모두 이렇게 작동합니다. 구현에 대한 컴파일 타임 종속성이 없습니다.
// SPI (Service Provider Interface) pattern with modules
// 1. Define the service interface in an API module
module com.example.spi {
exports com.example.spi;
}
package com.example.spi;
public interface Plugin {
String name();
void run();
}
// 2. Implement in a provider module
module com.example.plugin.impl {
requires com.example.spi;
provides com.example.spi.Plugin with com.example.plugin.impl.DefaultPlugin;
}
package com.example.plugin.impl;
import com.example.spi.Plugin;
public class DefaultPlugin implements Plugin {
public String name() { return "default"; }
public void run() { System.out.println("running"); }
}
// 3. Consume in an app module
module com.example.app {
requires com.example.spi;
uses com.example.spi.Plugin; // declares intent to load services
}
// Loading services at runtime
import java.util.ServiceLoader;
ServiceLoader<Plugin> loader = ServiceLoader.load(Plugin.class);
for (Plugin p : loader) {
System.out.println("Found: " + p.name());
p.run();
}
// ServiceLoader is how JDBC drivers, SLF4J backends, and many
// plugin systems are discovered without compile-time dependencies.모듈 경로 vs 클래스패스
모듈 경로(--module-path)는 강한 캡슐화를 가진 모듈식 JAR를 가지고; 클래스패스(-cp)는 캡슐화 없는 이름 없는 모듈로 레거시 JAR를 가집니다. 모듈식 JAR는 둘 다에서 작동합니다. 자동 모듈은 module-info 없이 모듈 경로에 배치된 JAR입니다 — 이름은 파일 이름이나 Automatic-Module-Name 매니페스트 속성에서 옵니다. --add-opens는 강한 캡슐화 아래에서 깨지는 레거시 리플렉션을 위한 탈출구입니다.
// Compile a module
// javac -d out/com.example.app \
// --module-source-path src \
// --module com.example.app
// Run a modular app
// java --module-path out --module com.example.app/com.example.app.Main
// Package as a modular JAR (includes module-info.class)
// jar --create --file app.jar --main-class com.example.app.Main -C out/com.example.app .
// Module path vs classpath:
// --module-path: modules with module-info, strong encapsulation enforced
// --class-path (or -cp): legacy "unnamed module", everything public, no encapsulation
// Mixing: modular JARs can be used on the classpath too (automatic module)
// java -cp lib/app.jar:lib/dep.jar com.example.app.Main
// An automatic module: a JAR without module-info on the module path.
// Its name is derived from the JAR filename (Automatic-Module-Name in MANIFEST.MF
// gives an explicit name).
// Inspect a module JAR
// jar --describe-module --file app.jar
// List observable modules
// java --list-modules
// java --describe-module java.sql
// Add opens at runtime for legacy reflection (escape hatch)
// java --add-opens com.example.app/com.example.app.internal=ALL-UNNAMEDjdeps & jlink (커스텀 런타임)
jdeps는 바이트코드를 분석하여 모듈 종속성을 나열합니다 — 모듈로 마이그레이션하고 사용하지 않는 종속성을 찾는 데 유용. jlink는 앱에 필요한 모듈만 포함하는 커스텀 JRE를 만들어, 자체 포함되고 더 작고 빠르게 시작하는 런타임을 생성합니다. 이것이 Docker 이미지와 설치 프로그램에 이상적입니다: 300MB JDK 설치를 요구하는 대신 앱과 함께 30-50MB JRE를 배포. 함께, jdeps + jlink가 린, 자체 포함 Java 배포를 가능하게 합니다.
# jdeps: analyze dependencies (find unused, list required modules)
# List dependencies of a JAR
jdeps --module-path lib app.jar
# Generate module-info.java for an existing JAR
jdeps --generate-module-info ./out app.jar
# Show which JDK modules a JAR uses
jdeps --print-module-deps --ignore-missing-deps app.jar
# Output: java.base,java.logging,java.sql
# jlink: create a custom stripped-down JRE containing only needed modules
jlink \
--module-path "$JAVA_HOME/jmods:./out" \
--add-modules com.example.app \
--output ./custom-jre \
--strip-debug \
--compress=zip-6 \
--no-header-files \
--no-man-pages \
--launcher app=com.example.app/com.example.app.Main
# The custom JRE is self-contained:
# ./custom-jre/bin/app # launches the app
# ./custom-jre/bin/java # the stripped JVM
# Benefits of jlink:
# - Smaller distribution (only needed modules)
# - Faster startup (less to load)
# - No need to install Java on target machine
# - Can cross-target (different OS/arch) with matching jmods
# Common workflow:
# 1. jdeps to find required modules
# 2. jlink to build a custom runtime
# 3. Package app + custom JRE together (Docker image, installer)동시성 심층
Lock: ReentrantLock & ReadWriteLock
ReentrantLock은 synchronized보다 더 많은 제어를 제공합니다: tryLock(비차단/시간제한), 공정성, 인터럽트 가능성, 잠금 상태 검사. 항상 finally에서 unlock. ReadWriteLock은 많은 동시 읽기 허용하지만 배타적 쓰기 — 읽기 많은 캐시에 훌륭. StampedLock(Java 8+)은 더 나은 읽기 처리량을 위해 낙관적 읽기를 추가. 단순한 경우에는 synchronized를 선호하세요; 고급 기능이 필요할 때 Lock을 사용하세요.
import java.util.concurrent.locks.*;
// ReentrantLock: more flexible than synchronized
class Counter {
private final ReentrantLock lock = new ReentrantLock();
private int count = 0;
public void increment() {
lock.lock();
try {
count++;
} finally {
lock.unlock(); // MUST be in finally
}
}
public int get() {
lock.lock();
try { return count; }
finally { lock.unlock(); }
}
}
// tryLock with timeout (avoids deadlock-induced hangs)
if (lock.tryLock(1, java.util.concurrent.TimeUnit.SECONDS)) {
try { /* work */ } finally { lock.unlock(); }
}
// Fair lock (FIFO ordering, slower)
ReentrantLock fair = new ReentrantLock(true);
// ReadWriteLock: many readers OR one writer
class Cache {
private final ReentrantReadWriteLock rw = new ReentrantReadWriteLock();
private final Lock read = rw.readLock();
private final Lock write = rw.writeLock();
private java.util.Map<String, String> data = new java.util.HashMap<>();
public String get(String key) {
read.lock();
try { return data.get(key); }
finally { read.unlock(); }
}
public void put(String key, String val) {
write.lock();
try { data.put(key, val); }
finally { write.unlock(); }
}
}동시성 컬렉션
ConcurrentHashMap은 일꾼 스레드 안전 맵입니다 — check-then-act 대신 compute/merge를 원자적 업데이트에 사용하세요. CopyOnWriteArrayList는 읽기 많고 쓰기 드문 리스트(이벤트 리스너)에 최적 — 쓰기가 배열을 복사. BlockingQueue는 생산자-소비자 파이프라인의 중춄입니다(put은 가득 차면 차단, take는 비어 있으면 차단). ConcurrentLinkedQueue는 비차단이고 무제한. 이것들이 조잡하고 느린 동기화 래퍼(Collections.synchronizedX)를 대체합니다.
import java.util.concurrent.*;
import java.util.*;
// ConcurrentHashMap: thread-safe HashMap (no null keys/values)
ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
map.put("a", 1);
map.putIfAbsent("a", 2); // only if absent
map.compute("a", (k, v) -> v == null ? 1 : v + 1); // atomic update
map.merge("a", 1, Integer::sum); // add 1 atomically
Integer val = map.getOrDefault("a", 0);
// CopyOnWriteArrayList: snapshot semantics, fast reads, slow writes
CopyOnWriteArrayList<String> listeners = new CopyOnWriteArrayList<>();
listeners.add("x"); // copies entire array
for (String l : listeners) { /* safe iteration, no ConcurrentModificationException */ }
// BlockingQueue: producer-consumer pattern
BlockingQueue<String> queue = new ArrayBlockingQueue<>(100);
// Producer
queue.put("task"); // blocks if full
boolean added = queue.offer("task", 1, TimeUnit.SECONDS); // timed
// Consumer
String task = queue.take(); // blocks if empty
String polled = queue.poll(1, TimeUnit.SECONDS);
// ConcurrentLinkedQueue: non-blocking, unbounded
Queue<String> q = new ConcurrentLinkedQueue<>();
q.offer("a");
// SkipListMap / SkipListSet: concurrent sorted collections
ConcurrentNavigableMap<Integer, String> sorted = new ConcurrentSkipListMap<>();
import java.util.concurrent.TimeUnit;CountDownLatch & CyclicBarrier
CountDownLatch는 일회용 게이트입니다 — N 스레드가 카운트다운하고, 다른 스레드가 대기; 재설정 불가. CyclicBarrier는 재사용 가능합니다 — 스레드가 랑데부 지점에서 서로를 기다리고, 모두 도착하면 선택적 액션. Phaser가 가장 유연합니다: 가변 파티, 다중 단계, 트리 구조. 시작 조정에는 latch, 병렬 다단계 알고리즘에는 barrier, 동적 참여자 수에는 phaser를 사용하세요.
import java.util.concurrent.*;
import java.util.*;
// CountDownLatch: one-shot gate, N threads must arrive before proceeding
CountDownLatch ready = new CountDownLatch(3);
List<String> results = Collections.synchronizedList(new ArrayList<>());
for (int i = 0; i < 3; i++) {
final int id = i;
new Thread(() -> {
try { Thread.sleep(id * 100); } catch (InterruptedException e) {}
results.add("worker-" + id);
ready.countDown(); // signal done
}).start();
}
ready.await(); // main thread blocks until count reaches 0
System.out.println("All done: " + results);
// CyclicBarrier: reusable barrier, threads wait for each other
CyclicBarrier barrier = new CyclicBarrier(3, () ->
System.out.println("--- phase complete ---"));
Runnable worker = () -> {
try {
System.out.println(Thread.currentThread().getName() + " phase 1");
barrier.await(); // wait for all 3
System.out.println(Thread.currentThread().getName() + " phase 2");
barrier.await(); // reusable for next phase
} catch (Exception e) {}
};
for (int i = 0; i < 3; i++) new Thread(worker).start();
// Phaser: more flexible (variable parties, multiple phases)
Phaser phaser = new Phaser(3);
phaser.register(); // dynamically add a party
phaser.arriveAndAwaitAdvance();Semaphore & Exchanger
Semaphore는 N 허가에 대한 접근을 제어합니다 — acquire는 사용 가능할 때까지 차단하고, release는 반환합니다. 속도 제한, 연결 풀, 또는 제한된 리소스 시나리오에 사용하세요. tryAcquire는 비차단과 시간제한 변형을 제공합니다. Exchanger는 두 스레드가 랑데부 지점에서 값을 교환하게 합니다 — 두 스레드가 버퍼를 교환하는 파이프라인 설계에 틈새이지만 우아합니다. 둘 다 java.util.concurrent에 있으며 일부 조정 패턴에 잠금보다 저수준입니다.
import java.util.concurrent.*;
// Semaphore: limit concurrent access to N permits
Semaphore pool = new Semaphore(5); // 5 concurrent allowed
pool.acquire(); // blocks until a permit is available
try {
// critical section (at most 5 threads here at once)
System.out.println("Working, permits left: " + pool.availablePermits());
} finally {
pool.release(); // return the permit
}
// Try-acquire (non-blocking)
if (pool.tryAcquire()) {
try { /* work */ } finally { pool.release(); }
} else {
System.out.println("Too busy, try later");
}
// Timed acquire
if (pool.tryAcquire(1, TimeUnit.SECONDS)) {
try { /* work */ } finally { pool.release(); }
}
// Use case: rate limiting, connection pools, parking lots
// Exchanger: two threads swap values
Exchanger<String> ex = new Exchanger<>();
new Thread(() -> {
try {
String got = ex.exchange("from-A"); // gives "from-A", receives "from-B"
System.out.println("A got: " + got);
} catch (InterruptedException e) {}
}).start();
String got = ex.exchange("from-B"); // gives "from-B", receives "from-A"
System.out.println("B got: " + got);
import java.util.concurrent.TimeUnit;CompletableFuture 고급
CompletableFuture는 Java의 Promise입니다 — thenApply(map), thenCompose(flatMap), thenCombine(두 개 zip)로 비동기 작업을 조합. allOf는 모두를, anyOf는 첫 번째를 대기. exceptionally는 에러에서 복구; handle은 둘 다 커버. orTimeout(Java 9+)은 너무 오래 걸리면 취소. 항상 명시적 executor를 전달하세요 — 기본 commonPool은 차단 작업 아래 굶을 수 있습니다. 이것이 Java의 반응형 스타일 비동기 코드의 기반입니다.
import java.util.concurrent.*;
import java.util.*;
ExecutorService pool = Executors.newFixedThreadPool(4);
// Async composition (like JS Promises)
CompletableFuture<String> future = CompletableFuture
.supplyAsync(() -> "hello", pool) // async supplier
.thenApply(String::toUpperCase) // transform
.thenCompose(s -> CompletableFuture.supplyAsync( // flatMap
() -> s + " world"))
.thenApply(s -> s + "!");
System.out.println(future.join()); // "HELLO world!"
// Combine two independent futures
CompletableFuture<Integer> a = CompletableFuture.supplyAsync(() -> 10);
CompletableFuture<Integer> b = CompletableFuture.supplyAsync(() -> 20);
CompletableFuture<Integer> sum = a.thenCombine(b, Integer::sum);
System.out.println(sum.join()); // 30
// Run multiple and wait for all (or any)
List<CompletableFuture<Integer>> futures = List.of(
CompletableFuture.supplyAsync(() -> 1, pool),
CompletableFuture.supplyAsync(() -> 2, pool),
CompletableFuture.supplyAsync(() -> 3, pool)
);
CompletableFuture<Void> all = CompletableFuture.allOf(
futures.toArray(new CompletableFuture[0]));
all.join();
List<Integer> results = futures.stream().map(CompletableFuture::join).toList();
// Error handling
CompletableFuture<String> safe = CompletableFuture
.supplyAsync(() -> { throw new RuntimeException("boom"); })
.exceptionally(ex -> "fallback: " + ex.getMessage());
System.out.println(safe.join()); // "fallback: boom"
// handle: both success and failure
CompletableFuture<String> handled = CompletableFuture
.supplyAsync(() -> "ok")
.handle((val, ex) -> ex == null ? val : "error");
// Timeout (Java 9+)
CompletableFuture<String> timed = CompletableFuture
.supplyAsync(() -> { Thread.sleep(5000); return "slow"; })
.orTimeout(1, TimeUnit.SECONDS)
.exceptionally(ex -> "timed out");가상 스레드 (Java 21)
가상 스레드(Java 21)는 JVM이 소수의 캐리어(플랫폼) 스레드 풀에서 스케줄링하는 경량 스레드입니다. 가상 스레드가 I/O에서 차단되면, 일시 중단되고 캐리어가 다른 것을 실행합니다 — 따라서 수백만 개의 동시 차단 연산을 가질 수 있습니다. 이것이 복잡한 반응형/비동기 체인 대신 단순한 차단 코드를 작성하게 합니다. I/O 바운드 작업(HTTP 핸들러, DB 호출)에 사용하세요; CPU 바운드 작업에는 플랫폼 스레드나 parallelStream이 여전히 적절합니다.
import java.util.concurrent.*;
import java.util.*;
// Virtual threads: lightweight, cheap, millions possible
// Java 21 LTS feature — "Project Loom"
// Start a virtual thread
Thread vt = Thread.ofVirtual().start(() -> {
System.out.println("Running on: " + Thread);
});
// Builder pattern
Thread vt2 = Thread.ofVirtual().name("worker-1").start(() -> {
// blocking I/O here is fine — virtual thread yields, not the OS thread
});
// Per-thread factory
ThreadFactory factory = Thread.ofVirtual().factory();
// ExecutorService for virtual threads (Java 21+)
try (ExecutorService es = Executors.newVirtualThreadPerTaskExecutor()) {
// Submit a million tasks — each gets its own virtual thread
List<Future<String>> futures = new ArrayList<>();
for (int i = 0; i < 1_000_000; i++) {
final int id = i;
futures.add(es.submit(() -> {
Thread.sleep(100); // blocking call is cheap on virtual threads
return "done-" + id;
}));
}
// virtual threads yield when blocking, so 1M concurrent is feasible
}
// Why virtual threads?
// - Platform threads (OS threads) are heavy (~1MB stack, kernel scheduling)
// - Virtual threads are user-mode, ~KB, scheduled by JVM on a small carrier pool
// - Blocking I/O on a virtual thread doesn't block a platform thread
// - Lets you write straightforward blocking code at scale (no reactive complexity)
// Best practice: use virtual threads for I/O-bound work,
// NOT for CPU-bound work (use platform threads / parallelStream).컬렉션 프레임워크 심층
Comparator & 정렬
Comparator.comparing(keyExtractor)는 키 함수에서 comparator를 구축합니다 — 원시 compare 로직을 작성하는 것보다 훨씬 깔끔. thenComparing은 보조 정렬 키를 체인. nullsFirst/nullsLast는 null을 안전하게 처리. 오토박싱을 피하려면 comparingInt/comparingLong/comparingDouble을 사용. List.sort()는 제자리 정렬(가변 리스트만); Stream.sorted()는 새 정렬된 스트림 반환. Comparator가 정렬, TreeSet/TreeMap 순서, stream 연산을 구동합니다.
import java.util.*;
import java.util.stream.*;
record Person(String name, int age) {}
List<Person> people = List.of(
new Person("Alice", 30),
new Person("Bob", 25),
new Person("Carol", 30),
new Person("Dave", 25)
);
// Comparator.comparing (key extractor)
people.stream().sorted(Comparator.comparing(Person::name)).toList();
people.stream().sorted(Comparator.comparingInt(Person::age)).toList();
// Reversed
people.stream().sorted(Comparator.comparing(Person::age).reversed()).toList();
// Chained (thenComparing): sort by age, then by name
List<Person> sorted = people.stream().sorted(
Comparator.comparingInt(Person::age)
.thenComparing(Person::name)
).toList();
// [Bob(25), Dave(25), Alice(30), Carol(30)]
// Nulls handling
Comparator<String> cmp = Comparator.nullsFirst(Comparator.naturalOrder());
List.of("b", null, "a").stream().sorted(cmp).toList(); // [null, a, b]
// Custom comparator
Comparator<Person> byNameLen = (a, b) -> a.name().length() - b.name().length();
// Mutable list sort
List<String> names = new ArrayList<>(List.of("charlie", "alice", "bob"));
names.sort(Comparator.naturalOrder());
// names = [alice, bob, charlie]
// Comparing with primitive specializations avoids boxing
Comparator<Person> byAge = Comparator.comparingInt(Person::age);수정 불가 & 불변 컬렉션
List.of/Set.of/Map.of(Java 9+)는 진정한 불변 컬렉션을 만듭니다 — null 없음, 변경 없음. Collections.unmodifiableX는 백업 컬렉션의 변경을 여전히 반영하는 읽기 전용 뷰를 만듭니다. List.copyOf(Java 10+)는 독립적인 불변 사본을 만듭니다. Arrays.asList는 배열의 고정 크기 뷰입니다(set은 작동, add/remove는 안 됨). 필요에 따라 선택하세요: 상수에는 불변 팩토리, 내부를 안전하게 노출에는 수정 불가 뷰, 방어적 사본에는 copyOf.
import java.util.*;
// Java 9+ immutable factories (List.of, Set.of, Map.of)
List<String> immutable = List.of("a", "b", "c");
Set<Integer> set = Set.of(1, 2, 3);
Map<String, Integer> map = Map.of("a", 1, "b", 2);
Map<String, Integer> bigMap = Map.ofEntries(
Map.entry("x", 1), Map.entry("y", 2), Map.entry("z", 3)
);
// immutable.add("d"); // throws UnsupportedOperationException
// immutable.set(0, "z"); // throws
// nulls not allowed in these immutable collections
// Unmodifiable view (wraps an existing collection)
List<String> mutable = new ArrayList<>(List.of("a", "b"));
List<String> view = Collections.unmodifiableList(mutable);
// view.add("c"); // throws
mutable.add("c"); // but changes to backing list ARE visible in view
System.out.println(view); // [a, b, c]
// CopyOf (Java 10+): creates immutable copy
List<String> copy = List.copyOf(mutable); // independent immutable copy
// Arrays.asList: fixed-size view of an array
String[] arr = {"a", "b"};
List<String> fixed = Arrays.asList(arr);
fixed.set(0, "x"); // OK (writes through to array)
// fixed.add("c"); // throws (size fixed)
// To make a truly mutable copy:
List<String> mut = new ArrayList<>(Arrays.asList(arr));Queue & Deque 구현
ArrayDeque가 선호되는 스택과 queue 구현입니다 — 레거시 Stack(동기화됨)과 LinkedList보다 빠릅니다. PriorityQueue는 Comparator로 요소를 정렬합니다(기본 최소 힙) — 스케줄링, top-K 문제에 사용. Deque는 양 끝을 지원; 스택 의미론에는 addFirst/removeFirst, queue 의미론에는 addLast/removeFirst를 사용. 동시 queue에는 java.util.concurrent 구현(LinkedBlockingQueue 등)을 사용하세요.
import java.util.*;
// Queue: FIFO (offer/poll/peek)
Queue<String> queue = new LinkedList<>();
queue.offer("a"); queue.offer("b");
System.out.println(queue.peek()); // "a" (head)
System.out.println(queue.poll()); // "a" (remove head)
// Deque: double-ended (add/remove at both ends)
Deque<String> deque = new ArrayDeque<>();
deque.addFirst("a"); deque.addLast("b");
System.out.println(deque.peekFirst()); // "a"
System.out.println(deque.peekLast()); // "b"
deque.removeFirst(); deque.removeLast();
// ArrayDeque as a stack (push/pop/peek)
Deque<String> stack = new ArrayDeque<>();
stack.push("first"); // addFirst
stack.push("second");
System.out.println(stack.pop()); // "second" (LIFO)
System.out.println(stack.peek()); // "first"
// PriorityQueue: orders by Comparator (not insertion order)
PriorityQueue<Integer> pq = new PriorityQueue<>();
pq.offer(5); pq.offer(1); pq.offer(3);
System.out.println(pq.poll()); // 1 (smallest first)
System.out.println(pq.poll()); // 3
// With custom comparator
PriorityQueue<String> byLen = new PriorityQueue<>(Comparator.comparingInt(String::length));
byLen.offer("aaa"); byLen.offer("a"); byLen.offer("aa");
System.out.println(byLen.poll()); // "a"
// BlockingQueue implementations (concurrency): see concurrency-deep section
// ArrayDeque is faster than Stack/LinkedList for stack/queue useMap merge, compute, getOrDefault
이 Map 메서드는 일반 패턴을 원자적이고 간결하게 만듭니다. merge는 단어 카운팅 관용구입니다 — 기존 값과 새 값을 결합하고, 함수가 null을 반환하면 항목 제거. computeIfAbsent는 지연 캐시 패턴(메모이제이션). getOrDefault는 null 검사를 피합니다. replaceAll은 모든 값을 변환. 이것들이 check-then-act get/put 춤보다 훨씬 깔끔하며, 동시 코드에서 ConcurrentHashMap의 원자적 업데이트의 빌딩 블록입니다.
import java.util.*;
Map<String, Integer> counts = new HashMap<>();
// getOrDefault: safe read with default
int n = counts.getOrDefault("missing", 0); // 0, no null
// putIfAbsent: only set if not present
counts.putIfAbsent("a", 1); // sets to 1
counts.putIfAbsent("a", 2); // no change (already present)
// compute: recompute value for a key
counts.compute("a", (k, v) -> v == null ? 1 : v + 1); // increment
// computeIfAbsent: lazy initialization (cache pattern)
Map<String, List<String>> groups = new HashMap<>();
groups.computeIfAbsent("key", k -> new ArrayList<>()).add("item");
// creates list if absent, then adds — atomic, no race in single thread
// computeIfPresent: update only if present
counts.computeIfPresent("a", (k, v) -> v > 0 ? v - 1 : null); // decrement, remove at 0
// merge: combine existing and new value (great for counting)
counts.merge("word", 1, Integer::sum); // word count pattern
counts.merge("word", 1, Integer::sum); // now 2
counts.merge("word", 1, Integer::sum); // now 3
// merge with removal: function returns null -> entry removed
counts.merge("word", 1, (old, v) -> old > 1 ? old - 1 : null);
// replaceAll: transform all values
Map<String, Integer> doubled = new HashMap<>(counts);
doubled.replaceAll((k, v) -> v * 2);
// Word frequency counter (idiomatic)
String text = "the cat the dog the bird";
Map<String, Integer> freq = new HashMap<>();
for (String w : text.split(" ")) {
freq.merge(w, 1, Integer::sum);
}Collections 유틸리티 메서드
Collections.*는 고전 유틸리티를 제공합니다: sort, binarySearch(정렬된 입력 필요), shuffle, reverse, frequency, min/max. singleton/empty 팩토리는 불변 단일 요소 또는 빈 컬렉션을 반환 — null 반환보다 emptyList()를 선호. nCopies는 메모리 효율적(하나의 요소 공유). 동기화 래퍼는 레거시 코드용이지만 java.util.concurrent 컬렉션을 선호. checked 래퍼는 런타임에 제네릭 타입 위반을 잡아, 원시 타입과의 상호 운용에 유용.
import java.util.*;
import java.util.stream.*;
List<Integer> nums = new ArrayList<>(List.of(3, 1, 4, 1, 5, 9, 2, 6));
// Sorting
Collections.sort(nums); // in-place, natural order
Collections.sort(nums, Comparator.reverseOrder());
nums.sort(Comparator.naturalOrder()); // List.sort (preferred)
// Searching (list must be sorted first)
int idx = Collections.binarySearch(nums, 4); // index, or negative if absent
// Shuffling & reversing
Collections.shuffle(nums);
Collections.reverse(nums);
// Frequency & disjoint
int freq = Collections.frequency(nums, 1); // count occurrences
boolean dis = Collections.disjoint(List.of(1, 2), List.of(3, 4)); // true
// Min/max
int min = Collections.min(nums);
int max = Collections.max(nums, Comparator.reverseOrder());
// Singleton collections (immutable, single element)
Set<String> one = Collections.singleton("only");
List<Integer> oneList = Collections.singletonList(42);
Map<String, Integer> oneMap = Collections.singletonMap("k", 1);
// Empty collections (prefer over returning null)
List<Object> empty = Collections.emptyList();
Set<Object> emptySet = Collections.emptySet();
// nCopies (immutable list of n copies)
List<String> padding = Collections.nCopies(5, "x"); // [x,x,x,x,x]
// Synchronized wrappers (legacy — prefer concurrent collections)
List<String> sync = Collections.synchronizedList(new ArrayList<>());
// Checked wrappers (catch heap pollution at runtime)
List<String> checked = Collections.checkedList(new ArrayList<>(), String.class);
// checked.add(123); // throws ClassCastException at the add site테스팅 (JUnit 5 & Mockito)
JUnit 5 기본
JUnit 5(Jupiter) 어노테이션: @Test는 테스트를 표시; @BeforeEach/@AfterEach는 각 테스트 전후에 실행; @BeforeAll/@AfterAll은 클래스에 대해 한 번 실행(정적이어야 함). @DisplayName은 테스트 이름을 커스터마이징. @Disabled는 테스트를 건너뜀. assertThrows는 예외를 검증. 테스트는 독립적이어야 합니다 — 상태를 재설정하려면 정적 필드가 아닌 @BeforeEach를 사용. JUnit 5는 org.junit.jupiter.api에 있습니다(JUnit 4의 org.junit과 다름).
import org.junit.jupiter.api.*;
import static org.junit.jupiter.api.Assertions.*;
class CalculatorTest {
private Calculator calc;
@BeforeAll
static void setUpOnce() {
// runs once before all tests (must be static)
System.out.println("Starting CalculatorTest");
}
@BeforeEach
void setUp() {
// runs before each test
calc = new Calculator();
}
@Test
@DisplayName("2 + 2 should equal 4")
void addsTwoNumbers() {
assertEquals(4, calc.add(2, 2));
}
@Test
void dividesByZeroThrows() {
ArithmeticException ex = assertThrows(
ArithmeticException.class,
() -> calc.divide(1, 0)
);
assertEquals("Division by zero", ex.getMessage());
}
@AfterEach
void tearDown() {
// runs after each test (cleanup)
calc = null;
}
@AfterAll
static void tearDownOnce() {
// runs once after all tests
}
@Disabled("until bug #42 is fixed")
@Test
void skippedTest() {}
}
class Calculator {
int add(int a, int b) { return a + b; }
int divide(int a, int b) {
if (b == 0) throw new ArithmeticException("Division by zero");
return a / b;
}
}어설션
JUnit 5 어설션: assertEquals/assertNotEquals, assertTrue/False, assertNull/NotNull, assertSame(동일성). assertAll은 일부가 실패해도 모두 실행되도록 그룹화. assertTimeout은 느린 테스트를 실패. 메시지는 문자열이나 Supplier(지연 — 테스트가 통과할 때 문자열 연결을 피함)일 수 있습니다. assertIterableEquals는 정렬된 컬렉션을 비교. 이것들은 org.junit.jupiter.api.Assertions에서 옵니다.
import org.junit.jupiter.api.*;
import static org.junit.jupiter.api.Assertions.*;
class AssertionsTest {
@Test
void equality() {
assertEquals(4, 2 + 2);
assertEquals(4, 2 + 2, "math is broken");
assertNotEquals(5, 2 + 2);
}
@Test
void booleans() {
assertTrue(5 > 3);
assertFalse(5 < 3, "5 should not be less than 3");
}
@Test
void nullness() {
assertNull(null);
assertNotNull(new Object());
}
@Test
void sameInstance() {
String a = "x";
assertSame(a, a); // ==
assertNotSame(a, new String("x"));
}
@Test
void collections() {
assertIterableEquals(List.of(1, 2, 3), List.of(1, 2, 3));
assertLinesMatch(List.of("a.*", "b"), List.of("abc", "b"));
}
@Test
void groupedAssertions() {
// All run even if one fails — reports all failures
assertAll("person",
() -> assertEquals("Alice", "Alice"),
() -> assertEquals(30, 30),
() -> assertNotNull("x")
);
}
@Test
void timeout() {
// Fails if it takes longer than 100ms
assertTimeout(Duration.ofMillis(100), () -> {
Thread.sleep(10);
});
// assertTimeoutPreemptively: stops the task early (in another thread)
}
@Test
void customMessage() {
int result = 5;
assertEquals(4, result, () -> "expected 4 but got " + result);
// Supplier<String> — message built lazily only on failure
}
import java.time.Duration;
import java.util.List;
}매개변수화 테스트
매개변수화 테스트는 같은 테스트 로직을 여러 입력으로 실행합니다. @ValueSource는 단일 인수 배열을 제공. @CsvSource는 CSV 행을 여러 매개변수로 매핑. @MethodSource(가장 유연)는 정적 Stream<Arguments>를 사용. @EnumSource는 열거형 값을 순회. @NullAndEmptySource는 null/빈 케이스를 추가. 이것이 복사-붙여넣기 테스트 메서드를 제거하고 데이터 중심 테스팅을 깔끔하게 만듭니다. 표시 이름은 쉬운 진단을 위해 매개변수를 자동 포함합니다.
import org.junit.jupiter.params.*;
import org.junit.jupiter.params.provider.*;
import static org.junit.jupiter.api.Assertions.*;
class ParameterizedTests {
@ParameterizedTest
@ValueSource(ints = {1, 2, 3, 4, 5})
void positiveNumbersArePositive(int n) {
assertTrue(n > 0);
}
@ParameterizedTest
@ValueSource(strings = {"", " ", "\t"})
void blankStrings(String s) {
assertTrue(s.isBlank());
}
@ParameterizedTest
@NullAndEmptySource
@ValueSource(strings = {" ", "\t"})
void blankOrNull(String s) {
assertTrue(s == null || s.isBlank());
}
@ParameterizedTest
@CsvSource({
"1, 1, 2",
"2, 3, 5",
"10, -5, 5"
})
void addition(int a, int b, int expected) {
assertEquals(expected, a + b);
}
@ParameterizedTest
@CsvFileSource(resources = "/testdata.csv", numLinesToSkip = 1)
void fromCsv(int a, int b, int expected) {
assertEquals(expected, a + b);
}
@ParameterizedTest
@MethodSource("additionProvider")
void fromMethod(int a, int b, int expected) {
assertEquals(expected, a + b);
}
static java.util.stream.Stream<Arguments> additionProvider() {
return java.util.stream.Stream.of(
Arguments.of(1, 1, 2),
Arguments.of(2, 3, 5)
);
}
@ParameterizedTest
@EnumSource(TimeUnit.class)
void allEnums(TimeUnit unit) {
assertNotNull(unit);
}
}라이프사이클, Nested & 조건부
@Nested는 라이프사이클을 공유하는 내부 테스트 클래스를 만듭니다 — 외부 설정이 내부 테스트에 적용되는 BDD 스타일 'when X then Y' 구조에 훌륭. 조건부 어노테이션(@EnabledOnOs, @EnabledIfSystemProperty, @EnabledIfEnvironmentVariable)은 환경을 기반으로 테스트를 건너뜀. @Tag는 선택적 실행을 위해 테스트를 그룹화(예: 빠름 vs 느림, 단위 vs 통합). Nested 클래스는 @BeforeAll을 가질 수 없습니다(비-정적이므로). 이 기능들이 테스트 조직을 표현적으로 만듭니다.
import org.junit.jupiter.api.*;
import static org.junit.jupiter.api.Assertions.*;
import org.junit.jupiter.api.condition.*;
class LifecycleTest {
@Test
void topLevel() {}
@Nested
@DisplayName("when stack is empty")
class WhenEmpty {
@BeforeEach
void setUp() { /* stack is empty here */ }
@Test
void throwsOnPop() {
assertThrows(Exception.class, () -> {});
}
@Nested
@DisplayName("after pushing one element")
class AfterPush {
@BeforeEach
void push() { /* push one */ }
@Test
void popReturnsElement() {
// ...
}
}
}
// Conditional execution
@Test
@EnabledOnOs(OS.LINUX)
void onlyOnLinux() {}
@Test
@EnabledIfSystemProperty(named = "env", matches = "ci")
void onlyInCi() {}
@Test
@EnabledIfEnvironmentVariable(named = "DATABASE", matches = "postgres")
void onlyWithPostgres() {}
@Test
@DisabledIf("customCondition")
void conditional() {}
static boolean customCondition() { return java.time.LocalTime.now().getHour() < 9; }
// Tagging for selective runs
@Test
@Tag("slow")
void slowIntegrationTest() {}
@Test
@Tag("fast")
void fastUnitTest() {}
// Run only fast: mvn test -Dgroups=fast
import org.junit.jupiter.api.condition.OS;
import java.util.concurrent.TimeUnit;Mockito 모킹
Mockito는 종속성을 위한 테스트 더블을 만듭니다. @Mock은 모의를 만들고; @InjectMocks은 모의가 주입된 실제 객체를 구축. when(...).thenReturn(...)은 반환 값을 스텁; verify(...)는 상호작용을 검사. 인수 매처(any(), eq(), argThat())는 호출을 유연하게 매치. Spy는 실제 객체를 감쌉니다(부분 모킹). Arrange-Act-Assert 패턴이 테스트를 읽기 쉽게 유지. 모킹은 테스트 중인 단위를 종속성(데이터베이스, 네트워크, 시간)에서 격리합니다.
import org.junit.jupiter.api.*;
import org.mockito.*;
import static org.mockito.Mockito.*;
import static org.junit.jupiter.api.Assertions.*;
class UserServiceTest {
@Mock
UserRepository repo; // mock dependency
@InjectMocks
UserService service; // real service with mocks injected
@BeforeEach
void setUp() {
MockitoAnnotations.openMocks(this);
}
@Test
void returnsUserWhenFound() {
// Arrange: stub the mock
User alice = new User(1, "Alice");
when(repo.findById(1)).thenReturn(alice);
// Act
String name = service.getUserName(1);
// Assert
assertEquals("Alice", name);
verify(repo).findById(1); // called once
verify(repo, never()).findById(2);
}
@Test
void throwsWhenNotFound() {
when(repo.findById(99)).thenReturn(null);
// or: when(repo.findById(99)).thenThrow(new RuntimeException());
assertThrows(RuntimeException.class, () -> service.getUserName(99));
}
@Test
void argumentMatchers() {
when(repo.findById(anyInt())).thenReturn(new User(0, "default"));
// matchers: eq(), any(), anyInt(), contains(), argThat()
service.getUserName(42);
verify(repo).findById(intThat(n -> n > 0));
}
@Test
void verifyInteractionDetails() {
service.getUserName(1);
verify(repo, times(1)).findById(1);
verify(repo, atLeastOnce()).findById(anyInt());
verifyNoMoreInteractions(repo);
}
@Test
void spy_partialMock() {
List<String> spy = spy(new ArrayList<>());
spy.add("real");
when(spy.size()).thenReturn(100); // stub one method
assertEquals(100, spy.size()); // stubbed
assertEquals(1, spy.size()); // wait, this would be 100 too
}
}
interface UserRepository { User findById(int id); }
record User(int id, String name) {}
class UserService {
UserRepository repo;
UserService(UserRepository r) { repo = r; }
String getUserName(int id) {
User u = repo.findById(id);
if (u == null) throw new RuntimeException("not found");
return u.name();
}
}JUnit 테스팅
기본 테스트
JUnit 5는 org.junit.jupiter.api의 @Test를 사용합니다. assertEquals는 예상이 실제와 같음을 검증. 다른 어설션: assertTrue, assertThrows, assertAll.
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
class CalcTest {
@Test
void testAdd() {
assertEquals(5, calc.add(2, 3));
}
}매개변수화 테스트
매개변수화 테스트는 같은 테스트를 다른 입력으로 실행합니다. @ValueSource는 단일 인수를 제공. @CsvSource는 여러 인수를 제공. 테스트 중복을 줄입니다.
@ParameterizedTest
@ValueSource(ints = {1, 2, 3, 4})
void testPositive(int n) { assertTrue(n > 0); }
@ParameterizedTest
@CsvSource({"1,2,3", "4,5,9"})
void testAdd(int a, int b, int expected) {
assertEquals(expected, calc.add(a, b));
}라이프사이클 메서드
@BeforeAll/@AfterAll은 클래스당 한 번 실행(정적이어야 함). @BeforeEach/@AfterEach는 각 테스트 전후에 실행. 데이터베이스 연결과 모의 설정에 사용.
class DbTest {
@BeforeAll static void setupAll() { /* once before all */ }
@AfterAll static void tearDownAll() { /* once after all */ }
@BeforeEach void setup() { /* before each test */ }
@AfterEach void tearDown() { /* after each test */ }
}어설션
assertAll은 일부가 실패해도 모든 어설션을 실행. assertThrows는 코드가 특정 예외를 던지는지 검증. 시간 제한 테스트에는 assertTimeout을 사용.
@Test
void testAll() {
assertAll("person",
() -> assertEquals("Alice", p.getName()),
() -> assertEquals(30, p.getAge())
);
}
@Test
void testException() {
assertThrows(IllegalArgumentException.class, () -> service.process(-1));
}Mockito
@Mock은 모의 객체를 만들고, @InjectMocks은 주입합니다. when().thenReturn()이 호출을 스텁. verify()는 메서드가 호출되었는지 검사. Mockito가 표준 모킹 프레임워크입니다.
@ExtendWith(MockitoExtension.class)
class UserServiceTest {
@Mock UserRepository repo;
@InjectMocks UserService service;
@Test
void testFind() {
when(repo.findById(1)).thenReturn(Optional.of(new User("Alice")));
assertEquals("Alice", service.findUser(1).getName());
verify(repo).findById(1);
}
}Maven/Gradle
Maven POM
Maven은 pom.xml을 사용. groupId/artifactId/version이 프로젝트를 식별. 종속성은 scope(compile, test, provided)를 가집니다. Maven은 표준 디렉토리 구조를 강제.
<project>
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>myapp</artifactId>
<version>1.0.0</version>
<dependencies>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.10.0</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>Maven 명령
Maven 라이프사이클: clean, compile, test, package, install, deploy. 각 단계는 선행 단계를 실행. 테스트를 건너뛰려면 -DskipTests 사용.
mvn clean # Remove target/
mvn compile # Compile sources
mvn test # Run tests
mvn package # Build JAR
mvn install # Install to local repo
mvn dependency:tree # Show dependency treeGradle 빌드
Gradle은 build.gradle(Groovy) 또는 build.gradle.kts(Kotlin) 사용. 컴파일 종속성은 implementation, 테스트는 testImplementation. Gradle이 Maven보다 빠름.
plugins { id 'java'; id 'application' }
repositories { mavenCentral() }
dependencies {
implementation 'com.google.guava:guava:32.1.3-jre'
testImplementation 'org.junit.jupiter:junit-jupiter:5.10.0'
}
application { mainClass = 'com.example.App' }Gradle 명령
Gradle 작업: build, test, run, clean. 래퍼(gradlew)가 일관된 버전을 보장. 병렬 모듈 빌드를 위해 --parallel 사용.
gradle clean # Clean build
gradle build # Build + test
gradle test # Run tests
gradle run # Run application
gradle bootRun # Run Spring Boot
gradle dependencies # Show dependency tree다중 모듈 프로젝트
다중 모듈 프로젝트는 대형 앱을 분할. settings.gradle이 모듈을 나열. project(:core)가 모듈 간 종속성 생성. 각 모듈은 자체 build.gradle을 가짐.
// settings.gradle
include 'core', 'web', 'api'
// build.gradle (root)
subprojects {
apply plugin: 'java'
repositories { mavenCentral() }
}
// In web/build.gradle
dependencies { implementation project(':core') }Spring 기본
Spring Boot 앱
@SpringBootApplication은 자동 구성, 컴포넌트 스캔, 구성을 활성화. SpringApplication.run이 내장 서버를 시작. XML 구성을 제거.
@SpringBootApplication
public class App {
public static void main(String[] args) {
SpringApplication.run(App.class, args);
}
}REST 컨트롤러
@RestController는 @Controller와 @ResponseBody를 결합. @GetMapping, @PostMapping은 단축키. @PathVariable은 URL 매개변수를 추출하고, @RequestBody는 JSON을 바인딩.
@RestController
@RequestMapping("/api/users")
public class UserController {
@GetMapping("/{id}")
public User getUser(@PathVariable Long id) {
return userService.findById(id);
}
@PostMapping
public User create(@RequestBody User user) {
return userService.save(user);
}
}의존성 주입
@Autowired는 종속성을 주입. 생성자 주입이 권장됨(테스트 가능, 불변). @Service, @Repository, @Component는 주입을 위한 스테레오타입.
@Service
public class UserService {
private final UserRepository repo;
@Autowired // Constructor injection (recommended)
public UserService(UserRepository repo) {
this.repo = repo;
}
}구성
@Configuration은 구성 클래스를 표시. @Bean은 Spring이 관리하는 빈을 선언. @Primary는 빈을 우선으로 만듦. 서드파티 클래스에 사용.
@Configuration
public class AppConfig {
@Bean
public RestTemplate restTemplate() {
return new RestTemplate();
}
@Bean @Primary
public DataSource primaryDataSource() {
return DataSourceBuilder.create().build();
}
}애플리케이션 속성
application.properties는 Spring Boot를 구성. 프로필은 환경별 구성을 활성화. spring.profiles.active=dev로 활성화. @Value 또는 @ConfigurationProperties 사용.
# application.properties
server.port=8080
spring.datasource.url=jdbc:postgresql://localhost/mydb
spring.jpa.hibernate.ddl-auto=update
# Profile-specific
# application-dev.properties
server.port=9090JDBC 심층
Connection & Statement
DriverManager.getConnection이 연결을 설정. Statement는 정적 SQL을 실행. ResultSet은 결과를 순회. 항상 리소스를 닫거나 try-with-resources 사용.
Connection conn = DriverManager.getConnection(url, user, pass);
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery("SELECT * FROM users");
while (rs.next()) {
System.out.println(rs.getString("name"));
}
rs.close(); stmt.close(); conn.close();PreparedStatement
PreparedStatement는 쿼리를 매개변수화하여 SQL 인젝션을 방지. 인덱스(1 기반)로 값을 설정. try-with-resources가 자동으로 닫음. 사전 컴파일로 성능 향상.
String sql = "INSERT INTO users (name, email) VALUES (?, ?)";
try (PreparedStatement ps = conn.prepareStatement(sql)) {
ps.setString(1, "Alice");
ps.setString(2, "[email protected]");
ps.executeUpdate();
}트랜잭션 관리
setAutoCommit(false)가 트랜잭션을 시작. commit은 저장, rollback은 취소. 어떤 문이 실패하면 무결성을 유지하기 위해 rollback. Spring에서는 @Transactional이 처리.
conn.setAutoCommit(false);
try {
stmt.executeUpdate("UPDATE accounts SET bal = bal - 100 WHERE id = 1");
stmt.executeUpdate("UPDATE accounts SET bal = bal + 100 WHERE id = 2");
conn.commit();
} catch (SQLException e) {
conn.rollback();
}연결 풀링
연결 풀링은 연결을 재사용. HikariCP가 가장 빠른 풀. maximumPoolSize가 동시 연결을 제한. 항상 닫기(풀로 반환). Spring Boot는 HikariCP를 자동 구성.
HikariConfig config = new HikariConfig();
config.setJdbcUrl(url);
config.setMaximumPoolSize(10);
HikariDataSource ds = new HikariDataSource(config);
Connection conn = ds.getConnection();ResultSet 메타데이터
ResultSetMetaData는 결과 구조를 설명: 열 이름, 타입, 속성. 일반 데이터 접근에 유용. 열 인덱스는 1 기반.
ResultSet rs = stmt.executeQuery("SELECT * FROM users");
ResultSetMetaData meta = rs.getMetaData();
int cols = meta.getColumnCount();
for (int i = 1; i <= cols; i++)
System.out.println(meta.getColumnName(i) + ": " + meta.getColumnTypeName(i));동시성 유틸리티
ExecutorService
ExecutorService는 스레드 풀을 관리. submit은 비동기 결과를 위한 Future를 반환. get은 완료될 때까지 차단(타임아웃 포함). 항상 executor를 종료.
ExecutorService executor = Executors.newFixedThreadPool(4);
Future<String> future = executor.submit(() -> {
Thread.sleep(1000);
return "Result";
});
String result = future.get(5, TimeUnit.SECONDS);
executor.shutdown();CompletableFuture
CompletableFuture는 함수형 비동기 프로그래밍을 가능하게. supplyAsync는 ForkJoinPool에서 실행. thenApply는 변환, thenAccept는 소비, exceptionally는 에러 처리.
CompletableFuture.supplyAsync(() -> fetchData())
.thenApply(data -> process(data))
.thenAccept(result -> System.out.println(result))
.exceptionally(ex -> { ex.printStackTrace(); return null; });동시성 컬렉션
ConcurrentHashMap은 전체 잠금 없이 스레드 안전. CopyOnWriteArrayList는 쓰기 시 복사(읽기 많음). BlockingQueue는 생산자-소비자 패턴을 지원.
ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
map.computeIfAbsent("b", k -> k.length());
CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();
BlockingQueue<Task> queue = new LinkedBlockingQueue<>(100);CountDownLatch & CyclicBarrier
CountDownLatch는 N 스레드를 대기(일회용). CyclicBarrier는 N 스레드를 대기 후 재설정(재사용 가능). 시작에는 latch, 단계별 계산에는 barrier 사용.
CountDownLatch latch = new CountDownLatch(3);
for (int i = 0; i < 3; i++)
new Thread(() -> { work(); latch.countDown(); }).start();
latch.await(); // Wait for all
CyclicBarrier barrier = new CyclicBarrier(3, () -> System.out.println("All ready"));Atomic 변수
Atomic 변수는 잠금 없는 스레드 안전 연산을 제공. compareAndSet은 낙관적 잠금을 가능하게. LongAdder는 높은 경합 카운터에 AtomicLong보다 빠름.
AtomicInteger counter = new AtomicInteger(0);
counter.incrementAndGet();
counter.compareAndSet(0, 1);
LongAdder adder = new LongAdder();
adder.increment();JVM 내부
메모리 영역
JVM 메모리: Heap(객체, GC 관리), Stack(메서드 호출, 스레드별), Metaspace(클래스 메타데이터). Young Gen은 복사 GC, Old Gen은 mark-sweep-compact 사용.
// Heap: objects and arrays (shared)
// - Young Gen: Eden, S0, S1
// - Old Gen: long-lived objects
// Stack: method frames (per thread)
// Metaspace: class metadata (Java 8+)
// JVM flags: -Xms512m -Xmx2g클래스 로딩
클래스 로딩은 지연. Bootstrap 이 핵심 Java 로드, Extension이 확장 로드, Application이 클래스패스 로드. 정적 초기화자는 한 번 실행. 커스텀 클래스로더가 핫 리로드를 가능하게.
// Bootstrap -> Extension -> Application classloaders
class MyClass {
static { System.out.println("Static init"); }
}
// Class.forName("MyClass") triggers loading
// -verbose:class shows class loading바이트코드
Java는 바이트코드로 컴파일(스택 기반). javap -c가 클래스 파일을 역어셈블. 각 명령은 피연산자 스택에 push/pop. Java 에이전트가 로드 시 바이트코드를 수정할 수 있음.
// javap -c MyClass.class
// Method int add(int, int):
// iload_1 // Load local var 1
// iload_2 // Load local var 2
// iadd // Add
// ireturn // Return intJIT 컴파일
JIT는 자주 실행되는 바이트코드를 네이티브 코드로 컴파일. 계층형 컴파일이 시작과 최고 성능의 균형. 핫 메서드는 인라인되고 최적화됨.
// JIT compiles hot methods to native code
// -XX:+PrintCompilation // Show JIT activity
// -XX:CompileThreshold=10000 // Method call count
// Tiered: Interpreter -> C1 -> C2스레드 덤프
스레드 덤프는 모든 스레드 상태와 스택 트레이스를 표시. 교착 상태와 멈춤 디버깅에 필수. jstack이 명령줄 도구. BLOCKED와 WAITING 스레드를 찾으세요.
// Get thread dump
jstack <pid>
// Or: kill -3 <pid>
// Deadlock detection
jstack -l <pid> | grep -A 20 "Found deadlock"가비지 컬렉션
GC 알고리즘
Serial GC는 작은 앱용. Parallel GC는 처리량을 최대화. G1 GC는 처리량과 지연의 균형(기본값). ZGC는 대형 힙에 서브-밀리초 일시 정지를 제공.
# Serial GC (single-threaded)
-XX:+UseSerialGC
# Parallel GC (throughput)
-XX:+UseParallelGC
# G1 GC (balanced, default in Java 9+)
-XX:+UseG1GC
# ZGC (low-latency)
-XX:+UseZGCG1 GC 튜닝
G1은 힙을 영역으로 분할. MaxGCPauseMillis는 소프트 일시 정지 목표를 설정. G1은 가장 많은 가비지가 있는 영역을 우선. GCViewer나 GCEasy로 로그 분석.
# Set heap
-Xms4g -Xmx4g
# Max GC pause target
-XX:MaxGCPauseMillis=200
# Region size (1-32MB)
-XX:G1HeapRegionSize=16m
# Enable GC logging
-Xlog:gc*:file=gc.log:time,uptime메모리 누수
메모리 누수는 의도치 않은 객체 유지로 발생. 정적 컬렉션, 닫히지 않은 리소스, 리스너 등록이 일반적. jmap이 객체 수를 표시. MAT로 hprof 분석.
// Common leak: static collections
static Map<String, Object> cache = new HashMap<>();
// Objects never removed -> leak
// Detect with:
jmap -histo <pid> | head -20
// Heap dump:
jmap -dump:format=b,file=heap.hprof <pid>약한 참조
WeakReference는 강한 참조가 없을 때 GC를 허용. SoftReference는 메모리 압력까지 생존. WeakHashMap 키는 약함. GC를 방해하지 않아야 하는 캐시에 사용.
WeakReference<Object> weakRef = new WeakReference<>(new Object());
SoftReference<byte[]> softRef = new SoftReference<>(new byte[1024]);
WeakHashMap<Object, String> map = new WeakHashMap<>();
map.put(key, "value"); // Entry removed when key is GC'd종료화
finalize()는 더 이상 사용되지 않음(예측 불가, 느림). Cleaner API(Java 9+)가 더 나은 정리를 제공. 결정적 정리를 위해 try-with-resources가 선호됨.
// Cleaner API (Java 9+)
class Resource implements AutoCloseable {
private final Cleaner.Cleanable cleanable;
Resource() {
cleanable = Cleaner.create().register(this, () -> cleanup());
}
public void close() { cleanable.clean(); }
}Stream Collector
Grouping By
groupingBy는 분류자로 요소를 분할. 두 번째 인수는 집계를 위한 다운스트림 collector. counting, averaging, summing이 일반적인 다운스트림 collector.
Map<String, List<Person>> byCity =
people.stream().collect(Collectors.groupingBy(Person::getCity));
Map<String, Long> countByCity =
people.stream().collect(Collectors.groupingBy(
Person::getCity, Collectors.counting()));분할
partitioningBy는 두 그룹(true/false)으로 분할. 불리언 키에 대해 groupingBy보다 효율적. 결과는 항상 두 키를 가짐. 다운스트림 collector가 각 파티션을 집계.
Map<Boolean, List<Person>> partition =
people.stream().collect(Collectors.partitioningBy(p -> p.getAge() >= 18));
// {false=[minors], true=[adults]}
Map<Boolean, Long> count =
people.stream().collect(Collectors.partitioningBy(
p -> p.getAge() >= 18, Collectors.counting()));Joining
joining은 선택적 구분자, 접두사, 접미사로 문자열을 연결. 요소는 문자열이어야 함; 먼저 map 사용. 내부적으로 StringBuilder 사용.
String names = people.stream()
.map(Person::getName)
.collect(Collectors.joining(", "));
// "Alice, Bob, Charlie"
String csv = people.stream()
.map(p -> p.getName() + "=" + p.getAge())
.collect(Collectors.joining("\n", "[", "]"));Reducing
reducing는 폴드 연산을 수행. 세 인수 버전은 identity, mapper, reducer를 받음. 두 인수 버전은 Optional 반환. 표준 collector가 부족할 때 사용.
int totalAge = people.stream()
.collect(Collectors.reducing(0, Person::getAge, Integer::sum));
Optional<Person> oldest = people.stream()
.collect(Collectors.reducing((p1, p2) ->
p1.getAge() > p2.getAge() ? p1 : p2));커스텀 Collector
Collector.of는 커스텀 collector를 생성: supplier, accumulator, combiner, finisher. combiner는 병렬 스트림의 부분 결과를 병합. 특수 출력 형식에 유용.
Collector<Person, ?, String> toJson = Collector.of(
StringBuilder::new,
(sb, p) -> sb.append(`{"name":"${p.getName()}"}`),
StringBuilder::append,
StringBuilder::toString
);일반적인 함정
Integer 캐싱
Java는 -128에서 127까지 Integer 값을 캐시. ==는 값이 아닌 참조를 비교. 캐시 범위 밖의 Integer는 ==가 false 반환. Integer에는 항상 .equals() 사용.
Integer a = 127; Integer b = 127;
System.out.println(a == b); // true (cached)
Integer c = 128; Integer d = 128;
System.out.println(c == d); // false (not cached)
System.out.println(c.equals(d)); // true