스터디플래너/공부하기

[자바의 정석] Ch.12 지네릭스, 열거형, 애너테이션

2022. 6. 8. 22:59

1. 지네릭스(Generics)

1.1 지네릭스란

 지네릭스는 다양한 타입의 객체를 다루는 메서드나 컬렉션 클래스에 컴파일 시의 타입체크(compile-time type check)를 해주는 기능이다. 객체의 타입을 컴파일 시에 체크하기 때문에 객체의 타입 안정성을 높이고 형변환의 번거로움이 줄어든다.

 지네릭스를 사용하는 이유를 간단히 말하면 클래스, 인터페이스, 메서드를 정의할 때, 클래스나 인터페이스의 타입이 매개변수가 되도록 할 수 있기 때문이다. 메소드를 선언할 때 사용하는 익숙한 매개변수처럼 자료형 매개변수는 같은 코드에 다른 입력값으로 재사용할 수 있는 방법을 제공한다. 우리에게 익숙한 매개변수와 지네릭스의 차이는 값을 입력하느냐 자료형을 입력하느냐의 차이다. 지네릭스를 사용한 코드는 그렇지 않은 코드보다 많은 장점이 있다. 

 첫번째, 타입 안정성을 제공한다. 자바 컴파일러는 지네릭 코드에 자료형을 엄격하게 확인하고, 코드가 타입 안정성을 해친다면  에러를 발생시킨다. 컴파일 에러를 고치는 것이 찾기 어려운 런타임 에러를 고치는 것보다 쉽다. 두번째, 코드가 간결해진다. 첫번째 코드는 지네릭스가 없어 형변환이 필요한 코드이고 두번째 코드는 지네릭스를 사용해서 형변환을 하지 않아도 되는 코드이다. 지금은 list에 값을 넣는 메소드가 한 줄이니까 지네릭스를 사용하든 형변환을 사용하든 상관 없어 보일 수 있지만 저 메소드가 여러줄이 된다면 list를 선언할 때 지네릭스를 사용하는 편이 훨씬 깔끔하다. 지네릭스는 객체의 타입을 미리 정의해 번거로운 형변환을 줄여준다.

List list = new ArrayList();
list.add("hello");
String s = (String) list.get(0);
List<String> list = new ArrayList<String>();
list.add("hello");
String s = list.get(0);   // no cast

 

1.2 지네릭스 클래스의 선언

// no generics
class Box{
	Object item;
    
    void setItem(Object item) { this.item = item; }
    void getItem() { return item; }
}

// apply generics
class Box<T>{
	T item;
    
    void setItem(T item) { this.item = item; }
    void getItem() { return item; }
}

Box<T>에서 T는 타입변수(Type variable)로 'Type'의 T를 따왔다. ArrayList<E>의 E는 Element, Map<K, V>는 각각 Key와 Value이다. 모두 기호만 다를 뿐 임의의 참조형 타입을 의미한다. Box<T>에서 T대신 E를 넣어도 되고 ArrayMap<E> 대신 T를 넣어도 된다. 이 클래스로 객체를 생성할 때는 T대신 사용할 자료형을 지정해주면 된다.

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

지네릭스의 용어

지네릭스의 제한

지네릭스는 인스턴스별로 다르게 동작할 수 있도록 만든 기능이므로 객체별로 다른 타입을 지정하는 것이 적절하다. T는 인스턴스 변수로 간주되기 때문에 모든 객체에 동일하게 적용해야 하는 static 멤버는 선언할 수 없다. static멤버는 인스턴스 변수를 참조할 수 없기 때문이다. 그리고 지네릭 타입의 배열을 생성하는 것도 허용되지 않는다. 지네릭 배열 타입의 참조변수를 선언하는 것은 가능하지만 배열을 생성하는 것은 허용되지 않는다.

class Box<T>{
	T[] itemArr; // OK, T 타입 배열을 위한 참조변수를 선언한 것
    T[] toArray(){
    	T[] tempArr = new T[itemArr.length]; // Error, 지네릭 배열 생성 불가
        return tempArr;
    }
}

new 연산자는 컴파일 시점에 T가 무슨 타입인지 정확히 알아야 한다. 하지만 위 코드를 실행하면 컴파일 시점에 Box<T>클래스의 T가 어떤 타입인지 알 수 없다. instanceof연산자도 마찬가지이다. 지네릭 배열을 생성해야 한다면 Reflection API의 newInstance()와 같이 동적으로 객체를 생성하는 메서드를로 배열을 생성하거나, Object배열을 생성해서 복사한 다음 T[]로 형변환해야 한다. 

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

Bot<T>의 객체를 생성할 때는 참조변수와 생성자에 대입된 타입(매개변수화된 타입)이 일치해야 한다. 일치하지 않으면 에러가 발생한다. 두 타입이 상속관계에 있어도 참조변수와 생성자의 타입이 일치하지 않으면 에러가 발생한다. 단 두 지네릭 클래스의 타입이 상속관계에 있고 지네릭스로 대입된 타입이 같은 것은 괜찮다.

Box<Apple> appleBox = new Box<Apple>(); // OK
Box<Apple> appleBox = new Box<Grape>(); // Error, 참조변수의 선언된 타입은 Apple인데 생성자의 참조변수는 Grape
Box<Fruit> appleBox = new Box<Apple>(); // Error, Fruit과 Apple이 상속관계라도 대입된 타입이 다름
Box<Apple> appleBox = new FruitBox<Apple>(); // OK, Box와 FruitBox가 상속관계에 있고, 지네릭스로 대입된 타입이 Apple과 동일

