Language/Java

[자바 스터디 14주차] 제네릭

소프 2021. 2. 27.

목표

자바의 제네릭에 대해 학습하세요.

학습할 것

  • 제네릭 사용법
  • 제네릭 주요 개념(바운디드 타입, 와일드 카드)
  • 제네릭 메소드 만들기
  • Erasure

제네릭이란?

  • 데이터타입을 일반화하는것을 의미한다.
  • 제네릭은 클래스나 메소드에서 사용할 내부 데이터 타입을 컴파일 시에 미리 지정하는 방법이다.
  • 이렇게 컴파일 시 type check를 하면 장점이 있다.
    • 클래스나 메소드 내부에서 사용되는 객체의 타입의 안정성을 높일 수 있다.
    • 반환값에 대한 타입 변환 및 타입 검사에 들어가는 노력을 줄일 수 있다.
  • Java 5 이전에서는 여러 타입을 사용하는 대부분의 클래스나 메소드에서 인수나 반환값으로 Object타입을 사용했었다. 하지만 이 경우에는 반환된 Object 객체를 다시 원하는 타입으로 타입을 변환해야 하고, 이 때 오류가 발생할 가능성도 생긴다. 하지만 Java5부터 도입된 제네릭을 사용하면 컴파일 시에 미리 타입이 정해지므로, 타입 검사나 타입 변환과 같은 번거로운 작업을 생략할 수 있다.

제네릭을 사용하는 이유

  • 제네릭 타입을 사용함으로써 잘못된 타입이 사용될 수 있는 문제를 컴파일과정에서 제거할 수 있다.
  • 자바 컴파일러는 코드에서 잘못 사용된 타입 때문에 발생하는 문제점을 제거하기 위해 제네릭 코드에 강한 타입 체크를 한다.
  • 행 시 타입 에러가 나는 것보다 컴파일 시에 미리 타입을 강하게 체크해서 에러를 사전에 방지하는 것이 좋다.
  • 제네릭 코드를 사용하면 타입을 국한하기 때문에 요소를 찾아올 때 타입 변환을 할 필요가 없어 프로그램 성능이 향상된다.

코드 예시

제네릭을 사용하지 않고 작성한 모든 자료형의 객체에서 사용할 수 있는 Box class.

public class Box{
	private Object object;
    
    public void set(Object object) { this.object = object; }
    public Object get() { return object; }
}

예제에서는 메서드가 Object를 받거나 반환하므로 primitive 타입이 아니라면 원하는대로 자유롭게 사용할 수 있다. 그러나 컴파일 시 클래스가 어떻게 사용되는지 확인할 방법이 없다.

 

예를 들어 Integer를 Box에 넣어서 Integer를 얻을 것으로 예상했지만 다른 부분에서 실수로 String을 전달하여 런타임 에러가 발생할 수 있다.

 

 

제네릭을 사용하여 작성한 모든 자료형의 객체를 담아 사용할 수 있는 Box class.

public class Box<T> {
	//T는 "타입"을 나타냄
    private T t;
    
    public void set(T t) { this.t = t; }
    public T get() { return t; }
}

이 클래스로 생성되는 Box 객체는 특정한 타입을 매개변수로 받아 클래스 내부에서 사용하게 된다. 제네릭 타입은 프리미티브 타입을 타입 매개변수로 받을 수 없는데 아래 Eraser 에서 설명을 하겠다.

 

제네릭 사용법

제네릭 클래스 선언하기

class name <T1, T2, ... Tn> P /* ... */ }

꺽쇠 괄호( <> )로 구분 된 타입 매개변수는 클래스 이름 뒤에  온다. 객체가 생성될 때 타입 파라미터를 받는 부분이다.

 

타입 파라미터와 일반 클래스 또는 인터페이스 이름의 차이를 구분하기 위해서 정해진 규칙에 따라 타입 파라미터는 단일 대문자를 사용한다.

 

