[11주차] Enum

janjanee·2022년 8월 1일
0
post-thumbnail

2021.02.05 작성글 이전

11. Enum

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

11-0. enum 이란?

열거형(enums)은 서로 관련된 상수를 편리하게 선언하기 위한 것으로 여러 상수를 정의할 때 사용하면 유용하다.
JDK 1.5 부터 새로 추가되었다.

class Card {
    static final int CLOVER = 0;
    static final int HEART = 1;
    static final int DIAMOND = 2;
    static final int SPADE = 3;

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

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

    final Kind kind;
    final Value value;
}

첫 번째 블록의 코드를 보면 Card 클래스는 Card의 종류와 값들을 상수로 정의하였다.
두 번째 코드에서 enum을 이용하여 Kind, Value 열거형을 정의하고 kind, value의 타입이 int -> 열거 타입으로 바뀌었다.

자바의 열거형은 '타입에 안전한 열거형(typesafe enum)' 이라서 실제 값이 같아도 타입이 다르면
컴파일 에러가 발생한다.
말 그대로 값이 같아도 타입까지 체크하기 때문에 타입에 안전 하다는 뜻이다.

if (Card.CLOVER == Card.TWO)                // 결과는 true, 그런데 의미적으론 false 여야 함.
if (Card.Kind.CLOVER == Card.Value.TWO)     //  컴파일 에러. 같은 값이지만 타입이 다름.

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

11-1. enum 정의하는 방법

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

괄호{} 안에 상수의 이름을 나열하기만 하면 된다.

예를 들어 피자의 종류를 열거형으로 나타내보자.

enum Pizza { PEPPERONI, SUPREME, POTATO }

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

void buyPizza() {
    pizza = Pizza.POTATO;
}
  • 열거형 상수간 비교는 '=='을 사용할 수 있다.
  • '<', '>'와 같은 비교연산자는 사용할 수 없다.
  • compareTo()는 사용가능하다.
  • switch 문에서도 사용가능하다. (단, 열거형의 이름은 적지 않고 상수만 적는다.)

아래 코드는 enum을 정의하고 사용한 예제이다.

enum Coffee { AMERICANO, LATTE, MOCHA, CAPPUCCINO  }

public class EnumEx1 {

    public static void main(String[] args) {
        Coffee c1 = Coffee.AMERICANO;
        Coffee c2 = Coffee.valueOf("LATTE");
        Coffee c3 = Enum.valueOf(Coffee.class, "AMERICANO");

        System.out.println("c1 = " + c1);
        System.out.println("c1 = " + c2);
        System.out.println("c1 = " + c3);

        System.out.println("c1 == c2 ? " + (c1 == c2));
        System.out.println("c1 == c3 ? " + (c1 == c3));
        System.out.println("c1.equals(c3) ? " + (c1.equals(c3)));
        System.out.println("c1.compareTo(c3) ? " + (c1.compareTo(c3)));
        System.out.println("c1.compareTo(c2) ? " + (c1.compareTo(c2)));

        switch (c1) {
            case AMERICANO:
                System.out.println("Coffee is AMERICANO."); break;
            case LATTE:
                System.out.println("Coffee is LATTE."); break;
            default:
                System.out.println("None Coffee.");
        }

        Coffee[] cArr = Coffee.values();

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

    }

}
// 결과

c1 = AMERICANO
c1 = LATTE
c1 = AMERICANO
c1 == c2 ? false
c1 == c3 ? true
c1.equals(c3) ? true
c1.compareTo(c3) ? 0
c1.compareTo(c2) ? -1
Coffee is AMERICANO.
AMERICANO = 0
LATTE = 1
MOCHA = 2
CAPPUCCINO = 3

11-2. java.lang.Enum, enum이 제공하는 메소드

모든 Enum 클래스는 java.lang.Enum 클래스를 상속받는다.

public enum Pizza {
    PEPPERONI,
    SUPREME,
    POTATO,
}

Pizza 라는 Enum 클래스를 하나 생성했다. 컴파일된 바이트코드를 확인해보자.

