개발 공부/Java

이펙티브 자바 아이템 26 - 로 타입은 사용하지 말라 - 핵심 정리

개발인생 2022. 12. 7. 11:04
반응형

아이템 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 라는 매개변수를 사용할 수 있다.

이러한 클래스를 제네릭 클래스 라고 부른다.

이떄 ED 라고 해도 무방하며 선언하는 사람의 자유이다.

    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> 처럼 extendssuper 가 없는 경우를 비한정적 와일드 카드 타입 이라고 한다.

비한정적 와일드 카드 타입 은 아무런 타입이나 대응이 된다.

<?><? extends Object> 가 생략된 것이다.

비한정적 와일드 카드 타입extendssuper 를 써서 타입 한정을 지을 수 있다.

<? 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 로 형변환하는 코드를 넣어주게된다.

우리는 코드를 편하게 작성하지만 컴파일된 코드 중간 중간에 로 타입 을 쓰던 버전과 비슷하게

타입을 캐스팅 하는 코드가 들어가게 된다.

실질적으로 컴파일된 클래스 파일에서는 로 타입 이 사용되고 소스 코드에서도 로 타입 이 지원된다.

자바의 하위버전 호환성을 유지하기 위함이다.


ListList<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 와 같이 로 타입 을 사용하는 것을 타입 안정성을 잃었다 라고 한다.


SetSet<?>의 차이

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<?> 로 받을 수 있다.

SetSet<?> 의 차이점은 안정성이다.

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 에서 제네릭 타입을 사용하는 것은 코드를 장황하게 만들 뿐이다.

.classinstanceof 를 사용하는 경우를 제외하고 모두 매개변수화 타입 을 사용하는 것을 권장한다.

반응형