제네릭

hyemin·2022년 3월 2일
0

JAVA

목록 보기
1/3

지네릭스

지네릭스란?

지네릭스는 다양한 타입의 객체들을 다루는 메서드나 컬렉션 클래스에 컴파일 시의 타입을 체크(compile-time type check)를 해주는 기능이다.

  • 객체의 타입을 컴파일 시에 체크하기 때문에 객체의 타입 안정성을 높이고 형변환의 번거러움이 줄어든다.
  • 타입 안정성을 높인다는 것은 의도하지 않은 타입의 객체가 저장되는 것을 막고, 저장된 객체를 꺼내올 때 원래의 타입과 다른 타입으로 잘못 형변환되어 발생할 수 있는 오류를 줄여준다는 뜻이다.

지네릭스의 장점

  1. 타입 안정성을 제공한다.
  2. 타입체크와 형변환을 생략할 수 있으므로 코드가 간결해진다.

다룰 객체의 타입을 미리 명시해줌으로써 번거러운 형변환을 줄여준다.

지네릭 클래스의 선언

지네릭 타입은 클래스와 메서드에 선언할 수 있다.

클래스에 선언하는 지네릭 타입

아래와 같은 클래스 Box가 정의되어 있을 때

    class Box{
        Object item;

        void setItem(Object item){this.item = item;}
        Object getItem(){return item;}
    }

위의 클래스를 지네릭스 클래스로 변경할 때

    class Box<T>{ // 지네릭 타입 T를 선언한다.
        T item

        void setItem(T item){this.item = item;}
        T getItem(){return item;}
    }
  • Box<T>에서 'T'를 타입 변수라고 한다.(Type의 첫 글자를 따온 것)
  • 타입 변수는 T가 아닌 다른 것을 사용해도 되지만, 상황에 맞게 의미 있는 문자로 선택해서 사용하는 것이 좋다.
  • 타입 변수가 여러 개인 경우에는 콤마','를 구분자로 나열하면 된다.

    객체를 생성할 때는 참조변수와 생성자 타입에 T대신 사용될 실제 타입을 지정해 주어야 한다.

    Box<String> b = new Box<String>(); //타입 T대신 실제 타입을 지정한다.
 
    //b.setItem(new Obeject()); String 타입이 아니므로 에러발생
    b.setItem("ABC") //String 타입이므로 에러가 발생하지 않는다.
    String item = b.getItem(); //형변환 없이 가져올 수 있다.

이렇게 위처럼 T대신에 String 타입을 지정해주었기 때문에, 지네릭 클래스 Box는 아래와 같이 정의된 것이다.

    class Box<String>{ // 지네릭 타입 T를 선언한다.
        String item

        void setItem(String item){this.item = item;}
        String getItem(){return item;}
    }

지네릭 클래스여도 이전의 방식으로 객체를 생성하는 것이 허용되지만, 지네릭 타입을 지정하지 않아서 안전하지 않다는 경고가 발생한다.

지네릭스의 용어

Box<T> : 지네릭 클래스. 'T의 Box' 또는 'T Box'라고 읽는다. 

T : 타입 변수 또는 타입 매개변수.(T는 타입 문자)

Box : 원시 타입(raw type)

지네릭스의 제한

  • 모든 객체에 대해 동일하게 동작해야하는 static멤버에 타입 변수 T를 사용할 수 없다.
  • 지네릭 타입의 배열을 선언하는 것도 허용되지 않는다.(지네릭 배열 타입의 참조변수를 선언하는 것은 가능하지만, 배열생성은 불가능)
    T[] itemArr; //T타입의 배열을 위한 참조변수는 가능

    T[] tmpArr = new T[5]; //에러 발생 지네릭 배열 생성불가.

지네릭 배열을 생성할 수 없는 이유는 new연산자 때문인데, 이 연산자는 컴파일 시점에 타입 T가 뭔지 정확히 알아야한다.

  • 하지만 위의 코드로는 T가 어떤 타입이 될 지 알 수 없기 때문에 배열을 생성하는 것이 불가능하다.
  • 이와 같은 이유로 instanceof도 사용 불가능하다.

지네릭 클래스의 객체 생성과 사용

참조변수와 생성자에 대입된 타입(매개변수화된 타입)이 일치해야 한다.

  • JDK1.7부터는 추정이 가능한 경우 타입을 생략할 수 있게 되었다.
    Box<Apple> apple = new Box<Apple>();
    Box<Apple> apple = new Box<>(); //생략가능.

