본 도구에서 사용하는 오픈소스 라이브러리

본 도구의 코드에는 1개의 오픈소스 라이브러리가 포함되어 있습니다.

Java 치트시트 — 빠른 참조

Java 17+ 문법, OOP, 컬렉션, 자주 쓰는 표준 라이브러리를 모은 빠른 참조로, 일상 작업의 약 80%를 커버합니다.

J

Java Java 17 (LTS)

JDK (OpenJDK / Oracle) · OOP · 제네릭 · 함수형 · 정적 · 강한 타입 · 공칭 타입

추천 학습 경로

먼저 "Hello World와 빌드 환경"(javac/java 또는 Maven)을 실행해 보고 → 변수, 타입, 제어 흐름에 익숙해지고 → 컬렉션과 Stream API로 데이터를 처리한 뒤 → 객체 지향(상속/인터페이스/제네릭)을 파고 → 예외 처리와 람다를 익히고 → 마지막에 필요할 때 파일, 네트워크, 시간, 빌드/디버깅을 찾아봅니다. FAQ 섹션은 나중에 함정을 피하기 위해 다시 보는 것이 좋습니다.

1.Hello World와 빌드 환경

Java 프로그램을 처음부터 작성·컴파일·실행합니다: main 메서드, javac/java, 패키지, 빌드 도구.

최소 프로그램

모든 Java 프로그램은 main 메서드부터 시작합니다: public static void main(String[] args). System.out.println으로 한 줄을 출력합니다.

1
2
3
4
5
6
public class Hello {
public static void main(String[] args) {
System.out.println("Hello, world!");
}
}
// 파일명은 public 클래스명과 동일해야 함:Hello.java

컴파일과 실행

javac는 .java를 .class 바이트코드로 컴파일하고, java가 JVM을 띄워 실행합니다. javap으로 바이트코드를 디스어셈블해 봅니다.

1
2
3
4
// $ javac Hello.java # Hello.class 생성
// $ java Hello # 실행(.class 확장자 없이)
// $ javap -c Hello # 역컴파일하여 바이트코드 확인
// $ java -version # JDK 버전 확인

명령줄 인수

main(String[] args)는 명령줄 인수를 받으며, args[0]이 첫 번째 사용자 인수입니다. args.length로 개수를 확인합니다.

1
2
3
4
5
6
7
8
public static void main(String[] args) {
if (args.length == 0) {
System.out.println("No args");
return;
}
System.out.println("Hello, " + args[0] + "!");
}
// $ java Hello Nick

종료 코드

System.exit(code)는 JVM을 종료하고 종료 코드를 반환합니다: 0 성공, 0 이외 실패. main이 아닌 스레드가 실행 중이어도 강제 종료합니다.

1
2
3
4
5
if (error) {
System.err.println("failed");
System.exit(1); // 0 이 아니면 실패를 의미
}
// 정상 종료 시 호출하지 않아도 0 을 반환

패키지 선언

package는 소속 패키지를 선언하고 디렉터리 구조와 대응하며, 완전한 한정 이름으로 타입을 식별합니다. 생략하면 무명 패키지에 속합니다.

1
2
3
4
5
package com.example.app;
// 파일은 com/example/app/ 디렉터리에 배치
public class Main {
// 정규화된 이름:com.example.app.Main
}

import 문

import는 다른 패키지의 클래스나 static 멤버를 가져와 완전한 한정 이름을 생략하게 합니다. java.lang은 기본으로 import됩니다.

1
2
3
4
5
import java.util.List;
import java.util.ArrayList;
import static java.lang.Math.PI; // 정적 임포트
import java.util.*; // 와일드카드 임포트(남용 비권장)
List<String> list = new ArrayList<>();

클래스와 파일명

public 클래스 이름은 파일명과 일치해야 합니다. 한 .java 파일에 public이 아닌 여러 클래스를 둘 수 있지만, public 클래스는 하나만 둘 수 있습니다.

1
2
3
4
5
// FileName.java 안에서:
public class FileName { // 파일명과 동일한 이름
public static void main(String[] args) { }
}
class Helper { } // non-public 이면 같은 파일 가능

Maven / Gradle

대형 프로젝트는 빌드 도구를 사용합니다: Maven(pom.xml)과 Gradle(build.gradle)이 의존성, 컴파일, 테스트, 패키징을 관리합니다.

1
2
3
4
5
6
7
8
// pom.xml (Maven)
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.10.1</version>
</dependency>
// $ mvn compile / mvn test / mvn package
// $ gradle build / gradle run

2.변수와 상수

변수 선언, 타입 추론, final 상수, 스코프, 타입 변환.

변수 선언

선언 = 타입 + 이름 + 선택적 초기화. Java의 지역 변수는 사용 전 초기화가 필수이며, 필드에는 기본값이 있습니다.

1
2
3
4
5
6
int count = 42;
double price = 19.99;
String name = "Nick";
boolean ok = true;
char c = 'A';
// 여러 개 동시 선언:int a = 1, b = 2;

var 타입 추론

var(Java 10+)는 컴파일러가 타입을 추론합니다. 지역 변수에만 사용할 수 있습니다. 타입이 모호하면 명시적 선언이 더 읽기 좋습니다.

1
2
3
4
5
var count = 42; // int
var name = "Nick"; // String
var list = new ArrayList<String>(); // ArrayList<String>
// var 는 필드·메서드 매개변수·반환값에 사용 불가
// var 는 단순 추론일 뿐, 실행 시 타입은 변하지 않음

final 상수

final 지역 변수는 한 번만 대입할 수 있습니다. final 필드는 선언 시 또는 생성자에서 초기화해야 하며, static final은 상수입니다.

1
2
3
4
5
6
final int MAX = 100; // 읽기 전용 지역 변수
class Config {
final String name = "app"; // final 필드
static final double PI = 3.14; // 상수
}
// final 변수에 재할당하면 컴파일 오류

스코프

블록 스코프: 중괄호 안의 변수는 그 블록에서만 보입니다. 중첩 블록에서 외부의 같은 이름 변수를 가릴 수 있지만 권장하지 않습니다.

1
2
3
4
5
6
7
int x = 1;
{
int y = 2; // 이 블록 안에서만 보임
System.out.println(x + y);
}
// y 는 블록 밖에서 접근 불가
// System.out.println(y); // 컴파일 오류

명명 규칙

클래스명은 UpperCamelCase, 변수/메서드명은 lowerCamelCase, 상수는 UPPER_SNAKE_CASE, 패키지명은 모두 소문자입니다.

1
2
3
4
5
class UserProfile { }
int userId = 1;
String getUserName() { return "nick"; }
static final int MAX_RETRY = 3;
package com.example.app;

타입 변환

암시적 변환은 좁은 타입에서 넓은 타입으로(int→long→double). 명시적 캐스트는 정밀도를 잃을 수 있습니다(절단).

1
2
3
4
5
6
int i = 42;
long l = i; // 암시적:int → long
int back = (int) l; // 명시적 캐스트
long big = 3000000000L;
int overflow = (int) big; // 오버플로:음수로 잘림
String s = String.valueOf(i); // 숫자 → 문자열

null과 NPE

null은 참조 타입의 빈 값입니다. null에 대해 메서드 호출이나 필드 접근을 하면 NullPointerException(NPE)이 발생합니다. Objects 유틸리티로 안전하게 처리할 수 있습니다.

1
2
3
4
5
6
7
8
String s = null;
// s.length() // NPE!
if (s != null) {
System.out.println(s.length());
}
// Java 8+ 에서는 Optional 로 null 허용을 표현:
Optional<String> opt = Optional.ofNullable(s);
opt.ifPresent(System.out::println);

리터럴

정수 리터럴은 _로 구분할 수 있고(가독성), 0x는 16진, 0b는 2진입니다. long은 L, float은 f를 붙입니다.

1
2
3
4
5
6
7
int million = 1_000_000; // 밑줄 구분
int hex = 0xFF; // 255
int bin = 0b1101; // 13
long big = 42L;
float f = 3.14f;
double d = 3.14;
char ch = '\u0041'; // 'A'

3.데이터 타입

기본 타입과 래퍼 클래스, 문자열, 배열, 열거형, 제네릭, record.

기본 타입

Java의 8가지 기본 타입: byte/short/int/long(정수), float/double(부동소수), char, boolean. 값이 직접 저장됩니다.

1
2
3
4
5
6
7
8
byte b = 1;
short s = 2;
int i = 42;
long l = 42L;
float f = 3.14f;
double d = 3.14;
char c = 'A';
boolean flag = true;

래퍼 클래스

Integer/Double/Boolean 같은 래퍼 클래스는 기본 타입을 객체로 감쌉니다. 필요 시 자동 박싱/언박싱이 일어납니다.

1
2
3
4
5
Integer num = 42; // 자동 박싱 int → Integer
int value = num; // 자동 언박싱
Integer parsed = Integer.parseInt("42");
// 박싱 비교 주의:
// new Integer(42) == new Integer(42) 는 false

문자열

String은 불변 문자 시퀀스이며, equals는 내용 비교, ==는 참조 비교입니다. 수정은 매번 새 객체를 생성합니다.

1
2
3
4
5
String s = "hello";
String t = new String("hello");
boolean eq = s.equals(t); // true(내용)
boolean sameRef = s == t; // false(참조)
String upper = s.toUpperCase(); // "HELLO"

배열

배열은 고정 길이, 인덱스로 접근, length 속성을 갖습니다. int[]로 선언, new int[n]로 생성, {}로 초기화합니다.

1
2
3
4
5
6
int[] arr = new int[5]; // 기본값 0
int[] nums = { 1, 2, 3 };
int first = nums[0];
nums[2] = 99;
int len = nums.length; // 3(속성, 메서드 아님)
int[][] grid = new int[3][3]; // 2차원

열거형

enum은 상수 집합을 정의하며, 필드와 메서드를 포함할 수 있습니다. switch는 열거형 값을 바로 사용할 수 있습니다. 컴파일 시점 타입 안전성.

1
2
3
4
5
6
enum Color { RED, GREEN, BLUE }
Color c = Color.RED;
String name = c.name(); // "RED"
int ord = c.ordinal(); // 0(선언 순서)
Color parsed = Color.valueOf("BLUE");
// enum 은 생성자와 필드를 가질 수 있음:enum Status { OK(200), ERR(500); ... }

제네릭

