11회차. Enum

KIMA·2023년 2월 12일
0
post-thumbnail

목표

자바의 열거형에 대해 학습한다.

학습할 것

열거형(Enum)이란?

열겨형은 서로 관련된 상수를 편리하게 선언하기 위한 것이다.

  • 열거형을 사용하기 전

    class Card {
      // kind
      static final int CLOVER = 0;
      static final int HEART = 1;
      static final int DIAMOND = 2;
      static final int SPADE = 3;
    
      // value
      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 Kind kind;
      final Value value;
    }

장점

  • 첫째, 자바의 열거형은 C언어의 열거형보다 더 향상되어, 열거형이 갖는 값뿐만 아니라 타입도 관리하기 때문에 논리적인 오류를 줄일 수 있다. 즉, 자바의 열거형은 typesafe하다.
    • C언어의 열거형은 타입이 달라도 값이 같으면 조건식 결과가 true이다.
      if(Card.CLOVER == Card.TWO) // true
    • 자바의 열거형은 값이 같아도 타입이 다르면 컴파일 에러가 발생한다.
      컴파일 에러 발생 코드
  • 둘째, 상수의 값이 바뀌어도 해당 상수를 참조하는 모든 소스를 다시 컴파일하지 않아도 된다.

enum 정의와 사용

enum 정의

enum 열거형이름 {상수명1, 상수명2, ...}
  • 각 열거형 상수의 값은 정의된 순서이다.
    • 0부터 시작한다.
  • 열거형 상수의 값이 불연속적인 경우
    enum Direction {
      EAST(1, ">"), SOUTH(5, "V"), WEST(-1, "<"), NORTH(10, "^");
      
      // 열거형 상수 값을 저장할 인스턴스 변수
      private final int value; 
      private final String symbol;
      
      // 열거형 상수 값을 저장할 생성자
      Direction(int value, String symbol) { 
        this.value = value; 
        this.symbol = symbol;
      } 
      
      public int getValue() { return value; }
      public String getSymbol() { return symbol; }
    }
    • 첫째, 열거형 상수의 이름 옆에 원하는 값을 괄호()와 함께 적어준다.
    • 둘째, 지정된 값을 저장할 수 있는 인스턴스 변수와 생성자를 추가해준다.
    • 주의할 점
      • 열거형 상수를 모두 정의한 다음에 다른 멤버를 추가해야한다.
      • 열거형 인스턴스 변수는 상수를 저장하므로 final을 붙이는 것이 좋다.
      • 열거형의 생성자는 묵시적으로 private이므로, 열거형의 객체를 생성할 수 없다.

enum 사용

열거형이름.상수명;
열거형상수명.메서드명();

열거형의 내부 구현 방식

열거형 상수 하나하나가 해당 열거형의 객체이다.

enum Direction {EAST, SOUTH, WEST, NORTH};

위는 다음과 같다.

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;
  }
}

enum에 추상메서드 추가하기

열거형에 추상메서드를 선언하면 각 열거형 상수가 이 추상 메서드를 반드시 구현해야 한다.

  • 추상메서드에서 열거형의 인스턴스 변수를 사용할 경우, 해당 변수를 protected로 선언해야 접근이 가능하다.
  • 사용할 일이 많지 않으므로, 가볍게 참고만 한다.
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 final int BASIC_FARE;
  
  Transportation(int basicFare) {
    BASIC_FARE = basicFare;
  }
  
  public int getBasicFare() { return BASIC_FARE; }
}
  • Transportation의 내부 구현 방식은 다음과 같다.

    abstract static class Transportation {
          static final Transportation BUS = new Transportation(100) {
              int fare(int distance) {
                  return distance * basicFare;
              }
          };
          static final Transportation TRAIN = new Transportation(150) {
              int fare(int distance) {
                  return distance * basicFare;
              }
          };
          static final Transportation SHIP = new Transportation(100) {
              int fare(int distance) {
                  return distance * basicFare;
              }
          };
          static final Transportation AIRPLANE = new Transportation(300) {
              int fare(int distance) {
                  return distance * basicFare;
              }
          };
    
          protected final int basicFare;
    
          private Transportation(int basicFare) {
              this.basicFare = basicFare;
          }
    
          public int getBasicFare() {
              return basicFare;
          }
    
          abstract int fare(int distance); // 거리에 따른 요금을 계산하는 추상 메서드
      }

enum간의 비교

  • 동등비교 : equals()가 아닌 ==로 비교가 가능하다.

    • 열거형 상수는 실제로 객체의 주소값을 저장하기 때문이다.
    • 따라서, 그만큼 빠른 성능을 제공한다.
  • 대소비교는 <, >를 사용할 수 없고, compareTo()를 사용할 수 있다.

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

    • 가독성을 위해 case문에 열거형의 이름은 적지 않고 상수의 이름만 적어야 한다는 제약이 있다.
    enum Direction{EAST, WEST, SOUTH, NORTH}
    
    void move(Direction direction) {
      switch(direction) {
        case EAST: // Direction.EAST로 작성할 수 없다.
          break;
        case WEST:
          break;
        case SOUTH:
          break;
        case NORTH:
          break;
      }
    }

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

