[Effective Java] 제네릭 Item 26 - 로 타입은 사용하지 말라

Sewon·2021년 8월 3일
0

for Java

목록 보기
1/2
post-thumbnail
  • 제네릭은 자바5부터 사용이 가능함
  • 제네릭을 지원하기 전에는 특정 타입의 객체만 컬렉션에 저장하기 위해 타입 체크를 해야했고, 컬렉션에서 객체를 꺼낼 때마다 특정 타입으로 형변환을 해야했음
  • 제네릭을 사용하면 컬렉션이 담을 수 있는 타입을 컴파일러에게 알려줌 → 컴파일러는 알아서 형변환 코드를 추가할 수 있게됨 → 엉뚱한 타입의 객체를 넣으려는 시도를 컴파일 과정에서 차단할 수 있음
    (제네릭은 다양한 타입이 존재함 - 매개변수타입, 한정적 와일드 카드 타입 등등)
//Raw type
List names = new ArrayList(); // warning : raw type
names.add("fordevelop");
names.add("me");
names.add(Boolean.FALSE); // not a compilation error!

for(Object o : names){
	String name = (String)o; // throw ClassCastException
    System.out.println(name);
}
-> java.lang.Boolean 타입은 java.lang.String 타입으로 형변환 불가함

//Generics
List<String> names = new ArrayList<>();
names.add("fordevelop");
names.add("me");
names.add(Boolean.FALSE); // compilation error!
-> String 타입만 저장하도록 컴파일러가 체크해줌

what-is-a-raw-type-and-why-shouldnt-we-use-it

관련 용어 정리

제네릭 클래스 or 제네릭 인터페이스 : 클래스와 인터페이스 선언에 타입 매개변수(type parameter)가 쓰인 것
제네릭 타입(generic type) : 제네릭 클래스와 제네릭 인터페이스를 통틀어 말한 것

매개변수화 타입

제네릭 타입은 매개변수화 타입(parameterized type)을 정의함

ex) List<String> : 원소의 타입이 String인 리스트를 뜻하는 매개변수화 타입
	String : 정규 타입 매개변수 E에 해당하는 실제 타입 매개변수 

로 타입(raw type)

제네릭 타입을 정의하면 그에 대한 로 타입도 함께 정의됨
로 타입(raw type) : 제네릭 타입에서 타입 매개변수를 전혀 사용하지 않을 때

ex) List<E>의 로 타입 : List

타입 선언에서 제네릭 타입 정보가 전부 지워진 것처럼 동작함

[예시]

상황1. 컬렉션의 로 타입

//Stamp 인스턴스만 취급함 
private final Collection stamps = ...;

//실수로 동전을 넣음
stamps.add(new Coin(...)); //로 타입 사용 시 컴파일 에러 체크 불가

for(Iterator i = stamps.iterator(); i.hasNext(); ){
	Stamp stamp = (Stamp)i.next(); //ClassCastException 던짐
    stamp.cancel();
}

문제점 : 런타임에야 오류를 알아챌 수 있음. 그러나 런타임에 문제를 겪는 코드와 원인을 제공한 코드가 물리적으로 상당히 떨어져 있을 가능성이 큼. 위의 코드에서처럼 ClassCastException이 발생하면, stamps에 동전을 넣은 지점을 찾기 위해 코드 전체를 훑어봐야 할 수도 있음.

상황2. 매개변수화된 컬렉션 타입 - 타입 안전성 확보!

private final Collection<Stamp> stamps = ...;

개선점 : stamps에는 Stamp의 인스턴스만 넣어야 함을 컴파일러가 인지하게 됨. 따라서 stamps에 그외의 타입의 인스턴스를 넣으려 하면, 컴파일 오류가 발생함.

로 타입(raw type)을 사용하면 안되는 이유

제네릭이 주는 안전성과 표현력을 모두 잃게 됨
기존의 제네릭이 없을 때의 코드와의 호환성을 위해 로 타입을 만들어놓았음

