개발

Generics / Variance

빠빠담 2023. 6. 4. 21:14
반응형

Generics

메서드, 컬렉션, 클래스에 컴파일 시의 타입체크(compile-time type check)를 해주는 기능

 

- 객체의 타입을 컴파일 시에 체크하기 때문에 객체의 타입 안정성을 높이고 형변환의 번거로움이 줄어든다

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

  - JDK.15부터 지네릭이 도입되어 하위 호환성을 위해 타입 제거

 

제한

1. Static 멤버에 타입 변수(ex. T)를 사용할 수 없다

- 모든 객체에 대해 동일하게 동작해야하는 static 멤버에 타입변수 사용 불가

- 타입변수는 인스턴스변수로 간주

public class Box<T> {
    static T item;
    static int compare(T t1, T t2) {
        return 1;
    }
}

// 'Box.this' cannot be referenced from a static context
// Make item/compare not static

2. 배열 생성 허용되지 않는다

- 지네릭 배열 타입의 참조변수를 선언하는 것은 가능, new T[10] 과 같이 배열을 생성하는 것은 불가

- new 연산자는 컴파일 시점에 타입 변수(T)가 뭔지 정확히 알아야 한다

- instanceof 연산자도 new 연산자와 같은 이유로 T를 피연산자로 사용할 수 없다

public class Box<T> {
    T[] items;
    T[] toArray() {
        T[] tmpArr = new T[items.length]; // Type parameter 'T' cannot be instantiated directly
        return tmpArr;
    }
}

 

Raw Type의 위험성

Raw Type이란, 제너릭 타입에서 타입 매개 변수를 전혀 사용하지 않을 때를 말한다

public class Trouble<T> {
    public List<String> getStrs() {
        return Arrays.asList("str");
    }

    public static void main(String[] args) {
        Trouble t = new Trouble();

        for (String str : t.getStrs()) { // java: incompatible types: java.lang.Object cannot be converted to java.lang.String
            System.out.println(str);
        }
    }
}
The superclasses (respectively, superinterfaces) of a raw type are the erasures of the superclasses (superinterfaces) of any of the parameterizations of the generic type.
The type of a constructor, instance method, or non-static field of a raw type C that is not inherited from its superclasses or superinterfaces is the raw type that corresponds to the erasure of its type in the generic declaration corresponding to C.

Raw Type의 슈퍼 클래스는 Raw Type이다.
상속 받지 않은 Raw Type의 생성자, 인스턴스 메서드, 인스턴스 필드는 Raw Type이다.

Raw Type은 타입 파라미터 T만 지워버리는 것이 아니라 슈퍼 클래스의 타입 파라미터도 지우고, 해당 클래스에 정의된 모든 타입 파라미터를 지워버린다.

그래서 t.getStrs()의 반환 타입이 List<String>이 아닌 Raw Type List가 된 것이다.

 

Variance

변성(Variance)은 타입의 계층 관계에서 서로 다른 타입 간에 어떤 관계가 있는지 나타내는 개념이다.

Type Parameter에 타입 경계를 명시하여 Sub-Type, Super-Type을 가능하게 해준다

경계 Bound Java Kotlin
상위 경계 Upper bound Type<? extends T> Type<out T>
하위 경계 Lower bound Type<? super T> Type<in T>
public class FruitBox <T extends Fruit> {}
public class FruitBox <T extends Fruit & Eatable> {}
public class FruitBox <T super Citrus> {}

 

전제

public class Animal {}
public class Tiger extends Animal {}
public class Lion extends Animal {}

public class Cage<T> {
    private final List<T> animals = new ArrayList<>();
    public void push(T animal) {
        this.animals.add(animal);
    }
    public List<T> getAll() {
        return animals;
    }
}

문제

