ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Enum
    BackEnd/자바 2021. 1. 31. 09:38

    Enum열거형(enumerated type)이라고 부른다. 열거형은 서로 연관된 상수들의 집합이라고 할 수 있다.

    Enum 은 class 선언 자리에 enum 을 대신 넣어 아래와 같이 만들 수 있다.

    public enum Fruits {
    	
    	APPLE,PEACH,BANANA;
    	
    	private Fruits(){
    		System.out.println("Fruits constructor");
    	}
    	
    }
    

    Enum 생성자는 private 으로만 선언할 수 있으며, 변수를 선언한 만큼 생성자가 돌아가 아래와 같은 결과가 나온다.

    public class Test {
    	public static void main(String[] args) {
    		Fruits type = Fruits.APPLE;
    	}
    } 
    --------------->
    //Fruits constructor
    //Fruits constructor
    //Fruits constructor

    만약 BANANA 없이 APPLE, PEACH 만 선언하면 console 창에 Fruits constructor 가 두 번만 찍히는데 그 이유는 다음 코드가 아래와 동일하기 때문이다.

    class Fruit{
        public static final Fruit APPLE  = new Fruit();
        public static final Fruit PEACH  = new Fruit();
        public static final Fruit BANANA = new Fruit();
        private Fruit(){}
    }

    상수 하나 당 인스턴스 하나를 만들어  public static final 필드로 공개한다.

    클라이언트가 직접 인스턴스를 생성하거나 확장할 수 없으니 열거 타입 선언으로 만들어진 인스턴스들은 딱 하나씩만 존재함이 보장된다. 싱글톤은 원소가 하나뿐인 열거타입이라 할 수 있고, 거꾸로 열거타입은 싱글톤을 일반화한 형태라고 볼 수 있다.

     

    Fruit 이라는 클래스 타입의 변수 APPLE , PEACH, BANANA 를 만든다.

    그래서 생성 후 APPLE 을 스트링 비교를 하면 false 가 나온다.

    ordinal 은 변수의 순서로 APPLE 은 0이, BANANA 는 2가 나온다.

    하지만 ordinal 은 쓸 일이 거의 없고 이 메서드를 함부로 이용하면, 상황에 따라 코드가 지저분해지고 실용성이 떨어진다. 이 메서드는 EnumSet 과 EnumMap 같이 열거 타입 기반의 범용 자료구조에 쓸 목적으로 설계되었다.

    public class Test {
    	public static void main(String[] args) {		
            Fruits type = Fruits.APPLE;
    		Fruits type2 = Fruits.BANANA;
    		System.out.println(type.equals("APPLE")); //false
    		System.out.println(type2.ordinal()); //2
    	}
    } 

    열거타입 상수 각각을 특정 데이터와 연결지으려면 생성자의 매개변수를 통해

    데이터를 받아 인스턴스 필드에 저장하면 된다.

    public enum Fruits {
    	
    	APPLE("red"),PEACH("peach"),BANANA("yellow");
    	
    	public String color;
    	
    	private Fruits(String color){
    		this.color = color;
    		System.out.println("Fruits constructor - " + this + ", color = " + this.color);
    	}
    	
    	String getColor() {
    		return this.color;
    	}
    }
    
    public class Test {
    	public static void main(String[] args) {
    		Fruits type = Fruits.APPLE;
    		System.out.println(type.color); //red 
    		System.out.println(type.getColor()); //red
          }
    }

     enum 안에 모든 내용을 배열로 가져오는 .values() 와 이름으로 상수를 찾는 valueOf(String arg) 

    public class Test {
    	public static void main(String[] args) {
    				
            for(Fruits f : Fruits.values() ) {
    			System.out.println(f);       //APPLE, PEACH , BANANA
    		}
    		
    		Fruits type2 = Fruits.valueOf("PEACH");
    		System.out.println(type2);              //PEACH
    		System.out.println(type2.getColor());   //pink
        }    
    }		

     

    ※ 왜 이런 enum 타입을 써야하는 걸까? (이펙티브 자바 6장)

    public static final int APPLE_FUJI = 0;
    public static final int APPLE_PIPPIN = 1;
    public static final int APPLE_GRANNY = 2;
    
    public static final int ORANGE_NAVEL = 0;
    public static final int ORANGE_TEMPLE = 1;
    public static final int ORANGE_BLOOD = 2;
    
    

    이런 정수 열거 패턴 기법에는 1. 타입 안전을 보장할 방법이 없으며, 2. 표현력이 좋지 않다.

    오렌지를 건네야 하는 메서드에 사과를 보내고 동등(==) 연산자로 비교해도 컴파일러는 아무 경고 메세지를 보내지 않는다.

    또 3. 정수 열거 패턴을 사용한 프로그램은 깨지기 쉽다.

    평범한 상수를 나열한 것 뿐이라 컴파일하면 그 값이 클라이언트에 그대로 새겨지고 값이 바뀌면 클라이언트도 반드시 다시 컴파일 해야한다. 

    정수 상수는 문자열로 출력하기 다소 까다롭고, 단지 숫자로만 보여 의미를 알기도 어려우며, 상수가 몇 개인지 알기도 어렵다.

    문자열 열거 패턴은 의미를 알기 쉽지만, 문자열 값을 하드코딩해야하고, 오타가 있어도 컴파일러는 확인할 길이 없다.

    문자열 비교에 따른 성능저하도 따라온다.

     

    public enum Apple {FUJI, PIPIN, GRANNY}
    public enum Orange {NAVEL, TEMPLE, BLOOD}

    열거 타입은 컴파일타임 타입 안정성을 보장한다.

    Apple 열거타입을 매개변수로 받는 메서드를 선언하면, 건네받은 참조는 Apple 의 세가지 값 중 하나가 분명하다.

    타입이 다른 열거 타입 변수에 할당하려하거나 다른 열거 타입끼리 == 연산자로 비교하려는 꼴이기 때문이다.

     

    열거타입에는 각자의 이름공간이 있어 이름이 같은 상수도 평화롭게 공존하며, 열거타입에 새로운 상수를 추가하거나 순서를 바꿔도 다시 컴파일하지 않아도 된다.

     

    열거타입에는 어떤 메서드도 추가 할 수 있다.

    가장 단순하게는 상수 모음이지만, 실제로는 클래스이므로 고차원의 추상 개념 하나를 완벽히 표현해 낼 수도 있다. 

     

    위에 APPLE,PEACH,BANANA 에 color 는 각각의 상수에 다른 데이터를 연결하는 예시였다.

    더 나아가 상수마다 동작이 달라져야 하는 상황도 있을 것이다.

    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 메소드 내 case 문도 함께 추가해야한다.

    만약 깜박한다면 런타임에 new AssertionError 가 발생할 것이다. 

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

    상수 선언 바로 옆에 각각 메서드를 선언하면 잊기 어렵고, abstract apply 메소드로도 선언했기 때문에 만약 어떤 상수에서 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);
    	
    	private String symbol;
    	
    	private Operation(String symbol) {
    		this.symbol = symbol;
    	}
    	
    	@Override
    	public String toString() {
    		return symbol;
    	}
    }    

    위와 같이 데이터를 연결할 수도 있다.

    toString 을 symbol 을 return 해 아래와 같이 쉽게 출력할 수도 있다.  (op 가 symbol 이 출력됨)

    public class Test1 {
    
    	public static void main(String[] args) {
    		Operation plus = Operation.PLUS;
    		double result = plus.apply(30, 40);
    		System.out.println(result); //70
    	
    		double x = 10.5;
    		double y = 20.5;
    		for(Operation op : Operation.values()) {
    			System.out.printf("%f %s %f = %f%n ",x,op,y,op.apply(x, y));
                //10.500000 + 20.500000 = 31.000000			
    		}
    	}
    }
    

     

    반대로 String 을 받아 enum 으로 바꿀 수도 있다. 

    이렇게 String symbol 을 받아 enum 안에 있는 상수를 Optional 로 감싸 돌려준다. 없더라도 NPE 이 나지 않도록 Optional 로 감싸준 것이다.

    	private static final Map<String,Operation> stringToEnum = Stream.of(values()).collect(Collectors.toMap(Object::toString,e->e));
    	//{*=*, +=+, -=-, /=/}
    	//key : * = value : *
    	public static Optional<Operation> fromString(String symbol){
    		return Optional.ofNullable(stringToEnum.get(symbol));
    	}
    	

     

    자바 8 이전에는 빈 해시맵을 만든 다음 values 가 반환한 배열을 순회하며 {문자열, 열거타입상수} 쌍을 맵에 추가했을 것이다. 물론 지금도 이렇게 해도 된다.

    하지만 열거 타입 상수는 생성자에서 자신의 인스턴스를 맵에 추가할 수 없다. 이렇게 하려면 컴파일 오류가 나는데 ,

    만약 이를 허용했다면 런타임에 NPE 가 발생했을 것이다.  

    열거 타입의 정적 필드 중 열거 타입의 생성자에서 접근할 수 있는 것은 상수 변수뿐이다.

    (아이템24: 멤버클래스는 되도록 static 으로 만들어라.)

    열거 타입 생성자가 실행되는 시점에는 정적 필드들이 아직 초기화 되기 전이라, 자기 자신을 추가하지 못하게 하는 제약이 꼭 필요하다. 이 제약의 특수한 예로 , 열거타입 생성자에서 다른 타입의 다른 상수에도 접근 할 수 없다.

     

    1. 빈 해시맵에 {문자열, 열거타입상수} 추가하기

     

        public Map<String,Operation> map = new HashMap<String,Operation>();
    	
        private Operation(String symbol) {
    		this.symbol = symbol;
    		map.put(symbol, this);
    	}
    	
        public  Map<String,Operation> getMap(){
    		return map;
    	}
        Operation plus = Operation.PLUS;
        System.out.println(plus.getMap());

    2. 생성자에서 자신의 인스턴스를 맵에 추가할 수 없다

    열거 타입의 정적 필드 중 열거 타입의 생성자에서 접근할 수 있는 것은 상수 변수뿐이다.

     

    만약  map 이 static 이라면 생성자에서 접근할 수 없다.

     

    열거 타입 생성자가 실행되는 시점에는 정적 필드들이 아직 초기화 되기 전이라,

    자기 자신을 추가하지 못하게 하는 제약이 꼭 필요하다.

    (열거타입이 상수들을 먼저 생성하고난 다음 정적 필드를 초기화하기 때문에.)

     

    컴파일 에러

    	public static Map<String,Operation> map = new HashMap<String,Operation>();
    	
    	private Operation(String symbol) {
    		this.symbol = symbol;
    		map.put(symbol, this); //Cannot refer to the static enum field  Operation.map within an initializer
    	}

     

    NPE 발생 

    	public static Map<String,Operation> map = new HashMap<String,Operation>();
    	
    	private Operation(String symbol) {
    		this.symbol = symbol;
    		putString(symbol,this);
    	}
    	
    	public void putString(String symbol, Operation operation) {
    		map.put(symbol,operation);
    	}
        
    	Operation plus = Operation.PLUS;

     

    상수별 메서드 구현에는 열거 타입 상수끼리 코드를 공유하기 어렵다는 단점이 있다.

    예) 급여명세서 요일 열거 타입

    직원의 시간당 기본 임금,

    분 단위 일당 계산 메서드,

    주중 오버타임 잔업 수당,

    주말 잔업 수당;

     

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

    이 코드는 휴가와 같은 새로운 값을 열거 타입에 추가하려면 그 값을 처리하는 case 문을 넣어야한다.  

    상수별 메서드 구현으로 급여를 정확히 계산하기 위해서는 

    1. 잔업 수당을 계산하는 코드를 모든 상수에 중복해서 넣거나,

    2. 계산 코드를 평일용과 주말용으로 나눠 각 상수에서 필요한 메서드를 호출한다.

     

    하지만 두 방식 모두 가독성이 떨어지고 오류가 발생할 가능성이 높아진다.

    가장 깔끔한 방법은 새로운 상수를 추가할 때 잔업수당 '전략'을 선택하도록 하는 것이다.

    잔업수당 계산을 private 중첩 열거 타입으로 옮기고 PayrollDay 열거 타입 생성자에서 이 중 적절한 것을 선택한다.

    그러면 PayrollDay 열거 타입은 잔업수당 계산을 전략 열거 타입에 위임하고, switch 문이나 상수별 메서드 구현이 필요없어진다. 이 패턴은 switch 보다 복잡하지만 더 안전하고 유연하다.

     

    public enum PayrollDay {
    	
        MONDAY(PayType.WEEKDAY), TUESDAY(PayType.WEEKDAY), WEDNESDAY(PayType.WEEKDAY), 
        THURSDAY(PayType.WEEKDAY), FRIDAY(PayType.WEEKDAY), 
        SATURDAY(PayType.WEEKEND), SUNDAY(PayType.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 {
    			int overtimePay(int minsWorked, int payRate) {
    				return minsWorked <= MINS_PER_SHIFT ? 0 : (minsWorked - MINS_PER_SHIFT) * payRate / 2;
    			}
    		},
    		WEEKEND {
    			int overtimePay(int minsWorked, int payRate) {
    				return minsWorked * payRate;
    			}
    		};
    		
    		abstract int overtimePay(int minsWorked,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);
    			
    		}
    	}
    }

     

    대부분의 경우 열거 타입의 성능은 정수 상수와 별반 다르지 않다.

    열거 타입을 메모리에 올리는 공간과 초기화하는 시간이 들긴 하지만 체감될 정도는 아니다.

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


    ordinal 인덱싱 대신 EnumMap 을 사용하라

    public class Plant {
    	//정원에 심은 식물들을 배열로 관리,
    	//생애주기(한해살이, 여러해살이, 두해살이) 별로 묶기 
    	enum LifeCycle{ANNUAL,PERENNIAL,BIENNIAL}
    	
    	final String name;
    	final LifeCycle lifeCycle;
    	
    	Plant(String name,LifeCycle lifeCycle){
    		this.name = name;
    		this.lifeCycle = lifeCycle;
    	}
    	
    	@Override
    	public String toString() {
    		return name;
    	}
    }
    
    public class Test1 {
    
    	public static void main(String[] args) {
    		Plant[] garden = {
    	            new Plant("바질", LifeCycle.ANNUAL),
    	            new Plant("캐러웨이", LifeCycle.BIENNIAL),
    	            new Plant("딜", LifeCycle.ANNUAL),
    	            new Plant("라벤더", LifeCycle.PERENNIAL),
    	            new Plant("파슬리", LifeCycle.BIENNIAL),
    	            new Plant("로즈마리", LifeCycle.PERENNIAL)
    	        };
    		Set<Plant>[] plantsLife = new Set[Plant.LifeCycle.values().length];
    		
    		for(int i=0;i<plantsLife.length;i++) {
    			plantsLife[i] =  new HashSet<>();
    		}
    		
    		for(Plant p : garden) {
    			plantsLife[p.lifeCycle.ordinal()].add(p);
    		}
    		
    		for(int i=0;i<plantsLife.length;i++) {
    			System.out.printf("%s : %s%n",Plant.LifeCycle.values()[i] , plantsLife[i]);
    		}
            //ANNUAL : [딜, 바질]
            //PERENNIAL : [라벤더, 로즈마리]
            //BIENNIAL : [파슬리, 캐러웨이]
    
    	}
    }
    

    위에 ordinal 메서드를 이용한 방식은 

    배열은 제네릭과 호환되지 않으니 비검사 형변환을 수행해야하고, 깔끔히 컴파일 되지 않을 것이다.

    배열은 각 인덱스의 의미를 모르니 출력결과에 직접 레이블을 달아야 한다.

    정확한 정수값을 사용한다는 것을 보장해야한다. 

     

    아래와 같이 EnumMap 으로 위 문제를 해결할 수 있다.

    public class Test1 {
    
    	public static void main(String[] args) {
    		Plant[] garden = {
            		 new Plant("바질", LifeCycle.ANNUAL),
               		 new Plant("캐러웨이", LifeCycle.BIENNIAL),
               		 new Plant("딜", LifeCycle.ANNUAL),
               		 new Plant("라벤더", LifeCycle.PERENNIAL),
               		 new Plant("파슬리", LifeCycle.BIENNIAL),
               		 new Plant("로즈마리", LifeCycle.PERENNIAL)
           		 };
    		
    		Map<Plant.LifeCycle, Set<Plant>> map = new EnumMap<>(Plant.LifeCycle.class);
    		
    		for(Plant.LifeCycle lc : Plant.LifeCycle.values()) {
    			map.put(lc, new HashSet<>()); //  key : ANNUAL, values : new * 3
    		}
    		for(Plant p : garden) {
    			map.get(p.lifeCycle).add(p);
    		}
    		System.out.println(map); 
            //{ANNUAL=[딜, 바질], PERENNIAL=[로즈마리, 라벤더], BIENNIAL=[캐러웨이, 파슬리]}
    	}
    }

    비트 필드 대신 EnumSet 을 사용하라 

    public class Text {
    	public static final int STYLE_BOLD = 1<<0;
    	public static final int STYLE_ITALIC = 1<<1;
    	public static final int STYLE_UNDERLINE = 1<<2;
    	public static final int STYLE_STRIKETHROUGH = 1<<3;
    	
    	public void applyStyles(int styles) {
    		...
    	}
    }
    
    	text.applyStyles(STYLE_BOLD | STYLE_ITALIC);

    비트 필드를 사용하면 비트별 연산을 통해 합집합과 교집합같은 집합 연산을 효율적으로 수행 할 수 있다.

    하지만 비트 필드는 정수 열거 상수의 단점을 그대로 지니며, 훤씬 해석하기 어렵다.

    비트 필드 하나에 녹아있는 모든 원소를 순회하기도 까다롭다. 마지막으로, 최재 몇 비트가 필요한지 API 작성 시 미리 예측해 적절한 타입(int나 long) 을 선택해야한다.

     

    	public enum Style {BOLD,ITALIC,UNDERLINE,STRIKETHROUGH; }
    	
    	public void applyStyles(Set<Style> styles) {
    		
    	}
    	text.applyStyles(EnumSet.of(Style.BOLD,Style.ITALIC));

    EnumSet 의 단점은 불변으로 만들 수 없다는 것이다.

    불변으로 만들기 위해서는

    명확성과 성능이 조금 떨어지지만 Collections.unmodifiableSet 으로 EnumSet 을 감싸 사용할 수 있다.

    반응형

    'BackEnd > 자바' 카테고리의 다른 글

    tdd  (0) 2021.05.11
    상속보다는 컴포지션을 사용하라 > 이펙티브 자바 아이템18  (0) 2021.04.26
    예외처리  (0) 2021.01.16
    인터페이스  (0) 2021.01.09
    자바 기초 (패키지, import , classpath, 접근제어자)  (0) 2021.01.01
Designed by Tistory.