어떤 클래스를 만들다 보면 클래스안에 어떤 다른 객체를 담는 역할을하는 클래스를 만들 수 있습니다. 예를 들어 스택이라는 자료구조안에 어떤 Element들을 쌓는경우가 생기는데 이런경우 보통 제네릭타입으로 만들면 유용합니다. 특히 Object타입으로 담고있다면 더더욱 명확하게 제네릭을 사용해서 더 구체적인 타입으로 코딩을 하도록 유도하면 런타임시에 ClassCastException을 많이 줄여줄 수 있습니다.
Object를 이용한 제네릭 스택
package me.whiteship.chapter05.item29.object;
import me.whiteship.chapter05.item29.EmptyStackException;
import java.util.Arrays;
import java.util.List;
// Object를 이용한 제네릭 스택 (170-174쪽)
public class Stack {
private Object[] elements;
private int size = 0;
private static final int DEFAULT_INITIAL_CAPACITY = 16;
public Stack() {
elements = new Object[DEFAULT_INITIAL_CAPACITY];
}
public void push(Object e) {
ensureCapacity();
elements[size++] = e;
}
public Object pop() {
if (size == 0)
throw new EmptyStackException();
Object 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);
}
}
위에 구현한 스택은 Object타입으로 요소들을 받고 있기 때문에 아래와 같이 pop을 할때 Object타입이기 때문에 넣어줬던 타입에 맞게 강제로 타입캐스팅을 해야합니다.
public static void main(String[] args) {
Stack stack = new Stack();
for (String arg : List.of("a", "b", "c")) {
stack.push(arg);
}
while (!stack.isEmpty()) {
System.out.println(((String) stack.pop()).toUpperCase());
}
}
이렇게 강제 타입캐스팅을 하면 String타입이 아니고 다른 타입을 넣었을경우 ClassCastException이 일어날 수 있습니다. 이런 경우를 미연에 방지하기 위해 제네릭으로 스택을 구현하는게 좋은데 2가지 방법이 있습니다.
E[]를 이용한 제네릭 스택
package me.whiteship.chapter05.item29.technqiue1;
import me.whiteship.chapter05.item29.EmptyStackException;
import java.util.Arrays;
import java.util.List;
// E[]를 이용한 제네릭 스택 (170-174쪽)
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")
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);
}
}
public class Stack<E> {
private E[] elements;
}
첫번째 방법은 제네릭 타입을 선언하고 Object대신에 제네릭타입의 배열을 선언하고 선언한 변수에 오브젝트 타입의 배열을 생성합니다.
@SuppressWarnings("unchecked")
public Stack() {
elements = (E[]) new Object[DEFAULT_INITIAL_CAPACITY];
}
이렇게 Object타입의 배열을 선언하는 이유는 new E[DEFAULT_INITIAL_CAPACITY] 이런식으로 생성할 수 없고 컴파일시에 에러가 발생합니다. 따라서 Object배열로 선언 후 제네릭 배열타입으로 타입캐스팅을 하는 방법이 있습니다. 대신 런타임에는 타입캐스팅하는 (E[])부분이 소거되기때문에 Object타입의 배열로 동작합니다. 대신에 pop을 할때 지정한 타입으로 캐스팅없이 바로 꺼낼 수 있다는 장점이 있고 (E[]) new Object[DEFAULT_INITIAL_CAPACITY]; 이 부분에서 런타임경고가 뜨는데 SuppressWarnings어노테이션으로 경고를 무시할 수 있습니다. 하지만 힙오염이 될 가능성이 있습니다. 힙 오염은 아이템 32에서 자세히 다뤄보도록 하겠습니다.
Object[]를 이용한 제네릭 Stack
package me.whiteship.chapter05.item29.technqiue2;
import me.whiteship.chapter05.item29.EmptyStackException;
import java.util.Arrays;
import java.util.List;
// Object[]를 이용한 제네릭 Stack (170-174쪽)
public class Stack<E> {
private Object[] elements;
private int size = 0;
private static final int DEFAULT_INITIAL_CAPACITY = 16;
public Stack() {
elements = new Object[DEFAULT_INITIAL_CAPACITY];
}
public void push(E e) {
ensureCapacity();
elements[size++] = e;
}
// 코드 29-4 배열을 사용한 코드를 제네릭으로 만드는 방법 2 (173쪽)
// 비검사 경고를 적절히 숨긴다.
public E pop() {
if (size == 0)
throw new EmptyStackException();
// push에서 E 타입만 허용하므로 이 형변환은 안전하다.
@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);
}
}
힙 오염을 방지하려면 제네릭배열이아니라 Object배열로 선언 합니다.
private Object[] elements;
public Stack() {
elements = new Object[DEFAULT_INITIAL_CAPACITY];
}
그 대신에 제네릭타입으로만 push를 받도록 합니다.
public void push(E e) {
ensureCapacity();
elements[size++] = e;
}
pop을 할때 제네릭타입으로 형변환을 합니다.
public E pop() {
if (size == 0)
throw new EmptyStackException();
// push에서 E 타입만 허용하므로 이 형변환은 안전하다.
@SuppressWarnings("unchecked") E result = (E) elements[--size];
elements[size] = null; // 다 쓴 참조 해제
return result;
}
이러한 방법의 장점은 힙 오염이 발생할 여지가 없습니다. 하지만 무언가를 꺼낼때마다 해당하는 타입으로 형변환을 해줘야합니다.
'개발관련 서적 정리 > Effective Java' 카테고리의 다른 글
아이템 32. 제네릭과 가변인수를 함께 쓸 때는 신중하라 (0) | 2023.02.05 |
---|---|
아이템 30. 이왕이면 제네릭 메서드로 만들라 (0) | 2023.02.05 |
아이템 28. 배열보다는 리스트를 사용하라 (0) | 2023.02.05 |
아이템 25. 톱 레벨 클래스는 한 파일에 하나만 담으라 (0) | 2023.02.05 |
아이템 24. 멤버 클래스는 되도록 static으로 만들라 (0) | 2023.02.05 |