Generic이란 클래스의 타입을 클래스 외부에서 설정해주는 것이다. 다양한 종류의 데이터를 다룰 때 데이터의 타입을 특정 타입으로 고정시켜 줄 수 있다.
List list = new ArrayList();
list.add("a");
list.add(1);
위의 코드는 제네릭을 사용하지 않고, 다양한 종류의 데이터를 다루고 있는 코드이다. "a"
와 1
앞에 (Object)
가 생략되어있다.
이럴 경우 List에서 값을 꺼낼 때, 사용하기 위해서는 다시 특정 타입으로 변경해줘야한다.
List list = new ArrayList();
list.add("a");
list.add(1);
String s = (String)list.get(0);
int i = (int)list.get(1);
이런 경우 제네릭을 사용한다. 특정 타입을 제한함으로 타입 안정성이 보장되며, 타입 체크와 형 변환을 생략할 수 있게 해주어 코드가 간결해진다.
List<String> list = new ArrayList<>();
list.add("a");
list.add(1); // 컴파일 에러 String으로 특정 타입을 정해줬기 때문에 int값을 넣을 수 없다.
위의 예시에서 본 것 처럼 Generic은 <> 안에 특정 타입을 설정해주는 식으로 사용한다.
그럼 위에서 사용한 List에서 Generic은 어떻게 선언되어있을까?
public interface List<E> extends Collection<E> {
//...
}
List 옆에 < > 표시안에 E 알파벳이 들어있다. 사실 이 알파벳은 다른 알파벳이어도 상관없다. 그냥 Element라는 의미로 적어준 것이고 알파벳에 따라 기능이 달라진다거나 하는 것은 없다.
따라서 일반적으로 클래스를 만들어 줄 때 아래와 같이 사용할 수 있다.
public class Person<T> {
private T name;
public Person(T name) {
this.name = name;
}
}
Person<String> person = new Person<>("js");
Generic에는 상한과 하한이라는 개념이 존재한다.
아래와 같은 코드가 존재한다고 하자
class Animal {
}
class Dog extends Animal {
}
class Cat extends Animal {
}
상한은 위의 경계를 정하는 것이라고 생각할 수 있다.
List<? extends Animal> animals -> Animal의 모든 자손부터 Animal 타입까지
@Test
void extendsTest(List<? extends Animal> animals) {
Animal animal = animals.get(0); // Animal타입으로 가져오는 이유는 상한 경계가 Animal이기 때문에 Dog이든 Cat이든 Animal로 받을 수 있기 때문
animals.add(new Dog()); // 컴파일 에러. <? extends Animal> 타입이 Dog이라는 보장이 없다. List<Cat>으로 넘어올 수도 있가 때문.
}
Animal을 상한으로 둔 경우는 Animal 타입으로 가져올 수는 있지만(조회), add와 같은 수정은 불가능하다.
-> 조회가능, 수정 불가
하한은 밑의 경계를 정하는 것이라고 할 수 있다.
List<? super Animal> animals -> Animal 타입부터 Object까지
@Test
void superTest(List<? super Animal> animals) {
Object object = animals.get(0); // Object타입으로 나오는 이유는 상한이 없기 때문에 어떤 타입이 넘어오든 최상위 조상인 Object의 자식임이 보장되기 때문
animals.add(new Dog()); // Dog을 넣을 수 있는 이유는 Dog가 Animal의 자식이기 때문에, Animal로 캐스팅 될 수 있으므로.
}
Animal을 하한으로 둔 경우는 Animal 타입으로 가져올 수는 없지만, 수정은 가능하다.
-> 조회 불가, 수정 가능
조회 용도로만 사용 -> 상한 사용
내부를 건드릴 거면 -> 하한 사용