[Java Study 11주차] Enum
본 포스팅은 백기선님이 진행하시는 자바 스터디 를 진행하며 혼자 공부하고 이해한 내용을 바탕으로 정리한 글입니다. 오류나 지적 사항이 있다면 댓글로 알려주시면 감사하겠습니다.
Enum이란?
enum(열거형)은 서로 관련된 상수를 편리하게 선언하기 위한 것으로 상수를 여러 개 정의할 때 사용한다. enum은 여러 상수를 정의한 후, 정의된 것 이외의 값은 허용하지 않는다.
가장 간단하게 선언하는 법은 다음과 같다.
enum 열거형이름 {상수명1 , 상수명2, 상수명3, ...}
public class EnumDemo {
enum Season {SPRING, SUMMER, FALL, WINTER}
public static void main(String[] args) {
printSeason(Season.SPRING);
}
public static void printSeason(Season season) {
switch (season) {
case SPRING -> System.out.println("봄입니다.");
case SUMMER -> System.out.println("여름입니다.");
case FALL -> System.out.println("가을입니다.");
case WINTER -> System.out.println("겨울입니다.");
default -> throw new IllegalArgumentException("계절의 이름이 아닙니다.");
}
}
}
실행 결과
봄입니다.
Enum은 왜 만들어졌는가?
Enum을 잘 사용하면 코드의 가독성을 높이고 논리적인 오류를 줄일 수 있다. Enum을 잘 사용하기 위해서는 우선 Enum이 왜 탄생했는지 알 필요가 있다.
과일의 이름을 입력받으면 해당 과일의 칼로리를 출력하는 프로그램이 있고, 과일의 이름을 다음과 같이 상수로 관리한다고 생각해보자.
public class EnumDemo {
public static final int APPLE = 1;
public static final int PEACH = 2;
public static final int BANANA = 3;
public static void main(String[] args) {
int type = APPLE;
switch (type) {
case APPLE:
System.out.println("32 kcal");
break;
case PEACH:
System.out.println("52 kcal");
break;
case BANANA:
System.out.println("16 kcal");
break;
}
}
}
우선 위 코드에서 마음에 들지 않는 점은 각각의 상수에 부여된 1, 2, 3이라는 리터럴은 단순히 상수들을 구분하고 이용하기 위해 부여된 것이지 논리적으로 아무런 의미가 없다. 다시 말해 APPLE은 정수 1과 아무런 관련도 없고 굳이 1이어야 할 이유도 없다.
두번째 문제는 이름의 충돌이 발생할 수 있다는 것이다. 만약 이 프로그램이 커져서, IT 회사의 정보가 추가되었고 회사 이름을 상수로 관리하려 한다 해보자.
public class EnumDemo {
public static final int APPLE = 1;
public static final int PEACH = 2;
public static final int BANANA = 3;
... ...
public static final int APPLE = 1;
public static final int GOOGLE = 2;
public static final int FACEBOOK = 3;
}
과일 '사과'와 회사 '애플'은 이름은 같지만 서로 다른 의미를 가진다. 하지만 위의 예시처럼 사용하려면 이름이 중복되기 때문에 컴파일 에러가 발생한다.
이름의 중복은 아래처럼 이름을 다르게 해주거나
public class EnumDemo {
public static final int FRUIT_APPLE = 1;
public static final int FRUIT_PEACH = 2;
public static final int FRUIT_BANANA = 3;
... ...
public static final int COMPANY_APPLE = 1;
public static final int COMPANY_GOOGLE = 2;
public static final int COMPANY_FACEBOOK = 3;
}
인터페이스로 만들면 구분이 가능해진다. (하지만 이런 식으로 상수를 인터페이스로 관리하는 것은 안티패턴이다. 인터페이스는 규약을 정하기 위해 만든 것이지, 이런 식으로 쓰라고 만든 개념이 아니기 때문이다.)
interface Fruit {
int APPLE = 1, PEACH = 2, BANANA = 3;
}
interface Company {
int APPLE = 1, GOOGLE = 2, FACEBOOK = 3;
}
하지만 여전히 문제가 남아있다. fruit와 company 모두 int 타입의 자료형이기 때문에 아래와 같은 코드가 가능하다.
if (Fruit.APPLE == Company.APPLE) {
......
}
하지만 '과일'과 '회사'는 서로 비교조차 되어서는 안되는 다른 개념이다. 따라서 위와 같은 코드는 애초에 작성할 수 없게 컴파일 과정에서 막아주는 것이 좋다.
둘이 애초에 비교를 하지 못하도록 하려면 서로 다른 객체로 만들어주면 된다.
class Fruit {
public static final Fruit APPLE = new Fruit();
public static final Fruit PEACH = new Fruit();
public static final Fruit BANANA = new Fruit();
}
class Company {
public static final Company APPLE = new Company();
public static final Company GOOGLE = new Company();
public static final Company FACEBOOK = new Company();
}
public class EnumDemo {
public static void main(String[] args) {
if (Fruit.APPLE == Company.APPLE) {} // 컴파일 에러 발생
}
}
이렇게 하면 위에서 언급한 세 가지 문제가 모두 해결된다.
- 상수와 리터럴이 논리적인 연관이 없음.
- 서로 다른 개념끼리 이름이 충돌할 수 있음.
- 서로 다른 개념임에도 비교하는 코드가 가능함
이것이 바로 Enum의 실체이다. 이처럼 상수를 클래스로 정의해서 관리할 때 얻을 수 있는 이점들을 모두 취하면서 상수들을 더욱 간단히 선언할 수 있도록 하기 위해 만들어졌다.
사용자 정의 타입은 switch문의 조건에 들어갈 수 없지만 Enum은 가능하다. (switch문의 조건으로 들어갈 수 있는 데이터 타입은 byte, short, char, int, enum, String, Byte, Short, Character, Integer이다.)
public class EnumDemo {
public static void main(String[] args) {
Fruit type = Fruit.APPLE;
switch (type) { // 컴파일 에러
case Fruit.APPLE:
System.out.println("32 kcal");
break;
case Fruit.PEACH:
System.out.println("52 kcal");
break;
case Fruit.BANANA:
System.out.println("16 kcal");
break;
}
}
}
Java Enum의 특징
- enum에 정의된 상수들은 해당 enum type의 객체이다.
C 등의 다른 언어에도 열거형이 존재한다. 하지만 다른 언어들과 달리 Java의 enum은 단순한 정수 값이 아닌 해당 enum type의 객체이다.
앞서 살펴본 것처럼,
enum Fruit { APPLE, PEACH, BANANA }
만일 이런 열거형이 정의되어 있을 때, 이를 클래스로 정의한다면 다음처럼 표현할 수 있다.
class Fruit {
public static final Fruit APPLE = new Fruit("APPLE");
public static final Fruit PEACH = new Fruit("PEACH");
public static final Fruit BANANA = new Fruit("BANANA");
private String name;
private Fruit(String name) {
this.name = name;
}
}
물론 실제 enum의 구현과는 다르지만, 이런 형태라고 생각하면 enum을 학습하는데 있어서 훨씬 이해가 쉽다.
- 생성자와 메서드를 추가할 수 있다.
java에서 enum은 엄연한 클래스이다. enum을 정의하는 법은 클래스를 정의하는 법과 거의 비슷한데 몇 가지 차이가 있다. 우선 class 대신에 enum이라고 적는다. 첫 줄에는 열거할 상수의 이름을 선언한다. 이름은 대문자로 선언하는 것이 관례이며 각 상수는 콤마로 구분한다. 제일 마지막 상수의 끝에는 세미콜론을 붙여야 한다. 앞부분에서 살펴본 것처럼, 간단히 상수의 이름만 선언할 때는 세미콜론을 붙이지 않아도 된다.
enum Fruit {
APPLE, PEACH, BANANA; // 열거할 상수의 이름 선언, 마지막에 ; 을 꼭 붙여야한다.
Fruit() {
System.out.println("생성자 호출 " + this.name());
}
}
public class EnumDemo {
public static void main(String[] args) {
Fruit apple = Fruit.APPLE;
// Fruit grape = new Fruit();
// 에러 발생. 열거형의 생성자의 접근제어자는 항상 private이다.
}
}
생성자 호출 APPLE
생성자 호출 PEACH
생성자 호출 BANANA
생성자를 정의할 수 있는데, enum의 생성자의 접근제어자는 private
이기 때문에 외부에서 상수를 추가할 수 없다. 열거형의 멤버 중 하나를 호출하면, 열거된 모든 상수의 객체가 생성된다. 위 예시를 보면 APPLE 하나를 호출했는데 열거된 모든 상수의 생성자가 호출되었음을 확인할 수 있다. 상수 하나당 각각의 인스턴스가 만들어지며 모두 public static final
이다.
생성자를 이용해서 상수에 데이터를 추가할 수 있다.
enum Currency {
PENNY(1), NICKLE(5), DIME(10), QUARTER(25);
private int value;
Currency(int value) {
this.value = value;
}
public int value() {
return value;
}
}
public class EnumDemo {
public static void main(String[] args) {
System.out.println(Currency.DIME.value()); // 10
}
}
다음과 같이 switch문을 이용해서 각 상수별로 다른 로직을 실행하는 메서드를 정의할 수도 있다.
enum Transport {
BUS(1200), TAXI(3900), SUBWAY(1200);
private final int BASIC_FARE; // 기본요금
Transport(int basicFare) {
BASIC_FARE = basicFare;
}
public double fare() { // 운송 수단별로 다르게 책정되는 요금
switch (this) {
case BUS -> {
return BASIC_FARE * 1.5;
}
case TAXI -> {
return BASIC_FARE * 2.0;
}
case SUBWAY -> {
return BASIC_FARE * 0.5;
}
default -> throw new IllegalArgumentException(); // 실행될 일 없는 코드이지만 없으면 컴파일 에러
}
}
}
이렇게 추상 메서드를 선언해서 각 상수 별로 다르게 동작하는 코드를 구현할 수도 있다.
enum Transport {
BUS(1200) {
@Override
double fare(int distance) {
return distance * BASIC_FARE * 1.5;
}
},
TAXI(3900) {
@Override
double fare(int distance) {
return distance * BASIC_FARE * 2.0;
}
},
SUBWAY(1200) {
@Override
double fare(int distance) {
return distance * BASIC_FARE * 0.5;
}
};
protected final int BASIC_FARE; // 기본요금, protected로 선언해야 상수에서 접근 가능
Transport(int basicFare) {
BASIC_FARE = basicFare;
}
abstract double fare(int distance); // 거리에 따른 요금 계산
}
- 상수 간의 비교가 가능하다.
enum 상수 간의 비교에는 ==
를 사용할 수 있다. 단 >
, <
같은 비교연산자는 사용할 수 없고 compareTo()를 사용할 수 있다.
Enum은 언제 사용하는가
필요한 원소를 컴파일 타임에 다 알 수 있는 상수 집합이라면 항상 열거 타입을 사용하자. 태양계 행성, 한 주의 요일, 체스 말처럼 본질적으로 열거 타입인 타입은 당연히 포함된다. 그리고 메뉴 아이템, 연산 코드, 명령줄 플래그 등 허용하는 값 모두를 컴파일타임에 이미 알고 있을 때도 쓸 수 있다. 열거 타입에 정의된 상수 개수가 영원히 고정 불변일 필요는 없다. 열거 타입은 나중에 상수가 추가돼도 바이너리 수준에서 호환되도록 설계되었다
이펙티브 자바 3/E. Item 34. 219쪽
실무에서 어떻게 활용될 수 있는지는 아래의 글을 참조하면 좋을 것 같다.
Enum이 제공하는 메서드
메서드 | 설명 |
T[] values() | 해당 enum 타입에 정의된 상수 배열을 반환한다. |
Class<E> getDeclaringClass() | 열거형의 객체를 반환한다. |
String name() | 열거형 상수의 이름을 문자열로 반환한다. |
int ordinal() | 열거형 상수가 정의된 순서를 반환한다.(0부터 시작) |
T valueOf(Class<T> enumType, String name) | 지정된 열거형에서 name과 일치하는 열거형 상수를 반환한다. |
values()를 제외한 나머지는 java.lang.Enum 클래스에 정의된 메서드이다.
values()
이 메서드는 모든 열거형이 가지고 있는 것으로 컴파일러가 자동으로 추가해준다.
enum Transport {
BUS, TAXI, SUBWAY;
}
public class EnumDemo {
public static void main(String[] args) {
for (var e : Transport.values()) {
System.out.println(e.name());
}
}
}
BUS
TAXI
SUBWAY
ordinal()
상수가 정의된 순서를 반환한다.
public static void main(String[] args) {
for (var e : Transport.values()) {
System.out.println(e.ordinal());
}
}
0
1
2
Java API 문서에서는enum의ordinal메서드에 대해 다음과 같이 말한다.
Most programmers will have no use for this method. It is designed for use by sophisticated enum-based data structures, such as EnumSet and EnumMap.
대부분의 프로그래머는 이 메서드를 쓸 일이 없다. 이 메서드는 EnumSet과 EnumMap 같이 열거 타입 기반의 범용 자료구조에 쓸 목적으로 설계되었다.
ordinal은 Enum 내부에서 사용하기 위해 만든 것이지, 프로그래머가 이 메서드에 의존하는 코드를 작성하는 것은 안티패턴이다.
T valueOf(Class enumType, String name)
지정된 열거형에서 name과 일치하는 열거형을 반환한다.
public static void main(String[] args) {
// 두 가지 형태로 가능
Transport taxi = Enum.valueOf(Transport.class, "Taxi");
Transport bus = Transport.valueOf("BUS");
}
java.lang.Enum
java.lang 에 포함된 Enum 클래스는 모든 자바 열거형의 조상이다. 모든 열거형은 Enum 클래스를 상속받기 때문에 enum type은 별도의 상속을 받을 수 없다.
public abstract class Enum<E extends Enum<E>>
implements Constable, Comparable<E>, Serializable {
private final String name;
private final String name() {
return name;
}
......
}
앞서 언급했던 메서드들은 모두 여기 정의되어 있다. toString을 제외한 대부분의 메서드는 final로 선언되어 있기 때문에 별도의 오버라이딩을 할 수 없다.
EnumSet
EnumSet 열거형을 위해 고안된 특별한 Set 인터페이스 구현체이다.HashSet과 비교했을 때, 성능 상의 이점이 많기 때문에 열거형 데이터를 위한 Set이 필요한 경우 EnumSet을 사용하는 것이 좋다.
EnumSet의 상속 구조는 다음과 같다.
출처: https://www.geeksforgeeks.org/enumset-class-java/
EnumSet의 중요한 특징 몇 가지를 정리해보면 다음과 같다.
- EnumSet은 AbstractSet 클래스를 상속하고 Set 인터페이스를 구현한다.
- 오직 열거형 상수만을 값으로 가질 수 있다. 또한 모든 값은 같은 enum type이어야 한다.
- null value를 추가하는 것을 허용하지 않는다. NullPointerException을 던지는 것도 허용하지 않는다.
- ordinal 값의 순서대로 요소가 저장된다.
- tread-safe하지 않다. 동기식으로 사용하려면 Collections.synchronizedMap을 사용하거나, 외부에서 동기화를 구현해야한다.
- 모든 메서드는 arithmetic bitwise operation을 사용하기 때문에 모든 기본 연산의 시간 복잡도가 O(1)이다.
사용법
enum Color {
RED, YELLOW, GREEN, BLUE, BLACK, WHITE
}
public class EnumDemo {
public static void main(String[] args) {
EnumSet<Color> set1, set2, set3, set4, set5;
set1 = EnumSet.allOf(Color.class);
set2 = EnumSet.of(Color.RED, Color.GREEN, Color.BLUE);
set3 = EnumSet.complementOf(set2);
set4 = EnumSet.range(Color.YELLOW, Color.BLACK);
set5 = EnumSet.noneOf(Color.class);
set5.add(Color.BLACK);
set5.add(Color.BLUE);
set5.remove(Color.BLUE);
System.out.println("set1 = " + set1);
System.out.println("set2 = " + set2);
System.out.println("set3 = " + set3);
System.out.println("set4 = " + set4);
System.out.println("set5 = " + set5);
System.out.println(set5.contains(Color.BLACK));
}
}
실행 결과
set1 = [RED, YELLOW, GREEN, BLUE, BLACK, WHITE]
set2 = [RED, GREEN, BLUE]
set3 = [YELLOW, BLACK, WHITE]
set4 = [YELLOW, GREEN, BLUE, BLACK]
set5 = [BLACK]
true
결과값을 보면 굳이 설명이 없어도 이해할 수 있을 것이다. 살짝 덧붙이면 range()의 경우 첫 번째 인자의 ordinal과 두 번째 인자의 ordinal을 포함한 그 사이에 있는 값들을 set으로 반환한다.
참고 자료
생활코딩(https://www.youtube.com/watch?v=AWEvmFs7RJ4)
자바의 정석 (남궁성 저)