[JAVA] 제네릭(Generic)

2022. 9. 15. 09:56

제네릭(Generic)

제네릭은 다양한 타입의 객체들을 다루는 메서드나 컬렉션 클래스에 컴파일 시 타입을 체크해주는 기능이다.

객체의 타입을 컴파일 시에 체크해주기 때문에 형변환의 번거로운 작업이 줄고 안정성이 증가한다.

public class GenericTest {
    public static void main(String[] args) {
        ArrayList arr = new ArrayList();
        for(int i =1 ; i <= 5; i++){
            arr.add(i);
        }
        int sum = 0;

        for (Object o : arr) {
            sum += (int)o;
        }
    }
}

위와 같이 ArrayList를 사용하면 Object로 반환되기 때문에 하나하나 형변환해서 더해야 하고 문자열같이 다른 타입의 데이터가 리스트에 들어갈 수도 있기 때문에 예외가 터져 프로그램이 종료될 위험이 있다.

 

제네릭을 사용하면 위와 같은 문제들을 해결할 수 있다.

ArrayList의 코드를 보면 <E> 를 타입 변수라고 하고 E부분에 타입을 적고 사용하면 된다.

ArrayList<Integer> arr = new ArrayList<>();

위처럼 <> 안에 타입만 지정해 주면 된다. 

 

<> 안에 들어가는 타입 변수는 아무렇게나 적어줘도 상관없지만 문서에는 다음과 같이 나와있다.

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


제네릭(Generic) 클래스

class Data<T>{
    private T data;
    public Data(T data){
        this.data = data;
    }
    public T getData() {
        return data;
    }
    public void setData(T data) {
        this.data = data;
    }
}

제네릭 클래스는 위와 같이 <>와 타입 변수로 클래스를 만들면 인스턴스를 생성할 때 원하는 임의의 타입을 넣어서 사용할 수 있다.

Data<String> d1 = new Data<>("123");
Data<Integer> d2 = new Data<>(123);
Data<Boolean> d3 = new Data<>(true);

인스턴스를 생성하는 new Data<>에는 타입이 생략되었는데 참조 변수의 타입으로 유추할 수 있기 때문에 생략이 가능하다.


클래스 타입 변수의 제한

제네릭 클래스는 타입 변수를 명시해 한 타입으로 제한해서 사용할 수 있게 한다. 클래스의 타입 변수로 어떤 타입이든 명시하면 사용할 수 있다는 말이다. 클래스에 명시하는 타입을 제한하고 싶을 때는 방법은 extends 키워드를 사용해서 타입을 제한할 수 있다.

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

class Fruit{}
class Apple extends Fruit{}
class Grape extends Fruit{}
class Toy{}

public class GenericTest {
    public static void main(String[] args) {
        FruitBox<Fruit> fruitBox = new FruitBox<Fruit>();
        FruitBox<Apple> appleBox = new FruitBox<Apple>();
        FruitBox<Grape> grapeBox = new FruitBox<Grape>();
        FruitBox<Toy> toyBox = new FruitBox<Toy>(); //에러
    }
}

FruitBox의 제네릭 타입 변수를 보면 <T extends Fruit>으로 명시했다.

이는 FruitBox의 타입 변수는 Fruit과 하위 클래스들만 가능하다고 명시한 것이다. 그래서 main을 보면 Toy 클래스는 Fruit 클래스의 상속을 받지 않아서 인스턴스를 생성하지 못하고 에러가 난다. 

 


와일드 카드

와일드 카드는 제네릭에서 타입의 상한, 하한을 제한할 수 있다.

<? extends T> 상한 제한. T와 하위 타입만 가능
<? super T>    하한 제한. T와 상위 타입만 가능
<?>                 제한 없음 <? extends Object>와 같음
class Phone {}

class IPhone extends Phone {}
class Galaxy extends Phone {}

class IPhone13Pro extends IPhone {}
class ZFlip3 extends Galaxy {}

class User<T> {
    public T phone;

    public User(T phone) {
        this.phone = phone;
    }
}

IPhone과 Galaxy는 Phone을 상속받고 IPhone13Pro와 ZFlip3는 각각 IPhone과 Glaxay를 상속받는 구조를 갖는다고 하자.

class Function {
    public static void katalk(User<? extends Phone> user) {
        System.out.println("phone = " + user.phone.getClass().getSimpleName() + " katalk");
    }

    public static void faceId(User<? extends IPhone> user) {
        System.out.println("phone = " + user.phone.getClass().getSimpleName() + " IPhone FaceId");
    }

    public static void samsungPay(User<? extends Galaxy> user) {
        System.out.println("phone = " + user.phone.getClass().getSimpleName() + " Samsung Pay");
    }
}

Functin 클래스는 기능을 정의한 클래스이다. 위와 같이 와일드카드로 타입을 제한하고 각 기능을 호출해보자.

public class GenericTest {
    public static void main(String[] args) {
        Function.katalk(new User<Phone>(new Phone()));
        Function.katalk(new User<IPhone>(new IPhone()));
        Function.katalk(new User<Galaxy>(new Galaxy()));
        Function.katalk(new User<IPhone13Pro>(new IPhone13Pro()));
        Function.katalk(new User<ZFlip3>(new ZFlip3()));

//        Function.faceId(new User<Phone>(new Phone())); //에러
        Function.faceId(new User<IPhone>(new IPhone())); //애러
        Function.faceId(new User<IPhone13Pro>(new IPhone13Pro()));
//        Function.faceId(new User<Galaxy>(new Galaxy())); //에러
//        Function.faceId(new User<ZFlip3>(new ZFlip3())); //에러

//        Function.samsungPay(new User<Phone>(new Phone())); //에러
//        Function.samsungPay(new User<IPhone>(new IPhone())); //에러
//        Function.samsungPay(new User<IPhone13Pro>(new IPhone13Pro())); //에러
        Function.samsungPay(new User<Galaxy>(new Galaxy()));
        Function.samsungPay(new User<ZFlip3>(new ZFlip3()));
    }
}

katalk 메서드는 <? extends Phone>으로 Phone을 상속받는 하위 클래스들로 타입을 명시해서 모든 타입 변수가 가능하다.

faceId는 <? extends IPhone>으로 명시했고 Phone 그리고 Galaxy와 하위 클래스는 faceId 메서드의 타입 변수가 될 수 없다.

마찬가지로 samsungPay에는 IPhone과 하위클래스 그리고  Phone 클래스는 타입 변수로 명시가 불가능하다.

 

 

BELATED ARTICLES

more