Notice
Recent Posts
Recent Comments
Link
«   2025/05   »
1 2 3
4 5 6 7 8 9 10
11 12 13 14 15 16 17
18 19 20 21 22 23 24
25 26 27 28 29 30 31
Archives
Today
Total
관리 메뉴

일상기록

Java - 열거형(Enums) 본문

Java

Java - 열거형(Enums)

너 구 나 2023. 4. 18. 15:15

열거형이란

열거형은 서로 관련된 상수를 편리하게 선언하기 위한 것으로 여러 상수를 정의할 때 사용하면 유용하다. 원제 자바는 C언어와 달리 열거형이라는 것이 존재하지 않았으나 JDK1.5부터 새로 추가되었다. 자바의 열거형은 C언어의 열거형보다 더 향상된 것으로 열거형이 갖는 값뿐만 아니라 타입도 관리하기 때문에 논리적인 오류를 줄일 수 있다.

// 기존 클래스
class Card {
	// 카드 무늬
	static final int CLOVER = 0;
    static final int HEART = 0;
    static final int DIAMOND = 0;
    static final int SPADE = 0;
    
    // 카드 숫자
    static final int TWO = 0;
    static final int THREE = 1;            	
    static final int FOUR = 2;
    
    final int kind;
    final int Value;
}

// 열거형
class Card {
	enum Kind {CLOVER, HEART, DIAMOND, SPADE} // 열거형 Kind를 정의
    enum Value {TWO, THREE, FOUR}		  // 열거형 Value를 정의
    
    final int kind;
    final int Value;
}

기존의 많은 언어들, 예를 들어 C언어에서는 타입이 달라도 값이 같으면 조건식결과가 참(true)이였으나, 자바의 열거형은 '타입에 안전한 열거형(typesafe enum)'이라서 실제 값이 같아도 타입이 다르면 컴파일 에러가 발생한다. 이처럼 값뿐만 아니라 타입까지 체크하기 때문에 타입에 안전하다고 하는 것이다.

if(Card.CLOVER == Card.TWO)                       //  true지만 false이어야 의미상 맞음 (C언어에서는 값만 체크)
if(Card.Kind.CLOVER == Card.Value.TWO)    //  컴파일 에러 값은 같지만 타입이 다름 (java는 값, 타입을 체크)  

더 중요한 것은 상수의 값이 바뀌면, 해당 상수를 참조하는 모든 소스를 다시 컴파일해야 한다는 것이다. 하지만 열거형 상수를 사용하면, 기존의 소스를 다시 컴파일 하지 않아도 된다.

열거형의 정의와 사용

열거형을 정의하는 방법은 간단하다. 괄호{}안에 상수의 이름을 나열하기만 하면 된다.

enum 열거형 이름 { 상수명1, 상수명2, ...}

예를 들어 동서남북 4방향을 상수로 정의하는 열거형 Direction은 다음과 같다.

enum Direction { EAST, SOUTH, WEST, NORTH}

이 열거형에 정의된 상수를 사용하는 방법은 '열거형이름.상수명'이다. 클래스의 static변수를 참조하는 것과 동일하다.

class Unit{
	int x, y;	// 유닛의 위치
    Direction dir;	// 열거형을 인스턴스 변수로 선언
    
    void init(){
    	dir = Direction.EAST	// 유닛의 방향을 EAST로 초기화
    }
}

열거형 상수간의 비교에는 '=='를 사용할 수 있다. equals()가 아닌 '=='로 비교가 가능하다는 것은 그만큼 빠른 성능을 제공한다는 얘기이다. 그러나 '<', '>'와 같은 비교연산자는 사용할 수 없고 compareTo()는 사용가능하다. 

compareTo()는 두 비교대상이 같으면 0, 왼쪽이 크면 양수, 오른쪽이 크면 음수를 반환한다.

if(dir == Direction.EAST) {
	x++;
} else if(dir > Direction.WEST) { // 에러 열거형 상수에 비교연산자 사용불가
	...
} else if(dir.compareTo(Direction.WEST) > 0) { // compareTo()는 사용가능
	...
}

switch문의 조건식에도 열거형을 사용할 수 있다.

void move() {
	switch(dir) {	// 개선된 switch문 JDK14부터 지원 (break; 생략가능)
    	case EAST -> x++;	// Direction.EAST라고 쓰면 안됨
        case WEST -> x--;
        case SOUTH -> y++;
        case NORTH -> y--;
    }
}

주의할 점은 case문에 열거형의 이름은 적지 않고 상수의 이름만 적어야 한다는 제약이 있다.

모든 열거형의 조상 - java.lang.Enum

열거형 Direction에 정의된 모든 상수를 출력하려면, 다음과 같이 해야한다.

Direction[] dArr = Direction.values();

