item 26 : 로 타입은 사용하지 말라
1. 제네릭과 와일드카드
1) 공변과 불공변
공변(convariant) : A가 B의 하위 타입일 때, T<A>가 T<B>의 하위 타입이면 T는 공변이라고 한다. 타입 A가 B의 하위 타입(subtype)이라면,
A[]
는B[]
의 하위 타입이 될 수 있는 성질이다.불공변(invariant) : A가 B의 하위 타입일 때, T<A>가 T<B>의 하위 타입이 아니면, T는 불공변이라고 한다. 제네릭 타입
List<A>
와List<B>
가 있을 때,A
가B
의 하위 타입이더라도List<A>
는List<B>
의 하위 타입이 아니다. 즉, 제네릭 타입은 불공변성을 가진다.
대표적으로 배열은 공변, 제네릭은 불공변인데 예를 들어 배열의 요소들을 출력하는 메소드가 있다고 하자.
[배열의 공변성 예제]
배열은 공변성이 있으므로, 하위 타입의 배열을 상위 타입의 배열로 사용할 수 있다.
위의 코드에서 Integer[]
는 Object[]
의 하위 타입으로 간주된다. 따라서 printArray(Object[] arr)
메서드에 Integer[]
를 인자로 전달해도 문제가 발생하지 않는다.
[제네릭의 불공변성 예제]
제네릭 타입은 불공변성을 가지므로, List<Integer>
를 List<Object>
로 사용할 수 없다.
위의 코드에서 List<Integer>
는 List<Object>
의 하위 타입이 아니기 때문에, printCollection(Collection<Object> c)
메서드에 List<Integer>
를 전달하면 컴파일 오류가 발생한다.
제네릭 타입은 불공변성이기 때문에, List<Integer>
와 List<Object>
는 아무런 관계가 없다.
이러한 제네릭의 불공변 때문에 (제네릭의 ?타입)가 등장할 수 밖에 없었다.
2) 제네릭의 등장 이전 및 도입 배경
제네릭이 도입되기 전, 컬렉션의 요소를 다루는 메서드(= 로 타입)는 타입 안전성을 보장하지 못했다. 컬렉션의 타입 매개변수를 명시할 수 없기 때문에, 모든 요소는
Object
타입으로 처리되었고, 타입 캐스팅이 필요한 상황에서 문제가 발생할 수 있었다.
예시 1: 컬렉션 요소 출력
위 코드에서 컬렉션의 요소들은 Object
타입으로 취급되기 때문에, 특정 타입으로 다루려면 타입 캐스팅이 필요하다. 이는 런타임에 타입 에러를 발생시킬 수 있는 잠재적인 위험을 내포하고 있다다.
예시 2: 컬렉션 요소 합 구하기
위의 메서드는 Collection
에 있는 요소들이 Integer
타입이라고 가정하고 작성된 것이다. 하지만 만약 String
과 같은 다른 타입의 요소를 가진 컬렉션을 전달하면, 컴파일 시에는 문제가 없지만 런타임에 ClassCastException
이 발생할 수 있다.
위와 같은 문제를 해결하기 위해 Java 개발자들은 타입을 지정하여 컴파일 시점에 타입 안전성을 보장할 수 있는 방법을 고안하였고, 그 결과 제네릭이 등장하게 되었다.
제네릭을 사용하면, 컬렉션이나 메서드에 타입 매개변수를 지정할 수 있어, 컴파일 시점에 타입을 검사할 수 있다. 이렇게 하면 런타임 오류의 가능성을 줄이고, 코드의 안정성과 가독성을 높일 수 있다.
3) 제네릭(Generic)이란?
제네릭은 런타임 형변환 오류를 방지하기 위해, 자바 5(JDK 1.5)부터 도입되었다. 컴파일러가 안전하게 자동으로 형변환을 추가해줄 수 있게 되었다.
제네릭 클래스 혹은 인터페이스란, 클래스와 인터페이스 선언에 타입 매개변수(T
)가 쓰인 것을 말한다. 각각의 제네릭 타입은 일련의 매개변수화 타입을 정의한다.
제네릭 형식 : 클래스/인터페이스 이름<실제 타입 매개변수>
제네릭 타입을 하나 정의하면, 그에 딸린 로 타입(Raw Type)도 함께 정의된다.
4) 와일드 카드의 등장 이유
제네릭 도입 후 코드 수정
제네릭을 사용하면 컬렉션에 타입을 지정할 수 있어, 컴파일 시점에 타입 안전성을 보장할 수 있다. 예를 들어, Collection<Integer>
타입을 사용하여 숫자들의 합을 구하는 메서드를 작성할 수 있다.
수정된 코드 예제
위 코드에서는
Collection<Integer>
타입을 사용하여, 컬렉션이Integer
타입의 요소만 포함하도록 제한했다.컴파일 시점에 타입 검사가 이루어져, 다른 타입의 컬렉션이 전달되면 컴파일 오류가 발생합니다. 이로써 타입 안전성을 보장할 수 있다.
제네릭 타입은 불공변성(Invariance) 을 가진다. 즉, Collection<Integer>
와 Collection<Object>
는 아무런 관계가 없다. 제네릭이 도입되기 전에는 가능했던 작업이 이제는 불가능해진 경우가 발생할 수 있다.
아래와 같이 printCollection
메서드를 작성하고 List<Integer>
를 전달하려고 하면, 컴파일 오류가 발생한다.
Collection<Object>
는Collection<Integer>
의 상위 타입이 아니기 때문에, 제네릭 타입에서는 서로 호환되지 않는다. 이로 인해printCollection
메서드에List<Integer>
를 전달하려고 하면 컴파일 오류가 발생한다.이는 제네릭의 불공변성으로 인한 문제이다.
와일드카드의 도입
위와 같은 문제를 해결하기 위해 와일드카드(?)
가 도입되었다. 와일드카드를 사용하면 제네릭 타입을 보다 유연하게 사용할 수 있으며, 모든 타입의 컬렉션에서 공통으로 사용할 수 있는 메서드를 작성할 수 있다.
와일드카드 타입 사용 예제
Collection<?>
는 으로, 어떤 타입의 컬렉션이라도 인자로 받을 수 있다.List<Integer>
,List<String>
,List<Object>
등 다양한 타입의 컬렉션을 모두 전달할 수 있어, 보다 유연한 메서드를 작성할 수 있다.단, 와일드카드 타입에서는 컬렉션에 새로운 요소를 추가할 수 없고,
null
만 허용됩니다. 이는 타입 안전성을 유지하기 위함이다.
와일드 카드에 대한 설명 블로그 : https://mangkyu.tistory.com/241
2. 로(raw) 타입이란?
1) 로 타입의 정의
2) 로 타입을 사용하면 문제가 뭘까?
로 타입의 단점을 나타내주는 몇 가지 예시를 들어보자.
실수로 도장 대신 동전을 넣어도 오류 없이 컴파일 되고 실행되는 문제가 발생한다.
오류는 이상적으로 컴파일할때 발견하는 것이 좋지만, 로 타입을 사용한다면 런타임에나 오류를 발견할 수 있다.
3) 제네릭 지원 이후에는?
제네릭을 지원한 이후에는 매개변수화된 컬렉션 타입으로 타입 안전성을 확보한다. 제네릭을 사용하면 타입 선언 자체에 Stamp 인스턴스만 취급한다라는 것이 녹아든다.
컴파일 오류가 바로 발생한다. 컴파일러는 컬렉션에서 원소를 꺼내는 모든 곳에 보이지 않는 형변환
을 추가 하여 절대 실패하지 않음을 보장한다.
제네릭을 사용하여 컬렉션의 타입을 지정함으로써, 컴파일러가 타입 안전성을 보장할 수 있다.
4) 로 타입(Raw Type)과 제네릭 코드의 비교
로 타입을 사용한 코드
위 예제에서 List
는 로 타입으로 사용되었다. 이 경우, 리스트에 어떤 타입의 객체든 추가할 수 있기 때문에, 각 요소를 꺼낼 때 타입 캐스팅이 필요하다. 만약 잘못된 타입으로 캐스팅하려고 하면 ClassCastException
이 발생할 수 있다.
제네릭을 사용한 코드
위 코드에서 List<String>
은 제네릭 타입을 사용하여 선언되었다.
이제 이 리스트에는
String
타입만 저장할 수 있으며, 컴파일 시점에 타입 오류가 발생할 가능성을 줄일 수 있다. 요소를 가져올 때도 타입 캐스팅이 필요하지 않다.
5) 왜 로 타입보다 제네릭을 사용해야 할까?
타입 안전성이 확보된다.
위 예제처럼 컴파일러가 stamps
에는 Stamp
의 인스턴스만 넣어야 함을 인지하기 때문에, 다른 엉뚱한 타입의 인스턴스는 컴파일 에러를 내뱉게 된다.
올바른 인스턴스라면 컴파일러는 컬렉션에서 원소를 꺼내는 모든 곳에 보이지 않는 형변환을 추가하기 때문에 그 이후부터는 정상적으로 작동할 것이다.
로 타입은 제네릭이 안겨주는 안전성과 표현력이 없다.
하지만 그럼에도 로 타입이 존재하고 있는 것은, 로 타입을 사용하는 메서드에 매개변수화 타입의 인스턴스를 넘겨도 동작해야 했기 때문이다.
따라서 이러한 마이그레이션 호환성을 위해 로 타입을 지원하고 제네릭 구현에는 소거방식을 도입하게 된다. 소거 방식이란, 런타임에 타입 정보가 사라지는 것을 의미한다.
3. 로 타입은 권장되지 않는다(List와 List<Object>의 차이)
로 타입을 쓰면 제네릭이 안겨주는 안전성과 표현력을 모두 잃게 된다. 제네릭이 등장하기 이전의 코드와의 호환성을 위해서 로 타입이 남겨져 있다.
List
와 같은 로 타입은 권장하지 않지만 List<Object>
는 괜찮다. 모든 타입을 허용한다는 의사를 컴파일러에게 명확하게 전달한 것이기 때문이다.
그렇다면
List와 List<Object>의 차이
는 무엇일까?
List
는 제네릭 타입과 무관한 것이고, List<Object>
는 모든 타입을 허용한다는 것입니다.
다시 말해서 매개변수로 List
를 받는 메서드에 List<String>
을 넘길 수 있지만, 제네릭의 하위 규칙 때문에 List<Object>
를 받는 메서드에는 매개변수로 넘길 수 없다.
List<String>
은 로 타입인 List
의 하위 타입이지만 List<Object>
의 하위 타입은 아니기 때문이다. 위의 공변 설명에서 함 그래서 List<Object>
와 같은 매개변수화 타입을 사용할 때와 달리 List
같은 로 타입을 사용하면 타입 안전성을 잃게 된다.
위의 코드는 컴파일은 성공하지만 로 타입인 List
를 사용하여 unchecked call to add(E) as a member of raw type List...
라는 경고 메시지가 발생된다. 그런데 실행을 하게 되면 strings.get(0)
의 결과를 형변환하려 할 때 ClassCastException
이 발생한다. Integer를 String으로 변환하려고 시도했기 때문이다.
위 코드를
List<Object>
로 변경하면?
컴파일 오류가 발생하며 incompatible types: List<String> cannot be converted to List<Object>...
라는 메시지가 출력된다. 실행 시점이 아닌 컴파일 시점에 오류를 확인할 수 있어 보다 안전하다.
4. 전체 용어 정리 및 사용 코드
1. 매개변수화 타입 (Parameterized Type)
타입 매개변수를 사용해 실제 타입으로 지정된 제네릭 타입
2. 실제 타입 매개변수 (Actual Type Parameter)
매개변수화 타입에서 구체적으로 지정된 타입
List<E>
3. 제네릭 타입 (Generic Type)
타입 매개변수를 가지는 클래스나 인터페이스
E
4. 정규 타입 매개변수 (Formal Type Parameter)
제네릭 타입 또는 제네릭 메서드에서 사용되는 타입 매개변수
5. 비한정적 와일드카드 타입 (Unbounded Wildcard Type)
타입 매개변수를
?
로 지정하여 어떤 타입이든 허용함List<?>
6. 로 타입 (Raw Type)
제네릭 타입에서 타입 매개변수를 사용하지 않은 형태
List
7. 한정적 타입 매개변수 (Bounded Type Parameter)
특정 타입 또는 그 하위 타입으로 제한된 타입 매개변수
<E extends Number>
8. 재귀적 타입 한정 (Recursive Type Bound)
자기 자신을 타입 매개변수로 참조하는 타입 한정
<T extends Comparable<T>>
9. 한정적 와일드카드 타입 (Bounded Wildcard Type)
와일드카드 타입이 특정 타입 또는 그 하위/상위 타입으로 제한됨
List<? extends Number>
10. 제네릭 메서드 (Generic Method)
타입 매개변수를 사용하는 메서드
static <E> List<E> asList(E[] a)
11. 타입 토큰 (Type Token)
런타임에 제네릭 타입 정보를 제공하기 위해 사용하는 클래스 리터럴
String.class
위의 예제들은 제네릭 프로그래밍의 다양한 개념을 설명하며, 각각의 코드에 대한 주석을 통해 해당 개념이 어떻게 적용되는지 쉽게 이해할 수 있도록 돕습니다.
4. 원소의 타입을 모른채 쓰고 싶다면? 비한정적 와일드 카드 타입
제네릭을 사용하는 이유는 타입 안전성을 보장하고, 코드의 가독성과 유지보수성을 높이기 위함이다.
하지만 모든 상황에서 특정 타입을 명시할 수 없는 경우가 있기 때문에 비한정적 와일드카드(
?
)와 로 타입(Raw Type)이 존재한다.
1) 비한정적 와일드카드 타입 (Set<?>
)
Set<?>
)비한정적 와일드카드 타입은 제네릭 타입 매개변수가 무엇이든 상관없이 사용할 수 있도록 한다.
제네릭 타입의 안전성을 유지하면서도, 실제 타입 매개변수에 의존하지 않는 메서드를 작성할 수 있다.
Set<?>
와 같은 비한정적 와일드카드 타입은Set<String>
,Set<Integer>
등 어떤 타입의Set
이라도 사용할 수 있다.하지만 와일드카드 타입에서는
null
외에는 어떤 원소도 추가할 수 없다.
2) 로 타입 (Set
)
Set
)로 타입은 제네릭 도입 이전의 컬렉션 타입으로, 타입 안전성을 보장하지 않는다.
로 타입을 사용할 경우, 어떤 타입의 객체든 추가할 수 있으며, 컴파일 시점에 타입 검사가 이루어지지 않아 런타임 오류의 가능성이 높다.
제네릭 타입의 타입 정보가 런타임에 지워지기 때문에
Set<String>
과Set
은 동일하게 취급된된다.
3) 로 타입과 비한정적 와일드카드 타입의 차이
특성
로 타입 (Set
)
비한정적 와일드카드 (Set<?>
)
타입 안전성
보장되지 않음
보장됨
타입 불변식 유지
위반하기 쉬움
타입 불변식 유지
원소 추가
어떤 타입의 원소도 추가 가능
null
외에는 추가할 수 없음
메서드 호출
타입에 관계없이 사용 가능
제네릭 타입에 의존하지 않는 메서드만 사용 가능
사용 가능 상황
하위 버전과의 호환성 필요 시, 클래스 리터럴, instanceof
제네릭 타입에 의존하지 않는 메서드 작성 시
6. 로 타입이 필요한 예외적인 상황
1) 클래스 리터럴
제네릭 타입은 에서 사용할 수 없다. List.class
와 같은 로 타입만 사용할 수 있으며, List<String>.class
나 List<?>.class
는 허용되지 않는다.
2) instanceof
연산자
instanceof
연산자제네릭 타입 정보는 런타임에 제거되므로, instanceof
연산자는 로 타입이나 비한정적 와일드카드 타입에서만 사용할 수 있다. Set<?>
을 사용해 타입 캐스팅을 할 수 있다.
5. 정리
로 타입은 타입 안전성이 보장되지 않기 때문에, 사용을 피해야 합니다. 로 타입을 사용하는 주요 이유는 하위 버전과의 호환성 때문입니다.
**비한정적 와일드카드 타입 (
<?>
)**은 제네릭 타입의 타입 매개변수를 알 수 없거나 신경 쓰지 않아도 될 때 사용합니다. 타입 안전성을 유지하면서 제네릭을 사용할 수 있습니다.로 타입과 비한정적 와일드카드 타입은 클래스 리터럴과
instanceof
연산자와 같은 특정 상황에서 로 타입이 필요할 때만 사용해야 하며, 그 외에는 제네릭 타입을 사용하는 것이 좋습니다.
✨ 최종 정리
로 타입을 사용하면 런타임에 예외가 일어날 수 있으니 사용하면 안 된다. 오직 하위 버전과 호환하기 위해서 남아있다.
로 타입은 제네릭이 도입되기 이전 코드와의 호환성을 위해 제공될 뿐이다.
Set<Object>
는 어떤 타입의 객체도 저장할 수 있는 매개변수화 타입이고,Set<?>
는 모종의 타입 객체만 저장할 수 있는 와일드카드 타입이다.이들의 로 타입인 Set은 제네릭 타입 시스템에 속하지 않는다. Set<Object>와 Set<?>는 안전하지만, 로 타입인 Set은 안전하지 않다.
참고 글 :
[이펙티브 자바 3판] 아이템 26. 로 타입은 사용하지 말라
[JAVA] 제네릭과 와일드카드 타입에 대해 쉽고 완벽하게 이해하기(공변과 불공변, 상한 타입과 하한 타입)
[ java] generic type erasure란 무엇이띾
Last updated