티스토리 뷰

제네릭(Generics)

제네릭은 클래스, 인터페이스, 메소드를 정의할 때 매개변수로 받은 Type으로 정의된다.

// 1. 제네릭 클래스
public Coffee<T>{
	// 객체를 생성할 때, new Class<String>으로 하면 클래스 내부 T로 정의된 타입은 String이 된다.
}

// 2. 제네릭 인터페이스
public interface List<E> extends Collection<E> {
	// ... 리스트 인터페이스의 추상메소드들
}

// 3. 제네릭 메소드
public static <T> T order(T t){
/*
이 메소드에서 사용될 타입은 메소드의 수식자와 반환형 사이에 위치한다.
(메소드에서 사용할 T타입이 있음을 <T>로 알리는 것이다)
이 메소드에서 T 타입은 입력받은 T타입으로 정의된다.
*/    
	return t;
}

제네릭 네이밍 컨벤션

  • E - Element (used extensively by the Java Collections Framework)
  • K - Key
  • N - Number
  • T - Type
  • V - Value
  • S,U,V etc. - 2nd, 3rd, 4th types

로 타입(raw type)을 사용하지마라

// 로 타입
List
// 로 타입이 아님
List<Object>

List 라는 로타입은 generic을 아예 사용하지 않는 것이고, List<Object>는 임의의 객체를 허용하는 generic 이다. List<Object>는 컴파일러에게 Object 타입을 사용한다고 명시하고 있다. 컴파일러는 타입을 인식하여 런타임이 아닌 컴파일 타임에 오류를 찾아낼 수 있다. 

 

그렇다면 List<Object>와 List<T>는 같은 의미일까? 아니다.

List<Object>는 Object 타입 그 자체 타입을 받는다. 하지만 List<T>는 T타입으로 들어오는 타입을 받는다. 실제로는 T 타입의 조상이 Object 이겠지만, Box<T>와 Box<Object>는 타입이 다르고 상속관계도 아니다. List<String>은 List의 하위타입이지만, List<String>이 List<Object>의 하위타입은 아니다.

// 1
class Box<Object>{
}
//2
class BoxAny<T>{
}



public class GenericTest {

    @Test
    void genericTest(){
        final Box<String> stringBox = new Box<>(); // 정상동작
        // 1. String은 Object 타입이므로 생성할 수 있음
        final BoxAny<String> stringBoxAny = new BoxAny<>(); // 정상동작

        printBoxObject(stringBox); // 컴파일 오류
        //1. Box<String>과 Box<Object>는 타입이 다르고 상속관계도 아니므로 오류
        printBoxAny(stringBox); // 정상동작
    }

    public static void printBoxObject(Box<Object> box) { System.out.println(box); }
    public static <T> void printBoxAny(BoxAny<T> box) { System.out.println(box); }

}

 

모든 타입을 지정해줄 때, 사용하고 싶다면 <T>로 제네릭 타입을 사용하면 된다. 하지만 제네릭 타입을 사용하고 싶지만, 들어오는 실제 타입 매개변수가 무엇인지 신경쓰고 싶지 않다면 와일드카드(<?>)를 사용한다.

 

Wildcards(와일드 카드)

와일드카드는 <?> 이다. 이것의 의미는 알 수 없는 타입이고 모든 타입이 될 수 있다. 

In generic code, the question mark (?), called the wildcard, represents an unknown type.
The wildcard is never used as a type argument for a generic method invocation, a generic class instance creation, or a supertype.
-javadocs

 

제네릭 타입<T>와 와일드 카드<?>의 차이는?

  • 제네릭 : 타입을 모르지만, 타입을 정해지면 그 타입의 특성에 맞게 사용한다.
  • 와일드 카드 : 무슨 타입인지 모르고, 무슨 타입인지 신경쓰지 않는다. 타입을 확정하지 않고 가능성을 열어둔다.
List<?> list; 
/*
1. 원소를 꺼내 와서는 Object에 정의되어 있는 기능만 사용하겠다. equals(), toString(), hashCode()… 
2. List에 타입이 뭐가 오든 상관 없다. 나는 List 인터페이스에 정의되어 있는 기능만 사용하겠다. 
size(), clear().. 단, 타입 파라미터와 결부된 기능은 사용하지 않겠다! add(), addAll() 
*/

List<T> list; 
/*
1. 원소를 꺼내 와서는 Object에 정의되어 있는 기능만 사용하겠다. equals(), toString(), hashCode()… 
2. List에 타입이 뭐가 오든 상관 없다. 
나는 List 인터페이스에 정의되어 있는 기능만 사용을 하고, 타입 파라미터와 결부된 기능도 사용하겠다.
*/
//출처: https://vvshinevv.tistory.com/55?category=692309 [왜 모르는가?]