for(Direction d : dArr){
	System.out.printf("%s = %d%n", d.name(), d.ordinal()); // 이름 = 순서
}

values()는 열거형의 모든 상수를 배열에 담아 반환한다. 이 메소드는 모든 열거형이 가지고 있는 것으로 컴파일러가 자동으로 추가해 준다. ordinal()은 모든 열거형의 조상인 java.lang.Enum클래스에 정의된 것으로, 열거형 상수가 정의된 순서(0부터 시작)를 정수로 반환한다.

 Enum클래스에는 그 밖에도 다음과 같은 메소드가 정의되어 있다.

열거형에 맴버 추가하기

Enum클래스에 정의된 ordinal()이 열거형 상수가 정의된 순서를 반환하지만, 이 값을 열거형 상수의 값으로 사용하지 않는 것이 좋다. 이 값은 내부적인 용도로만 사용되기 위한 것이기 때문이다.

 열거형 상수의 값이 불연속적인 경우에는 이때는 다음과 같이 열거형 상수의 이름 옆에 원하는 값을 괄호()와 함께 적어주면 된다.

enum Direction { EAST(1), SOUTH(5), WEST(-1), NORTH(10) }

지정된 값을 저장할 수 있는 인스턴스 변수와 생성자를 새로 추가해 주어야 한다. 이 때 주의할 점은, 먼저 열거형 상수를 모두 정의한 다음에 다른 맴버들을 추가해야한다는 것이다. 열거형 상수 마지막에도 ';(세미클론)'이 붙는다.

enum Direction {
	EAST(1), SOUTH(5), WEST(-1), NORTH(10)	// 끝에 ';'를 추가해야 한다.
    
    private final int value; // 정수를 저장할 필드(인스턴스 변수)를 추가
    Direction(int value) { this.value = value; } // 생성자 추가 private생략
    
    public int getValue() { return value; }
}

열거형의 인스턴스 변수는 반드시 final이어야 한다는 제약은 없지만, value는 열거형 상수의 값을 저장하기 위한 것이므로 final을 붙였다. 그리고 외부에서 이 값을 얻을 수 있게 getValue()도 만들었다.

Direction d = new Direction(1);       //  에러  열거형의 생성자는 외부에서 호출불가

열거형 Direction에 새로운 생성자가 추가되었지만, 위와 같이 열거형의 객체를 생성할 수 없다. 열거형의 생성자는 제어자가 묵시적으로 private이기 때문이다.

enum Direction {
	...
    Direction(int value) { // private Direction(int value)와 동일
    ...
    }
}

필요하다면, 다음과 같이 하나의 열거형 상수에 여러 값을 지정할 수도 있다. 다만 그에 맞게 인스턴스 변수와 생성자 등을 새로 추가해주어야 한다.

enum Direction {
	EAST(1, ">"), SOUTH(2, "V"), WEST(3, "<"), NORTH(4, "^");
    
    private final int value;
    private final String symbol;
    
    Direction(int value, String symbol) { // 접근 제어자 private이 생략됨
    	this.vlaue = value;
        this.symbol = symbol;
    }
    
    public int getValue(){ return vlaue; }
    public String getSymbol(){ return symblo; }
}

열거형에 추상 메소드 추가하기

열거형 Transportation은 운송 수단의 종류 별로 상수를 정의하고 있으며, 각 운송 수단에는 기본요금(BASIC_FARE)이 책정되어 있다.

enum Transportation {
	BUS(100), TRAIN(150), SHIP(100), AIRPLANE(300);
    
    private final int BASIC_FARE;
    
    private Transportation(int basicFare) {
    	BASIC_FARE = basicFare;
    }
    
    int fare () { // 운송 요금을 반환
    	return BASIC_FARE;
    }
}

그러나 이것만으로는 부족하다. 거리에 따라 요금을 계산하는 방식이 각 운송 수단마다 다를 것이기 때문이다. 이럴 때, 열거형에 추상 메소드 'fare(int distance)'를 선언하면 각 열거형 상수가 이 추상 메소드를 반드시 구현해야 한다.

enum Transportation {
	BUS(100) { int fare(int distance) { return distance*BASIC_FARE;}},
    TRAIN(150) { int fare(int distance) { return distance*BASIC_FARE;}},
    SHIP(150) { int fare(int distance) { return distance*BASIC_FARE;}},
    AIRPLANE(150) { int fare(int distance) { return distance*BASIC_FARE;}};
    
    abstract int fare(int distance); // 거리에 따른 요금을 계산하는 추상 메소드
    
    protected final int BASIC_FARE; // protected로 해야 각 상수에서 접근가능
    
    Transportation(int basicFare) {
    	BASIC_FARE = basicFare;
    }
    
