Enum - 규칙 34 확장 가능한 enum을 만들어야 한다면 인터페이스를 이용하라.

JAVA/Effective Java|2018. 5. 29. 23:12

연산자를 정의해둔 Enum을 사용할 때 더많은 유형의 자료형을 사용하기 위해서 
기존의 enum객체를 계승해서 작성하고 싶을 수 있다.

하지만 enum 자료형은 계승해서 사용하는 방법은 어렵다.

왜냐하면 모든 Enum 객체들은 함축적으로 Enum 객체를 상속받고 있다. 
자바에서는 클래스는 하나이상의 부모를 가질 수 없으므로, enum 객체는 추가 상속이 어렵다.

그렇기에 인터페이스를 먼저 구현하고 그에 맞게 enum을 구현할 수 있도록 인터페이스를 활용하는 방식으로 대처할 수 있다.



1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
package effective34;
 
/**
 * 연산자를 정의해둔 인터페이스
 * 
 * @author wedul
 *
 */
public interface Operation {
    /**
     * 연산자별 동작을 구현한 메소드
     * 
     * @param x
     * @param y
     * @return
     */
    double apply(double x, double y);
    
    /**
     * symbol
     * 
     * @return
     */
    String getSymbol();
}
 
package effective34;
 
public enum BasicOperation implements Operation {
    PLUS("+") {
        @Override
        public double apply(double x, double y) { return x + y; }
    },
    MINUS("-") {
        @Override
        public double apply(double x, double y) { return x - y; }
    },
    TIMES("*") {
        @Override
        public double apply(double x, double y) { return x * y; }
    },
    DIVIDE("/") {
        @Override
        public double apply(double x, double y) { return x / y; }
    };
    
    private final String symbol;
    
    BasicOperation(String symbol) {
        this.symbol = symbol;
    }
 
    @Override
    public String getSymbol() {
        return symbol;
    }
 
}
 
package effective34;
 
public enum ExtendedOperation implements Operation {
    EXP("^") {
        @Override
        public double apply(double x, double y) {
            return Math.pow(x,  y);
        }
    };
    
    private final String symbol;
    
    ExtendedOperation(String symbol) {
        this.symbol = symbol;
    }
    
    @Override
    public String getSymbol() {
        return this.symbol;
    }
}
cs



위와 같은 방식으로 구현하게 되면 다음과 같이 인터페이스만을 넘겨서 다양한 경우에서 사용할 수 있다.



1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public static void main(String args[]) {
    double x = 10;
    double y = 30;
    getOperationResult(BasicOperation.class, x, y);
    getOperation2(ExtendedOperation.EXP, x, y);
}
    
/**
 * 전달받은 Enum 객체에 구현된 apply를 모두 실행
 * 
 * @param op
 * @param x
 * @param y
 */
private static <extends Enum<T> & Operation> void getOperationResult(Class<T> op, double x, double y) {
    for (Operation operation : op.getEnumConstants()) {
        System.out.println(operation.getSymbol() + " result : " + operation.apply(x, y));
    }
}
    
private static <extends Operation> void getOperation2(T op, double x, double y) {
    System.out.println(op.getSymbol() + " result : " + op.apply(x, y));
}
cs




결론
enum클래스는 내부적으로 Enum<T>를 상속받고 있기 때문에 다른 클래스를 extend 할 수 없다.
그렇기 때문에 enum의 기능을 계승해서 사용하고 싶은경우에는 interface를 만들어서 구현해서 사용해야 한다.

출처 : 조슈아 블로크, 『 Effective Java 2/E』, 이병준 옮김, 인사이트(2014.9.1), 규칙34 인용.

댓글()

Enum - 규칙 33 ordinal을 배열 첨자로 사용하는 대신 EnumMap을 이용하라.

JAVA/Effective Java|2018. 5. 29. 23:11


Enum 상수별로 특정정보들을 
저장하고 싶을 때 EnumMap을 사용한다.