JDK1.7부터는 추정이 가능한 경우 타입을 생략할 수 있다. 참조변수 타입으로부터 어떤 타입의 객체만 저장할 지 알 수 있기 때문에 생성자에서 반복하여 지정하지 않아도 된다.

Box<Apple> appleBox = new Box<>(); // OK

 

1.4. 제한된 지네릭 클래스

생성된 Box<T>의 객체에 객체를 추가할 때, 대입된 타입과 다른 타입의 객체는 추가할 수 없다. 타입 T가 Fruit인 경우 Fruit의 자손 클래스가 메서드의 매개변수가 될 수 있다. 

Box<Apple> appleBox = new Box<Apple>();
appleBox.add(new Apple());
appleBox.add(new Grape()); // Error, Box<Apple>에는 Apple객체만 추가할 수 있음

Box<Fruit> fruitBox = new Box<Fruit>();
fruitBox.add(new Fruit());
fruitBox.add(new Apple());
fruitBox.add(new Grape());
import java.util.ArrayList;

class Fruit{ public String toString(){ return "Fruit"; } }
class Grape extends Fruit{ public String toString(){ return "Grape"; } }
class Apple extends Fruit{ public String toString(){ return "Apple"; } }
class Toy{ public String toString(){ return "Toy"; }}
public class FruitBoxEx1 {
    public static void main(String[] args) {
        Box<Fruit> fruitBox = new Box<>();
        Box<Apple> appleBox = new Box<>();
        Box<Toy> toyBox = new Box<>();
        //Box<Grape> grapeBox = new Box<Apple>();

        fruitBox.add(new Fruit());
        fruitBox.add(new Apple());

        appleBox.add(new Apple());
        appleBox.add(new Apple());
        //appleBox.add(new Fruit());
        //appleBox.add(new Toy());

        toyBox.add(new Toy());
        //toyBox.add(new Apple());

        System.out.println(fruitBox);
        System.out.println(appleBox);
        System.out.println(toyBox);
    }
}

class Box<T>{
    ArrayList<T> list = new ArrayList<T>();
    void add(T item){ list.add(item); }
    T get(int i) { return list.get(i); }
    int size() { return  list.size(); }

    public String toString() { return list.toString(); }
}

 

1.5 와일드 카드

지네릭 타입은 컴파일러가 컴파일할 때만 사용하고 제거한다. 따라서 지네릭 타입만 다른 두 메소드를 선언할 경우 컴파일시 오버로딩이 성립하지 않고 컴파일 에러를 발생시킨다. 이 때 와일드카드('?')를 사용하면 어떤 타입이든 될 수 있다. 와일드카드('?') 자체는 Object타입과 다를 게 없으므로 'extends'와 'super'를 이용하면 상한과 하한을 제한할 수 있다.

<? extends T>  와일드 카드의 상한 제한, T와 그 자손들만 가능
<? super T>      와일드 카드의 하한 제한, T와 그 조상들만 가능
<?>                    제한 없음. <? extends Object>와 동일하여 모든 타입이 가능.

Comparator에서 <? super T>를 자주 사용하는데, 이는 Comparator를 T와 그 조상클래스를 비교할 때 사용할 수 있기 때문이다.

 

1.6 지네릭 메서드

 지네릭 메서드는 메서드 선언부에 지네릭 타입이 선언된 메서드로 지네릭 타입은 반환타입 바로 앞이다. 지네릭 클래스에 정의된 타입 매개변수와 지네릭 메서드에 정의된 타입 매개변수는 전혀 다르다. 같은 T를 사용하더라도 같은 것이 아니다. static 멤버는 타입 매개변수를 사용할 수 없지만 지네릭 메서드에서는 선언하고 사용할 수 있다. 메서드를 호출할 때 대부분 컴파일러가 타입을 추정할 수 있지만, 생략할 수 없는 경우도 있다. 그럴 때는 'this.'나 '클래스 이름.'을 써야한다.

 

1.7 지네릭 타입의 형변환

지네릭 타입과 원시타입 간의 형변환은 바람직하지 않다. JDK1.5에서 지네릭스를 발표한 이후 원시타입만 쓰기보다 지네릭 타입을 사용하는 것이 좋다. 와일드 카드를 사용하는 지네릭스 타입은 형변환이 가능하다. 

Box<Object> objBox = null;
Box<String> strBox = null;
objBox = (Box<Object>)strBox; //Error
strBox = (Box<String>)objBox; //Error

Box <? extends Object> wBox = new Box<String>(); //OK

1.8 지네릭 타입의 제거

지네릭 타입은 컴파일시 다 제거된다. 컴파일러는 지네릭 타입을 이용해서 소스파일을 체크하고, 필요한 곳에 형변환을 넣어준다. 따라서 컴파일된 파일에는 지네릭 타입에 대한 정보가 없다. 지네릭스가 도입된 JDK1.5 이전 버전의 소스코드와 호환성을 유지하기 위해서이다. 이는 Java의 발전을 늦추고 있어 언젠가 사라질 수 있다.

 

1. 지네릭 타입의 경계를 제거한다. <T extends Fruits>은 Fruit으로 치환된다. 클래스의 선언을 제거한다.

2. 지네릭 타입을 제거한 후 타입이 일치하지 않으면 형변환한다.

* 와일드 카드가 포함된 경우 적절한 타입으로 형변환한다.