Dev Book Review/Effective Java

[Effective Java] item 34. int 상수 대신 열거 타입을 사용하라

열거타입 (enum) : 일정 개수의 상수 값을 정의한 다음 그외의 값은 허용하지 않는 타입 정수 열거 패턴 (int enum pattern) : 이전까지 사용하던 패턴 1. 정수 열거 패턴 (int enum pattern)의 단점 public static final int APPLE_FUJI = 0; public static final int APPLE_PIPPIN = 1; public static final int APPLE_GRANNY_SMITH = 2; public static final int ORANGE_NAVEL = 0; public static final int ORANGE_TEMPLE = 1; public static final int ORANGE_BLOOD = 2; 타입 안전을 보장할 ..

[Effective Java] item 34. int 상수 대신 열거 타입을 사용하라

728x90

열거타입 (enum) : 일정 개수의 상수 값을 정의한 다음 그외의 값은 허용하지 않는 타입

정수 열거 패턴 (int enum pattern) : 이전까지 사용하던 패턴

 

1. 정수 열거 패턴 (int enum pattern)의 단점

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

public static final int ORANGE_NAVEL = 0;
public static final int ORANGE_TEMPLE = 1;
public static final int ORANGE_BLOOD = 2;
  • 타입 안전을 보장할 방법이 없으며 표현력이 좋지 않다.
    Apple에서 Orange를 사용해도 컴파일러의 경고 메세지가 없다.
  • 접두어를 사용한 이름 충돌을 방지하는 방법을 사용한다.
  • 평범한 상수 나열이라, 컴파일 하면 그 값이 그대로 새겨지기 때문에 프로그램이 깨지기 쉽다.
  • 정수 열거 그룹에 속한 모든 상수를 한 바퀴 순회하는 방법도 마땅치 않으며 상수가 몇개인지도 알 수 없다.

 

2. 문자열 열거 패턴(string enum pattern)

정수 열거 패턴보다 더 나쁘다

private final String APPLE = "1";
private final String GRAPE = "2";
private final String ORANGE = "3";
  • 문자열에 오타가 있어도 컴파일러에서 확인할 길이 없어 런타임 버그
  • 문자열 비교에 따른 성능저하

 

3. 열거 타입 (enum)

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

완전한 형태의 클래스 (정수값 뿐인)라서 다른 언어의 열거 타입보다 훨씬 강력하다.

 

a. 열거 타입의 아이디어

  • 열거 타입은 클래스
  • 상수 하나당 자신의 인스턴스를 하나씩 만들어서 public static final 필드로 공개한다.
  • 열거 타입은 final이다 : 밖에서 접근가능한 생성자를 제공하지 않는다.
  • 열거 타입 선언으로 만들어진 인스턴스는 딱 1개만 존재한다.
 

b. 열거 타입과 싱글턴

  • 열거 타입은 인스턴스 통제클래스이다 : 언제 어느 인스턴스를 살아 있게 할지를 통제할 수 있음 (정적 팩터리 방식 클래스)
  • 싱글턴 = 원소가 하나뿐인 열거타입
  • 열거타입 = 싱글턴을 일반화한 형태
 

c. 열거타입의 장점

  • 컴파일 타입 안전성 제공 : Apple 열거타입 인수에 Orange를 넘길 수 없음
  • 이름 같은 상수 공존 : 각자의 이름공간이 있기 때문! 공개 되는 것이 필드의 이름이라 상수 값이 클라이언트에 컴파일 되어 각인되지 않기 때문이다.
  • 임의의 메서드나 필드를 추가할 수 있고 임의의 인터페이스를 구현하게 할 수 있다.
  • 상수를 하나 제거했을 때 : 제거한 상수를 참조하지 않는 클라이언트에 아무 영향이 없다.
    참조를 한 클라이언트에서는 컴파일(런타임-다시 컴파일 X일 때) 오류가 발생할 것! (정수 열거 패턴에서는 기대할 수 없는 대응)

 

4. 데이터와 메서드를 갖는 열거 타입

각 상수와 연관된 데이터를 해당 상수 자체에 내재시킨다.

고차원의 추상 개념 하나를 표현 할 수 있다.

public enum Planet {
    MERCURY(3.302e+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; // 표면중력(단위: m / s^2)

    // 중력상수(단위: m^3 / kg s^2)
    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
    }
}

열거 타입 상수 각각을 특정 데이터와 연결지을 때 생성자에서 데이터를 받아 인스턴스 필드에 저장한다.

  • 열거 타입은 근본적으로 불변이라 모든 필드는 final이야 한다.
  • 필드를 private으로 두고 별도의 public 접근자 메서드를 두자
 

a. 열거타입의 배열 values()

자신 안에 정의된 상수들의 값을 배열에 담아 반환하는 정적 메서드 값들은 선언된 순서로 저장된다.

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

b. 열거타입을 올바르게 사용하기

  • 일반 클래스와 마찬가지로 기능을 클라이언트에게 노출해야할 합당한 이유가 없다면 private으로, 혹은 (필요하다면) package-private으로 선언하라
  • 널리 쓰이는 열거타입 = 톱레벨 클래스로 구현
  • 특정 톱레벨 클래스에서만 사용 = 해당 클래스의 멤버 클래스로 구현

 

5. 상수별 메서드 구현(constant-specific method implementation)

switch를 이용한 구현은 새로운 상수를 추가할 때마다 해당 case문도 추가해야해서 깨지기 쉽다.

상수별 메서드 구현

열거 타입에 추상 메서드를 선언하고, 각 상수별 클래스 몸체(constant-specific class body)를 각 상수에 맞게 재정의하는 방법

import java.util.*;
import java.util.stream.Stream;
import static java.util.stream.Collectors.toMap;

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

열거 타입의 valueOf(string)

