[Live Study 11주차] Enum

이호석·2022년 9월 10일
0

목표

  • 자바의 열거형에 대해 학습하세요.

학습할 것

  • enum 정의하는 방법
  • enum이 제공하는 메소드 (values()와 valueOf())
  • java.lang.Enum
  • EnumSet

Enum의 이해

열거형을 소개하기 앞서 Enum에 대해 이해를 해봅시다.
열거형은 서로 관련된 상수를 편리하게 선언하기 위한 것으로 여러 상수를 정의할 때 사용하면 유용하다.

C언어에서도 열거형을 정의하여 사용할 수 있지만, Java에서의 열거형은 열거형의 값 뿐만 아니라 타입도 관리하기 때문에 보다 향상된 기능을 제공해주고, 보다 논리적인 오류를 줄일 수 있다.

자바에서의 열거형은 타입까지 관리해준다. 그렇다면 타입을 관리하여 얻는 이점은 무엇일까?

package enumeration;


class Card1 {
    static final int CLOVER = 0;
    static final int HEART = 1;
    static final int DIAMOND = 1;
    static final int SPADE = 1;

    static final int TWO = 0;
    static final int THREE = 1;
    static final int FOUR = 2;
}

class Card2 {
    // 열거형의 정의 default로 0부터 순차적으로 값이 초기화된다.
    enum Kind {CLOVER, HEART, DIAMOND, SPADE}
    enum Value {TWO, THREE, FOUR}
}

public class Main {
    public static void main(String[] args) {

        // 타입체크가 되지 않음, true지만, 의미상 false여야 함
        if (Card1.CLOVER == Card1.TWO) {
            System.out.println("Card1.CLOVER == Card1.TWO");
        }

		// 컴파일 오류 발생 
        // (Operator '==' cannot be applied to 'enumeration.Card2.Kind', 'enumeration.Card2.Value')
        if (Card2.Kind.CLOVER == Card2.Value.TWO) {
            System.out.println("Card2.Kind.CLOVER == Card2.Value.TWO");
        }
    }
}

Card1은 Enum을 이용하지 않고 직접 상수를 정의해서 사용하고 있고,
Card2는 Enum을 이용한다.

Card1의 CLOVER와 TWO는 값이 0이므로 서로 비교하게 되면 true값을 반환하지만, 실제 우리가 의도한 의미를 생각했을때 false반환해야 한다. 즉, 이 코드는 타입은 고려하지 않고 순전히 값만 고려했을때 발생할 수 있는 문제를 보여준다.

반대로 Card2의 CLVOER와 TWO는 둘의 Enum타입이 다르므로 애초에 컴파일 시점에서 오류를 반환한다.

자바의 Enum은 Type Safety를 보장해 주므로 실제 값이 같아도 타입이 다르다면 컴파일 에러가 발생한다.

또한 상수의 값이 변경되면 해당 상수를 참조하는 모든 소스를 다시 컴파일 해야 하지만, 열거형 상수를 사용하면 이름을 변경하지 않고 값만 변경한다면 참조는 그대로이고 재컴파일 대상에서 제외되므로 기존 소스를 재컴파일하지 않는다.



enum 정의하는 방법

자바는 enum이라는 키워드를 통해 열거형을 정의한다.
상수만 존재하는 경우 세미콜론을 생략할 수 있으며 열거형이름.상수명으로 사용할 수 있다.

// 정의
enum Direction { EAST, SOUTH, WEST, NORTH }

// 사용
class Unit {
	int x, y;
    Direction dir;
    
    void init() {
    	dir = Direction.EAST;
    }
}

또한 열거형은 몇가지의 특징을 가진다.

  • 열거형 상수간 비교에는 ==사용 가능, 하지만 <, >와 같은 비교연산자는 사용 불가
  • compareTo() 사용 가능
  • switch문의 조건식에도 사용가능 단, case문에 열거형의 이름이 아닌 상수의 이름만 적어야 한다.

위의 특징을 코드로 직접 살펴보자


Class Unit {
	int x, y;
    Direction dir;
    
    void init() {
    	dir = Direction.EAST;
    }
}

