책, 세미나, 컨퍼런스 후기/2022

Effective Java 3/E - 5장 제네릭 - 1

TwinParadox 2022. 2. 5. 18:36
728x90

26.  Raw Type은 사용하지 말라

  • 클래스와 인터페이스 선언에 타입 매개변수가 쓰이면 제네릭 클래스/인터페이스라고 한다.
  • 제네릭 클래스와 인터페이스를 합쳐 제네릭 타입이라고 한다.
    • 각 제네릭 타입은 매개변수화 타입을 정의하며, <> 안에 실제 타입 매개변수를 나열한다.
    • 제네릭 타입을 하나 정의하면 Raw 타입, 타입 매개변수를 전혀 사용하지 타입이 정의된다.

 

Raw Type 사용으로 인해, 제네릭이 가져오는 안전성과 표현력을 모두 상실한다.

  • List<Object>와 List는 엄연히 다르다.
    • List<Object>는 컴파일러에 모든 타입 허용을 명확히 전달한 것
    • List는 제네릭에서 완전히 벗어난 것
    • List<Integer>는 Raw Type의 하위 타입이지만, 타입을 명시한 List<Object>의 하위는 아님
  • 원소의 타입을 몰라도 되는 경우가 필요하다면 List<?>처럼 비한정적 와일드카드 타입을 사용하자.

 

Raw Type이 필요한 경우

  • class 리터럴
  • instanceof

 

 

27. 비검사 경고를 제거하라

가능한 모든 비검사 경고를 제거해야 한다.

  • 컴파일 단계에서 경고(Warning)을 보기 위해 다음 옵션을 추가한다.
  • -Xlint:uncheck
  • 경고를 모두 제거하면, 타입 안전성을 확보할 수 있어 런타임에 ClassCastException 문제에서 자유로워진다.
  • 경고 제거가 불가능하지만, 타입 안전성이 확실하다면 @SuppressWarnings을 추가한다. 단, 어노테이션은 최대한 좁은 범위에 적용해야 하며, 무시한 근거에 대한 주석 작성 필요
@SuppressWarnings("unchecked")
public Stack() {
    this.elements = (E[]) new Object[DEFAULT_CAPACITY];
}

 

 

 

28. 배열보다는 리스트를 사용하라

배열과 제네릭타입의 차이

  • 배열은 공변이고, 제네릭은 불공변이다.
  • 배열은 실체화되어, 런타임에도 원소의 타입을 확인하지만, 제네릭 타입은 런타임에는 해당 정보가 없다.

 

List<E>, List<Integer>와 List<?>의 차이

  • List<E>, List<Intger>는 실체화 불가 타입으로, 런타임에는 컴파일 타임보다 타입 정보가 적다
  • List<?>는 실체화가 가능하다.
  • 배열을 비한정적 와일드카드 타입으로 생성은 가능해도, 유용하지 않다.

 

배열과 제네릭을 섞어 쓰기 어렵다.

둘을 섞어 사용하면서 생성 오류 및 비검사 형변환 경고가 뜨는 경우, E[]를 List<E>로 바꾸는 것만으로도 쉽게 해결이 가능하며, 약간의 성능 손실로, 타입 안전성과 상호운용성을 챙길 수 있다.

 

 

29. 이왕이면 제네릭 타입으로 만들라

일반 클래스를 제네릭으로 만들기 위해 클래스 선언에 타입 매개변수를 추가하라.

public class Stack {
	private Object[] elements;
	private int size;
	public static final int DEFAULT_CAPACITY = 16;

	public Stack() {
		this.elements = new Object[DEFAULT_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;
	}
}

 

 

Object 배열 생성 후 제네릭 배열로 형변환 제네릭 배열 생성을 금지하는 제약을 우회하기

  • 컴파일러 오류 대신 경고로 바뀌지만, 이런 경우 일반적으로 타입 안전성이 떨어진다.
  • 타입 안전성을 증명한 후, @SuppressWarnings로 경고를 숨긴다,
  • 배열 타입의 선언을 통해 E 타입만 받음을 명시하여 가독성이 더 좋고, 코드도 짧다.
public class Stack<E> {
	private E[] elements;
	private int size;
	public static final int DEFAULT_CAPACITY = 16;

	@SuppressWarnings("unchecked")
	public Stack() {
		this.elements = (E[]) new Object[DEFAULT_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;
	}
}

 

필드의 타입을 실체화 불가 타입의 배열(E[])에서 Object 배열로 변경

  • 배열이 반환한 원소를 E로 형변환한다.
  • 컴파일러가 E에 대해 런타임에 형변환에 대한 안전성을 확인할 수 없다.
  • 타입 안전성을 검증한 후, @SuppressWarning로 경고를 숨긴다.
  • 배열에서 원소를 사용할 때마다 형변환이 필요하다.
  • 배열의 런타임 타입이 컴파일타임 타입의 다름으로 인해 힙 오염이 발생하는 것을 방지하고 자하는 경우, 이 방법을 사용한다.
public class Stack<E> {
	private Object[] elements;
	private int size;
	public static final int DEFAULT_CAPACITY = 16;

	public Stack() {
		this.elements = new Object[DEFAULT_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;
	}
}

 

 

 

 

728x90