가장 일반적으로 사용되는 타입 매개변수의 이름은 다음과 같다.

 

  • E - 엘리먼트 
  • K - 키
  • N - 숫자
  • T - 타입
  • V - 값
  • S, U, V 등 
public class WitchPot<T> {
	private T meterial;
}

 

제네릭 타입 호출하기

코드 내에서 제네릭 클래스를 참조하려면 T를 Integer 와 같은 구체적인 값으로 대체하는 제네릭 타입 호출을 수행해야한다.

GenericClass<Type args> var;

객체 만들기는 일반적인 객체 생성과 유사하지만, 인스턴스 생성 부분에서도 선언처럼 <Type args>를 기입해야 한다.

GenericClass<Type args> var = new GenericClass<Type args>();

 

public class WithcPot<T>{
	private T meterial;
    
    public static void main(String[] args){
    	WitchPot<Integer> pot = new WitchPot<Integer>();
    }
}

 

다이아몬드

Java SE 7부터 컴파일러가 선언을 살펴본 후 타입을 추론할 수 있다면 일반 클래스의 생성자를 호출하는 데 필요한 타입 인자를 빈 타입 인자 <>로 바꿀 수 있다.

 

<>를 다이아몬드라고 한다.

public class WitchPot<T> {
    private T meterial;

    public static void main(String[] args) {
        //선언부에 Integer로 명시되어 있기 때문에 타입 추론을 통해 다이아몬드로도 객체 생성 가능.
        WitchPot<Integer> pot; = new WitchPot<>();
    }
}

 

여러개의 타입 파라미터

 

제네릭 클래스는 여러 타입 매개 변수를 가질 수 있다.

interaface Pair <K, V> {
	public K getKey();
    public V getValue();
}

public class OrderPair<K, V> implements Pair<K, V> {
	private K key;
    private V value;
    
    public OrderPair(K key, V value){
    	this.key = key;
        this.value = value;
    }
    
    public K getKey() { return key; }
    public V getValue() { return value; }
    
    public static void main(String[] args){
    	// 다음 코드는 OrderPair 클래스의 두 인스턴스를 만든다.
        // 선언에서 K 및 V의 타입을 유추할 수 있으므로 다이아몬드 표기법을 사용하여 코드를 단축할 수 있다.
        // p1에서 두번째 인자로 받은 타입은 사실 int이지만 오토박싱에 의하여 자동으로 Integer로 변환된다.
        Pair<String, Integer> p1 = new OrderPair<>("Even", 8);
        Pair<String, String> p2 = new OrderPair<>("hello", "world");
    }
	
}

 

제네릭 주요 개념(바운디드 타입, 와일드 카드)

바운디드 타입

제네릭 타입에서 타입 인자로 사용할 수 있는 타입을 제한하려는 경우가 있을 수 있다. 예를 들어 숫자에 대해 작동하는 메소드는 Number 또는 해당 하위 클래스의 인스턴스만 허용하려고 할 수 있다. 

 

바운디드 타입 파라미터를 선언하려면 타입 파라미터의 이름, extends 키워드, 상위 바운드를 나열한다.

<T extends UpperBound>

여기서의 extends 키워드는 특별하게도 implements의 기능까지 포함하기 때문에 상위 바운드는 인터페이스가 될 수 있다.

 

또는 여러개의 상위 바운드를 가질 수도 있다.

<T extends B1 & B2 & B3>

만약 여러 상위 바운드 중에서 클래스가 있다면 해당 클래스가 가장 앞에 와야한다. 안그러면 컴파일 에러가 발생한다.

<T extends Class1 & Interface1 & Interface2>

 

간단 예시

public class BoundTypeSample <T extends Number> {
	public void set(T value) {}
    
    public static void main(String[] args){
    	BoundTypeSampleL<Integer> boundTypeSample = new BoundTypeSample<>();
        boundTypeSample.set("Hi"); //컴파일 에러
    }
}
  • BoundTypeSample 클래스의 Type 파라미터를 T로 선언하고 <T extends Number>로 선언한다. BoundTypeSample의 타입으로 Number의 서브타입만 허용한다는 것이다.
  • Integer는 Number의 서브타입이기 때문에 BoundTypeSample와 같은 선언이 가능하지만, set함수의 인자로 문자열을 전달하려고 했기 때문에 컴파일 에러가 발생하게된다.

 

