제네릭은 클래스, 메서드의 코드를 작성할 때 타입을 지정하는 것이 아닌 나중에 지정할 수 있도록 일반화 해두는 것을 의미한다. 매개변수 타입이나, 리턴하는 값의 타입 정의를 외부로 미루게 되고 타입에 대해 유연성과 안전성을 확보하는 기술이다.
class Cart<T>{
private T food;
public Cart(T food){
this.food = food;
}
}
위에서 T를 타입 매개변수라고 하며 <T>와 같이 꺽쇠 안에 넣어 클래스 이름 옆에 작성해 줌으로서 클래스 내부에서 사용할 타입 매개변수를 선언가능하다. T는 임의의 문자로 어떤 문자를 사용해도 상관없다. 여러개 사용할 경우
클래스명 <T,K,V>와 같이 여러개 선언이 가능하다.
다만, 제네릭 클래스를 사용할때는 클래스 변수에는 타입 매개변수를 사용할 수 없다. 클래스 변수는 모든 인스턴스가 공유하는 변수이기 때문인데 클래스 선언후 여러 인스턴스에서 한 클래스 변수에 여러타입을 적용할 경우 클래스변수가 공통된 값을 공유할 수 없는 문제점이 발생하게 된다.
위의 제네릭 클래스를 인스턴스로 사용할 때에는 다음과 같이 선언해준다.
Cart<String> cart1 = new Cart<String>("chicken");
단, 타입 매개변수에 치환될 타입으로 기본 타입을 지정할 수는 없다. int,double와 같은 원시 타입을 지정해야하는 경우 Integer,Double 같은 래퍼클래스를 활용한다.
제네릭 클래스에 상속과 구현을 통해 다형성을 적용할 수 있다.
class Chicken{};
class FriedChicken extends Chicken{} //Chicken클래스를 상속받고 있다.
class Shose{}
class Cart<T>{
private T food;
public void setFood(T food) {
this.food = food;
}
}
public class main {
public static void main(String[] args) {
Cart<Chicken> chickenCart = new Cart<>();
chickenCart.setFood(new FriedChicken()); //Cart클래스의 Chicken타입이지만 하위클래스 FriedChicken타입을 써도 오류가 나지 않는다.
chickenCart.setFood(new Shose());// 상속받지 않기 때문에 Shose타입은 오류가 발생한다.
}
}
위와 같이 상속관계에서 타입을 적절히 지정해 사용할 수 있다.
위와 같은 제네릭 클래스에서는 타입을 지정하는데 제한이 없지만 타입 매개변수를 선언할 때 아래와 같이 코드를 작성하면 Cart 클래스를 인스턴스 할때 chicken클래스의 하위 클래스만 지정하도록 제한된다.
class Chicken implements Animal{};
class FriedChicken extends Chicken {...}
class Cart<T extends Chicken>{...}
또한 특정 인터페이스를 구현한 클래스만 타입으로 지정할 수 있도록 제한도 가능하다. 이 경우에도 동일하게 extends를 사용한다.
interface Animal{...};
class Chicken implements Animal{...};
class Cart<T extends Animal>{...};
만약 특정 클래스를 상속받으면서 동시에 특정 인터페이스를 구현한 클래스만 타입으로 지정할 수 있도록 제한하려면 아래와 같이 &을 사용하여 코드를 작성한다. 이 경우 클래스를 인터페이스보다 앞에 위치시켜야 한다.
interface Animal{};
class Chicken implements Animal{};
class FriedChicken extends Chicken implements Animal{...}
class Cart<T extends Chicken & Animal>{...}
제네릭은 클래스 내부 메서드에서도 사용이 가능하다. 제네릭 메서드의 타입 매개변수 선언은 반환 타입 앞에서 이루어지며, 해당 메서드내에서만 선언한 타입 매개변수를 사용 가능하다.
class Cart {
public <T> void add(T element){
}
}
위와 같이 제네릭 메서드를 선언가능하다. 또한 제네릭 메서드의 타입 매개변수는 제네릭 클래스의 타입 매개변수와 별개의 것이다.
class Cart<T>{
public T food;
public <T> T buy(T count){
return count;
}
}
public class main {
public static void main(String[] args) {
Cart<String> cart = new Cart<>();//String타입의 cart인스턴스 생성
cart.food = "치킨을 구매했습니다.";// 인스턴스변수에 String 할당
System.out.println(cart.food.getClass().getName()); //getClass().getName()메서드를 통해 해당 변수의 클래스를 확인할 수 있다.
System.out.println(cart.buy(3).getClass().getName());//메서드도 확인 가능하다.
}
}
//출력값
//java.lang.String
//java.lang.Integer
동일하게 타입매개변수로 T를 사용한다고 해도 서로 다른 타입매개변수로 간주한다. 또한 클래스 명 옆에 선언한 타입 매개변수는 클래스가 인스턴스화 될 때 타입이 지정되고, 제네릭 메서드의 타입 지정은 메서드가 호출될 때 이루어짐을 알 수 있다. 또한 메서드 타입 매개변수는 static 메서드도 선언하여 사용이 가능하다.
String 클래스의 메서드는 제네릭 메서드를 정의하는 시점에는 사용이 불가능한데 제네릭 메서드를 정의하는 시점에 어떤 타입이 입력되는지 알 수 없기 때문이다.
제네릭에서는 알 수 없는 타입을 나타낼 때 와일드 카드를 사용할 수 있다. 와일드 카드는 어떠한 타입으로든 대체될 수 있는 타입 파라미터를 의미하며 기호 ? 로 와일드 카드를 사용 가능하다.
일반적으로 와일드 카드는 extends와 super 키워드를 조합하여 사용한다.
<? extends T>
<? super T>
<? extends T> 는 와일드 카드에 상한 제한을 두는 것으로 T와 T를 상속받는 하위클래스 타입만 타입 파라미터로 받을 수 있도록 지정한다
반면 <? super T>는 와일드 카드에 하한 제한을 두는 것으로, T와 T의 상위 클래스만 타입 파라미터로 받도록 한다.
위의 키워드와 조합하지 않은 와일드카드<?>는 <? extends Object>와 같다.