JAVA · 심층 가이드
Java 기초 문법 완전 정리
오토박싱과 문자열 풀 같은 오래된 규칙부터 record, sealed, switch 패턴 매칭까지 한 파일에 공존하는 현대 Java 문법을 29개 주제로 훑습니다.
지금의 Java는 사실상 두 개의 언어가 겹쳐 있습니다. 아래층에는 원시 타입과 래퍼 클래스의 이중 구조, 문자열 상수 풀, 제네릭 이레이저처럼 초기 설계에서 굳어진 규칙이 그대로 남아 있고, 위층에는 record, sealed, 텍스트 블록, switch 패턴 매칭처럼 최근 몇 개 릴리스에서 들어온 문법이 쌓여 있습니다. 실무 코드베이스는 이 둘이 한 파일에 섞여 있어서, 최신 문법만 익히면 남이 짠 코드를 못 읽고 옛 문법만 알면 새 코드를 못 씁니다.
묶어서 읽으면 효율이 좋은 조합이 있습니다. 원시 타입 vs 래퍼 클래스 심화와 자동 박싱과 언박싱은 한 덩어리이고, 문자열 풀과 intern()은 StringBuilder와 StringBuffer와 이어집니다. 타입 판별 쪽은 instanceof 패턴 매칭을 먼저 보고 switch 심화 — 패턴 매칭으로 넘어가야 흐름이 자연스럽습니다. 여기에 Record 클래스를 붙이면 분기문이 타입 계층 위에서 완전성 검사를 받습니다.
가장 악명 높은 함정은 래퍼 비교입니다. Integer는 -128부터 127까지를 캐시해 두기 때문에, 작은 값에서는 == 비교가 우연히 true가 되고 128부터 갑자기 false가 됩니다. 테스트를 작은 숫자로만 짜면 이 버그는 운영에서 처음 드러납니다. 값 비교는 예외 없이 equals로 하고, Long 키를 int로 조회하는 실수도 같이 조심해야 합니다. 박싱된 null을 원시 타입에 대입할 때 터지는 NPE도 이 구조에서 나옵니다.
01기본 타입 & 래퍼 클래스 & 오토박싱
Java의 원시 타입(primitive)과 래퍼 클래스(Wrapper)의 차이, 오토박싱/언박싱의 원리.
Java code
public class Types {
public static void main(String[] args) {
// 원시 타입 (primitive)
int i = 42;
long l = 100_000_000L;
double d = 3.141592653589793;
float f = 3.14f;
boolean b = true;
char c = 'A';
byte bt = 127;
short s = 32767;
// 래퍼 클래스 (Wrapper)
Integer boxed = 100; // 오토박싱
int unboxed = boxed; // 언박싱
// Integer 캐시: -128~127은 같은 객체
Integer a = 127;
Integer b2 = 127;
System.out.println(a == b2); // true (캐시)
Integer c2 = 128;
Integer d2 = 128;
System.out.println(c2 == d2); // false! (새 객체)
System.out.println(c2.equals(d2)); // true (값 비교)
// 유용한 래퍼 메서드
System.out.println(Integer.MAX_VALUE); // 2147483647
System.out.println(Integer.toBinaryString(42)); // 101010
System.out.println(Integer.parseInt("FF", 16)); // 255
System.out.println(Double.isNaN(0.0 / 0.0)); // true
// 타입 캐스팅
double pi = 3.99;
int truncated = (int) pi; // 3 (소수점 버림)
long bigNum = 1_000_000_000L;
int overflow = (int) bigNum; // 오버플로우 주의!
}
}Integer == Integer는 -128~127 범위에서만 true를 반환합니다 (캐시). 래퍼 클래스 비교는 항상 .equals()를 사용하세요.
반복문 안에서 오토박싱을 남용하면 성능 저하가 심각합니다. Long sum = 0L; for(...) sum += i;는 매 반복마다 Long 객체를 생성합니다.
02String & StringBuilder & Text Block
Java의 불변 String, 가변 StringBuilder, 그리고 Java 15+의 Text Block을 완전히 마스터합니다.
Java code
public class Strings {
public static void main(String[] args) {
// String — 불변 (immutable)
String s1 = "Hello";
String s2 = "Hello";
String s3 = new String("Hello");
System.out.println(s1 == s2); // true (intern pool)
System.out.println(s1 == s3); // false (new 객체)
System.out.println(s1.equals(s3)); // true
// 주요 메서드
String str = " Java Programming ";
System.out.println(str.trim());
System.out.println(str.strip()); // Unicode 공백 처리
System.out.println(str.toUpperCase());
System.out.println("hello".contains("ell")); // true
System.out.println("a,b,c".split(",").length); // 3
System.out.println(String.join("-", "a","b","c")); // a-b-c
System.out.println("abc".repeat(3)); // abcabcabc
System.out.println("".isBlank()); // true (Java 11+)
System.out.println(" ".isBlank()); // true
// StringBuilder — 가변, 효율적인 문자열 조합
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 5; i++) {
sb.append("item").append(i).append(", ");
}
sb.delete(sb.length()-2, sb.length()); // 마지막 ", " 제거
System.out.println(sb);
// Text Block (Java 15+)
String json = """
{
"name": "Java",
"version": 21
}
""";
System.out.println(json);
String html = """
<html>
<body>%s</body>
</html>
""".formatted("Hello!");
System.out.println(html);
}
}String 연결(+)은 컴파일러가 StringBuilder로 최적화합니다. 하지만 루프 안에서는 최적화가 안 됩니다. 루프에서는 StringBuilder를 직접 사용하세요.
==로 String 비교 시 리터럴끼리는 intern pool에서 같은 객체를 재사용해 우연히 맞을 수 있습니다. 항상 .equals()를 사용하세요.
03조건문 & 반복문 & switch expression
Java의 전통적 제어흐름과 Java 14+의 switch 표현식으로 코드를 더 안전하게 작성합니다.
Java code
public class ControlFlow {
public static void main(String[] args) {
// if-else
int score = 85;
String grade = score >= 90 ? "A" :
score >= 80 ? "B" :
score >= 70 ? "C" : "F";
// switch expression (Java 14+) — 값 반환, break 불필요
String result = switch (score / 10) {
case 10, 9 -> "우수";
case 8 -> "양호";
case 7 -> "보통";
default -> "미흡";
};
// switch expression with yield
int dayNum = 3;
String day = switch (dayNum) {
case 1 -> "월요일";
case 2 -> "화요일";
case 3 -> {
System.out.println("수요일 처리");
yield "수요일"; // 블록에서 값 반환
}
default -> "기타";
};
// 향상된 for 루프
int[] nums = {1, 2, 3, 4, 5};
int sum = 0;
for (int n : nums) sum += n;
// 레이블 break (중첩 루프 탈출)
outer:
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 3; j++) {
if (i == 1 && j == 1) break outer;
System.out.print(i + "" + j + " ");
}
}
System.out.println();
System.out.println(grade + " / " + result + " / " + day);
}
}새 switch expression은 모든 케이스를 커버하지 않으면 컴파일 에러가 납니다. 전통적 switch문보다 안전합니다.
전통적 switch 문에서 break를 빠뜨리면 다음 case로 fall-through됩니다. 의도치 않은 경우 버그의 원인이 됩니다. Java 14+의 -> 표현식은 fall-through가 없습니다.
04Record 클래스 (Java 16+)
불변 데이터 클래스를 위한 Record. equals, hashCode, toString, getter를 자동 생성합니다.
Java code
import java.util.Objects;
// Record — 불변 데이터 클래스
public record Point(double x, double y) {
// compact constructor — 유효성 검사
public Point {
if (Double.isNaN(x) || Double.isNaN(y)) {
throw new IllegalArgumentException("NaN 불가");
}
}
// 추가 메서드
public double distance() {
return Math.sqrt(x * x + y * y);
}
public Point translate(double dx, double dy) {
return new Point(x + dx, y + dy); // 새 Record 반환
}
}
// Generic Record
public record Pair<A, B>(A first, B second) {
public static <A, B> Pair<A, B> of(A a, B b) {
return new Pair<>(a, b);
}
}
// Record implements interface
public interface Shape {
double area();
}
public record Circle(double radius) implements Shape {
@Override
public double area() {
return Math.PI * radius * radius;
}
}
class Main {
public static void main(String[] args) {
var p1 = new Point(3, 4);
var p2 = new Point(3, 4);
System.out.println(p1.x()); // 3.0 (accessor)
System.out.println(p1.distance()); // 5.0
System.out.println(p1.equals(p2)); // true
System.out.println(p1); // Point[x=3.0, y=4.0]
var translated = p1.translate(1, 2);
System.out.println(translated);
var pair = Pair.of("Java", 21);
System.out.println(pair.first() + " " + pair.second());
}
}Record는 상속이 불가능합니다(암묵적으로 final). 인터페이스 구현은 가능합니다. 불변 DTO, Value Object 패턴에 완벽합니다.
Record의 접근자 메서드는 getX()가 아닌 x()입니다. 기존 JavaBeans 컨벤션과 다르므로 Jackson 같은 라이브러리 설정 시 주의하세요.
05Sealed Class & Pattern Matching (Java 17/21)
대수적 데이터 타입을 Java에서 구현하는 Sealed Class와 패턴 매칭.
Java code
// Sealed class (Java 17+)
public sealed interface Shape
permits Circle, Rectangle, Triangle {}
public record Circle(double radius) implements Shape {}
public record Rectangle(double w, double h) implements Shape {}
public record Triangle(double base, double height) implements Shape {}
// Pattern Matching for switch (Java 21)
public class PatternMatching {
static double area(Shape shape) {
return switch (shape) {
case Circle c -> Math.PI * c.radius() * c.radius();
case Rectangle r -> r.w() * r.h();
case Triangle t -> 0.5 * t.base() * t.height();
// else 불필요 — sealed로 완전성 보장
};
}
// Guarded patterns (Java 21)
static String classify(Shape shape) {
return switch (shape) {
case Circle c when c.radius() > 10 -> "대형 원";
case Circle c -> "소형 원";
case Rectangle r when r.w() == r.h() -> "정사각형";
case Rectangle r -> "직사각형";
case Triangle t -> "삼각형";
};
}
// instanceof Pattern Matching (Java 16+)
static void printInfo(Object obj) {
if (obj instanceof String s && !s.isBlank()) {
System.out.println("문자열 길이: " + s.length());
} else if (obj instanceof Integer i && i > 0) {
System.out.println("양의 정수: " + i);
}
}
public static void main(String[] args) {
Shape[] shapes = {
new Circle(5), new Rectangle(3, 4), new Triangle(6, 8)
};
for (var s : shapes) {
System.out.printf("%s → %.2f%n", classify(s), area(s));
}
printInfo("Hello");
printInfo(42);
}
}Sealed class + pattern matching은 Kotlin의 sealed class + when과 동일한 역할을 합니다. Java 21에서 정식 기능이 됐습니다.
switch에서 Sealed class를 사용할 때 permits 타입 중 하나라도 빠지면 컴파일 에러가 납니다. 이것이 exhaustive 검사의 강점입니다.
06Generic — 타입 경계 & 와일드카드
제네릭의 타입 경계(extends, super)와 와일드카드로 유연한 API를 설계합니다.
Java code
import java.util.*;
public class Generics {
// 상한 경계 — T는 Number 또는 그 하위 타입
static <T extends Number & Comparable<T>> T max(List<T> list) {
return list.stream()
.max(Comparator.naturalOrder())
.orElseThrow();
}
// 와일드카드 — ? extends (공변, 읽기 전용)
static double sumList(List<? extends Number> list) {
return list.stream().mapToDouble(Number::doubleValue).sum();
}
// 와일드카드 — ? super (반공변, 쓰기 가능)
static void addNumbers(List<? super Integer> list) {
list.add(1); list.add(2); list.add(3);
}
// PECS 원칙: Producer Extends, Consumer Super
static <T> void copy(List<? extends T> src, List<? super T> dst) {
for (T t : src) dst.add(t);
}
// 제네릭 클래스
static class Pair<A, B> {
private final A first;
private final B second;
Pair(A first, B second) { this.first = first; this.second = second; }
public A first() { return first; }
public B second() { return second; }
// 정적 팩토리
static <A, B> Pair<A, B> of(A a, B b) { return new Pair<>(a, b); }
@Override public String toString() {
return "(%s, %s)".formatted(first, second);
}
}
public static void main(String[] args) {
System.out.println(max(List.of(3, 1, 4, 1, 5))); // 5
System.out.println(sumList(List.of(1, 2.5, 3L))); // 6.5
List<Number> nums = new ArrayList<>();
addNumbers(nums);
System.out.println(nums); // [1, 2, 3]
var pair = Pair.of("Java", 21);
System.out.println(pair); // (Java, 21)
}
}PECS 원칙: 데이터를 제공(Producer)한다면 extends, 데이터를 소비(Consumer)한다면 super를 사용하세요.
제네릭 배열 생성(new T[])은 컴파일 에러입니다. List를 사용하거나 (T[]) new Object[n]으로 우회합니다(비권장).
07String Templates (Java 21 Preview)
STR."..." 문법으로 안전한 문자열 보간
Java code
<span class="cm">// String Templates (Java 21 Preview) 예제
// data/prompts.js의 생성 프롬프트로 상세 코드 생성 가능</span>
fun main() { println("String Templates (Java 21 Preview)") }JAVA 공식 문서를 함께 참고하세요.
자주 발생하는 실수에 주의하세요.
08Pattern Matching for switch (Java 21)
Java 21에서 정식 도입된 switch 패턴 매칭. instanceof 체크와 캐스팅을 한 번에 처리하고, sealed class와 함께 사용하면 컴파일러가 모든 케이스를 검증합니다.
Java code
// Pattern Matching for switch (Java 21)
public class PatternSwitch {
sealed interface Shape permits Circle, Rectangle, Triangle {}
record Circle(double radius) implements Shape {}
record Rectangle(double w, double h) implements Shape {}
record Triangle(double base, double height) implements Shape {}
static String describe(Shape shape) {
return switch (shape) {
case Circle c when c.radius() > 10
-> "큰 원 (반지름: " + c.radius() + ")";
case Circle c
-> "원 (반지름: " + c.radius() + ")";
case Rectangle r
-> "사각형 (" + r.w() + "x" + r.h() + ")";
case Triangle t
-> "삼각형 (넓이: " + (t.base() * t.height() / 2) + ")";
// sealed이므로 default 불필요 — 컴파일러가 완전성 검증
};
}
// null 처리도 가능
static String format(Object obj) {
return switch (obj) {
case null -> "null";
case Integer i -> "정수: " + i;
case String s -> "문자열: " + s;
case int[] arr -> "배열 (길이: " + arr.length + ")";
default -> "기타: " + obj.getClass().getSimpleName();
};
}
public static void main(String[] args) {
System.out.println(describe(new Circle(15)));
System.out.println(describe(new Rectangle(3, 4)));
System.out.println(format(null));
System.out.println(format(42));
System.out.println(format("Hello"));
}
}when 가드 조건으로 세밀한 분기가 가능합니다. sealed 타입과 함께 사용하면 default 없이도 컴파일러가 모든 케이스를 보장합니다.
switch 식에서 패턴 순서가 중요합니다. 더 구체적인 패턴을 먼저 배치해야 합니다. case Circle c가 case Circle c when ...보다 앞에 오면 가드 조건이 도달하지 않습니다.
09Text Blocks & String Templates
Java 15+의 텍스트 블록(""")으로 여러 줄 문자열을 들여쓰기 그대로 작성합니다. JSON, SQL, HTML 등 내장 문자열 작성이 훨씬 편해집니다.
Java code
public class TextBlocks {
public static void main(String[] args) {
// 1. 기본 텍스트 블록 (Java 15+)
String json = """
{
"name": "홍길동",
"age": 30,
"skills": ["Java", "Kotlin", "Go"]
}
""";
System.out.println(json);
// 2. SQL 쿼리
String sql = """
SELECT u.name, u.email, COUNT(o.id) as order_count
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
WHERE u.active = true
GROUP BY u.name, u.email
HAVING COUNT(o.id) > 5
ORDER BY order_count DESC
""";
// 3. formatted() — 값 삽입 (Java 15+)
String name = "이순신";
int age = 45;
String greeting = """
안녕하세요, %s님!
나이: %d세
등급: %s
""".formatted(name, age, age >= 40 ? "VIP" : "일반");
System.out.println(greeting);
// 4. stripIndent()와 translateEscapes()
String html = """
<html>
<body>
<h1>pullh</h1>
<p>pull + h 프로그래밍 학습 플랫폼</p>
</body>
</html>
""";
// 5. \ 줄 이어쓰기 (Java 14+)
String longLine = """
이것은 매우 긴 한 줄 문자열을 \
텍스트 블록에서 여러 줄로 나눠 \
작성하는 방법입니다.""";
System.out.println(longLine);
// → "이것은 매우 긴 한 줄 문자열을 텍스트 블록에서 여러 줄로 나눠 작성하는 방법입니다."
}
}텍스트 블록의 들여쓰기는 닫는 """의 위치로 결정됩니다. 닫는 따옴표를 왼쪽에 놓으면 전체 들여쓰기가 유지되고, 내용과 같은 레벨에 놓으면 공통 들여쓰기가 제거됩니다.
텍스트 블록 안에서 %s를 사용하려면 formatted()을, $를 그대로 쓰려면 이스케이프 불필요합니다. 하지만 \는 여전히 이스케이프 문자로 작동하므로 Windows 경로 등에 주의하세요.
10원시 타입 vs 래퍼 클래스 심화
원시 타입과 래퍼 클래스의 메모리 차이, ==와 equals()의 함정을 깊이 이해합니다.
Java code
public class PrimitiveVsWrapper {
public static void main(String[] args) {
// 원시 타입 — 스택에 저장, 빠름
int a = 127;
int b = 127;
System.out.println(a == b); // true
// 래퍼 — 힙에 저장, 캐시 범위 주의
Integer x = 127;
Integer y = 127;
System.out.println(x == y); // true (캐시)
System.out.println(x.equals(y)); // true
Integer p = 128;
Integer q = 128;
System.out.println(p == q); // false! (캐시 범위 초과)
System.out.println(p.equals(q)); // true
// 메모리 차이: int=4B, Integer=16B+
System.out.println(Integer.BYTES); // 4
}
}Integer 캐시 범위는 -128~127입니다. -XX:AutoBoxCacheMax로 상한을 늘릴 수 있습니다.
래퍼 타입끼리 ==로 비교하면 캐시 범위 밖에서는 false가 됩니다. 항상 equals()를 사용하세요.
11자동 박싱과 언박싱
오토박싱(auto-boxing)과 언박싱(unboxing)의 동작 원리와 성능 영향을 알아봅니다.
Java code
import java.util.ArrayList;
import java.util.List;
public class AutoBoxing {
public static void main(String[] args) {
// 오토박싱: int -> Integer
Integer wrapped = 42;
// 컴파일러가 Integer.valueOf(42)로 변환
// 언박싱: Integer -> int
int unwrapped = wrapped;
// 컴파일러가 wrapped.intValue()로 변환
// 컬렉션에서 오토박싱
List<Integer> nums = new ArrayList<>();
for (int i = 0; i < 5; i++) {
nums.add(i); // 오토박싱 발생
}
// 성능 함정: 반복문에서 불필요한 박싱
Long sum = 0L; // Long 사용 — 매번 박싱!
for (long i = 0; i < 100_000; i++) {
sum += i; // 언박싱 + 덧셈 + 박싱
}
// 해결: long sum = 0L; 사용
}
}성능이 중요한 루프에서는 반드시 원시 타입을 사용하세요. 래퍼 타입은 컬렉션 저장 시에만 사용하는 것이 좋습니다.
Integer가 null일 때 언박싱하면 NullPointerException이 발생합니다. null 체크를 먼저 하세요.
12문자열 풀과 intern()
String Pool의 동작 원리와 intern() 메서드를 이해합니다.
Java code
public class StringPool {
public static void main(String[] args) {
// 리터럴 — String Pool에 저장
String s1 = "hello";
String s2 = "hello";
System.out.println(s1 == s2); // true (같은 참조)
// new — 항상 새 객체 (힙)
String s3 = new String("hello");
System.out.println(s1 == s3); // false
System.out.println(s1.equals(s3)); // true
// intern() — Pool에서 가져오기
String s4 = s3.intern();
System.out.println(s1 == s4); // true
// 문자열 연결 최적화 (Java 9+)
// 컴파일러가 invokedynamic으로 최적화
String name = "World";
String greeting = "Hello, " + name + "!";
System.out.println(greeting);
}
}Java 9+에서는 문자열 연결이 invokedynamic으로 최적화되어 StringBuilder를 직접 쓸 필요가 줄었습니다.
new String("abc")는 Pool 외에 힙에 별도 객체를 생성합니다. 특별한 이유 없이 new String()을 사용하지 마세요.
13StringBuilder와 StringBuffer
가변 문자열 StringBuilder와 스레드 안전한 StringBuffer의 사용법입니다.
Java code
public class StringBuilderDemo {
public static void main(String[] args) {
// StringBuilder — 단일 스레드에서 사용
StringBuilder sb = new StringBuilder();
sb.append("Hello");
sb.append(", ");
sb.append("World!");
System.out.println(sb.toString());
// 메서드 체이닝
String result = new StringBuilder()
.append("Java ")
.append(21)
.append(" is ")
.append("awesome")
.toString();
System.out.println(result);
// 삽입, 삭제, 뒤집기
StringBuilder edit = new StringBuilder("ABCDE");
edit.insert(2, "XY"); // "ABXYCDE"
edit.delete(0, 2); // "XYCDE"
edit.reverse(); // "EDCYX"
System.out.println(edit);
// 용량 관리
StringBuilder sized = new StringBuilder(100);
System.out.println(sized.capacity()); // 100
}
}반복문에서 문자열을 이어붙일 때는 StringBuilder를 사용하세요. 초기 용량을 예측해서 지정하면 재할당을 줄일 수 있습니다.
StringBuffer는 synchronized라 느립니다. 멀티스레드가 아니면 항상 StringBuilder를 쓰세요.
14switch 심화 — 패턴 매칭
Java 21의 switch 패턴 매칭과 가드를 활용한 강력한 분기 처리입니다.
Java code
public class SwitchPatterns {
sealed interface Shape permits Circle, Rect, Triangle {}
record Circle(double r) implements Shape {}
record Rect(double w, double h) implements Shape {}
record Triangle(double base, double height) implements Shape {}
static double area(Shape shape) {
return switch (shape) {
case Circle c -> Math.PI * c.r() * c.r();
case Rect r -> r.w() * r.h();
case Triangle t -> 0.5 * t.base() * t.height();
};
}
static String classify(Object obj) {
return switch (obj) {
case null -> "null";
case Integer i when i > 0 -> "양수: " + i;
case Integer i -> "정수: " + i;
case String s -> "문자열: " + s;
default -> "기타: " + obj;
};
}
public static void main(String[] args) {
System.out.println(area(new Circle(5)));
System.out.println(classify(42));
System.out.println(classify(-3));
System.out.println(classify(null));
}
}switch 패턴 매칭에서 when 가드를 사용하면 조건부 분기를 깔끔하게 표현할 수 있습니다. 더 구체적인 패턴을 위에 배치하세요.
switch에서 패턴 순서가 중요합니다. Integer i when i > 0보다 Integer i를 먼저 쓰면 가드 조건에 도달하지 못합니다.
15instanceof 패턴 매칭
Java 16+의 instanceof 패턴 매칭으로 형변환 없이 바로 변수를 사용합니다.
Java code
public class InstanceofPattern {
public static void main(String[] args) {
Object obj = "Hello, Java 21!";
// 기존 방식
if (obj instanceof String) {
String s = (String) obj;
System.out.println(s.toUpperCase());
}
// 패턴 매칭 (Java 16+)
if (obj instanceof String s) {
System.out.println(s.length());
}
// 논리 연산자와 함께
if (obj instanceof String s && s.length() > 5) {
System.out.println("긴 문자열: " + s);
}
// 부정에서도 활용
if (!(obj instanceof String s)) {
System.out.println("문자열 아님");
return;
}
// 여기서 s 사용 가능
System.out.println(s.toLowerCase());
}
}패턴 변수의 스코프는 확정적으로 true인 범위에서만 유효합니다. &&와 함께 쓸 수 있지만 ||에서는 쓸 수 없습니다.
obj instanceof String s || s.isEmpty()는 컴파일 에러입니다. ||의 오른쪽에서는 패턴 변수가 확정되지 않습니다.
16for-each와 반복자 패턴
향상된 for문과 Iterable 인터페이스, Iterator 패턴을 알아봅니다.
Java code
import java.util.*;
public class ForEachDemo {
public static void main(String[] args) {
List<String> fruits = List.of("사과", "바나나", "체리");
// for-each (내부적으로 Iterator 사용)
for (String fruit : fruits) {
System.out.println(fruit);
}
// 인덱스가 필요한 경우
for (int i = 0; i < fruits.size(); i++) {
System.out.printf("[%d] %s%n", i, fruits.get(i));
}
// Iterator 직접 사용 (삭제 시 필요)
List<String> mutable = new ArrayList<>(fruits);
Iterator<String> it = mutable.iterator();
while (it.hasNext()) {
if (it.next().startsWith("바")) {
it.remove(); // 안전한 삭제
}
}
System.out.println(mutable); // [사과, 체리]
}
}for-each 중에 컬렉션을 수정하려면 Iterator.remove()를 사용하거나 removeIf()를 활용하세요.
for-each 안에서 list.remove()를 호출하면 ConcurrentModificationException이 발생합니다.
17열거형(Enum) 심화
열거형에 필드, 메서드, 추상 메서드를 추가하는 고급 패턴입니다.
Java code
public enum Planet {
MERCURY(3.303e+23, 2.4397e6),
VENUS (4.869e+24, 6.0518e6),
EARTH (5.976e+24, 6.37814e6),
MARS (6.421e+23, 3.3972e6);
private final double mass; // kg
private final double radius; // m
Planet(double mass, double radius) {
this.mass = mass;
this.radius = radius;
}
static final double G = 6.67300E-11;
double surfaceGravity() {
return G * mass / (radius * radius);
}
double surfaceWeight(double otherMass) {
return otherMass * surfaceGravity();
}
public static void main(String[] args) {
double earthWeight = 75.0;
double mass = earthWeight / EARTH.surfaceGravity();
for (Planet p : values()) {
System.out.printf("%s에서 몸무게: %.2fkg%n",
p, p.surfaceWeight(mass));
}
}
}values()는 호출 시마다 배열을 복사합니다. 자주 호출하면 List.of(values())로 캐시하세요.
열거형 생성자는 항상 private입니다. public으로 선언하면 컴파일 에러가 발생합니다.
18어노테이션 기초와 커스텀 어노테이션
내장 어노테이션과 커스텀 어노테이션 정의 방법을 배웁니다.
Java code
import java.lang.annotation.*;
// 커스텀 어노테이션 정의
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Benchmark {
int warmup() default 3;
int iterations() default 10;
String description() default "";
}
// 사용 예시
class PerformanceTest {
@Benchmark(iterations = 100, description = "정렬 성능")
public void testSort() {
// 테스트 코드
}
@Override // 컴파일 타임 검증
public String toString() {
return "PerformanceTest";
}
@Deprecated(since = "2.0", forRemoval = true)
public void oldMethod() { }
@SuppressWarnings("unchecked")
public void legacyCode() { }
}@Retention(RUNTIME)이면 리플렉션으로 런타임에 읽을 수 있고, SOURCE면 컴파일 후 사라집니다.
@Target을 지정하지 않으면 모든 곳에 붙일 수 있어 의도치 않은 사용이 발생합니다. 항상 타겟을 명시하세요.
19가변인자(Varargs)
... 문법으로 가변 개수 인자를 받는 메서드를 작성합니다.
Java code
public class VarargsDemo {
// 가변인자 메서드
static int sum(int... numbers) {
int total = 0;
for (int n : numbers) {
total += n;
}
return total;
}
// 일반 파라미터 + 가변인자
static String format(String prefix, Object... args) {
StringBuilder sb = new StringBuilder(prefix);
for (Object arg : args) {
sb.append(" ").append(arg);
}
return sb.toString();
}
public static void main(String[] args) {
System.out.println(sum()); // 0
System.out.println(sum(1, 2, 3)); // 6
System.out.println(sum(10, 20)); // 30
// 배열도 전달 가능
int[] arr = {1, 2, 3, 4, 5};
System.out.println(sum(arr)); // 15
System.out.println(format("결과:", 1, "a", true));
}
}가변인자는 내부적으로 배열입니다. 성능이 중요하면 자주 사용하는 인자 개수에 대해 오버로드 메서드를 제공하세요.
가변인자는 메서드당 하나만, 마지막 파라미터에만 올 수 있습니다. void m(int... a, String... b)는 불가능합니다.
20자바독(Javadoc) 작성법
Javadoc 주석으로 API 문서를 자동 생성하는 방법입니다.
Java code
/**
* 수학 유틸리티 클래스.
* <p>다양한 수학 연산을 제공합니다.</p>
*
* @author 홍길동
* @version 1.0
* @since Java 21
*/
public class MathUtils {
/**
* 두 정수의 최대공약수를 구합니다.
*
* @param a 첫 번째 정수 (양수)
* @param b 두 번째 정수 (양수)
* @return a와 b의 최대공약수
* @throws IllegalArgumentException a 또는 b가 음수인 경우
* @see Math#max(int, int)
*/
public static int gcd(int a, int b) {
if (a < 0 || b < 0) {
throw new IllegalArgumentException("음수 불가");
}
while (b != 0) {
int temp = b;
b = a % b;
a = temp;
}
return a;
}
}javadoc -d docs src/*.java 명령으로 HTML 문서를 생성합니다. IDE에서는 /** 입력 후 Enter로 자동 템플릿이 생성됩니다.
@param의 파라미터명이 실제 메서드 시그니처와 다르면 경고가 발생합니다. 리팩토링 후 Javadoc도 함께 수정하세요.
21모듈 시스템(JPMS) 심화
Java 9+의 모듈 시스템으로 패키지를 캡슐화하고 의존성을 명시합니다.
Java code
// module-info.java (com.example.core 모듈)
// module com.example.core {
// exports com.example.core.api; // 공개 패키지
// exports com.example.core.model;
// // com.example.core.internal은 비공개
//
// requires java.logging; // 의존 모듈
// requires transitive java.sql; // 전이적 의존
//
// provides com.example.core.api.Service
// with com.example.core.internal.ServiceImpl;
// }
// 공개 API
package com.example.core.api;
public interface Service {
String process(String input);
}
// 내부 구현 (외부 접근 불가)
package com.example.core.internal;
import com.example.core.api.Service;
public class ServiceImpl implements Service {
@Override
public String process(String input) {
return input.toUpperCase();
}
}requires transitive는 의존하는 모듈을 자동으로 읽기 가능하게 합니다. 라이브러리 모듈에서 유용합니다.
exports하지 않은 패키지는 다른 모듈에서 접근할 수 없습니다. 리플렉션도 opens 없이는 차단됩니다.
22var(지역 변수 타입 추론) 심화
var의 허용 범위와 제약사항을 정확히 이해합니다.
Java code
import java.util.*;
import java.util.stream.*;
public class VarAdvanced {
public static void main(String[] args) {
// var 사용 가능한 곳
var list = List.of(1, 2, 3); // List<Integer>
var map = new HashMap<String, Integer>(); // HashMap
var stream = list.stream(); // Stream<Integer>
// 람다에서 var (Java 11+) — 어노테이션 붙이기 가능
list.stream()
.map((@SuppressWarnings("unused") var x) -> x * 2)
.forEach(System.out::println);
// for문에서 var
for (var i = 0; i < 10; i++) {
// i는 int로 추론
}
// try-with-resources에서 var
// var reader = new java.io.FileReader("test.txt");
// var 사용 불가: 필드, 파라미터, 반환타입
// 다이아몬드 연산자와 조합 시 주의
var ambiguous = new ArrayList<>(); // ArrayList<Object>!
}
}var와 다이아몬드 연산자(<>)를 함께 쓸 때는 타입이 Object로 추론될 수 있으니 명시하세요.
var x = null;은 타입을 추론할 수 없어 컴파일 에러입니다. var x = (String) null;처럼 캐스팅이 필요합니다.
23배열 심화 — 다차원과 유틸리티
다차원 배열, Arrays 유틸리티, 배열 복사 방법을 다룹니다.
Java code
import java.util.Arrays;
public class ArrayAdvanced {
public static void main(String[] args) {
// 다차원 배열 (비정형 가능)
int[][] jagged = {
{1, 2, 3},
{4, 5},
{6, 7, 8, 9}
};
// Arrays 유틸리티
int[] arr = {5, 3, 1, 4, 2};
Arrays.sort(arr);
System.out.println(Arrays.toString(arr)); // [1,2,3,4,5]
int idx = Arrays.binarySearch(arr, 3); // 2
int[] copy = Arrays.copyOfRange(arr, 1, 4); // [2,3,4]
Arrays.fill(copy, 0); // [0,0,0]
// 깊은 비교 (다차원)
int[][] a = {{1, 2}, {3, 4}};
int[][] b = {{1, 2}, {3, 4}};
System.out.println(Arrays.deepEquals(a, b)); // true
System.out.println(Arrays.deepToString(a)); // [[1,2],[3,4]]
// 배열을 스트림으로
int sum = Arrays.stream(arr).sum();
System.out.println("합계: " + sum);
}
}System.arraycopy()는 네이티브 메서드라 Arrays.copyOf()보다 빠릅니다. 대량 복사 시 사용하세요.
Arrays.sort()는 원본 배열을 변경합니다. 원본 보존이 필요하면 먼저 복사하세요.
24정규식(Pattern/Matcher)
Java의 정규 표현식 API와 주요 패턴을 다룹니다.
Java code
import java.util.regex.*;
public class RegexDemo {
public static void main(String[] args) {
// 기본 매칭
String email = "user@example.com";
boolean valid = email.matches(
"[a-zA-Z0-9+_.-]+@[a-zA-Z0-9.-]+");
System.out.println("이메일 유효: " + valid);
// Pattern + Matcher (재사용)
Pattern p = Pattern.compile("(\\d{3})-(\\d{4})");
Matcher m = p.matcher("전화: 010-1234, 020-5678");
while (m.find()) {
System.out.printf("전체: %s, 앞: %s, 뒤: %s%n",
m.group(), m.group(1), m.group(2));
}
// 치환
String result = "Hello World".replaceAll("\\s+", " ");
System.out.println(result); // "Hello World"
// Named group (Java 7+)
Pattern np = Pattern.compile(
"(?<year>\\d{4})-(?<month>\\d{2})");
Matcher nm = np.matcher("2024-03");
if (nm.matches()) {
System.out.println(nm.group("year")); // 2024
}
}
}Pattern.compile()은 비용이 큰 연산입니다. 반복 사용 시 static final로 한 번만 컴파일하세요.
Java 문자열에서 백슬래시는 이중 이스케이프가 필요합니다. 정규식 \d는 자바에서 "\\d"로 씁니다.
25날짜/시간 API (java.time)
Java 8+의 java.time 패키지로 날짜와 시간을 안전하게 다룹니다.
Java code
import java.time.*;
import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoUnit;
public class DateTimeDemo {
public static void main(String[] args) {
// 현재 날짜/시간
LocalDate today = LocalDate.now();
LocalTime now = LocalTime.now();
LocalDateTime dateTime = LocalDateTime.now();
ZonedDateTime zoned = ZonedDateTime.now(ZoneId.of("Asia/Seoul"));
// 생성 및 조작 (불변)
LocalDate birthday = LocalDate.of(2000, 1, 15);
LocalDate nextMonth = today.plusMonths(1);
LocalDate lastYear = today.minusYears(1);
// 차이 계산
long days = ChronoUnit.DAYS.between(birthday, today);
Period period = Period.between(birthday, today);
System.out.printf("%d년 %d개월 %d일%n",
period.getYears(), period.getMonths(), period.getDays());
// 포맷팅
DateTimeFormatter fmt =
DateTimeFormatter.ofPattern("yyyy년 MM월 dd일 (E)");
System.out.println(today.format(fmt));
}
}java.time 클래스는 모두 불변(immutable)이고 스레드 안전합니다. Date/Calendar 대신 사용하세요.
LocalDateTime은 시간대 정보가 없습니다. 글로벌 서비스에서는 ZonedDateTime이나 Instant를 사용하세요.
26Optional 심화
Optional의 다양한 메서드와 올바른 사용 패턴을 알아봅니다.
Java code
import java.util.Optional;
public class OptionalAdvanced {
record User(String name, String email) {}
static Optional<User> findUser(String name) {
if ("홍길동".equals(name)) {
return Optional.of(new User("홍길동", "hong@test.com"));
}
return Optional.empty();
}
public static void main(String[] args) {
// 기본 사용
Optional<User> user = findUser("홍길동");
// orElse vs orElseGet
User u1 = user.orElse(new User("기본", "none"));
User u2 = user.orElseGet(() -> new User("기본", "none"));
// map으로 변환
String email = findUser("홍길동")
.map(User::email)
.orElse("없음");
// flatMap — Optional 반환 메서드 체이닝
// filter — 조건 필터링
String result = findUser("홍길동")
.filter(u -> u.email().contains("@"))
.map(User::email)
.orElse("유효하지 않음");
// or (Java 9+) — 대안 Optional 제공
User found = findUser("없는사람")
.or(() -> findUser("홍길동"))
.orElseThrow();
// ifPresentOrElse (Java 9+)
findUser("없는사람").ifPresentOrElse(
u -> System.out.println("찾음: " + u.name()),
() -> System.out.println("사용자 없음")
);
}
}orElse()는 값이 있어도 인자를 평가합니다. 생성 비용이 크면 orElseGet()을 사용하세요.
Optional을 필드, 파라미터, 컬렉션 원소로 사용하지 마세요. 반환 타입으로만 사용하는 것이 올바른 패턴입니다.
27람다 표현식 심화
람다의 다양한 형태와 클로저, 효과적 final 변수를 이해합니다.
Java code
import java.util.*;
import java.util.function.*;
public class LambdaAdvanced {
public static void main(String[] args) {
// 다양한 형태
Runnable r = () -> System.out.println("실행");
Consumer<String> c = s -> System.out.println(s);
BiFunction<Integer, Integer, Integer> add = (a, b) -> a + b;
Comparator<String> comp = (a, b) -> a.length() - b.length();
// 여러 줄 람다
Function<List<Integer>, Integer> sumFn = list -> {
int sum = 0;
for (int n : list) sum += n;
return sum;
};
// effectively final 변수 캡처
String prefix = "결과: "; // effectively final
Function<Integer, String> fmt = n -> prefix + n;
// 변수 수정 우회 (배열/AtomicInteger)
int[] counter = {0};
List.of(1, 2, 3).forEach(n -> counter[0] += n);
System.out.println(counter[0]); // 6
// Comparator 체이닝
List<String> names = new ArrayList<>(
List.of("Charlie", "Bob", "Alice", "Dave"));
names.sort(Comparator.comparingInt(String::length)
.thenComparing(Comparator.naturalOrder()));
System.out.println(names);
}
}람다가 3줄을 넘으면 메서드로 추출하고 메서드 참조를 사용하는 것이 가독성이 좋습니다.
람다에서 외부 변수를 수정하려면 effectively final이어야 합니다. int counter = 0; list.forEach(n -> counter++);는 컴파일 에러입니다.
28메서드 참조 4가지 유형
메서드 참조(::)의 네 가지 형태와 활용법을 배웁니다.
Java code
import java.util.*;
import java.util.function.*;
public class MethodRefDemo {
public static void main(String[] args) {
List<String> names = List.of("Alice", "Bob", "Charlie");
// 1. 정적 메서드 참조: 클래스::정적메서드
Function<String, Integer> parse = Integer::parseInt;
System.out.println(parse.apply("42"));
// 2. 인스턴스 메서드 참조 (특정 객체): 객체::메서드
String str = "Hello";
Supplier<Integer> len = str::length;
System.out.println(len.get());
// 3. 임의 객체의 인스턴스 메서드: 클래스::메서드
Function<String, String> upper = String::toUpperCase;
names.stream().map(upper).forEach(System.out::println);
// 4. 생성자 참조: 클래스::new
Function<String, StringBuilder> sbCreator = StringBuilder::new;
StringBuilder sb = sbCreator.apply("init");
// 배열 생성자 참조
IntFunction<int[]> arrCreator = int[]::new;
int[] arr = arrCreator.apply(10); // new int[10]
// 실전: 정렬에서 메서드 참조
List<String> sorted = names.stream()
.sorted(String::compareToIgnoreCase)
.toList();
System.out.println(sorted);
}
}메서드 참조는 람다보다 간결하고 의도가 명확합니다. IDE에서 람다를 메서드 참조로 변환할 수 있는지 확인하세요.
메서드 참조에서 오버로드된 메서드가 있으면 함수형 인터페이스의 시그니처로 구분됩니다. 모호하면 컴파일 에러가 발생합니다.
29함수형 인터페이스 커스텀
@FunctionalInterface로 커스텀 함수형 인터페이스를 정의합니다.
Java code
@FunctionalInterface
interface Validator<T> {
boolean test(T value);
// default 메서드는 허용
default Validator<T> and(Validator<T> other) {
return value -> this.test(value) && other.test(value);
}
default Validator<T> or(Validator<T> other) {
return value -> this.test(value) || other.test(value);
}
default Validator<T> negate() {
return value -> !this.test(value);
}
}
public class CustomFunctional {
public static void main(String[] args) {
Validator<String> notEmpty = s -> !s.isEmpty();
Validator<String> shortEnough = s -> s.length() <= 20;
Validator<String> hasAt = s -> s.contains("@");
// 합성
Validator<String> emailValidator = notEmpty
.and(shortEnough)
.and(hasAt);
System.out.println(emailValidator.test("a@b.com")); // true
System.out.println(emailValidator.test("")); // false
System.out.println(emailValidator.test("no-at-sign")); // false
}
}@FunctionalInterface는 추상 메서드가 정확히 하나인지 컴파일러가 검증합니다. default와 static 메서드는 개수 제한 없습니다.
추상 메서드를 2개 이상 선언하면 @FunctionalInterface가 컴파일 에러를 냅니다. Object의 메서드(toString 등)는 제외됩니다.
정리하며
- 래퍼 타입 값 비교에 ==를 쓰면 -128~127 캐시 범위 안에서만 우연히 통과합니다
- instanceof 패턴 매칭을 먼저 익히면 switch 패턴 매칭은 같은 문법의 확장으로 읽힙니다
- record와 sealed를 함께 쓰면 switch가 모든 하위 타입 처리를 컴파일 시점에 강제합니다
- 문자열 반복 결합은 루프 안에서만 StringBuilder로 바꾸면 대부분의 이득을 얻습니다
더 깊이 들어가고 싶다면 Java 학습 라이브러리에서 다른 주제 가이드를 이어서 보거나, 언어 비교에서 같은 개념이 다른 언어에서 어떻게 표현되는지 확인해 보세요.