상수 이름을 입력받아 이 이름에 해당하는 상수를 반환해 주는 메서드

fromString 메서드 제공

열거타입의 toString을 재정의 할 때 함께 제공하는 걸 고려해보자.

toString이 반환하는 문자열을 해당 열거 타입 상수로 변환해주는 메서드

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

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

열거타입 정적 필드의 생성 시점

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

Operation 상수가 stringToEnum 맵에 추가되는 시점 : 열거타입 생성 후 정적 필드가 초기화 될 때

열거 타입 상수는 생성자에서 자신의 인스턴스를 맵에 추가할 수 없다 : 컴파일 오류

  • 열거 타입의 정적 필드 중 열거 타입 생성자에서 접근 할 수 잇는 것은 상수 변수 뿐이다.
  • 열거 타입 생성자 실행 시점에는 정적 필드 초기화 전이다.
  • 열거 타입 생성자에서 같은 열거 타입의 다른 상수에도 접근 할 수 없다.
    (열거 타입의 인스턴스를 public static final으로 선언함. 다른 형제 상수도 static이므로 열거 타입 생성자에서 정적 필드에 접근할 수 없다는 제약이 적용된다.)

 

6. 상수별 동작 혼합 : 전략 열거 타입 패턴

열거 타입 상수 일부가 같은 동작을 공유한다면 전략 열거 타입 패턴을 사용하자.

package effectivejava.chapter6.item34;

import static effectivejava.chapter6.item34.PayrollDay.PayType.*;

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

    public static void main(String[] args) {
        for (PayrollDay day : values())
            System.out.printf("%-10s%d%n", day, day.pay(8 * 60, 1));
    }
}

추가하려는 메서드가 의미상 열거타입에 속하는 경우 다음과 같이 전략 열거 타입 패턴을 사용한다.

그렇지 않은 경우에는 switch를 적용해서 간단하게 만든다.

 

7. 열거타입을 사용해야 할 때

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

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

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

    Ex) 메뉴 아이템, 연산 코드, 명령줄 플래그

  • 열거타입의 성능은 상수와 별반 다르지 않다

 

댓글

Comments

Dev Book Review/Effective Java

[Effective Java] Chapter 5: 제네릭

용어정리 한글 영문 예 매개변수화 타입 parameterized type List 실제 타입 매개변수 actual type parameter String 제네릭 타입 generic type List 정규 타입 매개변수 formal type parameter E 비한정적 와일드카드 타입 unbounded wildcard type List 로 타입 raw type List 한정적 타입 매개변수 bounded type parameter 재귀적 타입 한정 recursive type bound 한정적 와일드카드 타입 Bounded wildcard type 로타입 : 제네릭 타입 시스템에 속하지 않는다. Set Set, Set는. 안전하지만, 로타입인 Set은 안전하지 않다. Link : jyami.tistory...

[Effective Java] Chapter 5: 제네릭

728x90

용어정리

한글 영문
매개변수화 타입 parameterized type List<String>
실제 타입 매개변수 actual type parameter String
제네릭 타입 generic type List<E>
정규 타입 매개변수 formal type parameter E
비한정적 와일드카드 타입 unbounded wildcard type List<?>
로 타입 raw type List
한정적 타입 매개변수 bounded type parameter <E extends Number>
재귀적 타입 한정 recursive type bound <T extends Comparable<T>>
한정적 와일드카드 타입 Bounded wildcard type <? extends Number>
제네릭 메서드 generic method static <E> List<E> asList(E[] a)
타입 토큰 type token String.class

 

item26. 로 타입은 사용하지 말라

  • 로타입을 사용하면 런타임에 예외가 일어날 수 있으니 사용하면 안 된다.
  • 로 타입은 제네릭이 도입되기 이전 코드와의 호환성을 위해 제공될 뿐이다.
  • 매개변수화 타입 : 어떤 타입의 객체도 저장할 수 있다.Set<Object>
  • 와일드카드 타입 : 모종의 타입 객체만 저장할 수 있다. Set<?>
  • 로타입 : 제네릭 타입 시스템에 속하지 않는다. Set
  • Set<Object>, Set<?>는. 안전하지만, 로타입인 Set은 안전하지 않다.
  • Link : jyami.tistory.com/90
 

[Effective Java] item26. 로타입은 사용하지 말라

1. 용어정리 public class Example{ private T member; } 제네릭 클래스[인터페이스] : 클래스[인터페이스] 선언에 타입 매개변수(type parameter)가 쓰인다. Example.class 제네릭 타입(Generic Type) : 제네..

jyami.tistory.com

 

item27. 비검사 경고를 제거하라

  • 비검사 경고는 중요하지 무시하지 말자.
  • 모든 비검사 경고는 런타임에 ClassCastException을 일을킬 수 있는 잠재적 가능성을 뜻하니 최선을 다해 제거하라
  • 경고를 없앨 방법을 찾지 못하겠다면 그 코드가 타입 안전함을 증명하고 가능한 한 범위를 좁혀 @SuppressWarning("unchecked") 애너테이션으로 경고를 숨겨라
  • 그런다음 경고를 숨기기로 한 근거를 주석으로 남겨라
  • Link : jyami.tistory.com/91
 

[Effective Java] item 27. 비검사 경고를 제거하라

1. 할수있는 한 모든 비검사 경고를 제거하자 제네릭을 사용하기 시작했을 때 볼 수 있는 수많은 컴파일러 경고 비검사 형변환 경고 비검사 메서드 호출 경고 비검사 매개변수화 가변인수 타입 �

jyami.tistory.com

 

