본문 바로가기
프로그래밍언어/Java

[자바의 정석] 12. 지네릭스

by qkzkdo 2023. 9. 28.
728x90

1.1 지네릭스란?

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

객체의 타입을 컴파일 시에 체크하기 때문에 객체의 타입 안정성을 높이고 형변환의 번거로움이 줄어든다.(컴파일 한계를 넘어서게 해줌)

*컴파일러에게 타입정보를 주는 것. 형변환 에러를 줄일 수 있다.

 

지네릭스의 장점

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

간단히 얘기하면 다룰 객체의 타입을 미리 명시해줌으로써 번거로운 형변환을 줄여준다는 얘기다.

 

 

1.2 지네릭 클래스의 선언

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

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

//일반 클래스
class Box {
    Object item;

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

//지네릭 클래스로 변경
class Box<T> { //지네릭 타입 T를 선언 //Box클래스는 지네릭 클래스가 됐다.
    T item;

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

 

Box<T>에서 T를 '타입 변수(type variable)'라고 하며, 'Type'의 첫 글자에서 따온 것이다. 타입 변수는 T가 아닌 다른 것을 사용해도 된다. 타입 변수가 여러 개인 경우 Map<K, V>와 같이 콤마','를 구분자로 나열하면 된다.(Key, Value)

 

이들은 기호의 종류만 다를 뿐 '임의의 참조형 타입'을 의미한다는 것은 모두 같다.

 

이제 지네릭 클래스가 된 Box클래스의 객체를 생성할 때는 다음과 같이 참조변수와 생성자 타입 T대신에 사용될 실제 타입을 지정해주어야 한다.

Box<String> b = new Box<String>(); //타입 T 대신, 실제 타입을 지정
b.setItem(new Object()); //에러. String이외의 타입은 지정불가
b.setItem("ABC"); //Ok. String타입이므로 가능
String item = (String) b.getItem(); //형변환이 필요없음(String밖에 못들어 온다는걸 알고있음)

위의 코드에서 타입 T대신에 String타입을 지정해줬으므로, 지네릭 클래스 Box<T>는 다음과 같이 정의된 것이다.

class Box { //지네릭 타입을 String으로 지정
    String item;

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

 

지네릭이 도입되기 이전의 코드와 호환을 위해, 지네릭 클래스인데도 예전의 방식으로 객체를 생성하는 것이 허용된다.(다만, 경고발생)

Box b = new Box(); //OK. T는 Object로 간주된다.
b.setItem("ABC"); //경고. unchecked or unsafe operation
b.setItem(new Ojbect()); //경고. unchecked or unsafe operation

 

아래와 같이 타입 변수 T에 Object타입을 지정하면, 타입을 지정하지 않은 것이 아니라 알고 적은 것이므로 경고는 발생하지 않는다.

Box<Object> b = new Box<Obejct>(); 
b.setItem("ABC"); //경고발생 안함
b.setItem(new Ojbect()); //경고발생 안함

앞으로 지네릭 클래스를 사용할 때는 반드시 타입을 지정해서 지네릭스와 관련된 경고가 나오지 않도록 하자.

 

지네릭스의 용어

Box<T> : 지네릭스 클래스. ‘T의 Box’또는 ‘T Box’라고 읽는다.
T : Box<T>의 타입 변수 또는 타입 매개변수.(T는 타입 문자)
Box : 원시 타입(raw type)

 

지네릭스의 제한

지네릭 클래스 Box의 객체를 생성할 때, 객체별로 다른 타입을 지정하는 것은 적절하다. 지네릭스는 이처럼 인스턴스별로 다르게 동작하도록 하려고 만든 기능이니까.

* 타입 변수에 대입은 인스턴스별로 다르게 가능

 

Box<Apple> appleBox = new Box<Apple>(); //OK. Apple객체만 저장가능
Box<Grape> grapeBox = new Box<Grape>(); //OK. Grape객체만 저장가능

 

①static(모든 인스턴스에 공통) 멤버에 타입변수 사용 불가

class Box<T> {
    static T item; //에러
    static int compare(T t1, T t2) { ... } //에러
    ...
}

 

②배열(객체) 생성할 때 타입 변수 사용 불가. 타입 변수로 배열 선언은 가능

class Box<T> {
    T[] itemArr; //OK. T타입의 배열을 위한 참조변수
        ...
    T[] tmpArr() {
        T[] tmpArr = new T[itemArr.length]; //에러. 지네릭 배열 생성불가
        ...
        return tmpArr;
    }
        ...
}

 

지네릭 배열을 생성할 수 없는 것은 new연산자 때문인데, 이 연산자는 컴파일 시점에 타입 T가 뭔지 정확히 알아야 한다. 그런데 위의 코드에 정의된 Box<T>클래스를 컴파일하는 시점에는 T가 어떤 타입이 될지 전혀 알 수 없다. instanceof연산자도 new연산자와 같은 이유로 T를 피연산자로 사용할 수없다.

 

 

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

지네릭 클래스 Box<T>의 객체에는 한 가지 종류, 즉 T타입의 객체만 저장할 수 있다. 전과 달리 ArrayList를 이용해서 여러 객체를 저장할 수 있도록 하였다.

class Box<t> {
    ArrayList<T> list = new ArrayList<T>();

    void add(T item) {list.add(item);}
    T get(int i) { return list.get(i);}
    ArrayList<T> getList() { return list;}
    int size() { return list.size();}
    public String toString() { return list.toString();
}

 

 

1.4 제한된 지네릭 클래스

지네릭 타입에 'extends'를 사용하면, 특정 타입의 자손들만 대입할 수 있게 제한할 수 있다.

다음과 같이 지네릭 타입에 ‘extends Fruit’를 사용하면, 특정 타입의 자손들만 대입할 수 있게 제한할 수 있다.

class FruitBox<T Extends Fruit> { //Fruit의 자손만 타입으로 지정가능
    ArrayList<T> list = new ArrayList<T>();
    ...
}
//여전히 한 종류의 타입만 담을 수 있지만, 
//Fruit클래스의 자손들만 담을 수 있다는 제한이 더 추가된 것이다.

 

 

1.5 와일드 카드

*하나의 참조 변수로 대입된 타입이 다른 객체를 참조 가능

 

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

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

<? extends T> : 와일드 카드의 상한 제한. T와 그 자손들만 가능  *많이사용
<? super T> : 와일드 카드의 하한 제한. T와 그 조상들만 가능
<?> : 제한 없음. 모든 타입이 가능. <? extends Obejct>와 동일

 

 

1.6 지네릭 메서드

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

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

 

 

1.7 지네릭 타입의 형변환

*지네릭 타입과 원시 타입간의 형변환은 바람직하지 않다.(경고 발생)

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

box = (Box)objBox; //Ok. 지네릭 타입 → 원시 타입. 경고 발생
objBox = (Box<Object>)box; //Ok. 원시 타입 → 지네릭 타입. 경고 발생

 

 

1.8 지네릭 타입의 제거

컴파일러는 지네릭 타입을 이용해서 소스파일을 체크하고, 필요한 곳에 형변환을 넣어준다. 그리고 지네릭 타입을 제거한다. 즉, 컴파일된 파일(*.class)에는 지네릭 타입에 대한 정보가 없는 것이다.

이렇게 하는 주된 이유는 지네릭이 도입되기 이전의 소스 코드와의 호완성을 유지하기 위해서이다.

 

 

 

출처 : 남궁성. 「자바의 정석」. 도우출판. 2016

728x90