제네릭 메소드의 타입 파라미터 제한하기

public class WitchPot<T> {
	private T material;
    
    public T get(){
    	return this.meterial;
    }
    
    static <U extends Meterial> WitchPot<U> put(U u){
    	return new WitchPot<U>();
    }
    
    public static void main(String[] args){
    	//Meterial을 상속받은 객체만 받도록 제한되었기 때문에 문자열은 컴파일 에러!
        WitchPot.put("나는 문자열입니다.");
    }
}

 

제네릭 클래스에서 바운디드 타입의 상위 클래스의 메소드 사용하기

public class Meterial {
    public String name = "개구리";

    public String getName() {
        return this.name;
    }
}
public class WitchPot<T extends Meterial> {
	private T meterial;
    
    public WitchPot(T meterial){
    	this.meterial = meterial;
    }
    
    public String get(){
    	//바운디드 타입의 상위 클래스인 Meterial의 메소드 getName() 사용
        return this.meterial.getName();
    }
    
    static <U extends Meterial> WitchPot<U> put(U u){
    	return new WitchPot<U>(u);
    }
    
    public static void main(String[] args){
    	Meterial meterial = new Meterial();
        WitchPot<Meterial> pot = put(meterial);
    }
}

 

바운디드 타입이 아니라면 특정 타입으로 사용하길 기대하는 메소드를 사용하지 못할 수 있다.

public static <T> int countGreaterThan(T[] tArray, T t){
	int count = 0;
    for(T e : tArray)
    	//compareTo() 메소드를 사용할 수 없기 때문에 컴파일 에러
        if(e.compareTo(t) > 0)
        	++count;
    return count;
}

compareTo()가 추상화 되어있는 Comparable 인터페이스를 사용하여 문제 해결!

  public static <T extends Comparable<T>> int countGreaterThan(T[] tArray, T t) {
        int count = 0;
        for (T e : tArray)
            if (e.compareTo(t) > 0)
                ++count;
        return count;
    }

 

제네릭과 상속 및 하위 타입

일반적인 상속관계에서 아래처럼 상위 클래스 타입으로 대입이 가능한 것처럼

public void someMethod(Number n) { /* ... */ }
someMethod(new Integer(10));   // OK
someMethod(new Double(10.1));   // OK

제네릭 타입도 이와 같이 타입 파라미터로 주어진 타입에 하위 타입으로 대입할 수 있다.

public class Box<T extends Number>{
	private T t;
    
    public void set(T t) { this.t = t; }
    public T get() { return t;}
    
    public Number show(T e){
    	return this.t = e;
    }
    
    public static void main(String[] args){
    	Box<Number> box = new Box<>();
        box.show(new Integer(10)).intValue());
        box.show(new Double(20)).intValue());
    }
}

그러나 기존 타입의 상속관계가 제네릭 타입까지 이전되지는 않는다. 예를 들어 Number는 Integer의 상위 클래스이지만 Box<Number>는 Box<Integer>의 상위 클래스가 아니다.

제네릭 타입 사이의 상속관계 구현을 위해서는 일반 클래스처럼 상속관계를 명시해야 한다. 좋은 예로 ArrayList<E>는 List<E>를 상속받고 List<E>는 Collection<E>를 구현하고 있다.

ArrayList<E> implements List<E>
List<E> extends Collection<E>

 

 

와일드 카드

제네릭 타입 코드에서 와일드 카드라고 하는 물음표 ( ? ) 는 알 수 없는 유형을 나타낸다. 와일드 카드는 파라미터 변수,필드 또는 지역변수의 타입 등 다양한 상황(때때로 리턴타입에도 사용)에서 사용할 수 있다. 와일드 카드는 제네릭 메서드 호출, 제네릭 클래스 인스턴스 생성 또는 수퍼 타입의 타입 인자로 사용될 수 없다.

 

