본문 바로가기

우아한테크코스/테코톡

제네릭

[10분 테코톡] 🌱 시드의 제네릭을 들으며 정리한 글입니다.

제네릭이란? 

클래스나 메서드에서 사용할 내부 데이터 타입을 외부에서 지정하는 기법

제네릭 클래스 

클래스 선언에 타입 매개변수가 쓰이면, 이를 제네릭 클래스라고 한다.

정의

class Box<T> {
    private T ob;

    public void set(T o) {
        ob = o;
    }
    public T get() {
        return ob;
    }
}

클래스이름 옆에 꺽쇠 괄호와 타입 매개변수를 지정해주면 된다. <T>

Box<Apple> aBox = new Box<Apple>();

Box의 타입 매개변수를 외부에서 지정한 것이다. 
클래스 내부에서 사용하는 타입은 지정한 타입이 된다. 
하지만 실제로 지정한 타입으로 변경되는 것은 아니다. (타입 이레이져)

제네릭을 사용하는 이유 

class Apple {
    public String toString() {
        return "I am an apple.";
    }    
}

class Orange {
    public String toString() {
        return "I am an orange.";
    }
}

class Box {
    private Object ob;

    public void set(Object o) {
        ob = o;
    }
    public Object get() {
        return ob;
    }
}


class FruitAndBox2 {
    public static void main(String[] args) {
        Box aBox = new Box();
        Box oBox = new Box();

        // 상자에 과일을 담는다.
        aBox.set(new Apple());
        oBox.set(new Orange());

        // 상자에서 과일을 꺼낸다.
        Apple ap = (Apple)aBox.get();
        Orange og = (Orange)oBox.get();
        Orange errorOg = (Orange)aBox.get(); // 런타임 에러 발생

        System.out.println(ap);
        System.out.println(og);
    }
}

타입 안정성

위 코드에서 발생할 수 있는 문제는 문법적으로는 문제가 없지만
자료형에 대한 검증이 런타임 시점에 이루어지므로 예외가 발생한다. 

제네릭을 사용하면 이런 타입 오류를 컴파일 시점에 잡아준다. 

캐스팅 삭제

Object 클래스는 사용하는 타입에 맞게 캐스팅을 해주어야 했지만, 
제네릭은 컴파일 타임에 타입이 정해지므로 형변환을 하지 않아도 된다. 
떄문에 타입 검사에 대한 노력을 줄이며, 가독성이 좋아진다. 

제네릭 메서드

꺽쇠 괄호 <> 안의 타입으로 매개변수의 데이터 타입을 지정한다. 
타입 파라미터의 범위는 메서드 블럭 이내로 한정한다. 

public <T> Box<T> makeBox(T o) {...}

제네릭 메서드는 제네릭 클래스가 아니더라도 독립적으로 정의할 수 있다. 

여기서 주의할 점은 제네릭 메서드의 타입 매개변수와 제네릭 클래스의 타입 매개변수는 다를 수 있다. 

Class Name<T> {
  public <T> void printName(T t) {
  	System.out.println(t.getClass().getName());
  }
}

public static void main(String[] args) {
  Name<String> name = new Name<>();
  Class.printName(1); // java.lang.Integer
  Class.printName(3.14); // java.lang.Double
}

타입 매개변수의 제한 

Box에는 어떤 타입이던지 담을 수 있었지만, 이 특성에 따라 타입을 제한할 수도 있어야 한다.

상한 경계

class Box<T extends Fruit> {...}

타입 매개변수의 클래스는 Fruit 클래스이거나 Fruit의 하위 클래스여야 한다. 
Fruit이 상속하는 클래스는 들어올 수 없다. 

하한 경계

class Box<T super Fruit> {...}

타입 매개변수의 클래스는 Fruit 클래스이거나 Fruit의 상위 클래스여야 한다. 

와일드 카드 

비경계 와일드 카드

? 의 형태로 사용한다.  ex) List<?>
모든 타입이 인자가 될 수 있다.

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

List<String> strings = new ArrayList<>();
printList(strings); // 컴파일 에러

List<Object>만을 받기 때문에 List<String>이 인수로 온다면 에러가 발생한다.