class Main {
	public static void main(String[] args) {
    	Unit unit = new Unit();
        unit.init();
        
        // ==과 compareTo() 사용
        if (unit.dir == Direction.EAST) {
        	System.out.println("참이므로 출력");
        } else if (dir.compareTo(Direction.WEST)) {
        	System.out.println("출력될 일 없음");
        }
        
        
        // switch문에서의 이용        
        int x = 0, y = 0;
        switch(dir) {
        	case EAST: x++;
            	break;
            case WEST: x--;
            	break
            case SOUTH: y++;
            	break;
            case NORTH: y--;
            	break;
        }
        
    }
}

열거형에 멤버 추가

Enum의 상수들이 불연속적인 값이라면 우리가 원하는 값을 열거형에 직접 추가할 수 있다. 다만 임의의 값을 지정했다면 지정된 값을 저장할 수 있는 인스턴수 변수 및 생성자를 추가해야한다.

enum Direction {
    // 인스턴스 변수 및 생성자가 추가되면 반드시 세미콜론을 붙여줘야 한다.
	EAST(1), SOUTH(5), WEST(-1), NORTH(10);
    
    private final int value;
    // 생성자는 public이면 안된다.
    Direction(int value) {
    	this.value = value;
    }
    
    // 메소드도 추가할 수 있다.
    public int getValue() {
    	return value;
    }
    
}

인스턴스 변수는 굳이 final일 필요가 없지만, 어차피 열거형의 value값은 변경되지 않으므로 명시적으로 작성했다.

또한 열거형의 생성자는 묵시적으로 private이다. 이는 열거형을 사용자가 직접 new를 통해 생성하는것을 방지하기 위해서이다.


열거형 상수에 여러값 지정

열거형 상수는 여러 값을 지정할 수 있다. 다만 추가되는 것과 동일한 자료형의 인스턴스 변수와 생성자도 정의해야 한다.

enum Direction {
    // 인스턴스 변수 및 생성자가 추가되면 반드시 세미콜론을 붙여줘야 한다.
	EAST(1, ">"), SOUTH(5, "V"), WEST(-1, "V"), NORTH(10, "^");
    
    private final int value;
    private final String symbol;
    
    // 생성자는 public이면 안된다.
    Direction(int value, String symbol) {
    	this.value = value;
        this.symbol = symbol;
    }
}

열거형에 추상 메서드 추가

열거형은 추상메서드를 추가할 수 있는데, 이렇게 되면 모든 열거형 상수는 마치 익명 객체를 구현하듯 해당 추상 메서드를 구현해야 한다.

다음 예시는 대중교통의 요금들을 열거형으로 지정했는데, 각 대중교통마다 거리당 요금이 다르다. 이를 추상 메서드를 통해 해결해 보자

enum Transportation {
	BUS(100) {
    	// 추상 메서드 구현
		int fare(int distance) { return distance * BASIC_FARE; }
	},
   	TRAIN(150) {
		int fare(int distance) { return distance * BASIC_FARE; }
	},
    SHIP(100) {
		int fare(int distance) { return distance * BASIC_FARE; }
	},
    AIRPLANE(300) {
		int fare(int distance) { return distance * BASIC_FARE; }
	}
    
    // 거리에 따른 요금을 계산
    abstract int fare(int distance);
    
    // protected로 선언해야 각 상수에서 접근할 수 있다.
    protected final int BASIC_FARE;
    
    Transportation(int basicFare) {
    	BASIC_FARE = basicFare;
    }
}

추상메서드를 선언한 열거형에서 각 열거형 상수는 해당 추상 메서드를 반드시 구현해야한다.

위의 코드를 보면 BASIC_FAREprotected 접근제어자로 선언된 것을 볼 수 있는데, 이를 이해하기 위해서는 열거형의 내부적인 구조를 알아야 한다.


열거형의 이해(내부구조)

모든 열거형은 추상 클래스 Enum의 자손이므로 Enum과 비슷한 MyEnum은 다음과 같이 이루어진다.

