[Effective Java] 아이템 26 : 로 타입은 사용하지 말라

Rupee·2022년 7월 29일
0

이펙티브 자바

목록 보기
25/76
post-thumbnail

☁️ 제네릭(Generic)

제네릭은 런타임 형변환 오류를 방지하기 위해, 자바 5(JDK 1.5)부터 도입되었다. 컴파일러가 안전하게 자동으로 형변환을 추가해줄 수 있게 되었는데, 이에 대해서는 아래에서 알아보도록 하자.

제네릭 클래스 혹은 인터페이스란, 클래스와 인터페이스 선언에 타입 매개변수(T)가 쓰인 것을 말한다. 각각의 제네릭 타입은 일련의 매개변수화 타입을 정의한다.

🔖 제네릭 형식
클래스/인터페이스 이름<실제 타입 매개변수>

제네릭 타입을 하나 정의하면, 그에 딸린 로 타입(Raw Type)도 함께 정의된다.

로 타입이란, 제네릭 타입에서 타입 매개변수를 전혀 사용하지 않을 때를 의미한다 (ex)List). 또한 타입 선언에서 제네릭 타입 정보가 전부 지워진 것처럼 동작한다.

☁️ 로 타입(Raw Type)

로 타입의 단점을 나타내주는 몇 가지 예시를 들어보자.

private final Collection stamps = ...;
stamps.add(new Coin(...));

실수로 도장 대신 동전을 넣어도 오류 없이 컴파일 되고 실행되는 문제가 발생한다.

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

오류는 이상적으로 컴파일할때 발견하는 것이 좋지만, 로 타입을 사용한다면 런타임에나 오류를 발견할 수 있다.

☁️ 왜 로 타입보다 제네릭을 사용해야 할까?

1. 타입 안전성이 확보된다.

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

위 예제처럼 컴파일러가 stamps 에는 Stamp 의 인스턴스만 넣어야 함을 인지하기 때문에, 다른 엉뚱한 타입의 인스턴스는 컴파일 에러를 내뱉게 된다.

올바른 인스턴스라면 컴파일러는 컬렉션에서 원소를 꺼내는 모든 곳에 보이지 않는 형변환을 추가하기 때문에 그 이후부터는 정상적으로 작동할 것이다.

2. 로 타입은 제네릭이 안겨주는 안전성과 표현력이 없다.

하지만 그럼에도 로 타입이 존재하고 있는 것은, 로 타입을 사용하는 메서드에 매개변수화 타입의 인스턴스를 넘겨도 동작해야 했기 때문이다.

따라서 이러한 마이그레이션 호환성을 위해 로 타입을 지원하고 제네릭 구현에는 소거방식을 도입하게 된다. 소거 방식이란, 런타임에 타입 정보가 사라지는 것을 의미한다.

☁️ (참고) 자바의 타입 소거 방식

https://devlog-wjdrbs96.tistory.com/263#:~:text=소거란%20원소%20타입을,을%20제거한다는%20뜻입니다.

☁️ List VS List< Object>

List 같은 로 타입을 사용하면, 아무 원소나 넣을 수 있어져서 타입 안전성을 잃게 된다.

반면, List<Object> 처럼 임의 객체를 허용하는 매개변수화 타입은 괜찮다. 그래도 모든 타입을 허용한다는 의사를 컴파일러에게 명확히 전달하는 것이기 때문이다.

제네릭의 하위 타입 규칙인 불공변 특성으로 인해, List<String>List<Object> 를 하위 타입이 아닌 아예 다른 타입으로 간주하기 때문에 이를 인자로 받는 메서드로 넘길 수 없어서 안전하다.

하지만, 제네릭이 아닌 경우 List<String>List하위 타입이라고 간주하기 때문에 컴파일 타임에 인자로 전달이 가능하게 되고, 아래와 같이 런타임 중에 에러가 나는 경우가 발생하게 된다.

예시를 봐보자.

// String 컬렉션에 Integer를 넣고 있다.
public class Raw {
    public static void main(String[] args) {
        List<String> strings = new ArrayList<>();
        unsafeAdd(strings, Integer.valueOf(42));
        String s = strings.get(0);  // 컴파일러가 자동으로 형변환 코드를 넣어준다.
    }