item28. 배열보다는 리스트를 사용하라

  • 배열과 제네릭에는 매우 다른 타입 규칙이 적용된다.
  • 배열은 공변이고 실체화되는 반면, 제네릭은 불공변이고 타입정보가 소거된다.
  • 그 결과 배열은 런타임에는 타입 안전하지만 컴파일타임에는 그렇지 않다.
  • 제네릭은 반대로, 컴파일 타임에는 타입 안전하지만 런타임에는 그렇지 않다.
  • 그래서 둘을 섞어쓰기란 쉽지 않으며, 둘을 섞어 쓰다가 컴파일 오류나 경고를 만나면, 가장 먼저 배열을 리스트로 대체하는 방법을 적용하자.
  • 제네릭은 컴파일타임단에서 TypeCasting을 잡아주므로 런타임에 ClassCastingException이 뜨지 않는다.
  • Link : jyami.tistory.com/92
 

[Effective Java] item28. 배열보다는 리스트를 사용하라

1. 배열과 제네릭의 차이 배열 공변 (convariant) - Sub 가 Super 의 하위타입이라면 배열 Sub[] 는 배열 Super[] 의 하위타입이다. (함께 변한다) 배열에서는 실수를 런타임에 타입 오류를 알 수 있다 Object[]

jyami.tistory.com

 

item29. 이왕이면 제네릭 타입으로 만들라

  • 클라언트에서 직접 형변환해야 하는 타입보다 제네릭 타입이 더 안전하고 쓰기 편하다.
  • 새로운 타입을 설계할 때는 형변환 없이도 사용할 수 있도록 하라
  • 그렇게 하려면 제네릭 타입으로 만들어야 할 경우가 많다
  • 기존 타입중 제네릭이었어야 하는게 있다면 제네릭 타입으로 변경하자.
  • 기존 클라이언트에는 아무 영향을 주지 않으면서, 새로운 사용자를 훨씬 편하게 해준다. (raw 타입의 등장 이유)
  • Link : jyami.tistory.com/93
 

[Effective Java] item29. 이왕이면 제네릭 타입으로 만들라

1. 제네릭클래스로 만드는 방법 a. 클래스 선언에 타입매개변수를 추가한다. 보통 E를 많이 사용한다. 제네릭 필드를 쓴다는 것을 명시하는 것이다. b. 실체화 불가 타입으로는 배열을 만들 수 없�

jyami.tistory.com

 

item30. 이왕이면 제네릭 메서드로 만들라

  • 제네릭 타입과 마찬가지로, 클라이언트에서 입력 매개변수와 반환값을 명시적으로 형변환해야 하는 메서드보다 제네릭 메서드가 더 안전하며 사용하기도 쉽다.
  • 타입과 마찬가지로, 메서드도 형변환 없이 사용할 수 있는 편이 좋으며, 많은 경우 그렇게 하려면 제네릭 메서드가 되어야한다.
  • 역시 타입과 마찬가지로 형변환을 해줘야하는 기존 메서드는 제네릭하게 만들자
  • 기존 클라이언트는 그대로 둔 채 새로운 사용자의 삶을 훨씬 편하게 만들어줄 것이다 (raw 타입의 등장이유)
  • Link : jyami.tistory.com/94
 

[Effective Java] item30. 이왕이면 제네릭 메서드로 만들라

1. 제네릭 메서드 만들기 메서드도 제네릭으로 만들 수 있다. ex ) Collections의 '알고리즘' 메서드 메서드 선언에서 원소타입을 타입 매개변수로 지정한다. 메서드 안에서 이 타입 매개변수를 사용�

jyami.tistory.com

 

Item31. 한정적 와일드 카드를 사용해 API 유연성을 높여라

  • 조금 복잡해도 와일드 카드 타입을 적용하면 API가 훨씬 유연해진다.
  • 널리쓰일 라이브러리를 작성한다면 반드시 와일드카드 타입을 적절히 사용해주자.
  • PECS 공식을 기억하자
  • producer는 extends를 consumer는 super를 사용한다.
  • Comparable과 Comparator는 모두 소비자이다.
  • Link : jyami.tistory.com/95
 

[Effective Java] item31. 한정적 와일드 카드를 사용해 API 유연성을 높여라