List vs List< Object >

List 같은 로 타입은 사용하면 안되지만, List< Object >처럼 임의 객체를 허용하는 매개변수화 타입은 괜찮음
차이점 :
→ List< Object >는 모든 타입을 허용한다는 것을 컴파일러에게 명확히 전달함
→ 매개변수로 List를 받는 메서드에 List< String > 넘길 수 있음. 매개변수로 List< Object >를 받는 메서드에는 넘길 수 없음.
이유) 제네릭의 하위 타입 규칙에 의해, List< String >은 로 타입인 List의 하위 타입이지만, List< Object >의 하위 타입은 X
즉, List< Object > 같은 매개변수화 타입을 사용할 때와 달리 List 같은 로 타입을 사용하면 안전성을 잃게 됨

[예시1]

상황1. 런타임 때 오류 발생 - unsafeAdd 메서드가 로 타입(raw type)을 사용

public class test1 {
	private static void unsafeAdd(List list, Object o) {
		list.add(o);
	}
	
	public static void main(String[] args) {
		List<String> strings = new ArrayList<>();
		
		unsafeAdd(strings, Integer.valueOf(32));
		String s = strings.get(0);
	}
}

문제상황 :
→ 컴파일은 되지만, 로 타입인 List를 사용해 경고 발생함
→ strings.get(0)의 결과를 형변환하려 할 때 ClassCastException 던짐(Integer -> Object로 upcasting 되었으므로, Object -> Integer로만 downcasting 가능함)

상황2. 컴파일 시 오류 발생 - unsafeAdd 메서드가 제네릭 사용

public class test1 {
	private static void unsafeAdd(List<Object> list, Object o) {
		list.add(o);
	}
	
	public static void main(String[] args) {
		List<String> strings = new ArrayList<>();
		
		unsafeAdd(strings, Integer.valueOf(32));
		String s = strings.get(0);
	}
}

문제상황 : List< String >은 List< Object >의 하위 타입이 아니므로, 컴파일 불가능 (런타임 전에 오류 체크 가능함)

[예시2] - 비한정적 와일드카드 타입 사용

상황. 잘못된 예 - 모르는 타입의 원소도 받는 로 타입(raw type) 사용

static int numElementsInCommon(Set s1, Set s2){
    int result = 0;
    for(Object o1 : s1)
    	if(s2.contains(o1)) result++;
    return result;
}

문제점 : Set 로 타입(raw type)을 사용해 안전하지 않음

개선. 비한정적 와일드카드 타입 사용 - 안전하며 유연함

static int numElementsInCommon(Set<?> s1, Set<?> s2){...}

개선점 :
비한정적 와일드카드 타입(unbounded wildcard type) 사용
→ 제네릭 타입을 쓰고 싶지만 실제 타입 매개변수가 무엇인지는 신경 쓰고 싶지 않을 때 < ? > 사용함 (제네릭 타입인 Set< E >의 비한정적 와일드카드 타입은 Set< ? >임)
→ 가장 범용적인 매개변수화 Set 타입
→ raw type보다 안전함

비한정적 와일드 카드 타입?!

Unbounded Wildcard를 사용하면 유용한 2가지 경우

  1. Object 클래스에서 제공해주는 기능을 사용하여 구현할 수 있는 메서드를 작성할 때
  2. 제네릭 클래스의 메소드가 type parameter에 의존하지 않을 때
    ex) List.size(), List.clear()
    Class< T >의 대부분의 메소드들이 매개변수 타입인 T에 의존하지 않기 때문에 Class< ? > (비한정적 와일드 카드 타입)은 자주 사용됨

사용 예시 - 매개변수 타입에 상관없이 List 출력하는 제네릭 메서드 구현

  • List< Object > 사용
public static void printList(List<Object> list){
	for(Object elem : list)
    		System.out.println(elem + " ");
    	System.out.println();
}