public abstract class MyEnum<T extends MyEnum<T>> implements Comparable<T> {
    static int id = 0;
    int ordianl;
    String name = "";

    public int ordinal() {
        return ordinal;
    }

    MyEnum(String name) {
        this.name = name;
        ordianl = id++;
    }

    @Override
    public int compareTo(T t) {
        return ordianl - t.ordianl();
    }
}

MyEnum이 생성될때마다 ordinal변수에 서로다른 번호를 붙여준다.
또한 Comparable 인터페이스를 구현해 열거형 상수간의 비교를 할 수 있다.

여기서 MyEnum은 <T extends MyEnum<T>>로 선언되어 있다. 즉, MyEnum을 상속받는 클래스만 T로 들어올 수 있으며, 자식 클래스라면 반드시 ordinal() 메서드를 가지고 있으므로 compareTo(T t)를 간단하게 구현할 수 있다.

위의 MyEnum을 이용해 MyTransportationclass를 이용해 생성해보고, 추상 메서드인 int fare(int distance)메서드도 구현해보자

public abstract class MyTransportation extends MyEnum {
    static final MyTransportation BUS = new MyTransportation("BUS", 100) {
        int fare(int distance) {
            return distance * BASIC_FARE;
        }
    };
    static final MyTransportation TRAIN = new MyTransportation("TRAIN", 150) {
        int fare(int distance) {
            return distance * BASIC_FARE;
        }
    };
    static final MyTransportation SHIP = new MyTransportation("SHIP", 100) {
        int fare(int distance) {
            return distance * BASIC_FARE;
        }
    };
    static final MyTransportation AIRPLANE = new MyTransportation("AIRPLANE", 300) {
        int fare(int distance) {
            return distance * BASIC_FARE;
        }
    };

    abstract int fare(int distance);

    protected final int BASIC_FARE;

    MyTransportation(String name, int basicFare) {
        super(name);
        BASIC_FARE = basicFare;
    }

    public String name() {
        return name;
    }
    @Override
    public String toString() {
        return name;
    }
}

이것이 Enum의 실제 동작하는 방식이다. 즉 각각의 상수들은 실제로 객체가 되며 이러한 특징 덕분에 타입비교 및 값 비교를 할 수 있고 타입비교시에 type safety하다.



enum이 제공하는 메소드 (values()와 valueOf())

java.lang.Enum은 여러 메서드를 제공해주며 컴파일러에 의해 추가되는 몇가지의 특수 메서드도 존재한다.

final 메서드

일반적으로 Enum에서 제공되는 메서드는 final 키워드가 붙여져 있어 메서드의 재정의를 금지하고 있다.

메서드설명
Class<E> getDeclaringClass()열거형의 Class객체를 반환한다.
String name()열거형 상수의 이름을 문자열로 반환한다.
int ordinal()열거형 상수가 정의된 순서를 반환한다.(0부터 시작)
T valueOf(Class<T> enumType, String name)지정된 열거형에서 name과 일치하는 열거형 상수를 반환한다.

컴파일러에 추가되는 특수 메서드

java.lang.Enum에 정의되지 않은 특수 메서드가 존재하는데 이는 컴파일러가 열거형을 만들때 자동으로 추가해주는 특수 메서드이다. 해당 메서드들은 static 메서드로 생성된다.

  • static E values(): 열거형의 모든 상수를 배열에 담아 반환
  • static E valueOf(String name): 문자열 상수에 대한 참조를 얻을 수 있게 해준다.



java.lang.Enum

모든 열거형들은 묵시적으로 java.lang.Enum 클래스를 상속받고 있다.
따라서 enum을 제외한 다른 일반 클래스에서 명시적으로 Enum 클래스를 상속받으면 컴파일러에서 오류를 반환한다.

Enum 클래스의 메서드들은 대부분 final로 선언되어 있어, 메서드의 재정의를 금지하고 있다.



EnumSet

Class EnumSet<E extends Enum<E>>

자바 EnumSet은 다음과 같이 정의되어 있다. EnumSet은 enum만 set의 요소가 될 수 있고, 단일 enum만 추가할 수 있다.