1. 매개변수화 타입의 불공변 매개변수화 타입은 불공변이다(invariant) : 서로 다른 타입 Type1 , Type2 가 있을 때 List 은 List 의 하위타입도 아니고 상위타입도 아니다. (리스코프 치환 원칙

jyami.tistory.com

 

item32. 제네릭과 가변인수를 함께 쓸 때는 신중하라

  • 가변인수와 제네릭은 궁합이 좋지 않다.
  • 가변인수 기능은 배열을 노출하여 추상화가 완벽하지 못하고, 배열과 제네릭의 타입 규칙이 서로 다르기 때문이다.
  • 제네릭 varargs 매개변수는 타입 안전하지는 않지만, 허용된다.
  • 메서드에 제네릭(혹은 매개변수화된) varargs 매개변수를 사용하고자 한다면, 먼저 그 메서드가 타입 안전한지 확인한 다음 @SafeVarargs 애너테이션을 달아 사용하는데 불편함이 없게끔 하자.
  • Link : jyami.tistory.com/96
 

[Effective Java] item32. 제네릭과 가변인수를 함께 쓸 때는 신중하라

1. 가변인수와 제네릭을 함께 사용할 때의 헛점 가변인수 메서드를 호출하면 가변인수를 담기위한 배열이 자동으로 하나 만들어진다. 내부로 감춰야했을 배열을 클라이언트에 노출해서 문제가

jyami.tistory.com

 

item33. 타입 안전 이종 컨테이너를 고려하라

  • 컬렉션 API로 대표되는 일반적인 제네릭 형태에서는 한 컨테이너가 다룰 수 있는 타입 매개변수의 수가 고정되어 있다.
  • 하지만 컨테이너 자체가 아닌 키를 타입 매개변수로 바꾸면 이런 제약이 없는 타입 안전 이종 컨테이너를 만들 수 있다.
  • 타입 안전 이종 컨테이너는 Class를 키로 쓰며, 이런식으로 쓰이는 Class 객체를 타입 토큰이라 한다.
  • 또한, 직접 구현한 키 타입도 쓸 수 있다.
  • 데이터베이스 행(컨테이너)을 표현한 DatabaseRow 타입에는 제네릭타입인 Column<T>를 키로 쓸 수 있다.
  • Link : jyami.tistory.com/97
 

[Effective Java] item33. 타입 안전 이종 컨테이너를 고려하라

1. 타입 안전 이종 컨테이너 패턴 매개변수화 되는 대상은 원소가 아닌 컨테이너 자신이다 Set 가 있을 때 매개변수화 되는 것은 Integer 가 아니라 List 이다. 하나의 컨테이너에서 매��

jyami.tistory.com

 

댓글

Comments

Dev Book Review/Effective Java

[Effective Java] item33. 타입 안전 이종 컨테이너를 고려하라

1. 타입 안전 이종 컨테이너 패턴 매개변수화 되는 대상은 원소가 아닌 컨테이너 자신이다 Set가 있을 때 매개변수화 되는 것은 Integer가 아니라 List이다. 하나의 컨테이너에서 매개변수화 할 수 있는 타입의 수가 제한된다. 이보다 유연한 수단 : 타입 안전 이종 컨테이너 패턴 타입 안전 이종 컨테이너 패턴 (type safe heterogeneous container pattern) = 컨테이너 대신 키를 매개변수화 한 다음, 컨테이너에 값을 넣거나 뺄대 매개변수화 한 키를 함께 제공한다. 각 타입의 Class 객체를 매개변수화한 키 역할로 사용한다 : 이때 class 리터럴의 타입은 Class이다. public class Favorites{ // 타입 이종 컨테이너 추상화 public void..

[Effective Java] item33. 타입 안전 이종 컨테이너를 고려하라

728x90

1. 타입 안전 이종 컨테이너 패턴

매개변수화 되는 대상은 원소가 아닌 컨테이너 자신이다
Set<Integer>가 있을 때 매개변수화 되는 것은 Integer가 아니라 List<Integer>이다.

하나의 컨테이너에서 매개변수화 할 수 있는 타입의 수가 제한된다.
이보다 유연한 수단 : 타입 안전 이종 컨테이너 패턴

타입 안전 이종 컨테이너 패턴 (type safe heterogeneous container pattern)
= 컨테이너 대신 키를 매개변수화 한 다음, 컨테이너에 값을 넣거나 뺄대 매개변수화 한 키를 함께 제공한다.
각 타입의 Class 객체를 매개변수화한 키 역할로 사용한다 : 이때 class 리터럴의 타입은 Class<T>이다.

public class Favorites{ // 타입 이종 컨테이너 추상화
  public <T> void putFavorite(Class<T> type, T instance);
  public <T> T getFavorite(Class<T> type)
}

타입토큰 : 컴파일 타임 정보와 런타임 타입 정보를 알아내기 위해 메서드들이 주고받는 class 리터럴

public class Favorites { // 타입 이종 컨테이너 구현
  private Map<Class<?>, Object> favorites = new HashMap<>();
  public <T> void putFavorite(Class<T> type, T instance) {
    favorites.put(Objects.requireNonNull(type), type.cast(instance));
  }
  public <T> T getFavorite(Class<T> type) {
    return type.cast(favorites.get(type));
  }
}

여기서 와일드 카드 타입으로 put 할수 없다고 생각할 수 있지만, 이때 키가 와일드 카드 타입이기 때문에 넣을 수 있다.

Map의 값이 Object를이기 대문에 Class의 cast 메서드를 사용해 동적 형변환한다.
이때 cast 메서드에서 제네릭의 이점을 완벽히 사용한다 : 비검사 형변환 없이도 Favorites를 타입 안전하게 한다.

public class Class<T>{
  T cast(Object obj);
}

 

2. 타입 안전 이종 컨테이너의 제약

a. 악의적인 클라이언트가 Class 객체를 로타입으로 넘기면 Favorites 인스턴스의 타입 안정성이 쉽게 깨진다.

f.putFavorite((Class) Integer.class, "Integer의 인스턴스가 아니다.");
int favoriteInteger = f.getFavorite(Integer.class)

따라서 위에 구현한대로, put을 해줄 당시에 type.cast(instacne)와 같은 동적 형변환을 넣어주자

b. 실체화 불가 타입에는 사용할 수 없다.

  • List<String> 용 Class 객체를 얻을 수 없기 때문이다.
  • List.class 를 사용해야하지만 이렇게 했을 때 List<String>.class, List<Integer>.class 모두를 허용하여 객체 참조를 한다면 오류가 많아질 것이다.

 

3. 한정적 타입 토큰

한정적 타입 매개변수나 한정적 와일드카드를 사용하여 표현가능한 타입을 제한하는 타입토큰

애너테이션 API는 한정적 타입 토큰을 적극적으로 사용한다.

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

annotationType : 애너테이션 타입을 뜻하는 한정적 타입 토큰
대상 요소에 달려있는 애너테이션을 런타임에 읽어오는 기능을 한다.
이 메서드는 토큰으로 명시한 타입의 애너테이션이 대상 요소에 달려있으면 그 애너테이션을 반환하고, 없다면 null을 반환

즉, 애너테이션된 요소는 그 키가 애너테이션 타입인 타입 안전 이종 컨테이너인 것이다.

Class<?> 타입의 객체를 한정적 타입 토큰을 받는 메서드에 넘기고 싶을 때

asSubclass 메서드 : 호출된 인스턴스 자신의 Class 객체를 인수가 명시한 클래스로 형변환 한다.

if 성공 : 인수로 받은 클래스 객체를 반환
else : ClassCastException

static Annotation getAnnotation(AnnotationElement element, String annotationTypeName){
  Class<?> annotationType = null; //바한정적 타입 토큰
  try{
    annotationType = Class.forName(annotationTypeName);
  }catch (Exception ex){
    throw new IllegalArgumentException(ex);
  }
  return element.getAnnotation(annotationType.asSubClass(Annotation.class))
}

댓글

Comments

Dev Book Review/Effective Java

[Effective Java] item32. 제네릭과 가변인수를 함께 쓸 때는 신중하라

1. 가변인수와 제네릭을 함께 사용할 때의 헛점 가변인수 메서드를 호출하면 가변인수를 담기위한 배열이 자동으로 하나 만들어진다. 내부로 감춰야했을 배열을 클라이언트에 노출해서 문제가 생겼다. 제네릭타입의 가변인수 메서드를 호출하면, 제네릭 타입의 배열이 생성되며, 제네릭 타입의 배열은 item28에서 말한 것 처럼, 타입을 런타임에 체크하기 때문에 클래스 캐시팅 에러가 날 가능성이 있다. 메서드 선언시, 실체화 불가 타입으로 varargs 매개변수를 선언하면 컴파일러가 경고를 보낸다. 힙오염이 가능하기 때문이다. 제네릭과 varages를 혼용하면 타입 안정성이 깨진다. 따라서 제네릭 varargs 배열 매개변수에 값을 저장하는 것은 안전하지 않다. static void dangerous(List...st..

[Effective Java] item32. 제네릭과 가변인수를 함께 쓸 때는 신중하라

728x90

1. 가변인수와 제네릭을 함께 사용할 때의 헛점

가변인수 메서드를 호출하면 가변인수를 담기위한 배열이 자동으로 하나 만들어진다.
내부로 감춰야했을 배열을 클라이언트에 노출해서 문제가 생겼다.

제네릭타입의 가변인수 메서드를 호출하면, 제네릭 타입의 배열이 생성되며, 제네릭 타입의 배열은 item28에서 말한 것 처럼, 타입을 런타임에 체크하기 때문에 클래스 캐시팅 에러가 날 가능성이 있다.

메서드 선언시, 실체화 불가 타입으로 varargs 매개변수를 선언하면 컴파일러가 경고를 보낸다.

image

힙오염이 가능하기 때문이다.

제네릭과 varages를 혼용하면 타입 안정성이 깨진다. 따라서 제네릭 varargs 배열 매개변수에 값을 저장하는 것은 안전하지 않다.

static void dangerous(List<String>...stringLists){
  List<Integer> intList = List.of(42);
  Object[] objects = stringLists;
  objects[0] = intList;    // 힙오염 발생
  String s = stringLists[0].get(0)     // ClassCastException
}

이런 위험에도 varargs 매개변수를 받으면 메서드가 실무에서 매우 유용하다.

Arrays.asList(T... a), Collections.addAll(Collection<? superT> c, T... elements),EnumSet.of(E first, E... rest)

2. @SafeVarargs 애너테이션

a. @SuppressWarnings("unchecked")

호출하는 곳 마다 이 애너테이션을 달아 경고를 숨겨야했다.

지루하고, 가독성을 떨어드리고, 때로는 진짜 문제를 알려주는 경고마저 숨긴다.

b. @SafeVarargs

메서드 작성자가 그 메서드가 타입 안전함을 보장하는 장치

컴파일러가 이 약속을 믿고 이 메서드가 안전하지 않을 수 있다는 경고를 더이상 하지 않는다.

3. 메서드가 안전한지 확신 할 수 있을 때

  • 메서드가 varargs 매개변수를 담는 배열에 아무것도 저장하지 않을 때
  • varargs 배열의 참조가 밖으로 노출 되지 않을 때

즉, 순수하게 인수들을 전달하는 일만 할 때 메서드가 안전하다.

4. 제네릭 varargs 매개변수 배열에 다른 메서드가 접근하도록 허용하지 말자

자신의 제네릭 매개변수 배열의 참조를 노출하므로 안전하지 않다.

  • 힙 오염을 메서드를 호출한 쪽의 콜스택으로까지 전이하는 결과를 낳는다.
static <T> T[] toArray(T... args){
  return args; // 참조가 밖으로 
}

예외사항

a. @SafeVarargs로 제대로 애노테이트 된 또다른 varargs 메서드에 넘기는건 안전하다.

b. 이 배열 내용의 일부 함수를 호출만 하는(varargs를 받지 않는) 일반 메서드에 넘기는 것도 안전하다.

5. varargs 매개변수를 List 매개변수로 바꿀 때

static <T> List<T> flatten(List<List<? extends T>> lists){
  List<T> restul = new ArrayList<>();
  for(List<? extends T> list : lists)
    result.addAll(list);
  return result;
}
  • 컴파일러가 이 메서드의 타입 안전성을 검증할 수 있다.
  • @SafeVarargs 애너테이션을 달지 않아도 된다.
  • 실수로 안전하다고 판단할 걱정도 없다.
  • 기존의 varargs는 배열로 메서드의 타입 안정성 검증이 불가능 했었다.

 

댓글

Comments

Dev Book Review/Effective Java

[Effective Java] item31. 한정적 와일드 카드를 사용해 API 유연성을 높여라

1. 매개변수화 타입의 불공변 매개변수화 타입은 불공변이다(invariant) : 서로 다른 타입 Type1,Type2가 있을 때 List 은 List 의 하위타입도 아니고 상위타입도 아니다. (리스코프 치환 원칙에 어긋난다.) 이때 불공변 방식보다 유연한 방식 : 한정적 와일드카드 타입 2. 한정적 와일드 카드 타입을 이용한 확장 유연성 극대화를 위해 원소의 생산자나 소비자용 입력 매개변수에 왈일드카드 타입을 사용하라. 입력 매개변수가 생산자와 소비자 역할을 동시에 한다면 와일드 카드 타입을 써도 좋을 게 없다. public void pushAll(Iterable list, int i, int j); 와일드 카드 타입을 사용하면, set, add와 같은 추가하는 메서드를 작성하지 못한다 따라서 실제 타..

[Effective Java] item31. 한정적 와일드 카드를 사용해 API 유연성을 높여라

728x90

1. 매개변수화 타입의 불공변

매개변수화 타입은 불공변이다(invariant) : 서로 다른 타입 Type1,Type2가 있을 때 List<Type1>List<Type2> 의 하위타입도 아니고 상위타입도 아니다. (리스코프 치환 원칙에 어긋난다.)

이때 불공변 방식보다 유연한 방식 : 한정적 와일드카드 타입

 

2. 한정적 와일드 카드 타입을 이용한 확장

유연성 극대화를 위해 원소의 생산자나 소비자용 입력 매개변수에 왈일드카드 타입을 사용하라.

입력 매개변수가 생산자와 소비자 역할을 동시에 한다면 와일드 카드 타입을 써도 좋을 게 없다.

public void pushAll(Iterable<? extends E> src) { // 생산자
    for (E e : src)
    push (e);
}
public void popAll(Collection<? super E> dst) { // 소비자
  dst.addAll(list);
  list.clear();
}

PECS : producer-extends, consumer-super (= Get and Put Princlple)

매개변수화 타입 T가 생산자면 <? extends T>를 사용하고 소비자라면 <? super T>를 사용하라

 

3. 반환타입에서의 한정적 와일드 카드 타입

반환타입에는 한정적 와일드 카드 타입을 사용하면 안된다
유연성을 높여주지 않고 클라이언트 코드에서도 와일드 카드 타입을 사용하게 하기 때문이다.

public static <E> Set<E> union(Set<? extends E> s1, Set<? extends E> s2)

클래스 사용자가 와일드 카드 타입을 신경써야 한다면 그 API에 문제가 있을 가능성이 크다

 

4. 매개 변수와 인수

  • 매개변수(parameter) : 메서드 선언에 정의한 변수 void add(int value)
  • 인수(argument) : 메서드 호출 시 넘기는 '실제 값' add(10)
  • 타입 매개변수(type parameter) : Class Set<T> {...}
  • 타입 인수(type argument) : Set<Integer> = ...

 

5. 예시

public static <E extends Comparable<? super E>> E max(List<? extends E> list)

이렇 게 구현했을 때만 아래 리스트를 max로 처리하는 것이 가능하다.

List<ScheduledFuture<?>> scheduledFulter = ... ;

 

6. 타입매개 변수가 하나이면 와일드카드로 대체하기

메서드 선언에 타입매개변수가 한번만 나오면 와일드 카드로 대체하라

비한정적 타입 매개변수 -> 비한정적 와일드 카드
한정적 타입 매개변수 -> 한정적 와일드 카드

public static <E> void swap(List<E> list, int i, int j);
public static void swap(List<?> list, int i, int j);

와일드 카드 타입을 사용하면, set, add와 같은 추가하는 메서드를 작성하지 못한다 따라서 실제 타입으로 변환해주기 위해 제네릭의 타입 매개변수를 사용해야하는데 이때

와일드 카드 타입을 실제 타입으로 바꿔주는 private 도우미 메서드를 따로 작성한다.
실제 타입을 알아내려면 이 도우미 메서드는 제네릭 메서드여야하기 때문이다.

public class Swap {
    public static void swap(List<?> list, int i, int j) {
        swapHelper(list, i, j);
    }

    // 와일드카드 타입을 실제 타입으로 바꿔주는 private 도우미 메서드
    private static <E> void swapHelper(List<E> list, int i, int j) {
        list.set(i, list.set(j, list.get(i)));
    }
}

외부에서는 와일드 타입 기반의 멋진 선언을 유지할 수 있다.

댓글

Comments

Dev Book Review/Effective Java

[Effective Java] item30. 이왕이면 제네릭 메서드로 만들라

1. 제네릭 메서드 만들기 메서드도 제네릭으로 만들 수 있다. ex ) Collections의 '알고리즘' 메서드 메서드 선언에서 원소타입을 타입 매개변수로 지정한다. 메서드 안에서 이 타입 매개변수를 사용하게 수정한다. 타입 매개변수 목록은 메서드의 제한자와 반환타입 사이에 온다. 한정적 와일드 카드 타입을 사용하면, 반환타입 입력타입 등을 좀더 유연하게 개선할 수 있다. public static Set union(Set s1, Set s2){ Set result = new HashSet(s1); result.addAll(s2); return result; } 2. 제네릭 : 불변 객체를 여러 타입으로 활용할 수 있게 만드는 방법 불변객체가 제네릭 타입일 때 여러 타입으로 활용이 가능하다. 요청 타입 ..

[Effective Java] item30. 이왕이면 제네릭 메서드로 만들라

728x90

1. 제네릭 메서드 만들기

메서드도 제네릭으로 만들 수 있다. ex ) Collections의 '알고리즘' 메서드

  • 메서드 선언에서 원소타입을 타입 매개변수로 지정한다.
  • 메서드 안에서 이 타입 매개변수를 사용하게 수정한다.
  • 타입 매개변수 목록은 메서드의 제한자와 반환타입 사이에 온다.
  • 한정적 와일드 카드 타입을 사용하면, 반환타입 입력타입 등을 좀더 유연하게 개선할 수 있다.
public static <E> Set<E> union(Set<E> s1, Set<E> s2){
  Set<E> result = new HashSet<>(s1);
  result.addAll(s2);
  return result;
}

 

2. 제네릭 : 불변 객체를 여러 타입으로 활용할 수 있게 만드는 방법

불변객체가 제네릭 타입일 때 여러 타입으로 활용이 가능하다. 요청 타입 변수에 맞게 객체의 타입을 바꾸어주는 "제네릭 싱글턴 팩터리"가 필요하다.

 

a. 함수객체 : 함수 내부에 들어가는 객체

List<String> str = Arrays.asList("a","b","c");
str.sort(Collectioins.reverseOrder());

제네릭 싱글턴 팩터리 패턴을 띄고있다.

reverseComparator를 형변환하는 코드로 비검사 형변환 경고가 발생하여, @SuppressWarning 애너테이션을 추가하여 경고 없이 컴파일되도록 숨겼다.

 

b. 재귀적 타입 한정 (recursive type bound)

자기 자신이 들어간 표현식을 사용하여 타입 매개변수의 허용범위를 한정한다.

public static <E extends Comparable<E>> E max(Collection<E> c);

모든 타입 E는 자기 자신과 같은 타입인 원소 모두와 비교 가능하다

cf) 시뮬레이트한 셀프타입 관용구 - [아이템 2]

