아래의 예시로 알아보면
class Basket {
private String item;
Basket(String item) {
this.item = item;
}
public String getItem() {
return item;
}
public void setItem(String item) {
this.item = item;
}
}
Basket 클래스는 오로지 String 타입의 데이터만을 저장할 수 있는 인스턴스를 만들 수 있다. 그에 따라, 다양한 타입의 데이터를 저장할 수 있는 객체를 만들고자 할 때 제네릭을 사용할 수 있다.
class Basket<T> {
private T item;
public Basket(T item) {
this.item = item;
}
public T getItem() {
return item;
}
public void setItem(T item) {
this.item = item;
}
}
위의 코드처럼 클래스 이름뒤에 <변수명> 의 형태로 정의가 되고 이때 예시 코드의 T에 해당하는 타입명을 인스턴스를 생성할 때에 정의를 해주어야 한다.
아래의 예제를 통해 정의 예시를 확인해보자.
Basket<String> basket1 = new Basket<String>("기타줄");
위 코드처럼 T에 String 타입으로 정의를 해 basket1은 String 타입의 데이터를 갖는 인스턴스이다.
제네릭이 사용된 클래스를 제네릭 클래스라고 한다. 앞에서 봤던 Basket 클래스가 바로 제네릭 클래스이다.
class Basket<T> {
private T item;
public Basket(T item) {
this.item = item;
}
public T getItem() {
return item;
}
public void setItem(T item) {
this.item = item;
}
}
위 코드에서 T는 타입 매개변수라 한다.
만약, 타입 매개변수를 여러 개 사용해야 한다면, 아래와 같이 선언하면 된다.
class Basket<K, V> { ... }
제네릭 클래스에서 타입 매개변수를 임의의 타입으로 사용할 수 있다고 하였다. 이때 아래와 같이 클래스 변수에는 타입 매개변수를 사용할 수 없다.
class Basket<T> {
private T item1; // O
static T item2; // X
}
클래스 변수에 타입 매개변수를 사용할 수 있다면, Basket<String>
으로 만든 인스턴스와, Basket<Integer>
로 만든 인스턴스가 공유하는 클래스 변수의 타입이 서로 달라지게 되어, 클래스 변수를 통해 같은 변수를 공유하는 것이 아니게 된다. 따라서, static이 붙은 변수 또는 메서드에는 타입 매개변수를 사용할 수 없다.
class Flower { ... }
class Rose extends Flower { ... }
class RosePasta { ... }
class Basket<T> {
private T item;
public T getItem() {
return item;
}
public void setItem(T item) {
this.item = item;
}
}
class Main {
public static void main(String[] args) {
Basket<Flower> flowerBasket = new Basket<>();
flowerBasket.setItem(new Rose()); // 다형성 적용
flowerBasket.setItem(new RosePasta()); // 에러
}
}
flowerBasket은 Flower 타입의 데이터를 저장할 수 있고, new Rose() 는 Flower을 상속받아 flower.Basket.setItem() 메서드에 Rose 타입으로 정의가 될 수 있다.
반면, RosePasta 타입은 Flower와 아무런 관계가 없기에, flowerBasket 인스턴스에서 정의 될 수 없다.
앞서 살펴본 예제의 Basket 클래스는 인스턴스화 할 때 어떠한 타입도 지정해줄 수 있다. 즉, 타입을 지정하는 데에 있어 제한이 없다.
class Flower { ... }
class Rose extends Flower { ... }
class RosePasta { ... }
class Basket<T extends Flower> {
private T item;
...
}
class Main {
public static void main(String[] args) {
// 인스턴스화
Basket<Rose> roseBasket = new Basket<>();
Basket<RosePasta> rosePastaBasket = new Basket<>(); // 에러
}
}
위 코드를 통해 제한된 제네릭 클래스를 알아보자
Basket 클래스의 타입 매개변수 T는 Flower 을 상속받게 된다. 즉, Flower 또는 Flower 을 상속하는 type만 정의가 될 수 있다. 따라서, T는 Flower 또는 Rose 로 정의가 될 수 있다.
제한된 클래스는 특정 클래스를 상속받은 클래스만 타입으로 제한하는 것 뿐만 아니라, 인터페이스를 구현한 클래스만 타입으로 지정할 수 있도록 제한할 수 있다.
아래의 예제를 통해 알아보면
interface Plant { ... }
class Flower implements Plant { ... }
class Rose extends Flower implements Plant { ... }
class Basket<T extends Flower & Plant> { // (1)
private T item;
...
}
class Main {
public static void main(String[] args) {
// 인스턴스화
Basket<Flower> flowerBasket = new Basket<>();
Basket<Rose> roseBasket = new Basket<>();
}
}
인터페이스 Plant 의 구현된 Flower와 Flower 의 상속 Plant의 구현클래스인 Rose 로 Basket이 정의가 될 수 있다.
이때 Flower 을 상속받으면서 동시에 Plant 을 구현한 범위 내에서 정의가 되려면 & 를 사용한다.
클래스 전체를 제네릭으로 선언할 수 있지만, 클래스 내부의 특정 메서드만 제네릭으로 선언할 수 있다.
제네릭 메서드의 예시는
class Basket {
...
public <T> void add(T element) {
...
}
}
처럼 정의가 될 수 있다.
제네릭 메서드의 타입 매개변수는 제네릭 클래스의 타입 매개변수와 별개이다. 아래의 예시를 통해 보면
class Basket {
...
public <T> void add(T element) {
...
}
}
Basket 클래스의 T와 add 메서드의 T는 별개의 것으로 간주된다.
이는 타입이 지정되는 시점이 서로 다르기 때문이다.
클래스명 옆에 선언한 타입 매개변수는 클래스가 인스턴스화될 때 타입이 지정되고 , 제네릭 메서드의 타입 지정은 메서드가 호출될 때 이루어진다.
Basket<String> basket = new Bakset<>(); // 위 예제의 1의 T가 String으로 지정됩니다.
basket.<Integer>add(10); // 위 예제의 2의 T가 Integer로 지정됩니다.
basket.add(10); // 타입 지정을 생략할 수도 있습니다.
제네릭 메서드는 클래스 타입 매개변수와 달리 static 메서드에서도 선언하여 사용할 수 있다.
class Basket {
...
static <T> int setPrice(T element) {
...
}
}
제네릭 메서드를 정의하는 시점에ㅓ 실제 어떤 타입이 입력되는 지 알 수 없기에, 제네릭 메서드를 정의하는 시점에서 length() 을 사용할 수 없다.
class Basket {
public <T> void print(T item) {
System.out.println(item.length()); // 불가
}
}
하지만 Object 클래스의 메서드는 사용이 가능하다. 모든 클래스는 Object를 상속받기 때문이다.
class Basket {
public <T> void getPrint(T item) {
System.out.println(item.equals("Kim coding")); // 가능
}
}
자바의 제네릭에서 와일드카드는 어떠한 타입으로든 대체될 수 있는 타입 파라미터를 의미하며, 기호 ? 로 와일드카드를 사용할 수 있다.
일반적으로 와일드카드는 extends와 super 키워드를 조합하여 사용한다.
<? extends T>
<? super T>
<? extends T>
는 와일드카드에 상한 제한을 두는 것으로서, T와 T 를 상속받는 하위 클래스 타입만 타입 파라미터로 받을 수 있도록 지정한다.<? super T>
는 와일드카드에 하한 제한을 두는 것으로, T와 T의 상위 클래스만 타입 파라미터로 받도록 한다.