    public int getBasicFare() { return BASIC_FARE; }
}

위의 코드는 열거형에 정의된 추상 메소드를 각 상수가 어떻게 구현하는지 보여준다. 마치 익명 클래스를 작성한 것처럼 보일 정도로 유사하다.

예제에선 각 열거형 상수가 추상 메소드 fare()를 똑같은 내용으로 구현했지만, 다르게 구현될 수도 있게 하기위해 추상 메소드로 선언한 것이다. 열거형에서 추상 메소드를 선언할 일은 그리 많지 않으므로 이렇게 사용 할 수도 있다는 정도만 알고 있으면 된다.

열거형의 이해

지금까지 열거형에 대해서 살펴보았는데, 열거형의 이해를 돕기 위해 마지막으로 열거형이 내부적으로 어떻게 구현되었는지에 대해 설명하고자 한다. 만일 열거형 Direction이 다음과 같이 정의되어 있을 때,

enum Direction { EAST, SOUTH, WEST, NORTH }

실제론 열거형 상수 하나하나가 Direction객체이다. 위의 문장을 클래스로 정의한다면 다음과 같다.

class Direction {
	static final Direction EAST = new Direction("EAST");
	static final Direction SOUTH = new Direction("SOUTH");    
	static final Direction WEST = new Direction("WEST");
   	static final Direction NORTH = new Direction("NORTH");
    
    private String name;
    
    private Direction(String name) {
    	this.name = name;
    }
}

Direction클래스의 static상수 EAST, SOUTH, WEST, NORTH의 값은 객체의 주소이고, 이 값은 바뀌지 않는 값이므로 '=='로 비교가 가능한 것이다.

 모든 열거형은 추상 클래스 Enum의 자손이므로, Enum을 흉내 내어 MyEnum을 작성 하면 다음과 같다.

abstract class MyEnum<T extends MyEnum<T>> implement Comparable<T> {
	static int id = 0; // 객체에 붙일 일련번호(0부터 시작)
    
    int ordinal;
    String name = "";
    
    public int ordinal() { return ordinal; }
    
    MyEnum(String name) {
    	this.name = name;
        ordinal = id++; // 객체를 생성할때 마다 id값 증가
    }
    
    public int compareTo(T t){
    	return ordinal - t.ordinal();
    }
}

코드를 설명하자면 객체가 생성될 때 마다 번호를 붙여 인스턴스변수 ordinal에 저장한다. 그후 Comparable인터페이스를 구현해서 열거형 상수간의 비교가 가능하도록 되어 있다. (compareTo())구현 내용은 간단하다. 두 열거형 상수의 ordinal값을 서로 빼주기만 하면 된다. 만일 클래스를 MyEnum<T>와 같이 선언하였다면, compareTo()를 위와 같이 간단히 작성할 수 없었을 것이다. 타입 T에 ordinal()이 정의되어 있는지 확인할 수 없기 때문이다.

abstract class MyEnum<T> implements Comparable<T> {
		...
    public int compareTo(T t) {
    	return ordinal - t.ordinla(); // 에러 타입 T에 ordinal()이 있나?
    }
}

그래서 MyEnum<T extends MyEnum<T>>와 같이 선언한 것이며, 이것은 타입 T가 MyEnum<T>의 하위클래스(자손)이어야 한다는 의미이다. 타입 T가 MyEnum의 하위클래스(자손)이므로 ordinal()이 정의되어 있는 것은 분명하므로 형변환 없이도 에러가 나지 않는다.

 그리고 추상 메소드를 새로 추가하면, 클래스 앞에도 'abstract'를 붙여줘야 하고, 각 static상수들도 추상 메소드를 구현화해주어야 한다. 아래의 코드에서는 익명 클래스의 형태로 추상 메소드를 구현했다.

abstract class Direction extends MyEnum {
	static final Direction EAST = new Direction("EAST") { // 익명 클래스
    	Point move(Point p) { ... }
    };
    static final Direction SOUTH = new Direction("SOUTH") { // 익명 클래스
    	Point move(Point p) { ... }
    };
    static final Direction WEST = new Direction("WEST") { // 익명 클래스
    	Point move(Point p) { ... }
    };
    static final Direction NORTH = new Direction("NORTH") { // 익명 클래스
    	Point move(Point p) { ... }
    };
    
    private String name;
    
    private Direction(String name) {
    	this.name = name;
    }
    
    abstract Point move(Point p);
}

 

'Java' 카테고리의 다른 글

java.util.function패키지  (0) 2023.04.18
Java - 람다식(Lambda expression)  (1) 2023.04.18
Java - 제네릭 타입의 제거  (0) 2023.04.18
Java - 제네릭 타입의 형변환  (0) 2023.04.18
Java - 제네릭 메소드  (0) 2023.04.18