두 지네릭스 타입이 상속관계에 있더라도 참조변수와 생성자에 대입된 타입이 다르면 안된다.

  • 하지만 메서드의 경우 상속의 관계는 추가가 가능하다.
    class Box<T>{
        ArrayList<T> list = new ArrayList<T>();
        void add(T item);
    }
//Apple의 조상이 Fruit인 경우
Box<Fruit> fruitBox = new Box<Fruit>();
fruitBox.add(new Fruit());
fruitBox.add(new Apple()); //이것도 가능하다.
📌 위의 코드로 확인한 것 처럼 자손들은 매개변수가 될 수 있다.

## 제한된 지네릭 클래스
타입 문자로 사용할 타입을 '명시'하여 한 종류의 타입만 저장할 수 있도록 제한할 수 있지만, 여전히 모든 타입을 명시하여 저장할 수 있다.

**그렇다면, 타입 매개변수 T에 지정할 수 있는 타입의 종류를 제한할 수 있는 방법을 알아보자.**

- 지네릭 타입에 'extends'를 사용하면, 특정 타입의 자손들만 대입할 수 있게 제한할 수 있다.
~~~java
    class FruitBox<T extends Fruit>{//Fruit타입의 자손들만 T타입으로 지정 가능하다.
        ....
    }

위의 코드를 보면 여전히 한 종류의 타입만 담을 수 있지만, Fruit타입의 자손들만 T타입으로 담을 수 있다는 제한이 더 추가되었다.

클래스가 아니라 인터페이스를 구현해야하는 제약이 필요하다면, 이때도 'extends'를 사용해야한다.'implements'를 사용하지 않는 점에 주의하자.

  • 클래스의 자손이면서 인터페이스도 구현해야한다면 '&'기호로 연결하자
    class FruitBox<T extends Fruit & 인터페이스>{...}

와일드 카드

와일드 카드는 '?'로 표현하며, 와일드 카드는 어떠한 타입도 될 수 있다.

  • 지네릭 타입이 다른 것만으로는 메서드 오버로딩을 할 수 없다. 그 이유는 컴파일은 컴파일 시에만 지네릭 타입을 사용하고 제거해버리기 때문에 지네릭 타입만 다르다면 오버로딩이 아닌 '메서드 중복 정의'이다.

예를 들어 static메서드를 정의할 경우 타입 매개변수 T를 매개변수에 사용할 수 없으므로, 타입 매개변수 대신에 특정 타입을 지정해주어야한다.

    static Juice makeJuice(FruitBox<Fruit> box){//T가 아닌 Fruit로 특정 타입을 지정해주었다.
        ...
    }

이렇게 매개변수에 타입 매개변수 T가 아닌 Fruit로 특정 타입을 지정해주었다.

그렇다면 Fruit타입이 아닌 다른 타입을 넣어서 makeJuice메서드를 사용하고 싶을 경우에는 지네릭 타입을 바꿔서 작성해야할까?
답은 아니다. 앞에서 말한 것과 같이 오버로딩이 아닌 메서드 중복 정의가 일어나 컴파일 에러가 발생한다.

✔️ 이러한 문제점을 해결하기 위해 와일드 카드를 사용하는 것이다.

'?'만으로는 Object타입과 다를게 없으므로, 다음과 같이 'extends'와 'super'로 상한(upper bound)과 하한(lower bound)을 제한할 수 있다.

<<? extends T> : 와일드 카드의 상한 제한. T와 그 자손들만 가능하다.

<? super T> : 와일드 카드의 하한 제한. T와 그 조상들만 가능

<?> : 제한없음. 모든 타입이 가능. <? extends Object>와 동일

지네릭 메서드

메서드의 선언부에 지네릭 타입이 선언된 메서드를 지네릭 메서드라고 한다.

  • 지네릭 타입의 선언 위치는 반환타입 바로 앞이다.
    static <T> void sort(...)

그리고 지네릭 클래스에 정의된 타입 매개변수와 지네릭 메서드에 정의된 타입 매개변수는 전혀 다른 별개의 것이다.

  • 같은 타입 문자 T를 사용해도 같은 것이 아니라는 점을 주의하자.
   class FruitBox<T>{
       ...
        static <T> void sort(...)
   }

