devseop08 님의 블로그
[OOP] 열거형 본문
열거형 선언 방법
static final상수들을 나열: 양이 증가할 수록 유지와 관리 어려움
class Card {
static final int CLOVER = 0;
static final int HEART = 1;
static final int DIAMOND = 2;
static final int SPADE = 3;
static final int TWO = 2;
static final int THREE = 3;
static final int FOUR = 4;
final int kind;
final int num;
}- 열거형을 선언:
enum 열거형 이름 { 상수명1, 상수명2, 상수명3}형식으로 선언
enum Kind {
CLOVER,
HEART,
DIAMOND,
SPADE
}
enum Value {
TWO,
THREE,
FOUR
}
class Card {
final Kind kind;
final Value num;
}열거 객체의 정적 초기화와 싱글턴 보장 방식
- 컴파일 시점의 구조:
enum은 컴파일되면 사실상java.lang.Enum을 상속하는final클래스가 된다.
enum Status{ READY, RUNNING } - 컴파일 후
javap -c명령어로 해당 enum의 클래스 파일을 디컴파일 시: 정적 초기화가 이루어지는 싱글턴 패턴 구조로 컴파일된 것을 확인 가능- 모든 상수는 정적 초기화 블록에서 한 번만 인스턴스화
- 생성자는
private이고, 컴파일러가 강제로private로 제한 - 따라서 클래스 외부에서는 절대
new로 인스턴스를 새로 생성할 수 없다.
public final class Status extends java.lang.Enum<Status> {
public static final Status READY;
public static final Status RUNNING;
private static final Status[] $VALUES;
static { // 정적 초기화
READY = new Status("READY", 0);
RUNNING = new Status("RUNNING", 1);
$VALUES = new Status[] { READY, RUNNING };
}
private Status(String name, int ordinal) { // 싱글턴 패턴을 위한 private 생성자
super(name, ordinal);
}
public static Status[] values() {
return $VALUES.clone();
}
public static Status valueOf(String name) { ... }
}- JVM 클래스 로더 관점
- 각
enum클래스는 JVM의 클래스 로더 단위로 관리된다. - 클래스 로딩 시 힙 영역에 1회 초기화/생성 -> Runtime Data Area의 메서드 영역에서 참조가 공유된다
- 따라서
enum상수, 즉 열거형 객체는 클래스 언로드 전까지 GC에 회수되지 않고 유지된다.
- 각
┌───────────────────────────────┐
│ 메소드 영역 │ (JDK 8 이후 Metaspace)
│ - 클래스 메타데이터 │
│ (클래스 이름, 필드, 메서드) │
│ - enum 정의 정보 │
│ ex) enum Status { │
│ READY, RUNNING, ... │
│ } │
└───────────────┬───────────────┘
│
클래스 로딩 시 enum 메타데이터 저장
│
┌───────────────▼───────────────┐
│ 힙 │
│ - enum 상수 객체 │
│ Status.READY │
│ Status.RUNNING │
│ Status.DONE │
│ → JVM이 클래스 초기화 시 단 1회 생성 │
│ → 모든 참조는 이 객체를 공유 │
└───────────────┬───────────────┘
│
참조를 통해 enum 상수 사용
│
┌───────────────▼───────────────┐
│ 스택 │
│ - 지역 변수 슬롯 │
│ ex) Status s = Status.READY │
│ └── 스택에 "힙 객체 참조" 저장 │
└───────────────────────────────┘
- 리플렉션 차단: 보통 싱글턴 패턴 클래스는 리플렉션으로 생성자를 열어두면 두 번째 인스턴스가 생길 수 있으나
enum은 예외Constuctor.newInstance()호출 시IllegalArgumentException발생
java.lang.IllegalArgumentException: Cannot reflectively create enum objects- 직렬화 보장
- 일반 싱글턴 클래스는
readResolve()를 구현하지 않으면 역직렬화 시 새 인스턴스가 생길 수 있으나enum은 언어 차원에서 보장 - 역직렬화 시 항상 기존 상수 인스턴스를 반환
- 즉, GC나 직렬화/역직렬화 과정을 거쳐도 새로운 객체는 생기지 않는다.
열거형의 조상: 추상 클래스 java.lang.Enum
public abstract class Enum<E extends Enum<E>>
implements Constable, Comparable<E>, Serializable {
private final String name;
public final String name() {
return name;
}
private final int ordinal;
public final int ordinal() {
return ordinal;
}
protected Enum(String name, int ordinal) {
this.name = name;
this.ordinal = ordinal;
}
public String toString() {
return name;
}
public final boolean equals(Object other) {
return this==other;
}
@Stable
private int hash;
public final int hashCode() {
int hc = hash;
if (hc == 0) {
hc = hash = System.identityHashCode(this);
}
return hc;
}
protected final Object clone() throws CloneNotSupportedException {
throw new CloneNotSupportedException();
}
public final int compareTo(E o) {
Enum<?> other = o;
Enum<E> self = this;
if (self.getClass() != other.getClass() && // optimization
self.getDeclaringClass() != other.getDeclaringClass())
throw new ClassCastException();
return self.ordinal - other.ordinal;
}
public final Class<E> getDeclaringClass() {
Class<?> clazz = getClass();
Class<?> zuper = clazz.getSuperclass();
return (zuper == Enum.class) ? (Class<E>)clazz : (Class<E>)zuper;
}
public static <T extends Enum<T>> T valueOf(Class<T> enumClass,
String name) {
T result = enumClass.enumConstantDirectory().get(name);
if (result != null)
return result;
if (name == null)
throw new NullPointerException("Name is null");
throw new IllegalArgumentException(
"No enum constant " + enumClass.getCanonicalName() + "." + name);
}
}protected Enum(String name, int ordinal)생성자 =>Enum은abstract클래스이기 때문에 직접적으로 생성자를 호출하지 못하므로,enum의 생성자에서super를 통해 호출할 수 있도록 하기 위해protected선언private final String name과private final int ordinal=> 불변성을 갖도록 하기 위해private final선언ordinal은enum객체가 갖는 순서값이다.각
enum타입에 있는values()와valueOf(String)정적 메서드는 컴파일러가 해당 타입에 자동 생성하는 유틸이며,Enum클래스의 인스턴스 메서드가 아니다.equals메서드는final로 선언되어 오버라이드 불가하고return this==other;로 참조 동일성을 판단하도록 구현돼 있으므로enum객체 간의equals메서드를 통한 비교도 참조 동일성을 판단한다.hashCode메서드는final로 선언되어 오버라이드 불가하고 아이덴티티 기반의 hash 값을 반환하며enum객체는 싱글턴을 보장하므로enum객체의 참조값을HashMap,HashSet의 키로 사용이 가능하다.comparteTo메서드 또한final로 선언되어 오버라이드 불가하다. 두enum객체가 서로 같은enum타입인지 검사하고 같지 않으면ClassCastException을 발생시키기 때문에 같은 타입의enum객체에 대해서만compartTo메서드를 사용 가능하다.열거형 객체 비교
열거형 상수, 즉 열거형 객체 간의 == 연산은 열거형 상수(열거형 객체)의 참조값끼리 비교하므로 열거형 상수 간의 참조 동일성을 판단한다.
equals메서드도 == 연산과 완전히 동일하게 열거형 상수 간의 참조 동일성을 판단한다.열거형 상수, 즉 열거형 객체 간의
>,>=,<,<=연산은 불가하다.>,>=,<,<=연산 대신compareTo메서드를 사용해야 하며 두 열거형 상수의 타입이 같은 타입이 아니면ClassCastException을 발생시키므로 같은 타입의 열거형 상수에 대해서만 사용 가능하다.compareTo메서드는enum객체가 갖는 순서값을 이용하는ordinal()기반의 비교를 제공한다.equals메서드를enum객체 간의 내용 동등성이 아닌 참조 동일성을 판단하는 것으로 고정(final선언)한 이유enum객체는 애초에 싱글턴 객체이므로 같은 타입의 두enum객체 간의 참조값 비교는 내용 동등성을 판단하는 것과 같다.- 그리고 서로 다른 타입의 두
enum객체의 내용 동등성을 판단하는 것은 애초에 의미가 없으므로equals메서드와 == 연산 모두 참조 동일성을 판단하는 것으로 충분하다. - 결국엔
enum객체가 싱글톤 객체임을 보장하기 위함에서 기인한 결과이다.
enum Status { READY, RUNNING }
Status a = Status.READY;
Status b = Status.READY;
Status c = Status.RUNNING;
assert a == b; // true
assert a.equals(b); // true (== 로 비교)
assert !a.equals(c); // true
assert !Objects.equals(a, null); // true (null 안전)열거형 멤버 추가하기
Enum클래스로부터 상속받은 멤버들 외에 멤버를 더 추가할 시 주의사항- 열거형의 불변성과 스레드 안정성을 보장하도록 멤버 변수
private final선언: 불변 설계
- 열거형의 불변성과 스레드 안정성을 보장하도록 멤버 변수
- 추가된 인스턴스 변수 초기화를 위한 생성자 추가(
private선언이 생략된 생성자): 열거형의 생성자는 항상private=> 외부에서 해당 열거형 객체 생성 불가
- 추가된 인스턴스 변수 초기화를 위한 생성자 추가(
- 열거형 상수 생성에 인자 전달
enum Direction {
EAST(1, ">"), SOUTH(2, "V"), WEST(3, "<"), EAST(4, "^");
private static final Direction[] DIR_ARR = Direction.values();
private final int value;
private final String symbol;
Direction(int value, String symbol) {
this.value = value;
this.symbol = symbol;
}
public int getValue() {
return value;
}
public String getSymbol() {
return symbol;
}
public static Direction of(int direction) {
if(direction < 1 || direction > 4) {
throw IllegalArgumentException("Invalid value :" + direction);
}
return DIR_ARR[direction - 1];
}
}열거형 사용 시 장점
- 관련 상수들의 유지와 관리가 용이하다.
- 심볼릭 상수로 가독성이 좋은 코드를 작성하게 해준다: 기존엔 1, boolean,
a와 같은 상수 리터럴에는 식별자를 지정해줄 수 없었지만 열거형 상수가 심볼릭 상수로써, 식별자와 같은 역할을 해준다. - 상수를 마치 변수처럼 다룰 수 있게 된다. => 열거형 객체의 참조값 저장, 메서드 호출
- 타입 안정성 보장:
final로 선언되며 상속도 제한되기 때문에 대입 연산 시 정확한 타입의 값을 할당할 수 있다.
Week w1 = 1; // 타입 에러
Week w2 = Week.SUNDAY; // 정확한 타입만 대입 가능'Language > Java' 카테고리의 다른 글
| [API] 람다와 스트림: Mordern Java - 13. 디폴트 메서드 (0) | 2025.09.21 |
|---|---|
| [OOP] 제네릭스 (0) | 2025.09.20 |
| [API] java.text 패키지 형식화 클래스: DecimalFormat, DateFormat, SimpleDateFormat (0) | 2025.09.17 |
| [API] System, Math, Number, Wrapper, BigInteger, BigDecimal (0) | 2025.09.16 |
| [API] StringBuffer, StringBuilder, StringJoiner (0) | 2025.09.15 |