1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Class Fruit {
  enum Type { APPLE, BANANA, PEAR }
 
  final String name;
  final Type type;
 
  Fruit(String name, Type type) {
   this.name = name;
   this.type = type;
  }
}
 
Map<Fruit.Type, Set<Fruit>> fruitByType = new EnumMap<Fruit.Type, Set<Fruit>>(Herb.Type.class);
for (Fruit.Type t : Fruit.Type.values()) {
  fruitByType.put(t, new HashSet<Fruit>());
}
cs



EnumMap을 사용하면 깔끔하고 안전하며, ordinal을 이용해 구현한 프로그램과 성능면에서 더욱 좋다.

ordinal 값을 배열 첨자로 사용하는 것은 적절치 않기 때문에,
EnumMap을 사용하는 것이 좋다.


출처 : 조슈아 블로크, 『 Effective Java 2/E』, 이병준 옮김, 인사이트(2014.9.1), 규칙33 인용.

댓글()

Enum - 규칙 32 비트 필드(bit field) 대신 EnumSet을 사용하라.

JAVA/Effective Java|2018. 5. 29. 23:10
1
2
3
4
5
6
7
8
9
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;
 
  // 비트 연산하여 OR한 값.
  public void applyStyles(int styles) { }
}
cs



클래스에서 비트연산을 할 때
위와 같이 진행할 수 있다.

하지만 비트 필드는 int enum 패턴과 똑같은 단점을 가지고 있다.

또한

비트 필드를 출력한  결과는 int enum 상수를 출력한 결과보다도 이해하기 어렵다.
이 문제는 EnumSet을 사용하여 해결 할 수 있다.

[해결방법]
EnumSet 클래스를 사용하면 특정한 enum 자료형의 값으로 구성된 집합을 효율적으로
표현할 수 있다.

EnumSet은 Set 인터페이스를 구현하며, removeAll이나 retainAll 같은 일괄 연산도 비트 단위 산술 연산도 가능하다.




1
2
3
4
5
6
7
8
public class Text {
   public enum Style { BOLE, ITALIC, UNDERLINE }
 
   // 어떤 Set 객체도 인자로 전달할 수 있는 EnumSet 사용 가능
   public void applyStyles(Set<Style> styles) {}
}
 
text.applyStyles(EnumSet.of(Style.BOLD, Style.ITALIC));
cs




요약하자면 열거 자료형을 집합에 사용해야 한다면 비트필드를 이용하지말고,
EnumSet 클래스를 이용하는게 간결하다.



출처 : 조슈아 블로크, 『 Effective Java 2/E』, 이병준 옮김, 인사이트(2014.9.1), 규칙32 인용.

댓글()

Enum - 규칙 31 ordinal 대신 객체 필드를 사용하라.

JAVA/Effective Java|2018. 5. 29. 23:08

Enum 상수에는 
그 순서에 맞는 int 값이 반환된다.



1
2
3
4
5
6
7
8
9
10
11
public enum Fruit {
    APPLE, BANANA, PEAR;
}
 
public class Main {
    public static void main (String args[]) {
        System.out.println(Fruit.BANANA.ordinal());
    }
}
 
// 출력결과 1
cs




하지만 이렇게 
Enum의 oridnal 기능을 사용하는 것은 단점이 있다.

1. 상수 순서를 변경하게 되면 순서를 사용하던 곳에서 문제가발생할 수 있다.
2. 이미 사용한 정수값에 대응하는 새로운 enum 상수를 정의하는 것이 불가능 하다.
3. ordinal 간격이 1이 아닌 2나 3정도의 간격을 사용하고 싶을 때 어렵다,

그래서 이런 문제는 다음과 같이 해결이 가능하다.



1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public enum Fruit {
    APPLE(3), BANANA(5), PEAR(7);
    
    private int numberOfOrdinal;
    
    private Fruit(int numberOfOrdinal) {
        this.numberOfOrdinal = numberOfOrdinal;
    }
    
    public int getNumberOfOrdinal() {
        return this.numberOfOrdinal;
    }
}
 
