티스토리 뷰

이 글은 백기선의 자바 라이브 스터디 유튜브 영상과 스터디원들의 정리글을 참고하여 정리 한 글입니다.

https://github.com/whiteship/live-study/issues/11

 

1. 목표

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

 

2. 학습할 것 (필수)

  • enum을 정의하는 방법
  • enum이 제공하는 메소드
  • java.lang.Enum
  • EnumSet

 

3. Enum이란 무엇인가

Enum은 열거형이라고 불리며, 서로다른 상수를 편리하게 선언하기 위한 것으로 상수를 여러 개 정의할 때 사용한다. 

Enum은 여러 상수를 정의한 후, 정의된 것 이외의 값은 허용하지 않는다.

 

일단 enum을 왜쓸까?

상수를 편하게 관리하기 위해서 public static final을 통해 전역변수로서 상수를 설정할 수 있는데 어떠한 이점이 있기에 쓰는 것일까?

enum을 잘 사용하면 코드의 가독성을 높히고, 논리적인 오류를 줄일 수 있다고 한다.

public static final을 통해 상수를 하나하나 선언하는 것 보다 enum을 통해서 상수를 선언하면 어떤 장점이 있을까?

그 이유는 Type safety와 관련이 있다.

 

자바는 Type safe한 언어이다.. 라는 말은 얼핏 들어 보았지만 정확히 Type safety하다는 것이 무슨 의미 일까?

Type safety란 컴파일러가 컴파일 하는 동안 타입을 검정하여 변수에 잘못된 타입을 할당할경우 컴파일 에러를 던지는 것이다. 타입이 다른 경우 컴파일 타임에서 잡을 수 있는 경우(IDE에서 빨간줄로) 타입 세이프 하다고 하는 것이다.

 

그럼 왜 enum을 사용하는 것이 type safe할까

예를들어 다음과 같이 과일이름을 입력받으면, 해당 과일의 칼로리를 출력하는 프로그램이 있고, 과일이름을 다음과 같이 상수로 관리한다고 해보자.

	...
    public static final int APPLE = 1;
	public static final int BANANA = 2;
	public static final int ORANGE = 3;
    
    ...
     public static final int MS = 1;
     public static final int APPLE = 2;
     public static final int AMAZON = 3;
    
    public static void main(String[] args) {
		
		
		int type = APPLE;
		switch (type) {
		case APPLE:
			System.out.println("32 kcal");
			break;
		 case BANANA:
             System.out.println("52 kcal");
             break;
         case ORANGE:
             System.out.println("16 kcal");
             break;
		}

위 코드에서의 문제점은 첫번째, 상수에 부여된 1, 2, 3이라는 임의의 리터럴은 구분하고 이용하기 위해서 사용된 것이지, 논리적으로 아무 의미가 없다. 또한 둘째, 상수를 더 추가하였을때, 상수의 이름이 중복될 경우 충돌하여 컴파일 에러를 발생한다. 그리고 그것을 해결하기 위해서 이름을 다르게 해줄수도 있고, 다음과 같이 인터페이스를 이용하여 구분 할 수 있다.

interface Fruits {
	int APPLE = 1, BANANA = 2, ORANGE = 3;
}

interface Companies {
	int MS = 1, APPLE = 2, AMAZON = 3;
}

하지만 여기서의 문제는, 셋째, 타입 세이프 하지 않다는 것이다.

만약 아래와 같은 코드를 작성할때,

if(Fruits.APPLE == Companies.APPLE) {
...
}

위와같이 과일과 회사는 서로 비교 조차 되어서는 안되는 개념으로 컴파일 에러를 발생하지 않기 때문에 타입 세이프 하지 못하다. 따라서 위와 같은 코드는 애초에 작성할 때 부터 컴파일 과정에서 막아주는 것이 좋다.

타입 비교 자체가 불가능 하게 서로 다른 객체로 다음과 같이 만들어 줄 수 있다.

class Fruits {
    public static final Fruit APPLE = new Fruit();
    public static final Fruit BANANA = new Fruit();
    public static final Fruit ORANGE = new Fruit();
}

class Companies {
    public static final Company MS = new Company();
    public static final Company APPLE = new Company();
    public static final Company AMAZON = new Company();
}

이렇게 하면 위의 세가지 문제가 모두 해결된다.

1) 상수와 리터럴의 논리적인 연관성이 없다