문제 상황 :
→ printList()의 목적은 타입에 상관없이 List를 출력하는 것이지만, 위의 코드의 경우 해당 목적을 만족하지 못함.
→ Object 인스턴스의 리스트만 출력이 가능함. 즉 List< Integer >, List< Double >, List< String >은 List< Object >의 하위 타입이 아니기 때문에 출력 불가함!

  • List< ? > 사용
public static void printList(List<?> list){
	for(Object elem : list)
    		System.out.println(elem + " ");
        System.out.println();
}

List<Integer> list1 = Arrays.asList(1,2,3);
List<String> list2 = Arrays.asList("1", "2", "3");
printList(list1);
printList(list2);

개선 상황 :
→ 타입에 상관없이, List< A >는 List< ? >의 하위 타입임!

unboundedWildcards

주의 사항

Collection<?> c = new ArrayList<String>();
c.add(new Object()); // 컴파일 에러 발생! 

→ 비한정적 와일드 타입 객체에는 null만 삽입 가능함 (null은 모든 타입의 member이기 때문에)
→ 위의 코드를 보면, 참조변수 c의 원소 타입은 unknown type으로 무엇을 나타내는지 명확하지 않음.
→ add() 사용 시 해당 메서드로 전달된 인자들은 unknown type의 하위 타입이어야 하므로 null을 제외한 특정 타입 삽입 불가함

wildcards

로 타입 예외

class 리터럴에는 로 타입으로 작성

→ class 리터럴에 매개변수화 타입 사용 불가함(배열과 기본 타입은 허용)
ex) List.class, String[].class, int.class는 허용 / List< String >.class, List< ? >.class는 허용 X

instanceof 연산자

런타임에는 제네릭 타입 정보가 지워지므로 로 타입이든 비한정적 와일드카드 타입이든 instanceof는 똑같이 동작함
→ 즉 < ? >는 아무런 역할을 하지 못하므로, 차라리 로 타입으로 쓰는 것이 깔끔함

  • 로 타입을 써도 좋은 예 - instanceof 연산자
if(o instanceof Set){ //로 타입
	Set<?> s = (Set<?>)o; //와일드카드 타입으로 형변환 
    ...
}

→ 위의 코드를 보면, o의 타입이 Set임을 확인한 다음 와일드카드 타입인 Set< ? >으로 형변환 함(검사 형변환(checked cast)이므로 컴파일러 경고가 뜨지 않음)

런타임 때의 제네릭 타입 정보

Type Erasure

→ 컴파일 타임에만 타입을 명시해두고, 런타임에는 타입 정보를 제거(교체)하는 것 (즉 generic type 정보는 컴파일 타임에서만 사용 가능함)
→ 이전 버전의 자바 코드와의 호환성을 위해서 사용됨
→ 자바 컴파일러가 타입 매개변수를 교체함(2가지 경우 존재)

경우1. type parameter is unbounded

→ Object로 교체함

public class Node<T>{
    private T data;
    private Node<T> next;
    
    public Node(T data, Node<T> next){
    	this.data = data;
        this.next = next;
    }
    
    public T getData(){ return data; }
    ...
}

//Replaces it with Object
public class Node{
    private Object data;
    private Node next;
    
    public Node(Object data, Node next){
    	this.data = data;
        this.next = next;
    }
    
    public Object getData(){ return data; }
    ...
}

경우2. type parameter is bounded

→ first bound class로 교체함

public class Node<T extends Comparable<T>>{
    private T data;
    private Node<T> next;
    
    public Node(T data, Node<T> next){
    	this.data = data;
        this.next = next;
    }
    
    public T getData(){ return data; }
    ...
}

//Replaces it with the first bound class, Comparable
public class Node{
    private Comparable data;
    private Node next;
    
    public Node(Comparable data, Node next){
    	this.data = data;
        this.next = next;
    }
    
    public Comparable getData(){ return data; }
    ...
}

java tutorials Erasure of Generic Types
java generics questions

참고)
책 이펙티브 자바 3/E

profile
for develop ☁️

0개의 댓글