Upper Bounded Wildcards

상한 와일드 카드를 사용하여 바운디드 타입의 상위 제한을 완화할 수 있다.

<? extends UpperBound>

이런 제네릭 타입은 UpperBound클래스 또는 인터페이스의 하위 타입과 매칭될 수 있다.

 

예를 들어 제네릭의 상속관계에서 봤던 것 처럼 List<Member>는 List<Integer>의 상위 클래스가 아니다. 그렇기 때문에 List<Number>를 파라미터로 가지는 메소드에 List<Integer>를 인자로 호출하면 커파일 에러가 발생한다.

public class WildCardTest {

    public static double sumOfList(List<Number> list) {
        double s = 0.0;
        for (Number n : list)
            s += n.doubleValue();
        return s;
    }

    public static void main(String[] args) {
        List<Integer> li = Arrays.asList(1, 2, 3);

        //컴파일 에러 발생 java.util.List<java.lang.Integer> cannot be converted to java.util.List<java.lang.Number>
        System.out.println("sum = " + sumOfList(li));
    }
}

위와 같은 제한을 완화하기 위해서 Upper Bound Wildcards를 사용할 수 있다. List<Integer>도 메소드 인자로 사용할 수 있도록 와일드 카드를 추가한다. List<? extends Number>List<Number>보다 덜 제한적이다.

 

public class WildCardTest{
	//Upper Bounded Wildcards
    public static double sumOfList(List<? extends Number> list) {
    	double s = 0.0;
        for(Number n : list)
        	s += n.doubleValue();
        return s;
    }
    
    public static void main(String[] args){
    	List<Integer> li = Arrays.asList(1, 2, 3);
        
        System.out.println("sum = " + sumOfList(li));
    }
}

하지만 우리는 아래 코드로 같은 기능을 하는 메소드를 와일드 카드 없이 만들 수 있다.

public class WildCardTest{

    public static <T extends Number> double sumOfList(List<T> list) {
    	double s = 0.0;
        for(T n : list)
        	s += n.doubleValue();
        return s;
    }
    
    public static void main(String[] args){
    	List<Integer> li = Arrays.asList(1, 2, 3);
        
        System.out.println("sum = " + sumOfList(li));
    }
}

두가지 방법의 차이

(1) public static double sumOfList(List<? extends Number> list)

=> 와일드 카드는 참조가 불가능하기 때문에 메소드에서 참조될 수 없다.

 

(2) public static <T extends Number> double sumOfList(List<T> list)

=> T elem; 처럼 메소드내에서 타입을 참조하여 사용할 수 있다.

 

 

Unbounded Wildcards

두 가지 시나리오에서 와일드 카드를 별도의 상하한계 없이 사용한다. <?>

 

  • Object 클래스에서 제공하는 기능만을 사용하여 구현할 수 있는 메서드를 작성하는 경우
  • 코드가 타입 매개변수에 의존하지 않는 제네릭 클래스의 메소드를 사용하는 경우 (예를 들어 List.size() 또는 List.clear() )

List<Object>를 파라미터로 받는 아래의 메소드는 리스트의 요소들을 순회하여 보여주려고 하지만 List<Integer> 같은 타입은 List<Object>의 하위 클래스가 아니기 때문에(Object -> Number -> Integer) 의도했던 것과 다르게 인자로 List<Object>외의 다른 타입을 받을 수 없다.

public class WildCardTest {

    public static void printList(List<Object> list) {
        for (Object elem : list)
            System.out.println(elem + " ");
        System.out.println();
    }

    public static void main(String[] args) {
        List<Integer> li = Arrays.asList(1, 2, 3);
        List<String>  ls = Arrays.asList("one", "two", "three");

        //컴파일 오류
        printList(li);
        printList(ls);
    }
}

이럴때 와일드 카드로 Object를 대신한다면 모든 콘크리트 타입 리스트를 인자로 받을 수 있다.

public class WildCardTest {

