관리 메뉴

bright jazz music

[effective java 3rd - ch.05 제네릭] Item26 : 로타입(Raw Type)은 사용하지 말라 본문

JAVA/effective java 3rd edition

[effective java 3rd - ch.05 제네릭] Item26 : 로타입(Raw Type)은 사용하지 말라

bright jazz music 2022. 11. 24. 09:42

제네릭을 사용하는 이유

  • 제네릭을 사용하면 컬렉션이 담을 수 있는타입을 컴파일러에 알려주게 된다.
  • 따라서 컴파일러는 알아서 형변환(캐스팅) 코드를 추가할 수 있게 된다.
  • 이는 컬렉션에 엉뚱한 타입의 객체가 들어오는 것을 차단한다.
  • 결과적으로 프로그램이 더욱 안전하고 명확해진다.

 

제네릭이 사용되기 이전의 문제들

  • 자바에서 제네릭을 지원하기 전에는 컬렉션에서 객체를 꺼낼 때마다 형변환(캐스팅)을 해야 했다. (귀찮음)
  • 그런데 누군가 컬렉션에 엉뚱한 타입의 객체를 담아 두었다면 런타임에 형변환 오류가 나곤 했다. (오류 발생)
  • 제네릭은 java 5부터 지원하기 시작했다.

 

제네릭이란

  • 클래스와 인터페이스 선언에 타입 매개변수(type parameter)가 쓰이면 이를 제네릭 클래스 또는 제네릭 인터페이스라고 한다. 
  • e.g. List 인터페이스.: List인터페이스는 원소(Element)의 타입을 나타내는 타입 매개변수 E를 받는다. 따라서 이 인터페이스의 완전한 이름은 List<E>지만 그냥 List라고 흔히 표기한다.
  • 제네릭 클래스와 제네릭 인터페이스를 통틀어 제네릭 타입(generic type)이라고 한다.

 

타입 매개변수화 매개변수화 타입

각각의 제네릭 타입은 일력의 매개변수화 타입(parameterized type)을 정의한다.

 

  • 먼저  클래스(또는 인터페이스) 이름이 나온다. 이어서 꺾쇠괄호 안에 실제 타입 매개변수(type parameter)들을 나열한다.
  • e.g. List<String>은 원소의 타입이 String인 리스트를 뜻하는 매개변수화 타입(parameterized type)이다.
  • 즉, 매개변수화 타입(parameterized type)은 타입 매개변수(type parameter)를 받는 타입을 일컫는 표현이다.
  • List<String>이라는 타입은 매개변수화 타입이며, 타입 매개변수로는 String을 받고 있다.
  • 여기서 String은 "정규(formal)타입 매개변수" E에 해당하는 "실제(actual) 타입 매개변수"이다.

 

로타입 (Raw Type)

  • 하나의 제네릭 타입을 정의하면 그에 딸린 raw type도 함께 정의된다.
  • raw type이란 제네릭 타입 매개변수를 전혀 사용하지 않은 경우를 말한다.
  • e.g. List<E>의 raw type은 List다. 
  • raw type을 사용하는 이유는, 제네릭 도래 이전의 코드와 호환되도록 하기 위해서이다(궁여지책)

 

제네릭 지원 이전의 컬렉션 선언 방법

// Stamp  인스턴스만 취급
private final Collection stamps = ... ;
//이 코드를 사용하면 실수로 Stamp 대신 Coin을 넣어도 아무런 오류 없이 컴파일 되고 실행된다.
stamps.add(new Coin(...)); //unchecked call이라는 경고 출력
//컬렉션에서 이 동전을 다시 꺼내기 전에는 오류를 알아채지 못함.
for(Iterator i = stamps.iterator(); i.hasNext();) {
    Stamp stamp = (Stamp) i.next(); // classCastException 발생
    stamp.cancel();
}

 

제네릭을 사용한 컬렉션 선언 방법

 //매개변수화 된 컬렉션 타입 - 타입안전성 확보
private final Collection<Stamp> stamps = ...;
//지정한 타입 매개변수와 다른 타입의 인스턴스를 넣으려고 하면 컴파일 오류 발생.
//incompatible types: Coin cannot be converted to Stamp
 stamps.add(new Coin());

 

  • 이러한 이유로 raw type(타입 매개변수가 없는 제네릭 타입)을 절대 쓰지 않는 것이 좋다. 
  • 로타입을 사용하면 제네릭이 부여하는 안정성과 표현력을 모두 잃는다.

 

 