- 관련 자바봄 이슈) 

재귀적 타입 한정을 이용하는 제네릭타입이다.

추상메서드 self 를 지원하여 하위클래스에서 형변환 하지 않고도 메서드 연쇄를 지원할 수 있으며. self 타입이 없는 자바를 위한 우회방법을 이야기 한다.

public abstract class PayCard {
    public enum Benefit {
        POINT("포인트"), SALE("할인"), SUPPORT("연회비지원");
        Benefit(String benefit) {
        }
    }

    final Set<Benefit> benefits;

    abstract static class Builder<T extends Builder<T>> { // 재귀적 타입한정
        EnumSet<Benefit> benefits = EnumSet.noneOf(Benefit.class);

        public T addBenefit(Benefit benefit) {
            this.benefits.add(benefit);
            return self();
        }

        abstract PayCard build();

        protected abstract T self();
    }

    PayCard(Builder<?> builder) {
        benefits = builder.benefits.clone();
    }
}
public static class Builder extends PayCard.Builder<Builder>{
        private final Sale sale;

        public Builder(Sale sale) {
            this.sale = sale;
        }

        @Override
        NaverPayCard build() {
            return new NaverPayCard(this);
        }

        @Override
        protected Builder self() {
            return this;
        }
    }

재귀적 타입 한정을 사용함으로 인해서, PayCard의 하위타입인 NaverPayCard에서 상위 클래스의 메서드를 호출 할 수 있게 된다.

