✔ 메서드는 잘못된 입력을 절대 받아들이면 안 된다.
매개변수가 유효하지 않으면 즉시(fail-fast) 예외를 던져야 한다. 유효성 검사를 하지 않으면 다음 문제가 발생할 수 있다:
1.중간 실행 중 모호한 예외 발생
2. 잘못된 반환값 생성
3. 객체 상태(invariant)가 깨져 미래의 예측 불가한 시점에 오류 발생
→ 이런 오류는 실패 원자성(Item 76)까지 깨뜨릴 수 있다.
따라서:
모든 public 메서드는 “메서드 본문이 실행되기 전에” 매개변수 유효성을 검사해야 한다.
@throws 태그로 어떤 조건 위반 시 어떤 예외가 발생하는지 문서화할 것.null, 범위, 크기, 인덱스 등✔ 표준적으로 많이 쓰는 예외
IllegalArgumentExceptionIndexOutOfBoundsExceptionNullPointerExceptionObjects.requireNonNull가장 간편하고 널리 쓰이는 null 체크 방식.
this.strategy = Objects.requireNonNull(strategy, "strategy must not be null");
Objects.checkIndex (Java 9+)List/Array용 범위 체크 유틸리티.
int idx = Objects.checkIndex(index, list.size());
(※ 메시지는 지정할 수 없고 List/Array 전용이라는 제약이 있음)
-ea 필요private void sort(int[] arr, int offset, int length) {
assert arr != null;
assert offset >= 0 && length >= 0;
}
static List<Integer> intArrayAsList(int[] a) {
Objects.requireNonNull(a);
return new AbstractList<>() { ... };
}
→ null 검사가 없으면 List 뷰 생성은 되지만, 이후 사용하는 시점에 NPE 발생
예: Collections.sort(list)
→ 비교 과정에서 자연스럽게 ClassCastException 발생하므로 사전 검증 불필요.
정리
1. 모든 public 메서드/생성자는 매개변수 유효성 검사를 메서드 시작 시점에 수행해야 한다.
2. 제약 조건과 위반 시 예외를 문서화(@throws)하고, null·범위·인덱스 등은 반드시 검증한다.
3. private 메서드는 assert 활용 가능하지만, public API는 반드시 명시적 검사(requireNonNull 등)를 사용한다.
Period 같은 불변 클래스를 만들어도, 가변 객체(Date 등)를 참조로 받으면 내부 불변식이 깨진다.
Period p = new Period(start, end);
end.setYear(78); // p 내부가 수정됨 → 불변 아님
즉, 불변처럼 보이지만 실제로는 공격·버그에 취약하다.
“매개변수 유효성 검사 전에 복사하고, 복사본을 기준으로 검증해라.”
멀티스레드 환경에서
public Period(Date start, Date end) {
this.start = new Date(start.getTime());
this.end = new Date(end.getTime());
if (this.start.compareTo(this.end) > 0) {
throw new IllegalArgumentException(start + " after " + end);
}
}
다시 “가변 필드”를 그대로 넘기면 내부가 수정된다.
p.getEnd().setYear(78); // 내부 필드 수정 가능 → 불변 아님
public Date getStart() { return new Date(start.getTime()); }
public Date getEnd() { return new Date(end.getTime()); }
Period가 내부적으로 보유한 Date는 java.util.Date로 확정적이므로 clone도 안전.
하지만 Item13에 따라 생성자를 쓰는 방식이 일반적으로 더 권장됨.
“어떤 참조도 외부가 객체 내부를 바꿀 수 있게 해선 안 된다.”
방어적 복사에는 비용이 존재한다.
정리
1. 가변 객체를 매개변수로 받거나 반환할 때는 반드시 방어적 복사를 해야 한다.
2. 생성자에서는 유효성 검사보다 ‘복사’가 먼저이며, clone은 절대 쓰지 않는 것이 안전하다.
3. getter에서도 내부 가변 객체를 그대로 노출하지 말고 반드시 복사 후 반환한다.
4. 방어적 복사를 하면 불변식이 깨지는 공격/버그를 완전히 차단할 수 있다.
5. 비용이 크거나 신뢰 가능한 상황에서만 복사를 생략하며, 그 경우 반드시 문서화한다.
subList + indexOf 조합으로 3개 매개변수가 필요한 작업을 해결.Card라는 클래스로 묶기.이렇게 하면:
execute() 같은 메서드를 호출해서 실행.예)
QueryBuilder qb = new QueryBuilder()
.limit(10)
.orderBy("name")
.filter("age > 20");
qb.execute();
이는
HashMap을 받도록 설계하지 말고 Map을 받도록 하자.Thermometer.newInstance(true); // 의미 모호
Thermometer.newInstance(TemperatureScale.CELSIUS); // 명확
정리
1. 메서드 이름은 일관성 있고 이해 가능하게 지어라.
2. 편의 메서드 남발은 해롭다 — 각 메서드는 자신의 역할만 하게 하라.
3. 매개변수는 4개 이하로, 특히 같은 타입 여러 개는 피하라.
4. 매개변수는 쪼개기·도우미 클래스·빌더 패턴으로 줄여라.
5. 매개변수 타입은 인터페이스로 받고, boolean보다 enum을 사용하라.
오버라이딩은 런타임 타입(runtime type) 이 기준이지만,
오버로딩은 컴파일 타입(compile-time type) 만 보고 어떤 메서드를 호출할지 결정한다.
즉:
whatIsThisClass(Collection<?> c);
여기서 실제 런타임 객체가 Set, List여도
컴파일 타입이 Collection이면 Collection 버전이 항상 호출된다.
그래서 아래 예제에서 3번 모두 "컬렉션"이 출력된다:
public String whatIsThisClass(Set<?> set) { return "셋"; }
public String whatIsThisClass(List<?> list) { return "리스트"; }
public String whatIsThisClass(Collection<?> c) { return "컬렉션"; }
Collection<?>[] arr = { new HashSet<>(), new ArrayList<>(), map.values() };
for (Collection<?> c : arr) {
System.out.println(whatIsThisClass(c));
}
Burger -> ChickenBurger -> Whopper
Burger b = new Whopper();
b.print(); // "와퍼"
→ 여기서는 런타임 타입(실 객체 타입) 이 기준이므로 기대한 대로 동작한다.
✦ 결론:
오버라이딩은 다형성. 오버로딩은 단순한 이름 재사용 기능.
다중정의는 아래 상황에서 특히 혼란을 일으킨다:
특히 타입 계층이 얽혀 있는 경우(Set, List, Collection 등)는 더욱 금지.
가장 안전한 해결책.
readInt(), readLong()
컴파일러가 어떤 메서드에 매칭할지 극도로 모호함.
예: Integer용 오버로딩과 Object 오버로딩을 함께 두는 경우
다중정의(Overloading) — 정적 바인딩 → 컴파일타임 결정
재정의(Overriding) — 동적 바인딩 → 런타임 결정
🚫 위험한 다중정의는 피하고
✔ 안전한 다중정의만 허용
int... numbers, String... args)✔ 내부적으로는 배열 생성 → 값 복사 → 메서드 호출의 오버헤드가 항상 존재한다.
static int sum(int... args) {
int sum = 0;
for (int arg : args)
sum += arg;
return sum;
}
sum(1,2,3); // 6
sum(); // 0
static int min(int... args) {
if(args.length == 0)
throw new IllegalArgumentException();
int min = args[0];
for(int i = 1; i < args.length; i++)
if(args[i] < min)
min = args[i];
return min;
}
min() 호출 시 런타임 오류 발생static int min(int firstArg, int... remainingArgs) {
int min = firstArg;
for (int arg : remainingArgs)
if (arg < min)
min = arg;
return min;
}
장점
가변인수는 매 호출마다 배열 생성 비용이 든다 → 비용이 크다.
public void test() {}
public void test(int a1) {}
public void test(int a1, int a2) {}
public void test(int a1, int a2, int a3) {}
public void test(int a1, int a2, int a3, int... rest) {}
효과:
→ 호출할 때마다 “배열 생성 + 복사” 오버헤드 발생
정리
1. 인수 개수가 정해져 있지 않다면 가변인수를 사용한다.
2. 필수 인수는 가변인수 앞에 두어야 한다.
3. 1개 이상 전달해야 하는 경우는firstArg + varargs형태로 만들 것.
4. 성능 민감 코드라면 다중정의로 배열 생성을 최소화한다.
5. 내부적으로 가변인수 호출 시 항상 배열을 새로 만든다.
다음과 같은 코드처럼, “값이 없으니 null 반환”은 흔하지만 나쁜 API 설계다.
return cheesesInStock.isEmpty() ? null : new ArrayList<>(cheesesInStock);
이렇게 하면 클라이언트 코드가 매 호출마다 null 체크를 강제당한다.
List<Cheese> cheeses = shop.getCheeses();
if (cheeses != null && cheeses.contains(Cheese.Mozzarella)) {
...
}
→ API 사용성이 떨어지고, null 체크가 누락되면 NPE 발생 위험이 커진다.
빈 컬렉션을 매번 새로 생성하지 않고 불변 싱글턴 컬렉션을 쓰면 된다.
return cheesesInStock.isEmpty()
? Collections.emptyList()
: new ArrayList<>(cheesesInStock);
Collections.emptyList()는
Set/Map도 동일하게 emptySet(), emptyMap() 제공.
return null;
private static final Cheese[] EMPTY_CHEESE_ARRAY = new Cheese[0];
public Cheese[] getCheeses() {
return cheesesInStock.toArray(EMPTY_CHEESE_ARRAY);
}
new Cheese[0] 배열은 불변이기 때문에 재사용이 가능하다.
정리
✔ null 대신 빈 컬렉션/배열을 반환하라
✔ API 사용성 ↑, NPE ↓
✔ 성능 문제 거의 없음
✔ 필요하면Collections.emptyXxx()같은 불변 빈 컬렉션 사용
✔ 배열도 0길이 배열 싱글턴 사용
Javadoc = Java 코드에서 API 문서를 생성하는 공식 주석 시스템
메서드를 문서화할 때 반드시 아래 3개를 명시해야 한다.
✔ 1) 전제조건(preconditions) – @param, @throws
메서드 호출자가 만족해야 하는 조건
예: index >= 0, object != null
✔ 2) 사후조건(postconditions) – @return
메서드가 성공적으로 수행된 후 반드시 만족해야 하는 조건
예: “정렬된 리스트를 반환한다”
✔ 3) 부작용(side effects)
배경 스레드 생성, 전역 상태 변경 등
→ 문서화하지 않으면 API 오용이 발생할 수 있음
(예: Thread.stop() API 문서처럼 상세히 설명해야 함)
✔ @param
매개변수 설명
✔ @return
반환 값 설명
{@return} 사용 가능 → 중복 설명 줄어듬✔ @throws
체크/언체크 예외 기술
✔ {@code ...}
✔ {@literal ...}
✔ @implSpec
-tag "implSpec:a:Implementation Requirements:"✔ @summary (JDK 10+)
문서 첫 문장 생성 시 마침표 문제 해결하는 용도
✔ {@index ...} (JDK 9+)
검색 색인 기능 추가
✔ 제네릭 타입
모든 타입 매개변수에 반드시 @param 문서 작성
@param <K> 키 타입
@param <V> 값 타입
✔ Enum
각 상수까지 모두 문서화해야 하는 거의 유일한 타입
/** 플루트, 오보 등 목관악기 */
WOODWIND
✔ 애너테이션
애너테이션 자체 + 멤버 모두 문서화해야 함
✔ package-info.java
패키지 설명을 여기에 작성 (package 선언 포함)
✔ module-info.java
모듈 설명을 여기에 작성
API에서 스레드 안전성을 반드시 명시
("불변", "thread-safe", "not thread-safe", "조건부 thread-safe" 등)
직렬화 가능 클래스는 직렬화 형태(documented serialized form) 명시
→ @serial, @serialField, @serialData 태그 사용 가능
Javadoc이 문서를 자동 상속하는 우선순위:
→ 즉, 인터페이스 문서가 가장 강력한 API 계약이 된다.
복잡한 API라면 문서에 관련 클래스, 패키지 링크를 제공하는 것이 좋다.
See {@link java.util.Collections}
IDE와 JDK 자체에 Javadoc 검증 기능이 있음
(문서 누락, 태그 오류 등을 자동 검사)
정리
✔ 공개된 API요소에는 반드시 Javadoc 작성
✔ 메서드는 "계약(전제조건·사후조건·부작용)"을 정확히 문서화
✔ 코드/HTML 안전성을 위해{@code},{@literal}적극 활용
✔ 상속용 API는 반드시@implSpec사용
✔ 제네릭/Enum/Annotation 모두 문서화해야 함
✔ thread-safety / serialization 도 문서에서 필수
✔ Javadoc 상속 규칙 준수 (인터페이스 문서 = 최우선 계약)
참고 블로그:
https://github.com/back-end-study/effective-java/tree/main/8%EC%9E%A5_%EB%A9%94%EC%84%9C%EB%93%9C