Dev Book Review/Effective Java

[Effective Java] item 35. ordinal 메서드 대신 인스턴스 필드를 사용하라

1. ordinal 메서드를 잘못쓸 때 ordinal 메서드 : 해당 상수가 그 열거 타입에서 몇 번째 위치인지 반환하는 메서드 상수 선언 순서를 바꾸면 오동작한다. 이미 사용중인 정수와 값이 같은 상수는 추가할 수 없다. 중간에 값을 비울 수 없다 : 값을 비우기 위한 더미(dummy) 상수 추가 2. 해결 방법 열거 타입 상수에 연결 된 값을 ordinal 메서드로 얻지 말고 인스턴스 필드에 저장하라. 3. Enum API 문서 이 메서드는 EnumSet과 EnumMap과 같이 열거 타입 기반의 범용 자료 구조에 쓸 목적으로 설계 되었다.

[Effective Java] item 35. ordinal 메서드 대신 인스턴스 필드를 사용하라

728x90

1. ordinal 메서드를 잘못쓸 때

ordinal 메서드 : 해당 상수가 그 열거 타입에서 몇 번째 위치인지 반환하는 메서드

  • 상수 선언 순서를 바꾸면 오동작한다.
  • 이미 사용중인 정수와 값이 같은 상수는 추가할 수 없다.
  • 중간에 값을 비울 수 없다 : 값을 비우기 위한 더미(dummy) 상수 추가

2. 해결 방법

열거 타입 상수에 연결 된 값을 ordinal 메서드로 얻지 말고 인스턴스 필드에 저장하라.

3. Enum API 문서

이 메서드는 EnumSet과 EnumMap과 같이 열거 타입 기반의 범용 자료 구조에 쓸 목적으로 설계 되었다.

 

댓글

Comments

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

Algorithm

Leet code submit 주의점 - 전역변수 초기화 | Leet Code Submit Caution - Global Variable Initialization

https://leetcode.com/problems/add-two-numbers/ Add Two Numbers - LeetCodeLevel up your coding skills and quickly land a job. This is the best place to expand your knowledge and get prepared for your next interview.leetcode.com이문제 풀다가 간단한 문제고, 문제 풀이도 잘 했는데 뭐가 문제지 했었다. 결론부터 말하자면 LeetCode에서 코드를 제출할 때 static 변수를 사용하지 말자. 사용하더라도 한번의 코드가 끝나면 static 변수를 초기화해주자public class Solution{ static int carry = 0..

Leet code submit 주의점 - 전역변수 초기화 | Leet Code Submit Caution - Global Variable Initialization

728x90

https://leetcode.com/problems/add-two-numbers/

 

Add Two Numbers - LeetCode

Level up your coding skills and quickly land a job. This is the best place to expand your knowledge and get prepared for your next interview.

leetcode.com

이문제 풀다가 간단한 문제고, 문제 풀이도 잘 했는데 뭐가 문제지 했었다. 

결론부터 말하자면 LeetCode에서 코드를 제출할 때 static 변수를 사용하지 말자. 사용하더라도 한번의 코드가 끝나면 static 변수를 초기화해주자

public class Solution{
	static int carry = 0;
    
    public ListNode addTwoNumbers(ListNode l1, ListNode l2) {
    	// carry에 따라 결과값 변화
    }
}

 

대충 요약하면 이런 상황이었는데,

이상하게 testcase에 해당 wrong answer이 나는 걸 그대로 테스트하면 accept 되는데, 실제로 submit을 해서 넣어 돌리면 wrong answer이 떳다. 이상하게 내 output에 이 올바르지 않은 값이었다.

그래서 엥,, 이상하다 하고 leet code 측에 메일을 보냈었는데, 문뜩 생각이 들어서 확인을 해보니, test case를 1000개를 실행해 주었고, 내가 전역변수로 설정한 carry값이 다음 test case 실행에 영향을 미췄더라면? 이라는 가설을 내고 확인해보았더니 맞았다.

리트코드에서 전역변수를 사용할 때는 꼭 전역변수를 초기화해주자. 초기화하지 않은 전역변수 값이 submition시 test case에 영향을 줄 수 있다.

알고리즘을 역시 안풀어보니까 이런거에도 끙끙 대는구나 싶다... 다시 리트코드 풀러가야지 총총

https://leetcode.com/problems/add-two-numbers/

 

Add Two Numbers - LeetCode

Level up your coding skills and quickly land a job. This is the best place to expand your knowledge and get prepared for your next interview.

leetcode.com

I was solving this problem and it was a simple one — I thought my solution was correct, so I couldn't figure out what was going wrong. 

Long story short: when submitting code on LeetCode, don't use static variables. If you do use them, make sure to reset the static variables after each run.

public class Solution{
	static int carry = 0;
    
    public ListNode addTwoNumbers(ListNode l1, ListNode l2) {
    	// carry에 따라 결과값 변화
    }
}

 

To summarize, here's what was going on:

Strangely, when I tested the exact wrong answer test case myself, it would pass just fine. But when I actually submitted the code, it came back as a wrong answer. Somehow my output had this incorrect value.

So I was like, huh, that's weird, and I even sent an email to the LeetCode team. But then it suddenly hit me — I checked and found out that they run around 1,000 test cases, and I hypothesized: what if the carry value I declared as a global variable was carrying over and affecting the next test case? I tested that theory, and sure enough, that was exactly the problem.

When using global variables on LeetCode, always make sure to reset them. Uninitialized global variable values can affect other test cases during submission.

I guess this is what happens when you don't practice algorithms regularly — you end up struggling with stuff like this... Time to get back to grinding LeetCode problems. Off I go~

댓글

Comments

Develop/JAVA

자바의 제네릭 타입 소거, 리스트에 관하여 (Java Generics Type Erasure, List) | Java's Generic Type Erasure, Regarding Lists (Java Generics Type Erasure, List)

1. 자바의 제네릭과 로타입 (Java Generics and Raw Type)public class Example{ private T member;}위와 같이 클래스 및 인터페이스 선언에 타입 매개변수 T 가 사용되면 제네릭 클래스, 제네릭 인터페이스라고 말하는데, 이때 사용된 이 클래스 Example 를 제네릭타입이라고 이야기한다.제네릭을 사용하면 로타입이라는 개념이 나오는데, 로타입은 제네릭 타입에서 타입 매개변수를 전혀 사용하지 않았을 때를 의미한다 즉, 위 제네릭 타입Example를 Example 로만 선언하여 사용했을 경우를 말한다.public class Example { private T member; public Example(T member) { this.membe..

자바의 제네릭 타입 소거, 리스트에 관하여 (Java Generics Type Erasure, List) | Java's Generic Type Erasure, Regarding Lists (Java Generics Type Erasure, List)

728x90

1. 자바의 제네릭과 로타입 (Java Generics and Raw Type)

public class<T> Example{
	private T member;
}

위와 같이 클래스 및 인터페이스 선언에 타입 매개변수 T 가 사용되면 제네릭 클래스, 제네릭 인터페이스라고 말하는데, 이때 사용된 이 클래스 Example<T> 를 제네릭타입이라고 이야기한다.

제네릭을 사용하면 로타입이라는 개념이 나오는데, 로타입은 제네릭 타입에서 타입 매개변수를 전혀 사용하지 않았을 때를 의미한다 즉, 위 제네릭 타입Example<T>Example 로만 선언하여 사용했을 경우를 말한다.

public class Example<T> {
    private T member;

    public Example(T member) {
        this.member = member;
    }

    public static void main(String[] args) {
        Example<Integer> parameterType = new Example<>(1);
        Integer parameterTypeMember = parameterType.member;
        System.out.println(parameterTypeMember);

        Example rawType = new Example(1);
        Object rawTypeMember = rawType.member;
        System.out.println(rawTypeMember);
    }
}

위 코드는 제네릭 파라미터 타입과 로타입을 사용한 경우이다. 하지만 로타입은 사용하지말자.

제네릭의 장점은 컴파일 타임에 타입에 대한 안정성을 보장받을 수 있다는 점이다. 제네릭 타입으로 선언한 변수는 컴파일 타임에 타입 체크를 하기 때문에 런타임에서 ClassCastException과 같은 UncheckedException을 보장 받을 수 있다.

반면 아래와 같이 로타입으로 사용될 경우에는 제네릭을 사용했을 때의 안정성과 표현력이라는 장점을 발휘할 수 없기 때문에, IDE 에서도 "Raw use of parameterized class 'Example' " 라는 경고를 주는 것을 볼 수 있다.

그럼 로타입이 나오게 된 이유는 무엇일까? 로타입이 나오게 된 이유는 제네릭의 특징인 소거와 관련이 있다.

제네릭은 JDK5 에서 도입이되었다. 버그를 줄이기 위한 목적과, 다른 추상화된 타입에 대한 레이어를 추가하기 위해서이다.
이에 따라 제네릭을 도입한 JDK5는 기존의 코드를 모두 수용하면서 제네릭을 사용하는 새로운 코드와의 호환성을 유지 했어야 했다. 따라서 코드의 호환성 때문에 : 로타입의 지원 + 제네릭을 구현할 때 소거(erasure)하는 방식을 이용하였다.

 

2. 제네릭의 타입소거 (Generics Type Erasure)

소거란 원소 타입을 컴파일 타임에만 검사하고 런타임에는 해당 타입 정보를 알 수 없는 것이다. 다른 말로는 컴파일 타임에만 타입에 대한 제약 조건을 적용하고, 런타임에는 타입에 대한 정보를 소거하는 프로세스이다.

List<Object> ol = new ArrayList<Long>(); // 컴파일 에러
ol.add("타입이 달라 넣을 수 없다");

다음과 같은 상황에서 컴파일시에 타입 오류를 바로 알 수 있다 (리스트도 제네릭 타입으로 구현되어있기 때문에)

Java 컴파일러는 타입소거를 아래와 같이 적용한다.

  • 제네릭 타입( Example<T>) 에서는 해당하는 타입 파라미터 (T) 나 Object로 변경해준다. 
    Object로 변경하는 경우는 unbounded 된 경우를 뜻하며, 이는 <E extends Comparable<E>>와 같이 bound를 해주지 않은 경우를 의미한다.
    따라서 이 소거 규칙에 대한 바이트코드는 제네릭을 적용할 수 있는 일반 클래스, 인터페이스, 메서드에만 해당된다.
  • 타입 안정성 보존을 위해 필요하다면 type casting을 넣어준다.
  • 확장된 제네릭 타입에서 다형성을 보존하기 위해 bridege method를 생성한다.
public static <E> boolean containsElement(E[] elements, E element) {
	for (E e : elements) {
		if (e.equals(element)) {
			return true;
		}
	}
	return false;
}

실제로 이렇게 선언되어있는 제네릭 메서드의 경우 선언 방식에 따라 컴파일러가 타입파라미터 E를 변경한다.

public static boolean containsElement(Object[] elements, Object element) {
	for (Object e : elements) {
		if (e.equals(element)) {
			return true;
		}
	}
	return false;
}

컴파일러는 첫번째 규칙에 따라 타입 파라미터 E가 bound하게 선언되어있지 않기 때문에 타입 파라미터 EInteger로 우선적으로 바꾼다.

이때 만약 프로그래머가 continasElement(Integer[], Integer) 형식으로 해당 메서드를  사용했다면, 컴파일러 내부에서 두번재 규칙에 따라 타입 안정성 보존을 위해 Object -> Integer로의 타입 캐스트 코드를 넣어주어 제네릭의 타입 안정성을 보장해주는 것이다. 

반면 로타입일 경우에는, 타입 파라미터가 정해져있지 않아. Object로 변환한 것에서 끝난다.

더보기

반면 타입 파라미터 E를 bound하게 설정한다면 

public static <E extends Comparable<E>> void containsElement(E[] elements) {
	for (E e : elements) {
		System.out.println("%s", e);
	}
}

타입이 소거될때 Object로 바뀌는 것이 아닌 한정시킨 타입인 Comparable로 변환된다.

public static void containsElement(Comparable[] elements) {
	for (Comparable e : elements) {
		System.out.println("%s", e);
	}
}

추가로 세번째 규칙에 대해서 언급하자면 java compiler는 제네릭의 타입안정성을 위해 Bridge Method도 만들어낼 수있다. Bridge Method는 java 컴파일러가 컴파일 할 때 메서드 시그니처가 조금 다르거나 애매할 경우에대비하여 작성된 메서드이다. 이 경우는 파리미터화된 클래스나 인터페이스를 확장한 클래스를 컴파일 할 때 생길 수 있다.

public class IntegerStack extends Stack<Integer> {
    public Integer push(Integer value) {
        super.push(value);
        return value;
    }
}

 

Java 컴파일러는 다형성을 제네릭 타입 소거에서도 지키기 위해, IntegerStackpush(Integer) 메서드와 Stack의 push(Object) 메서드 시그니처 사이에 불일치가 없어야 했다. 따라서 컴파일러는 런타임에 해당 제네릭 타입의 타입소거를 위한 Bridge 메서드를 만드는데 아래와같은 방식으로 만든다.

public class IntegerStack extends Stack {
    // Bridge method generated by the compiler
     
    public Integer push(Object value) {
        return push((Integer)value);
    }
 
    public Integer push(Integer value) {
        return super.push(value);
    }
}

extends Stack<Integer> -> Stack 으로  변경한 것을 볼 수 있으며, push에 parameter를 Object가 아닌 Integer로 맞추기 위한 도우미 메서드가 늘어났다는 것을 알 수 있다. 결과적으로 Stack 클래스의 push method는 타입소거를 진행한 후에, IntegerStack 클래스의 원본 push 방법을 사용하게 한다.

 

3. 제네릭에서는 리스트를 사용하자

실체화 불가 타입(Non-Reifiable Type)에 대한 설명이 있다 runtime에 타입 정보를 갖고있지 않고, compile-time에 타입 소거가 되는 타입을 의미한다고 한다. 이에 반대하는 개념으로는 실체화(reifiable)가 있다. 이는 타입 정보를 런타임에 완벽하게 사용할 수 있는 유형으로, 소거와는 반대 개념이다. 

실체화 불가 타입의 대표적인 예시로는 List<String> List<Number> 와 같은 리스트가 있고, 실체화 타입의 대표적인 예시로는 String[], Number[] 와 같은 배열이 있다.

이펙티브 자바에서는 타입소거라는 특성이 있는 제네릭은 실체화 불가 타입인 List와 함께 사용하기를 권장한다. Array 에서는 런타임에 타입정보를 갖고있는데, 제네릭을 사용하면 타입이 소거되기 때문에 해당 제네릭 변수에 대한 정보를 런타임에 갖고있지 않기 때문이다.

사실 이 부분은 이펙티브 자바를 읽으면서 스터디에서 했던 제네릭과 관련한 이야기를 예시로 이야기 하려한다.

관련 이슈 : https://github.com/Java-Bom/ReadingRecord/issues/88

 

[아이템 32] toArray · Issue #88 · Java-Bom/ReadingRecord

193p 마지막 코드에서 toArray를 바로 String[]배열로 받는건 되는데 왜 pickTwo를 거쳐서 String[]로 받는건 안될까?

github.com

static <T> T[] pickTwo(T a, T b, T c) {
  switch (ThreadLocalRandom.current().nextInt(3)) {
    case 0:
      return toArray(a, b);
    case 1:
      return toArray(b, c);
    case 2:
      return toArray(a, c);
  }
  throw new AssertionError();
}

static <T> T[] toArray(T... args) {
  return args;
}

public static void main(String[] args) {
  String[] strings = pickTwo("좋은", "빠른", "저렴한");  
}

위 코드에서 ClassCastException이 터진다. 반면 아래와 같이 pickTwo 메서드를 사용했을 때는 ClassCastException이 터지지 않는다. 왜그럴까? 

static <T> List<T> pickTwoList(T a, T b, T c) {
  switch (ThreadLocalRandom.current().nextInt(3)) {
    case 0:
      return Arrays.asList(a, b);
    case 1:
      return Arrays.asList(b, c);
    case 2:
      return Arrays.asList(a, c);
  }
  throw new AssertionError();
}

List<String> strings = pickTwoList("좋은", "빠른", "저렴한");

문제를 좀 더 단순히 해보자.

static <T> List<T> pickList(T a) {
        return Arrays.asList(a);
    }

    static <T> T[] toArray(T... args) {
        return args;
    }

    static <T> T[] pickArray(T a) {
        return toArray(a);
    }

    public static void main(String[] args) {
        List<String> stringList = pickList("자바봄");
        String[] stringArray = pickArray("자바봄");
   }
}

위 코드에서 pickList를 지나 pickArray를 실행하면 runtimeException이 터지는 것을 볼 수 있다. 내용은 [Ljava.lang.Object; cannot be cast to [Ljava.lang.String; 이다 무엇이 문제일까?

정답은 Array와 List의 실체화에 있었다. 

코드를 이해해보자. 위에서는 제네릭 타입추론이 2 depth가 들어간다. pickArray에서 타입 매개변수 TString과 대응하여 들어가기 때문에 pickArray(String) toArray(String...) 이 들어갈 것으로 예상한다. 하지만 실제 런타임에 확인해보면 pickArray(String),  toArray(Object...)가 들어간다. 그 이유는 위에서 제네릭 타입추론을 이야기할 때 우선적으로 bounded가 아닌 매개변수일경우 컴파일러가 Object로 대체한다는 이야기와 대응된다. pickArray에서는 main에 있는 String 타입으로 타입추론이 가능했으나 toArray는 제네릭 타입을 바라보고 있으므로 타입추론을 Object로 하여 런타임에 타입정보를 갖고있게 된다.

따라서 pickArrayString[] 으로 타입캐스팅을 준비하였으나, 위에서 말한 것처럼 런타임에 toArray가 갖고있는 타입은 Object[] 이므로 런타임에 (String[]) Object[] 와 같은 형식으로 강제적으로 타입캐스팅을 하다가 ClassCastException이 발생한다. (String 배열은, Object 배열의 하위 타입이 아니기 때문에 Casting이 되지 않는다.)

반면에 pickList를 호출할때는 컴파일이 성공한다. 이 이유는 List가 실체화 불가타입이었기 때문이다. 컴파일 타임에 캐스팅할 정보가 이미 결정이되고, 런타임때에는 제네릭의 소거라는 특성 때문에 Java 컴파일러가 타입에 맞는 캐스팅 방식을 올바르게 추가해줘서 캐스팅 에러가 나지 않는다.

정리하자면, 리스트 + 제네릭은 컴파일 타임에 결정된 캐스팅 정보가 올바르기 때문에 통과가 된다. 제네릭의 장점인 컴파일 타임에 타입이 안맞는 것을 체크해주는 걸 List에서도 수행하기 때문이다. 반면 배열 같은 경우엔 타입 캐스팅을 런타임에 결정하기 때문에 문제가 생긴 것이다. 

 

조금 길었지만 사실 결론은 간단하다. 제네릭은 타입소거라는 특징으로 컴파일러가 컴파일 타임에 타입을 추론할 수 있으며, 이런 타입 추론 기능을 강력하게 사용하기 위해서는 런타임에 타입을 추론하는 Array 대신에 컴파일타임에 타임을 추론하는 List를 함께 사용해야 안정성을 보장 할 수 있다는 것이다.

 

참고 글

https://www.baeldung.com/java-generics

Effecitve Java 3/E - Chapter 5: Generics 

https://docs.oracle.com/javase/tutorial/java/generics/erasure.html

https://www.baeldung.com/java-type-erasure

 

1. Java Generics and Raw Type

public class<T> Example{
	private T member;
}

When a type parameter T is used in a class or interface declaration like above, we call it a generic class or generic interface. The class Example<T> used here is referred to as a generic type.

When using generics, the concept of raw types comes up. A raw type is when you don't use a type parameter at all with a generic type — in other words, when you declare and use the generic type Example<T> as just Example.

public class Example<T> {
    private T member;

    public Example(T member) {
        this.member = member;
    }

    public static void main(String[] args) {
        Example<Integer> parameterType = new Example<>(1);
        Integer parameterTypeMember = parameterType.member;
        System.out.println(parameterTypeMember);

        Example rawType = new Example(1);
        Object rawTypeMember = rawType.member;
        System.out.println(rawTypeMember);
    }
}

The code above shows both a generic parameterized type and a raw type in use. But don't use raw types.

The advantage of generics is that they guarantee type safety at compile time. Since variables declared with a generic type are type-checked at compile time, you're protected from UncheckedException errors like ClassCastException at runtime.

On the other hand, when used as a raw type like below, you lose the benefits of safety and expressiveness that generics provide. That's why you can see the IDE giving you a warning like "Raw use of parameterized class 'Example' ".

So why do raw types exist in the first place? The reason raw types came about is related to erasure, a key characteristic of generics.

Generics were introduced in JDK5. The goals were to reduce bugs and to add a layer of abstraction over types.
Because of this, JDK5 — which introduced generics — had to accommodate all existing code while maintaining compatibility with new code that uses generics. So for the sake of code compatibility, they supported raw types and implemented generics using erasure.

 

2. Generics Type Erasure

Erasure means that element types are only checked at compile time and the type information is not available at runtime. In other words, it's a process where type constraints are enforced only at compile time, and type information is erased at runtime.

List<Object> ol = new ArrayList<Long>(); // 컴파일 에러
ol.add("타입이 달라 넣을 수 없다");

In a situation like this, you can immediately catch the type error at compile time (because List is also implemented as a generic type).

The Java compiler applies type erasure as follows:

  • In a generic type (Example<T>), it replaces the type parameter (T) with the corresponding type or Object
    Replacing with Object happens when the type is unbounded — meaning it hasn't been bounded like <E extends Comparable<E>>.
    Therefore, the bytecode for this erasure rule only applies to regular classes, interfaces, and methods that can use generics.
  • It inserts type casting where necessary to preserve type safety.
  • It generates bridge methods to preserve polymorphism in extended generic types.
public static <E> boolean containsElement(E[] elements, E element) {
	for (E e : elements) {
		if (e.equals(element)) {
			return true;
		}
	}
	return false;
}

For a generic method declared like this, the compiler replaces the type parameter E depending on how it's declared.

public static boolean containsElement(Object[] elements, Object element) {
	for (Object e : elements) {
		if (e.equals(element)) {
			return true;
		}
	}
	return false;
}

Following the first rule, since the type parameter E is not declared as bounded, the compiler first replaces the type parameter E with Integer.

At this point, if the programmer used the method in the form of containsElement(Integer[], Integer), the compiler internally inserts type casting code from Object to Integer according to the second rule to preserve type safety, thus guaranteeing the type safety of generics.

In the case of raw types, however, since no type parameter is specified, it simply ends with the conversion to Object.

더보기

On the other hand, if you set the type parameter E as bounded:

public static <E extends Comparable<E>> void containsElement(E[] elements) {
	for (E e : elements) {
		System.out.println("%s", e);
	}
}

When the type is erased, instead of being replaced with Object, it gets converted to the bounded type Comparable.

public static void containsElement(Comparable[] elements) {
	for (Comparable e : elements) {
		System.out.println("%s", e);
	}
}

Additionally, regarding the third rule, the Java compiler can also generate Bridge Methods for generic type safety. A Bridge Method is a method created by the Java compiler during compilation to handle cases where method signatures are slightly different or ambiguous. This can happen when compiling a class that extends a parameterized class or interface.

public class IntegerStack extends Stack<Integer> {
    public Integer push(Integer value) {
        super.push(value);
        return value;
    }
}

 

The Java compiler needed to ensure there was no mismatch between the push(Integer) method signature of IntegerStack and the push(Object) method signature of Stack, in order to preserve polymorphism even during generic type erasure. So the compiler creates a Bridge method for the type erasure of that generic type at runtime, and it does it like this:

public class IntegerStack extends Stack {
    // Bridge method generated by the compiler
     
    public Integer push(Object value) {
        return push((Integer)value);
    }
 
    public Integer push(Integer value) {
        return super.push(value);
    }
}

You can see that extends Stack<Integer> has been changed to just Stack, and a helper method has been added to match the push parameter to Integer instead of Object. As a result, the Stack class's push method, after type erasure, delegates to the original push method of the IntegerStack class.

 

3. Use Lists with Generics

Non-Reifiable Type refers to a type that doesn't hold type information at runtime and undergoes type erasure at compile time. The opposite concept is reifiable, which refers to types whose type information is fully available at runtime — the opposite of erasure. 

Typical examples of non-reifiable types include lists like List<String> and List<Number>, while typical examples of reifiable types include arrays like String[] and Number[].

Effective Java recommends using generics — which have the characteristic of type erasure — with List, a non-reifiable type. This is because arrays hold type information at runtime, but when you use generics, the type gets erased, so the generic variable's information isn't available at runtime.

This part is actually something I want to illustrate with an example from a study group discussion about generics that we had while reading Effective Java.

Related issue : https://github.com/Java-Bom/ReadingRecord/issues/88

 

[Item 32] toArray · Issue #88 · Java-Bom/ReadingRecord

On p.193, the last code example — receiving toArray directly as a String[] array works, but why doesn't it work when receiving as String[] through pickTwo?

github.com

static <T> T[] pickTwo(T a, T b, T c) {
  switch (ThreadLocalRandom.current().nextInt(3)) {
    case 0:
      return toArray(a, b);
    case 1:
      return toArray(b, c);
    case 2:
      return toArray(a, c);
  }
  throw new AssertionError();
}

static <T> T[] toArray(T... args) {
  return args;
}

public static void main(String[] args) {
  String[] strings = pickTwo("좋은", "빠른", "저렴한");  
}

In the code above, a ClassCastException is thrown. However, when using the pickTwo method like below, no ClassCastException occurs. Why is that?

static <T> List<T> pickTwoList(T a, T b, T c) {
  switch (ThreadLocalRandom.current().nextInt(3)) {
    case 0:
      return Arrays.asList(a, b);
    case 1:
      return Arrays.asList(b, c);
    case 2:
      return Arrays.asList(a, c);
  }
  throw new AssertionError();
}

List<String> strings = pickTwoList("좋은", "빠른", "저렴한");

Let's simplify the problem a bit.

static <T> List<T> pickList(T a) {
        return Arrays.asList(a);
    }

    static <T> T[] toArray(T... args) {
        return args;
    }

    static <T> T[] pickArray(T a) {
        return toArray(a);
    }

    public static void main(String[] args) {
        List<String> stringList = pickList("자바봄");
        String[] stringArray = pickArray("자바봄");
   }
}

In the code above, after pickList passes, executing pickArray throws a runtimeException. The message is [Ljava.lang.Object; cannot be cast to [Ljava.lang.String; — what's the problem?

The answer lies in the reifiability of Array vs. List.

Let's understand the code. Here, generic type inference goes 2 levels deep. Since the type parameter T in pickArray corresponds to String, you'd expect pickArray(String) toArray(String...) to be called. But when you actually check at runtime, it's pickArray(String), toArray(Object...) that gets called. The reason is exactly what we discussed above about generic type inference — when the parameter is unbounded, the compiler replaces it with Object first. While pickArray could infer the String type from main, toArray looks at the generic type, so it infers Object and holds that type information at runtime.

Therefore, pickArray prepares to cast to String[], but as mentioned above, the type that toArray holds at runtime is Object[], so at runtime it tries to forcefully cast like (String[]) Object[], which causes a ClassCastException. (A String array is not a subtype of an Object array, so the cast fails.)

On the other hand, calling pickList compiles successfully. The reason is that List is a non-reifiable type. The casting information is already determined at compile time, and at runtime, thanks to the erasure characteristic of generics, the Java compiler correctly adds the appropriate casting, so no casting error occurs.

To summarize, List + generics works because the casting information determined at compile time is correct. List performs the same compile-time type mismatch checking that is the advantage of generics. Arrays, on the other hand, determine type casting at runtime, which is where the problem arises.

 

That was a bit long, but the conclusion is actually simple. Generics use type erasure, which allows the compiler to infer types at compile time. To fully leverage this type inference capability, you should use List — which infers types at compile time — instead of Array — which infers types at runtime — to guarantee type safety.

 

References

https://www.baeldung.com/java-generics

Effecitve Java 3/E - Chapter 5: Generics 

https://docs.oracle.com/javase/tutorial/java/generics/erasure.html

https://www.baeldung.com/java-type-erasure

 

댓글

Comments

Daily/About Jyami

Backend Developer Resume

개발자 포트폴리오, 백엔드 개발자 레주메 (update 2020.06.12)Developer Portfolio, Backend Developer Resume (update 2020.06.12)

Backend Developer Resume

728x90

개발자 포트폴리오, 백엔드 개발자 레주메 (update 2020.06.12)

Developer Portfolio, Backend Developer Resume (update 2020.06.12)

댓글

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