https://github.com/Java-Bom/ReadingRecord/issues/75

 

 

[아이템 30] 시뮬레이트한 셀프 관용구 · Issue #75 · Java-Bom/ReadingRecord

180p 시뮬레이트한 셀프관용구에 대한 설명이 나오는데, 이게 2장의 Builder패턴 pizza를 상속받은 nypizza 빌더패턴에 대한 이야기야, 이거 한번 구현해보고 실제로 왜 재귀적 타입한정을 써야 이런 ��

github.com

 

댓글

Comments

Dev Book Review/Effective Java

[Effective Java] item29. 이왕이면 제네릭 타입으로 만들라

1. 제네릭클래스로 만드는 방법 a. 클래스 선언에 타입매개변수를 추가한다. 보통 E를 많이 사용한다. 제네릭 필드를 쓴다는 것을 명시하는 것이다. b. 실체화 불가 타입으로는 배열을 만들 수 없으니 해결한다. E[] element = new E[DEFAULT_INITAL_CAPACITY] b-1. 제네릭 배열을 금지하는 제약을 대놓고 우회한다. E[] elemet = (E[]) new Object[DEFAULT_INITAL_CAPACITY] 비검사 형변환이 프로그램의 타입 안정성을 해치지 않는지를 확인한다. > 이후 @SuppressWarnings 애너테이션으로 해당 경고를 숨긴다. 배열이 private 필드에 저장된다. 클라이언트로 반환되거나 다른 메서드에 전달되는 일이 없다. 배열에 저장되는 원소의..