제네릭은 타입을 매개변수화합니다: List<T>, Map<K,V>. 컴파일 시 타입 검사를 하고, 런타임에는 소거됩니다(type erasure).

1
2
3
4
5
6
7
List<String> names = new ArrayList<>();
Map<String, Integer> ages = new HashMap<>();
// 제네릭 메서드:
static <T> T first(List<T> list) {
return list.get(0);
}
// 와일드카드:List<? extends Number>

record 타입

record(Java 14 프리뷰, 16 정식)는 불변 데이터 캐리어를 한 줄로 선언하며, 생성자/equals/hashCode/toString이 자동 생성됩니다.

1
2
3
4
5
record Person(String name, int age) { }
var p = new Person("Nick", 30);
String n = p.name(); // 접근자(getName 이 아님)
String s = p.toString(); // Person[name=Nick, age=30]
// 불변:필드는 암시적으로 final

자동 박싱 함정

박싱/언박싱은 ==/연산에서 함정을 숨깁니다: -128~127의 Integer는 캐시되지만, 범위를 벗어나면 ==가 false일 수 있습니다.

1
2
3
4
5
Integer a = 100, b = 100;
boolean same = a == b; // true(캐시 풀)
Integer c = 200, d = 200;
boolean diff = c == d; // false!equals 를 써야 함
boolean ok = c.equals(d); // true

Object 루트 클래스

모든 클래스는 암묵적으로 Object를 상속합니다: toString, equals, hashCode, clone, finalize. equals를 오버라이드하면 hashCode도 반드시 오버라이드합니다.

1
2
3
4
5
6
7
8
9
10
11
class User {
private String name;
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof User u)) return false;
return name.equals(u.name);
}
@Override
public int hashCode() { return name.hashCode(); }
}

4.참조와 메모리

참조 의미, null, 객체 복사, 힙/스택, GC, 문자열 풀.

참조 의미

Java에는 포인터가 없고, 객체 변수는 참조를 보관합니다(포인터와 유사). 대입은 같은 객체를 공유하여 변경이 모든 별칭에 보입니다.

1
2
3
4
5
StringBuilder a = new StringBuilder("a");
StringBuilder b = a; // 참조 복사, 객체 공유
b.append("b");
System.out.println(a); // "ab"(a 도 바뀜)
// 기본형은 값 복사:int x = y 는 서로 영향 없음

null 참조

null은 참조가 어떤 객체도 가리키지 않음을 의미합니다. null 비교는 ==/!=를 쓰고, 메서드에 null을 넘기면 NPE가 발생합니다. 방어적으로 null 체크하세요.

1
2
3
4
5
6
String s = getMaybe();
if (s != null) {
System.out.println(s.length());
}
// Objects.requireNonNull(s, "s must not be null");
// Objects.toString(s, ""); 안전한 변환

Object 메서드

toString은 객체를 설명하고, equals는 내용 비교를, hashCode는 해시를 수행합니다. 기본 구현은 참조 비교이며, 내용 의미론을 원하면 재정의해야 합니다.

1
2
3
4
5
6
7
class Point {
int x, y;
Point(int x, int y) { this.x = x; this.y = y; }
@Override
public String toString() { return "(" + x + "," + y + ")"; }
}
System.out.println(new Point(1, 2)); // "(1,2)"

힙과 스택

객체는 힙에 할당되고(GC가 관리), 지역 변수와 참조는 스택에 있습니다. new로 만든 객체의 참조는 스택, 본체는 힙에 있습니다.

1
2
3
4
void method() {
int x = 1; // 스택:기본형
Person p = new Person(); // p 참조는 스택, 객체는 힙
} // 메서드 종료, 스택 해제, 객체는 GC 대기

GC 가비지 컬렉션

JVM이 도달 불가능한 객체를 자동으로 회수하므로 수동 free가 필요 없습니다. System.gc()는 힌트일 뿐 즉시 실행을 보장하지 않습니다.

1
2
3
4
5
// 더 이상 참조되지 않는 객체는 회수 대상
Person p = new Person();
p = null; // 원래 객체는 참조를 잃고 GC 대기
// System.gc(); // 권고일 뿐, 운영 환경에서는 호출 금지
// 세대별 GC:신세대 / 구세대

복사 의미

배열/객체 대입은 참조를 공유합니다. 독립 사본이 필요하면 clone, Arrays.copyOf, 또는 수동 복사를 씁니다. 깊은 복사는 계층별로 복사해야 합니다.

1
2
3
4
5
6
int[] a = { 1, 2, 3 };
int[] shallow = a; // 공유
int[] copy = a.clone(); // 독립된 복사본
copy[0] = 99;
System.out.println(a[0]); // 1(copy 는 독립)
// 객체 배열의 clone 은 얕은 복사, 요소는 여전히 공유

문자열 상수 풀

리터럴 문자열은 상수 풀에 캐시되어 같은 리터럴은 같은 객체를 공유합니다. new String(...)은 풀 밖의 새 객체를 만듭니다. intern()으로 풀에 넣습니다.

1
2
3
4
5
6
String a = "hello";
String b = "hello";
boolean same = a == b; // true(풀 안의 동일 객체)
String c = new String("hello");
boolean diff = a == c; // false(힙 위의 새 객체)
String d = c.intern(); // 풀 등록 후 a == d 는 true

finalize와 Cleaner

finalize는 GC 전에 호출되지만 시점이 보장되지 않습니다(비추천). 외부 자원은 AutoCloseable + try-with-resources로 해제하세요.

1
2
3
4
5
6
7
@Override
protected void finalize() { } // 폐기됨, 의존하지 말 것
// 올바른 방법:
class Conn implements AutoCloseable {
public void close() { /* 리소스 해제 */ }
}
try (var c = new Conn()) { } // 자동으로 close

5.제어 흐름

if/else, switch, for/while 루프, break/continue, 삼항 연산자.

if / else

if/else는 조건으로 분기합니다. 조건은 boolean 식이어야 합니다. else-if 체인으로 다중 분기할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
int score = 85;
String grade;
if (score >= 90) {
grade = "A";
} else if (score >= 60) {
grade = "B";
} else {
grade = "F";
}
System.out.println(grade);

switch 식

switch(Java 14+)는 ->로 값을 반환하거나 case를 합칠 수 있습니다. 기존 switch 문도 화살표 구문을 사용할 수 있습니다.

1
2
3
4
5
6
7
8
int day = 3;
String name = switch (day) {
case 1 -> "Monday";
case 2, 3 -> "Tue/Wed"; // 여러 값 병합
default -> "other";
};
// 기존 switch 에서도 -> 사용 가능:
// switch (day) { case 1 -> System.out.println("Mon"); }

for 루프

전통적 for: 초기화, 조건, 증감. 인덱스나 역방향이 필요할 때 사용합니다. 컬렉션 순회는 향상된 for를 우선하세요.

1
2
3
4
5
6
for (int i = 0; i < 10; i++) {
System.out.println(i); // 0..9
}
for (int i = arr.length - 1; i >= 0; i--) {
System.out.println(arr[i]); // 역순
}

향상된 for

for-each는 배열과 Iterable 컬렉션을 인덱스 없이 순회합니다. 컬렉션 구조를 변경해서는 안 됩니다(예외 발생).

1
2
3
4
5
for (String name : names) {
System.out.println(name);
}
for (int n : nums) { /* 읽기 전용 순회 */ }
// 인덱스/삭제가 필요하면 Iterator 나 일반 for 사용

while / do-while

while은 조건을 먼저 검사하고 실행, do-while은 최소 한 번 실행합니다. 횟수가 정해지지 않은 경우(스트림 읽기, 폴링)에는 while을 사용합니다.

1
2
3
4
5
6
int i = 0;
while (i < 5) { i++; } // 먼저 판단
int x = 0;
do {
x++; // 최소 1회는 실행
} while (x < 3);

break와 continue

continue는 현재 회차를 건너뛰고, break는 루프를 빠져나가며, 라벨이 있는 break/continue는 중첩 루프를 제어합니다.

1
2
3
4
5
6
7
8
outer:
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 3; j++) {
if (j == 1) continue; // 이번 반복 건너뜀
if (i == 2) break outer; // 모두 벗어남
}
}
// 레이블 스코프:outer: 가 바깥쪽 루프를 표시

삼항 연산자

cond ? a : b는 if/else를 한 줄로 표현합니다. 양쪽 타입은 호환되어야 합니다. 중첩 삼항은 가독성이 떨어지니 피하세요.

1
2
3
4
5
int age = 20;
String type = age >= 18 ? "adult" : "minor";
// 동일 표현:
String t2;
if (age >= 18) t2 = "adult"; else t2 = "minor";

return과 조기 반환

return은 메서드를 종료하고 값을 반환합니다(void는 return;). 조기 반환은 메서드를 읽기 쉽게 합니다(가드 절).

1
2
3
4
5
6
boolean isValid(String s) {
if (s == null || s.isEmpty()) return false;
if (s.length() > 10) return false;
return true;
}
// 조기 반환으로 깊은 중첩 방지

6.메서드와 람다

메서드 시그니처, 매개변수, 오버로딩, 재귀, 람다, 함수형 프로그래밍.

메서드 정의

메서드 = 접근 제한자 + 반환 타입 + 이름 + 매개변수 목록 + 본문. void는 반환값이 없음을 의미합니다.

1
2
3
4
5
6
7
public int add(int a, int b) {
return a + b;
}
public void say(String msg) {
System.out.println(msg); // void 는 반환값 없음
}
static int timesTwo(int x) { return x * 2; }

인자 전달

Java는 항상 값 전달입니다: 기본 타입은 값의 복사본, 참조 타입은 참조의 복사본이 전달됩니다(멤버 변경은 호출자에 영향, 참조 재대입은 영향 없음).

1
2
3
4
5
6
void set(int x) { x = 99; } // 기본형 복사본
void mutate(List<String> l) { l.add("x"); }
int n = 1;
set(n); // n 은 여전히 1
var list = new ArrayList<String>();
mutate(list); // list 에 "x" 가 추가됨

가변 인자

...(가변 인자)는 같은 타입의 임의 개수 인자를 받으며, 본질적으로 배열입니다. 매개변수 목록의 마지막에 하나만 둘 수 있습니다.

1
2
3
4
5
6
7
8
int sum(int... nums) {
int total = 0;
for (int n : nums) total += n;
return total;
}
int s = sum(1, 2, 3); // 6
int s2 = sum(); // 0(빈 배열)
int s3 = sum(new int[]{1,2}); // 배열도 전달 가능

