본문 바로가기
Language/Effective java

열거 타입과 애너테이션

by y.j 2022. 7. 13.
728x90

int 상수 대신 열거 타입을 사용하라

열거 타입은 일정 개수의 상수 값을 정의한 다음, 그 외의 값은 허용하지 않는 타입이다. 

 

자바 열거타입의 특징

  • 자바 열거타입은 다른 언어와 다르게 class이다.
  • 상수 하나 당 자신의 인스턴스를 하나씩 만들어 public static final 필드로 제공한다.
  • 클라이언트가 인스턴스를 직접 생성하거나 확장 할 수 없다.
  • 열거 타입은 컴파일타임 타입 안전성을 제공한다.
  • 공개되는 것은 필드 이름 뿐이라 순서를 바꿔도 다시 컴파일 하지 않아도 된다.
  • 열거 타입의 toString 메서드는 출력하기에 적합한 문자열을 내어준다.
  • 임의의 메서드 / 필드 / 인터페이스를 추가 할 수도 있다.

 


태양계의 열거타입

public enum Planet {
    MERCURY( 3.302e+23, 2.439e6 ),
    VENUS  ( 4.869e+24, 6.052e6 ),
    EARTH  ( 5.975e+24, 6.078e6 ),
    MARS   ( 6.419e+23, 3.393e6 ),
    JUPITER( 1.899e+27, 7.149e7 ),
    SATURN ( 5.685e+26, 6.027e7 ),
    URANUS ( 8.683e+25, 2.556e7 ),
    NEPTUNE( 1.024e+26, 2.477e7 );

    private final double mass;
    private final double radius;
    private final double surfaceGravity;

    private static final double G = 6.67300E-11;

    Planet(double mass, double radius) {
        this.mass = mass;
        this.radius = radius;
        surfaceGravity = G * mass / (radius * radius);
    }

    public double mass() { return mass; }
    public double radius() { return radius; }
    public double surfaceGravity() { return surfaceGravity; }

    public double surfaceGravity(double mass) {
        return mass * surfaceGravity;
    }
}

열거 타입 상수 각각을 특정 데이터와 연결지으려면 생성자에서 데이터를 받아 인스턴스 필드에 저장하며 된다. 필드를 public으로 선언해도 되지만( final이기 때문에 ), public 메서드를 생성하는 것이 낫다. 


열거 타입을 선언한 클래스 혹은 그 패키지에서만 유용한 기능은 private이나 package-private 메서드로 구현해야 한다. 널리 쓰이는 열거 타입은 톱 레벨 클래스로 만들고 특정 톱레벨 클래스에서만 쓰인다면 멤버클래스로 만들어 쓰자.

 

 

여러가지 기능을 하는 열거 타입

아래 사칙연산을 하는 Operation 열거 타입이다. case문으로 작성했지만 이쁘지 않다. 추가적인 operation을 추가하려면 case문을 추가해야 하고 깜빡한다면 런타임에 에러를 일으킨다.

public enum Operation {
    PLUS, MINUS, TIMES, DIVIDE;
    
    public double apply(double x, double y) {
        switch (this) {
            case PLUS: return x + y;
            case MINUS: return x - y;
            case TIMES: return x * y;
            case DIVIDE: return x / y;
        }
        throw new AssertionError("알 수 없는 연산 : " + this);
    }
}

 

위 코드를 아래처럼 바꾼다면 apply를 재정의한다는 것을 깜빡하기 어렵고 하지 않을 경우 컴파일 에러를 낸다.

public enum Operation {
    PLUS {public double apply(double x, double y) {return x + y;}},
    MINUS {public double apply(double x, double y) {return x - y;}},
    TIMES {public double apply(double x, double y) {return x * y;}},
    DIVIDE {public double apply(double x, double y) {return x / y;}};

    public abstract double apply(double x, double y);
}

 

상수별 클래스 몸체와 데이터를 사용한 열거 타입

toString을 재정의 하기 위해서는 fromString을 재정의해서 operation이 존재하는지 안하는지 검사 할 수 있어야 한다.

public enum Operation {
    PLUS("+") {
        public double apply(double x, double y) {return x + y;}
    },
    MINUS("-") {
        public double apply(double x, double y) {return x - y;}
    },
    TIMES("*") {
        public double apply(double x, double y) {return x * y;}
    },
    DIVIDE("/") {
        public double apply(double x, double y) {return x / y;}
    };

    private final String symbol;

    Operation(String symbol) {
        this.symbol = symbol;
    }

    public abstract double apply(double x, double y);

    @Override
    public String toString() {
        return symbol;
    }
    private static final Map<String, Operation> stringToEnum =
            Stream.of(values()).collect(
                    toMap(Object::toString, e->e));

    public static Optional<Operation> fromString(String symbol) {
        return Optional.ofNullable(stringToEnum.get(symbol));
    }
}

 

급여 계산 예제

분명 간결하지만 관리 관점에서 위험한 코드다. 휴가와 같은 새로운 값을 추가하려면 case문을 잊지말고 쌍으로 넣어줘야 한다.

public enum PayrollDay {
    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;
    }
}

 

수정 후 

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

    private final PayType payType;

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

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

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

        WEEKEND {
            @Override
            int overtimePay(int mins, int payRate) {
                return mins * 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);
        }
    }

}

 

열거 타입은 언제 쓸까?

필요한 원소를 컴파일 타임에 다 알수 있는 상수 집합

 - 태양계 행성, 한 주의 요일, 체스 말

 

열거 타입에 정의된 상수개수가 영원히 고정 불변일 필요는 없다.

728x90

'Language > Effective java' 카테고리의 다른 글

열거 타입과 애너테이션  (0) 2022.07.20
열거 타입과 애너테이션  (0) 2022.07.18
제네릭  (0) 2022.06.26
제네릭  (0) 2022.06.19
제네릭  (0) 2022.06.18

댓글