언어/Java

제네릭 (1)

honeyricecake 2023. 2. 1. 15:41

제네릭의 이해

 

제네릭 이전의 코드

=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 ser(Object o)
    {
        ob= o;
    }

    public Object get()
    {
        return ob;
    }
}

 

이렇게 짜면 Box 클래스에는 Apple, Orange 클래스 모두를 담을 수 있다.

하지만 이 경우 main 메소드를 보면

 

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();

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

이런 식으로 어쩔 수 없이 형 변환의 과정이 수반된다. (귀찮아서라도 안 하고 싶다.)

그리고 이는 컴파일러의 오류 발견 가능성을 낮추는 결과로 이어진다.

(컴파일러는 형변환해도 되는지 안 되는지를 신경쓰지 않고 이를 프로그래머에게 맡긴다.)

 

 

그나마 형변환 하면 타입케스트 에러라도 볼 수 있으나

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

        aBox.set("Apple");
        oBox.set(new Orange());

        System.out.println(aBox.get());
        System.out.println(oBox.get());
    }

이와 같이 코드를 작성해버리면 매우매우 문제가 커진다.

 

이제 위의 Box 클래스를 아래와 같이 개선해보자.

 

class Box<T> {  // 무엇이든 저장하고 꺼낼 수 있는 상자
    private T ob;

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

    public T get()
    {
        return ob;
    }
}

여기서 T는 필요에 따라 결정할 수 있다.

이 때 T는 타입 매개 변수라 한다.

 

이렇게 하면 Box 클래스는 다음과 같이 사용할 수 있다.

 

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

이렇게 하면 aBox에는 Apple형 객체만 필드변수로 저장할 수 있게 된다.

즉, Box에 저장된 자료형이 명확해진다!!

 

이 때 Box<Apple> 의 Aplle은 타입인자라 부른다. (Box<T>의 T가 타입 '매개변수' 였음을 떠올려보자.)

그리고 Box<Apple> 역시 새로운 자료형이고 매개변수로 인해 만들어진 타입이기 때문에 매개변수화 타입이라고 부른다.

 

 

이렇게 하면

 

이렇게 컴파일러 레벨에서 이전에 했던 실수를 잡아낼 수 있게 된다.

 

또한

Apple ap = aBox.get();

이와 같은 코드에서 이제는 '형변환'을 하지 않는다!!

 

 

제네릭의 기본 문법

 

1. 다중 매개 변수 기반 제네릭 클래스의 정의

 

class DBox<L, R>
{
    private L left;
    private R right;

    public void set(L o, R r)
    {
        left = o;
        right = r;
    }

    @Override
    public String toString()
    {
        return left + "&" + right;
    }
}

public static void main(String[] args) {
        DBox <String, Integer> box = new DBox<String, Integer>();
        box.set("Apple", 25);
        System.out.println(box);
    }

 

2. 타입 매개변수의 이름 규칙

 

일반적인 관례 - 한 문자로, 대문자로 이름을 짓는다.

 

보편적인 선택 - 

E  Element

K  Key

N  Number

T  Type

V  Value

 

3. 기본 자료형에 대한 제한 그리고 래퍼 클래스

 

Box<int> box = new Box<int>();

-> 타입 인자로 기본 자료형이 올 수 없으므로 컴파일 오류 발생

 

기본 자료형을 쓸 수 없다면? 래퍼 클래스를 사용하면 됨

 

4. 다이아몬드 기호 (<>)

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

오른쪽 <>에 타입인자를 생략 가능하다.

 

5. 매개변수화 타입을 '타입 인자'로 전달 가능하다.

 

6. 제네릭 클래스의 타입 인자 제한하기

 

class Box<T extends Number>
{
    private T ob;
    
    public void set(T o)
    {
        ob = o;
    }
    
    public T get()
    {
        return ob;
    }
}

이렇게 Box의 타입매개변수 T 에 들어갈 타입인자를 Number를 상속하는 클래스로 제한할 수 있다.

(참고: Number는 래퍼 클래스 중 Character, Boolean 을 제외한 모든 클래스들이 상속하는 클래스이다.)

 

이렇게 하면

 

이렇듯 Number 클래스를 상속하지 않는 String클래스를 Box클래스의 타입 인자로 줄 수 없는 것을 볼 수 있다.

 

이와 같은 타입 인자 제한의 효과를 보자.

 

예를 들어

class Box<T extends Number>

인 Box 클래스에서

필드 변수 private T ob 에 대해

ob.intValue()를 호출해야 한다고 가정해보자.

 

이 경우 무조건 그 클래스에 intValue 메소드가 정의되어 있어야 한다.

 

이런 경우로 인해 타입인자를 제한 하는 것이다.

 

그리고 제네릭 클래스의 타입인자를 인터페이스로도 제한할 수 있다.

ex.

interface Eatable
{
	publiv String eat();
}

class Box<T extends Eatable>
{
	T ob;
    ...
    ob.eat();
    ...
}

Box 클래스에 들어가는 모든 ob에 대하여 Eatable 인터페이스에서 구현한 eat함수를 호출하기 위해서는

위와 같이 타입인자를 인터페이스로 제한할 필요가 있다.

 

그리고 타입인자의 제한의 경우 인터페이스 역시 extends 키워드를 이용한다.

 

하나의 클래스와 하나의 인터페이스에 대해 동시 제한 역시 가능하다.

 

ex.

class Box<T extends Number & Eatable>

 

제네릭 메소드의 정의

 

클래스 전체가 아닌 하나의 메소드에 대해서만 제네릭 선언을 할 수도 있다.

 

ex.

class BoxFactory {
    public static <T> Box<T> makeBox(T o)
    {
        Box<T> box = new Box<>();
        box.set(o);
        return box;
    }
}

 

Box를 만드는 메소드를 만들고 싶을 때

Box가 제네릭 이므로

무슨 형의 ob를 담고 있는 Box인지 확실하게 해주기 위해서는 makeBox 함수에 제네릭 선언을 해주어서 타입인자를 넘겨주어야 한다.

 

이 때 제네릭 메소드의 T는 호출 시점에 결정한다.

 

Box<String> sBOx = BoxFactory.<String>makeBox("Sweet");

이런 식으로 메소드의 타입인자 T를 메소드 앞에 다이아몬드 기호를 이용해 넘겨주어 사용할 수 있다.

 

그리고 이를 타입인자를 생략하여 사용 가능하다.

 

Box<String> sBox = BoxFactory.makeBox("Sweet");

이 때 타입 인자의 판단은 메소드의 인자를 통해 자동으로 판단된다.

 

그리고 당연하게도 제네릭 메소드의 제한된 타입 매개변수 선언 역시 가능하다.