목차
목표: 자바의 지네릭스에 대해 학습하기
1. 지네릭스(Generics)
지네릭스는 '다양한 객체들을 다루는' 메서드나 컬렉션 클래스에, 컴파일 시의 타입 체크를 해주는 기능으로, JDK1.5에서 처음 도입되었다.
지네릭스를 왜 만들었을까? 예를 들어 ArrayList와 같은 컬렉션의 경우, 사실 사용 시에는 한 종류의 객체를 담는 경우가 더 많은데, 이때 매번 타입체크와 형변환을 하면 불편하지만 체크를 하지 않으면 원하지 않는 타입이 들어올 수도 있기 때문에 곤란하다.
지네릭스를 쓰면 이 문제를 해결할 수 있다. 지네릭스를 사용하면...
- 코드에서 컬렉션이 담을 수 있는 타입을 지정함으로써 타입체크와 형변환을 생략할 수 있다. 컴파일 시에 지네릭스 타입이 원래 객체타입으로 변환된다.
- 그래서 원치않는 타입을 대입하는 시도를 차단함과 동시에 타입 안정성까지 챙길 수 있다.
2. 지네릭스 사용법
지네릭 클래스 선언은 다음과 같이 하면 된다.
class Box<T> { //지네릭 타입 T를 선언
T item;
void setItem(T item) { this.item = tiem; }
T getItme() { return item; }
}
- Box<T>를 '지네릭 클래스'라고 부른다.
- 여기서 Box는 '원시타입(raw type)'이라고 지칭한다.
- T는 '타입변수' 또는 '타입매개변수'라고 부르며, 임의의 참조형 타입을 의미한다. T를 Object 타입의 참조변수 대신 넣는다.
- T가 아닌 다른 문자를 사용해도 된다. 상황에 맞게 의미있는 문자를 사용하자.
- 타입 변수가 여러개인 경우 사용하는 법 : Map<K, V>
이렇게 ArrayList를 이용해서 여러객체를 저장할 수 있도록 해서 쓰는 경우가 대부분이다.
class Box<T> { //지네릭 타입 T를 선언
ArrayList<T> list = new ArrayList<T>();
void add(T item) {
list.add(item);
}
T get(int i) {
return list.get(i);
}
ArrayList<T> getList(){
return list;
}
int size() {
return list.size();
}
public String toString() {
return list.toString();
}
}
지네릭 클래스 객체를 생성 및 사용하는 예시는 다음과 같다. 되도록 타입을 지정해서 지네릭 클래스를 사용하자.
(1-1) Box<String> b = new Box<String>(); //타입 T 대신, 실제타입 지정
(1-2) Box<String> b = new Box<>(); // JDK 1.7부터 타입 생략 가능
(2-1) b.setItem(new String("UL"));
(2-2) b.setItem(new Object());// Error: Stirng이외 지정불가
(3) String item = (String) b.getItem();// 형변환 필요X
//지네릭을 지정하지 않고 객체 생성(지정하면 Warn 안뜬다)
(4) Box = new Box(); //OK. T는 Object로 간주됨
(4-1) b.setItem("ABC"); // Warn: unchecked or unsafe operation
(4-2) b.setItem(new Object()); //Warn: unchecked or unsafe operation
타입
- (1-1) Box<String> b = new Box<String>(); 과 같이, 타입 매개변수에 타입을 지정하는 것을 '지네릭 타입 호출'이라고 한다.
- (1-1) 여기서 'String'을 매개변수화된 타입(parameterized type), 대입된 타입 이라고 한다.
- (1-2) JDK 1.7 버전 부터 대입되는 타입을 생략 할 수 있게 되었다.
- (2) Box<T>의 객체를 생성할 때는 참조변수와 생성자에 대입된 타입이 일치해야한다.
앞서 타입 변수가 타입 매개변수라고도 불린다고 했다. 그 이유는 같은 클래스를 생성해도 다른 타입으로 생성할 수 있는 것이, 같은 메서드 호출 시 파라미터만 변경하는 것과 비슷하기 때문이다.
- (3) 이렇게 지네릭스를 쓰면 형변환을 할 필요가 없어진다.
- (4) 지네릭을 지정하지 않고도 객체를 생성할 수 있지만 되도록 지양하도록 한다.
그런데 지네릭스의 대입된 타입이 상속 관계라면 대입이 가능한지 의문이 들 수 있다. 다음 예제를 보자.
(5-1) Box<Apple> appleBox = new Box<Grape>(); // Error: 참조변수와 생성자에 대입된 타입이 일치해야 한다
(5-2) Box<Fruit> appleBox = new Box<Apple>(); // Error: Apple은 Fruit의 자손
(5-3) Box<Apple> appleBox = new FruitBox<Apple>(); //OK. 다형성
상속
- (5-1) 참조변수가 Apple인데 생성자에 대입된 타입이 Grape일 경우 불일치하므로 에러가 발생한다.
- (5-2) 이것은 참조변수의 타입이 상속관계에 있어도 마찬가지로 에러가 발생한다.
- (5-3) 단, 두 지네릭 클래스의 타입이 다른데 상속관계일 때 대입된 타입이 같은 것은 괜찮다.
(6) Box<Apple> appleBox = new Box<>();
(6-1) appleBox.add(new Apple()); // OK
(6-2) appleBox.add(new Grape()); // Error: Apple만 추가 가능
(7) Box<Fruit> fruitBox = new Box<Fruit>(); // Apple은 Fruit의 자손
(7-1) fruitBox.add(new Fruit()); //OK
(7-2) fruitBox.add(new Apple()); //OK. void add(Fruit item)
- (6, 7) 대입된 타입과 다른 타입의 객체는 추가할 수 없지만 참조변수(타입 T)의 자손들은 메서드의 매개변수가 될 수 있다. (다형성)
- (7-2) 그러나 Apple의 조상인 Fruit이 타입 T일 경우 Fruit의 자손들은 매개변수가 될 수 있다.
타입 문자로 사용할 타입을 명시하면 한 종류의 타입만 저장할 수 있도록 제한 할 수 있지만, 그래도 여전히 모든 종류의 타입을 지정할 수 있다는 건 변함이 없다. -> ? 타입 매개변수 T에 지정할 수 있는 타입의 종류를 제한하는 방법 은 다음과 같다.
extneds를 사용해 특정 타입의 자손들만 대입할수 있게 제한하기
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의 자손이 아님
- <T extends Fruit> 이렇게 작성하면 Fruit의 자손만 타입으로 지정 가능하다.
- 메서드의 매개변수의 타입 T도 그 자손 타입이 될 수 있는데 이 예제의 경우는 유의해서 보길 바란다.
- FruitBox는 Box를 상속받는데, Box의 add 메서드는 대입된 타입 T만 들어올 수 있지, Fruit의 자손은 들어올 수 없다.
- 이것을 인터페이스로 구현한다면 implements를 사용하지 않고 extends를 사용한다는 점에 주의하자.
- 특정 클래스의 자손이면서 인터페이스도 구현해야한다면
<T extends 조상클래스 & 인터페이스>
처럼 쓴다.
지네릭 사용 불가 사례
1. static 멤버에 사용 불가
static 멤버는 모든 객체에 대해 동일하게 동작해야한다. 대입된 타입의 종류와 관계없이 동일해야한다. 따라서 static멤버에는 타입 변수 T를 사용할 수 없다.
다음과 같이 사용해서는 안된다.
class Box<T> {
static item; //Error: 멤버변수에 static 사용 불가
static int compare(T t1, T t2) {...} //Error: 멤버변수에 static 사용 불가
...
}
2. new 연산자로 지네릭 타입의 배열 생성 불가
지네릭 배열 타입의 참조변수를 선언하는 것은 가능하지만,
new 연산자는 컴파일 시점에 타입 T가 뭔지 정확히 알아야 하므로 new를 사용해서 배열을 생성하는 것은 안된다.
컴파일 후에야 지네릭스가 제거되며 원시타입으로 바뀌기 때문이다.
class Box<T> {
T[] itemArr; //OK
T[] toArray() {
T[] tmpArr = new T[itemArr.lenght]; //Error
...
return tempArr;
}
}
대신 'Reflection API'의 newInstance() 처럼 동적으로 배열을 생성하거나, Object배열을 생성해서 복사한 다음 T[]로 형변환하는 방법을 사용할 수 있다.
* 지네릭 타입의 형변환은 생략하도록 하겠다. ㅠㅠ
3. 지네릭스 주요 개념 (바운디드 타입, 와일드 카드)
Juicer라는 일반 클래스에, 매개변수로 FruitBox<Fruit> 타입을 받는 static 메서드를 작성했다고 치자.
class Juicer {
static Juice makeJuice(FruitBox<Fruit> box) {
String tmp = "";
for(Fruit f : box.getList()) tmp += f + " ";
return new Juice(tmp);
}
}
Fruit 타입말고 Apple 같은 다른 타입도 받게 하고싶다. 그런데 앞서 static에는 지네릭을 사용할 수 없다고 했다.
그렇담 오버로딩을 하면 되지 않을까?
NoNo. 지네릭 타입이 다른 것만으로는 오버로딩이 성립하지 않는다.
이럴 때 사용하기 위해 고안된 것이 와일드 카드이다.
- 기호 ?로 표현하며, 어떠한 타입도 될 수 있음 (와일드 카드에는 & 사용불가)
- ?만으로는 Object 타입과 다를 게 없음
- <?> 제한 없음. 모든 타입 가능. <? extends Object>와 동일
- 다음과 같이 제한
- <? extends T> 상한 제한, T와 그 자손들만 가능
- <? super T> 하한 제한, T와 그 조상들만 가능
- ?만으로는 Object 타입과 다를 게 없음
<? extends T>
Apple이나 다른 클래스들이 Fruit을 상속 받게하고, 와일드 카드의 상한 제한을 적용하면 다음과 같이 사용할 수 있게된다.
class Juicer {
static Juice makeJuice(FruitBox<? extends Fruit> box) { //와일드카드 상한제한
String tmp = "";
for(Fruit f : box.getList()) tmp += f + " ";
return new Juice(tmp);
}
}
<?> 또는 <? extends Object>
매개변수 타입이 <?> 일 경우 for문에서 box에 저장된 요소를 Fruit 타입의 참조변수로 못받지만 (Fruit의 자손이라는 보장이 없으므로) 컴파일 에러는 나지 않는다. 다음과 같이 FruitBox의 요소들이 Fruit의 자손이라고 선언했기 때문이다.
class FruitBox<T extends Fruit> extends Box { //Fruit의 자손만 타입으로 지정가능
ArrayList<T> list = new ArrayList<T>();
}
<? super T>
와일드 카드의 하한제한 적용 예시로는 Collections의 sort 메서드를 들 수 있다.
이 메서드를 쓰려면 정렬대상인 list와, 정렬 방법이 정의된 Comarator를 인자로 전달해야한다.
static <T> void sort(List<T> list, Comparator<? suprer T> c)
Comparator는 인터페이스라 추상메서드 compare를 구현해야한다.
sort 메서드의 인자를 Comparator<T> 로 받는다면, 매번 Fruit의 자손이 생길 때마다 Comparator를 구현하는 다음 코드를 작성해야하는 문제가 생긴다. (코드 중복 + 반복됨)
class 자손Comp implements Comparator<자손> {
public int compare(Grape t1, Grape t2){
return t2.weight - t1.weight;
}
}
그래서 sort 메서드의 인자가 Comparator<? super T>로 정의되어 있는 것이다.
여기에 Apple이 들어오면 Comparator<? super Apple>이 된다. 즉 Apple과 그 조상이 가능하다는 의미다.
그럼 다음과 같이 하나의 클래스로 해결된다.
class FruitComp implements Comparator<Fruit>{
public int compare(Fruit t1, Fruit t2){
return t1.weight - t2.weight;
}
}
그래서 Comparator에는 항상 <? super T>가 항상 붙는다.
4. 지네릭스 메서드 만들기
자 이제 메서드에 적용해보자...! 메서드의 선언부에 지네릭 타입이 선언된 메서드를 지네릭 메서드라고 한다.
지네릭 메서드는 다음과 같이 사용할 수 있다.
5. Erasure
지네릭 타입의 제거
'Language > Java' 카테고리의 다른 글
JVM은 무엇이며 자바 코드는 어떻게 실행하는 것인가 (0) | 2022.09.26 |
---|---|
Java 입출력 스트림 (0) | 2022.01.10 |