위의 코드에서 클래스에 선언된 타입 매개변수 T와 지네릭 메서드 sort에 선언된 타입 매개변수 T는 타입 문자만 같을 뿐 서로 다른 것이다.

  • static 멤버는 타입 매개변수를 사용할 수 없지만 이 처럼 메서드에 지네릭 타입을 선언하고 사용하는 것은 가능하다.
    • 메서드에 선언된 지네릭 타입은 지역 변수를 선언한 것과 같다.
    • 메서드 내에서만 지역적으로 사용될 것이므로 static이건 아니건 상관이 없는 것이다.

지네릭 메서드로 한 번 바꿔보자

    static Juice makeJuice(FruitBox<? extends Fruit> box){
        String tmp = "";
        for(Fruit f : box.getList()) tmp += f + " ";
        return new Juice(tmp);
    }

    static <T extends Fruit> Juice makeJuice(FruitBox<T> box){
        String tmp = "";
        for(Fruit f : box.getList()) tmp += f + " ";
        return new Juice(tmp);
    }

이러한 지네릭 메서드를 호출할 때에는 타입 변수에 타입을 대입해야한다.

    FruitBox<Fruit> fruitBox = new FruitBox<Fruit>

    //메서드호출
    Juicer.<Fruit>makeJuice(fruitBox);
  • 지네릭 메서드는 매개변수의 타입이 복잡할 때도 타입을 별도로 선언함으로써 코드를 간략히 할 수 있다.

지네릭 타입의 형변환

지네릭타입과 넌지네릭 타입간의 형변환은 항상 가능하다.
(다만 경고가 발생할 뿐이다.)

    Box box = null;
    Box<Object> objBox = null;

    box = (Box) objBox; //경고발생하지만 OK
    objBox = (Box<Object>) box; //경고발생하지만 OK

✔️ 그렇다면 대입된 타입이 다른 지네릭 타입간의 형변환은 가능할까?

     Box<Object> objBox = null;
     Box<String> strBox = null;

    objbox = (Box<Object>) strBox; //에러발생 
    strBox = (Box<String>) objbox;  //에러발생

위 코드로 확인해본것 처럼 불가능하다.

    Box<Object> objBox = new Box<String>(); //에러
                 //(Box<Object>) new Box<String>();을 생략한것 기억하자.       

앞서 배운것 처럼 위의 코드가 안된다는 것은 형변환이 불가능하다는 것이다.

✔️ 그럼 Box<String>Box<? extends Object>로 형변환이 될까?

    Box<? extends Object> wobjBox = new Box<String>();  //가능    

형변환이 가능하다. 이는 메서드의 매개변수에 다형성이 적용될 수 있었던 이유와 같다.

    static Juice makeJuice(FruitBox<? extends Fruit> box){
        String tmp = "";
        for(Fruit f : box.getList()) tmp += f + " ";
        return new Juice(tmp);
    }

이러한 메서드에 우리는 FruitBox<Apple>,FruitBox<Grape>,FruitBox<Fruit>등을 매개변수로 사용할 수 있었다.


    Optional<?> wopt = new Optional<Object>();
    Optional<Object> oopt = new Optional<Object>();

    Optional<Stinrg> sopt = (Optional<String>) wopt; //1.가능
    Optional<String> sopt1 = (Optional<String>) oopt; //2.에러발생, 형변환 불가  

Optional<?>는 Optional<? extends Object>와 같다고 배웠다.
그래서 1은 형변환이 가능한 것이고, 2는 앞서 확인해본바와 같이 형변환이 불가능한 것이다.

결론적으로, 와일드 카드가 포함된 것을 사용하면 형변환이 가능하다.

지네릭 타입의 제거

컴파일러는 지네릭 타입을 이용해서 소스파일을 체크하고, 필요한 곳에 형변환을 넣어준다. 그리고 지네릭 타입을 제거한다.

  • 즉, 컴파일된 파일(*.class)에는 지네릭 타입에 대한 정보가 없는 것이다.
  1. 지네릭 타입의 경계를 제거한다.
  2. 지네릭 타입을 제거한 후에 타입이 일치하지 않으면, 형변환을 추가한다.

Reference

자바의 정석 3rd Edition

profile
열심히 성장 중 :)

0개의 댓글

관련 채용 정보