지네릭스는 다양한 타입의 객체들을 다루는 메서드나 컬렉션 클래스에 컴파일 시의 타입을 체크(compile-time type check)를 해주는 기능이다.
다룰 객체의 타입을 미리 명시해줌으로써 번거러운 형변환을 줄여준다.
지네릭 타입은 클래스와 메서드에 선언할 수 있다.
아래와 같은 클래스 Box가 정의되어 있을 때
class Box{
Object item;
void setItem(Object item){this.item = item;}
Object getItem(){return item;}
}
위의 클래스를 지네릭스 클래스로 변경할 때
class Box<T>{ // 지네릭 타입 T를 선언한다.
T item
void setItem(T item){this.item = item;}
T getItem(){return item;}
}
Box<T>
에서 'T'를 타입 변수라고 한다.(Type의 첫 글자를 따온 것)객체를 생성할 때는 참조변수와 생성자 타입에 T대신 사용될 실제 타입을 지정해 주어야 한다.
Box<String> b = new Box<String>(); //타입 T대신 실제 타입을 지정한다.
//b.setItem(new Obeject()); String 타입이 아니므로 에러발생
b.setItem("ABC") //String 타입이므로 에러가 발생하지 않는다.
String item = b.getItem(); //형변환 없이 가져올 수 있다.
이렇게 위처럼 T대신에 String 타입을 지정해주었기 때문에, 지네릭 클래스 Box는 아래와 같이 정의된 것이다.
class Box<String>{ // 지네릭 타입 T를 선언한다.
String item
void setItem(String item){this.item = item;}
String getItem(){return item;}
}
지네릭 클래스여도 이전의 방식으로 객체를 생성하는 것이 허용되지만, 지네릭 타입을 지정하지 않아서 안전하지 않다는 경고가 발생한다.
Box<T> : 지네릭 클래스. 'T의 Box' 또는 'T Box'라고 읽는다.
T : 타입 변수 또는 타입 매개변수.(T는 타입 문자)
Box : 원시 타입(raw type)
T[] itemArr; //T타입의 배열을 위한 참조변수는 가능
T[] tmpArr = new T[5]; //에러 발생 지네릭 배열 생성불가.
지네릭 배열을 생성할 수 없는 이유는 new연산자 때문인데, 이 연산자는 컴파일 시점에 타입 T가 뭔지 정확히 알아야한다.
참조변수와 생성자에 대입된 타입(매개변수화된 타입)이 일치해야 한다.
Box<Apple> apple = new Box<Apple>();
Box<Apple> apple = new Box<>(); //생략가능.
두 지네릭스 타입이 상속관계에 있더라도 참조변수와 생성자에 대입된 타입이 다르면 안된다.
- 하지만 메서드의 경우 상속의 관계는 추가가 가능하다.
class Box<T>{ ArrayList<T> list = new ArrayList<T>(); void add(T item); }
//Apple의 조상이 Fruit인 경우
Box<Fruit> fruitBox = new Box<Fruit>();
fruitBox.add(new Fruit());
fruitBox.add(new Apple()); //이것도 가능하다.
📌 위의 코드로 확인한 것 처럼 자손들은 매개변수가 될 수 있다.
## 제한된 지네릭 클래스
타입 문자로 사용할 타입을 '명시'하여 한 종류의 타입만 저장할 수 있도록 제한할 수 있지만, 여전히 모든 타입을 명시하여 저장할 수 있다.
**그렇다면, 타입 매개변수 T에 지정할 수 있는 타입의 종류를 제한할 수 있는 방법을 알아보자.**
- 지네릭 타입에 'extends'를 사용하면, 특정 타입의 자손들만 대입할 수 있게 제한할 수 있다.
~~~java
class FruitBox<T extends Fruit>{//Fruit타입의 자손들만 T타입으로 지정 가능하다.
....
}
위의 코드를 보면 여전히 한 종류의 타입만 담을 수 있지만, Fruit타입의 자손들만 T타입으로 담을 수 있다는 제한이 더 추가되었다.
클래스가 아니라 인터페이스를 구현해야하는 제약이 필요하다면, 이때도 'extends'를 사용해야한다.'implements'를 사용하지 않는 점에 주의하자.
class FruitBox<T extends Fruit & 인터페이스>{...}
와일드 카드는 '?'로 표현하며, 와일드 카드는 어떠한 타입도 될 수 있다.
예를 들어 static메서드를 정의할 경우 타입 매개변수 T를 매개변수에 사용할 수 없으므로, 타입 매개변수 대신에 특정 타입을 지정해주어야한다.
static Juice makeJuice(FruitBox<Fruit> box){//T가 아닌 Fruit로 특정 타입을 지정해주었다.
...
}
이렇게 매개변수에 타입 매개변수 T가 아닌 Fruit로 특정 타입을 지정해주었다.
그렇다면 Fruit타입이 아닌 다른 타입을 넣어서 makeJuice메서드를 사용하고 싶을 경우에는 지네릭 타입을 바꿔서 작성해야할까?
답은 아니다. 앞에서 말한 것과 같이 오버로딩이 아닌 메서드 중복 정의가 일어나 컴파일 에러가 발생한다.
✔️ 이러한 문제점을 해결하기 위해 와일드 카드를 사용하는 것이다.
'?'만으로는 Object타입과 다를게 없으므로, 다음과 같이 'extends'와 'super'로 상한(upper bound)과 하한(lower bound)을 제한할 수 있다.
<<? extends T> : 와일드 카드의 상한 제한. T와 그 자손들만 가능하다.
<? super T> : 와일드 카드의 하한 제한. T와 그 조상들만 가능
<?> : 제한없음. 모든 타입이 가능. <? extends Object>와 동일
메서드의 선언부에 지네릭 타입이 선언된 메서드를 지네릭 메서드라고 한다.
static <T> void sort(...)
그리고 지네릭 클래스에 정의된 타입 매개변수와 지네릭 메서드에 정의된 타입 매개변수는 전혀 다른 별개의 것이다.
class FruitBox<T>{
...
static <T> void sort(...)
}
위의 코드에서 클래스에 선언된 타입 매개변수 T와 지네릭 메서드 sort에 선언된 타입 매개변수 T는 타입 문자만 같을 뿐 서로 다른 것이다.
지네릭 메서드로 한 번 바꿔보자
static Juice makeJuice(FruitBox<? extends Fruit> box){
String tmp = "";
for(Fruit f : box.getList()) tmp += f + " ";
return new Juice(tmp);
}
static <T extends Fruit> Juice makeJuice(FruitBox<T> box){
String tmp = "";
for(Fruit f : box.getList()) tmp += f + " ";
return new Juice(tmp);
}
이러한 지네릭 메서드를 호출할 때에는 타입 변수에 타입을 대입해야한다.
FruitBox<Fruit> fruitBox = new FruitBox<Fruit>
//메서드호출
Juicer.<Fruit>makeJuice(fruitBox);
지네릭타입과 넌지네릭 타입간의 형변환은 항상 가능하다.
(다만 경고가 발생할 뿐이다.)
Box box = null;
Box<Object> objBox = null;
box = (Box) objBox; //경고발생하지만 OK
objBox = (Box<Object>) box; //경고발생하지만 OK
✔️ 그렇다면 대입된 타입이 다른 지네릭 타입간의 형변환은 가능할까?
Box<Object> objBox = null;
Box<String> strBox = null;
objbox = (Box<Object>) strBox; //에러발생
strBox = (Box<String>) objbox; //에러발생
위 코드로 확인해본것 처럼 불가능하다.
Box<Object> objBox = new Box<String>(); //에러
//(Box<Object>) new Box<String>();을 생략한것 기억하자.
앞서 배운것 처럼 위의 코드가 안된다는 것은 형변환이 불가능하다는 것이다.
✔️ 그럼 Box<String>
이 Box<? extends Object>
로 형변환이 될까?
Box<? extends Object> wobjBox = new Box<String>(); //가능
형변환이 가능하다. 이는 메서드의 매개변수에 다형성이 적용될 수 있었던 이유와 같다.
static Juice makeJuice(FruitBox<? extends Fruit> box){
String tmp = "";
for(Fruit f : box.getList()) tmp += f + " ";
return new Juice(tmp);
}
이러한 메서드에 우리는 FruitBox<Apple>,FruitBox<Grape>,FruitBox<Fruit>
등을 매개변수로 사용할 수 있었다.
Optional<?> wopt = new Optional<Object>();
Optional<Object> oopt = new Optional<Object>();
Optional<Stinrg> sopt = (Optional<String>) wopt; //1.가능
Optional<String> sopt1 = (Optional<String>) oopt; //2.에러발생, 형변환 불가
Optional<?>는 Optional<? extends Object>와 같다고 배웠다.
그래서 1은 형변환이 가능한 것이고, 2는 앞서 확인해본바와 같이 형변환이 불가능한 것이다.
결론적으로, 와일드 카드가 포함된 것을 사용하면 형변환이 가능하다.
컴파일러는 지네릭 타입을 이용해서 소스파일을 체크하고, 필요한 곳에 형변환을 넣어준다. 그리고 지네릭 타입을 제거한다.
자바의 정석 3rd Edition