티스토리 뷰

반응형

대부분 제네릭에 대해 잘 알고 있다고 시작하는 포스팅이니 잘 모른다면 기본을 공부해보고 오는 것을 추천한다. 

 

https://blog.naver.com/ilgolc/222552815030

 

Java_Week14

Generics 란? 제네릭은 다양한 타입의 객체들을 다루는 메서드나 컬렉션 클래스에 컴파일 시의 타입 체크...

blog.naver.com

 

제네릭 기초에 관한 내용들이 포스팅 되어 있으니 한 번 보고 이 글을 읽기를 바란다.

 

제네릭은 자바 5 부터 나온 문법으로 기본적으로 컬렉션에서 많이 볼 수 있었을 것이다. 제네릭은 다음 처럼 사용할 수 있다.

List<Integer> list = new ArrayList<>();  // 뒤에 선언 부에는 생략이 가능

 

그럼 바로 시작해보자.

 

로 타입 보다 Generic

제네릭을 사용하지 않는다면 그것을 로 타입이라고 부른다. 예시를 살펴보면 다음과 같다.

private List users = new ArrayList();
private Set userSet = new HashSet();

이런 식으로 선언을 할 수 있는데 이러한 경우 컴파일러에서 형 변환 코드를 알아서 넣어주지 못하기 때문에 잘못된 타입의 요소를 넣어도 문제 없이 컴파일이 된다. 그렇기 때문에 위험할 수 있다.

 

다음의 예시를 보자.

public class FoodsWithRawType {

    private List foods = new ArrayList();

    public void addFood(Object obj) {
        foods.add(obj);
    }

    public void print() {
        for (Object food : foods) {
            Food f = (Food) food;

            System.out.println("이름 : " + f.getName());
            System.out.println("맛 : " + f.getTaste());
        }
    }
}

위와 같이 선언 할 경우 문제없이 코드가 잘 돌아간다. 하지만 큰 단점이 존재한다. 만약 이 코드를 실행해 본다고 가정해보자

public class FoodAppWithRawType {

    public static void main(String[] args) {
        FoodsWithRawType foodsRaw = new FoodsWithRawType();

        Food steak = new Food("스테이크", "맛있음");
        Food chicken = new Food("치킨", "짜면서 맛있음");
        Food bread = new Food("빵", "맛있음");
        Toy gunDam = new Toy("건담", "로봇");

        foodsRaw.addFood(steak);
        foodsRaw.addFood(chicken);
        foodsRaw.addFood(bread);
        foodsRaw.addFood(gunDam);
        
        foodsRaw.print();
    }
}

이렇게 코딩을 해도 전혀 컴파일러 쪽에선 에러나 경고메시지를 보내지 않는다. 그냥 그대로 모든 타입의 요소를 받는 것이다. 그리고 이는 충분히 현업단계에서도 나올 수 있는 실수이다.

 

그걸 눈치 못채고 실행하고 나서 런타임 단계에 들어서야 비로소 이 문제를 알 수 있다.

잘못된 값이 들어갔을 때까지도 알지 못한다. 그 값을 꺼내서 특정 타입에 맞는 동작을 할 때 비로소 이러한 문제를 알 수 있는 것이다. 그렇기 때문에 절대 사용해서 안되는 타입인 것이다.

 

그렇다면 제네릭은 이를 얼마나 훌룡하게 처리해줄까?

package generic;

import java.util.ArrayList;
import java.util.List;

public class FoodsWithGeneric {

    private final List<Food> foods = new ArrayList<>();

    public void addFood(Food obj) {
        foods.add(obj);
    }

    public void print() {
        for (Food food : foods) {
            System.out.println("이름 : " + food.getName());
            System.out.println("맛 : " + food.getTaste());
        }
    }
}

<>안에 특정 타입을 정의해 주면서 형변환의 번거로움도 없애줬을 뿐더러 타입에 대한 안정감 까지 더해주었다. 제네릭을 사용하면 컴파일러에서 잘못된 요소가 들어가는 경우 경고를 주기 때문이다. 

그렇기 때문에 현업 단계에서는 매우 중요한 문법이라고 할 수 있다. 

 

그렇다면 이런 로타입이 존재하는 이유는 무엇일까?

 

로 타입이 아직 남아있는 이유?