    public static void printList(List<?> list) {
        for (Object elem : list)
            System.out.println(elem + " ");
        System.out.println();
    }

    public static void main(String[] args) {
        List<Integer> li = Arrays.asList(1, 2, 3);
        List<String>  ls = Arrays.asList("one", "two", "three");

        printList(li);
        printList(ls);
    }
}

List<?>에는 어떤 객체든 담을 수 있지만 List<Object>에는 Object 객체만 담을 수 있다.

 

 

Lower Bounded Wildcards

 

하한 와일드 카드는 상한 와일드 카드와 비슷한 방식으로 제네릭 타입을 특정 타입의 상위 클래스로 제한한다.

<? super LowerBound>

참고로 상하한을 동시에 지정할 수는 없다.

List<Integer>는 List<Number>의 하위 클래스가 아니다. 그렇기 때문에 List<Integer>를 파라미터로 가지는 메소드에 List<Number>를 인자로 호출하면 컴파일 에러가 발생한다.

public class WildCardTest {

    public static void addNumToList(List<Integer> list) {
        for (int i = 1; i <= 10; i ++) {
            list.add (i);
        }
    }

    public static void main(String[] args) {
        List<Number> li = Arrays.asList(1, 2, 3);

        //컴파일 에러 발생 java.util.List<java.lang.Number> cannot be converted to java.util.List<java.lang.Integer>
        addNumToList(li);
    }
}

위와 같은 제한을 완화하기 위해서는 Lower Bounded Wildcards를 사용할 수 있다. List<Number>도 메소드 인자로 사용할 수 있도록 와일드 카드를 추가한다. List<? super Integer>는 List<Integer>보다 덜 제한적이다.

public class WildCardTest {

    public static void addNumToList(List<? super Integer> list) {
        for (int i = 1; i <= 10; i ++) {
            list.add (i);
        }
    }

    public static void main(String[] args) {
        List<Number> li = Arrays.asList(1, 2, 3);

        addNumToList(li);
    }
}

 

와일드 카드로 제네릭 타입 상속 구현

우리는 이제 List<Integer>가 List<Number>의 하위 클래스가 아니란 것 알고 있다.

List<Integer> intList = new ArrayList<>();

//컴파일 오류
List<Number> numList = intList;

만약 이런 상속관계가 필요하다면 와일드 카드를 이용하여 구현할 수 있다.

List<? extends Integer> intList = new ArrayList<>();
List<? extends Number> numList = intList;

 

와일드 카드를 사용한 제네릭 List 계층구조

 

와일드 카드 주의사항

와일드 카드를 사용한 제네릭 List타입은 비공식적으로 read-only로 간주된다. 하지만 아래 작업이 가능하기 때문에 이 말이 완전히 보장되지는 않는다.

  • null을 추가할 수 있다.
  • clear를 호출할 수 있다.
  • iterator를 가져오고 remove를 호출할 수 있다.
  • 와일드 카드를 캡처하고 List에서 읽은 요소를 쓸 수 있다.

 

와일드 카드 캡처

 

헬퍼 메소드를 이용하여 컴파일러에게 와일드 카드 타입을 유추할 수 있도록 도와주는 방식을 와일드 카드 캡처라고 한다.

 

컴파일러는 기본적으로 List<?>에 대해 List<Object>로 처리하려고 하며, set() 메소드에 엘리먼트 타입을 컴파일 타입에 확인할 수 없기 때문에 오류가 발생한다. (set 작업시 리스트의 타입에 맞게 넣어줘야해서 타입의 추론이 필요하다)

 

public class WildCardTest {

    static void foo (List<?> i) {
		//컴파일 오류
        i.set(0, i.get(2));
    }

    public static void main(String[] args) {
      List<Integer> li = Arrays.asList(1,2,3);
      System.out.println(li);
      foo(li);
      System.out.println(li);
    }
}

헬퍼 메소드를 추가해서 컴파일러가 와일드 카드 타입을 추론할 수 있도록 해준다.

public class WildCardTest {