모든 열거형은 추상 클래스 Enum의 자손이다.

abstract class Enum<E extends Enum<E>> implements Comparable<E> {
  static int id = 0; // 열거형 상수의 선언 순서
  
  private int ordinal;
  String name = "";
  
  public int ordinal() { return ordinal; }
  
  Enum(String name) {
    this.name = name;
    ordinal = id++;
  }
  
  public int compareTo(E e) {
    return ordinal - t.ordinal();
  }
}
  • 객체가 생성될 때마다 즉, 열거형 상수가 선언될 때마다 번호를 붙여서 인스턴스 변수 ordinal에 저장한다.
  • compareTo의 인자로 들어오는 클래스가 ordinal()를 가지고 있어야 하므로 즉, 해당 클래스가 Enum을 상속받아야 하므로 Enum<E extends Enum<E>>와 같이 선언하였다.

Enum 클래스에 정의된 메소드

모든 열거형이 해당 메소드들을 가지고 있으며, 컴파일러가 자동으로 추가해 준다.

name()

열거형 상수의 이름을 문자열로 반환한다.

ordinal()

열거형 상수가 정의된 순서(0부터 시작)를 정수로 반환한다.

values()

열거형의 모든 상수를 배열에 담아 반환한다.

Direction[] directionArr = Direction.values();

for(Direction d : directionArr) {
  System.out.printf("%s = %d%n", d.name(), d.ordinal());
}

getDeclaringClass()

열거형의 Class 객체를 반환한다.

valueOf(Class<T> enumType, String name)

지정된 열거형에서 name과 일치하는 열거형 상수를 반환한다.

System.out.println(Direction.WEST == Direction.valueOf("WEST")); // true

EnumSet

Enum 타입을 저장하기 위한 Set 구현체

  • 고려할 사항
    • 선언시 명시한 열거형 타입의 상수만 포함할 수 있다.
    • iterator는 열거형 상수가 선언된 순서로 요소를 순회한다.
    • null을 추가할 수 없다.
      • null을 삽입하면, NullPointException 이 발생한다.
    • 쓰레드에 안전하지 않으므로, 필요한 경우 외부에서 동기화한다.

생성

// Direction의 모든 요소(열거형 상수들)를 포함하는 EnumSet
EnumSet<Direction> set = EnumSet.allOf(Direction.class);

// 아무 요소도 포함하지 않은 EnumSet
EnumSet<Direction> set = EnumSet.noneOf(Direction.class);

// 들어갈 요소를 선택할 수 있는 EnumSet
EnumSet<Direction> set = EnumSet.of(Direction.WEST, Direction.SOUTH);

// 모든 요소에서 원하는 요소를 제거한 EnumSet
EnumSet<Direction> set = EnumSet.complementOf(EnumSet.of(Direction.WEST, Direction.SOUTH));
  • new 연산자로 생성할 수 없고, 정적 팩토리 메소드로만 생성할 수 있다.
    • 정적 팩토리 메소드는 Enum 원소 수에 따라 RegularEnumSet, JumboEnumSet 2가지의 EnumSet 구현체를 제공한다.
      EnumSet의 정적 팩토리 메소드

💡 new가 아닌 정적 팩토리 메소드 방식을 차용한 이유

  • 첫째, 개발자는 EnumSet의 구현 객체중 어떤 객체가 가장 적합한지 고를 필요가 없다.
  • 둘째, 간단한 EnumSet 초기화 과정
    • 모든 열거형 상수들을 EnumSet에 담는 일이 빈번한데, 이 과정을 일일이 개발자가 수행하면 번거롭다.
  • 셋째, EnumSet의 확장성
    • RegularEnumSet, JumboEnumSet외의 다른 EnumSet 구현 객체가 추가/삭제되어도 기존에 EnumSet을 사용하던 코드는 변경점이 없다.

CRUD

  • 추가 - add()

    EnumSet<Direction> set = EnumSet.of(Direction.WEST, Direction.SOUTH);
    set.add(Direction.NORTH);
  • 삭제 - remove()

    EnumSet<Direction> set = EnumSet.of(Direction.WEST, Direction.SOUTH);
    set.remove(Direction.WEST);
  • 조회 - contains()

    EnumSet<Direction> set = EnumSet.of(Direction.WEST, Direction.SOUTH);
    
    if(set.contains(Direction.WEST)) {
      System.out.println("WEST를 포함하고 있습니다.");
    }

복사

EnumSet.copyOf(EnumSet.of(Direction.WEST, Direction.SOUTH));

🤔 Set 구현체들이 많이 있는데, 왜 EnumSet을 만든걸까?

  • EnumSet은 내부적으로 비트 벡터를 사용하여 메모리 사용을 크게 줄일 수 있다.
  • EnumSet의 모든 메서드들은 비트 연산을 사용하여 구현되므로 일반적인 연산이 매우 빠르게 계산된다.

Reference

profile
안녕하세요.

0개의 댓글