public final enum com/jihan/javastudycode/week11/Pizza extends java/lang/Enum {

  // compiled from: Pizza.java

  // access flags 0x4019
  public final static enum Lcom/jihan/javastudycode/week11/Pizza; PEPPERONI

  // access flags 0x4019
  public final static enum Lcom/jihan/javastudycode/week11/Pizza; SUPREME

  // access flags 0x4019
  public final static enum Lcom/jihan/javastudycode/week11/Pizza; POTATO

  // access flags 0x9
  public static values()[Lcom/jihan/javastudycode/week11/Pizza;
   L0
    LINENUMBER 3 L0
    GETSTATIC com/jihan/javastudycode/week11/Pizza.$VALUES : [Lcom/jihan/javastudycode/week11/Pizza;
    INVOKEVIRTUAL [Lcom/jihan/javastudycode/week11/Pizza;.clone ()Ljava/lang/Object;
    CHECKCAST [Lcom/jihan/javastudycode/week11/Pizza;
    ARETURN
    MAXSTACK = 1
    MAXLOCALS = 0

  // access flags 0x9
  public static valueOf(Ljava/lang/String;)Lcom/jihan/javastudycode/week11/Pizza;
    // parameter mandated  name
   L0
    LINENUMBER 3 L0
    LDC Lcom/jihan/javastudycode/week11/Pizza;.class
    ALOAD 0
    INVOKESTATIC java/lang/Enum.valueOf (Ljava/lang/Class;Ljava/lang/String;)Ljava/lang/Enum;
    CHECKCAST com/jihan/javastudycode/week11/Pizza
    ARETURN
   L1
    LOCALVARIABLE name Ljava/lang/String; L0 L1 0
    MAXSTACK = 2
    MAXLOCALS = 1

    ...
  • extends java/lang/Enum 이라는 클래스를 상속받은것을 볼 수 있다.

    아래는 상속받은 Enum 클래스에 정의된 메소드 들이다.

    Class<E> getDeclaringClass()      // 열거형의 Class 객체를 반환
    String name()                     // 열거형 상수의 이름을 문자열로 반환한다.
    int ordinal()                     // 열거형 상수가 정의된 순서를 반환.
    T valueOf(Class<T> enumType, String name)     // 지정된 열거형에서 name과 일치하는 열거형 상수를 반환
  • 컴파일러가 자동적으로 추가해주는 메소드도 있다.

    static E values()                 // 열거형의 모든 상수를 배열에 담아 반환
    static E valueOf(String name)     // 열거형 상수의 이름으로 문자열 상수에 대한 참조를 얻음

다음은 위의 예제에서 언급되지 않은 메소드의 예제 코드이다.

name()

enum Animal
{
    TIGER, RABBIT, LION;
}

public class Test
{
    public static void main(String[] args)
    {
        Animal a1 = Animal.LION;
        System.out.print("Name of enum constant: ");

        System.out.println(a1.name());
    }
}

// 결과
Name of enum constant: LION

ordinal()

enum Animal
{
    TIGER, RABBIT, LION;
}

public class Test
{
    public static void main(String[] args)
    {
        Animal a1 = Animal.LION;
        System.out.print("ordinal of enum constant "+a1.name()+" : ");

        System.out.println(a1.ordinal());
    }
}

// 결과
ordinal of enum constant LION : 2

getDeclaringClass()

enum Animal
{
    TIGER, RABBIT, LION;
}

enum Day
{
    MONDAY, TUESDAY ;
}

public class Test
{
    // Driver method 
    public static void main(String[] args)
    {
        Animal a1 = Animal.valueOf("TIGER");
        Animal a2 = Animal.valueOf("RABBIT");
        Day d1 = Day.valueOf("MONDAY");
        Day d2 = Day.valueOf("TUESDAY");

        System.out.print("Class corresponding to "+ a1.name() +" : ");
        System.out.println(a1.getDeclaringClass());

        System.out.print("Class corresponding to "+ a2.name() +" : ");
        System.out.println(a2.getDeclaringClass());

        System.out.print("Class corresponding to "+ d1.name() +" : ");
        System.out.println(d1.getDeclaringClass());

        System.out.print("Class corresponding to "+ d2.name() +" : ");
        System.out.println(d2.getDeclaringClass());
    }
}

// 결과

Class corresponding to TIGER : class com.jihan.javastudycode.week11.Animal
Class corresponding to RABBIT : class com.jihan.javastudycode.week11.Animal
Class corresponding to MONDAY : class com.jihan.javastudycode.week11.Day
Class corresponding to TUESDAY : class com.jihan.javastudycode.week11.Day

11-3. enum에 멤버 추가하기

Enum 클래스의 ordinal() 이 열거형 상수가 정의한 순서를 반환하지만, 이 값을 상수의 값으로 사용하지 않는 것이 좋다.

열거형 상수의 값이 불연속적인 경우에 열거형 상수 이름 옆에 원하는 값을 괄호()에 적는다.

enum Coffee { AMERICANO(10), LATTE(20), MOCHA(30) }

이후 지정된 값을 저장할 수 있는 인스턴스 변수생성자 를 새로 추가해야한다.

  • 주의할 점
    • 열거형 상수를 모두 정의 후 다른 멤버를 추가.
    • 열거형 상수 마지막에 ';' 붙일 것
enum Coffee {
    AMERICANO(10),
    LATTE(20),
    MOCHA(30);

    private final int value;    // 정수를 저장할 필드(인스턴스 변수) 추가
    Coffee(int value) { this.value = value; }    // 생성자 추가

    public int getValue() { return value; }
}
  • 인스턴스 변수는 반드시 final일 필요는 없지만, value는 열거형 상수의 값을 저장하기 위한것이므로 final을 붙임
  • 외부에서 값을 얻기 위한 getValue()도 추가
Coffee c = new Coffee(40);   // 에러. 열거형의 생성자는 외부에서 호출불가
  • 열거형에 생성자가 추가되었지만, 객체를 생성할 수 없다.
  • 열거형의 생성자는 제어자가 묵시적으로 private 이기 때문이다.
enum Coffee {
    AMERICANO(10, "A"),
    LATTE(20, "L"),
    MOCHA(30, "M"),
    CAPPUCCINO(40, "C");

    private final int value;
    private final String symbol;

    Coffee(int value, String symbol) {
        this.value = value;
        this.symbol = symbol;
    }

    public int getValue() { return value; }
}
  • 열거형 상수에 여러 값을 지정할 수 도 있다
  • 그에 맞게 인스턴스 변수와 생성자 등을 새로 추가해줘야함

추상 메소드 추가하기

public enum Transportation {
    BUS(100),
    TRAIN(150),
    SHIP(100),
    AIRPLANE(300);

    private final int BASIC_FARE;

    Transportation(int basicFare) {
        BASIC_FARE = basicFare;
    }

    int fare() {
        return BASIC_FARE;
    }
}

운송수단 enum이 존재하고, 각 운송수단에는 기본요금(BASIC_FARE)이 책정되어있는 예제이다.

이 예제에 거리에 따라 요금을 계산하는 방식이 각 운송 수단마다 다른 상황을 만들고 싶다면
이럴 때, 열거형에 추상 메소드 'fare(int distance)'를 선언하면 된다.

각 열거형 상수가 추상 메소드를 구현해야한다.

public enum Transportation {
    BUS(100) {
        @Override
        int fare(int distance) {
            return distance * BASIC_FARE;
        }
    },
    TRAIN(150) {
        @Override
        int fare(int distance) {
            return distance * BASIC_FARE;
        }
    },
    SHIP(100) {
        @Override
        int fare(int distance) {
            return distance * BASIC_FARE;
        }
    },
    AIRPLANE(300) {
        @Override
        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;
    }

}

추상메소드를 적용하면 코드가 위와 같이 변경된다.
열거형에 추상 메소드를 선언할 일은 그리 많지 않으니 가볍게 참고하자.

11-4. Enum 이해

열거형이 내부적으로 어떻게 구현되었는지 더 자세한 이해를 해보자.

enum Coffee { AMERICANO, LATTE, MOCHA }

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

class Coffee {
    static final Coffee AMERICANO = new Coffee("AMERICANO");
    static final Coffee LATTE = new Coffee("LATTE");
    static final Coffee MOCHA = new Coffee("MOCHA");

    private String name;

    private Coffee(String name) {
        this.name = name;
    }
}

Coffee 클래스의 static 상수 AMERICANO, LATTE, MOCHA의 값은 객체의 주소이고,
이 값은 바뀌지 않는 값이므로 '==' 비교가 가능한 것이다.

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

    int ordinal;
    String name = "";

    public int ordinal() { return ordinal; }

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

    public int compareTo(T t) {
        return ordinal - t.ordinal();
    }
}

위의 코드는 모든 enum이 상속받는 Enum클래스를 흉내내어 만든 MyEnum 클래스이다.
만일 클래스를 MyEnum<T> 와 같이 선언했다면, compareTo()를 간단히 작성할 수 없었을 것이다.
타입 T에 ordinal()이 정의되어 있는지 확인할 수 없기 때문이다.

11-5. EnumSet

열거형 타입과 사용하기 위한 Set 구현체

  • AbstractSet 클래스를 상속받은 클래스이다. Set 인터페이스를 구현하였다.

  • 자바 컬렉션 프레임워크의 멤버이다.

  • HashSet 보다 훨씬 빠르다.

  • null Object를 허용하지 않는다.

  • 계층구조

    java.lang.Object
     ↳ java.util.AbstractCollection<E>
          ↳ java.util.AbstractSet<E>
               ↳ java.util.EnumSet<E>

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

바로 EnumSet을 만드는 예제를 살펴보자.

enum Food { PIZZA, COFFEE, CHICKEN, HAMBURGER };

public class EnumSetExample {

    public static void main(String[] args)
    {
        EnumSet<Food> set1, set2, set3, set4;

        set1 = EnumSet.of(Food.PIZZA, Food.COFFEE, Food.CHICKEN);
        set2 = EnumSet.complementOf(set1);
        set3 = EnumSet.allOf(Food.class);
        set4 = EnumSet.range(Food.COFFEE, Food.HAMBURGER);
        System.out.println("Set 1: " + set1);
        System.out.println("Set 2: " + set2);
        System.out.println("Set 3: " + set3);
        System.out.println("Set 4: " + set4);
    }
}
// 결과
Set 1: [PIZZA, COFFEE, CHICKEN]
Set 2: [HAMBURGER]
Set 3: [PIZZA, COFFEE, CHICKEN, HAMBURGER]
Set 4: [COFFEE, CHICKEN, HAMBURGER]

EnumSet은 추상클래스 이므로, new 연산자로 만들 수 없다.
위의 예제에서 볼 수 있듯이 다양한 static factory 메소드들을 통해 인스턴스를 만들 수 있다.

  • 요소 추가

    public class EnumSetExample {
    
      public static void main(String[] args)
      {
          EnumSet<Food> set1, set2;
    
          set1 = EnumSet.allOf(Food.class);
          set2 = EnumSet.noneOf(Food.class);
          System.out.println("Set 1: " + set1);
          System.out.println("Set 2: " + set2);
          System.out.println("============================================");
    
          set2.add(Food.PIZZA);
          System.out.println("EnumSet Using add(): " + set2);
    
          set2.addAll(set1);
          System.out.println("EnumSet Using addAll(): " + set2);
    
      }
    }
    // 결과
    Set 1: [PIZZA, COFFEE, CHICKEN, HAMBURGER]
    Set 2: []
    ============================================
    EnumSet Using add(): [PIZZA]
    EnumSet Using addAll(): [PIZZA, COFFEE, CHICKEN, HAMBURGER]

    add() 또는 addAll()을 이용하여 EnumSet 추가가 가능하다.

  • 요소 접근

    public class EnumSetExample {
    
      public static void main(String[] args)
      {
          EnumSet<Food> foods = EnumSet.allOf(Food.class);
    
          Iterator<Food> iterator = foods.iterator();
    
          while (iterator.hasNext()) {
              System.out.println(iterator.next());
          }
    
      }
    }
    // 결과
    PIZZA
    COFFEE
    CHICKEN
    HAMBURGER
  • 요소 삭제

    public class EnumSetExample {
    
        public static void main(String[] args)
        {
            EnumSet<Food> foods = EnumSet.allOf(Food.class);
    
            System.out.println("EnumSet: " + foods);
    
            boolean value1 = foods.remove(Food.CHICKEN);
            System.out.println("Is CHICKEN removed? " + value1);
    
            boolean value2 = foods.removeAll(foods);
            System.out.println("Are all elements removed? " + value2);
        }
    }
    // 결과
    
    EnumSet: [PIZZA, COFFEE, CHICKEN, HAMBURGER]
    Is CHICKEN removed? true
    Are all elements removed? true

번외

전략 열거 타입 패턴

급여명세서에 쓸 요일을 표현하는 열거 타입이 있다.
직원의 (시간당) 기본 임금과 그날 일한 시간(분단위)이 주어지면 일당을 계산해주는 메소드를 갖고있다.
주중 오버타임이 발생하면 잔업수당이 주어지고, 주말에는 무조건 잔업수당이 주어진다.

첫번째, switch문을 이용하여 case 문을 날짜별로 두어 계산을 쉽게 수행해보자.

public enum PayrollDay1 {
    MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY,
    SATURDAY, SUNDAY;

    private static final int MINS_PER_SHIFT = 8 * 60;

    int pay(int minutesWorked, int payRate) {
        int basePay = minutesWorked * payRate;

        int overtimePay;
        switch (this) {
            case SATURDAY:
            case SUNDAY:
                overtimePay = basePay / 2;
                break;
            default:
                overtimePay = minutesWorked <= MINS_PER_SHIFT ?
                        0 : (minutesWorked = MINS_PER_SHIFT) * payRate / 2;
        }

        return basePay + overtimePay;
    }
}

간결해보이지만 관리 관점에서 좋지않은 코드다. 휴가와 같은 새로운 값을 열거 타입에 추가하려면
그 값을 처리하는 case문을 쌍으로 추가해야한다.

위의 문제를 해결하기 위해서 다음과 같이 새로운 상수를 추가할 때 '전략' 을 선택하도록 만든다.

public enum PayrollDay {
    MONDAY(WEEKDAY), TUESDAY(WEEKDAY), WEDNESDAY(WEEKDAY), THURSDAY(WEEKDAY),
    FRIDAY(WEEKDAY), SATURDAY(WEEKEND), SUNDAY(WEEKEND);

    private final PayType payType;

    PayrollDay(PayType payType) {
        this.payType = payType;
    }

    int pay(int minutesWorked, int payRate) {
        return payType.pay(minutesWorked, payRate);
    }


    enum PayType {
        WEEKDAY{
            @Override
            int overtimePay(int minsWorked, int payRate) {
                return minsWorked <= MINS_PER_SHIFT ? 0 : (minsWorked - MINS_PER_SHIFT) * payRate / 2;
            }
        },
        WEEKEND{
            @Override
            int overtimePay(int minsWorked, int payRate) {
                return minsWorked * payRate / 2;
            }
        };

        abstract int overtimePay(int mins, int payRate);
        private static final int MINS_PER_SHIFT = 8 * 60;

        int pay (int minsWorked, int payRate) {
            int basePay = minsWorked * payRate;
            return basePay + overtimePay(minsWorked, payRate);
        }
    }
}

잔업수당 계산을 PayType(중첩 열거 타입)으로 옮기고, PayrollDay 열거 타입의 생성자에서 이 중 적당한 것을 선택하도록 바꿨다.
잔업수당 계산을 PayType에 위임하여, switch 문이나 상수별 메소드 구현이 필요 없게 되고, 이 패턴은 switch문 보다 복잡하지만 더 유연하고 안전하다.

References

profile
얍얍 개발 펀치

0개의 댓글