- Enum 정의하는 방법
- Enum이란?
enum(열거형)은 서로 관련된 상수를 편리하게 선언하기 위한 것으로 상수를 여러개 정의할 때 사용합니다. enum은 여러 상수를 정의한 후, 정의된 것 이외의 값은 허용하지 않습니다.
가장 간단하게 선언하는 방법은 다음과 같습니다.
enum 열거형이름 {상수명1 상수명2, 상수명3}
public class EnumDemo {
enum Season {SPRING, SUMMER, FALL, WINTER}
public static void printSeason(Season season) {
switch (season) {
case SPRING:
System.out.println("봄입니다.");
break;
case SUMMER:
System.out.println("봄입니다.");
break;
case FALL:
System.out.println("봄입니다.");
break;
case WINTER:
System.out.println("봄입니다.");
break;
default:
throw new IllegalArgumentException("계절의 이름이 아닙니다.")
}
}
public static void main(String[] args) {
printSeason(Season.SPRING);
}
}
- 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");
case BANANA:
System.out.println("16 kcal");
break;
default:
}
}
}
우선 위 코드에서 마음에 들지 않는 점은 각각의 상수에 부여된 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) {} // 컴파일 에러 발생
}
}
둘이 애초에 비교를 하지 못하도록 하려면 서로 다른 객체로 만들어 주면 됩니다.
public class EnumDemo {
public static void main(Strng[] 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;
}
}
}
이렇게 하면 위에서 언급했던 문제들 (1) 상수와 리터럴이 논리적인 연관이 없음 2) 서로 다른 개념끼리 이름이 충돌할 수 있음. 3) 서로 다른 개념임에도 비교하는 코드가 가능함 이 모두 해결 됩니다.
하지만 다른 문제가 발생합니다. 사용자 정의 타입은 switch문의 조건에 들어갈 수 없습니다. (switch문의 조건으로 들어갈 수 있는 데이터 타입은 byte, short, char, int, enum, String, Byte,, Short, Character, Integer 입니다.
- Java Enum의 특징
1. 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을 학습하는데 있어서 훨씬 이해하기가 쉽습니다.
2. 생성자와 메서드를 추가할 수 있습니다.
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 staitc final 입니다.
생성자를 이용해서 상수에 데이터를 추가할 수 있습니다.
enum Currency {
PENNY(1), NICKLE(5), DIM(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 * 2.0;
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);
}
3. 상수 간의 비교가 가능합니다.
enum 상수 간의 비교에는 == 를 사용할 수 있습니다. 단, > , < 같은 비교연산자는 사용할 수 없고, compareTo()를 사용할 수 있습니다.
Enum은 언제 사용하는가?
필요한 원소를 컴파일 타임에는 다 알 수 있는 상수 집합이라면 항상 열거 타입을 사용합시다. 태양계 행성, 한 주의 요일, 체스 말처럼 본질적으로 열거 타입인 타입도 당연히 포함됩니다. 그리고 메뉴 아이템, 연산코드, 명령줄 플래그 등 허용하는 값 모두를 컴파일타임에 이미 알고 있을 때도 쓸 수 있습니다. 열거 타입에 정의된 상수 개수가 영원히 고정 불변일 필요는 없습니다. 열거 타입은 나중에 상수가 추가 되어도 바이너리 수준에서 호환되도록 설계 되었습니다.
- 이펙티브 자바 3/E. Item 34. 219쪽
실무에서 어떻게 활용될 수 있는지는 아래의 글을 참조하면 좋습니다.
https://woowabros.github.io/tools/2017/07/10/java-enum-uses.html
- Enum이 제공하는 메소드
- T[] values() - 해당 enum 타입에 정의된 상수 배열을 반환합니다.
- Class<E> getDeclaringClass() - 열거형의 객체를 반환합니다.
- String name() - 열거형 상수의 이름을 문자열로 반환합니다.
- int ordina() - 열거형 상수가 정의된 순서를 반환합니다. (0부터 시작)
- T valueOf(Class<T> enumType, String name) - 지정된 열거형에서 name과 일치하는 열거형 상수를 반환합니다.
- 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.name())
}
}
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.
ordinal은 Enum 내부에서 사용하기 위해 만든 것이지, 프로그래머가 이 메소드에 의존하는 코드를 작성하는 것은 안티 패턴입니다.
- T valueOf(Class<T>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은 별도로 상속을 받을 수 없습니다.
package java.lang;
import java.io.Serializable;
import java.io.IOException;
import java.io.InvalidObjectException;
import java.io.ObjectInputStream;
import java.io.ObjectStreamException;
public abstract class Enum<E extends Enum<E>> implements 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;}
public final int hashCode() { return super.hashCode();}
protected final Object clone() throws CloneNotSupportedException {
throw new CloneNotSupportedException();
}
public final int compareTo(E o) {
Enum<?> other = (Enum<?>)o;
Enum<E> self = this;
if (self.getClass() != other.getClass() && // optimization
self.getDeclaringClass() != other.getDeclaringClass())
throw new ClassCastException();
return self.ordinal - other.ordinal;
}
@SuppressWarnings("unchecked")
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> enumType, String name) {
T result = enumType.enumConstantDirectory().get(name);
if (result != null)
return result;
if (name == null)
throw new NullPointerException("Name is null");
throw new IllegalArgumentException(
"No enum constant " + enumType.getCanonicalName() + "." + name);
}
protected final void finalize() { }
private void readObject(ObjectInputStream in) throws IOException,
ClassNotFoundException {
throw new InvalidObjectException("can't deserialize enum");
}
private void readObjectNoData() throws ObjectStreamException {
throw new InvalidObjectException("can't deserialize enum");
}
}
앞서 언급했던 메소드들은 모두 여기 정의되어 있습니다. toString을 제외한 대부분의 메소드는 final로 선언되어 있기 때문에 별도의 오버라이딩을 할 수 없습니다.
- enum 생성자는 왜 private인가요?
java에서 enum타입은 열거형을 의미하는 특별한 형태의 클래스입니다.
그렇기 때문에 일반 클래스와 같이 생성자가 있어야 합니다. 물론 생성자를 만들어 주지 않아도 java가 default 생성자를 만들어주긴 하지만, enum의 경우에는 생성자의 접근제어자를 private로 지정합니다.
enum 타입은 고정된 상수들의 집합으로써, 런타임이 아닌 컴파일타임에 모든 값을 알고 있어야 합니다. 클래스에서 enum 타입에 접근해서 동적으로 어떤 값을 정해줄 수 없습니다.
해당 enum클래스 내에서 까지도 new 키워드로 인스턴스 생성이 불가능합니다.
이렇게 하면 외부에서 접근 가능한 생성자가 없으므로 enum 타입은 실제로 final가 다름이 없습니다.
- EnumSet
EnumSet 열거형을 위해 고안된 특별한 Set 인터페이스 구현체입니다. HashSet과 비교했을 대, 성능 상의 이점이 많기 때문에 열거형 데이터를 위한 Set이 필요한 경우 EnumSet을 사용하는 것이 좋습니다.
EnumSet의 상속 구조는 다음과 같습니다.
EnumSet의 중요한 특징 몇가지를 정리해보면 다음과 같습니다.
- EnumSet은 AbstrsctSet 클래스를 상속하고 Set 인터페이스를 구현합니다.
- 오직 열거형 상수만을 값으로 가질 수 있습니다. 또한 모든 값은 enum type이어야 합니다.
- null value를 추가하는 것을 허용하지 않습니다. NullPointException을 던지는 것도 허용하지 않습니다..
- ordinal 값의 순서대로 요소가 저장됩니다.
- thread-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
wisdom-and-record.tistory.com/52
'JAVA' 카테고리의 다른 글
I/O (Input/Output) (0) | 2021.02.20 |
---|---|
어노테이션 (0) | 2021.02.03 |
멀티쓰레드 프로그래밍 (0) | 2021.01.19 |
자바의 예외처리 (0) | 2021.01.16 |
StringBuffer, StringBuilder 가 String 보다 성능이 좋은 이유와 원리 (0) | 2021.01.03 |