public class Client {
    public static void main(String[] args) {
        Cage<Tiger> tigerCage = new Cage<>();
        
        // (1) 이게 된다면
        Cage<Animal> animalCage = tigerCage; // java: incompatible types: Cage<Tiger> cannot be converted to Cage<Animal>
        // (2) Lion은 Animal이므로 가능
        animalCage.push(new Lion());
        // (3) Lion 리스트 리턴
        List<Tiger> tigers = tigerCage.getAll();
    }
}

 

Invariant (무변성)

A(Animal)가 B(Tiger)의 상위 타입일 때

GenericType<A>(Cage<Animal>)가 GenericType<B>(Cage<Tiger>)의 상위 타입이 아니면

변성 없음 (무변성)

- 즉, A(Animal)가 B(Tiger)의 상위 타입일 때, List<A>은 List<B>의 하위 타입도 아니고 상위 타입도 아니다

public class Animal {}
public class Carnivore extends Animal {}
public class Tiger extends Carnivore {}
public class Lion extends Carnivore {}

public class Cage<T> {
    private final List<T> animals = new ArrayList<>();
    public void push(T animal) { this.animals.add(animal); }
    public List<T> getAll() { return animals; }
}

public class Zookeeper {
    public void giveMeat(Cage<Carnivore> cage, Meat meat) {
        List<Carnivore> carnivores = cage.getAll();
        carnivores.forEach(carnivore -> carnivore.eat(meat));
    }
}

public class Client {
    public static void main(String[] args) {
        Zookeeper zookeeper = new Zookeeper();
        Cage<Tiger> tigerCage = new Cage<>();
        zookeeper.giveMeat(tigerCage, meat); // java: incompatible types: Cage<Tiger> cannot be converted to Cage<Carnivore>
    }
}

 

Covariant (공변)

A(Animal)가 B(Tiger)의 상위 타입이고

GenericType<A>가 GenericType<B>의 상위 타입이면 (Cage<? extends Animal>가 Cage<Tiger>의 상위 타입)

공변 - extends 사용해서 공변 처리

 

public class Animal {}
public class Carnivore extends Animal {}
public class Tiger extends Carnivore {}
public class Lion extends Carnivore {}

public class Cage<T> {
    private final List<T> animals = new ArrayList<>();
    public void push(T animal) { this.animals.add(animal); }
    public List<T> getAll() { return animals; }
}

public class Zookeeper {
    public void giveMeat(Cage<? extends Carnivore> cage, Meat meat) {
        List<? extends Carnivore> carnivores = cage.getAll();
        carnivores.forEach(carnivore -> carnivore.eat(meat));
    }
}

public class Client {
    public static void main(String[] args) {
        Zookeeper zookeeper = new Zookeeper();
        
        // Cage<? extends Carnivore> 타입에 Cage<Tiger> 할당 가능
        Cage<Tiger> tigerCage = new Cage<>();
        zookeeper.giveMeat(tigerCage, meat);
        
        // Cage<? extends Carnivore> 타입에 Cage<Lion> 할당 가능
        Cage<Lion> lionCage = new Cage<>();
        zookeeper.giveMeat(lionCage, meat);
    }
}

 

 문제점

공변에서 지네릭 타입을 사용하는 메서드에 값 전달 안 됨

public class Animal {}
public class Carnivore extends Animal {}
public class Tiger extends Carnivore {}
public class Lion extends Carnivore {}

public class Cage<T> {
    private final List<T> animals = new ArrayList<>();
    public void push(T animal) { this.animals.add(animal); }
    public List<T> getAll() { return animals; }
}

public class Zookeeper {
    public void giveMeat(Cage<? extends Carnivore> cage, Meat meat) {
        List<? extends Carnivore> carnivores = cage.getAll();
        carnivores.forEach(carnivore -> carnivore.eat(meat));
    }
}

public class Client {
    public static void main(String[] args) {
        Cage<Tiger> tigerCage = new Cage<>();
        Cage<? extends Carnivore> carnivoreCage = tigerCage;
        
        // Cage의 실제 타입이 Cage<Tiger>인지 Cage<Lion>인지 알 수 없음
        carnivoreCage.push(new Tiger()); // java: incompatible types: Tiger cannot be converted to capture#1 of ? extends Carnivore
    }
}

 