바로 호환성 때문이다. HashTable이나 Vector처럼 로 타입이 동작을 해야만 Generic도 동작하기 때문에
이러한 호환성 때문에 이러한 로직이 남아 있는 것이다. Generic은 자바 5부터 나온 문법으로 출시 당시
나온 문법이 아니기 때문이다. 그렇기 때문에 제네릭 구현 후에는 Eraser를 사용해 소거 한다.

 

아무튼 그럴일이 극히 드물겠지만 알아두고 로 타입을 사용하는 일은 없도록하자.

 

로 타입과 <Object>

하지만 이펙티브 자바에 따르면 List<Object>는 허용을 하고 있다. 이유는 무엇일까? 그렇다면 먼저 이 둘의 차이점을 알아야 한다.  먼저 List<Object>는 명확하게 모든 타입에 대해 허용하겠다는 것이 명시되어있다는 것이 로 타입이랑 다른 점이다. 

public class GenericObjectType {

    public static void main(String[] args) {
        List<String> list = new ArrayList<>();

        unsafeAdd(list, 42);
        String s = list.get(0);
    }

    private static void unsafeAdd(List<Object> list, Object o) {
        list.add(o);
    }
}
public class GenericObjectType {

    public static void main(String[] args) {
        List<String> list = new ArrayList<>();

        unsafeAdd(list, 42);
        String s = list.get(0);
    }

    private static void unsafeAdd(List list, Object o) {
        list.add(o);
    }
}

두 코드의 다른 점을 이해하기 바란다. unsafeAdd메서드를 주의깊게 보면 단지 지역 변수로 로타입의 List와 Object를 받는 List로 나눠져 있을 뿐이지만 차이는 꽤 크다. 

밑에 코드는 정상적으로 실행되는 반면 위에 Object로 제네릭을 선언 했을 시에는 컴파일 조차 되지 않고 오류를 뱉는다. 

 

다음 메시지를 꼭 기억하기 바란다.

List<Object> 같은 매개변수화 타입을 사용할 때와 달리 List 같은 로 타입은 타입 안정성을 잃게 만든다.

 

로 타입 대신 비 한정적 와일드 카드 타입

실제 타입 매개변수가 무엇인지 신경 쓰고 싶지 않은 경우가 생길 수 있다. 이 때 우리는 로 타입을 떠올릴 수 있는데 자바는 이러한 상황에 대비하여 만든 타입이 존재한다. 바로 비 한정적 와일드 카드 타입이다. 가장 범욕적인 매개 변수화 타입으로 선언은 간단하다. 

Set<?> set = new HashSet<>();

이 둘의 차이는 타입의 안정성이다. 로 타입 컬렉션에는 아무 원소나 넣을 수 있지만 비 한정적 와일드 카드 타입은 어떤 원소도 넣지 못한다.

보다 싶이 원소를 넣으려해도 컴파일 에러가 발생하여 넣을 수 없게 만들어 놨다. 

 

하지만 로 타입을 써야 하는 경우도 있다. 

 

class 리터럴에는 로 타입을 사용해야 한다

자바 명세는 class 리터럴에 매개 변수화 타입을 사용하지 못하게 했다. (배열과 기본타입은 가능하다.) 

List.class; // 가능하다.
 
String[].class // 가능하다.

int.class // 가능하다.

List<String>.class // 불가능하다.

List<?>.class // 불가능하다.

 

그렇기 때문에 이러한 경우에는 로 타입을 사용해야한다.

 

두 번째로는 instanceof연산자에는 매개변수화 타입이 사용이 불가능하다. 오로지 와일드 카드 타입만 들어올 수 있는데 instanceof는 영향을 받지 않고 동일하게 동작하여 굳이 지저분하게 비 한정적 와일드 카드 타입을 적용할 이유가 없다.

if (o instanceof Set) {
    Set<?> s = (Set<?>) o;
    // ...
}

 

제네릭과 로타입을 구분하고 제네릭을 써야 하는 이유에 대해 깊게 들어가 보았다. 이제 규칙에 맞게 알맞은 타입을 사용할 수 있을 것이다. 

 

Reference.

Effective Java Edition. 3 - 조슈아 블로크 저자

댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/09   »
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
29 30
글 보관함