[Java] enum 타입에 대한 모든 것 (feat. java.lang.Enum 내부구조를 파헤쳐보자)
안녕하세요
오늘은 Java 사용자라면 자주 쓰실 enum에 대해서 알아보겠습니다.
enum 타입은 아래와 같이 enum이라는 키워드를 사용하여 만들 수 있습니다.
public enum Color {
RED, GREEN, BLUE;
}
enum 타입을 만들 때는 class 키워드가 아니라 enum 키워드로 만들기 때문에 클래스가 아니라고 생각하실 수도 있는데요,
enum도 클래스이고, 다른 클래스들과 다를 것 없이 Object 클래스를 상속받습니다.
이렇게 enum 키워드를 통해 만들어진 모든 enum 타입은 java.lang.Enum 클래스를 상속받게 됩니다.
extends Enum을 안 써줘도 말이죠. (enum에는 extends를 쓸 수 없습니다.)
컴파일을 하면서 컴파일러가 extends Enum을 추가해줍니다.
이것이 바로 enum 타입에는 extends를 사용할 수 없는 이유입니다.
Java는 다중 상속을 지원하지 않기 때문에 enum 타입이 다른 클래스를 상속하고 있으면 extends Enum을 추가해줄 수 없기 때문입니다.
java.lang.Enum 클래스
그럼 모든 enum 타입의 부모 클래스인 java.lang.Enum 클래스에 대해서 알아보겠습니다.
Enum 클래스는 Java가 처음 나왔을 때부터 enum이 있었던 것이 아닙니다. JDK1.5부터 추가된 클래스입니다.
먼저 인스턴스 변수로 어떤 것이 있는지 보겠습니다.
name과 ordinal을 볼 수 있는데요, 두 변수 모두 final로 선언되어 있습니다.
그리고 이제 생성자를 봅시다.
생성자는 딱 하나가 있습니다.
유일한 생성자. 프로그래머들은 이 생성자를 호출할 수 없다.enum 클래스를 정의하면 컴파일러가 어떤 코드를 추가해주는데, 그 코드가 호출하는 생성자이다.
public enum Color {
RED, GREEN, BLUE;
}
이 enum 클래스의 경우 생성자가 호출되면서 name과 ordinal이 어떤 값으로 세팅되는지 디버거를 통해 직접 봐볼까요?
RED, GREEN, BLUE 순으로 선언했었는데, 이 순서대로 ordinal의 값으로 0부터 순서대로 저장되는 것을 볼 수 있습니다.
그리고 name에는 "RED", "GREEN", "BLUE"와 같이 enum 상수값이 문자열로 저장되었습니다.
ordinal에는 정의한 enum 상수 순서대로 0번부터 차례대로 저장됩니다.
equals() 메소드
equals() 메소드가 final로 선언되어 있는 것을 볼 수 있는데요,
final로 선언되어 있기 때문에 저희는 equlas() 메소드를 오버라이딩할 수 없습니다.
그런데 equals() 메소드 안을 보면 그냥 this == other 이렇게 == 연산자를 통해 비교하고 있습니다.
즉, enum 상수들끼리 비교를 할 때는 equals()로 비교를 하나 ==으로 비교를 하나 똑같다는 것입니다.
그런데 equals() 메소드는 같은 객체(동일성)인지를 비교하는 것이 아니라 내용이 같은지(동등성)를 비교하도록 오버라이딩 해야 하는 거 아닌가? 하는 생각이 드셨을 수도 있습니다.
네 equals() 메소드는 동등성을 비교해야 하는 것이 맞습니다. 하지만 enum 클래스는 이렇게 == 연산자를 통해 비교를 하는 것이 동등성을 비교하는 것과 결과가 같습니다.
위는 Java 공식 문서인데요,
각각의 enum 상수의 인스턴스는 딱 1개이기 때문에 비교를 하려는 두 객체 중 적어도 하나가 enum 상수라면 equals() 대신 == 연산자로 비교해도 된다.
enum 상수는 인스턴스가 단 하나만 생성됩니다. (싱글톤)
따라서 어차피 같은 enum 상수라면 같은 인스턴스이기 때문에 굳이 equals() 메소드로 비교하지 않고 == 연산자를 통해서 비교를 하면 되는 것입니다.
clone() 메소드
CloneNotSupportedException을 던집니다. 이는 enum의 '싱글톤' 상태에 필수적인 '절대 복제되지 않음'을 보장합니다.
clone 메소드는 호출하면 무조건 예외가 발생합니다.
위의 equals() 메소드에서 설명했던 것처럼, enum은 인스턴스가 딱 1개인 싱글톤이기 때문에 객체를 복사할 수 없습니다.
toString(), hashCode(), clone() 메소드
Object 클래스의 메소드인 equals(), toString(), hashCode(), clone() 메소드 중 toString()을 제외하고 모두 final로 선언되어있습니다.
toString()을 제외한 equals(), hashCode(), clone() 메소드는 저희가 재정의할 수 없습니다.
그리고 clone() 메소드는 아예 clone을 하지 못 하게 예외를 던지는 것을 볼 수 있습니다.
enum 상수들의 목록을 가져오려면 어떻게 해야 할까?
public enum Color {
RED, GREEN, BLUE;
}
만약 이 Color enum 타입의 상수들인 RED, GREEN, BLUE들을 배열로 가져오고 싶으면 어떻게 해야 할까요?
values() 라는 static 메소드를 사용하면 됩니다.
예를 들어 Color.values() 이렇게 말입니다.
public class EnumDemo {
public static void main(String[] args) {
Color[] colors = Color.values();
for (Color color : Color.values()) {
System.out.println(color);
}
}
}
위 코드를 실행하면
이렇게 Color enum 타입에 있는 상수들이 모두 출력되는 것을 볼 수 있습니다.
그런데 java.util.Enum 클래스에 들어가보면 values()라는 메소드는 없습니다.
그렇다면 도대체 values() 메소드는 어디에 정의되어 있는 것일까요?
자, 생각을 해봅시다.
자식은 부모를 알지만 부모는 자식을 모릅니다.
이것은 당연합니다.
java.lang.Enum 클래스를 상속받아서 만들어지는 enum 타입이 무엇일지 알지 못합니다.
Color라는 enum 타입을 만들 수도 있고 Grade라는 enum 타입을 만들 수도 있고, Category라는 enum 타입을 만들 수도 있고, Season이라는 enum 타입을 만들 수도 있고.... 가능한 enum 타입은 무한할 것입니다.
뿐만 아니라 Color 클래스의 enum 상수로 RED, YELLOW, GREEN이 올지, PINK, ORANGE, BLUE, PURPLE이 올지 등등 또한 무한할 것입니다.
따라서 java.lang.Enum 클래스는 자식을 모르기 때문에 values() 메소드를 구현할 수 없습니다.
그럼 도대체 values()는 어떻게 쓸 수 있는 것일까요?
공식 튜토리얼 문서를 보면 values() 메소드는 컴파일러가 enum을 만들 때 추가해준다고 합니다.
원소들이 enum 상수들로만 이루어져있다면 어떤 자료구조를 사용하는 것이 좋을까?
Set 자료구조를 사용하고 싶은데 element들이 Enum 상수들이라면 EnumSet을 사용하는 것이 좋습니다.
Map 자료구조를 사용하고 싶은데 element들이 Enum 상수들이라면 EnumMap을 사용하는 것이 좋습니다.
EnumSet과 EnumMap에 대한 내용은 다른 글에서 자세히 다루겠습니다.
참고
https://docs.oracle.com/javase/specs/jls/se7/html/jls-8.html#jls-8.9
https://docs.oracle.com/javase/tutorial/java/javaOO/enum.html