메서드 오버로딩

같은 이름에 매개변수 목록(개수/타입)이 다른 메서드는 오버로딩입니다. 인자 타입에 따라 가장 일치하는 버전이 선택됩니다. 반환 타입은 구분에 포함되지 않습니다.

1
2
3
4
5
int parse(String s) { return Integer.parseInt(s); }
int parse(int x) { return x; } // 타입이 다름
int parse(String s, int radix) { // 개수가 다름
return Integer.parseInt(s, radix);
}

재귀

메서드가 자신을 호출하는 것이 재귀입니다. 기저 조건이 없으면 호출 스택이 오버플로합니다.

1
2
3
4
5
int factorial(int n) {
if (n <= 1) return 1; // 베이스 케이스
return n * factorial(n - 1);
}
// 깊은 재귀는 스택 오버플로 주의:호출마다 콜 스택 소비

람다식

람다는 (매개변수) -> 식이며, 함수형 인터페이스의 인스턴스입니다. 타입 추론과 단일 매개변수 괄호는 생략 가능합니다.

1
2
3
4
5
6
7
8
Runnable r = () -> System.out.println("run");
BiFunction<Integer, Integer, Integer> add =
(a, b) -> a + b;
// 매개변수 1개면 괄호 생략 가능:x -> x * 2
// 여러 줄 본문은 중괄호 + return:
Comparator<Integer> c = (x, y) -> {
return x.compareTo(y);
};

메서드 참조

:: 메서드 참조는 람다의 단축형입니다: ClassName::staticMethod, obj::instanceMethod, ClassName::new.

1
2
3
4
5
6
7
List<String> names = List.of("b", "a");
names.stream().sorted(String::compareTo);
// Class::staticMethod
names.forEach(System.out::println);
// obj::instanceMethod
names.forEach(s -> System.out.println(s));
// Class::new 생성자 참조:Supplier<Person> s = Person::new;

함수형 인터페이스

추상 메서드가 하나만 있는 인터페이스는 람다 타깃이 될 수 있습니다. 대표: Runnable, Function, Consumer, Predicate, Supplier.

1
2
3
4
5
Predicate<Integer> isEven = n -> n % 2 == 0;
Function<String, Integer> len = String::length;
Consumer<String> print = System.out::println;
Supplier<Double> rand = Math::random;
// @FunctionalInterface 어노테이션으로 함수형 인터페이스 선언

Stream API

Stream은 컬렉션을 체인으로 처리합니다: filter, map, collect. Lazy하며 원본을 변경하지 않습니다.

1
2
3
4
5
6
7
List<Integer> nums = List.of(1, 2, 3, 4);
var evens = nums.stream()
.filter(n -> n % 2 == 0) // [2, 4]
.map(n -> n * 10) // [20, 40]
.toList(); // Java 16+
// 집계:sum/max/count/anyMatch/allMatch
int sum = nums.stream().mapToInt(Integer::intValue).sum();

7.문자열

문자열 리터럴, 연결, StringBuilder, 자주 쓰는 메서드, 포맷팅.

문자열 리터럴

큰따옴표 문자열, \n 이스케이프, 그리고 Java 15+의 텍스트 블록은 큰따옴표 3개로 여러 줄을 그대로 유지합니다.

1
2
3
4
5
6
7
8
String s = "Line\nTab\tindent";
String path = "C:\\Program Files\\";
// 텍스트 블록(Java 15+):큰따옴표 3개로 여러 줄 텍스트를 감싸고 자동 들여쓰기
// String html = """
// <div>
// <p>Hello</p>
// </div>
// """.stripIndent();

연결

+로 문자열을 연결하며, 다른 타입과 연결하면 자동으로 문자열로 변환됩니다. 적은 횟수라면 충분하고, 루프 내에서는 StringBuilder를 사용하세요.

1
2
3
4
5
String a = "foo" + "bar"; // "foobar"
int age = 30;
String msg = "Age: " + age; // 자동으로 문자열 변환
String joined = String.join(", ", "a", "b", "c");
// 반복문에서의 결합은 StringBuilder 사용(mem 절 참고)

자주 쓰는 메서드

자주 쓰는 메서드: length, substring, indexOf, replace, split, trim, 대소문자 변환.

1
2
3
4
5
6
7
String s = " Hello, World ";
int len = s.length(); // 14
char c = s.charAt(0); // ' '
String sub = s.substring(7, 12); // "World"
boolean has = s.contains("World"); // true
String rep = s.replace("World", "Java");
String[] words = s.trim().split(",");

StringBuilder

대량 연결은 StringBuilder(스레드 비안전) 또는 StringBuffer(스레드 안전)를 사용합니다: append 후 toString.

1
2
3
4
5
6
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 100; i++) {
sb.append(i).append(",");
}
String result = sb.toString(); // 한 번에 변환
// 가변:삭제/삽입은 sb.deleteCharAt / sb.insert

포맷팅

String.format: %d 정수, %s 문자열, %.2f 소수, %n 줄바꿈. 폭과 플래그도 지원합니다.

1
2
3
4
5
double d = 1234.567;
String s = String.format("%.2f", d); // "1234.57"
String t = String.format("%d%%", 50); // "50%"
String pad = String.format("%5d", 42); // " 42"
String n = String.format("%,d", 1234567); // "1,234,567"

정규식 매칭

String.matches는 전체 매칭, replaceAll은 정규식 치환, split은 정규식 분할. 정규식 메타문자 이스케이프에 주의하세요.

1
2
3
boolean m = "123-456".matches("\\d{3}-\\d{3}"); // true
String masked = "123456".replaceAll("(\\d{3})(\\d+)", "$1***");
String[] parts = "a,b;c".split("[,;]"); // [a, b, c]

문자 처리

charAt로 문자를 가져오고, Character.isDigit/isLetter/isWhitespace로 분류합니다. 문자열은 불변이지만 문자는 순회 가능합니다.

1
2
3
4
5
6
String s = "Java 17";
for (int i = 0; i < s.length(); i++) {
char c = s.charAt(i);
if (Character.isUpperCase(c)) { }
}
// 또는:for (char c : s.toCharArray())

문자열 변환

Integer.parseInt는 문자열을 숫자로, valueOf/toString은 숫자를 문자열로 변환합니다. 실패 시 NumberFormatException이 발생합니다.

1
2
3
4
5
6
7
int n = Integer.parseInt("42");
long l = Long.parseLong("42");
double d = Double.parseDouble("3.14");
String s = String.valueOf(42);
String hex = Integer.toHexString(255); // "ff"
// 안전한 파싱:
// try { ... } catch (NumberFormatException e) { }

8.컬렉션과 Stream

List/Set/Map/Queue의 일반 구현, 순회, Stream, 정렬.

List

List는 순서 있는 컬렉션. ArrayList는 배열 기반(조회 빠름), LinkedList는 연결 리스트 기반(삽입/삭제 빠름). List.of는 불변 리스트입니다.

1
2
3
4
5
6
7
8
List<String> list = new ArrayList<>();
list.add("a");
list.add(0, "first"); // 지정 위치에 삽입
String x = list.get(1);
list.remove("a");
int size = list.size();
boolean has = list.contains("b");
var fixed = List.of("a", "b"); // 불변

Set

Set은 중복을 제거합니다. HashSet은 무순서 O(1), LinkedHashSet은 삽입 순서 유지, TreeSet은 정렬. 중복 add는 false를 반환합니다.

1
2
3
4
5
6
Set<Integer> set = new HashSet<>();
set.add(1);
boolean added = set.add(1); // false(이미 존재)
boolean has = set.contains(1);
var ordered = new LinkedHashSet<String>(); // 순서 유지
var sorted = new TreeSet<Integer>(); // 오름차순

Map

Map은 키-값 매핑. HashMap은 O(1), LinkedHashMap은 삽입 순서 유지, TreeMap은 키 기준 정렬. getOrDefault로 안전하게 조회합니다.

1
2
3
4
5
6
7
8
Map<String, Integer> map = new HashMap<>();
map.put("Nick", 30);
Integer age = map.get("Nick");
int safe = map.getOrDefault("X", 0);
map.computeIfAbsent("k", k -> 1); // 없으면 계산
for (Map.Entry<String, Integer> e : map.entrySet()) {
System.out.println(e.getKey() + "=" + e.getValue());
}

Queue / Deque

Queue는 FIFO(offer/poll/peek), Deque는 양쪽 끝 큐(addFirst/addLast). ArrayDeque가 LinkedList보다 빠릅니다.

1
2
3
4
5
6
7
Queue<Integer> q = new ArrayDeque<>();
q.offer(1); q.offer(2);
int head = q.peek(); // 1(삭제 안 함)
int out = q.poll(); // 1(꺼냄)
Deque<Integer> d = new ArrayDeque<>();
d.addFirst(1); d.addLast(2);
int first = d.pollFirst();

순회와 반복

향상된 for / Iterator / forEach로 컬렉션을 순회합니다. 순회 중 삭제는 Iterator.remove 또는 모아둔 뒤 삭제합니다.

1
2
3
4
5
6
7
8
for (String s : list) { }
list.forEach(System.out::println);
// 순회하며 삭제:
Iterator<String> it = list.iterator();
while (it.hasNext()) {
if (it.next().isEmpty()) it.remove();
}
// 확장 for 에서는 직접 remove 불가(예외 발생)

정렬

List.sort 또는 Collections.sort로 정렬, Comparator로 순서를 정의, comparing으로 체이닝합니다.

1
2
3
4
5
6
7
List<Integer> nums = new ArrayList<>(List.of(3, 1, 2));
nums.sort(null); // 자연 순서 오름차순
nums.sort(Comparator.reverseOrder()); // 내림차순
// 커스텀 객체 정렬:
people.sort(Comparator
.comparing(Person::age)
.thenComparing(Person::name));

Stream 체이닝

filter/map/sorted/distinct/limit를 체인으로 연결하고, 최종 연산이 실행을 트리거합니다. collect로 다시 컬렉션으로 모읍니다.

1
2
3
4
5
6
7
8
var result = people.stream()
.filter(p -> p.age() >= 18)
.sorted(Comparator.comparing(Person::age).reversed())
.map(Person::name)
.distinct()
.limit(10)
.toList();
// 지연:최종 연산이 없으면 실제로 실행되지 않음

