핵심 요약
클라이언트에서 직접 형변환해야 하는 타입보다 제네릭 타입이 더 안전하고 쓰기 편하다. 그러니 새로운 타입을 설계할 때는 형변환 없이도 사용할 수 있도록 하라. 기존 타입 중 제네릭이었어야 하는 게 있다면 제네릭 타입으로 변경하자.
public class Stack {
private Object[] elements;
private int size = 0;
private static final int DEFAULT_INITiNAL_CAPACITY = 16;
public Stack() {
elements = new Object[DEFAULT_INITiNAL_CAPACITY];
}
public Object push(Object item) {
ensureCapacity();
elements[size++]= item;
return item;
}
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 == 0) {
elements = Arrays.copyOf(elements, 2 * size + 1);
}
}
}
위 코드의 문제점은 뭘까?
public class Main {
public static void main(String args[]) {
Stack stack = new Stack();
stack.push(1);
stack.push("2");
stack.push(3);
}
}
위 코드처럼 타입 안정성이 보장되지 않는다. 즉, 런타임에 ClassCastException이 발생할 수 있다. 따라서, 제네릭을 사용하여 타입 안정성을 보장하여야 한다.
이를 위해 타입 매개변수를 지정해주자.
public class Stack<T> {
private T[] elements;
private int size = 0;
private static final int DEFAULT_INITiNAL_CAPACITY = 16;
public Stack() {
elements = (T[])new Object[DEFAULT_INITiNAL_CAPACITY];
}
public T push(T item) {
ensureCapacity();
elements[size++]= item;
return item;
}
public T pop() {
if (size == 0) {
throw new EmptyStackException();
}
T result = elements[--size];
elements[size] = null; // 다 쓴 참조 해제
return result;
}
public boolean isEmpty() {
return size == 0;
}
private void ensureCapacity() {
if (elements.length == 0) {
elements = Arrays.copyOf(elements, 2 * size + 1);
}
}
}
public class Main {
public static void main(String args[]) {
Stack<Integer> stack = new Stack();
stack.push(1);
stack.push("2"); // 오류 발생
stack.push(3);
}
}
위 코드로 변경하면
public Stack() {
elements = (T[])new Object[DEFAULT_INITiNAL_CAPACITY];
}
위 부분에서 경고가 발생하므로, 개발자는 @SuppressWarnings로 안정성을 보장해준다.
@SuppressWarnings("unchecked")
public Stack() {
elements = (T[])new Object[DEFAULT_INITiNAL_CAPACITY];
}
해결 방법 1. Object 배열을 생성한 뒤 제네릭 배열로 형 변환
public class Stack<T> {
private T[] elements;
private int size = 0;
private static final int DEFAULT_INITiNAL_CAPACITY = 16;
@SuppressWarnings("unchecked")
public Stack() {
elements = (T[])new Object[DEFAULT_INITiNAL_CAPACITY];
}
public T push(T item) {
ensureCapacity();
elements[size++]= item;
return item;
}
public T pop() {
if (size == 0) {
throw new EmptyStackException();
}
T result = elements[--size];
elements[size] = null; // 다 쓴 참조 해제
return result;
}
public boolean isEmpty() {
return size == 0;
}
private void ensureCapacity() {
if (elements.length == 0) {
elements = Arrays.copyOf(elements, 2 * size + 1);
}
}
}
해결 방법 2. elements 필드의 타입을 E[]에서 Object[]로 변경
public class Stack<T> {
private Object[] elements;
private int size = 0;
private static final int DEFAULT_INITiNAL_CAPACITY = 16;
public Stack() {
elements = new Object[DEFAULT_INITiNAL_CAPACITY];
}
public T push(T item) {
ensureCapacity();
elements[size++]= item;
return item;
}
public T pop() {
if (size == 0) {
throw new EmptyStackException();
}
@SuppressWarnings("unchecked")
T result = (T) elements[--size];
elements[size] = null; // 다 쓴 참조 해제
return result;
}
public boolean isEmpty() {
return size == 0;
}
private void ensureCapacity() {
if (elements.length == 0) {
elements = Arrays.copyOf(elements, 2 * size + 1);
}
}
}
첫 번째 방법이 형 변환을 배열 생성 시 한 번만 해주면 되므로 더 좋아 보인다. 실제로 현업에서는 첫 번째 방법을 더 선호한다.
그럼에도 불구하고 책에서 두 번째 방법을 설명하는 이유는 뭘까?
한정적 매개변수를 사용하기 위해서이다.
유연성을 축소한다고 생각할 수 있으나, 이는 상당한 메리트가 있다. 아래 코드의 경우, Number가 제공하는 메소드를 사용할 수 있다. 즉, 확장성이 늘어난다.
세부 타입을 지정할 수 있다는 장점도 존재.
public class Stack<T extends Number> {
private Number[] elements;
private int size = 0;
private static final int DEFAULT_INITiNAL_CAPACITY = 16;
public Stack() {
elements = new Number[DEFAULT_INITiNAL_CAPACITY];
}
public T push(T item) {
ensureCapacity();
elements[size++]= item;
return item;
}
public T pop() {
if (size == 0) {
throw new EmptyStackException();
}
@SuppressWarnings("unchecked")
T result = (T) elements[--size]; // 어쩔 수 없이 반드시 캐스팅 해줘야 함
elements[size] = null; // 다 쓴 참조 해제
return result;
}
public boolean isEmpty() {
return size == 0;
}
private void ensureCapacity() {
if (elements.length == 0) {
elements = Arrays.copyOf(elements, 2 * size + 1);
}
}
}
public class Main {
public static void main(String args[]) {
Stack<Integer> stack = new Stack();
stack.push(1);
stack.push(2);
}
}
public class Main {
public static void main(String args[]) {
Stack<String> stack = new Stack(); // 불가능
stack.push(1);
stack.push(2);
}
}
위 코드처럼 만약 클래스가 위 코드처럼 한정적 타입 매개변수를 사용할 경우, 첫 번째 방식으로는 안 된다.
// 첫 번째 방식으로 한정적 타입 매개변수를 사용할 경우, 애초에 불가능
public class Stack<T extends Number> {
private T[] elements;
private int size = 0;
private static final int DEFAULT_INITiNAL_CAPACITY = 16;
@SuppressWarnings("unchecked")
public Stack() {
elements = (T[])new Object[DEFAULT_INITiNAL_CAPACITY];
}
public T push(T item) {
ensureCapacity();
elements[size++]= item;
return item;
}
public T pop() {
if (size == 0) {
throw new EmptyStackException();
}
T result = elements[--size];
elements[size] = null; // 다 쓴 참조 해제
return result;
}
public boolean isEmpty() {
return size == 0;
}
private void ensureCapacity() {
if (elements.length == 0) {
elements = Arrays.copyOf(elements, 2 * size + 1);
}
}
}
책에서는 첫 번째 방법이 힙 오염의 문제가 있다고 한다. 힙 오염이 뭘까?
자세한 거는 나중 아이템을 공부한 다음에 알 수 있으나, 지금 상태에서는 첫 번째 방법은 변질될 가능성이 있으나, 두 번째 방법은 개발자의 통제 아래에 있으므로 변질될 가능성이 없다는 느낌으로 이해하면 된다.
힙 오염(Heap Pollution) : 매개 변수 유형이 다른 서로 다른 타입을 참조할 때 생기는 문제
'Language > Java' 카테고리의 다른 글
| [effective java] 아이템 31. 한정적 와일드카드를 사용해 API 유연성을 높이라. (0) | 2023.05.14 |
|---|---|
| [effective java] 아이템 30. 이왕이면 제네릭 메서드로 만들라. (0) | 2023.05.09 |
| [effective java] 아이템 28. 배열보다는 리스트를 사용하라. (0) | 2023.05.09 |
| [effective java] 아이템 27. Unchecked Warning을 제거하라. (0) | 2023.05.09 |
| [effective java] 아이템 26. Raw 타입은 사용하지 말라. (0) | 2023.05.09 |