이펙티브 자바 아이템34

한주영·2024년 2월 1일
0

이펙티브자바

목록 보기
26/33

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

열거타입

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

정수열거타입 예시

public static final int APPLE_FUJI=0;
public static final int APPLE_PIPPIN=1;
public static final int APPLE_GRANNY_SMITH=2;

정수열거타입에는 단점이 존재
•타입안전을 보장할수없으며 표현력이 좋지않음
•정수 열거 패턴을 사용한 프로그램은 깨지기 쉬움
정수 상수는 문자열로 출력하기가 다소 까다로움

열거패턴의 단점을 깔끔히 씻어주는 동시에 여러장점을 안겨주는
대안이 바로 열거타입이다.

가장 단순한 열거타입

public enum Apple{FUJI,PIPPIN, GRNNY_SMITH}
public enum Orange{NAVEL,TEMPLE, BLOOD}

•열거타입 자체는 클래스이며,
•상수 하나당 자신의 인스턴스를 하나씩 만들어 public static final필드로 공개
•타입 안전성을 제공
•각자의 이름에 이름공간이있어서 이름이 같은 상수도 평화롭게 공존
toString 메서드는 출력하기에 적합한 문자열을 내어줌

거대한 열거타입을 설명하기 좋은예- 태양계의 여덟개 행성

public enum Planet {
    
    MERCURY(3.302+23,2.439e6),
    VENUS(4.869e+24, 6.052e6),
    EARTH(5.975e+24, 6.378e6),
    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 surfaceWeight(double mass){
        return mass * surfaceGravity; //F=ma
    }
}
// 어떤 객체의 지구에서의 무게를 입력받아 여덟 행성에서의 무게를 출력
public class WeightTable {
   public static void main(String[] args) {
      double earthWeight = Double.parseDouble(args[0]);
      double mass = earthWeight / Planet.EARTH.surfaceGravity();
      for (Planet p : Planet.values())
         System.out.printf("%s에서의 무게는 %f이다.%n",
                           p, p.surfaceWeight(mass));
   }
}

•열거타입 상수 각각을 특정 데이터와 연결지으려면 생성자에서 데이터를 받아 인스턴스 필드에 저장하면 된다.
•열거타입은 근본적으로 불변-> 모든 필드는 final이여야함.
•열거타입은 자신안에 정의된 상수들의 값을 배열에 담아 반환하는 정적 메서드인 value를 제공
•필드를 public으로 선언해도되지만 private으로 두고 별도의 public접근자 메서드를 두는게 나음

열거타입을 사용하는 상황

1)값에따라 분기하는 열거타입

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

 

동작은 하지만 그리 예쁜코드는 아니다.

2)상수별 메서드 구현을 활용한 열거타입
-> 각 상수에서 자신에맞게 재정의하는 방법
상수별 메서드 구현

public enum Opreation {

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

apply메서드가 상수 선언 바로옆에 붙여있어서 apply도 재정의한다는 사실을 감빡하기는 어려울것이다.
apply가 추상 메서드이므로 재정의하지않았다면 컴파일 오류로 알려줌

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

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

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

다음은 이 toString이 계산식으로 출력을 얼마나 편하게 해주는지 보여줌

public static void main(String[] args) {
        double x = Double.parseDouble(args[0]);
        double y = Double.parseDouble(args[1]);
        for (Operation op : Operation.values())
            System.out.printf("%f %s %f = %f%n",
                    x, op, y, op.apply(x, y));
    }

4)열거타입용 fromString 메서드 구현

열거타입에는 상수 이름을 입력받아 그 이름에 해당하는 상수를반환해주는 valueOf메서드가 자동 생성
toString메서드를 재정의할시-> 반환하는 문자열을 해당 열거타입상수로 변환해주는 메서드

private static final Map<String, Operation> stringToEnum =
            Stream.of(values()).collect(
                    toMap(Object::toString, e -> e));

    // 지정한 문자열에 해당하는 Operation을 (존재한다면) 반환한다.
    public static Optional<Operation> fromString(String symbol) {
        return Optional.ofNullable(stringToEnum.get(symbol));
    }

이렇게 상수별 메서드 구현을 상수별 데이터와 결합할수 있다.

5)값에따라 분기하여 코드를 공유하는 열거타입 - 좋은방법일까?

enum PayrolLDay {
    MONDAY, TUESDAY, WEDSENDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY:

    private static final int MINS_PER_SHIFT = 8 * 60;
    
    int pay(int minutesWorked, int payRate) {
        int basePay = minuitesWorked * payRate;
        
        int overtimePay;
        switch(this) {
            case SATURDAY: case SUNDAY:
                overtimePay = basePay /2;
                break;
            default:
                overtimePay = minuutesWorked <= MINS_PER_SHIFT ? 0 : (minutesWorked - MINS_PER_SHIFT) * payRate /  2;
        }
        return basePay + overtimePay;
    }
}

관리관점에서 위험하다
->새로운 값을 열거타입에 추가하면 case문을 잊지않고 넣어줘야한다.

상수별 메서드로 급여를 구현하는 방법 두가지
1)잔업수당을 계산하는 코드를 모든 상수에 중복해서 넣는다.
2)계산 코드를 평일용과 주말용으로 나눠 각각을 도우미 메서드로 작성한 다음 각 상수가 자신에게 필요한 메서드를 적절히 호출하면 된다.

->두 방식 모두 코드가 장황해져서 가독성이 크게 떨어지고 오류발생 가능성이 높아진다.

가장깔끔한 방법
새로운 상수를 추가할때 잔업수당을 '전략'을 선택하도록 하는것
잔업수당 계산을 private 중첩 열거타입으로 옮기고
PayrtrollDay 열거타입의 생성자에서 이 중 적당한 것을 선택하는 것이다.

6) 전략 열거타입 패턴

switch문은 열거타입의 상수별 동작을 구현하는데 적합하지않으므로
기존 열거타입의 상수별 동작을 넣을때는
switch문이 좋은 선택이 될수있다.

그래서 열거타입을 언제 쓰는게 좋은가?

필요한 원소를 컴파일타임에 다 알수있는 상수집합이라면 항상 열거타입을 사용하자.
•열거타입에 정의된 상수 개수가 영원히 고정 불편일 필요하는 없다

profile
백엔드개발자가 되고싶은 코린이:)

0개의 댓글