    private static void unsafeAdd(List list, Object o) { // 컴파일 에러 X
        list.add(o);
    }
}

String 컬렉션에 Integer 를 넣는것 까지는 성공했는데, 꺼낼 때 문제가 발생한다. 즉 해당 코드는 컴파일은 되지만, 로 타입인 List 를 사용하여 strings.get(0) 에서 런타임 중에 다음과 같은 경고가 발생하게 된다.

Test.java:10: warning: [unchecked] unchecked call to add(E) as a member of the raw type List

하지만 만약 List가 아닌 List<Object> 였다면 컴파일 조차 되지 않아 더욱 안전했을 것이다.

public class Raw {
    public static void main(String[] args) {
        List<String> strings = new ArrayList<>();
        unsafeAdd(strings, Integer.valueOf(42));
        String s = strings.get(0); 
    }

    private static void unsafeAdd(List<Object> list, Object o) {  // 컴파일 에러
        list.add(o);
    }
}

Test.java:10: error : inmopatible types: List< String> cannot be converted to List< Object>

☁️ 비한정적 와일드 카드 : ?

와일드 카드(?)란, 제네릭 타입을 쓰고 싶지만 실제 타입 매개변수가 무엇인지 신경쓰고 싶지 않을 경우 사용할 수 있는 것을 말한다.

중요한 것은 any 타입이 아닌 unknown 타입이라는 것이다.

  1. add : 컬렉션에 추가하는 경우는 넘겨주는 파라미터가 unknown 타입의 자식이여야 하는데, 정해지지 않았으므로 어떠한 타입을 대표하는지 알 수 없고 자식 여부를 검사할 수 없게 되어 에러가 발생한다.

  2. get : 값을 꺼내는 작업은 문제가 없다. 값을 꺼낸 결과가 unknown 타입이여도 우리는 해당 타입이 어떤 타입의 자식인지 확인이 필요하지 않으며, 적어도 Object의 타입임을 보장할 수 있기 때문이다.

https://docs.oracle.com/javase/tutorial/extra/generics/wildcards.html

따라서 값을 조회할 때는 로 타입과 다르게 안전성을 보장해주지만 추가할때는 null 외의 어떤 원소도 넣을 수 없게 된다.(이러한 문제점을 해결하기 위해 한정적 와일드 카드 타입이 나오게 되었다.)

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

☁️ 로 타입을 사용해야 하는 예외

1. class 리터럴에는 로 타입을 써야 한다.

자바 명세는 배열과 기본 타입 외의 class 리터럴에 매개변수화 타입을 사용하지 못하게 하고 있다. 예를 들어, List.class는 허용하지만 List<String>.class는 불가능하다.

2. instanceof 연산자를 사용해야 할 경우는 로 타입이나 비한정적 와일드카드 타입을 사용한다.

런타임에는 제네릭 타입 정보가 지워지므로, instanceof 연산자는 비한정적 와일드카드 타입 이외의 매개변수화 타입에는 적용할 수 없다. 따라서 로 타입이나 비한정적 와일드카듵 타입이나 같은 결과를 내기 때문에, 차라리 더 깔끔한 로 타입을 사용하자.

if (o instance of Set){   // 로 타입
	Set<?> s = (Set<?>) o;    // 와일드카드 타입
	...
{

o의 타입이 Set 임을 확인한 다음 비한정적 와일드 카드 타입으로 형변환 하고 있다. 검사 형변환이므로, 컴파일러 경고가 뜨지 않는다.

📚 핵심 정리
로 타입을 사용하면 런타임에 예외가 일어날 수 있으니 사용하면 안 된다. Set<Object>는 어떤 타입의 객체도 저장할 수 있는 매개변수화 타입이고, Set<?>는 모종의 타입 객체만 저장할 수 있는 와일드카드 타입이다. 그리고 이 들의 로 타입인 Set는 다른 것들과 달리 안전하지 않다.

참고 자료
[Java] 제네릭과 와일드카드 타입에 대해 쉽고 완벽하게 이해하기(공변과 불공변, 상한 타입과 하한 타입)

profile
개인용으로 공부하는 공간입니다. 잘못된 부분은 피드백 부탁드립니다!

0개의 댓글