클래스나 메소드에서 사용할 내부 데이터 타입을 컴파일 시에 미리 지정하는 방법이다
일반화를 뜻하는 generalize라는 단어에서 유래한 것으로 다양한 타입을 사용하는 클래스나 메소드에서 일반화되어 코드를 작성하고 사용할 때 데이터 타입을 지정하는 방식
왜 필요한가?
JDK 1.5 이전에는 여러 타입을 사용하는 클래스나 메소드에서는 Object 타입을 사용했다
근데 이러면 Object 객체를 다시 원하는 타입으로 변환해야 하고 이 과정에서 오류가 발생할 가능성도 있다.
제네릭을 사용하면 컴파일 시에 미리 타입이 정해서, 타입 검사나 타입 변환과 같은 번거로운 작업을 생략할 수 있게 된다
장점:
제네릭 사용법
class MyArray<T> { // T는 타입변수이며 다른 이름을 사용해도 된다, 여러개도 가능
T element;
void setElement(T element) { this.element = element; }
T getElement() { return element; }
}
MyArray<Integer> myArr = new MyArray<Integer>(); // 다음과 같이 생성
MyArray<Integer> myArr = new MyArray<>(); // Java SE 7부터 가능함.
+) 타입변수 자리에 실제 타입을 명시할 때는 기본 타입이 아닌 Wrapper 클래스를 명시한다
import java.util.*;
class LandAnimal { public void crying() { System.out.println("육지동물"); } }
class Cat extends LandAnimal { public void crying() { System.out.println("냐옹냐옹"); } }
class Dog 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>();
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 Generic01 {
public static void main(String[] args) {
AnimalList<LandAnimal> landAnimal = new AnimalList<>(); // Java SE 7부터 생략가능함.
landAnimal.add(new LandAnimal());
landAnimal.add(new Cat());
landAnimal.add(new Dog());
// landAnimal.add(new Sparrow()); // 오류가 발생함.
for (int i = 0; i < landAnimal.size() ; i++) {
landAnimal.get(i).crying();
}
}
}
위의 예제에서 Cat과 Dog 클래스는 LandAnimal 클래스를 상속받는 자식 클래스이므로, 부모 클래스로 타입변환이 되기 때문에 AnimalList<LandAnimal>
에 추가할 수 있다.
제네릭의 제거 시기
제네릭은 컴파일 시 컴파일러에 의해 코드에 선언된 타입으로 변환되어 class 파일에는 제네릭 타입이 아닌 선언한 타입의 클래스나 메소드만 포함된다
제네릭이 없는 코드와의 호환성을 위해서 이를 제거한다
즉 사용자의 편의를 위한 기능이지 컴퓨터는 제네릭 사용 여부에 관계없이 똑같이 작동함
타입 변수의 제한
타입 변수에 extends 키워드를 사용하여 특정 타입만을 사용하도록 제한할 수 있다
class AnimalList<T extends LandAnimal> { ... }
interface WarmBlood { ... }
class AnimalList<T extends WarmBlood> { ... }
class AnimalList<T extends LandAnimal & WarmBlood> { ... }
제네릭 메소드
메소드의 선언부에 타입 변수를 사용한 메소드
앞서서는 클래스에서 제네릭을 선언하는 것을 했고 지금은 메소드 단에서 선언하는 것
public static <T> void sort( ... ) { ... }
// 타입변수는 메소드 선언부에서 반환타입 앞에 위치함
와일드 카드
주로 메소드의 매개변수의 다양한 타입을 지정할 때 사용
<?> // 타입 변수에 모든 타입을 사용할 수 있음.
<? extends T> // T 타입과 T 타입을 상속받는 자손 클래스 타입만을 사용할 수 있음.
<? super T> // T 타입과 T 타입이 상속받은 조상 클래스 타입만을 사용할 수 있음.
그러면 와일드 카드랑 제네릭이랑은 뭐가 다른거지?
제네릭:
- 클래스나 메소드에 주로 사용된다
- 다양한 타입을 지정한 T를 사용하여 해당 타입을 반복적으로 참조할 때 사용한
와일드 카드:- 주로 메소드의 매개변수 타입으로 사용됩니다.
- 정확한 타입 정보가 필요하지 않거나, 메서드가 여러 다양한 타입의 인자로 호출될 수 있게 하기 위해 사용하는 용도이다
결국 모든 타입을 받기는 하지만 결국에 사용자가 지정한 한가지 타입을 반복적으로 사용할 때는 제네릭을 써야하고 단순히 다양한 타입의 매개변수를 받을 수 있는 것을 지정할때는 와일드 카드를 쓰면 된다
import java.util.*;
class LandAnimal { public void crying() { System.out.println("육지동물"); } }
class LandAnimal { public void crying() { System.out.println("육지동물"); } }
class Cat extends LandAnimal { public void crying() { System.out.println("냐옹냐옹"); } }
class Dog 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);
}
}