2) 서로 다른 개념간의 이름 충돌

3) 서로 다른 개념간의 비교가 가능하여 Type-safe하지 못함.

 

위와 같은 개념을 조금 더 다루기 쉽게 만든것이 enum이다. 특히 switch문을 이용할 때 간결해진다.

사용자 정의 타입은 switch 문에 들어갈 수 없지만, enum은 가능하다. switch문의 조건으로 들어갈 수 있는 데이터 타입은 byte, short, char, int, enum, String과 언급한 타입의 래퍼클래스 이기 때문이다.

 

그럼 enum은 언제 쓸까?

필요한 원소를 컴파일 타임에 다 알 수 있는 상수 집합이면 항상 열거타입을 사용하는 것이 좋다.

태양계 행성, 요일, 등 본질적인 열거타입은 당연히 포함 되며, 메뉴 아이템 같이 모두 컴파일 타임에 이미 알고 있을때도 사용할 수 있다.

 

4. Enum 선언하는 방법

Enum이란 엄연한 클래스의 한 종류이며, 선언하는 방법은 다음 과 같다.

위 코드를 다음과 같이 쓸 수 있다.

enum Fruits {APPLE, BANANA, ORANGE}
enum Companies { MS, APPLE, AMAZON}

사용법 : enum 이름 {상수1, 상수 2 ..}

 

5. 자바 Enum의 특징

- Enum을 통해서 정의된 상수들은 Enum type의 객체이다.

- Enum또한 엄연한 클래스이기 때문에 필드, 생성자, 메소드를 추가할 수 있다.

- 필드와 메소드를 가진다면, enum 상수 리스트가 메소드와 필드보다 먼저 와야하며, 이 경우 상수의 마지막에 세미콜론을 붙혀야 한다.

- 생성자를 이용해서 상수에 데이터 추가도 가능하다. 하지만 Enum의 생성자의 접근제어자는 private하게 자동으로 설정되기 때문에 외부에서 상수객체 추가하는것이 불가능하다.

- 열거형 멤버중 하나 호출시 열거된 모든 상수의 객체를 생성한다.

- 상수 하나당 각각의 인스턴스가 만들어지며 모두 public static final이다.

- 상수간의 비교가 가능한데, == 를 사용할 수 있고 (equals()재정의 안해주어도 비교가능), > < 같은 비교연산가는 사용할 수 없으며 Enum객체의 메소드를 사용하여야 한다 (뒤에서 다룸)

- 모든 Enum은 내부적으로 java.lang.Enum클래스를 상속받고 있기 때문에 interface는 implement받을 수 있지만 extend로 상속은 추가적으로 받지 못한다.

- new로 인해 직접적으로 인스턴스화 할 수 없음. 따라서 Fruits fruit = Fruits.BANANA와 같이 선언할 수 있다.

예시는 다음과 같다.

enum Season {
	SPRING(0), SUMMER(1), AUTUMN(2), WINTER(3);
	
	private int value;

	private Season(int value) {
		this.value = value;
	}

	public int getValue() {
		return value;
	}

	public void setValue(int value) {
		this.value = value;
	}
	@Override
	public String toString() {
		// TODO Auto-generated method stub
		return "overriding toString()" ;
	}
	
}
	
   ...