raw type을 사용하지 않고 과거 코드와의 호환성을 유지하는 방법

raw type의 위험성을 알면서도 막아 놓지 않은 이유는 과거 코드와의 호환 때문이다.

그럼 로타입을 사용하지 않는다면 어떻게 호환해야 할까?

  • <Object>를 타입 매개변수로 사용하는 제네릭 타입을 생성하라.
  •  e.g. List<Object>

 

List와 List<Object>의 차이

  • List는 제네릭을 사용하지 않는다는 것이고, List<Object>는 모든 타입을 허용한다는 의미이다.
  • 파라미터로 List를 받는 메서드에는 List<String>을 넘길 수 있다. 
  • 파라미터로 List<Object>를 받는 메서드에는 List<String>을 넘길 수 없다.
  • 이는 제네릭의 하위 타입 규칙 때문이다. List<String>은 raw type인 List의 하위 타입이지만, List<Object>의 하위 타입은 아니다.
  • 결과적으로 List<Object>를 사용하는 제네릭 타입(parameterized type)을 사용할 때와 달리 List와 같은 raw type을 사용하면 타입 안정성을 잃는다.

e.g.

 

package com.example.ssiach2ex1.entity;

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

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

        unsafeAdd(strings, Integer.valueOf(42));
        String s = strings.get(0); //컴파일러가 자동으로 형변환 코드를 넣어준다.
        
        //classCastException 발생: Integer를 String으로 변환하려 시도했기 때문에 
        
    }

    private static void unsafeAdd(List list, Object object){
        list.add(object);
        //예제에서는 Integer래퍼 객체를 넣으려고 시도
    }
}

//    parseInt(): 원시데이터인 int 타입을 반환
//        valueOf(): Integer 래퍼(wrapper)객체를 반환
package com.example.ssiach2ex1.entity;

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

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

        unsafeAdd(strings, Integer.valueOf(42));
        String s = strings.get(0); //컴파일러가 자동으로 형변환 코드를 넣어준다.
        
        //컴파일조차 되지 않음.
        //java: incompatible types: java.util.List<java.lang.String> cannot be converted to java.util.List<java.lang.Object>
    }

    private static void unsafeAdd(List<Object> list, Object object){
        list.add(object);
        //예제에서는 Integer래퍼 객체를 넣으려고 시도
    }
}

//    parseInt(): 원시데이터인 int 타입을 반환
//        valueOf(): Integer 래퍼(wrapper)객체를 반환

 

너무 귀찮아서 raw type을 쓰고자 할 때

아래와 같이 쓰고 싶을 수도 있다.

static int numElementsInCommon(Set s1, Set s2){ //raw type Set 사용: 아무거나 다 받을 수 있음
    int result = 0;
    for (Object o1 : s1)
        if (s2.contains(o1))
            result++;
    return result;
}

이 메서드는 동작은하지만 로타입을 사용했기 때문에 안전하지 않다.

따라서 이런 경우엔 비한정적 와일드카드 타입(unbounded wildcard type)을 대신 사용하는 게 좋다.

제네릭 타입을 쓰고 싶지만 실제 타입매개변수가 무엇인지 신경쓰고 싶지 않다면 "?"를 사용하면 된다.

 