Contravariant (반공변)

A(Animal)가 B(Tiger)의 상위 타입이고

GenericType<A>가 GenericType<B>의 하위 타입이면 (Cage<Animal>가 Cage<? super Tiger>의 하위 타입)

반공변 - super 사용해서 반공변 처리

public class Animal {}
public class Carnivore extends Animal {}
public class Tiger extends Carnivore {}
public class Lion extends Carnivore {}

public class Cage<T> {
    private final List<T> animals = new ArrayList<>();
    public void push(T animal) { this.animals.add(animal); }
    public List<T> getAll() { return animals; }
}

public class Zookeeper {
    public void giveMeat(Cage<? extends Carnivore> cage, Meat meat) {
        List<? extends Carnivore> carnivores = cage.getAll();
        carnivores.forEach(carnivore -> carnivore.eat(meat));
    }
}

public class Client {
    public static void main(String[] args) {
        Cage<Tiger> tigerCage = new Cage<>();
        // Cage<? super Tiger> 타입에 Cage<Tiger> 할당 가능
        Cage<? super Tiger> tigerAncestorCage = tigerCage;
        // tigerAncestorCage는 최소 Cage<Tiger>나 그 상위 타입
        tigerAncestorCage.push(new Tiger());

        Cage<Carnivore> carnivoreCage = new Cage<>();
        // Cage<? super Lion> 타입에 Cage<Carnivore> 할당 가능
        Cage<? super Lion> lionAncestorCage = carnivoreCage;
        // lionAncestorCage는 최소 Cage<Lion>나 그 상위 타입
        lionAncestorCage.push(new Lion());
    }
}

 

PECS

Producer-Extends, Consumer-Super

- 값을 제공하면 extends

public class Zookeeper {
    public void giveMeat(Cage<? extends Carnivore> cage, Meat meat) {
    	// 값을 제공하면 extends
        List<? extends Carnivore> carnivores = cage.getAll();
        carnivores.forEach(carnivore -> carnivore.eat(meat));
    }
}

- 값을 사용하면 super

public class Client {
    public static void main(String[] args) {
        Cage<Carnivore> carnivoreCage = new Cage<>();
        Cage<? super Lion> lionAncestorCage = carnivoreCage;
        // 값을 사용하면 super
        lionAncestorCage.push(new Lion());
    }
}

 

지점에 따른 변성

선언지점 변성 (declaration-site variance)

- 클래스에 선언 함으로 사용 지점에서 따로 타입을 지정해줄 필요 없음

사용지점 변성 (use-site variance)

- 메서드 파라미터, 또는 제네릭 클래스 생성시

- 구체적인 사용 위치에서 변성 지정

- 한정 와일드카드(bounded wildcard) 방식으로 표현한다

 

Appendix

한글 영어 예시
매개변수화 타입 parameterized type List<String>
실제 타입 매개변수 actual type parameter String
제네릭 타입 generic type List<T>
정규 타입 매개변수 formal type parameter T
비한정적 와일드카드 타입 unbounded wildcard type List<?>
원시 타입 raw type List
한정적 타입 매개 변수 bounded type parameter <T extends Number>
재귀적 타입 한정 recursive type bound <T extends Comparable<T>>
한정적 와일드카드 타입 bounded wildcard type List<? extends Number>

 

Reference

Java의 정석

[Happiness on code] Raw Type을 쓰면 안되는 이유

[민동현] raw 타입은 사용하지 말자

[최범균] 프로그래밍 초식 - 지네릭 변성

[lsb156] 공변성, 반공변성, 무공변성이란?

 

반응형

'개발' 카테고리의 다른 글

Redis cache eviction  (0) 2023.06.15
OS 메모리 관리  (0) 2023.06.15
BASE Principle  (0) 2023.06.04
Slack Webhook  (0) 2021.12.26
큰 단위의 설계만이 설계가 아니다.  (0) 2021.09.04