[아이템 29] 이왕이면 제네릭 타입으로 만들라

gang_shik·2022년 5월 16일
0

Effective Java 5장

목록 보기
4/6
// Object 기반 스택 - 제네릭이 절실해 보임
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를 적절한 타입 매개변수로 바꾸면 됨

public class Stack<E> {
		private E[] elements;
		private int size = 0;
		private static final int DEFAULT_INITIAL_CAPACITY = 16;

		public Stack() {
				elements = new E[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);
		}
}
  • 하지만 오류가 나는데 왜냐하면 E 와 같은 실체화 불가 타입으로는 배열을 만들 수 없기 때문임

  • 배열을 사용하는 코드를 제네릭으로 만들려 할 때 생기는 문제들임, 이를 해결하기 위해서 2가지 정도가 있음

  • 첫 번째는 제네릭 배열 생성을 금지하는 제약을 대놓고 우회하는 방법임, 이 방법으로는 Object 배열 생성 이후 제네릭 배열로 형변환을 하는것, 타입 안전하진 않음

  • 그래서 이 비검사 형변환이 프로그램의 타입 안전성을 해치지 않음을 우리 스스로 확인해야함

  • 그렇게 비검사 형변환이 안전함을 직접 증명했다면 범위를 최소로 좁혀 애너테이션으로 경고를 숨김, 생성자 전체에 경고를 숨겨도 좋음

// 배열 elements는 push(E)로 넘어온 E 인스턴스만 담는다
// 따라서 타입 안전성을 보장하지만,
// 이 배열의 런타임 타입은 E[]가 아닌 Object[]임
@SuppressWarnings("unchecked")
public Stack() {
		elements = (E[]) new Object[DEFAULT_INITIAL_CAPACITY];
}
  • 이를 해결하는 두 번째 방법elements 필드의 타입을 E[] 에서 Object[] 로 바꾸는 것임

  • E 는 실체화 불가 타입이므로 컴파일러는 런타임에 이뤄지는 형변환이 안전한지 증명할 방법이 없음

  • 이번에도 비검사 형변환을 수행하는 곳에 대해서만 우리가 직접 증명하고 경고를 숨길 수 있음

// 비검사 경고를 적절히 숨김
public E pop() {
		if (size == 0)
				throw new EmptyStackException();

		// push에서 E 타입만 허용하므로 이 형변환은 안전함
		@SuppressWarnings("unchecked") E result = (E) elements[--size];

		elements[size] = null; // 다 쓴 참조 해제
		return result;
}
  • 위 두 방법 모두 제네릭 배열 생성 제거하는 방법으로 잘 쓰임

  • 이 중 첫 번째 방식이 가독성이 좋고 짧지만 배열의 런타임 타입이 컴파일타임 타입과 달라 힙 오염을 일으킴, 그런 경우 두 번째 방식이 나음

  • 아래와 같이 사용할 수 있음, 이 때 명시적 형변환을 수행하지 않아도 성공함을 알 수 있음

public static void main(String[] args) {
		Stack<String> stack = new Stack<>();
		for (String arg : args)
				stack.push(arg);
		while (!stack.isEmpty())
				System.out.println(stack.pop().toUpperCase());
}
  • 이를 보면 마치 아이템 28과 모순돼 보이지만 사실 제네릭 타입 안에서 리스트를 사용할는 게 항상 가능하지도 꼭 더 좋은 것도 아님

힙 오염?

힙오염

public class HeapPollutionEx {
    public static void main(String[] args) {
        List<String> strings1 = Arrays.asList("첫 요소");
        List<String> strings2 = Arrays.asList("첫 요소");
        doSomthing(strings1, strings2);
    }

    private static void doSomthing(List<String> ... stringLists) {
        List<Integer> intList = Arrays.asList(42);
        Object[] objects = stringLists;
        objects[0] = intList; // 힙 오염 발생
        String s = stringLists[0].get(0); // ClassCastException
    }
}

위의 예시가 힙오염의 사례라고 볼 수 있음

제네릭과 매개변수화 타입은 실체화 되지 않는 실체화 불가 타입인데 이 실체화 불가 타입은 런타임에 컴파일타임보다 타입 관련 정보를 적게 담고 있는 것을 말함

doSomething 메서드의 인자로 들어온 가변 인자는 List<String> 배열인데 Object[] 배열 변수로 가변인자를 초기화하고 인덱스 0의 요소를 List<Integer> 타입의 변수로 초기화함

이 때 힙오염이 발생하는데 이 말은 해당 배열에 다른 타입이 두 가지가 존재함을 의미함

이런 상황을 힙오염이라고 볼 수 있음


  • 자바가 리스트를 기본 타입으로 제공하지 않으므로 ArrayList 같은 제네릭 타입도 결국은 기본 타입인 배열을 사용해 구현해야함

  • Stack 의 예처럼 대다수의 제네릭 타입은 타입 매개변수에 아무런 제약을 두지 않음, 어떤 참조 타입으로도 만들 수 있음 단 기본 타입은 사용할 수 없음

  • 이 기본 타입을 사용할 수 없는 것은 자바 제네릭 타입 시스템의 근본적인 문제이나 박싱된 기본 타입을 사용해 우회할 수 있음

  • 타입 매개변수에 제약을 두는 제네릭 타입도 있음

  • 예를 들어 class DelayQueue<E extends Delayed> implements BlockingQueue<E> 의 경우 타입 매개변수 목록인 <E extends Delayed>Delayed 하위 타입만 받는다는 뜻임

  • 이러한 타입 매개변수 E한정적 타입 매개변수라 함

한정적 타입 매개변수?

한정적 타입 매개변수

말 그대로 한정적 타입 매개변수, 매개변수에 제약 조건을 두는 것임, 예시의 경우 EDelayed의 하위타입만 받아서 해당 형변환 없이 활용도 가능하고 딱 제한을 해버리는 것을 의미함

복잡할 것 없이 타입을 한정한다는 것을 의미하고 이를 통해 형변환 없이 유용하게 쓸 수 있음

다른 예시

profile
측정할 수 없으면 관리할 수 없고, 관리할 수 없으면 개선시킬 수도 없다

0개의 댓글