generic에 대하여

제네릭이란?

제네릭은 다양한 타입의 객체들을 다루는 메서드나 컬렉션 클래스에 컴파일 시의 타입 체크를 해주는 기능

출처:https://devbox.tistory.com/entry/Java-제네릭 [장인개발자를 꿈꾸는 :: 기록하는 공간]

위 출처의 정의가 제네릭을 넓은 범위에 맞게 가장 잘 표현한 것 같다. 이를 좀 더 세분화하여, 제네릭은 클래스, 인터페이스, 메서드에서 사용될 수 있다. 이때 제네릭 클래스와 제네릭 인터페이스를 제네릭 타입 이라고 한다.

제네릭의 장점

  1. 타입 안정성 의도하지 않은 타입의 객체 생성을 컴파일 단계에서 막을 수 있다.
  2. 불필요한 강제 형변환 제거 제네릭이 없을때는 객체 접근시 형변환이 있어야 했는데 더 이상 필요 없다.

제네릭의 다형성

// 부모 타입의 자료구조에 자식 타입 데이터를 넣을 수 있다.
ArrayList<Animal> list = new ArrayList<Animal>();
list.add(new Animal());
list.add(new Tiger());
list.add(new Monkey());

// 하지만 아래는 허용하지 않는다.
ArrayList<Animal> list = new ArrayList<Tiger>(); //ERROR

// -> 이는 와일드카드로 해결 가능하다. (확인필요)
ArrayList<? extends Animal> list = new ArrayList<Tiger>();

제네릭 메서드

리턴타입 바로 앞에 메서드 내부(리턴타입, 파라미터, 내부 필드)에서 사용할 제네릭 타입을 선언해야 한다. 제네릭 메서드의 선언 문법은 아래와 같다.

[접근제어자] <타입 파라미터> 리턴타입 메서드명(파라미터들…) {}

// 예시
public <T> T exampleMethod(T t1) {
	T t2 = t1;
	return t2;
}

제네릭 배열을 지양해야 하는 이유 (effective java 28)

먼저 배열과 제네릭의 차이에 대해 알아보자. 크게 2가지 차이점이 있다.

1. 배열은 공변(covariant), 제네릭은 불공변(invariant)이다.

Sub가 Super의 하위 타입이라면, 배열 Sub[]는 배열 Super[] 의 하위 타입이 된다. 하지만 제너릭에서는 ArrayList와 ArrayList는 아예 다르게 인식된다.

// 배열은 공변이니 컴파일 통과 후 런타임시 Error 발생
Object [] a = new Integer[10];
a.add("String"); // 런타임시 Error

// 제네릭은 불변이니 컴파일 단계에서 Error 발생
ArrayList<Object> b = new ArrayList<Integer>(); // 컴파일시 Error
b.add("String");

2. 배열은 실체화(reify)된다.

제네릭은 컴파일 단계에서 타입 안정성을 체크하고 나면, 런타임에는 타입 정보가 소거(erasure)된다. 반면, 배열은 런타임에도 자신이 담기로 한 원소의 타입을 인지하고 있다. (erasure는 제네릭이 처음 나왔을때 레거시 코드와 제네릭 타입이 순조롭게 전환되도록 해줌.)

위와 같은 차이가 있어 제네릭와 배열은 서로 잘 어우러지지 못한다. 따라서 제네릭 배열 코드는 컴파일 단계에서 에러를 발생시킨다.

제네릭 배열을 막은 이유는?

제네릭의 취지인 타입 안정성이 지켜지지 않기 때문이다. 이를 허용한다면 런타임시 ClassCastException이 발생할 수 있고, 이는 곧 런타임시 발생할 타입 에러를 컴파일 단계에서 체크하기 위해 태어난 제네릭의 취지에 어긋난다.

Published 18 Nov 2019


jaegoon on github