Effective Java 4 | Generics

공부의 기록·2021년 12월 13일
0

Java (Effective Java)

목록 보기
5/12
post-thumbnail

지네릭

본 글은 2021년 12월 13일 에 기록되었다.
순서상으로는 Effective Java 중 첫 번째의 포스트 에 해당한다.
이주 전 Generics 를 배우고 Java : Generics 라는 포스트를 작성하였다.
정확히 모르는 개념에 대해서 두리뭉술하게 표현 점들과 앞으로 많은 부분에서 지네릭이 보이는 점들로, 첫 번째 공부 순서를 지네릭으로 하는 것이 맞다라는 생각을 하게 되었다.

본 내용은 지네릭을 알고 있는 사람을 전제로 하고 있는 것 같다.
따라서 본인이 지네릭에 대해서 잘 모른다면 Java : Generics 이 포스트를 보고 오는 것을 추천한다.

지네릭이란?

Java : Generics 을 처음 작성할 때는 지네릭의 목적이 다음과 같다고 생각했다.

  1. ArrayList <String> 는 배열원소로 String 타입받게 받지 못한다.
    따라서 나는 이를 매개변수화 타입 <E>로 선언한 값만 들어올 수 있게 제한하는 기능 이라고 생각했다.
    물론 기능적으로는 그런 결과를 만들지는 몰라도 지네릭의 목적성은 클래스나 메서드의 매개변수에 따라 일일히 오버라이딩 하지 않고도 여러 타입에 대응되게 만드는 것 인 것 같다.

  2. 로타입 ArrayList
    비한정적 와일드 카드 ArrayList < ? >
    한정적 와일드 카드 ArrayList <E extends String>

다음의 아이템들로 실 사용 사례를 조금 더 보도록 하겠다.

요약

이름예시설명
1일반 지네릭ArrayList<String>String 타입만을 받아들이는 ArrayList 가 만들어진다.
2로 타입ArrayList최초 대입한 데이터의 타입만 받아들이는 ArrayList 가 만들어진다.
3비한정적 와일드 카드ArrayList< ? >null 이외의 값을 받아들이지 않는 ArrayList 가 만들어진다.
[Java] 와일드카드 (wildcards)
4한정적 와일드 카드ArrayList< ? extends String />
ArrayList< ? super String />
여기서부터 이해하지 못한 내용이다.

실제 내용

아이템 26 : 로 타입은 사용하지 마라.

지네릭을 사용하는 방법은 크게 두 가지가 있다.

// 어떤 타입이든 받아들일 수 있는 로 Collection
private final Collection bigIntegers= ...;
// BigInteger 을 받아들이는 지네릭 Collection
private final Collection<BigInteger> bigInstgers= ...;

로타입은 말 그대로 어떤 타입이든 받아들일 수 있다.
그렇기 때문에, 로타입 변수를 사용하면 다음과같은 문제가 발생할 수 있다.

  1. 에러의 발생 시기가 문제를 일으키는 부분과 멀어질 수 있다. (bigIntegers 를 사용하는 순간에 에러가 발생할 수 있다라는 뜻)
  2. 에러가 발생하지 않고 의미가 모호한 unchecked call 의 경고를 받을 수도 있다.

위에 대한 자세한 예시는 Collection 클래스를 사용하는 경우로,
구글링을 하거나 본 도서 154페이지를 체크해보는 것을 추천한다.

그렇다면 언제 로타입을 사용하는 실수를 하게 되는가?

가장 대표적인 경우는 지네릭 타입을 쓰고 싶지만 실제 타입 매개변수가 무엇인지 신경쓰고 싶지 않을 때이다. 이럴 때 어떤 타입이든 받을 수 있게 로타입 매개변수를 사용하게 된다.

그러나 이러한 경우에는 비한정적 와일드 카드 연산자 ( ? ) 를 사용하도록 하자. 관련된 내용을 조금 더 풀어 써놓은 포스트 를 체크업해보자.

핵심정리
로타입을 사용하면 런타임에 에러가 생길 수 있으니 사용하면 안된다. (이후 생략)

아이템 27 : 비검사 경고 제거

해당 부분은 약간의 @Annotation 과 관련된 부분도 일부 있다.

사례 1 | 잘못된 지네릭 문법

Set<String> stringSet=new HashSet(); // 에러
Set<String> stringSet=new HashSet<String>(); // 정상
Set<String> stringSet=new HashSet<>(); // jdk 1.7 의 다이아몬드 연산자 사용

사례 2 | 잘못된 @Annotation 부터 올바른 @Annotation 까지