또한 EnumSet은 AbstractSet을 상속받고 있고, Set 인터페이스를 구현하고 다음과 같은 기능들을 제공하고 있다.

  • Set 인터페이스의 뼈대를 구현한 AbstractSet을 상속받고 Set인터페이스를 구현한다.
  • thread-safe하지 않음 즉, 동기화 되지 않는다. 동기화를 하기 위해서는 메소드 외부에서 동기화 되어야 한다. 혹은Collections.synchronizedSet(java.util.Set<T\>)을 이용해 EnumSet을 매핑한다.
    • Set<MyEnum> s = Collections.synchronizedSet(EnumSet.noneOf(MyEnum.class));
  • 열거형 집합은 내부적으로 비트 벡터로 표현되며 이 표현은 매우 작고 효율적이다.
    • 일반적으로 보장되진 않지만 HashSet보다 빠를것으로 기대된다.
  • null을 허용하지 않는다.

EnumSet의 메서드

EnumSet은 기존 Set에 정의된 메서드들을 제외하고 추가 메서드들을 제공해준다.
주요적인 메서드 몇개와 실제 사용예시를 알아보자

MethodDescription
static <E extends Enum<E>>
EnumSet<E> allOf(Class<E> elementType)
지정된 요소 유형의 모든 요소를 포함하는 열거형 집합을 만듦
static <E extends Enum<E>>
EnumSet<E> complementOf(EnumSet<E> s)
지정된 집합에 포함되지 않은 해당 형식의 모든 요소를 포함함
static <E extends Enum<E>>
EnumSet<E> copyOf(EnumSet<E> s)
처음에 동일한 요소(있는 경우)를 포함하는 지정된 열거형 집합과 동일한 요소 유형으로 열거형 집합을 만듭니다.
static <E extends Enum<E>>
EnumSet<E> copyOf(Collection<E> c)
지정된 컬렉션에서 초기화된 열거형 집합을 만듭니다.
static <E extends Enum<E>> EnumSet<E> of(E e1, E e2)매개변수로 지정된 요소를 포함하는 열거형 집합을 만든다.
import java.util.EnumSet;

enum Gfg {CODE, LEARN, CONTRIBUTE, QUIZ, MCQ};

public class EnumSetDemo {

    public static void main(String[] args) {

        EnumSet<Gfg> set1, set2, set3, set4, set5;

        set1 = EnumSet.of(Gfg.QUIZ, Gfg.CONTRIBUTE, Gfg.LEARN, Gfg.CODE);
        set2 = EnumSet.complementOf(set1);
        set3 = EnumSet.allOf(Gfg.class);
        set4 = EnumSet.range(Gfg.CODE, Gfg.CONTRIBUTE);
        set5 = EnumSet.copyOf(set4);

        System.out.println("Set 1: " + set1);
        System.out.println("Set 2: " + set2);
        System.out.println("Set 3: " + set3);
        System.out.println("Set 4: " + set4);
        System.out.println("Set 5: " + set5);
    }
}
출력결과
Set 1: [CODE, LEARN, CONTRIBUTE, QUIZ]
Set 2: [MCQ]
Set 3: [CODE, LEARN, CONTRIBUTE, QUIZ, MCQ]
Set 4: [CODE, LEARN, CONTRIBUTE]
Set 5: [CODE, LEARN, CONTRIBUTE]

이 외에도 일반적인 Set에서 제공하는 메서드를 통해 요소들을 추가할 수 있다.

enum Game { CRICKET, HOCKEY, TENNIS }

...

// Using add() method
games2.add(Game.HOCKEY);

// Printing the elements to the console
System.out.println("EnumSet Using add(): " + games2);

// Using addAll() method
games2.addAll(games1);

// Printing the elements to the console
System.out.println("EnumSet Using addAll(): " + games2);

출력결과
EnumSet Using add(): [HOCKEY]
EnumSet Using addAll(): [CRICKET, HOCKEY, TENNIS]



Referencese

profile
꾸준함이 주는 변화를 믿습니다.

0개의 댓글