on
[교재 EffectiveJava] 아이템 29. 이왕이면 제네릭 타입으로 만들라
[교재 EffectiveJava] 아이템 29. 이왕이면 제네릭 타입으로 만들라
728x90
배열 코드를 제네릭 코드로 변경
제네릭 타입과 메서드를 사용하는 일은 일반적으로 쉬운 편이지만, 제네릭 타입을 새로 만드는 일은 조금 더 어렵다.
제네릭을 사용하지 않은 기본 코드
package com.java.effective.item29; import java.util.Arrays; import java.util.EmptyStackException; 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[] elements 배열이 제네릭 타입이어야 마땅하다. 제네릭으로 바꿈으로써 단점을 장점으로 바꿀 수 있다. 위 코드는 해당 코드를 호출하는 클라이언트에서 형변환을 잘못했을때 런타임 오류가 발생할 위험이 있다.
제네릭을 사용한 코드
package com.java.effective.item29; import java.util.Arrays; import java.util.EmptyStackException; public class StackGeneric { private E[] elements; private int size = 0; private static final int DEFAULT_INITIAL_CAPACITY = 16; public StackGeneric() { elements = 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 StackGeneric() { elements = new Object[DEFAULT_INITIAL_CAPACITY]; // 컴파일 에러 발생 }
E와 같은 실체화 불가 타입으로는 배열을 만들 수 없다. 배열을 사용하는 코드를 제네릭으로 바꾸려하면 이 문제가 항상 발생할 것이다.
해결책 1. 제네릭 배열 생성
public StackGeneric() { elements = (E[]) new Object[DEFAULT_INITIAL_CAPACITY]; // 컴파일 에러 발생 }
컴파일러의 오류가 사라지고 경고가 생긴다. 위 코드로 오류는 해결했지만 타입 안전하지 않다. 컴파일러는 해당 프로그램이 타입 안전한지 증명할 방법이 없지만 우리는 가능하다. 이 비검사 형변환이 프로그램의 타입 안전성을 해치지 않음을 우리가 스스로 확인해야 한다. 배열 elements 는 private 필드에 저장되고, 클라이언트로 반환되거나 다른 메서드에 전달되는 일이 거의 없다. push 메서드를 통해 배열에 저장되는 원소의 타입은 항상 E다. 따라서 이 비검사 형변환은 확실히 안전하다.
@SuppressWarnings("unchecked") 추가하여 경고 숨김처리
@SuppressWarnings("unchecked") public StackGeneric() { elements = (E[]) new Object[DEFAULT_INITIAL_CAPACITY]; // 컴파일 에러 발생 }
비검사 형변환의 안전함을 직접 증명했으므로 @SuppressWarnings("unchecked") 을 사용하여 경고를 숨긴다. 생성자 전체에 경고를 숨겨도 문제가 없다.
중요한 것은, 배열 elements 의 런타입 타입은 E[] 가 아닌 Object[] 다. (제네릭의 런타임 시점에 소거 특징)
해결책 2. element 필드의 타입을 변경
package com.java.effective.item29; import java.util.Arrays; import java.util.EmptyStackException; public class StackGeneric { private Object[] elements; private int size = 0; private static final int DEFAULT_INITIAL_CAPACITY = 16; // @SuppressWarnings("unchecked") // public StackGeneric() { // elements = (E[]) new Object[DEFAULT_INITIAL_CAPACITY]; // 컴파일 에러 발생 // } public StackGeneric() { elements = 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); } } }
elements 배열의 타입을 Object[] 로 변경했다. 또다시 오류가 발생한다.
public E pop() { if (size == 0) { throw new EmptyStackException(); } E result = elements[--size]; elements[size] = null; // 다 쓴 참조 해제 return result; }
E result 로 받는 부분이 문제다. 이 부분도 형변환으로 수정하자.
E result = (E) elements[--size];
오류는 사라지고 경고가 뜬다. E는 실체화 불가 타입이므로 컴파일러는 런타임 시에 이뤄지는 형변환이 안전한지 증명할 방법이 없다. 우리가 스스로 증명해보자. elements 배열에 원소를 추가하는 push 메서드에서 E 타입만 허용하므로 위 코드는 타입 안전하다.
@SuppressWarnings("unchecked") E result = (E) elements[--size];
이번엔 해당 코드의 row 바로 위에 @SuppressWarnings("unchecked") 를 추가하였다.
해결책1, 해결책2 정리
해결책 1의 방법으 가독성이 더 좋다. 배열의 타입을 E[] 로 선언하여 오직 E 타입 인스턴스만 받음을 확실히 어필한다. 코드도 더 짧다. 또한 형변환도 생성자에 선언되어 있으므로 배열 생성시 단 한번만 실행된다. 해결책 2의 방법은 원소를 읽을때마다 형변환이 발생한다. 하지만 해결책 1의 경우, 배열의 런타임 타입이 컴파일타임 타입과 달라 힙 오염이 발생한다. (E 가 Object 가 아닐 경우)
package com.java.effective.item29; import java.util.Arrays; public class Main { public static void main(String[] args) { StackGeneric stack = new StackGeneric<>(); Arrays.stream(args).forEach(stack::push); while (!stack.isEmpty()) { System.out.println(stack.pop().toUpperCase()); } } }
Stack 에서 꺼낸 원소에서 String 의 toUpperCase 메서드를 호출할때 명시적 형변환을 수행하지 않았다. (컴파일러에 의해 자동 생성된) 이 형변환이 항상 성공함을 보장한다.
StackGeneric, StackGeneric, StackGeneric>, StackGeneric 등 어떤 참조 타입으로도 StackGeneric 인스턴스를 생성할 수 있다. 단, 기본 타입은 사용할 수 없다. 자바 제네릭 타입 시스템은 기본 타입을 사용할 수 없다. (Stack, Stack 등)
한정적 타입 매개변수 (bounded type parameter)
class DelayQueue implements BlockingQueue
Delayed 의 하위 타입만 받는다는 의미이다. 이렇게 함으로써 DelayQueue 자신과 DelayQueue 를 사용하는 클라이언트는 DelayQueue 의 원소에서 형변환 없이 곧바로 Delayed 클래스의 메서드를 호출할 수 있다. 모든 타입은 자기 자신의 하위 타입이므로 DelayQueue 로도 사용할 수 있다.
from http://devfunny.tistory.com/575 by ccl(A) rewrite - 2021-10-31 14:01:29