[Effective Java] item29. 이왕이면 제네릭 타입으로 만들라

728x90

1. 제네릭클래스로 만드는 방법

a. 클래스 선언에 타입매개변수를 추가한다.

  • 보통 E를 많이 사용한다.
  • 제네릭 필드를 쓴다는 것을 명시하는 것이다.

b. 실체화 불가 타입으로는 배열을 만들 수 없으니 해결한다.

E[] element = new E[DEFAULT_INITAL_CAPACITY]

b-1. 제네릭 배열을 금지하는 제약을 대놓고 우회한다.

E[] elemet = (E[]) new Object[DEFAULT_INITAL_CAPACITY]

비검사 형변환이 프로그램의 타입 안정성을 해치지 않는지를 확인한다. > 이후 @SuppressWarnings 애너테이션으로 해당 경고를 숨긴다.

  • 배열이 private 필드에 저장된다.
  • 클라이언트로 반환되거나 다른 메서드에 전달되는 일이 없다.
  • 배열에 저장되는 원소의 타입은 항상 E 이다.

특징

  • 가독성이 더 좋다. : E 타입만을 받는다는 점을 어필한다.
  • 형변환을 배열 생성시 단한번만 해주면된다.
  • 배열의 런타임 타입이 컴파일타임 타입과 달라 힙오염이 일어날 수 있다.

b-2. 배열 필드의 타입을 Object[]로 바꾼다.

E result = (E) elements[--size];

E가 실체화 불가 타입이므로 컴파일러는 런타임에 이뤄지는 형변환이 안전한지 증명할 방법이 없다. > 이후 비검사 형변환을 수행하는 할당문에서만 경고를 숨긴다.

특징

  • 배열에서 원소를 읽을 떄마다 형변환 해주어야 한다.
  • 힙오염이 해가되지 않는다.

 