그룹화와 집계

Collectors.groupingBy는 키로 그룹화, partitioningBy는 boolean으로 분할, summarizingInt는 통계를 냅니다.

1
2
3
4
5
6
7
Map<String, List<Order>> byRegion = orders.stream()
.collect(Collectors.groupingBy(Order::region));
Map<Boolean, List<Integer>> part = nums.stream()
.collect(Collectors.partitioningBy(n -> n % 2 == 0));
// 집계:Collectors.summingInt / averagingInt
int total = orders.stream()
.collect(Collectors.summingInt(Order::amount));

불변 컬렉션

List.of, Set.of, Map.of는 불변 컬렉션을 만듭니다. Collections.unmodifiableList는 읽기 전용 뷰를 감쌉니다.

1
2
3
4
5
var fixed = List.of(1, 2, 3); // 불변
// fixed.add(4); // UnsupportedOperationException
var mutable = new ArrayList<>(fixed); // 가변 복사본
var view = Collections.unmodifiableList(mutable);
// 래핑 후에는 어떤 수정도 예외 발생

9.메모리와 성능

GC, 약한 참조, 메모리 누수, 문자열 연결 성능, 버퍼.

문자열 연결 성능

루프 내 + 연결은 매번 (불변의) String을 새로 만듭니다. StringBuilder로 한 번에 모으면 성능이 크게 향상됩니다.

1
2
3
4
5
6
7
// 느림:반복 결합으로 중간 객체 대량 생성
String s = "";
for (int i = 0; i < 1000; i++) s += i;
// 빠름:StringBuilder 로 제자리 추가
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 1000; i++) sb.append(i);
String r = sb.toString();

약한 참조와 부드러운 참조

WeakReference는 GC를 막지 않습니다(캐시용), SoftReference는 메모리 부족 시에만 회수, PhantomReference는 회수 후 정리합니다.

1
2
3
4
5
WeakReference<BigObj> weak = new WeakReference<>(new BigObj());
System.gc();
BigObj obj = weak.get(); // 이미 null 일 수 있음
// 소프트 참조:메모리 부족 시에만 회수(이미지 캐시에 적합)
SoftReference<Image> soft = new SoftReference<>(image);

메모리 누수

흔한 누수: 정적 컬렉션이 객체를 보유, 자원을 닫지 않음, 리스너 미해제, ThreadLocal 미정리. 긴 수명의 보유자가 짧은 수명의 객체를 붙잡지 않도록 합니다.

1
2
3
4
5
// 정적 Map 이 계속 커짐 = 누수
static Map<String, Session> sessions = new HashMap<>();
// 사용 후 제거:sessions.remove(id)
// 또는 WeakHashMap<String, Session> 로 변경
// 리소스는 사용 후 닫기:try (var c = open()) { }

ThreadLocal

ThreadLocal은 스레드마다 독립된 변수 사본을 둡니다. 웹 컨테이너 스레드 풀에서는 remove()를 잊으면 요청 간 누수가 발생합니다.

1
2
3
4
5
private static final ThreadLocal<SimpleDateFormat> fmt =
ThreadLocal.withInitial(SimpleDateFormat::new);
String d = fmt.get().format(date);
// 핵심:사용 후 반드시 remove(),특히 스레드 풀 환경
// fmt.remove(); 않으면 스레드 재사용으로 객체 누수

ByteBuffer

ByteBuffer의 다이렉트 메모리(allocateDirect)는 GC 압력을 줄여 NIO와 대량 전송에 적합합니다. 수동 관리가 필요합니다.

1
2
3
4
5
6
7
ByteBuffer buf = ByteBuffer.allocate(1024); // 힙
ByteBuffer direct = ByteBuffer.allocateDirect(1024); // 직접 메모리
buf.putInt(42).putDouble(3.14);
buf.flip(); // 읽기 모드로 전환
int i = buf.getInt();
double d = buf.getDouble();
buf.clear(); // 버퍼 재사용

OutOfMemoryError

OOM은 메모리 고갈을 뜻합니다: Heap Space 포화, Metaspace 클래스 과다, Direct buffer 과다. JVM 플래그를 조정하거나 누수를 찾으세요.

1
2
3
4
5
// JVM 파라미터:
// $ java -Xms512m -Xmx2g -XX:+HeapDumpOnOutOfMemoryError App
// -Xmx 최대 힙、-Xms 초기 힙
// 힙 오버플로 덤프:.hprof 생성 후 도구로 분석
// 조사:컬렉션 무제한 증가 / 정적 보유 / 리소스 누수 찾기

배열 복사

System.arraycopy는 배열 구간을 빠르게 복사합니다. Arrays.copyOf는 복사하고 필요 시 늘립니다. 수동 루프는 느립니다.

1
2
3
4
5
int[] src = { 1, 2, 3, 4 };
int[] dst = new int[4];
System.arraycopy(src, 1, dst, 0, 2); // [2, 3, 0, 0]
int[] copy = Arrays.copyOf(src, src.length);
int[] grown = Arrays.copyOf(src, 6); // 확장하며 0 으로 채움

객체 풀 재사용

큰 객체를 자주 생성하면 GC 압력이 커집니다. 객체 풀로 재사용 인스턴스를 캐시하지만, 스레드 안전과 반환 경로에 주의하세요.

1
2
3
4
5
6
7
8
9
10
11
// 간단한 객체 풀:큰 객체의 잦은 new 를 피함
final class ConnectionPool {
private final ArrayDeque<Connection> idle = new ArrayDeque<>();
synchronized Connection acquire() {
return idle.isEmpty() ? new Connection() : idle.poll();
}
synchronized void release(Connection c) {
idle.push(c); // 반납 후 재사용
}
}
// 풀이 클수록 메모리 사용도 증가、재사용 이익과 트레이드오프

10.객체 지향

클래스, 캡슐화, 상속, 다형성, 추상 클래스, 인터페이스, 접근 제한자.

클래스와 객체

class는 데이터 타입을 정의합니다: 필드는 상태, 메서드는 동작, 생성자는 초기화, new는 인스턴스 생성.

1
2
3
4
5
6
7
8
9
10
public class Person {
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public String greet() { return "Hi, " + name; }
}
var p = new Person("Nick", 30);

캡슐화

필드는 private, 메서드는 public으로 접근을 제어합니다. getter/setter에 검증 로직을 넣어 불변식을 보호합니다.

1
2
3
4
5
6
7
8
public class Account {
private double balance;
public double getBalance() { return balance; }
public void deposit(double amount) {
if (amount <= 0) throw new IllegalArgumentException();
balance += amount;
}
}

상속

extends는 베이스 클래스를 상속합니다. Java는 단일 상속입니다. super로 부모 생성자/메서드를 호출하며, 자식은 부모의 일종(is-a)입니다.

1
2
3
4
5
6
7
8
9
public class Animal {
public void speak() { System.out.println("..."); }
}
public class Dog extends Animal {
@Override
public void speak() { System.out.println("Woof"); }
}
Animal a = new Dog();
a.speak(); // "Woof"(다형성)

다형성

부모 클래스/인터페이스 참조가 자식 인스턴스를 가리키고, 가상 메서드는 실제 타입으로 디스패치됩니다. @Override로 명시하세요.

1
2
3
4
5
6
7
8
9
10
11
public class Shape {
public double area() { return 0; }
}
public class Circle extends Shape {
private final double r;
public Circle(double r) { this.r = r; }
@Override
public double area() { return Math.PI * r * r; }
}
Shape s = new Circle(2);
double a = s.area(); // 12.57(실제 타입에 따라)

추상 클래스

abstract 클래스는 인스턴스화할 수 없으며, abstract 메서드를 가질 수 있습니다(자식이 반드시 구현). 상태를 공유하는 템플릿 베이스로 사용합니다.

1
2
3
4
5
6
7
8
9
10
11
12
public abstract class Shape {
public abstract double area(); // 구현 없음
public void describe() {
System.out.println("Area: " + area());
}
}
public class Square extends Shape {
private final double side;
public Square(double s) { this.side = s; }
@Override
public double area() { return side * side; }
}

인터페이스

interface는 계약을 정의합니다: 메서드는 기본적으로 public abstract. Java 8+에서 default/static 메서드가 추가되었고, 다중 구현 가능합니다.

1
2
3
4
5
6
7
8
9
10
11
public interface Logger {
void log(String msg); // 추상 메서드
default void warn(String m) { // default 구현
log("[WARN] " + m);
}
}
public class ConsoleLogger implements Logger {
@Override
public void log(String msg) { System.out.println(msg); }
}
Logger l = new ConsoleLogger(); // 인터페이스 지향

접근 제한자

public은 전체 공개, protected는 패키지+자식, 기본은 패키지, private는 같은 클래스만. 클래스 멤버는 기본적으로 패키지 비공개.

1
2
3
4
5
6
7
public class Demo {
public int pub; // 어디서든
protected int prot; // 패키지 내 + 하위 클래스
int def; // 패키지 내
private int priv; // 이 클래스 내에서만
}
// 최상위 클래스는 public 이거나 패키지 내 가시(기본)만 가능

static 멤버

static 필드/메서드는 클래스 소속이며 인스턴스 소속이 아닙니다. static 메서드는 인스턴스 멤버에 접근할 수 없습니다. static 블록으로 초기화합니다.

1
2
3
4
5
6
7
8
public class Counter {
private static int count; // 클래스 레벨
public static void inc() { count++; }
public static int get() { return count; }
}
Counter.inc(); // 클래스를 통해 접근
Counter.inc();
System.out.println(Counter.get()); // 2

내부 클래스와 익명 클래스

내부 클래스, 익명 클래스, 람다는 콜백을 간결하게 합니다. static 내부 클래스는 외부 인스턴스를 잡지 않아 메모리 누수를 피합니다.

1
2
3
4
5
6
7
8
// 익명 클래스(Java 8 이전 방식):
Runnable r = new Runnable() {
@Override public void run() { }
};
// 오히려 Lambda 권장:
Runnable r2 = () -> System.out.println("hi");
// 정적 중첩 클래스:static class Builder { }
// 내부 클래스는 바깥 인스턴스 참조를 자동 보유(누수 주의)

11.예외 처리

try/catch/finally, 예외 계층, 검사/비검사 예외, try-with-resources, 사용자 정의 예외.

try / catch / finally

