// 제네릭을 사용하지 않은 경우
ArrayList list = new ArrayList();
list.add("test");
String temp = (String) list.get(0); // 타입 변환이 필요함
// 제네릭을 사용한 경우
ArrayList<String> list2 = new ArrayList();
list2.add("test");
temp = list2.get(0); // 타입 변환이 필요 없음
ArrayList<Integer> list = new ArrayList<Integer>();
위 예시처럼 <> 괄호 안에 들어가는 타입을 지정해준다.
public class 클래스명<T>{
...
}
public interface 인터페이스명<T> {
...
}
class Person <T> {
public T info;
Person<String> p1 = new Person<String>();
Person<StringBuilder> p2 = new Person<StringBuilder>();
}
class ExGeneric<T> {
private T t;
public void setT(T t){
this.t = t;
}
public T getT(){
return t;
}
}
위 예시처럼 클래스를 설계할 때 구체적인 타입을 명시하지 않고 타입 파라미터로 넣어두었다가 실제 설계한 클래스가 사용되어질 때
ExGeneric<String> exGeneric = new ExGeneric<>();
이런식으로 구체적인 타입을 지정하여 사용하면 타입 변환을 최소화할 수 있다.
인터페이스도 클래스처럼 제네릭으로 설정해두고 사용할 수 있다.
interface InterfaceExGeneric<T>{
T example();
}
class ExGeneric implements InterfaceExGeneric<String> {
@Override
public String example(){
return null;
}
}
class ExMultiTypeGeneric<K, V> implements Map.Entry<K,V> {
private K key;
private V value;
@Override
public K getKey(){
return this.key;
}
@Override
public V getValue(){
return this.value;
}
@Override
public V setValue(V value){
this.value = value;
}
}
class People<T,M>{
private T name;
private M age;
People(T name, M age){
this.name = name;
this.age = age;
}
public T getName(){
return name;
}
public void setName(T name){
this.name = name;
}
public M getAge(){
return age;
}
public void setAge(M age){
this.age = age;
}
// 제네릭 메서드
public static<T, V> boolean compare(People<T,V> p1, People<T,V> p2){
boolean nameCompare = p1.getName().equals(p2.getName());
boolean ageCompare = p1.getAge().equals(p2.getAge());
return nameCompare && ageCompare;
}
}
public class ExGeneric {
public static void main(String []args){
// 타입 파라미터 지정
People<String, Integer> p1 = new People<String, Integer>("Jack",20);
// 타입 파라미터 추정
People<String, Integer> p2 = new People("Steve", 30);
// 제네릭 메서드 호출
boolean result = p1.compare(p1,p2);
System.out.println(result);
}
}
제네릭 메서드를 정의할 때 리턴타입이 무엇인지 상관없이 내가 제네릭 메서드라는 것을 컴파일러에게 알려줘야 한다. 그러기 위해 리턴타입을 정의하기 전에 제네릭 타입에 대한 정의를 반드시 명시해야 한다.
제네릭 클래스가 아닌 일반 클래스 내부에도 제네릭 메서드를 정의할 수 있다.
즉, 클래스에 지정된 타입 파라미터와 제네렉 메서드에 정의된 타입 파라미터는 상관이 없다는 것!
자바 코드에서 선언되고 사용된 제네릭 타입은 컴파일 시 컴파일러에 의해 자동으로 검사되어 타입 변환하게 된다.
그리고서 코드 내에 모든 제네릭 타입은 제거되며 컴파일된 class 파일에는 어떠한 제네릭 타입도 포함되지 않게 된다.
이런식으로 동작하는 이유는 제네릭을 사용하지 않는 코드와의 호환성을 유지하기 위해서이다.
class AnimalList<T extends LandAnimal>{
...
}
위 같이 클래스의 타입 변수에 제한을 걸어 놓으면 클래스 내부에 사용된 모든 타입 변수에 제한이 걸린다.
이땐 인터페이스를 구현할 때에도 extends 키워드를 사용해야 한다.
클래스와 인터페이스 동시에 상속받고 구현해야 한다면?
class AnimalList<T extends LandAnimal & WarmBlood> { ... }
<?> // 타입 변수에 모든 타입을 사용할 수 있다.
<? extends T> // T 타입과 T 타입을 상속받는 자손 클래스 타입만 사용할 수 있다.
<? super T> // T 타입과 T 타입이 상속받은 조상 클래스 타임만이 사욯알 수 있다.
와일드카드 사용 예시
class LandAnimal { public void crying() { System.out.println("육지동물"); } }
class Cat extends LandAnimal { public void crying() { System.out.println("냐옹냐옹"); } }
class Sparrow { public void crying() { System.out.println("짹짹"); } }
class AnimalList<T> {
ArrayList<T> al = new ArrayList<T>();
public static void cryingAnimalList(AnimalList<? extends LandAnimal> al) {
LandAnimal la = al.get(0);
la.crying();
}
void add(T animal) { al.add(animal); }
T get(int index) { return al.get(index); }
boolean remove(T animal) { return al.remove(animal); }
int size() { return al.size(); }
}
public class Generic03 {
public static void main(String[] args) {
AnimalList<Cat> catList = new AnimalList<Cat>();
catList.add(new Cat());
AnimalList<Dog> dogList = new AnimalList<Dog>();
dogList.add(new Dog());
AnimalList.cryingAnimalList(catList);
AnimalList.cryingAnimalList(dogList);
}
}
// 결과
냐옹냐옹
멍멍