열거형(enumerated type)이라고 부르며 서로 연관된 상수들의 집합이다. 기존에 상수를 사용하면서 발생했던 문제를 개선하고자 jdk1.5 부터 추가 된 기능이다.
아래의 예시들을 보며 이해해보자.
public class BeforeEnum {
public static void main(String[] args) {
/*
1번은 사과
2번은 바나나
3번은 오렌지
*/
int type = 2; // 1~3의 값이 올 수 있다.
switch (type) {
case 1:
System.out.println("사과");
break;
case 2:
System.out.println("바나나");
break;
case 3:
System.out.println("오렌지");
break;
}
}
}
하지만 이런 코드는 문제가 있다. 시간이 흘러 코드를 수정하다가 주석에 적힌 정보가 없어졌다고 가정해보자. 만약 다른 개발자가 이를 발견하면 type이 어떤 의미를 담고 있는지 알기 어려울 것이다.
주석이 없어졌다는 이유로 코드를 분석하기 어려우면 매우 힘들 것이다. 이럴 때 숫자별로 변수를 만든 후, 불변성을 유지하기 위해 final을 사용한다. 어차피 바뀌지 않을 값이면 클래스 변수로 지정하는 것이 좋다.
public class BeforeEnum {
private final static int APPLE = 1;
private final static int BANANA = 2;
private final static int ORANGE = 3;
public static void main(String[] args) {
int type = APPLE;
switch (type) {
case APPLE:
System.out.println("사과");
break;
case BANANA:
System.out.println("바나나");
break;
case ORANGE:
System.out.println("오렌지");
break;
}
}
}
이렇게 하면 처음 코드에서 발생했던 문제를 상수명을 통해 해결할 수 있어 유지보수가 훨씬 쉬워진다.
프로그램을 더 구체적으로 만들면서 일반 과일 외에도 열대 과일 상수가 필요해진다면 어떻게할까?
public class BeforeEnum {
// 일반 과일
private final static int APPLE = 1;
private final static int BANANA = 2;
private final static int ORANGE = 3;
// 열대 과일
private final static int APPLE = 1; // 컴파일 에러 발생
private final static int MANGO = 2;
private final static int PINEAPPLE = 3;
public static void main(String[] args) {
int type = APPLE;
switch (type) {
case APPLE:
System.out.println("사과");
break;
case BANANA:
System.out.println("바나나");
break;
case ORANGE:
System.out.println("오렌지");
break;
}
}
}
APPLE 상수가 중복 선언되었기 때문에 컴파일 에러가 발생한다. 이를 방지하기 위해 변수명에 접두사를 붙이는 방법을 사용해보자.
변수 이름이 중복되는 문제를 해결하기 위해 접두사를 붙여 구분할 수 있다. 그러나 상수가 많아지면 코드가 점점 지저분해지는 단점이 있다.
public class BeforeEnum {
// 일반 과일
private final static int NORMAL_APPLE = 1;
private final static int NORMAL_BANANA = 2;
private final static int NORMAL_ORANGE = 3;
// 열대 과일
private final static int TROPICAL_MANGO = 1;
private final static int TROPICAL_PINEAPPLE = 2;
private final static int TROPICAL_BANANA = 3;
public static void main(String[] args) {
int type = NORMAL_APPLE;
....
}
}
🔴 기존 상수를 사용하면서 발생한 문제들
만약 일반 과일과 열대 과일을 비교한다면 어떻게 될까? 실제로 NORMAL_APPLE과 TROPICAL_APPLE은 다르지만 값이 같기 때문에 논리적으로는 다르더라도 값 비교에서 같은 결과를 초래한다. 이는 typesafe하지 않은 코드를 유발한다.
public class BeforeEnum {
// 일반 과일
private final static int NORMAL_APPLE = 1;
private final static int NORMAL_BANANA = 2;
private final static int NORMAL_ORANGE = 3;
// 열대 과일
private final static int TROPICAL_APPLE = 1;
private final static int TROPICAL_MANGO = 2;
private final static int TROPICAL_PINEAPPLE = 3;
public static void main(String[] args) {
if (NORMAL_APPLE == TROPICAL_APPLE) {
// 일반 사과와 열대 사과는 논리적으로 다르지만, 같은 값으로 인해 true가 발생
}
}
}
🔴 enum을 사용한 상수 사용
enum을 사용하면 NORMAL_APPLE과 TROPICAL_APPLE을 비교하는 것 자체가 불가능하다. 각 enum은 독립적인 타입으로 구분되기 때문이다.
public class BeforeEnum {
enum NORMAL {
APPLE, BANANA, ORANGE;
}
enum TROPICAL {
MANGO, PINEAPPLE, BANANA;
}
public static void main(String[] args) {
if (NORMAL.APPLE == TROPICAL.BANANA) {
// 일반 사과와 열대 바나나는 비교 자체가 불가능 (컴파일 에러)
}
}
}
enum 키워드를 사용해 정의한다./*
enum 열거형이름 { 상수명1, 상수명2, ... }
*/
enum Day { // 0부터 연속적인 정수값 부여
SUNDAY, MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY;
}
과일 enum을 정의하고 EnumTest 클래스에서 사용해보자.
enum Fruit {
APPLE, MANGO, PINEAPPLE;
}
public class EnumTest {
public static void main(String[] args) {
Fruit fruit = Fruit.APPLE;
System.out.println(fruit + "입니다.");
}
}
실제로 컴파일까지 한 후 바이트 코드를 분석해보면 아래와 같이 출력된다.
// 내부적으로 위에서 작성한 enum은 아래와 같이 바뀐다.
class Fruit
{
public static final Fruit APPLE = new Fruit();
public static final Fruit MANGO = new Fruit();
public static final Fruit PINEAPPLE = new Fruit();
}
==와 compareTo()로 비교 가능하다.(>, <)는 사용 불가능하다.if (Fruit.APPLE > Fruit.MANGO) { // 열거형 상수에는 비교연산자 사용 불가
System.out.println("이렇게 사용하면 컴파일 에러가 발생합니다.");
}
❓ 비교연산자를 사용할 수 없는 이유 ❓
enum은 내부적으로 클래스이기 때문이다. 객체와 객체는 비교연산자를 사용할 수 없으므로 enum에서도 지원하지 않는다.
모든 enum은 내부적으로 java.lang.Enum 클래스를 부모 클래스로 가진다.
| 메소드 | 설명 |
|---|---|
Class getDeclaringClass() | 열거형의 Class 객체를 반환 |
String name() | 열거형 상수의 이름을 문자열로 반환 |
int ordinal() | 열거형 상수가 정의된 순서를 반환 (0부터 시작) |
T valueOf(Class enumType, String name) | 지정된 열거형에서 name과 일치하는 열거형 상수를 반환 |
compareTo(E o) | 지정된 객체보다 작으면 음수, 같으면 0, 크면 양수를 반환 |
그 밖에 clone(), equals(), finalize(), toString(), hashCode() 메소드들도 있는데 이는 Object 클래스로부터 상속받은 메소드이기 때문에 따로 언급하지 않는다.
열거형에서 사용하는 values()와 valueOf() 메서드의 자세한 내용을 java.lang.Enum에서 찾을 수 없다.
이유는 컴파일러가 자동으로 해당 메서드를 추가해 주기 때문이다.
🖥️ values() 메서드
열거형에 정의된 모든 상수를 배열로 반환한다.
enum Fruit {
APPLE, BANANA, ORANGE;
}
public class EnumTest {
public static void main(String[] args) {
Fruit[] fruits = Fruit.values();
System.out.println("현재 제공되는 과일 목록:");
for (Fruit fruit : fruits) {
System.out.println(fruit + " 과일이 있습니다.");
}
}
}
Output
현재 제공되는 과일 목록:
APPLE 과일이 있습니다.
BANANA 과일이 있습니다.
ORANGE 과일이 있습니다.
🖥️ valueOf(String name) 메서드
열거형에서 지정된 문자열과 일치하는 상수를 반환한다.
문자열이 존재하지 않으면 IllegalArgumentException 예외가 발생한다.
enum Fruit {
APPLE, BANANA, ORANGE;
}
public class EnumTest {
public static void main(String[] args) {
Fruit fruit = Fruit.valueOf("BANANA");
System.out.println(fruit + " 과일이 선택되었습니다.");
}
}
Output
BANANA 과일이 선택되었습니다.
컴파일 후 바이트코드를 확인하면 values()와 valueOf() 메서드가 static으로 선언된 것을 확인할 수 있다.
이는 컴파일러가 열거형에 자동으로 추가한 것이다.
열거형은 기본적으로 0부터 시작하는 연속적인 값을 가지지만 불연속적인 값이 필요할 경우 값을 직접 지정할 수 있다.
🖥️ 값 지정 예제
값을 지정할 때는 생성자와 인스턴스 변수를 추가로 정의해한다.
enum Fruit {
APPLE(100), BANANA(150), ORANGE(80);
private final int price; // 가격 저장용 변수 추가
Fruit(int price) { // 생성자
this.price = price;
}
public int getPrice() { // 값을 반환하는 메서드
return price;
}
}
⚠️ 열거형에서 생성자는 묵시적으로 private 이다.
Fruit apple = new Fruit(100); 와 같이 외부에서 생성자를 호출하여 열거형 객체를 생성할 수 없다.
🖥️ 값 사용 예제
public class EnumTest {
public static void main(String[] args) {
for (Fruit fruit : Fruit.values()) {
System.out.println(fruit + "의 가격은 " + fruit.getPrice() + "원입니다.");
}
}
}
Output
APPLE의 가격은 100원입니다.
BANANA의 가격은 150원입니다.
ORANGE의 가격은 80원입니다.
Set은 객체(데이터)를 중복 저장할 수 없고, 저장된 객체를 인덱스로 관리하지 않아 저장 순서가 보장되지 않는 자료구조이다.
공통 메서드 : add() iterator() size() remove() clear()
EnumSet은 Set 인터페이스를 기반으로 하며 Enum 타입 전용으로 설계된 자료구조이다.
import java.util.EnumSet;
enum Fruit {
APPLE, BANANA, ORANGE, MANGO, GRAPE
}
public class EnumSetExample {
public static void main(String[] args) {
// EnumSet 생성
EnumSet<Fruit> set1, set2, set3, set4;
// 지정된 값들로 EnumSet 생성
set1 = EnumSet.of(Fruit.APPLE, Fruit.BANANA, Fruit.ORANGE);
// set1에 없는 값들로 EnumSet 생성
set2 = EnumSet.complementOf(set1);
// 모든 Enum 값을 포함하는 EnumSet 생성
set3 = EnumSet.allOf(Fruit.class);
// 범위 내의 값들로 EnumSet 생성
set4 = EnumSet.range(Fruit.BANANA, Fruit.MANGO);
// 결과 출력
System.out.println("Set 1: " + set1);
System.out.println("Set 2: " + set2);
System.out.println("Set 3: " + set3);
System.out.println("Set 4: " + set4);
}
}
Output
Set 1: [APPLE, BANANA, ORANGE]
Set 2: [MANGO, GRAPE]
Set 3: [APPLE, BANANA, ORANGE, MANGO, GRAPE]
Set 4: [BANANA, ORANGE, MANGO]
switch 문을 사용한 구현
Enum 상수에 따라 분기 처리를 할 때 switch 문을 사용할 수 있다.
public enum TrafficLight {
RED, YELLOW, GREEN;
// 신호등 동작 정의
public String action() {
switch (this) {
case RED:
return "Stop";
case YELLOW:
return "Caution";
case GREEN:
return "Go";
}
throw new AssertionError("Unknown signal: " + this);
}
}
상수별 메서드 구현
switch 대신 상수별 메서드 구현을 사용하면 코드가 더 깔끔하고 유지보수가 쉬워진다.
public enum TrafficLight {
RED {
@Override
public String action() {
return "Stop";
}
},
YELLOW {
@Override
public String action() {
return "Caution";
}
},
GREEN {
@Override
public String action() {
return "Go";
}
};
// 추상 메서드 선언
public abstract String action();
}