try는 오류 가능 코드, catch는 처리, finally는 성공/실패 상관없이 실행(자원 정리). 예외는 상위로 전파됩니다.

1
2
3
4
5
6
7
try {
int n = Integer.parseInt("abc");
} catch (NumberFormatException e) {
System.out.println("bad number: " + e.getMessage());
} finally {
System.out.println("cleanup"); // 항상 실행됨
}

다중 catch

여러 catch는 타입으로 매칭되며, 구체적인 예외를 먼저, 광범위한 예외를 뒤에 둡니다. 멀티 catch | 로 관련 없는 타입을 묶을 수 있습니다.

1
2
3
4
5
6
7
8
9
try {
process();
} catch (FileNotFoundException e) {
// 구체적인 예외
} catch (IOException e) {
// 일반 예외
} catch (IllegalArgumentException | IllegalStateException e) {
// multi-catch:| 로 결합(상속 관계 없는 것)
}

throw와 재throw

throw new는 예외를 던집니다. catch 안의 throw;(Java 7+)는 원본을 그대로 재던지며 스택을 유지합니다. 메서드의 throws로 검사 예외 가능성을 선언합니다.

1
2
3
4
5
6
7
throw new IllegalArgumentException("invalid value");
public void read() throws IOException {
throw new IOException("io error");
}
try { read(); } catch (IOException e) {
throw new RuntimeException(e); // 래핑 후 재던짐
}

사용자 정의 예외

사용자 정의 예외는 Exception(검사) 또는 RuntimeException(비검사)을 상속합니다. 관례상 클래스명은 Exception으로 끝냅니다.

1
2
3
4
5
6
7
8
9
public class ConfigException extends RuntimeException {
public ConfigException(String message) {
super(message);
}
public ConfigException(String message, Throwable cause) {
super(message, cause);
}
}
throw new ConfigException("bad config");

검사 예외와 비검사 예외

검사 예외(IOException 등)는 catch 또는 선언이 필수이며, 비검사 예외(RuntimeException 계열)는 컴파일 시 강제되지 않습니다.

1
2
3
4
5
6
7
// 검사 예외:catch 또는 throws 필수
public void read() throws IOException {
Files.readString(Path.of("a.txt"));
}
// 비검사 예외:처리하지 않아도 됨
int x = Integer.parseInt("abc"); // 컴파일은 통과、실행 시 예외
// 커스텀 예외는 RuntimeException 하위 클래스가 일반적

try-with-resources

try (resource) { }는 AutoCloseable.close()를 자동으로 호출하며, 예외 시에도 닫습니다. 수동 finally 닫기를 대체합니다.

1
2
3
4
5
try (var reader = new BufferedReader(
Files.newBufferedReader(Path.of("a.txt")))) {
String line = reader.readLine();
} // reader 가 자동으로 닫힘
// 여러 리소스:try (var a = ...; var b = ...) { }

예외의 비용

예외 포획은 비용이 크므로(스택 워킹) 제어 흐름에 쓰지 마세요. 예측 가능한 오류는 반환값, null 체크, Optional을 사용합니다.

1
2
3
4
5
6
// 느림:예외를 흐름 제어에 사용
boolean ok1;
try { Integer.parseInt(s); ok1 = true; }
catch (NumberFormatException e) { ok1 = false; }
// 빠름:빈 값/정규식으로 먼저 판단
boolean ok2 = s != null && s.matches("\\d+");

로깅과 예외

예외는 (스택과 함께) 로깅하고, System.out으로 찍고 넘기지 마세요. 로깅 프레임워크(SLF4J + Logback)로 레벨별 출력합니다.

1
2
3
4
5
6
7
// SLF4J + Logback:
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
private static final Logger log =
LoggerFactory.getLogger(App.class);
// 예외 스택트레이스 기록:
// log.error("failed", e); // e 로 스택트레이스 보존

12.파일과 IO

Files/Path 기반 모던 IO, Scanner, Reader/Writer, 스트림 읽기.

Files 읽기/쓰기

java.nio.file.Files는 모던 API: readString/writeString, readAllLines, copy/move/delete.

1
2
3
4
5
String content = Files.readString(Path.of("in.txt"));
Files.writeString(Path.of("out.txt"), content);
List<String> lines = Files.readAllLines(Path.of("in.txt"));
Files.copy(Path.of("a"), Path.of("b"), StandardCopyOption.REPLACE_EXISTING);
Files.deleteIfExists(Path.of("tmp"));

Scanner 입력

Scanner는 콘솔이나 파일을 읽습니다: nextLine은 한 줄, nextInt/nextDouble은 타입별. hasNext로 입력 잔여를 확인합니다.

1
2
3
4
5
6
7
Scanner sc = new Scanner(System.in);
System.out.print("Name: ");
String name = sc.nextLine();
int age = sc.nextInt();
// 파일에서 읽기:
var file = new Scanner(Path.of("a.txt"));
while (file.hasNextLine()) System.out.println(file.nextLine());

Reader / Writer

문자 스트림으로 텍스트를 입출력: BufferedReader로 줄 단위 읽기, BufferedWriter로 쓰기, try-with-resources로 자동 닫기.

1
2
3
4
5
6
7
8
9
10
try (var reader = new BufferedReader(
new FileReader("a.txt"));
var writer = new BufferedWriter(
new FileWriter("b.txt"))) {
String line;
while ((line = reader.readLine()) != null) {
writer.write(line);
writer.newLine();
}
} // 두 스트림이 자동으로 닫힘

바이트 스트림

InputStream/OutputStream은 바이트 단위: FileInputStream, BufferedInputStream으로 버퍼링. 데이터 이동에는 transferTo.

1
2
3
4
5
6
7
8
9
try (var in = new FileInputStream("a.bin");
var out = new FileOutputStream("b.bin")) {
byte[] buf = new byte[8192];
int read;
while ((read = in.read(buf)) != -1) {
out.write(buf, 0, read);
}
}
// 간단 버전:Files.copy(Path.of("a"), Path.of("b"))

콘솔 IO

System.out은 출력, System.in은 입력, System.err은 오류. printf로 포맷 출력.

1
2
3
4
5
System.out.print("No newline");
System.out.println("With newline");
System.out.printf("%s is %d years old%n", "Nick", 30);
System.err.println("Error message"); // 표준 오류
// 포맷:%n 은 줄바꿈(크로스 플랫폼)、\n 을 쓰지 않음

Path 경로

Path는 경로를 나타냅니다. resolve로 결합, getFileName으로 이름, toAbsolutePath로 절대 경로, exists로 존재 확인, Files.walk로 재귀 순회.

1
2
3
4
5
6
7
8
9
Path dir = Path.of("data");
Path file = dir.resolve("a.txt"); // data/a.txt
String name = file.getFileName().toString();
boolean exists = Files.exists(file);
// 재귀적으로 나열:
try (var stream = Files.walk(dir)) {
stream.filter(p -> p.toString().endsWith(".txt"))
.forEach(System.out::println);
}

직렬화

ObjectOutputStream/ObjectInputStream으로 객체를 직렬화(클래스는 Serializable 구현 필요). 실무에서는 JSON이 더 흔합니다.

1
2
3
4
5
6
7
8
9
10
class User implements Serializable {
private static final long serialVersionUID = 1L;
String name;
}
// 쓰기:
try (var out = new ObjectOutputStream(
new FileOutputStream("u.bin"))) {
out.writeObject(new User());
}
// 읽기:ObjectInputStream in = ...; User u = (User) in.readObject();

임시 파일

Files.createTempFile은 시스템 임시 디렉터리에 파일을 만듭니다. 사용 후 Files.deleteIfExists로 정리해 잔여를 남기지 마세요.

1
2
3
4
5
6
Path tmp = Files.createTempFile("app-", ".log");
System.out.println(tmp); // 시스템 임시 디렉터리
Files.writeString(tmp, "temp data");
// 사용 후 즉시 삭제、잔여물 방지:
Files.deleteIfExists(tmp);
// 임시 디렉터리:Files.createTempDirectory("app");

13.자주 겪는 함정 (FAQ)

Java 개발자가 가장 자주 겪는 함정: == vs equals, 박싱 캐시, 동시성, 예외, 컬렉션 변경.

== vs equals

문자열/객체의 내용 비교는 반드시 equals를, ==는 참조 비교입니다. 리터럴 풀로 ==가 우연히 true가 될 수 있지만 의존하면 안 됩니다.

1
2
3
4
5
6
7
// BAD:== 는 참조 비교、리터럴 풀이 문제를 가림
String a = "nick";
String b = new String("nick");
boolean bad = a == b; // false
// GOOD:equals 는 내용 비교
boolean good = a.equals(b); // true

Integer 캐시 풀

자동 박싱된 -128~127은 캐시되어 ==가 true일 수 있으나, 범위를 벗어나면 false입니다. 객체 비교는 항상 equals를 쓰세요.

1
2
3
4
5
6
7
// BAD:== 로 박싱된 값을 비교
Integer a = 200, b = 200;
boolean bad = a == b; // false
// GOOD:equals 로 비교
Integer c = 200, d = 200;
boolean good = c.equals(d); // true

null 체크와 NPE

null에 대해 메서드를 호출하면 반드시 NPE가 납니다. nullable 값은 Optional 또는 사전 null 체크로 처리하고, 여기저기 try-catch NPE를 쓰지 마세요.

1
2
3
4
5
6
7
8
// BAD:null 체크 없음
String s = find();
System.out.println(s.length()); // NPE 가능
// GOOD:null 체크 또는 Optional
String t = find();
System.out.println(t == null ? 0 : t.length());
// 또는 Optional.ofNullable(find()).map(String::length).orElse(0)

예외 삼키기

catch 후 아무 처리도 하지 않으면 예외를 삼키는 것이며 디버깅이 어렵습니다. 최소한 로깅하거나 적절한 예외로 감싸 재던지세요.

1
2
3
4
5
6
7
8
// BAD:조용히 삼킴
catch (IOException e) { }
// GOOD:기록하거나 상위로 던짐
catch (IOException e) {
log.error("read failed", e);
throw new RuntimeException("read failed", e);
}

equals와 hashCode

equals를 오버라이드하면 hashCode도 반드시 오버라이드해야 합니다. 그렇지 않으면 HashSet/HashMap 동작이 깨집니다(논리적으로 같은 객체가 다른 키로 취급).

