아이템 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 를 사용하는 경우를 제외하고 모두 매개변수화 타입 을 사용하는 것을 권장한다.
'개발 공부 > Java' 카테고리의 다른 글
| 이펙티브 자바 아이템 27 - 비검사 경고를 제거하라 - 핵심 정리 (0) | 2022.12.08 |
|---|---|
| 이펙티브 자바 아이템 26 - 로 타입은 사용하지 말라 - 완벽 공략 (0) | 2022.12.07 |
| 이펙티브 자바 아이템 25 - 톱 레벨 클래스는 한 파일에 하나만 담으라 - 핵심 정리 (0) | 2022.12.02 |
| 이펙티브 자바 아이템 24 - 멤버 클래스는 되도록 static 으로 만들라 - 완벽 공략 (1) | 2022.12.02 |
| 이펙티브 자바 아이템 24 - 멤버 클래스는 되도록 static 으로 만들라 - 핵심 정리 (1) | 2022.12.02 |
댓글