제네릭

양성빈·2022년 8월 8일
post-thumbnail

참고
자바의 정석

제네릭

JDK1.5부터 처음 도입된 제네릭은 엄청난 혁명이였다. 당시에는 일부 개발자들만 선택적으로 사용하였으나 요즘은 제네릭을 모르면 Java API 문서도차 읽을 수 없을 지경이되어 엄청난 개념이 되었다.

제네릭이란?

제네릭은 다양한 타입의 객체들으 ㄹ다루는 메서드나 컬렉션 클래스에 컴파일 시의 타입체크를 해주는 기능이다. 객체의 타입을 컴파일 시에 체크하기 때문에 객체의 타입 안정성을 높이고 형변환의 번거로움이 줄어든다. 타입의 안정성을 높인다는 것은 의도허ㅏ지 않은 타입의 객체가 저장되는 것을 막고, 저장된 객체를 꺼내올 때 원래의 타입과 다른 타입으로 잘못 형변환되어 발생할 수 있는 오류를 줄여준다.

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

제네릭 클래스의 선언

제네릭 타입은 클래스와 메서드에 선언이 가능한데 사용방법은 아래와 같다.

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

위의 코드에서 T는 타입변수라고 하며 변수라 T가 아니라 다른 것을 사용해도 무방하다. 즉, 상황에 맞게 의미 있는 문자를 사용하는 것이 좋다. 또한 이들은 기호의 종류만 다를뿐 임의의 참조형 타입을 의미한다.

또한 우리는 이전 컬렉션을 배울때 컬렉션 클래스에 제네릭을 설정하지 앟았다. 하지만 그래도 에러는 나지 않았다. 그 이유는 제네릭 도입되기 이전의 코드와 호환성을 유지하기 위하여 제네릭을 사용하지 않는 코드를 허용하긴 하지만 경고문은 발생한다. 경고문을 읽어보면 반드시 제네릭 클래스 사용시 타입을 반드시 설정해주라는 문장이 담길 것이다. 앞으로 제네릭 클래스를 사용할 때는 반드시 타입을 설정해주자.

제네릭의 용어

class Box<T> {}

Box< T > : 제네릭 클래스. 'T의 Box' 또는 'T Box'라고 읽는다.
T: 타입 변수 또는 타입 매개변수
Box: 원시 타입

제네릭의 제한

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

하지만 모든 객체에 대해 동일하게 동작해야 하는 static 멤버에 타입변수 T를 사용이 불가능하다. 이유는 T는 인스턴스 변수로 간주되므로 static멤버는 인스턴스 변수를 참조할 수 없다.

또한 제네릭 타입의 배열을 생헝하는 것도 허용되지 않는다. 제네릭 배열 타입의 참조변수를 선언하는 것은 가능하지만 new 연산자를 통해 배열을 생성하는 것은 불가능하다.

제네릭 배열을 생성할 수 없는 이유는 new 연산자이기 때문인데 이 연산자는 컴파일 시점에 타입 T가 무엇인지 알아야 하는데 컴파일 시점에 T가 무엇인지 알 수 없기 때문에 배열을 생성이 불가능하다.

만약 굳이 배열을 생성해야 한다고 하면 new 연산자 대신에 Relection API를 사용하여 동적으로 객체를 생성하는 메서드로 배열을 생성하거나 Object 배열을 생성해서 복사한 다음 T[] 타입으로 형변환하는 것이 중요하다.

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

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

    void add(T item) {
        list.add(item);
    }

    T get(int i) {
        return list.get(i);
    }

    int size() {
        return list.size();
    }

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

제네릭 클래스의 객체를 생성할 때는 참조변수와 생성자에 대입된 타입이 일치해야 한다. 일치하지 않으면 에러가 발생하며 두 타입이 상속관계여도 대입된 타입이 일치하지 않으면 에러가 발생한다.

제한된 제네릭 클래스

타입문자로 사용할 타입을 명시하면 한 종류의 타입만 저장할 수 있도록 제한할 수 있지만 그래도 여전히 모든 종류의 타입을 지정할 수 있다는 것에 변함이 없다. 하지만 제네릭 타입에 extends 키워드를 사용하면 특정 타입의 자손들만 대입할 수 있다.