1
2
3
4
5
6
7
8
// BAD:equals 만 오버라이드
class A { public boolean equals(Object o) { ... } } // hashCode 불일치
// GOOD:둘 다 동시에 오버라이드
class B {
public boolean equals(Object o) { /* 동일 로직 */ }
public int hashCode() { return Objects.hash(fields); }
}

순회 중 삭제

향상된 for 안에서 List.remove를 호출하면 ConcurrentModificationException이 발생합니다. Iterator.remove 또는 모아둔 뒤 삭제하세요.

1
2
3
4
5
6
7
8
9
10
// BAD:확장 for 안에서 삭제
for (String s : list) {
if (s.isEmpty()) list.remove(s); // 예외 발생
}
// GOOD:Iterator 로 삭제
Iterator<String> it = list.iterator();
while (it.hasNext()) {
if (it.next().isEmpty()) it.remove();
}

날짜의 가변성

구 Date/Calendar는 가변이라 오류가 많습니다. java.time(LocalDate, LocalDateTime)으로 통일하면 불변이고 안전합니다.

1
2
3
4
5
6
7
// BAD:가변 Date
Date d = new Date();
d.setTime(0); // 상태가 외부에서 변경됨
// GOOD:java.time 은 불변
LocalDate today = LocalDate.now();
LocalDate next = today.plusDays(1); // 새 객체를 반환

스레드 안전성

HashMap/ArrayList는 스레드 안전하지 않아 다중 스레드 쓰기에서 깨집니다. ConcurrentHashMap 또는 동기 래퍼를 사용하세요.

1
2
3
4
5
6
7
8
// BAD:여러 스레드가 HashMap 에 쓰기
Map<String, Integer> map = new HashMap<>();
// 동시 put 은 내부 구조를 손상시킬 수 있음
// GOOD:동시성 컬렉션
Map<String, Integer> safe = new ConcurrentHashMap<>();
safe.put("k", 1);
// 또는 Collections.synchronizedMap(new HashMap<>())

Stream의 null

Stream 요소가 null이면 filter/map에서 NPE가 날 수 있습니다. 먼저 filter(Objects::nonNull)로 거르세요.

1
2
3
4
5
6
7
8
// BAD:null 요소를 map 에 전달
list.stream().map(String::toUpperCase) // null 이 있으면 NPE
// GOOD:먼저 null 제거
list.stream()
.filter(Objects::nonNull)
.map(String::toUpperCase)
.toList();

루프 내 연결

루프 내 += 연결은 중간 String을 대량 생성해 느립니다. StringBuilder를 사용하세요.

1
2
3
4
5
6
7
8
// BAD:반복 결합
String s = "";
for (int i = 0; i < 10000; i++) s += i;
// GOOD:StringBuilder
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 10000; i++) sb.append(i);
String r = sb.toString();

14.동시성과 스레드

Thread, synchronized, 스레드 풀 (ExecutorService), CompletableFuture.

Thread

Thread 또는 Runnable로 스레드를 만들고, start로 시작, join으로 대기, sleep으로 일시정지합니다. 대부분의 경우 스레드 풀이 더 적합합니다.

1
2
3
4
5
6
7
8
9
10
// 방법 1:Thread 상속
Thread t = new Thread(() -> {
System.out.println("worker");
});
// 방법 2:Runnable
Runnable task = () -> System.out.println("run");
Thread t2 = new Thread(task);
t2.start(); // 시작
// t2.join(); // 완료 대기
// Thread.sleep(100); // 밀리초 단위 정지

synchronized

synchronized는 락으로 상호배제를 보장합니다. 인스턴스 메서드는 this, 정적 메서드는 Class를 잠그고, 블록은 임의 객체를 잠글 수 있습니다.

1
2
3
4
5
6
7
8
private int count;
public synchronized void increment() { count++; }
// 동일:synchronized(this) { count++; }
// 정적 동기화:
public static synchronized void inc2() { }
// 락 객체는 전용 final 필드가 일반적:
private final Object lock = new Object();
synchronized (lock) { /* 임계 구역 */ }

volatile

volatile은 가시성(다른 스레드에서 즉시 보임)을 보장하지만 원자성은 보장하지 않습니다. 플래그는 volatile, 카운터는 AtomicInteger.

1
2
3
4
5
6
7
private volatile boolean running = true;
// 다른 스레드에서 즉시 읽기 가능
// 단 volatile 은 원자 연산을 해결하지 않음:
// volatile int n; n++; // 비원자적!
// AtomicInteger 사용:
AtomicInteger counter = new AtomicInteger();
counter.incrementAndGet();

스레드 풀

ExecutorService는 스레드 재사용을 관리합니다. newFixedThreadPool / newCachedThreadPool을 쓰고, 끝나면 shutdown을 호출합니다.

1
2
3
4
5
6
7
ExecutorService pool = Executors.newFixedThreadPool(4);
for (int i = 0; i < 10; i++) {
pool.submit(() -> System.out.println(Thread.currentThread().getName()));
}
pool.shutdown(); // 새 작업을 받지 않음
// pool.awaitTermination(5, TimeUnit.SECONDS);
// 권장:Executors.newVirtualThreadPerTaskExecutor()(Java 21)

Future와 Callable

Callable은 반환값이 있는 태스크, Future.get은 결과를 블록해 가져옵니다. FutureTask는 수동 제어가 가능합니다.

1
2
3
4
5
6
7
8
ExecutorService pool = Executors.newFixedThreadPool(2);
Future<Integer> f = pool.submit(() -> {
return compute(); // Callable 은 반환값 있음
});
int result = f.get(); // 블로킹하며 결과 대기
// f.get(5, TimeUnit.SECONDS); // 타임아웃
// f.cancel(true); // 취소
pool.shutdown();

CompletableFuture

비동기 조합: thenApply로 변환, thenCombine으로 결합, allOf로 다수 대기. 콜백은 체인되며 호출 스레드를 막지 않습니다.

1
2
3
4
5
6
7
8
CompletableFuture.supplyAsync(() -> fetch())
.thenApply(data -> parse(data))
.thenAccept(result -> System.out.println(result))
.exceptionally(ex -> { log(ex); return null; });
// 여러 개 대기:
var all = CompletableFuture.allOf(f1, f2);
all.join();
// join() 은 블로킹 대기、결과는 get() 으로 꺼냄

동시성 컬렉션

ConcurrentHashMap, CopyOnWriteArrayList, BlockingQueue는 스레드 안전합니다. 수동 락의 일반 컬렉션보다 이들을 우선하세요.

1
2
3
4
5
6
7
Map<String, Integer> map = new ConcurrentHashMap<>();
map.put("k", 1);
map.compute("k", (k, v) -> v == null ? 1 : v + 1);
Queue<Task> q = new ArrayBlockingQueue<>(100);
q.offer(task); // 가득 차면 false 반환
Task t = q.poll(); // 비어 있으면 null 반환
// CopyOnWriteArrayList 는 읽기 많고 쓰기 적은 상황에 적합

Lock 인터페이스

ReentrantLock은 synchronized보다 유연합니다: 타임아웃, 인터럽트, 공정. finally에서 반드시 unlock해야 합니다.

1
2
3
4
5
6
7
8
9
10
11
private final ReentrantLock lock = new ReentrantLock();
void work() {
lock.lock();
try {
/* 임계 구역 */
} finally {
lock.unlock(); // 반드시 finally 에서 해제
}
}
// lock.tryLock(1, TimeUnit.SECONDS) 타임아웃 포함
// ReadWriteLock 은 읽기/쓰기 분리로 동시성 향상

15.네트워크와 HTTP

HttpClient, URL 요청, JSON 직렬화, WebSocket.

HttpClient 기본

java.net.http.HttpClient(Java 11+)로 HTTP 요청을 보냅니다. send는 동기, sendAsync는 비동기. 빌더로 설정합니다.

1
2
3
4
5
6
7
8
HttpClient client = HttpClient.newHttpClient();
HttpRequest req = HttpRequest.newBuilder()
.uri(URI.create("https://example.com"))
.build();
HttpResponse<String> resp =
client.send(req, HttpResponse.BodyHandlers.ofString());
System.out.println(resp.statusCode());
System.out.println(resp.body());

POST와 JSON

JSON 바디를 가진 POST 요청. Jackson 또는 Gson으로 JSON을 (역)직렬화합니다.

1
2
3
4
5
6
7
8
9
String json = "{\"name\":\"Nick\"}";
HttpRequest req = HttpRequest.newBuilder()
.uri(URI.create("https://api.example.com/users"))
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(json))
.build();
var resp = client.send(req, HttpResponse.BodyHandlers.ofString());
// 파싱:Jackson ObjectMapper om = new ObjectMapper();
// User u = om.readValue(resp.body(), User.class);

비동기 요청

sendAsync는 CompletableFuture를 반환하며 호출 스레드를 막지 않습니다. 다중 요청을 thenApply로 병렬 처리합니다.

1
2
3
4
5
6
7
HttpClient client = HttpClient.newHttpClient();
var req = HttpRequest.newBuilder(URI.create("https://example.com")).build();
client.sendAsync(req, HttpResponse.BodyHandlers.ofString())
.thenApply(HttpResponse::body)
.thenAccept(System.out::println)
.join(); // 블로킹 대기(데모)
// 진짜 비동기 환경에서는 join 호출 안 함

URL과 URLConnection

구 API인 URL/HttpURLConnection은 단순 요청용. 대부분은 HttpClient가 더 간결합니다. 쿼리 파라미터는 URL 인코딩이 필요합니다.

1
2
3
4
5
6
7
URL url = new URL("https://example.com");
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod("GET");
int code = conn.getResponseCode();
String body = new String(conn.getInputStream().readAllBytes());
// 파라미터 인코딩:
String q = URLEncoder.encode("中文", StandardCharsets.UTF_8);

JSON 파싱

Jackson과 Gson이 주류 JSON 라이브러리입니다. @JsonProperty로 필드명을 매핑하고, Java 17의 record와 결합할 수 있습니다.

1
2
3
4
5
6
7
8
9
// Gson:
Gson gson = new Gson();
User u = gson.fromJson(json, User.class);
String out = gson.toJson(u);
// Jackson:
ObjectMapper om = new ObjectMapper();
User u2 = om.readValue(json, User.class);
String s2 = om.writeValueAsString(u2);
// Jackson 의존성은 pom 에 추가

Web API 호출

조합: 요청 구성 → 상태 코드 확인 → 역직렬화. 404/500 같은 비성공 상태를 처리합니다.