2. 제네릭 배열을 사용하는 이유

  • 자바가 리스트를 사용하는게 항상 가능하지도, 꼭 더 좋은 것도아니다. 리스트도 결국은 기본타입인 배열을 사용해 구현해야하기 때문이다.
  • 성능향상의 이슈로 배열을 사용할 수 있다.
  • 제네릭타입의 타입매개변수에는 primitive 타입을 사용할 수 없다. 박싱된 기본타입으로 우회할 수 있긴하다.

 

3. 한정적 타입 매개변수

class DelayQueue<E extends Delayed> implements BlockingQueue<E>

Delayed.class의 하위타입만 받는다는 의미이다. (모든 타입은 자기 자신의 하위 타입)

ClassCastException을 걱정할 필요가 없다.

댓글

Comments

Dev Book Review/Effective Java

[Effective Java] item28. 배열보다는 리스트를 사용하라

1. 배열과 제네릭의 차이 배열 공변 (convariant) - Sub가 Super의 하위타입이라면 배열 Sub[]는 배열 Super[]의 하위타입이다. (함께 변한다) 배열에서는 실수를 런타임에 타입 오류를 알 수 있다 Object[] objectArray = new Long[1]; objectArray[0] = "타입이 달라 넣을 수 없다" // ArrayStoreException 실체화(reify)된다 : 런타임에도 자신이 담기로 한 원소의 타입을 인지하고 확인한다. 제네릭 불공변 (invariant) - 서로 다른 타입 Type1,Type2가 있을 때 List 은 List 의 하위타입도 아니고 상위타입도 아니다 리스트에서는 컴파일할 때 타입 오류를 바로 알 수 있다. List ol = new Ar..

[Effective Java] item28. 배열보다는 리스트를 사용하라

728x90

1. 배열과 제네릭의 차이

배열

  • 공변 (convariant) - SubSuper의 하위타입이라면 배열 Sub[]는 배열 Super[]의 하위타입이다. (함께 변한다)
  • 배열에서는 실수를 런타임에 타입 오류를 알 수 있다
  Object[] objectArray = new Long[1];
  objectArray[0] = "타입이 달라 넣을 수 없다" // ArrayStoreException

실체화(reify)된다 : 런타임에도 자신이 담기로 한 원소의 타입을 인지하고 확인한다.

제네릭

  • 불공변 (invariant) - 서로 다른 타입 Type1,Type2가 있을 때 List<Type1>List<Type2> 의 하위타입도 아니고 상위타입도 아니다
  • 리스트에서는 컴파일할 때 타입 오류를 바로 알 수 있다.
  List<Object> ol = new ArrayList<Long>();    // 호환되지 않는 타입이다.
  ol.add("타입이 달라 넣을 수 없다.")

소거(erasure)된다 : 원소 타입을 컴파일 타임에만 검사하며 런타임에는 알 수 없다.
제네릭 지원 전과 제네릭 타입을 함게 사용할 수 있게 해주는 메커니즘이다.

 

2. 제네릭 배열은 사용불가하다.

배열은 제네릭 타입, 매개변수화 타입, 타입 매개변수로 사용할 수 없다.
new List<E>[], new List<String>[], new E[] : 컴파일시 제네릭 생성오류를 일으킨다.

이유 : 타입안전하지 않기 때문이다
컴파일러가 자동 생성한 형변환 코드에서 런타임에 ClassCastException이 발생할 수 있다 : 제네릭 타입의 취지와 맞지 않다.

 

3. 실체화 불가 타입

E, List<E>, List<String> : 실체화 불가 타입

실체화 되지 않아 런타임에는 컴파일 타임보다 타입점보를 적게 가지는 타입

List<?>, Map<?,?> : 소거 메커니즘 때문에 매개변수화 타입 가운데 실체화 가능한 타입 - 비한정적 와일드카드 타입

 

4. 배열의 불편함

a. 제네릭 컬렉션에서는 자신의 원소 타입을 담은 배열을 반환하는게 불가능하다.

아이템 33의 타입 안전 이종 컨테이너를 이용하여 자신의 원소타입을 추론할 수있다 (우회)

b. 제네릭 타입과 가변인수 메서드를 함께 쓰면 해석하기 어려운 경고 메세지를 받게된다.

가변인수 메서드를 호출할 때마다. 가변인수를 담는 배열이 만들어지는데, 이때 그 배열의 원소가 실체화 불가 타입이면 경고가 뜬다. @SafeVarargs로 해결한다.

 

5. 배열대신 리스트를 사용하자

  • 장점 : 타입 안정성과 상호 운용성이 좋아진다.
  • 단점 : 코드가 조금 복잡해지고 성능이 살짝 나빠질 수도 있다.
public class Chooser<T>{
  private final List<T> choiceList;

  public Chooser(Collection<T> choices){
    choiceList = new ArrayList<>(choices);
  }

  public T choose(){
    Random rnd = ThreadLocalRandom.current();
    return choiceList.get(rnd.netInt(choiceList.size()));
  }
}

List를 사용하면 런타임에 ClassCastException을 만날 일은 없다.

public class Chooser<T>{
  private final T[] choiceArray;

  public Chooser(Collection<T> choices){
    choicesArra = (T[]) choices.toArray; 
  }

    public Object choose(){
    Random rnd = ThreadLocalRandom.current();
    return choiceArray[rnd.nextInt(choiceArray.length)];
  }
}

T[] 로의 타입캐스팅 과정에서 경고가 뜬다. 제네릭에서는 원소의 타입정보가 소거되어 런타임에는 타임 정보를 알 수 없기 때문이다. item 27 비검사 경고를 제거하라는 말에 따라 위험요소를 제거할 수 있는 최선의 방법은 List로 구현하는 것이었다.

댓글

Comments