    static void foo (List<?> i) {
        originalMethodNameHelper(i);
    }

    private static <T> void originalMethodNameHelper(List<T> i) {
        i.set(0, i.get(2));
    }

    public static void main(String[] args) {
        List<Integer> li = Arrays.asList(1,2,3);
        System.out.println(li);
        foo(li);
        System.out.println(li);
    }
}

 

 

제네릭 메소드 만들기

제네릭 메소드는 타입 매개변수를 사용하는 메소드이다. 제네릭 타입을 선언하는 것과 비슷하지만 제네릭 메소드에서 타입 매개변수의 scope는 선언된 메소드로 제한된다.

 

제네릭 메소드의 구문에는 메소드의 리턴 타입 전에 나타나는 꺽쇠 괄호 안에 타입 매개변수 목록이 포함된다. static 제네릭 메소드의 경우 타입 매개변수 섹션이 메소드의 리턴타입 전에 나타나야 한다.

public <타입 파라미터 . . . > 리턴타입 메소드명 (매개변수, . . . ) { . . . }
public static <타입 파라미터 . . . > 리턴타입 메소드명 (매개변수, . . . ) { . . . }

 

타입 인자를 받고 해당 타입 인자에 해당하는 타입의 객체를 파라미터로 받는 static 제네릭 메소드

public class Util {
	public static <T> WitchPot<T> put(T t){
    	return new WitchPot<>(t);
    }
}
public class WitchPot<T>{
	private T meterial;
    
    public WitchPot(T meterial){
    	this.meterial = meterial;
    }
    
    public static void main(String[] args){
    	String frog = "개구리";
        WitchPot<String> pot = Util.<String>put(frog);
        
        System.out.println(pot.meterial); //개구리
    }
}

 

메소드 호출하기 (명시적으로 타입 파라미터 지정)

public class WitchPot<T>{
	private T meterial;
    
    public WitchPot(T meterial){
    	this.meterial = meterial;
    }
}

컴파일러가 제네릭 메소드의 반환 대상의 타입을 미리 검사하는 타입 추론 기능에 의해서 타입 파라미터는 생략이 가능하다.

 

Java SE 8부터는 컴파일러의 추론 개념이 확장되어 메소드 인자에 포함된 매개변수화된 타입까지 검사한다.

public class WitchPot<T>{
	private T meterial;
    
    public WitchPot(T meterial){
    	this.meterial = meterial;
    }
    
    public static void main(String[] args){
    	String frog = "개구리";
        //반환 대상이 WitchPot<String>인 것을 확인하고 String으로 추론한다.
        WitchPot<String> pot = Util.put(frog);
        
        System.out.println(pot.meterial); //개구리
    }
}

Erasure

제네릭은 타입 파라미터에 primitive타입을 사용하지 않는다. 왜그럴까?

바로 타입 소거(type Erasure) 때문이다.

 

예제 코드로 이해해보자.

public class Main{
	List<Integer> list = new ArrayList<>();
}

위 코드는 List<Integer>를 정의한 코드다. 위코드의 바이트 코드의 일부는 아래와 같다.

...
INVOKESPECIAL java/util/ArrayList.<init> ()V
...

여기서 주목해야할 부분은 ArrayList가 생성될 때 타입 정보가 없다는 것이다.

제네릭을 사용하지 않고 raw type으로 ArrayList를 생성해도 똑같은 바이트 코드를 볼 수 있다.

그리고 내부에서 타입 파라미터를 사용할 경우 Object 타입으로 취급하여 처리된다.

 

이것을 타입소거라고 한다.

타입 소거는 제네릭 타입이 특정 타입으로 제한 되어 있을 경우 해당 타입에 맞춰 컴파일시 타입 변경이 발생하고

타입 제한이 없을 경우 Object 타입으로 변경된다.

 

그럼 왜 이렇게 만들었을까? 그 이유는 하위 호환성을 지키기 위해서이다.

제네릭을 사용하더라도 하위 버전에서도 동일하게 동작해야하기 때문이다.

 

