이펙티브 자바 아이템 26 - 로 타입은 사용하지 말라 - 핵심 정리
아이템 26 - 로 타입은 사용하지 말라 - 핵심 정리
이 글은 백기선 님의 이펙티브 자바 강의와 이펙티브 자바 3 / E 편을 참고하여 작성하였습니다.
제네릭은 자바 5 버전 부터 들어온 기능이다.
public class GenericBasic {
public static void main(String[] args) {
// Generic 사용하기 전
List numbers = new ArrayList();
numbers.add(10);
numbers.add("whiteship");
for (Object number: numbers) {
System.out.println((Integer)number); // 오류 발생
}
}
}
제네릭이 도입되기 전에는 위와같이 List
를 타입없이 정의를 했다.
이렇게 타입없이 정의하는 걸 로 타입 이라고 한다.
타입을 정의할 수 있음에도 불구하고 선언하지 않는 걸 말한다.
로 타입 을 사용하는 경우에는 해당 컬렉션에 Object 로 값이 들어가게된다.
이렇게되면 꺼내서 사용을 할 때 오류가 발생할 가능성이 크다.
때문에 버그를 추적하기가 쉽지 않다.
public class GenericBasic {
public static void main(String[] args) {
// Generic 등장 이후
List<Integer> nuberms = new ArrayList<>();
nuberms.add(10);
nuberms.add("whiteship"); // 컴파일 에러
for (Integer number: nuberms) {
System.out.println(number);
}
}
}
Generic 등장 이후에는 위와같이 사용한다.
List<Integer>
를 선언해 List
에는 Integer
밖에 넣을 수 없다.
때문에 다른 타입의 값을 넣게되는 경우 컴파일 타임에 확인이 가능해진다.
로 타입 을 사용하면 형변환 이 필요하지만 Generic 을 사용하면 형변환 이 필요없어진다.
이러한 이유로 우리는 Generic 을 사용하게 된다.
용어 정리
public class Box<E> {
private E item;
private void add(E e) {
this.item = e;
}
private E get() {
return this.item;
}
public static void main(String[] args) {
Box<Integer> box = new Box<>();
box.add(10);
System.out.println(box.get() * 100);
printBox(box);
}
private static void printBox(Box<?> box) {
System.out.println(box.get());
}
}
제네릭 타입은 코드를 정의하는 입장과 사용하는 입장으로 나눠 생각하면 이해가 쉽다.
public class Box<E> {
}
클래스 선언부에 Box<E>
는 해당 클래스가 E
라는 매개변수를 사용할 수 있다.
이러한 클래스를 제네릭 클래스 라고 부른다.
이떄 E
는 D
라고 해도 무방하며 선언하는 사람의 자유이다.
private E item;
private void add(E e) {
this.item = e;
}
private E get() {
return this.item;
}
제네릭 클래스 는 E
라는 어떠한 타입의 매개변수를 정의할 수 있고,
해당 타입을 파라미터로 받는 메서드, return 하는 메서드를 선언할 수 있다.
이때 E
는 타입 매개변수 라고 부른다.
public static void main(String[] args) {
Box<Integer> box = new Box<>();
box.add(10);
System.out.println(box.get() * 100);
printBox(box);
}
Box<E>
클래스의 E
자리에 Box<Integer>
와 같이 Integer
를 넣었다.
이때 Integer
를 실제 타입 매개변수 라고 한다.
Box<Integer>
를 매개 변수화 타입 이라고 부른다.
Integer 라는 특성이 매개 변수화 되어있는 Box 라는 타입이다.
public class Box<E> {
}
위의 제네릭 클래스에서 E
는 아무거나 선언이 가능하다.
E
의 타입을 한정적으로 제한할 수 있는데
public class Box<E extends Number> {
}
<E extends Number>
를 한정적 타입 매개변수 라고 한다.
Number 클래스를 상속받은 클래스들로 제한할 수 있다.
public static void main(String[] args) {
Box<Integer> box = new Box<>(); // 가능하다.
Box<String> strBox = new Box<>(); // 불가능하다.
}
<E extends Number>
로 한정적 타입 매개변수 를 선언했기 때문에
Number 클래스를 상속받지 않은 String
클래스를 실제 타입 매개변수로 사용할 수 없다.
public static void main(String[] args) {
Box<?> box = new Box<>();
box.add(10);
System.out.println(box.get() * 100);
printBox(box);
}
타입을 선언하는 곳에서 ?
를 사용할 수 있는데 이를 비한정적 와일드 카드 타입 이라고 한다.
<? extends Number>
처럼 extends
나 super
가 없는 경우를 비한정적 와일드 카드 타입 이라고 한다.
비한정적 와일드 카드 타입 은 아무런 타입이나 대응이 된다.
<?>
는 <? extends Object>
가 생략된 것이다.
비한정적 와일드 카드 타입 을 extends
나 super
를 써서 타입 한정을 지을 수 있다.
<? extends Number>
는 한정적 와일드 카드 타입 이라고 한다.
이러한 와일드 카드는 컬렉션에 무언가를 넣을 때 사용하는게 아닌다.
와일드 카드 타입 으로 컬렉션을 선언하면 아무것도 넣을 수 없게된다.
public class Box<E> {
private E item;
private void add(E e) {
this.item = e;
}
private E get() {
return this.item;
}
public static void main(String[] args) {
Box<Integer> box = new Box<>();
box.add(10);
box.add("string"); // 불가능 하다.
System.out.println(box.get() * 100);
printBox(box);
}
private static void printBox(Box<?> box) {
System.out.println(box.get());
}
}
private static void printBox(Box<?> box) {
System.out.println(box.get());
}
printBox
메서드처럼 와일드 카드 타입 은 매개변수의 타입으로 사용해야한다.
printBox
는 비한정적 와일드 카드 타입 으로 매개변수를 받기 때문에 아무런 Box 타입이나 전달이 가능하다.
Box<Integer>
와 Box<Object>
는 엄연히 다른 타입이다.
매개변수화 타입을 사용해야 하는 이유
public class GenericBasic {
public static void main(String[] args) {
// Generic 사용하기 전
List numbers = new ArrayList();
numbers.add(10);
numbers.add("whiteship");
for (Object number: numbers) {
System.out.println((Integer)number);
}
}
}
Generic 을 사용하기 전에는 아무 타입이나 컬렉션에 넣을 수 있기 때문에 안정성 이 깨지게 된다.
public class GenericBasic {
public static void main(String[] args) {
// Generic 등장 이후
List<Integer> nuberms = new ArrayList<>();
nuberms.add(10);
nuberms.add("whiteship");
for (Integer number: nuberms) {
System.out.println(number);
}
}
}
Generic 을 사용하면 안정성이 깨지지 않게된다.
그리고 코드에 컬렉션에 어떤 타입이 들어가게되는지 선언시에 나타낼 수 있다.
표현력 이 올라간다.
List<Integer>
를 통해 Integer
타입이 들어가게 되는 걸 명확하게 알 수 있다.
자바는 하위버전 호환성을 위해 로 타입 을 허용했다.
Generic 을 컴파일하면 모든 Generic 타입이 사라지게된다.
public class Box<E> {
private E item;
private void add(E e) {
this.item = e;
}
private E get() {
return this.item;
}
public static void main(String[] args) {
Box<Integer> box = new Box<>();
box.add(10);
System.out.println(box.get() * 100);
printBox(box);
}
private static void printBox(Box<?> box) {
System.out.println(box.get());
}
}
위의 제네릭 클래스의 일부 바이트 코드를 보면
INVOKEVIRTUAL effective/code/chapter04/item26/terms/Box.get ()Ljava/lang/Object;
CHECKCAST java/lang/Integer
INVOKEVIRTUAL java/lang/Integer.intValue ()I
BIPUSH 100
위처럼 전체 타입이 Object
로 되어있고
CHECKCAST java/lang/Integer
Integer
로 타입 캐스팅하는 코드가 들어가 있다.
즉, Box<Integer>
는 소스 코드에만 보이는 정보인 것이다.
사실상 컴파일 된 코드는 로 타입처럼 보이지만 Box<Integer>
를 통해 컴파일러가 Integer
로 형변환하는 코드를 넣어주게된다.
우리는 코드를 편하게 작성하지만 컴파일된 코드 중간 중간에 로 타입 을 쓰던 버전과 비슷하게
타입을 캐스팅 하는 코드가 들어가게 된다.
실질적으로 컴파일된 클래스 파일에서는 로 타입 이 사용되고 소스 코드에서도 로 타입 이 지원된다.
자바의 하위버전 호환성을 유지하기 위함이다.
List
와 List<Object>
의 차이
// 코드 26-4 런타임에 실패한다. - unsafeAdd 메서드가 로 타입(List)을 사용 (156-157쪽)
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) {
list.add(o);
}
}
그렇다면 raw 타입과 Object 타입에는 어떤 차이가 있을까?
unsafeAdd
메서드를 보면 매개변수 타입을 굉장히 넓은 범위로 받고 있다.
어떤 값이 오던지 List
에 값을 넣어주게 된다.
위의 코드에서는 잘못된 값을 넣을 때 에러가 발생하는게 아니라 잘못된 값을 꺼낼 때 에러가 발생한다.
private static void unsafeAdd(List<Object> list, Object o) {
list.add(o);
}
매개변수 타입을 위처럼 변경하면 값을 아예 넣을 수없다.
List<String>
과 List<Object>
다른 타입이기 때문이다.
때문에 조금 더 안전한 코드를 사용할 수 있게된다.
private static void unsafeAdd(List list, Object o) {
list.add(o);
}
List
와 같이 로 타입 을 사용하는 것을 타입 안정성을 잃었다 라고 한다.
Set
과 Set<?>
의 차이
public class Numbers {
static int numElementsInCommon(Set s1, Set s2) {
int result = 0;
for (Object o1 : s1) {
if (s2.contains(o1)) {
result++;
}
}
return result;
}
public static void main(String[] args) {
System.out.println(Numbers.numElementsInCommon(Set.of(1, 2, 3), Set.of(1, 2)));
}
}
numElementsInCommon
메서드는 전달받은 두 개의 Set
에서 공통인자의 갯수를 세는 메서드이다.
코드를 실행해보면 원하는 값이 잘 나온다.
하지만 위처럼 로 타입 을 사용하면 안정성 이 깨지게 된다.
매개변수로 아무 타입의 Set
을 전달할 수 있게된다.
static int numElementsInCommon(Set s1, Set s2) {
s1.add("AWDASDASD");
int result = 0;
for (Object o1 : s1) {
if (s2.contains(o1)) {
result++;
}
}
return result;
}
위처럼 해당 컬렉션에 아무거나 넣을 수 있게된다.
static int numElementsInCommon(Set<?> s1, Set<?> s2) {
s1.add("AWDASDASD");
int result = 0;
for (Object o1 : s1) {
if (s2.contains(o1)) {
result++;
}
}
return result;
}
위처럼 Set<?>
로 매개변수를 받도록 수정한다.
<?>
를 사용하면 어떠한 타입이든 한 종류의 타입을 가지고 있는 Set
을 의미한다,
System.out.println(Numbers.numElementsInCommon(Set.of(1, 2, 3), Set.of(1, 2)));
위의 코드에서 Set.of(1, 2, 3)
는 하나 종류의 타입 만을 다루고 있다.
때문에 numElementsInCommon(Set<?> s1, Set<?> s2)
에 매개변수로 전달 할 수 있다.
Set<?>
이 어떠한 타입이라도 담을 수 있는 가장 범용적인 매개변수화 Set 타입 이다.
Set<Integer>
이나 Set<String>
도 Set<?>
로 받을 수 있다.
Set
와 Set<?>
의 차이점은 안정성이다.
Set
에는 아무거나 추가할 수 있지만 Set<?>
에는 null
외에는 아무것도 넣을 수 없다.
때문에 안전한 Set
이 된다.
모든 경우에 제네릭에 타입을 선언해 사용하는 것이 좋은 습관이다.
단, 두가지의 예외가 있는데
public class UseRawType<E> {
private E e;
public static void main(String[] args) {
System.out.println(UseRawType.class); //UseRawType<Integer>.class -> 컴파일 에러
UseRawType<String> stringType = new UseRawType<>();
System.out.println(stringType instanceof UseRawType);
}
}
UseRawType.class
처럼 .class
는 매개변수화 타입과 같이 사용할 수 없다.
UseRawType<Integer>.class
은 컴파일 했을 시 존재하지 않기 때문이다.
<Integer>
는 컴파일시 소거되기 때문에 UseRawType
라는 클래스만 남기 때문이다.
다른 경우로는 instanceof
가 있다.
stringType instanceof UseRawType<Integer>
처럼 제네릭 타입을 사용할 수는 있지만 어처피 소거되기 때문에 의미가 없다.
instanceof
에서 제네릭 타입을 사용하는 것은 코드를 장황하게 만들 뿐이다.
.class
와 instanceof
를 사용하는 경우를 제외하고 모두 매개변수화 타입 을 사용하는 것을 권장한다.