// 잘못된 사용 | 사용할 수는 있으나 너무 넓은 범위에 사용해서는 안된다.
@SuppressWarnings("resource")
String methodName() {
    Scanner scan=new Scanner(System.in);
    return scan.next();
}
// 잘못된 사용 | return 앞에는 Annotation 을 붙일 수 없다.
String methodName() {
    Scanner scan=new Scanner(System.in);
    @SuppressWarnings("resource") return scan.next();
}
// 올바른 사용
String methodName() {
    @SuppressWarnings("resource") Scanner scan=new Scanner(System.in);
    return scan.next();
}
// 최선의 사용 | 경고문구를 무시해도 되는 이유까지 명시해야 한다.
String methodName() {
    // 시스템 전체에서 Scanner 나 Stream 은 여기서만 쓰이므로 무시해도 괜찮다.
    @SuppressWarnings("resource") Scanner scan=new Scanner(System.in);
    return scan.next();
}

핵심 정리
모든 비검사 경고는 런타임에 ClassCastException 을 일으킬 수 있는 잠재적 가능성을 뜻하니 최선을 다해서 제거하자.
경고를 없앨 수 없다면 그 코드가 타입 안전함을 증명하고 가능한 범위를 좁혀서 @SuppressWarnings("unchecked") Annotation 으로 경고를 숨겨라.
그런 다음 그러한 판단의 근거를 주석으로 남겨라.

public <T> T[] toArray(T[] a){
   if(a.length<size){
       // 생성한 배열과 매개변수로 받은 배열의 타입이 모두 T[] 므로 올바른 형변환이다.
       @SuppressWarnings("unchecked") T[] result=
           (T[]) Arrays.copyOf(elements,size, a.getClass();
       return result;
    }
    System.arraycopy(elements, 0, a, 0, size);
    if (a.length > size)
        a[size]=null;
    return a;
}

아이템 28 : 배열보다는 리스트를 사용해라

배열보다 리스트를 사용해야 하는 이유는 다음과 같이 정리된다.

  1. 배열은 공변하고 제네릭은 불공변하다.
  2. 배열은 런타임 에러를 제네릭은 컴파일 에러를 발생한다.
  3. 배열은 실체화되고 제네릭은 컴파일 이후에 매개타입 정보를 드랍한다.

또한 배열과 리스트는 공존될 수 없는 특징이 있는데 이는 타입 불안정성 을 가지게 되기 때문이다.

공변 && 불공변

A 가 B 의 하위 타입이라면 배열 A[] 는 B[]의 하위 타입이 된다. 즉, 공변 관계이다.
그러나 List<A> 와 List<B> 는 서로 상위타입도 하위 타입도 아니다. 즉 불공변 관계이다.

런타임 에러 && 컴파일 에러

// 런타임 에러 발생 : 실행이 되며 ArrayStoreException 발생
// **배열의 실체화** 를 언급한 이유가 이것 때문이다.
Object[] objectArray=new Long[1];
objectArray[0]="타입이 달라 넣을 수 없다.";

// 컴파일 에러 발생 : 실행 조차 되지 않는다.
// 제네릭은 컴파일 이후에는 **매개변수 타입** 을 드랍하고 신경쓰지 않는다.
// 그러한 특징 때문에 E, List<E> List<String> 과 같은 타입을 실체화 불가 타입이라고도 부른다.
List<Object> objectList=new ArrayList<Long>();
objectArray.add("타입이 달라 넣을 수 없다.");

제네릭 배열

// 에러 발생 : ClassCastException 발생
List<String>[] stringLists=new List<String>[];
  1. 생성 매개변수로 Collection 을 받는 사용자 정의 클래스, Chooser 의 Object[]
    제네릭을 시급히 도입해야 한다.
public class Chooser {
    private final Ojbect[] choiceArray;
    
    public Chooesr(Collection choices){
      choiceArray=choices.toArray();
    }
    
    public Object choose(){
      Random rnd=ThreadLocalRandom.current();
      return choiceArray[rnd.nextInt(choiceArray.length)];
    }
}
  1. 생성 매개변수로 Collection<T> 를 받는 사용자 정의 클래스, Chooser 의 Object[]
    컴파일 되지 않는다. >> 3번째 코드는 컴파일은 되지만 권장하지 않는다.
public class Choocer {
    private final T[] choiceArray;
    
    public Chooesr(Collection<T> choices){
      choiceArray=choices.toArray(); // Object[] cannot be convereted to T[] - 타입 불일치
      choiceArray=(T[]) choices.toArray(); // [unchecked] unchecked cast - 비검사 경고
      @SuppressWarnings("unchecked") choiceARray=(T[]) choices.toArray();
    } // 하지만 최선의 선택이 아니므로 3번을 확인하자
    
    public Object choose(){
      Random rnd=ThreadLocalRandom.current();
      return choiceArray[rnd.nextInt(choiceArray.length)];
    }
}
  1. 생성 매개변수로 Collection<T> 를 받는 사용자 정의 클래스, Chooser 의 List<T>
public class Chooser {
    private final List<T> choiceArray;
    
    public Chooser(Collection<T> choices){
      choiceArray=new ArrayList<T>(choices); // ok : 매개변수화 타입 연산자 <T>
      choiceArray=new ArrayList<>(choices); // ok : 다이아몬드 연산자 <>
    }
    
    public T choose() {
      Random rnd=ThreadLocalRandom.current();
      return choiceArray[rnd.nextInt(choiceArray.length)];
   }
}

전술한 ### 아이템 28 의 초반부에서 언급했듯이,
배열과 리스트는 매우 다른 타입 규칙이 적용되고 있다.
그렇기 때문에 둘을 섞어쓰면 높은 확률로 에러 및 경고를 만나게 되고
이러한 경우를 해결하기 어렵다면 하나만 사용하는 것(기왕이면 제네릭)이 좋다.

아이템 29 : 제네릭 타입으로 만들어라

  1. Object 기반 Stack
    제네릭을 시급히 도입해야 한다.
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];
      return result;
    }
    
    public boolean isEmpty() {
      return size==0;
    }
    
    public void ensureCapacity() {
      if(elements.length==size)
        elements=Arrays.copyOf(elements, 2*size+1);
    }
}
  1. 제네릭으로 바꾸기 1단계
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);
      return result;
    }
}
  1. 선택지 1 과 2
    2번에서는 elements=new E[배열길이] 부분이 문제를 일으킨다.
    매개변수화 타입 배열은 만들 수 없는 것이다. 아래를 확인해보자.