public static void main (String args[]) {
    System.out.println(Fruit.BANANA.getNumberOfOrdinal());
}
cs




하지만 대부분의 Enum은 EnumSet이나, EnumMap처럼 일반적인 용도의 enum 기반 자료구조에서 사용할 목적으로 설계한 메서드 이기 때문에 특별한 경우가 아니고서는 ordinal 메서드를 사용하지 않는 것이 최선이다.


출처 : 조슈아 블로크, 『 Effective Java 2/E』, 이병준 옮김, 인사이트(2014.9.1), 규칙31 인용.



댓글()

Enum - 규칙 30 int 상수 대신 enum을 사용하라.

JAVA/Effective Java|2018. 5. 29. 23:06

기존의 프로젝트에서 
자주 사용 사용하는 설정 값이나 이름들을 열거 자료형으로서 다음같이 사용했다.



1
2
3
4
5
6
7
// int를 사용한 enum 패턴
public static final int FAIL = 1;
public static final int SUCCESS = 2;
 
// string을 사용한 enum 패턴
public static final String FEMAIL = "3";
public static final String MAIL = "4";
cs



int를 사용한 설정 값은 int enum 패턴, 문자열로 되어 있는 경우에는 string enum 패턴 이라고 한다.

하지만 이런 설정은 다음과 같은 단점이 있다.

1. 편의성이 떨어진다.
2. 이는 컴파일 시점 상수(compile-time constant)이기 때문에 상수를 사용하는 클라이언트와 함게 컴파일 되어야 하기 때문에 상수의 값이 변경되면 다시 컴파일 해야한다.
※ final이 붙은 변수는 컴파일 시점 상수라고 한다.

하지만 java 1.5버전에 enum 타입이 생기고 나서는 더 이상 이렇게 사용할 필요가 없다.


Enum type
Enum type의 열거 상수별로 하나의 객체를 public static final 필드 형태로 제공한다.

또한
Enum은 원래 변경 불가능하므로 모든 필드는 final로 선언되어야 한다. 

Enum 상수에 데이터를 넣으려면 객체 필드를 선언하고 생성자를 통해 받은 데이터를 그 필드에 저장하면 된다.



1
2
3
4
5
6
7
8
9
10
11
12
13
14
public enum SEX {
    FEMAIL("NO-ARMY"),
    MAIL("ARMY");
    
    private String variable;
    
    private SEX(String variable) {
        this.variable = variable;
    }
    
    public String getVariables() {
        return this.variable;
    }
}
cs




만약 Enum 자체가 특정 클래스안에서만 필요하다면 별도로 생성할 필요 없이 
해당 클래스 내부에 생성하여 사용하면 된다.



1
2
3
4
5
6
7
8
9
10
11
public class PNP {
    
    private enum team {
        WEB,
        RCP;
    }
    
    public static void main(String args[]) {
        System.out.println(team.WEB.toString());
    }
}
cs



만약 Enum 상수 각각에 사용되는 메소드를 다음과 같이 선언 할 수 있다.

하지만 이런 방식은 좋은 방식 아니다.

왜냐하면 
만약 현재 Enum상수가 다음 apply 메서드 switch 안에 정의가 안되있으면 throw가 발생해야 하고, 만약 Enum상수가 추가되면 switch도 추가해야하는 문제가 발생할 수 있다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public enum Operation {
    PLUS,
    MINUS,
    TIMES,
    DIVIDE;
 
    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("Unknown op: " + this);
    }
}
 
 
public class Main {
    public static void main(String args[]) {
        System.out.println(Operation.DIVIDE.apply(1030));
    }
}
cs




이런 문제를 각 Enum 상수 안에 
abstract 메소드를 구현하게 하는 각 상수별 클래스몸체(constant-specific class body)안에서 실제 메서드로 재정의할 수 있다.

이런 메서드는 상수별 메서드 구현이라고 부른다.