static int numElementsInCommon(Set<?> s1, Set<?> s2){ //비한정적 와일드카드 타입 (unbounded wildcard type) 사용

 

 

비한정적 와일드카드 타입과 raw 타입의 차이

비한정적 와일드카드 타입은 안전하고 로 타입은 안전하지 않다.

Set<?>과 Set의 차이.

  • raw type컬렉션에는 아무 원소나 넣을 수 있으니 타입 불변식을 훼손하기 쉽다.
  • 반면 Collection<?>에는 null을 제외하고는 어떤 원소도 넣을 수 없다.
  • 컬렉션에서 꺼낼 수 있는 타입의 객체도 전혀 알 수 없게 했다.
  • 즉, 비한정적 와일드카드 타입을 받는 제네릭 컬렉션에는, 원소를 넣을 수 없고, 꺼낼 때의 타입도 전혀 알 수 없다.
  • 이러한 제약을 받아들일 수 없다면 제네릭 메서드나 한정적 와일드카드 타입을 사용하면 된다.

 

raw type을 사용하는 경우

1. Class 리터럴에는 raw type을 써야 한다. 자바 명세는 class 리터럴에 매개변수화 타입(제네릭)을 사용하지 못하게 했다.(배열과 기본타입은 허용)

  • List.class, String[ ].class, int.class는 허용
  • List<String>.class, List<?>.class는 비허용

2. instanceOf 연산자를 사용하는 경우

  • 런타임에는 제네릭 타입 정보가 지워지므로, instanceOf 연산자는 비한정적 와일드카드 타입 이외의 매개변수화 타입에는 적용할 수 없다.
  • 즉, 제네릭에 사용할 경우, <?>를 사용하는 제네릭 타입에만 instanceOf연산자를 사용할 수 있다는 뜻이다.
  • 그리고 raw type이든 비한정적 와일드카드 타입이든 instanceOf는 완전히 똑같이 동작한다.
  • 이 경우 굳이 코드를 지저분하게 만드는 비한정적 와일드카드 타입을 사용할 이유가 없다.

if (o instancof Set) {      //Raw type
    Set<?> s = (Set<?>) o;  //unbounded wildcard type 캐스팅
    
    //O의 타입이 Set임을 확인한 다음 Set<?>으로 캐스팅해야 한다.
    //이는 검사 형변환(checked cast)이므로 컴파일러 경고가 뜨지 않는다.
}

 

 

참고

한글용어 영문 용어 아이템
매개변수화 타입 parameterized type List<String> 아이템 26
실제 타입 매개변수 actual type parameter String 아이템 26
제네릭 타입 generic type List<E> 아이템 26, 29
정규타입 매개변수 formal type parameter E 아이템 26
비한정적 와일드카드 타입 unbounded wildcard type List<?> 아이템 26
로 타입 raw type List 아이템 26
한정적 와일드카드 타입 bounded  wildcard type List<? extends Number> 아이템 31
제네릭 메서드 generic method static <E> List<E>,
asList(E[] a)
아이템 30
타입 토큰 type token String.class 아이템 33
재귀적 타입 한정 recursive type bound <T exnteds Comparable<T>> 아이템 30

 

 

---

 

정리

  • raw type을 사용하면 런타임에 예외가 일어날 수 있으니 사용하면 안 된다.
  • raw type은 제네릭이 도입되기 이전 코드와의 호환성을 위해 제공될 뿐이다.
  • Set<Object>는 어떤 타입의 객체도 저장할 수 있는 매개변수 타입이고, Set<?>는 모든 종의 타입 객체만 저장할 수 있는 와일드카드 타입이다.
  • raw type인 Set은 제네릭 타입 시스템에 속하지 않는다.
  • Set<Object>와 Set<?>은 안전하지만 raw type인 Set은 그렇지 않다.

 

 

 

---

 

실습: List와 List<Object>와  비교

 

//List<?>

public class Test {

    public static void main(String[] args){

        List<Object> list = new ArrayList<>() ;
        list.add("0번");
        list.add("1번");

        Test1.listReturn(list);
    }
}

class Test1 {
    public static void listReturn(List<?> list ){
        list.add(null);
//        list.add("2번"); 오류발생. null이외에는 넣을 수 없다.
//		java: incompatible types: java.lang.String cannot be converted to capture#1 of ?
        System.out.println(list);
    }
}

//[0번, 1번, null]

 

//List<Object>

public class Test {

    public static void main(String[] args){

        List<Object> list = new ArrayList<>() ;
        list.add("0번");
        list.add("1번");

        Test1.listReturn(list);
    }
}

class Test1 {
    public static void listReturn(List<Object> list ){
        list.add(null);
        list.add("2번");
        System.out.println(list);
    }
}

//[0번, 1번, null, 2번]

 

InstanceOf 사용

package com.example.ssiach2ex1;

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

public class Test {

    public static void main(String[] args){

        List<Object> list = new ArrayList<>() ;
        list.add("0번");
        list.add("1번");
        list.add(Integer.valueOf(30));

        Test1.listReturn(list);
    }
}

class Test1 {
    public static void listReturn(List<?> list ){
        list.add(null);
//        list.add("2번");



        System.out.println(list);
        if(list.get(2) instanceof Integer){
            System.out.println(true);
        }else
            System.out.println(false);

    }
}

//[0번, 1번, 30, null]
//true

 

 

 

 

 

Comments