public class EnumTest {
  private static void printSeason(Season season) {
          switch (season) {
          case SPRING:
              System.out.println("It's spring.");
              break;
          case SUMMER:
              System.out.println("It's summer.");
              break;
          case AUTUMN:
              System.out.println("It's autumn.");
              break;
          case WINTER:
              System.out.println("It's winter");
              break;

          default:
              throw new IllegalArgumentException("You should fill one of the seasons. Try again.");
          }
	}   
    public static void main(String[] args) {
		printSeason(Season.WINTER);
        // It's winter 출력
        System.out.println(Season.AUTUMN); //위에서 재정의한 "overriding toString()" 출력
		System.out.println(Season.AUTUMN.name()); //AUTUMN 출력
		System.out.println(Enum.valueOf(Season.class, "AUTUMN")); //위에서 재정의한 "overriding toString()" 출력
		System.out.println(Season.AUTUMN.toString()); // 위에서 재정의한 "overriding toString()" 출력
		
        System.out.println(Season.SPRING.compareTo(Season.WINTER)); // -3 출력
		System.out.println(Season.SPRING == Season.SPRING); // true 출력

toString()메소드가 behind the scene에서 불려지는것을 주의하자.

필드 선언할때에는, 반드시 필드 값이 enum의 생성자안에 공급되어야 한다.

 

자바 enum클래스는 또한 추상 메소드 (abstract method)를 가지는 것이 가능하다.

만약 enum클래스가 추상 메소드를 가지고 있다면, 각 enum 클래스의 인스턴스는 implement해야한다. 예시는 다음과 같다.

public enum Level {
    HIGH{
        @Override
        public String asLowerCase() {
            return HIGH.toString().toLowerCase();
        }
    },
    MEDIUM{
        @Override
        public String asLowerCase() {
            return MEDIUM.toString().toLowerCase();
        }
    },
    LOW{
        @Override
        public String asLowerCase() {
            return LOW.toString().toLowerCase();
        }
    };

    public abstract String asLowerCase();
}

각 자바 enum 상수. 즉 인스턴스에 대해서 다른 implementation이 필요할때 추상메소드를 사용하면 유용하다.

 

 

6. Enum이 제공하는 메소드

(1) name()

출처 : oracle 아래 reference링크 참고.

java.lang.Enum 클래스를 모두 enum은 상속받고 있으므로, Enum이 제공하는 메소드 중 name()은 enum상수의 이름을 리턴한다. 하지만 오라클에서 대부분의 프로그래머들은 이 메소드 보다 toString() 메소드를 사용해야 한다고 한다. 왜냐하면 toString()메소드가 유저-친화적인 이름을 반환할수 있기 때문이라고 적혀있다. name()은 final로 정의되어 override가 불가능하지만, toString()으로 override가 가능하기 때문인것으로 보인다.

 

(2) ordinal()

출처 : oracle 아래 reference링크 참고

enumeration 상수의 인덱스 번호. 순서를 반환한다. 처음 정의되어 있는 상수를 0을 시작으로 int값을 반환한다. 하지만 대부분의 프로그래머들은 이 메소드를 사용할 일이 없으며 이에 의존해 코딩하는것은 enum에 정의된 상수의 순서가 바뀌는 순간 깨지기 때문에 사용하지 않는 것이 좋다. EnumSet과 EnumMap과 같은 enum기반의 자료구조를 디자인하는데 사용되는 메소드이다.

 

(3) compareTo(E o)

출처 : oracle 아래 reference링크 참고

compareTo()의 매개변수로는 Enum만 받을 수 있으며, 명시된 enum사이의 순서를 비교해 integer값인 양수, 음수, 0을 반환한다.  반드시 같은 종류의 enum 타입간의 enum 상수 비교만이 가능하다.

왜 enum에서는 비교연산자를 쓸수 없을까?

enum은 클래스이므로 클래스에서 비교연산자를 쓸수 없다. 객체와 객체는 서로 비교연산자를 쓸수 없기에 지원하지 않는 것이다.

 

(4) getDeclaringClass

출처 : oracle 아래 reference링크 참고

enum상수의 enum 타입에 상응하는 클래스 객체를 반환한다. 그래서 어느 enum에 속한 상수인지 알 수 있다.

 

(5) T valueOf(Class enumType, String name)

출처 : oracle 아래 reference링크 참고

System.out.println(Season.valueOf("SPRING"));
System.out.println(Enum.valueOf(Season.class, "SPRING"));

위 코드를 출력해보면 Season에서 toString()에 정의해준 값이 출력된다. 명시한 enum 타입의 enum 상수와 명시된 이름이 동일한 enum 상수값을 리턴하는 메소드인데, 명시된 이름과 동일한 enum 상수가 없으면 IllegalArgumentException을 발생시킨다.

 

(5) values()

java.lang.Enum 클래스에 있는 메소드는 아니지만, 모든 enum이 가지고 있는 메소드로 컴파일러가 자동으로 추가해 주는 메소드로서, Enum선언한 이름.values()를 하면 상수로 이루어진 배열을 반환한다.

for (Season e : Season.values()) {
			System.out.println(e.name());	
			System.out.println(e.ordinal());
            }

위의 코드를 메인메소드에서 출력하면 다음과 같은 결과값이 나온다.

Eclipse를 통한 시행 결과

7. EnumSet

EnumSet이란 enum 타입을 위해서 고안된 특별한 Set 인터페이스 구현체이다.

성능상 이점이 많기 때문에 enum 데이터를 위한 set이 필요한 경우 사용하는 것이 좋다.

상속구조는 다음과 같다.

출처 : https://www.geeksforgeeks.org/enumset-class-java/

Set<Season> set = new HashSet<> ();
set.add(Season.SPRING);
set.add(Season.SUMMER);
...

EnumSet<Season> enumSet = EnumSet.allOf(Season.class);

위와같은 코드에서, Enum을 set으로 다룰때, EnumSet을 이용하면 편리하다.

Season enum의 상수를 하나하나 추가하는 것 보다 한번에 위와같이 변경가능하다.

EnumSet의 특징은 다음과 같다.

 

  • AbstractSet 클래스를 상속하고 Set 인터페이스를 구현한다.
  • 오직 열거형 상수만 값으로 가질수 있으며 모든 값은 같은 enum 타입이여야 한다.
  • null 값을 추가하는것을 허용하지 않으며 NullPointerException을 던지는것도 허용하지 않는다.
  • ordinal 순서대로 요소가 적용된다.
  • thread-safe하지 않다.

EnumSet에서의 메소드는

1) allOf() : enum에 정의된 정보를 모두 추가 할 수 있다.

2) noneOf() : 아무 것도 추가 하지 않는다.