1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
public enum Operation {
    PLUS("+") {
        @Override
        public double apply(double x, double y) {
            return x + y;
        }
    },
    MINUS("-") {
        @Override
        public double apply(double x, double y) {
            return x - y;
        }
    },
    TIMES("*") {
        @Override
        public double apply(double x, double y) {
            return x * y;
        }
    },
    DIVIDE("/") {
        @Override
        public double apply(double x, double y) {
            return x / y;
        }
    };
    
    private String variable;
    
    private Operation(String variable) {
        this.variable = variable;
    }
 
    public String getVariable() {
        return variable;
    }
 
    public abstract double apply(double x, double y);
}
cs




하지만 이런 상수별 메서드 구현의 단점은 enum 상수끼리 공유하는 코드를 만들기가 어렵다.

그래서 만약 여러 상황에 의해서 동작하는 enum타입 자체의 메서드를 만들고 싶은 경우에는 중첩 Enum을 만들어서 사용하는 것이 좋다.



1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public enum Pay {
    SAWON(WorkTime.WEEKDAYS, 10),
    DARI(WorkTime.HOLIDAY, 20);
    
    private WorkTime workDay;
    private int workTime;
    
    private Pay(WorkTime workDay, int workTime) {
        this.workDay = workDay;
        this.workTime = workTime;
    }
    
    double getPay() {
        return this.workDay.getPay(workTime);
    }
    
    private enum WorkTime {
        WEEKDAYS {
            @Override
            public double getPay(int workTime) {
                return workTime * 100;
            }
        },
        HOLIDAY {
            @Override
            public double getPay(int workTime) {
                return workTime * 100;
            }
        };
        
        public abstract double getPay(int workTime);
    }
}
cs




만약 이런 방식이 switch를 사용하는 것보다 더 효율적이다.

switch는 외부 enum 자료형 상수별로 달리 동작하는 코드를 만들어야 할 때는 enum 상수에 switch 문을 적용할 때 좋다.


예를 들어 Operation enum이 다른 누군가가 작성한 자료형이고,
그 각 상수가 나타내는 연산의 반대 연산을 반환하는 메서드를 만들어야 할 때 다음과 같이 하면 용이하다.


1
2
3
4
5
6
7
8
9
pulbic static Operation inverse(Operation op) {
  switch(op) {
      case PLUS: return Operation.MINUS;
      case MINUS: return Operation.PLUS;
      case TIME: return Operation.DIVIDE;
      case DIVIDE: return Operation.TIME;
      defaultthrow new AssertionError("Unknown op: " + op);
   }
}
cs




[결론]
일반적으로 enumb은 int, string 상수와 성능이 비슷하다.
하지만 enum을 사용한 코드는 가독성도 높고, 안전하며 강력하다.

출처 : 조슈아 블로크, 『 Effective Java 2/E』, 이병준 옮김, 인사이트(2014.9.1), 규칙30 인용.

댓글()

제네릭 - 규칙 29 형 안전 다형성 컨테이너를 쓰면 어떨지 따져보라

JAVA/Effective Java|2018. 5. 29. 23:02


제네릭은 Set이나 Map과 같이 하나 자료형을 가진 원소들을 담는 컨테이너에 가장 많이 사용된다.
ex) Map<String, String>, Set<Integer>

그렇기 때문에 형인자는 컨테이너별로 고정되게 되어있다.

그러나 가끔 여러개의 자료형을 Map과 콜렉션에 컨테이너로서 사용하고 싶을 경우가 있을 것이다.

이는 다음과 같은 접근법을 사용하면 가능하다.



1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import java.util.HashMap;
import java.util.Map;
 
public class Rule29   {
    public Map<Class<?>, Object> map = new HashMap<>();
 
    public <T> void putData(Class<T> type, T instance) {
        map.put(type, instance);
    }
    