1
2
3
4
5
6
7
8
9
10
var req = HttpRequest.newBuilder()
.uri(URI.create("https://api.github.com/users/" + login))
.header("User-Agent", "MyApp")
.GET().build();
var resp = client.send(req, BodyHandlers.ofString());
if (resp.statusCode() == 200) {
var user = om.readValue(resp.body(), User.class);
} else {
System.err.println("status: " + resp.statusCode());
}

WebSocket

java.net.http.WebSocket은 양방향 장기 연결입니다. Listener 콜백으로 메시지를 받고, onOpen/onText에서 처리합니다.

1
2
3
4
5
6
7
8
9
10
11
var ws = HttpClient.newHttpClient()
.newWebSocketBuilder()
.buildAsync(URI.create("wss://example.com/ws"),
new WebSocket.Listener() {
public void onOpen(WebSocket webSocket) {
webSocket.sendText("hello", true);
}
// onText/onError/onClose 콜백
})
.join();
ws.sendText("msg", true);

타임아웃과 재시도

HttpRequest.timeout은 요청 타임아웃, HttpClient.connectTimeout은 연결 타임아웃입니다. 타임아웃 시 HttpTimeoutException이 발생합니다.

1
2
3
4
5
6
7
8
HttpClient client = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(5))
.build();
var req = HttpRequest.newBuilder(uri)
.timeout(Duration.ofSeconds(10))
.build();
// 타임아웃 시 HttpTimeoutException 발생、잡아서 처리
// 간단한 재시도:catch 를 반복하며 재시도(백오프 포함)

16.날짜와 시간

java.time 로컬 날짜/시간, Instant, Duration/Period, 포맷팅.

LocalDate

LocalDate는 날짜만(시간 없음): now, of, plusDays, minusMonths. 불변이며 스레드 안전합니다.

1
2
3
4
5
6
7
LocalDate today = LocalDate.now();
LocalDate date = LocalDate.of(2024, 1, 1);
LocalDate next = date.plusDays(7);
LocalDate prev = date.minusMonths(1);
int year = date.getYear();
boolean leap = date.isLeapYear();
DayOfWeek dow = date.getDayOfWeek();

LocalTime

LocalTime은 시간만(날짜 없음): now, of, plusMinutes. LocalDate와 합쳐 LocalDateTime을 만듭니다.

1
2
3
4
5
6
LocalTime time = LocalTime.now();
LocalTime t = LocalTime.of(14, 30, 0);
LocalTime later = t.plusHours(1);
int hour = t.getHour();
// 날짜와 조합:
LocalDateTime dt = LocalDateTime.of(date, t);

Instant 타임스탬프

Instant는 UTC 시점(에포크 초/나노초)이며, 타임존을 넘나드는 저장에 적합합니다. 시스템 시각은 System.currentTimeMillis를 사용합니다.

1
2
3
4
5
6
7
Instant now = Instant.now();
long epochMilli = now.toEpochMilli();
Instant fromEpoch = Instant.ofEpochMilli(1_700_000_000_000L);
// 시스템 시간에서:
long nowMs = System.currentTimeMillis();
// Instant 와 LocalDateTime 상호 변환에는 타임존 필요:
LocalDateTime ldt = LocalDateTime.ofInstant(now, ZoneId.systemDefault());

Duration과 Period

Duration은 시간 기반 양(초/나노초), Period는 날짜 기반 양(년/월/일)입니다.

1
2
3
4
5
6
Duration d = Duration.ofMinutes(90);
long seconds = d.toSeconds(); // 5400
Duration between = Duration.between(t1, t2);
Period p = Period.of(1, 2, 3); // 1년 2개월 3일
Period since = Period.between(birthday, today);
int years = since.getYears();

포맷과 파싱

DateTimeFormatter로 포맷과 파싱. ISO 등 내장 포맷과 yyyy-MM-dd HH:mm:ss 같은 커스텀 패턴을 지원합니다.

1
2
3
4
5
6
DateTimeFormatter fmt = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm");
String s = LocalDateTime.now().format(fmt);
LocalDateTime parsed = LocalDateTime.parse("2024-01-01 12:30", fmt);
// 미리 정의된 형식:
String iso = LocalDate.now().format(DateTimeFormatter.ISO_DATE);
// 파싱 시 예외 주의:DateTimeParseException

시간대

ZonedDateTime은 시간대를 포함하고, ZoneOffset는 고정 오프셋입니다. ZoneId.systemDefault는 로컬. 저장은 UTC로 합니다.

1
2
3
4
5
6
7
ZoneId shanghai = ZoneId.of("Asia/Shanghai");
ZonedDateTime zdt = ZonedDateTime.now(shanghai);
Instant utc = zdt.toInstant();
ZonedDateTime back = utc.atZone(ZoneId.of("UTC"));
// 고정 오프셋:
ZoneOffset offset = ZoneOffset.ofHours(8);
OffsetDateTime odt = OffsetDateTime.of(ldt, offset);

구 API 변환

java.util.Date/Calendar와 새 API의 상호 변환. Date는 가변이고 오류가 많으니 새 코드는 java.time을 사용하세요.

1
2
3
4
5
6
7
8
// Date → Instant:
Instant inst = new Date().toInstant();
// Instant → Date:
Date d = Date.from(Instant.now());
// 변환:
LocalDateTime ldt = LocalDateTime.ofInstant(
inst, ZoneId.systemDefault());
// Calendar 는 이미 시대에 뒤처짐, 사용하지 말 것

타임스탬프 유틸

System.currentTimeMillis는 밀리초, nanoTime은 나노초 차이(간격 측정 전용, 절대 시각 아님). 타임스탬프와 포맷 간 상호 변환.

1
2
3
4
5
6
7
long start = System.nanoTime();
compute();
long elapsedNs = System.nanoTime() - start; // 간격
// 밀리초 타임스탬프 → LocalDateTime:
LocalDateTime t = LocalDateTime.ofInstant(
Instant.ofEpochMilli(System.currentTimeMillis()),
ZoneId.systemDefault());

17.프로세스와 시스템

ProcessBuilder로 프로세스 시작, 시스템 속성, 환경 변수, 런타임 정보.

ProcessBuilder

ProcessBuilder는 외부 프로그램을 실행합니다. 인자는 리스트로 전달해 인젝션을 피하세요. redirectErrorStream으로 출력을 합칩니다.

1
2
3
4
5
6
ProcessBuilder pb = new ProcessBuilder("git", "log", "-1");
pb.redirectErrorStream(true); // 오류 출력 병합
Process proc = pb.start();
String out = new String(proc.getInputStream().readAllBytes());
int code = proc.waitFor(); // 종료 대기
System.out.println(out);

시스템 속성

System.getProperty로 JVM 시스템 속성을 읽습니다: user.home, java.version, os.name. setProperty로 설정합니다.

1
2
3
4
5
6
String home = System.getProperty("user.home");
String ver = System.getProperty("java.version");
String os = System.getProperty("os.name");
System.setProperty("my.prop", "value");
// 전체:System.getProperties().forEach(...)
// JVM 파라미터:-Dmy.prop=value

환경 변수

System.getenv는 환경 변수를 읽습니다(읽기 전용), getenv()는 전부. 시스템 속성과 환경 변수를 구분하세요.

1
2
3
4
5
6
String path = System.getenv("PATH");
String home = System.getenv("HOME");
Map<String, String> all = System.getenv();
// 환경 변수는 읽기 전용;
// 시스템 속성은 -D 나 System.setProperty
// 설정 읽기는 둘 다 확인 권장:getenv 우선 또는 속성 우선

Runtime 정보

Runtime은 JVM 정보를 제공합니다: availableProcessors, maxMemory, totalMemory. gc()는 힌트입니다.

1
2
3
4
5
6
7
Runtime rt = Runtime.getRuntime();
int cores = rt.availableProcessors();
long maxMem = rt.maxMemory(); // 최대 힙
long used = rt.totalMemory() - rt.freeMemory();
// rt.gc() 는 회수만 권고(운영 환경에서는 사용 금지)
System.out.printf("cores=%d used=%dMB%n",
cores, used / 1024 / 1024);

명령줄 인수 파싱

main args에서 옵션을 파싱: 단순한 경우 수동 루프, 복잡하면 picocli/JCommander 라이브러리.

1
2
3
4
5
6
7
8
9
10
public static void main(String[] args) {
String file = null;
boolean verbose = false;
for (String a : args) {
if (a.equals("-v")) verbose = true;
else if (a.equals("-f")) { /* 다음 인자 가져옴 */ }
else file = a;
}
}
// 복잡한 CLI 는 picocli:@Command/@Option 어노테이션

셧다운 훅

Runtime.addShutdownHook은 JVM 종료 시 정리를 등록합니다. System.exit도 트리거합니다. 훅에서 무거운 작업은 피하세요.

1
2
3
4
5
6
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
System.out.println("shutting down...");
saveState();
}));
// JVM 정상 종료/System.exit 시 실행
// 훅 스레드는 동시 실행, 무거운 작업 하지 말 것

작업 디렉터리

user.dir 시스템 속성은 프로세스의 현재 작업 디렉터리입니다. ProcessBuilder.directory로 자식 프로세스의 시작 디렉터리를 지정합니다.

1
2
3
4
5
6
7
String cwd = System.getProperty("user.dir");
System.out.println(cwd); // 현재 작업 디렉터리
// 하위 프로세스 시작 디렉터리(다른 디렉터리에서 실행):
Path target = Path.of(System.getProperty("user.home"), "work");
ProcessBuilder pb = new ProcessBuilder("git", "status");
pb.directory(target.toFile());
Process p = pb.start();

시스템 플랫폼 정보

시스템 속성은 플랫폼 정보를 제공합니다: os.name(OS), os.arch(아키텍처), file.separator(경로 구분자), line.separator(줄바꿈).

1
2
3
4
5
6
String os = System.getProperty("os.name"); // Windows 11 / Linux
String arch = System.getProperty("os.arch"); // amd64
String sep = System.getProperty("file.separator"); // \ 또는 /
String eol = System.getProperty("line.separator"); // 줄바꿈 문자
String pathSep = System.getProperty("path.separator"); // ; 또는 :
System.out.println(os + " " + arch);

18.정규 표현식

Pattern/Matcher, 매칭, 캡처 그룹, 치환, 자주 쓰는 패턴.