3) of() : 직접 요소를 넣을 수 있다.

 

EnumSet이 생성자를 호출 할 수 없게 만든 이유는 보통 static한 메소드를 써서 인스턴스를 만드는 경우 장점이 설계하는 입장에서 실제 내부 구현체를 숨길수 있으며 내부적으로 변경가능하기 때문이다.

new를 통해서 구현하는 것은 allOf()와 같은 static메소드 안에 숨어 있는 것이다..

 

EnumSet과 비슷하게 EnumMap이라는 자바 Map을 implement하여 enum 인스턴스를 키로서 사용할 수 있는것도 있다.

예시는 다음과 같다.

EnumMap<Level, String> enumMap = new EnumMap<Level, String>(Level.class);
enumMap.put(Level.HIGH  , "High level");
enumMap.put(Level.MEDIUM, "Medium level");
enumMap.put(Level.LOW   , "Low level");

String levelValue = enumMap.get(Level.HIGH);

 

reference : 

https://stackoverflow.com/questions/260626/what-is-type-safe

https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/lang/Enum.html

https://wisdom-and-record.tistory.com/52#recentComments

https://www.geeksforgeeks.org/enumset-class-java/

https://b-programmer.tistory.com/262

http://tutorials.jenkov.com/java/enums.html

https://velog.io/@ljs0429777/11%EC%A3%BC%EC%B0%A8-%EA%B3%BC%EC%A0%9C-Enum

 

 

최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday