들어가기 전
이 글은 인프런 백기선님의 이펙티브 자바 강의를 복습하며 작성한 글입니다.
이 글의 요약
- 한정적 타입 매개변수를 이용해서 특정한 타입만 제네릭에서 사용할 수 있다.
- <E>인 경우 E[]는 컴파일 이후 Object[]로 변한다.
- <E extends Number>인 경우 E[]는 컴파일 이후 Number[]로 변한다.
- <E extends Number>일 때 (E[]) new Object[]를 하게 되면 classCastException이 발생한다.
- Object[]을 Number[]로 캐스팅하는 것인데 이 형식의 다운 캐스팅은 불가능하다.
- 한정적 타입 매개변수를 사용하고, 배열인 경우라면 E[]가 아니라 Number[] 형식으로 선언해준다.
- 한정적 타입 매개변수를 사용하면, 해당 매개변수의 최상위 타입이 제공하는 메서드는 사용 가능하다.
- 한정적 타입 매개변수의 조건을 여러개로 사용할 수 있다.
- <E extends Number & Serializable)
완벽 공략 43. 한정적 타입 매개변수 (Bounded Type Parameters)
- 매개변수화 타입을 특정한 타입으로 한정짓고 싶을 때 사용할 수 있다.
- <E extends Number>, 선언할 수 있는 제네릭 타입을 Number를 상속(extends) 했거나 구현한(implements)한 클래스로 제한한다.
- 한정적 타입 매개변수로 제한한 타입의 인스턴스를 만들거나, 메서드를 호출할 수도 있다.
- <E extends Number>, Number 타입이 제공하는 메서드를 사용할 수 있다.
- 한정적 타입 매개변수는 다수의 타입으로 한정 할 수 있다. 이 때 클래스 타입을 가장 먼저 선언해야 한다.
- <E extends Number & Serializable>, 선언할 제네릭 타입은 Integer와 Number를 모두 상속 또는 구현한 타입이어야 한다.
이 글에서는 한정적 타입 매개변수의 사용 예시를 좀 더 자세히 살펴보고자 한다.
매개변수화 타입을 특정한 타입으로 한정짓고 싶을 때 사용할 수 있음.
매개변수화 타입을 특정한 타입으로 한정할 수 있는데, 주로 <E extends Number> 같은 형태로 사용한다. 그런데 이 타입 한정 제네릭이 바이트 코드에서 어떻게 동작하는지를 알아야 제네릭 사용 시 발생하는 문제들을 잘 해결할 수 있다. 먼저 아래 코드를 살펴보자. 아래의 Stack<E> 클래의 elements 필드를 살펴보자. 여기서 E[]은 컴파일되면 어떻게 될까?
- 바이트 코드에서는 E[] → Object[]가 된다.
public class Stack<E> {
private E[] elements;
private int size = 0;
...
}
이것은 <E>에 어떠한 타입도 들어올 수 있어서 사실상 Object랑 같이 때문에 컴파일러가 Object[]로 컴파일 해주는 것이다. 이어서 인텔리제이를 이용해서 elemtnts가 정말로 Object[] 인지를 살펴본다.
- 바이트 코드를 보면 [LJava/lang/Object; elements로 선언되어있다. L은 배열인데, Object로 되어있다. 따라서 elements는 바이트코드에서 Object 배열로 선언되어있다.
타입 한정 하기
제네릭 <E>를 사용하면 어떠한 타입이든 들어올 수 있는 상황이다. 그런데 Number 클래스와 관련된 클래스만 들어올 수 있도록 하고 싶다면 <E Extends Number>를 이용해서 타입을 한정 지을 수 있다. 그렇다면 <E Extends Number>로 선언하면 어떤 바이트코드가 생성될까?
public class Stack<E extends Number> {
private E[] elements;
private int size = 0;
...
}
아래 코드에서 elements의 타입이 무엇인지를 살펴보자.
- elements의 타입은 [Ljava/lang/Number가 된다. 따라서 제네릭 E는 바이트 코드에서 Number[]로 변환된다.
- 즉, 이 과정은 E[] elements → Number[] element로 변환시킨다.
배열을 생성하는 부분도 살펴보자. 배열을 생성하는 코드는 다음과 같다.
@SuppressWarnings("unchecked")
public Stack() {
this.elements = (E[]) new Object[DEFAULT_INITIAL_CAPACITY];
}
배열 생성하는 부분의 바이트 코드는 다음과 같이 바뀐다. 아래에서 보면 이렇다.
- ANEWARRAY : Object 타입의 배열을 생성한다
- CHECKCAST : LJava/lang/Number. 생성된 Object[]를 Number[]로 변경한다.
Object[] 배열을 만들고, 이것을 Number[]로 캐스팅 한다. 이것은 (E[])를 이용해서 Object 배열을 변경하기 때문이다. 한정적 타입 매개변수를 쓰면 배열을 생성하는 과정에서도 캐스팅이 발생한다.
따라서 한정적 타입 매개변수를 사용하면 다음과 같이 제네릭의 타입이 변한다는 것을 알 수 있다.
- <T>인 경우, T는 Object로 치환된다. 왜냐하면 아무거나 들어와도 되기 때문이다
- <T extends Numer>인 경우, T는 Number로 치환된다. 왜냐하면 Number 하위 타입만 들어와야하기 때문이다.
코드 실행 시, ClassCastException 에러 발생
아래 Main 메서드를 실행하면 에러가 발생한다. 어떤 에러가 발생할까?
public class Stack<E extends Number> {
private E[] elements;
@SuppressWarnings("unchecked")
public Stack() {
this.elements = (E[]) new Object[DEFAULT_INITIAL_CAPACITY];
}
...
// 코드 29-5 제네릭 Stack을 사용하는 맛보기 프로그램 (174쪽)
public static void main(String[] args) {
Stack<Integer> stack = new Stack<>(); // 여기서 에러 발생
for (Integer arg : List.of(1, 2, 3))
stack.push(arg);
while (!stack.isEmpty())
System.out.println(stack.pop());
}
}
바로 아래와 같이 ClassCastException이 발생한다. 에러의 내용은 다음과 같다
Exception in thread "main" java.lang.ClassCastException: class [Ljava.lang.Object; cannot be cast to class [Ljava.lang.Number; ([Ljava.lang.Object; and [Ljava.lang.Number; are in module java.base of loader 'bootstrap')
at com.example.effectivejava1.chapter29.bounded_type.Stack.<init>(Stack.java:22)
at com.example.effectivejava1.chapter29.bounded_type.Stack.main(Stack.java:49)
이 에러는 Object[]를 Number[]로 다운 캐스팅 할 수 없기 때문에 발생한다. 그렇다면 어째서 이런 에러가 발생했을까? 이유는 제네릭과 바이트 코드에 감추어져 있다.
- E[]는 컴파일 이후에 Number[]이 된다.
- Object[]을 생성하고 E[]로 다운 캐스팅할 때는 Number[]로 바꾸는 것인데, Object → Number로는 다운 캐스팅이 안된다.
따라서 에러가 발생하는 것이다. 이전에 한정적 타입 매개변수를 사용하지 않았을 때는 문제가 없다. 왜냐하면 Object[] 배열을 생성했고, <E>는 모든 타입이 들어올 수 있기 때문에 사실상 Object였다. 따라서 E[]는 Object[] 였고, Object 배열을 생성 후 E[]로 형변환은 당연히 문제 없이 가능했다.
public class Stack<E extends Number> {
// E[]는 컴파일 이후 Number[]로 변환됨.
private E[] elements;
@SuppressWarnings("unchecked")
public Stack() {
// 처음 생성된 배열은 Object[]
// Object[]을 Number[] 으로 변환하는 작업. 그런데 다운 캐스팅이 안됨.
this.elements = (E[]) new Object[DEFAULT_INITIAL_CAPACITY];
}
]
제네릭에서 발생하는 ClassCastException 해결방법
- 배열은 E[]가 아니라 Number[]로 선언해줘야한다. E[]로 선언하면 컴파일 이후에 Number[]로 변환된다. 그러니 처음부터 Number[]로 선언하는 것을 추천한다.
- 배열을 생성할 때 Number[]로 바로 생성하고, 따로 형변환을 하지 않는다. 애초에 E[]는 컴파일 이후 Number[]가 되기 때문이다. Object[]을 생성하고 Number나 E로 타입 변환은 불가능하다. 다운캐스팅이기 때문이다.
- 데이터를 꺼낼 때 (pop 메서드)에서는 형변환을 해서 사용하도록 한다. Number에는 Integer, Double 등이 있을 수 있기 때문이다. 즉, E에는 Interger, Double 등이 올 수 있기 때문에 Number → Integer, Double 등으로 변환해서 사용한다.
public class Stack<E extends Number> {
// ClassCastException 발생
// private E[] elements;
private Number[] elements;
private int size = 0;
private static final int DEFAULT_INITIAL_CAPACITY = 16;
// 코드 29-3 배열을 사용한 코드를 제네릭으로 만드는 방법 1 (172쪽)
// 배열 elements는 push(E)로 넘어온 E 인스턴스만 담는다.
// 따라서 타입 안정성을 보장하지만,
// 이 배열의 런타임 타입은 E[]가 아닌 Object[]다!
@SuppressWarnings("unchecked")
public Stack() {
// this.elements = (E[]) new Object[DEFAULT_INITIAL_CAPACITY]; // E[] -> Number[]로 변경
this.elements = new Number[DEFAULT_INITIAL_CAPACITY];
}
...
public E pop() {
if (size == 0)
throw new EmptyStackException();
@SuppressWarnings("unchecked") E result = (E) elements[--size];
elements[size] = null; // 다 쓴 참조 해제
return result;
}
...
// 코드 29-5 제네릭 Stack을 사용하는 맛보기 프로그램 (174쪽)
public static void main(String[] args) {
Stack<Integer> stack = new Stack<>();
for (Integer arg : List.of(1, 2, 3))
stack.push(arg);
while (!stack.isEmpty())
System.out.println(stack.pop());
}
}
한정적 매개변수 타입의 인스턴스를 만들거나, 메서드를 호출할 수도 있다.
E가 Number의 하위 클래스로 제네릭 선언되어있으면, E 타입이 제공하는 인스턴스를 생성하거나 메서드를 호출할 수도 있다. 예를 들어 아래와 같이 e.byteValue()를 호출할 수 있다. 이것은 E의 최상위 클래스 Number에서 byteValue()라는 메서드를 제공해주기 때문이다.
public class Stack<E extends Number> {
public void push(E e) {
e.byteValue() // E 타입 인스턴스에서 제공하는 메서드.
ensureCapacity();
elements[size++] = e;
}
}
다수의 타입으로 한정 할 수 있다. (클래스 타입을 가장 먼저 선언해야 한다.)
제네릭을 쓸 때 다수의 타입으로 한정할 수도 있다. 예를 들면 아래와 같이 사용할 수 있다.
public class Stack<E extends Number & Serializable> {
...
}
아래의 내용은 Number / Serializable 클래스를 모두 구현 / 상속받은 클래스만으로 타입을 제한하겠다는 것이다.
'프로그래밍 언어 > JAVA' 카테고리의 다른 글
Effective Java : 아이템31. 완벽공략 44. 타입추론 (0) | 2023.04.19 |
---|---|
Effective Java : 아이템30. 이왕이면 제네릭 메서드로 만들라 (0) | 2023.04.18 |
Effective Java : 아이템29. 이왕이면 제네릭 타입으로 만들라. (0) | 2023.04.18 |
Effective Java : 아이템28. 완벽공략 42. @SafeVarargs (0) | 2023.04.18 |
Effective Java : 아이템31. 한정적 와일드카드를 사용해 API 유연성을 높여라 (0) | 2023.04.18 |