지네릭스(Generics)

박민수·2023년 2월 3일
0

자바의 정석

목록 보기
13/17
post-thumbnail

1. 지네릭스란?

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

지네릭스의 용어

위와 같이 클래스를 만들 때 클래스 명 옆에 지네릭스를 붙이고 선언하면 이것을 지네릭 클래스라 한다.

그리고 이때 지네릭스에 들어가는 자리의 변수를 타입변수, 타입 매개변수라 한다.

그리고 타입변수로서 들어간 문자는 타입 문자라 한다.
<T>는 타입 변수로 이 클래스로 객체를 생성할 때 해당 객체에 어느 자료형이든 저장 가능하단 뜻이다.
(T가 아니더라도 상관없다. String이든 Integer든 뭐든 가능하다는 의미)
<T>는 객체를 생성하고 이용할 때 어떤 타입인지 결정된다.

마지막으로 원시타입은 일반 클래스를 의미한다.

2. 지네릭스의 장점

(1) 타입의 안전성을 제공한다.

  • 의도하지 않은 타입의 객체가 저장되는 것을 막고, 가져올 때도 다른 타입으로 잘못 형변환되어 가져와지는 오류를 사전에 막을 수 있다.

(2) 타입체크와 형변환을 생략할 수 있으므로 코드가 간결해진다.

List stringOnlyList = new ArrayList(); //string만 들어가길 바라는 arrayList생성
            stringOnlyList.add(100); // 그러나 숫자 100이 추가되도 문제가 발생하지 않는다.
            String s1 = (String) stringOnlyList.get(0);
            System.out.println(s1); // 출력(실행)시 오류 발생 
            // class java.lang.Integer cannot be cast to class java.lang.String

이처럼 문자열만 들어가길 바라는 arrayList에 숫자가 들어가도 문제없이 추가된다.

또한 들어간 숫자가 문자열로 제대로 형변환이 안되면서 "class java.lang.Integer cannot be cast to class java.lang.String" 즉 형변환 오류가 발생했다.

마지막으로 이런 잘못된 코드를 컴파일러가 미리 잡아내지 못하고 실행전까지 알지 못한다는 것이다. (실행전까지 문제점을 알기 힘듦)

List<String> stringOnlyList = new ArrayList<String>(); //<String>지네릭스 추가
			// stringOnlyList.add(100); 처럼 작성에 오류 발생(string만 추가 가능)
            stringOnlyList.add("100"); 
            String s1 = stringOnlyList.get(0); // 형변환 생략 가능
            System.out.println(s1); // 100

지네릭스를 사용하면 타입의 안정성을 얻을 수 있으며, 형변환 절차를 생략할 수 있다. (어차피 string만 저장되기 때문에)

3. 지네릭스 제한

(1) 지네릭스는 애초에 인스턴스별로 다르게 동작하도록 만든 기능이다.
때문에 모든 객체에 대해 동일하게 동작해야 하는 static멤버에 지네릭스를 사용할 수 없다.

매개변수에 <T>와 같이 뭐가 들어갈지 모르는 값을 넣는건 안되지만, 아예 값을 지정해서<Apple>같이 선언하는 건 가능하다.

(2) 지네릭스 타입의 배열을 생성하는 것을 허용하지 않는다.

class G<T> {
T[] itemArr; // T타입의 배열을 위한 참조변수는 가능
	...
T[] toArray() {
	T[] tmpArr = new T[itemArr.length]; // 지네릭스 배열 생성 불가능
    ...
    return tmpArr;
   }
    ...
}   

지네릭스 배열을 생성할 수 없는 이유는 new연산자 때문이다.

이 연산자는 컴파일 시점에 타입 T가 뭔지 정확히 알아야 한다.

그러나 클래스 G가 컴파일하는 시점에서는 T가 어떤 타입이 될지 전혀 알 수 없다.

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

꼭 지네릭스 배열을 생성해야할 필요가 있을 때는 new연산자 대신 Reflection API의 newInstance()와 같이 동적으로 객체를 생성하는 메서드로 배열을 생성하거나, Object 배열을 생성해서 복사한 다음에 T[]로 형변환하는 방법 등을 사용한다.