primitive 타입을 사용하지 못하는 것도 바로 이 기본 타입은 Object 클래스를 상속받고 있지 않기 때문이다.

그래서 기본 타입 자료형을 사용하기 위해서는 Wrapper 클래스를 사용해야 한다.

 

※ Raw Type을 쓰면 안되는 이유

raw type이란 타입 파라미터가 없는 제네릭 타입을 말한다.

 

raw 타입사용시 발생하는 컴파일 에러 예시

public class Trouble<T> {
	public List<String> getStrs() {
    	return Arrays.asList("str");
    }
    
    public static void main(String[] args){
    	//Raw Type 변수
    	Trouble t = new Trouble();
        
        for(String str : t.getStrs()) {
        	System.out.println(str);
        }
    }
}
Trouble.java:17: error: incompatible types: Object cannot be converted to String
        for (String str : t.getStrs()) {
                                   ^

에러가 발생하는 이유는 JLS 4.8 에 나와있다.

The superclasses (respectively, superinterfaces) of a raw type are the erasures of the superclasses (superinterfaces) of any of the parameterizations of the generic type.

The type of a constructor, instance method, or non-static field of a raw type C that is not inherited from its superclasses or superinterfaces is the raw type that corresponds to the erasure of its type in the generic declaration corresponding to C.

번역하면 아래와 같다.

Raw Type의 슈퍼 클래스는 Raw Type이다.
상속 받지 않은 Raw Type의 생성자, 인스턴스 메서드, 인스턴스 필드는 Raw Type이다.

Raw Type은 타입 파라미터 T만 지워버리는 것이 아니라 슈퍼 클래스의 타입 파라미터도 지우고, 해당 클래스에 정의된 모든 타입 파라미터를 지워버린다. 그래서 t.getStrs()의 반환 타입이 List<String>이 아닌 Raw Type List가 된것이다.

 

자바와 같은 정적 타입 언어의 강점은 프로그램을 실행하기 전에 컴파일 에러를 잡을 수 있다는 것이다. 하지만 Raw Type을 부주의하게 사용하면 런타임 에러를 일으킬 수도 있다. 아래 코드는 런타임 에러를 발생시키는 예제이다.

List<String> good = new ArrayList();
List bad = good;
// warning: unchecked call to add(E) as a member of the raw type List
bad.add(1);
for (String str : good) {
	...
}

경고가 발생하긴 하지만 컴파일이 되긴 된다. 하지만 이 코드를 실행하면 java.lang.ClassCastException이 발생한다.

 

애초에 Raw Type은 자바에 제네릭이 도입되기 전 코드와 호환성을 보장하기 위한 것이다.

정적 타입 언어라는 자바의 강점을 이용하기 위해서 Raw Type을 사용하지 말아야 한다.

 

-----------------------------------------------------------------------------------------------------------------------------------

 

마지막으로 제네릭과 관련하여 한가지 더 생각해볼 문제가 있다.

다음 코드는 제네릭 타입 파라미터를 사용해서 배열을 생성하는 예제이다.

public class Exam_013<T> {
	private T[] myArray;
    
    Exam_013(int size){
    // myArray = new T[size]; //Type parameter 'T' cannot be instantiated directly
       myArray = (T[]) new Object[size];
    }
    
   	public static void main(String[] args){
    	Exam_013<String> e2 = new Exam_013<>(3);
    }
}

제네릭 타입을 사용해서 배열을 생성할때 왜 주석처리한 부분과 같이 생성할 수 없을까?

 

이유는 new 연산자를 사용하기 때문이다.

new 연산자는 동적 메모리 할당 영역인 heap 영역에 생성한 객체를 할당한다.

하지만 제네릭은 컴파일 타입에 동작하는 문법이다.

컴파일 타입에는 T의 타입이 어떤 타입인지 알 수 없기 때문에 Object 타입으로 생성한 다음 타입 캐스팅을 해주어야 사용할 수 있다.

 

static 변수에도 제네릭 타입을 사용할 수 없다.

public class Exam_014<T> {
	private T myValue_1;
    // cannot be referenced from a static context
    // private static T myValue_2;
}

static 키워드를 사용해서 멤버 필드를 선언하게 되면, 특정 객체에 종속되지 않고 클래스 이름으로 접근해서 사용할 수 있다.

제네릭 타입을 사용하면, 위의 예제의 경우 Exam_014<String>과 Exam_014<Integer> 등으로 객체를 생성해서 인스턴스마다 사용하는 타입을 다르게 사용할 수 있어야 하는데, static으로 선언한 변수가 가능할 수가 없다. 그렇기 때문에 static 변수에는 제네릭 타입을 사용할 수 없다.


하지만 static 메서드에는 제네릭을 사용할 수 있다.

 

static 키워드를 사용하면 클래스 이름으로 접근하여 객체를 생성하지 않고 여러 인스턴스에서 공유해서 사용할 수 있다. 변수같은 경우 해당 값을 사용하려면 값의 타입을 알아야 하지만, 메소드의 경우 해당 기능을 공유해서 사용하는 것이기 때문에 제네릭 타입 변수 T를 매개변수로 사용한다고 하면 해당 값은 메소드 안에서 지역 변수로 사용되기 때문에 변수와 달리 메소드는 static으로 선언되어 있어도 제네릭을 사용할 수 있다.

 

제네릭 타입 주의사항

(1) 프리미티브 타입을 타입 인자로 사용할 수 없다.

//컴파일 오류
Pair<int, char> p = new Pair<>(8, 'a');

 

(2) 타입 매개변수로 인스턴스를 생성할 수 없다.

public static <E> void test(List<E> list){
	E elem = new E(); //컴파일 오류
    list.add(elem);
}

 

(3) 타입 매개변수는 정적 필드로 사용할 수 없다.

public class Test<T> {
	public static T test; //컴파일 오류
}

 

(4) 제네릭 타입에 캐스팅 또는 instanceof 사용 불가

public static <E> void test(List<E> list){
	if(list instanceof ArrayList<Integer>) { //컴파일 오류
    	//..
    }
}
//대신 상하한 없는 와일드 칼드를 사용하면 가능하다.
public static void test(List<?> list) {
    if (list instanceof ArrayList<?>) {// OK
        // ...
    }
}
public static void main(String[] args) {
	List<Integer> li = new ArrayList<>();
    //List<Number> ln = (List<Number>) li; //컴파일 오류
    
    //OK
    ArrayList<Integer> l2 = (ArrayList<Integer>) li;
    
}

 

(5) 제네릭 타입 배열 생성 불가

List<Integer>[] arrayOfLists = new List<Integer>[2]; //컴파일 오류

 

(6) 제네릭 클래스는 Throwable 클래스를 직접 또는 간접적으로 상속받을 수 없다.

class MathException <T> extends Exception { /* ... */ } //컴파일 오류

class QueueFullException <T> extends Throwable { /* ... */ } //컴파일 오류

 

(7) 제네릭 메소드의 타입 매개변수의 객체를 catch 할 수 없다.

public static <T extends Exception, J> void execute(List<J> jobs) {
    try {
        for (J job : jobs)
            // ...
    } catch (T e) {   //컴파일 오류
        // ...
    }
}

 

(8) throws는 가능하다

class Parset <T extends Exception> {
	public void parse(File file) throws T { // OK
    
    }
}

 

(9) 타입 Erasure 단계 후에 동일한 서명을 가지게 되는 메소드 오버로딩 불가능

public class Example {
    //타입 Erasure 후에는 print(Set)으로 동일하므로 오버로딩 안됨, 컴파일 오류
    public void print(Set<String> strSet) { }
    public void print(Set<Integer> intSet) { }
}

참고

rockintuna.tistory.com/102

blog.naver.com/hsm622/222251602836

happinessoncode.com/2018/02/08/java-generic-raw-type/

https://docs.oracle.com/javase/tutorial/java/generics/index.html

jordy-torvalds.tistory.com/16

 

댓글