이펙티브 자바 아이템 29 - 이왕이면 제네릭 타입으로 만들라 - 완벽 공략
아이템 29 - 이왕이면 제네릭 타입으로 만들라 - 완벽 공략
이 글은 백기선 님의 이펙티브 자바 강의와 이펙티브 자바 3 / E 편을 참고하여 작성하였습니다.
한정적 타입 매개변수
한정적 타입 매개변수는 제네릭 타입을 특정 타입으로 한정지을 수 있는 기능이다.
public class Stack<E> {
private E[] 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")
@SuppressWarnings("unchecked")
public Stack() {
elements = (E[]) new Object[DEFAULT_INITIAL_CAPACITY];
}
public void push(E e) {
ensureCapacity();
elements[size++] = e;
}
public E pop() {
if (size == 0)
throw new EmptyStackException();
E result = elements[--size];
elements[size] = null; // 다 쓴 참조 해제
return result;
}
public boolean isEmpty() {
return size == 0;
}
private void ensureCapacity() {
if (elements.length == size)
elements = Arrays.copyOf(elements, 2 * size + 1);
}
// 코드 29-5 제네릭 Stack을 사용하는 맛보기 프로그램 (174쪽)
public static void main(String[] args) {
Stack<String> stack = new Stack<>();
for (String arg : List.of("a", "b", "c"))
stack.push(arg);
while (!stack.isEmpty())
System.out.println(stack.pop().toUpperCase());
}
}
Stack
클래스를 컴파일 할 때 E
라는 타입은 Object
로 바뀌게 된다.
우리가 만약 타입을 한정짓고 싶다면
예를들어 Stack
클래스는 숫자만 받을 수 있도록하고 싶다면
public class Stack<E extends Number> {
private E[] 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")
@SuppressWarnings("unchecked")
public Stack() {
elements = (E[]) new Object[DEFAULT_INITIAL_CAPACITY];
}
public void push(E e) {
ensureCapacity();
elements[size++] = e;
}
public E pop() {
if (size == 0)
throw new EmptyStackException();
E result = elements[--size];
elements[size] = null; // 다 쓴 참조 해제
return result;
}
public boolean isEmpty() {
return size == 0;
}
private void ensureCapacity() {
if (elements.length == size)
elements = Arrays.copyOf(elements, 2 * size + 1);
}
// 코드 29-5 제네릭 Stack을 사용하는 맛보기 프로그램 (174쪽)
public static void main(String[] args) {
Stack<String> stack = new Stack<>();
for (String arg : List.of("a", "b", "c"))
stack.push(arg);
while (!stack.isEmpty())
System.out.println(stack.pop().toUpperCase());
}
}
위와같이 Stack<E extends Number>
로 작성한다.
이렇게하면 Number
라는 클래스 또는 인터페이스를 구현하거나 상속한 클래스들로만 제한이된다.
Number
를 확장한 모든것들로 제한 이 된다.
Integer
는 포함이 되지만 String
은 포함되지 않는다.
public class Main {
public static void main(String[] args) {
Stack<String> stack = new Stack<>(); // 컴파일 에러
for (String arg : List.of("a", "b", "c"))
stack.push(arg);
while (!stack.isEmpty())
System.out.println(stack.pop().toUpperCase());
}
}
Stack<E extends Number>
클래스에 String
을 넣으려고하면 컴파일 에러가 발생한다.
public class Main {
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());
}
}
하지만 위처럼 Integer
로 바꾼다해서 코드는 잘 동작하지 않는다.
public class Stack<E extends Number> {
private E[] 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")
@SuppressWarnings("unchecked")
public Stack() {
elements = (E[]) new Object[DEFAULT_INITIAL_CAPACITY];
}
public void push(E e) {
ensureCapacity();
elements[size++] = e;
}
public E pop() {
if (size == 0)
throw new EmptyStackException();
E result = elements[--size];
elements[size] = null; // 다 쓴 참조 해제
return result;
}
public boolean isEmpty() {
return size == 0;
}
private void ensureCapacity() {
if (elements.length == size)
elements = Arrays.copyOf(elements, 2 * size + 1);
}
// 코드 29-5 제네릭 Stack을 사용하는 맛보기 프로그램 (174쪽)
public static void main(String[] args) {
Stack<String> stack = new Stack<>();
for (String arg : List.of("a", "b", "c"))
stack.push(arg);
while (!stack.isEmpty())
System.out.println(stack.pop().toUpperCase());
}
}
private E[] elements; // 이 부분이 Number 의 배열로 변환
Stack<E extends Number>
로 타입을 제한하고 나면 기존의 Obejct
배열이
Number
의 배열로 바뀐다.
@SuppressWarnings("unchecked")
public Stack() {
elements = (E[]) new Object[DEFAULT_INITIAL_CAPACITY];
}
위의 부분도 Number
의 배열로 캐스팅된다.
Stack<E>
를 컴파일 되었을 때 E
가 Object
로 바뀌었다면
Stack<E extends Number>
를 컴파일 하면 E
가 Number
로 바뀌게 된다.
push()
나 pop()
역시 E
가 Number
로 바뀌게 된다.
public class Main {
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());
}
}
때문에 위의 코드에서는 Object
배열을 Number
의 배열로 바꾸려고 해서 에러가 난 것이다.
Object
배열은 아무거나 넣을 수 있기 때문에 Number
의 배열로 바꾸는게 위험하다.
반대로 Number
의 배열을 Object
배열로 바뀌는 건 가능하다.
공변이기 때문이고, 모든 Number
는 Object
를 상속받았기 때문이다.
// E[]를 이용한 제네릭 스택 (170-174쪽)
public class Stack<E extends Number> {
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() {
elements = new Number[DEFAULT_INITIAL_CAPACITY];
}
public void push(E e) {
ensureCapacity();
elements[size++] = e;
}
public E pop() {
if (size == 0)
throw new EmptyStackException();
@SuppressWarnings("unchecked") E result = (E)elements[--size];
elements[size] = null; // 다 쓴 참조 해제
return result;
}
public boolean isEmpty() {
return size == 0;
}
private void ensureCapacity() {
if (elements.length == size)
elements = Arrays.copyOf(elements, 2 * size + 1);
}
}
위와같이 배열을 Number
의 배열로 선언하고 값을 꺼낼 때 E
로 캐스팅하면 해결된다.
이때 경고 메세지가 발생하는데 해당 배열에 들어가는 값은 전부 E
타입이 보장되므로
@SuppressWarnings("unchecked")
를 사용해 경고 메세지를 무시할 수 있다.
이때 @SuppressWarnings("unchecked")
은 가장 작은 단위로 사용한다.
이렇게 한정적 타입 매개변수를 사용하면 제한한 타입의 인스턴스를 만들거나 메서드를 호출할 수도 있다.
public void push(E e) {
ensureCapacity();
elements[size++] = e;
}
push(E e)
메서드는 E
라는 타입으로 매개변수를 받지만
해당 매개변수를 통해 Number
클래스가 가진 모든 메서드들을 사용할 수 있다.
타입 한정에는 여러가지 클래스나 인터페이스를 다수로 선언할 수 있다.
이때 받는 타입은 선언한 모든 것들을 구현하고 있어야한다.
public class Stack<E extends Number & Serializable> {
}
예를들어 위처럼 선언하면 실제타입 매개변수는 Number
와 Serializable
을 전부 상속하고 구현하고 있어야한다.
이렇게 다수의 클래스와 인터페이스를 선언할 때는 클래스를 먼저 적어야한다.