[item-26] 로 타입은 사용하지 말라
제네릭 타입: 클래스 혹은 인터페이스 선언에 타입 매개변수가 쓰이는 경우
로 타입: 제네릭 타입에서 타입 매개변수를 사용하지 않는 것
- ex) List, Set ..(제네릭 타입) → List<String>, Set<String>
- 제네릭 타입을 하나 정의하면 Raw Type도 함께 정의됩니다.
- Raw Type은 제네릭 타입에서 타입 매개변수를 전혀 사용하지 않을 때를 의미합니다.
- Raw Type은 타입 선언에서 제네릭 타입 정보가 전부 지워진 것처럼 동작합니다.
로 타입을 절대 쓰지 말자
- 자바 언어 차원에서 로 타입 사용을 막지는 않았지만, 절대로 사용하지 말자
- 로 타입을 쓰면 제네릭의 안정성과 표현력을 모두 잃게 됩니다.
- 로 타입은 호환성 때문에 만들어졌습니다.
로 타입을 사용
private final Collection stamps = ...;
stamps.add(new Coin(...));
// unchecked call "경고"를 호출하지만 컴파일도 되고 실행도 됩니다
- add 메서드 호출 시, ClassCastException 오류 발생 (Runtime Exception)
제네릭 사용
private final Collection<Stamp> stamps = ...;
stamps.add(new Coin()); // 컴파일 오류 발생
- 컴파일 오류가 바로 발생
로 타입과 Object
- List 같은 로 타입은 사용해서는 안되지만, List <Object>처럼 임의 객체를 허용하는 매개변수화 타입은 괜찮습니다.
- List는 제네릭 타입에서 완전히 발을 뺀 것입니다.
- List<Object>는 모든 타입을 허용한다는 의사를 컴파일러에게 명확히 전달해야 합니다.
- 매개변수로 List를 받는 메서드에 List<String>을 넘길 수 있지만, List<Object>를 받는 메서드에는 넘길 수 없습니다.
- rawType을 사용하면 컴파일은 되지만, 실행하면 ClassCastException이 발생합니다. 하지만, 매개변수화 타입을 사용하면 컴파일조차 되지 않습니다.
로 타입을 사용한 경우 (list)
- 런타임 시점에, strings.get(0) 호출 시 ClassCastException 발생
public static void main(String[] args) {
List<String> strings = new ArrayList<>();
add(strings, Integer.valueOf(0));
String s = strings.get(0);
}
// raw type
private static void add(final List list, final Object o) {
list.add(o);
}
List<Object> 변경 시
- 컴파일 에러, incompatible types: List<String> cannot be converted to List<Object> 메시지 출력
public static void main(String[] args) {
List<String> strings = new ArrayList<>();
add(strings, Integer.valueOf(0));
String s = strings.get(0);
}
// raw type
private static void add(final List<Object> list, final Object o) {
list.add(o);
}
Raw 타입 예시 (잘못 사용 예시)
- 동작은 하지만 안전하지 않습니다
- 그렇기에 비한정적 와일드 카드 타입인 ?를 대신해서 사용하는 게 좋습니다.
- 제네릭을 사용하고 싶지만, 실제 타입 매개변수가 무엇인지 신경 쓰고 싶지 않을 때 사용합니다.
- Set<E>의 비한정적 와일드카이드 타입은 Set<?>
- Set<?>은 어떤 타입의 Set이든 참조할 수 있는 범용 타입이지만, 타입을 모르므로 요소는 추가할 수 없습니다(null 제외)
Example
public class TypeTest {
private static void addtoObjList(final List<Object> list, final Object o) {
list.add(o);
}
private static void addToWildList(final List<?> list, final Object o) {
// null 외에 허용되지 않는다
list.add(o);
}
private static <T> void addToGenericList(final List<T> list, final T o) {
list.add(o);
}
public static void main(String[] args) {
List<String> list = new ArrayList<>();
String s = "string";
// 메서드에서 Error
addToWildList(list, s);
// List<Object> 이므로 incompatible types 오류
addtoObjList(list, s);
// 가능
addToGenericList(list, s);
}
}
로 타입을 사용할 수 있는 예외사항: instanceof
- instanceof: 부모를 상속해서 만들어진 자식 객체가 여러 타입인 경우에 특정 클래스가 맞는지 확인하기 위한 메서드
ex 1) piece 객체가 Empty라는 클래스 타입인지를 확인하는 메서드
if (piece instanceof Empty) {
return;
}
ex 2) o 타입이 set 클래스 타입인지 확인하는 메서드
// o의 타입이 Set인지 확인한 다음, 와일드카드 타입으로 형변환해야 합니다.
// 여기서 로 타입인 Set이 아닌 와일드카드 타입으로 변환함에 주의!
if( o instanceof Set) {
Set<?> s = (Set<?>) o;
}
다형성 적용 vs instanceof
// 다형성
public abstract class Piece {
public abstract int calculate(int point);
}
public class King extends Piece {
public int calculate(int point) {
return point + 10;
}
}
public class Pawn extends Piece {
public int calculate(int point) {
return point + 1;
}
}
public class Empty extends Piece {
public int calculate(int point) {
return point;
}
}
public class Point {
public int calculate(Piece p, int point) {
return p.calculate(point);
}
}
// instanceof
public class Point {
public int calculate(Piece p, int point) {
if(p instanceof King) {
return point + 10;
} else if(p instanceof Pawn) {
return point + 1;
} else if(p instanceof Empty) {
return point;
}
}
}
- instanceof 대신 다형성을 권장합니다.
- instanceof 사용 시, 캡슐화를 깨뜨릴 뿐만 아니라, OCP, SRP 원칙에 위배됩니다.
[item-27] 비검사 경고를 제거하라
- 비검사 경고 (unchecked warnings)를 제거하면 런타임에 형변환 관련 예외(ClassCastException)가 발생할 일이 없으며 코드의 올바른 동작도 기대할 수 있게 합니다.
- @SuppressWarnings(”unchecked”) 어노테이션을 붙여 경고를 숨기는게 좋습니다.
- @SuppressWarnings: 컴파일 경고를 사용하지 않도록 설정해 주는 어노테이션
- unused, serial, all, deprecation, null, unchecked ..
[item-28] 배열보다는 리스트를 사용하라
배열 vs 제네릭
배열
- 공변(covariant): Sub 클래스가 Super라는 클래스의 하위 타입이라면, 배열 Sub[]는 배열 Super[]의 상위 타입이 되는 것
제네릭
- 불공변(invariant): 서로 다른 Type1, Type2가 있을 때, List<Type1>, List<Type2>는 서로 상, 하위 타입이 될 수 없는 것
Object[] array = new Long[1];
array[0] = "String"; // 컴파일은 되지만, 런타임 시에 오류가 발생한다.
List<Object> list = new ArrayList<Long>();
list.add("String"); // 컴파일조차 되지 않는다.
배열은 런타임 시점에도 자신이 담기로 한 원소의 타입인지를 확인합니다. 반면, 제네릭은 타입 정보가 런타임 시점에는 소거됩니다. 이를 통해 런타임 시점에 ClassCastException을 만나지 않고, 컴파일 시점에 오류를 잡을 수 있습니다.
배열에서는 위와 같은 실수를 런타임 시점 (애플리케이션 실행 시점)에 알 수 있지만, 리스트는 코드를 실행하기 전에 알 수 있습니다. 그래서 더욱 안전합니다.
[item-29] 이왕이면 제네릭 타입으로 만들라
- 클라이언트에서 직접적으로 형변환을 해야 하는 타입보다는 제네릭 타입이 더 안전하고 사용 기하기에도 편리합니다.
- 그러므로 새로운 타입을 설계할 때는 형변환 없이도 사용할 수 있도록 하는 것이 좋습니다.
- 과정
- 클래스 선언에 타입 매개변수를 추가합니다. (class {name}<T>)
- 일반 타입을 타입 매개변수로 바꾸면 됩니다. (T {variable name})
클라이언트에서 형 변환을 해야 하는 경우
- 타입 안정성이 없으며, 실수로 잘못된 형 변환 시 ClassCastException 예외 발생
class Box {
private Object item;
public void set(Object item) {
this.item = item;
}
public Object get() {
return item;
}
}
Box box = new Box();
box.set("item");
String message = (String) box.get(); // 형변환 필요
제네릭을 활용한 개선
- 컴파일 타임에 타입을 체크하고, 형변환이 필요 없을 뿐만 아니라 타입 안정성까지 확보할 수 있게 됩니다.
class Box<T> {
private T item;
public void set(T item) {
this.item = item;
}
public T get() {
return item;
}
}
Box<String> box = new Box<>();
box.set("item");
String message = box.get(); // 형변환 불필요
[item-30] 이왕이면 제네릭 메서드로 만들라
ex) 타입 매개변수 목록 <E>, 반환 타입 Set<E>
// 제네릭 메서드
public static <E> Set<E> union(Set<E> s1, Set<E> s2) {
Set<E> result = new HashSet<>(s1);
result.addAll(s2);
return result;
}
- 위 코드는 경고 없이 컴파일되며, 타입 안전하며 쓰기도 쉽습니다.
- 제네릭은 런타임에 타입 정보가 소거되므로 하나의 객체를 어떤 타입으로는 매개변수화 할 수 있습니다.
- 이를 통해 불변 객체를 여러 타입으로 만들 수 있습니다.
재귀적 타입한정 적용
puiblic static <E extends Comparable<E>> E max(Collection<E> c);
- E로 받을 타입은 오직 Comparable <E>를 구현한 타입만 가능하다는 뜻입니다.
- 즉, Comparable을 구현한 타입만 가능하다는 뜻입니다.
[item-31] 한정적 와일드카드를 사용해 API의 유연성을 높여라
- 제네릭은 불공변이기 때문에 하위 타입 객체를 추가하는 경우에 문제가 발생하기 쉽습니다.
- 이러한 경우에는 매개변수에 한정적 와일드 카드를 적용함으로써 하위 객체 또는 상위 객체까지 연산을 적용할 수 있습니다.
- 반환 값에 한정적 와일드카드를 적용하는 것은 오히려 유연성을 깨뜨립니다.
Iterable<? extends E> src; // E의 하위 타입
Iterable<? super E> src; // E의 상위 타입
PECS (Producer-Extends-Consumer-Super)
- 매개변수 타입이 생산자를 나타내면 <? extends T>를 사용합니다.
- 매개변수 타입이 소비자를 나타내면 <? super T>를 사용합니다.
<? extends T> (stack: push)
public void pushAll(Iterable<E> src) {
for (E e : src) {
push(e);
}
}
<? super T> (stack: pop)
public void popAll(Collection<? super E> dst) {
while(!isEmpty()) {
dst.add(pop());
}
}
Advanced
public class Union {
public static <E> Set<E> union(Set<? extends E> s1, Set<? extends E> s2) {
Set<E> result = new HashSet<>(s1);
result.addAll(s2);
return result;
}
public static void main(String[] args) {
// Set.of 메서드는 java 9 이상부터 지원
Set<Double> doubleSet = Set.of(1.0, 2.1);
Set<Integer> integerSet = Set.of(1, 2);
Set<Number> unionSet = union(doubleSet, integerSet);
}
}
- 재귀적 한정 타입을 적용한 부분 Comparable은 E 인스턴스를 소비하는 소비자이므로 super가 적용해야 합니다.
// 변경 전
public static <E extends Comparable<E>> E max(Collection<E> collection)
// 변경 후(PECS 공식 2번 적용)
public static <E extends Comparable<? super E>> E max(Collection<? extends E> collection)
타입 매개변수가 한 번만 나오는 경우
- 와일드카드로 대체하는 것이 좋습니다.
public static <E> void swap(List<E> list, int i, int j); (X)
public static void swap(List<?> list, int i, int j); (O)
[item-32] 제네릭과 가변인수를 함께 쓸 때는 신중하라
- 가변인수 메서드를 호출하면, 가변인수를 담기 위한 배열이 자동으로 생성
- 가변인수(Varargs): 매개변수의 개수가 정해져 있지 않은 메서드 인자를 받을 때 사용하는 문법
static void dangerous(List<String>... stringLists) {
List<Integer> intList = List.of(42);
Object[] objects = stringLists;
objects[0] = intList; // 힙 오염 발생
String s = stringLists[0].get(0); // ClassCastException
}
public static void main(String[] args) {
List<String> stringList = new ArrayList<>();
stringList.add("Hi there");
dangerous(stringList);
}
제네릭이나 매개변수화 타입의 varargs 매개변수를 받는 메서드 대표적 예시
- Arrays.list(T… a), EnumSet.of(E first, E… set)
@SafeVarargs // 제네릭 가변인수 관련 컴파일 경고를 숨기기 위한 어노테이션
@SuppressWarnings("varargs")
public static <T> List<T> asList(T... a) {
return new ArrayList<>(a);
}
@SafeVarargs
public static <E extends Enum<E>> EnumSet<E> of(E first, E... rest) {
EnumSet<E> result = noneOf(first.getDeclaringClass());
result.add(first);
for (E e : rest)
result.add(e);
return result;
}
'Java > Effective Java' 카테고리의 다른 글
[Effective Java] 6장. 열거 타입과 애너테이션 (0) | 2025.05.20 |
---|---|
[Effective Java] 4장. 클래스와 인터페이스 (0) | 2024.08.08 |
[Effective Java] 3장. 모든 객체의 공통 메서드 (0) | 2024.08.08 |
[Effective Java] 2장. 객체 생성과 파괴 (0) | 2024.08.08 |