Pattern과 Matcher

Pattern.compile로 정규식을 컴파일(캐시·재사용)하고, matcher로 텍스트에 매칭합니다. find, matches, lookingAt 세 가지가 있습니다.

1
2
3
4
5
6
Pattern p = Pattern.compile("\\d{3}-\\d{4}");
Matcher m = p.matcher("call 123-4567");
boolean found = m.find(); // 부분 매치
Matcher m2 = p.matcher("123-4567");
boolean full = m2.matches(); // 전체 매치
// 직접 판단:"123-4567".matches("\\d{3}-\\d{4}")

캡처 그룹

괄호는 부분 문자열을 캡처하고, group(1)은 첫 그룹, 명명 그룹 (?<name>...)은 group("name")을 씁니다. find 루프로 전부 추출합니다.

1
2
3
4
5
6
7
8
9
Pattern p = Pattern.compile("(\\d{4})-(\\d{2})");
Matcher m = p.matcher("date: 2024-01");
if (m.find()) {
String year = m.group(1); // 2024
String month = m.group(2); // 01
}
// 이름 있는 그룹:
Pattern p2 = Pattern.compile("(?<year>\\d{4})-(?<month>\\d{2})");
// m.group("year") 로 2024 가져옴

전체 검색

find 루프 또는 matcher.results로 전체 매치를 얻습니다. 전체 치환은 replaceAll($1 같은 그룹 참조 가능).

1
2
3
4
5
6
7
8
Matcher m = Pattern.compile("\\w+").matcher(text);
while (m.find()) {
System.out.println(m.group()); // 각 단어
}
// Java 9+:
m.results().forEach(r -> System.out.println(r.group()));
// 치환:
String masked = "123456".replaceAll("(\\d{3})(\\d+)", "$1***");

치환

replaceAll / replaceFirst은 정규식으로 치환하며 $1/$2로 그룹을 참조합니다. Matcher.appendReplacement는 구간별로 처리합니다.

1
2
3
4
5
6
7
8
9
String s = "a1b2c3";
String all = s.replaceAll("\\d", "#"); // a#b#c#
String first = s.replaceFirst("\\d", "#"); // a#b2c3
// 콜백 치환:
Matcher m = Pattern.compile("\\d").matcher(s);
StringBuffer sb = new StringBuffer();
while (m.find()) m.appendReplacement(sb, "{" + m.group() + "}");
m.appendTail(sb);
// sb = "a{1}b{2}c{3}"

Pattern 플래그

Pattern.CASE_INSENSITIVE는 대소문자 무시, MULTILINE은 ^/$를 줄마다, DOTALL은 .이 줄바꿈에도 매칭되도록 합니다.

1
2
3
4
5
6
Pattern p = Pattern.compile("^java",
Pattern.CASE_INSENSITIVE | Pattern.MULTILINE);
Matcher m = p.matcher("Java\nnot java\n");
// 인라인 플래그:
Pattern p2 = Pattern.compile("(?i)java"); // 대소문자 무시
// (?s) DOTALL、(?m) MULTILINE

자주 쓰는 패턴

이메일, URL, IP, 휴대폰의 자주 쓰는 정규식. 운영 환경의 검증(실제 이메일 등)은 전용 라이브러리를 사용하세요.

1
2
3
4
5
String email = "^[^@\\s]+@[^@\\s]+\\.[^@\\s]+$";
String ip = "^((?:\\d{1,3}\\.){3}\\d{1,3})$";
String phone = "^1[3-9]\\d{9}$"; // 중국 본토 휴대폰
boolean ok = input.matches(phone);
// 주의:\\d 는 Java 문자열에서 정규식의 \d

수량자와 앵커

* 0회 이상, + 1회 이상, ? 0 또는 1회, {n,m} 지정 횟수. ^ 줄 시작, $ 줄 끝, \b 단어 경계.

1
2
3
4
5
6
"\\d+" // 숫자 1개 이상
"colou?r" // color 또는 colour
"\\d{2,4}" // 2~4 자리 숫자
"^start" // 행 시작의 start
"end$" // 행 끝의 end
"\\bword\\b" // 독립된 단어 word

전방탐색과 후방탐색

전방탐색 (?=...)은 뒤를 살피고, 부정 전방탐색 (?!...)은 뒤를 제외하며, 후방탐색 (?<=...)은 앞을 살핍니다. 모두 문자를 소비하지 않습니다.

1
2
3
4
5
6
7
Pattern p = Pattern.compile("\\d+(?=\\s*元)"); // 元 이 이어질 때만 매치
Matcher m = p.matcher("价格 100 元");
if (m.find()) System.out.println(m.group()); // 100
// 부정 전방 탐색:숫자 뒤에 문자가 이어지지 않음
Pattern p2 = Pattern.compile("\\d+(?!\\p{L})");
// 후방 탐색은 고정 길이 필요:
Pattern p3 = Pattern.compile("(?<=价格为)\\d+");

19.빌드와 디버깅

Maven/Gradle, javac/jar, JUnit, JVM 디버깅.

Maven 라이프사이클

mvn compile/test/package/install은 Maven 라이프사이클 단계입니다. target 디렉터리에 class와 jar가 출력됩니다.

1
2
3
4
5
6
// $ mvn clean compile # 클린 후 컴파일
// $ mvn test # 테스트 실행
// $ mvn package # jar 패키징
// $ mvn install # 로컬 저장소에 설치
// $ mvn dependency:tree # 의존성 트리 확인
// 출력은 target/ 디렉터리

pom.xml

pom.xml은 프로젝트를 정의합니다: groupId/artifactId/version 좌표, dependencies, properties.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<project>
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>app</artifactId>
<version>1.0.0</version>
<properties><maven.compiler.release>17</maven.compiler.release></properties>
<dependencies>
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.10.1</version>
</dependency>
</dependencies>
</project>

Gradle

Gradle은 Groovy/Kotlin DSL 기반입니다. task가 빌드 단계를 정의하고, 의존성은 중앙 저장소에서 해석됩니다.

1
2
3
4
5
6
7
8
9
// build.gradle:
plugins { id 'java' }
repositories { mavenCentral() }
dependencies {
implementation 'com.google.code.gson:gson:2.10.1'
testImplementation 'org.junit.jupiter:junit-jupiter:5.9.2'
}
test { useJUnitPlatform() }
// $ gradle build / gradle test / gradle run

JUnit 테스트

JUnit 5: @Test는 테스트 메서드, @BeforeEach는 초기화, assert*는 단언. mvn test로 실행.

1
2
3
4
5
6
7
8
9
10
11
import org.junit.jupiter.api.*;
import static org.junit.jupiter.api.Assertions.*;
class CalculatorTest {
@Test
void add_returns_sum() {
var calc = new Calculator();
assertEquals(5, calc.add(2, 3));
assertTrue(calc.add(1, 1) == 2);
}
}
// 단언 실패는 기대값과 실제값을 명확히 보고

jar 패키징

jar 명령으로 class를 패키징. 실행 가능 jar는 매니페스트에 Main-Class가 필요합니다. java -jar로 실행.

1
2
3
4
5
// $ jar cf app.jar com/ # 디렉터리 패키징
// $ jar cfe app.jar Main com/ # 메인 클래스 지정
// $ java -jar app.jar # 실행
// 내용 확인:jar tf app.jar
// MANIFEST.MF 의 Main-Class: Main

JVM 플래그

-Xmx 최대 힙, -Xms 초기 힙, -XX:+PrintGCDetails GC 로그, -D 시스템 속성.

1
2
3
4
5
// $ java -Xms256m -Xmx2g -jar app.jar
// $ java -Dserver.port=8080 -jar app.jar
// $ java -XX:+PrintGCDetails -XX:+HeapDumpOnOutOfMemoryError app
// 기본값 확인:java -XX:+PrintFlagsFinal -version
// 디버깅:java -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005

로깅 설정

SLF4J를 파사드, Logback을 구현으로. logback.xml에서 레벨과 출력을 설정. 운영에서는 System.out 사용 금지.

1
2
3
4
5
6
7
8
9
10
11
// logback.xml:
<configuration>
<root level="INFO">
<appender-ref ref="STDOUT" />
</root>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender" />
</configuration>
// 코드:
private static final Logger log =
LoggerFactory.getLogger(App.class);
log.info("user {} logged in", id);

의존성 관리

Maven 의존성은 scope로 분류: compile/test/runtime. exclusions로 전이 의존을 제외하고, 버전 충돌은 dependencyManagement에서 통일합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<dependencies>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.9.2</version>
<scope>test</scope> <!-- 仅测试期可用 -->
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>32.1.3-jre</version>
<exclusions> <!-- 排除传递依赖 -->
<exclusion>
<groupId>org.checkerframework</groupId>
<artifactId>checker-qual</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
// 버전 충돌은 dependencyManagement 에서 버전을 통일

공식 링크

공식 문서와 리소스에 대한 직접 링크.

이 치트시트에 대해

이 페이지는 Java 17(LTS)의 자기 완결형 빠른 참조로, 언어 핵심, JDK의 자주 쓰는 라이브러리, 빌드 생태계가 실제 프로젝트에서 차지하는 일상 사용의 약 80%를 다룹니다. 내용은 모던한 관용구를 중심으로 합니다: var 지역 변수 타입 추론, switch 식, 텍스트 블록, record, sealed 클래스, Stream API. Java는 1995년 Sun Microsystems가 출시했으며, "한 번 작성하고 어디서나 실행"하는 JVM 생태계로 유명하고, 엔터프라이즈 백엔드, Android 개발, 빅 데이터에서 가장 널리 쓰이는 언어 중 하나입니다. 19개 섹션은 각각 한 주제에 집중합니다: 기본 문법, 변수, 타입과 참조, 제어 흐름, 함수, 문자열, 컬렉션 프레임워크(List/Set/Map), 메모리 관리(GC), 객체 지향, 예외 처리, 입출력, 흔한 실수, 동시성, 네트워크, 시간, 프로세스, 정규식, 빌드 도구(Maven/Gradle). 각 서브섹션은 "개념 소개 + 바로 복사할 수 있는 코드 스니펫"의 짝으로 구성됩니다. 모든 코드와 텍스트는 브라우저에서 로컬로 렌더링되며, 어떤 데이터도 기기를 떠나지 않습니다. 권위 있는 참조는 Oracle 공식 Java 문서를 보세요.

버전 2.1.0