null 이외의 어떤 원소도 Collection<?> 타입에 넣을수도(add) 없고, 가져와도(get) 타입을 알 수 없는 제약이 있다.

    private void test(final List<?> drinks) {
        final Object o = drinks.get(0); // 자동완성되는 타입: Object
    }
    
    private <T> void test(final List<T> drinks) {
        final T t = drinks.get(0); // 자동완성되는 타입: T
    }

이런 제약을 해결하려면 제네릭 메서릭 메소드나 한정적 와일드카드 타입을 사용해야 한다. 특정 타입을 지정하여 타입에 따른 메소드를 사용하고 싶다면 <T> 타입을 사용해야하고, <?>는 Object 타입으로 작동하게 된다.

 

한정적 와일드카드에서 extends를 사용하면 아래의 예시는 get 하였을 때는 상위 인터페이스인 Drink 객체가 된다. 하지만 add 하기에는 파라미터에서 타입이 확신할 수 없으므로 불가하다.

interface Drink{
}

class Coffee implements Drink{
}

class Milk implements Drink{
}

public class GenericTest {
    private void test1(final List<? extends Drink> drinks ){
        final Drink drink = drinks.get(0);
        // <? extends Drink>에 들어가는 타입은 Drink가 될수도, Drink를 구현한 Coffee가 될수도 Milk가 될수도 있다.
        // 하지만 결국 Drink 타입을 구현하므로 drinks에서 꺼내오는 것은 Drink 타입이 된다.
        Drink drink1 = new Coffee();
        drinks.add(drink1); // ⚡ 컴파일오류
        // <? extends Drink>가 Drink 타입임을 확신할 수 없다. drinks에 넣을수 없다.
    }

    private <T extends Drink> void test2(final List<T> drinks){
        final T t = drinks.get(0);
        // <T extends Drink>에 들어가는 타입은 Drink가 될수도, Drink를 구현한 Coffee가 될수도 Milk가 될수도 있다.
        // 하지만 결국 Drink 타입을 구현하므로 Drink 타입이 될 수 있다.
        drinks.add(drinks.get(0));    //정상 작동
        // T라는 제네릭타입은 입력받는 순간 정해지고 그 타입으로 고정되므로 문제없이 추가된다.
    }
}

 

상한 제한과 하한제한에서 와일드카드 차이는?

    private void test1(final List<? extends Drink> drinks) {
        final Drink drink = drinks.get(0);
        // <? extends Drink> 여기에 들어가는 타입은 Drink가 될수도, Drink를 구현한 Coffee가 될수도 Milk가 될수도 있다.
        // 하지만 결국 Drink 타입을 구현하므로 drinks에서 꺼내오는 것은 Drink 타입이 된다.
        Drink drink1 = new Coffee();
        drinks.add(drink1); // ⚡ 컴파일오류
        // 와일드카드는 무슨 타입인지 신경쓰지 않는다.
        // 따라서 <? extends Drink>가 Drink 타입임을 확신할 수 없다. 
        // List<Coffee>일수도 List<Drink> 일수도 있다. 따라서 drinks에 넣을수 없다.

    }

    private void test3(final List<? super Drink> drinks) {
        final Object object = drinks.get(0);
        // Drink의 부모만 오기 때문에 모든 것을 포괄하는 Object 타입이 된다. (사실상 타입을 알 수 없는 것과 같다)
        Drink drink1 = new Coffee();
        drinks.add(drink1); // 정상 작동
        // Object 타입으로 변경될 것이고 특정 타입이 아닌 Object 타입이 되므로 drinks에 넣을수 있다.
        // 하지만 들어가는 것은 Drink의 하위 타입이어야 한다. 왜냐면 Drink 상위타입 어떤 것이 될 수 있는 가능성이 있기 때문이다.
    }

매개변수화 타입T가 생산자라면 <? extends T>를 사용한다. List<? extends Drink> drinks는 get등오로 생산만 가능하기 때문이다. 하지만, 소비자라면 <? super T>로 사용한다. 사실상 get은 무의미하며, add, remove 등을 사용가능(소비)하기 때문이다.  

정리

수업시간에 알게된 제네릭을 이해해보려고 했는데 너무 어렵다. 몇시간 동안 알아봤지만 여전히 잘 모르겠다.

 

매개변수로 받는 타입을 받아서 사용하려면 제네릭을 사용한다. 타입을 딱 지정해서 사용하려면 <T>로 지정할 수 있고, 이것은 타입이 딱 지정된다. 하지만 와일드카드는 매개변수에서 받는다고 하더라도 타입의 가능성을 열어준다. 따라서 타입이 확정되지 않는 것을 암시하기 때문에 거기서 차이가 발생한다.

참고

자바 오라클 공식문서 제네릭

제네릭 설명 블로그

이펙티브 자바 - 아이템 26, 30, 31

 

 

공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/05   »
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 31
글 보관함