class FruitBox<T extends Fruit> {
	ArrayList<T> list = new ArrayList<>();
}

만약 인터페이스를 구현해야 한다면 implements 키워드 대신에 & 키워드를 사용한다.

class FruitBox<T extends Fruit & Eatable> extends Box<T> {

}

와일드 카드

어떤 클래스에 static 메서드가 있고 매개변수로 제네릭 클래스를 받는다고 할 때 뭔가 이상하다고 느낄것이다. 방금 전까지 static 메서드에는 제네릭을 사용할 수 없다고 했다. 즉, 제네릭 클래스를 매개변수로 받지 말던지 타입 매개변수 대신에 특정 타입을 둬야 할 것이다. 이런 점에서 불편한점이 있다. 어떤 측면에서 제네릭 클래스를 매개변수로 받는 메서드를 여러개 오버로딩 하면 될것 같지만 제네릭 타입이 다른 것만으로는 오버로딩이 성립되지 않기 때문이다. 이를 위해 나타난 것이 와일드 카드이다. 와일드 카드 기호는 '?'이며 와일드 카드는 어떠한 타입도 될 수 있다.

?만으로는 Object타입과 동일하므로 extends 키워드나 super 키워드로 상한과 하한을 제한 할 수 있다.

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

제네릭 메서드

메서드의 선언부에 제네릭 타입이 선언된 메서드를 제네릭 메서드라고 한다. 대표적인 예시로 Collections.sort()이다.

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

주의할 점은 제네릭 클래스에 정의된 타입 매개변수와 제네릭 메서드에 정의된 타입 매개변수는 이름만 같을 뿐 전혀 별개의 것이다. 또한 알아둘 점이 static멤버에는 타입 매개변수를 사용할 수 없지만 메서드에 제네릭 타입을 선언하고 사용하는 것은 가능하다.

⚠️ 주의
제네릭 메서드를 호출할 때 대입된 타입을 생략할 수 없는경우에는 참조변수나 클래스 이름을 생략할 수 없다.

제네릭 메서드는 매개변수의 타입이 복잡할 때 유용하다.

한가지 예제를 보면서 살펴보자.

public static <T extends Comparable<? super T>> void sort(List<T> list)

타입 T를 요소로하는 List를 매개변수로 허용한다.

T는 Comparable을 구현한 클래스이어야 하며 (< T extends Comparable >). T 또는 그 상위의 타입을 비교하는 Comparable이어야한다는 것을 의미한다. 만일 T가 Student이고, Person의 자손이라면 < ? super T>는 Student, Person, Object가 모두 가능하다.

제네릭 타입의 형변환

제네릭 타입과 원시타입간의 형변환이 가능할까? 제네릭 타입과 넌제네릭 타입간의 형변환은 항상 가능하다. 그러면 대입된 타입이 다른 제네릭 타입 간에는 형변환이 가능할까? 결론은 불가능하다. 그러면 와일드 카드에 extends Object를 사용하면 형변환은 가능하다. 이렇게 하여 다형성을 적용할 수 있다.

제네릭 타입의 제거

컴파일러는 제네릭 타입을 이용해서 소스파일을 체크하고 필요한 곳에 형변환을 넣어준다. 그리고 제네릭 타입을 제거한다. 즉, 컴파일된 파일에는 제네릭 타입에 대한 정보가 없다. 그러면 제네릭 타입의 제거과정을 살펴보자.

  1. 제네릭 타입의 경계를 제거한다.
    • 제네릭 타입 < T extends Fruit >라면 T는 Fruit로 치환된다. < T >인 경우는 T는 Object로 치환된다. 그리고 클래스 옆의 선언은 제거된다.
  2. 제네릭 타입을 제거한 후에 타입이 일치하지 않으면, 형변환을 추가한다.
    • List의 get()은 Object타입을 반환하므로, 형변환이 필요하다. 만약 와일드 카드가 포함된 경우라면 적절한 타입으로의 형변환이 추가된다.
profile
모든 것을 즐길줄 아는 개발자입니다!

0개의 댓글