| 구분 | 인터페이스 | 추상 클래스 |
|---|---|---|
| 상속 구조 | 다중 구현 가능 | 단일 상속만 가능 |
| 확장성 | 기존 클래스에 implements만 추가하면 됨 | 기존 계층구조 변경 어려움 |
| 필드 | 인스턴스 필드 X | 인스턴스 필드 O |
| 접근제한자 | 모든 메서드는 public | 제한자 자유로움 |
| 정적 멤버 | public static만 가능 | 제한 없음 |
→ 따라서 새로운 타입 정의나 기능 확장엔 인터페이스가 훨씬 유연함
implements만 붙이면 끝)Comparable = “정렬 가능한 객체” 기능을 섞는 믹스인 인터페이스public class Monster implements Rotatable, Moveable, Resizable { ... }
public class Sun implements Moveable, Rotatable { ... }
→ 추상 클래스로 하면 “조합 폭발(Combinatorial Explosion)” 일어남
→ n개의 속성이 있으면 2^n 조합 필요해짐
→ 인터페이스가 훨씬 효율적
public interface Singer { ... }
public interface Songwriter { ... }
public interface SingerSongwriter extends Singer, Songwriter { ... }
→ 클래스로 만들면 조합 폭발
→ 인터페이스는 조합만으로 간단히 해결
equals, hashCode, toString 같은 Object 메서드는 디폴트로 정의 금지@implSpec 자바독 태그로 구현 규약 문서화해야 함static List<Integer> intArrayAsList(int[] a) {
return new AbstractList<>() {
public Integer get(int i) { return a[i]; }
public Integer set(int i, Integer val) { int old=a[i]; a[i]=val; return old; }
public int size() { return a.length; }
};
}
→ 인터페이스 유연성 + 추상 클래스 편의성 둘 다 챙김
→ 대표 예: AbstractList, AbstractSet, AbstractMap
AbstractMap.SimpleEntrydefault 키워드 덕분에 인터페이스에도 구현 코드 작성 가능default boolean removeIf(Predicate<? super E> filter) {
Objects.requireNonNull(filter);
boolean result = false;
for (Iterator<E> it = iterator(); it.hasNext();) {
if (filter.test(it.next())) {
it.remove();
result = true;
}
}
return result;
}
Collection.removeIf()SynchronizedCollectionremoveIf는 동기화 고려 안 함removeIf는 그 사실을 모르므로 락 안 걸고 접근함ConcurrentModificationException 터짐 💥Collections.synchronizedCollection()은 이 방식 사용함implements 하는 순간 → “이 인스턴스는 이런 기능을 제공합니다” 라고 약속하는 것임public interface PhysicalConstants {
static final double AVOGADROS_NUMBER = 6.022_140_857e23;
static final double BOLTZMANN_CONSTANT = 1.380_648_52e-23;
static final double ELECTRON_MASS = 9.109_383_56e-31;
}
이런 식으로 상수만 담은 인터페이스 = 상수 인터페이스 패턴, 매우 나쁜 설계
이유 👇
Integer.MIN_VALUE, Double.MAX_VALUEpublic class PhysicalConstants {
private PhysicalConstants() {} // 인스턴스화 방지
public static final double AVOGADROS_NUMBER = 6.022_140_857e23;
public static final double BOLTZMANN_CONSTANT = 1.380_648_52e-23;
public static final double ELECTRON_MASS = 9.109_383_56e-31;
}
static import로 간결하게 사용 가능import static effective_java.item22.PhysicalConstants.*;
public class Test {
double atoms(double mols) {
return AVOGADROS_NUMBER * mols;
}
}
→ 이렇게 하면 클래스 이름 없이 바로 상수 접근 가능
class Figure {
enum Shape { RECTANGLE, CIRCLE }
// 태그 필드
final Shape shape;
// 사각형일 때만 사용하는 필드
double length;
double width;
// 원일 때만 사용하는 필드
double radius;
// 생성자 2개
Figure(double radius) {
shape = Shape.CIRCLE;
this.radius = radius;
}
Figure(double length, double width) {
shape = Shape.RECTANGLE;
this.length = length;
this.width = width;
}
double area() {
switch (shape) {
case RECTANGLE: return length * width;
case CIRCLE: return Math.PI * (radius * radius);
default: throw new AssertionError(shape);
}
}
}
radius or length, width 중 일부는 항상 무의미)즉, 클래스 설계에 "if/switch 기반 타입 구분"이 들어가면, 객체지향 원칙을 위반한 신호 ⚠️
abstract class Figure {
abstract double area();
}
class Circle extends Figure {
final double radius;
Circle(double radius) { this.radius = radius; }
@Override
double area() { return Math.PI * radius * radius; }
}
class Rectangle extends Figure {
final double length;
final double width;
Rectangle(double length, double width) {
this.length = length;
this.width = width;
}
@Override
double area() { return length * width; }
}
| 항목 | 태그 클래스 | 계층구조 활용 |
|---|---|---|
| 코드 구조 | 여러 타입을 한 클래스에 몰아넣음 | 타입별로 독립적인 클래스로 분리 |
| 확장성 | 새로운 타입 추가 시 switch 수정 필요 | 새로운 서브클래스 추가만 하면 됨 |
| 가독성/유지보수성 | 복잡하고 조건문 많음 | 명확한 클래스 구조 |
| 안정성 | 런타임 오류 가능성 (잘못된 태그 처리 등) | 컴파일 타임에 오류 잡힘 |
| 종류 | static 여부 | 바깥 인스턴스 참조 | 주요 용도 |
|---|---|---|---|
| 정적 멤버 클래스 (static member class) | ✅ static | ❌ X | 바깥 클래스와 밀접하지만 인스턴스 참조 불필요할 때 |
| 비정적 멤버 클래스 (non-static member class) | ❌ | ✅ O | 바깥 인스턴스의 메서드/필드에 접근할 필요 있을 때 |
| 익명 클래스 (anonymous class) | 문맥 따라 다름 | 비정적 문맥이면 O | 간단한 콜백, 함수 객체 구현 시 |
| 지역 클래스 (local class) | ❌ | 비정적 문맥이면 O | 메서드 내부에서 한정적으로 사용할 때 |
static으로 선언되며, 바깥 클래스의 인스턴스와 독립적이다.private 멤버에는 접근 가능하다 (정적 문맥이므로 직접 참조 필요).Map.Entry, Enum 내부 구조 등public class Calculator {
public static enum Operation {
PLUS("+", (x, y) -> x + y),
MINUS("-", (x, y) -> x - y);
}
}
public class Outer {
private String name = "Outer";
public class Inner {
void print() {
System.out.println(Outer.this.name);
}
}
}
사용 예시:
컬렉션 클래스의 Iterator 구현체 (ListItr 등)는 종종 바깥 인스턴스를 참조해야 하므로 non-static으로 정의된다.
static을 붙여야 한다.static List<Integer> intArrayAsList(int[] a) {
return new AbstractList<>() {
public Integer get(int i) { return a[i]; }
public Integer set(int i, Integer val) { int old = a[i]; a[i] = val; return old; }
public int size() { return a.length; }
};
}
void foo() {
class Local {
void print() { System.out.println("Local class"); }
}
new Local().print();
}
자바에서는 소스 파일 하나에 여러 개의 톱레벨 클래스(Top-level class) 를 선언할 수 있다.
컴파일러 입장에선 문법적으로 문제가 없지만,
이 방식은 예측 불가능한 동작과 심각한 유지보수 문제를 일으킬 수 있다.
// Utensil.java
class Utensil {
static final String NAME = "PAN";
}
class Dessert {
static final String NAME = "CAKE";
}
// Dessert.java
class Utensil {
static final String NAME = "POT";
}
class Dessert {
static final String NAME = "PIE";
}
// Main.java
public class Main {
public static void main(String[] args) {
System.out.println(Utensil.NAME + Dessert.NAME);
}
}
| 컴파일 명령 | 결과 |
|---|---|
javac Main.java Utensil.java | PANCAKE |
javac Main.java Dessert.java | POTPIE |
javac Main.java | PANCAKE |
javac Dessert.java Main.java | POTPIE |
즉, 어느 파일이 먼저 컴파일되느냐에 따라 결과가 달라짐 ⚠️
이건 클래스의 정의가 충돌하기 때문.
톱레벨 클래스는 무조건 한 파일에 하나만!
만약 논리적으로 한 곳에 묶어야 한다면
👉 정적 멤버 클래스(static nested class) 를 사용하자.
// Main.java
public class Main {
public static void main(String[] args) {
System.out.println(Utensil.NAME + Dessert.NAME);
}
private static class Utensil {
static final String NAME = "PAN";
}
private static class Dessert {
static final String NAME = "CAKE";
}
}
이 방식은