이 둘은 상속 관계가 아니기 때문인데, List<String>은 List<Object>의 서브타입이 아니기 때문이다.
아래와 같이 와일드 카드를 사용하면 문제가 해결된다.

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

List<String> strings = new ArrayList<>();
printList(strings); // OK

List<String>은 List<?>

의 서브타입이기 때문이다.

Get

비경계 와일드카드의 원소는 어떤 타입도 될 수 있다. 
어떤 타입이 와도 읽을 수 있도록 get시 모든 타입의 공통 조상인 Object로 받는다. 

public static void get(List<?> list) {
	Object object = list.get(0);
	Integer integer = list.get(0); // 컴파일 에러
}

Add

List<?>에는 null만 삽입할 수 있다. 
비경계 와일드카드의 원소가 어떤 타입인지 알 수 없다. 
그러므로 타입 안정성을 지키기 위해 null 만 삽입할 수 있다. 

public static void main(String[] args) {
	List<Integer> ints = new ArrayList<>();
    addDouble(ints);
}

private static void addDouble(List<?> ints) {
	ints.add(3.14);
}

만약 모든 타입의 값을 넣을 수 있다면 List<Integer>에 Double 형을 추가할 수 있는 모순이 생긴다. 
이는 제네릭의 타입 안정성에 위배되기 때문에 null만을 삽입할 수 있다.

상한 경계 와일드카드

? extneds T(T 상한 와일드카드)의 형태로 사용한다. (T 상한 와일드카드의 리스트 = List<? extends T>)
T 혹은 T의 하위 클래스만 인자로 올 수 있다. 

Get

List<? extends T>에서 get한 원소는 T이다. 
상한 경계 와일드카드의 원소는 T 혹은 T의 하위 클래스이다. 
원소들의 최고 공통 조상인 T로 읽으면, 어떤 타입이 오든 T로 읽을 수 있다.

public static void get(List<? extends Fruit> list) {
    for (Fruit fruit : list) {
        // ...
    }
}

Add

List<? extends T>에는 null만 삽입할 수 있다. 
상한 경계 와일드카드의 원소가 어떤 타입인지 알 수 없기 때문이다. 

List<Apple> apples = new ArrayList<>();
List<? extends Fruit> fruits = apples;

fruits.add(new Banana()); // 컴파일 에러

Fuit의 하위 클래스를 삽입할 수 있다면 다음과 같이 타입 안정성이 깨지기 때문이다.

하한 경계 와일드 카드

? super T(T 하한 와일드카드)의 형태로 사용한다. (T 하한 와일드카드의 리스트 = List<? super T>)
T 혹은 T의 상위 클래스만 인자로 올 수 있다. 

Get

List<? super T>에서 get한 원소는 Object이다. 
T 하한 경계 와일드카드의 원소는 T의 상위 클래스 중 어떤 타입도 될 수 있다.
어떤 타입이 와도 읽을 수 있도록, T의 공통 조상인 Object로 받는다. 
아래만을 제한하므로 위로는 어떤 타입이든 올 수 있다.

public static void get(List<? super Fruit> list) {
    for (Object fruit : list) {
        // ...
    }
}

Add

List<? super T>에는 T 혹은 T의 하위 클래스만 삽입할 수 있다. 
하한 경계 와일드 카드의 원소는 T 혹은 T의 상위 클래스이다. 

List<? super Fruit> fruits = new ArrayList<>();

fruits.add(new Banana());
fruits.add(new Fruit());
fruits.add(new Food()); // 컴파일 에러

만약 fruits가 List<Fruit>일 경우 Food는 Fruit의 상위 클래스이므로 원소를 추가할 수 없다.
fruits는 어떤 리스트가 올 지 모른다. 
하지만 어떤 리스트라도 Fruit 또는 그 하위 클래스는 원소로 추가할 수 있다.

 

참고자료


이번 JDBC 미션에서 제네릭을 굉장히 많이 활용했는데,
그 기초를 좀 더 잡고갈 필요가 있는 것 같아서 테코톡 들으면서 정리정리.

'우아한테크코스 > 테코톡' 카테고리의 다른 글

DB Replication  (0) 2021.11.20
인덱싱  (0) 2021.10.05
Servlet과 Spring  (0) 2021.08.06
CORS  (0) 2021.08.06
Cache  (0) 2021.08.06