Java

제네릭스

hwanguu 2023. 9. 6. 18:14

제네릭스란?

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

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

 

타입 안정성
의도하지 않은 타입의 객체가 저장되는 것을 막고, 저장된 객체를 꺼내올 때 원래의 타입과 다른 타입으로 잘못 형변환되어 발생할 수 있는 오류를 줄여준다.

 

ArrayList<Integer> integers = new ArrayList<>();
integers.add(1);
integers.add("1");  //  Error : Integer 타입만 Add 할수 있음
integers.add(2);
integers.add(3);

Integer integer = integers.get(0);
Integer integer2 = integers.get(1);
Integer integer3 = integers.get(2);
String integer4 = integers.get(2);  //  Error : Integer 타입만 받을수 있음

System.out.println("integer = " + integer);
System.out.println("integer2 = " + integer2);
System.out.println("integer3 = " + integer3);

위 코드와 같이 Integer 타입으로 ArrayList를 생성하게 되면, add 할때, get 할때 따로 타입 체크를 하지 않아도 되는 편리함과, 안정성이 생긴다.

 

 

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

 

 

제네릭스 클래스 선언

 

public class Box {

    Object item;

    public Object getItem() {
        return item;
    }

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

    public Box(Object item) {
        this.item = item;
    }
}

기존에 위와같이 클래스를 선언 했다면 제네릭 클래스는 아래와 같이 선언한다

public class GBox<T> {  //  제네릭타입 T 를 선언

    T item;

    public T getItem() {
        return item;
    }

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

    public GBox(T item) {
        this.item = item;
    }
}

 

Box 클래스에서 제네릭 클래스로 변경한다면 클래스 이름 옆에 <T>를 붙이면 된다. 그리고 Object를 모두 T로 변경한다.

GBox<T>에서 T를 '타입 변수' 라고 하며, 'Type'의 첫 글자에서 따온 것이다.

타입변수는 T가 아닌 다른것을 사용해도 된다. 타입 변수가 여러개인 경우에는 Map<K,V>와 같이 콤마 ',' 를 구분자로 나열하면 된다.

public class MapBox<K,V> {
    
    K key;
    
    V value;
}

 

무조건 T를 사용하기보다 가능하면, 상황에 맞게 의미있는 문자를 선택해서 사용하는 것이 좋다.

 

 

제한된 제네릭 클래스

타입 매개변수 T에 지정할 수 있는 타입의 종류를 제한하고싶다면 어떻게 해야 할까?

public abstract class Fruit {

    public abstract void print();
}

public class Apple extends Fruit {

    @Override
    public void print() {
        System.out.println("Apple");
    }
}

public class Banana extends Fruit {

    @Override
    public void print() {
        System.out.println("Banana");
    }
}

public class Tomato extends Fruit{
    @Override
    public void print() {
        System.out.println("Tomato");
    }
}

public class NoFruit {}
public class FruitBox<T extends Fruit> {	//	Fruit 타입만 FruitBox에 add 할수있도록 제한

    private final List<T> list = new ArrayList<>();

    public void add(T data) {
        list.add(data);
    }

    public List<T> getList() {
        return list;
    }
}

위 코드와 같이 FruitBox<T extends Fruit> 처럼 extends를 사용하게 되면 특정타입의 자손들만 대입할 수 있게 제한할 수 있다.

 

public static void main(String[] args) {
    FruitBox<Fruit> fruitFruitBox = new FruitBox<>();
    fruitFruitBox.add(new Apple());
    fruitFruitBox.add(new Banana());
    fruitFruitBox.add(new Tomato());
    fruitFruitBox.add(new NoFruit());   //  Fruit을 상속받지 않았기 때문에 add할 수 없다.
}

 

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

 

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

만일 클래스 Fruit의 자손이면서 Eatable인터페이스도 구현해야 한다면 위와 같이 '&' 기호로 연결할수 있다.

 

 

와일드카드

 

와일드 카드 종류
<? extends T>  와일드 카드의 상한 제한, T와 그 자손들만 가능
<? super T>      와일드 카드의 하한 제한, T와 그 조상들만 가능
<?>                  제한 없음, 모든 타입이 가능. <? extends Obejct> 와 동일
참고
와일드 카드에는 '&'를 사용할 수 없다. 즉 <? extends T & E> 와 같이 사용할 수 없다

 

public class Juicer {

    public static void print(FruitBox<? extends Fruit> box) {
        box.getList().forEach(f -> System.out.println("f = " + f));
    }
    
}

위와같이 FruitBox 에 ?를 사용하는 것을 와일드 카드라고 한다.

? extends Fruit 이라면 매개변수로 Fruit이거나 Fruit을 상속받은 타입은 모두 매개변수로 가능하다.

즉 FruitBox<Apple>, FruitBox<Banana>, FruitBox<Tomato> 가 모두 가능해진다.

 

? super Apple 라면 Apple 이거나 Apple의 조상들만 매개변수로 가능하다.

 

 

제네릭 메서드

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

Collections.sort()가 제네릭 메서드이며, 제네릭 타입의 선언 위치는 반환 타입 바로 앞이다.

 

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

 

static 옆에 있는 <T>는 메서드에 선언된 제네릭 타입이다. 이런 메서드를 제네릭 메서드라고 한다.

 

제네릭 클래스에 정의된 타입 매개변수와 제네릭 메서드에 정의된 타입 매개변수는 전혀 별개의 것이다. 같은 타입 문자 T를 사용해도 같은 것이 아니라는 것에 주의해야한다.

 

앞서 Juicer의 클래스의 print 메서드를 제네릭 메서드로 변경한다면 아래와 같이 된다.

public class Juicer {

    public static <T extends Fruit> void print(FruitBox<T> box) {
        box.getList().forEach(f -> System.out.println("f = " + f));
    }
    
}

 

 

 

References

Java의 정석, 남궁 성 지음

'Java' 카테고리의 다른 글

날짜 계산 - TemporalAdjusters  (0) 2023.09.11
날짜와 시간 - LocalDateTime, ZonedDateTIme  (0) 2023.09.11
Java Collection Set  (0) 2023.08.24
Java Collection Map  (0) 2023.08.24
HashSet  (0) 2023.08.24