개발/Java&Kotlin

[Java] Generics

devhooney 2022. 8. 17. 09:54
728x90

자바의 정석 챕터 13을 읽고 정리

 

1. 지네릭스란?

- 지네릭스는 다양한 타입의 객체들을 다루는 메소드나 컬렉션 클래스에 컴파일 시 타입 체크를 해주는 기능

- 타입 안정성 제공

- 타입체크와 형변환을 생략할 수 있어서 코드가 간결해짐

 

2. 지네릭 클래스 선언

- 클래스에 지네릭 타입을 넣을 수 있다.

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

// 지네릭 타입 T를 선언
class Box<T> {
    T item;
    
    void setItem(T item) {
    	this.item = item;
    }
    
    T getItem() {
    	return item;
    }
}

- T는 타입변수

 

지네릭스의 용어

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

T: 타입 변수 또는 타입 매개변수

Box: 원시 타입(raw type)

 

지네릭스의 제한

- 지네릭 클래스 Box의 객체를 생성할 때, 객체별로 다른 타입을 지정할 수 있다.

Box<Apple> = new Box<>();
Bpx<Grape> = new Box<>();

- static멤버에는 타입변수 T를 사용할 수 없다.

- static은 모든 객체에 동일하게 동작해야 하기 때문

T[] itemArr; // T타입의 배열을 위한 참조변수
T[] tmpArr = new T[itemArr.length]; // 에러

- 지네릭 타입의 배열은 사용할 수 없다.

- new 연산자 때문

- new는 컴파일 시점에 타입T가 뭔지 정확히 알아야 한다.

- instanceof 연산자도 마찬가지.

- 지네릭 배열이 필요한 경우 Object 배열을 생성하여 복사한 뒤 T[]로 형변환 해서 사용.

 

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

- 지네릭 클래스 Box<T>가 있다고 가정하면, 이 Box<T>는 T타입의 객체만 저장 가능하다.

class Box<T> {
    ArrayList<T> list = new ArrayList<>();
    
    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() { list.toString(); }
}

- Box<T>의 객체를 생성할 때는 참초변수와 생성자에 대입된 타입이 일치해야 한다.

Box<Apple> appleBox = new Box<Apple>();
Box<Apple> appleBox = new Box<>();
Box<Apple> appleBox = new Box<Grape>(); // 에러

- 두 타입이 상속관계여도 마찬가지. Apple이 Fruit의 자식이라고 가정

Box<Fruit> appleBox = new Box<Apple>(); // 에러

- 두 개의 지네릭 타입이 상속관계, 대입된 타입이 같은 경우 가능. FruitBox가 Box의 자식이라고 가정

Box<Apple> appleBox = new FruitBox<Apple>();
Box<Apple> appleBox = new FruitBox<>(); // 생략 가능

- 생성된 Box<T> 객체에 void add(T item)으로 객체를 추가할 때, 대입된 타입과 다른 타입의 객체는 추가할 수 없다.

Box<Apple> appleBox = new Box<>();
appleBox.add(new Apple());
appleBox.add(new Grape()); // 에러

- 타입 T가 Fruit인 경우 void add(Fruit item)이 되므로 Fruit 자식들은 추가가 가능하다.

Box<Fruit> fruitBox = new Box<>();
fruitBox.add(new Fruit());
fruitBox.add(new Apple());

 

4. 제한된 지네릭 클래스

- extends로 특정 타입의 자식들만 대입할 수 있게 제한할 수 있다.

class FruitBox<T extends Fruit> { // Fruit의 자식들만 타입으로 지정 가능
    ArrayList<T> list = new ArrayList<>();
}

FruitBox<Apple> appleBox = new FruitBox<>();
FruitBox<Toy> toyBox = new FruitBox<>(); // 에러

- 인터페이스를 구현해야 하는 제약을 줄 때도 extends 사용

interface Eatable {}

class FruitBox<T extends Eatable> {
...
}

- Fruit의 자식이면서 Eatable 인터페이스를 사용해야 할 경우

class FruitBox<T extends Fruit & Eatable> {
...
}

 

728x90

 

5. 와일드 카드

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

- 위의 클래스는 지네릭 클래스가 아니다.

- static 이므로 T를 사용할 수 없다.

FruitBox<Fruit> fBox = new FruitBox<>();
FruitBox<Apple> aBox = new FruitBox<>();

...

System.out.println(Juicer.makeJuice(fBox); // FruitBox<Fruit>
System.out.println(Juicer.makeJuice(aBox); // 에러 FruitBox<Apple>

- 이렇게 지네릭 타입을 FruitBox<Fruit>으로 고정하면, <Apple>은 makeJuice()의 매개변수가 될 수 없다.

- 여러개를 만든다?

- 지네릭 타입이 다르다고 오버로딩은 되지 않는다.

- 이럴 때 와일드 카드(?)를 사용한다.

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

<? super T>: 와일드 카드 하한 제한. T와 T의 부모들만 가능

<?>: 제한 없음. 모든 타입 가능.

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

- 이 메소드는 <Apple>, <Grape>도 가능하다.

FruitBox<Fruit> fBox = new FruitBox<>();
FruitBox<Apple> aBox = new FruitBox<>();

...

System.out.println(Juicer.makeJuice(fBox); // FruitBox<Fruit>
System.out.println(Juicer.makeJuice(aBox); // FruitBox<Apple>

 

6. 지네릭 메소드

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

static <T> void sort(List<T> list, Comparator<? super T> c)

- 앞서 나왔던 makeJuice()를 지네릭 메소드로 바꾸면?

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

- 이 메소드를 호출 할 때는 타입변수에 타입을 대입해야 한다.

FruitBox<Fruit> fBox = new FruitBox<>();
...
System.out.println(Juicer.<Fruit>makeJuice(fBox));
System.out.println(Juicer.makeJuice(fBox)); // 사실 생략해도 컴파일러가 타입을 추정가능함

 

 

728x90