// 배열 elemtns 는 push(E) 로 넘어온 E 인스턴스만 담기에 타입 안정성이 확보된다.
// 이 배열의 런타임 타입은 E[] 가 아닌 Object[] 이다.
@SuppressWarnings("unchecked")
public Stack() {
  elements=(E[]) new Object[DEFAULT_INITIAL_CAPACITY];
}

// 비검사 경고를 적절히 숨기고
// 다 쓴 참조를 해제한다.
public E pop() {
  if(size==0)
    throw new EmptyStackException();
  @SuppressWarnings("unchecked") E result=(E) elements(--size);
  
  elements[size]=null;
  return result;
}

선택지 1은 Object 배열을 만들어 매개변수화 타입 으로 캐스팅하는 방법 이다.
선택지 2는 비검사 형변환을 수행하는 할당문에 @Annotation 을 적용하는 방법 이다.

선택지 1은 코드도 더 짧고 직관적이다.
또한 형변환을 배열 생성 시에만 한 번 하면 된다.
그렇기 때문에 현업에서 해당 코드를 많이 사용한다.
하지만, 이 코드는 힙 오염 | Heap Pollution 을 일으킬 수 있다.

선택지 2는 코드가 더 길지만 비검사 경고가 더 좁은 범위에 사용되어 있다.
하지만, 형변환을 매 메서드 실행 시마다 해야 하기도 한다.
그렇기 때문에 자주 사용되지는 않는다.
하지만, 선택지 1의 힙오염 이슈가 해가 되는 경우에는 이 방법이 대안이 될 수 있다.

핵심 정리
클라이언트에서 직접 형변환해야 하는 타입보다 제네릭 타입이 더 안전하고 쓰기 편하다.
그러니 새로운 타입을 설계할 때는 형변환 없이도 사용할 수 있도록 하라.

그렇게 하려면 제네릭 타입으로 만들어야 하는 경우가 많다.
기존에 제네릭이었어야 하는 것이 있다면 제네릭 타입으로 변경하자.
기존 클라이언트에는 아무런 영향을 주지 않으면서 새로운 사용자를 훨씬 편하게 해주는 기능이다.

아이템 30 : 제네릭 메서드를 만들어라

아이템 31 : 한정적 와일드 카드 사용과 API 유연성

아이템 32 : 제네릭과 가변인수의 사용 자제

아이템 33 : 타입 안전 이종 컨테이너 구조

profile
2022년 12월 9일 부터 노션 페이지에서 작성을 이어가고 있습니다.

0개의 댓글