4. 지네릭스 다형성

지네릭 클래스도 다형성을 이용할 수 있다.
Box클래스의 자손으로 FruitBox가 있다면

Box<Apple> appleBox = new Box<Grape>(); // Error: 대입된 지네릭스 타입이 일치해야 한다
Box<Fruit> appleBox = new Box<Apple>(); // Error: 대입된 지네릭스 타입이 일치해야 한다 (Apple이 Fruit의 자손이라는 가정)
Box<Apple> appleBox = new FruitBox<Apple>(); //OK. 다형성

Box<T>객체를 생성할 때 참조변수와 생성자에 대입된 타입(매개변수화된 타입)이 일치해야 한다.

Apple이 Fruit의 자손이라고 해도 지네릭스의 타입을 일치해줘야 한다.

단 Box클래스의 자손인 FruitBox를 선언하고 대입된 타입이 같다면 가능하다.

즉 상속관계에 있는 클래스를 다형성을 이용하여 선언할 때, 지네릭스 타입이 일치해야 한다.

단 문제점이 될 수 있는 것은

FruitBox<Toy> toyBox = new ToyBox<Toy>();

타입 문자에 들어갈 수 있는 타입은 과일이 아니더라도 들어갈 수 있다는 것이다.

5. 지네릭스 타입 제한

타입 문자에 들어갈 수 있는 타입을 제한하는 방법으로는 2가지가 있다.

(1) 지네릭스 타입에 과일만 들어가게 하기 위해서는 지네릭 타입에 'extends'를 사용하여 특정 타입의 자손들만 들어가도록 제한할 수 있다.

class Box<T> {
	ArrayList<T> list = new ArrayList<T>();
	
	void add(T item) { // 대입된 타입 T의 자손타입도 들어올 수 있다
		list.add(item);
	}
	...
}

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

class Apple extends Fruit { ...}
class Grape extends Fruit { ...}
...
FruitBox<Apple> appleBox = new FruitBox<Apple>(); // OK. Apple은 Fruit의 자손
FruitBox<Toy> toyBox = new ToyBox<Toy>(); // Error: Toy는 Fruit의 자손이 아님

appleBox.add(new Apple()); // OK.
appleBox.add(new Grape()); // Error: Grape는 Apple의 자손이 아님

만약 Fruit클래스가 아니라 인터페이스를 구현해야 한다는 제약이 필요하다면, 이때도 'extends'를 사용한다. (implements를 사용하지 않는다)

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

만약 제약 조건이 여러개라면 '&' 를 사용한다.

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

(2) 와일드 카드<?>
과일박스를 대입하면 주스를 만들어 반환하는 Juicer라는 클래스가 있다고 가정한다.

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

위에 언급했듯이 static메서드에 타입변수를 지정할 때는 특정 타입을 지정해줘야 한다.

FruitBox<Fruit> fruitBox = new FruitBox<Fruit>();
FruitBox<Apple> appleBox = new FruitBox<Apple>();
		...
System.out.println(Juicer.makeJuice(fruitBox); // OK. FruitBox<Fruit>
System.out.println(Juicer.makeJuice(appleBox); // Error. FruitBox<Apple>를 출력하기 위해서 오버로딩을 통해 static메서드를 만든다면

FruitBox타입만 출력할 수 있기 때문에 FruitBox도 출력하기 위해서 오버로딩을 통해 static메서드를 만든다면 컴파일 에러가 발생한다.

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

지네릭스 타입만 Fruit에서 Apple로 변경되었다고 해서 오버로딩이 성립하지 않기 때문에, 같은 이름의 메서드가 중복되었다는 에러가 나타나게 된다.

이를 극복하기 위해서 나온 것이 '와일드 카드'이다.

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

class Juicer {
	static Juice makeJuice(FruitBox<? extends Fruit> box) {
		String tmp = "";
		for(Fruit f : box.getList()) tmp += f + " ";
		return new Juice(tmp);
	}
}
으로 선언하게 되면 Fruit와 그 자손들까지 가능하게 된다.
profile
쉽게 쉽게

0개의 댓글