    public Object getData(Class<?> type) {
        return type.cast(map.get(type));
    }
    
}
 
 
public class Main {
    public static void main(String agrs[]) {
        Rule29 rule = new Rule29();
        rule.putData(String.class"test");
        rule.putData(Integer.class222);
        rule.putData(Student.classnew Student("cjung"));
        
        System.out.println(rule.getData(String.class));
        System.out.println(rule.getData(Integer.class));
        System.out.println(((Student) rule.getData(Student.class)).getStudent());
        
    }
}
cs




위와 같은 방법을 사용하면 키를 자료형을 사용하고
값을 그 자료형에 맞는 데이터를 넣을 수 있다.

이렇게 모든 키의 자료형이 서로 다른 클래스를 
형 안전 다형성 컨테이너(typesafe heterogeneous container)라고 한다.

그리고

위와 같이 Rule29 객체에서 setData, getData에서 사용된 Class 객체는 제네릭 클래스로서
컴파일 시간 자료형이나 실행시간 자료형 정보를 메소드들에 전달할 목적으로 class 리터럴을 이용하는 경우를 자료형 토큰(type token)이라 한다.


이런 형 안전 다형성 컨테이너를 사용하면 다음과 같은 장단점이 있다.

[형 안전 다형성 컨테이너의 장점]
1. 이런 방식을 사용할 경우 내가 요청한 자료형만을 반환하게 되므로  형 안정성이 보장된다.
ex) String.class를 키를 사용하는 데이터는 무조건 String 데이터만 반환한다.

2. Rule29에서 사용한 public Map<Class<?>, Object> map = new HashMap<>()을 살펴보자.
자료형으로 와일드카드 자료형을 사용하였기 때문에 상이한 형인자 자료형을 가질 수 있다.
ex) Class<String>, Class<Integer>

3. getData메소드에서 Class<?>의 cast 메소드를 사용함으로써 동적 형변환을 사용한다.
=> 키로사용된 Class의 자료형과 일치하면 Casting되서 정확한 데이터가 반환될 것이고 그렇지 않은 경우 ClassCastException이 발생할 것이다.
=> 이를 이용하여 무점검 형변환하는 코드가 없는 형 안전성을 확보 할 수 있다.


[형 안전 다형성 컨테이너의 단점]
1. 악의적으로 Rule29 객체의 형안전성을 어길 수 있다.
=> Class 객체를 무인자 형태로 사용할 경우 형 안전성이 훼손된다.

2. 악의적으로 rule.putData(String.class, 123)과 같이 자료형과 다른 데이터를 삽입할 수 있다.
-> put 하는 순간에 type을 확인하여 맞을 때만 삽입하도록 한다.
-> map.put(type, type.cast(instance));
-> java.util.Collections에는 정적 팩토리 매서드들이 존재한다. (checkedSet, checkedList, checkedMap)
     => 이런 종류의 정적 팩토리 메소드들은 형안정성을 보장한다.
     => 이것을 사용해서 형 안정성을 확보할 수 있다.


1
2
3
4
5
6
7
8
9
10
    public static void main(String... args) {
        List<String> list = new ArrayList<>();
        list = Collections.checkedList(list, String.class);
        list.add("one");
        System.out.println(list);
 
        List list2 = list;
        list2.add(2); // ClassCastException을 발생시킨다. (기존의 List의 경우 raw type으로 사용하였을 경우 용인된다.)
        System.out.println(list2);
    }
cs



3. 실체화 불가능 자료형에는 쓰일 수 없다.
ex) List<String>, List<Integer>은 다음과 같은 Class 객체를 얻을 수 없다. (List<Integer>.class)
=> List.class를 사용하여 해결할 수는 있다.

[기타]
특정 자료형 토큰을 사용하도록 제한하고 싶은경우 한정적 형인자나 한정적 와일드카드를 사용할 수 있다.

ex) public <T extends Annotation> T getAnnotation(Class<T> annotationType);


출처 : 조슈아 블로크, 『 Effective Java 2/E』, 이병준 옮김, 인사이트(2014.9.1), 규칙29 인용.

댓글()