제네릭(Generics)
제네릭은 컴파일 시 타입을 체크해 주는 기능이다.
JDK 1.5 이후로는 꼭 타입을 지정해줘야 하는데 이때 제네릭을 사용하여 객체의 타입 안정성을 높이고 형변환의 번거로움을 줄일 수 있다.
타입 변수
클래스를 작성할 때 Object 타입 대신 타입 변수(E)를 선언해서 사용한다.
객체를 생성할 때 타입 변수 대신 실제 타입을 지정한다.
실제 타입이 지정되면 형변환을 생략할 수 있다.
참조 변수와 생성자의 대입된 타입은 일치해야 한다.
JDK 1.7부터 생성자에 타입 변수를 지정하는 것을 생략할 수 있다.
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
ArrayList를 살펴보면 <E>라는 타입 변수를 선언해준 것을 확인할 수 있는데 이것을 통해 타입을 유동적으로 설정해 줄 수 있다.
ArrayList<Member> arrayList = new ArrayList<>(); // 생성자 타입 생략 가능
ArrayList<Member> arrayList2 = new ArrayList<Member>();
ArrayList에 Member라는 클래스를 타입으로 지정해놓고 사용한다면 위와 같은 코드를 작성할 수 있다. 이때 타입이 일치해야 되는데 생성자의 타입을 생략할 수 있다.
제네릭 클래스의 다형성
class Product {}
class Tv extends Product {}
class Audio extends Product {}
public class Ex_2 {
public static void main(String[] args) {
ArrayList<Product> list = new ArrayList<Product>(); // 타입 변수가 일치함
ArrayList<Product> list1 = new ArrayList<Tv>(); // 상속 관계여도 타입은 일치해야 한다.
List<Product> list2 = new ArrayList<Product>(); // 같은 제네릭 클래스는 다형성이 가능하다.
}
제네릭 클래스는 대입되는 타입이 상속관계여도 일치하지 않으면 에러가 발생한다.
위의 코드 예시와 같이 타입 변수가 Product 클래스라면 같은 Product 타입의 클래스가 되야 한다. 그런 반면에 Tv 클래스는 Product 클래스를 상속하고 있지만 제네릭 클래스의 다형성에서는 성립할 수 없기 때문에 같은 Tv 클래스를 타입으로 지정해줘야 한다.
단, 제네릭 클래스간의 다형성은 성립하기 때문에 List에 ArrayList를 대입할 수 있다. (타입 변수는 일치해야 함)
list.add(new Tv());
list.add(new Audio()); // 매개변수의 다형성은 가능하다. 해당 메서드의 타입 변수가 Product로 바뀌기 때문
위의 코드로 참조 변수를 만들었다면 add() 메서드를 사용하는데 있어 매개변수의 다형성 성립이 가능하다.
Product 클래스로 타입 변수가 지정되면 그다음에는 Product 타입으로 인식하기 때문에 상속받는 클래스의 객체를 생성해서 추가할 수 있게 된다.
ArrayList<Product> list = new ArrayList<Product>(); // 타입 변수가 일치함
ArrayList<Tv> tvList = new ArrayList<Tv>();
printAll(tvList); // tvList는 제네릭 타입이 Tv이기 때문에 매개변수 다형성이 안된다.
private static void printAll(ArrayList<Product> list) {
for (Product p : list) {
System.out.println("p = " + p);
}
}
이번 코드 예제에서는 Tv 클래스의 참조 변수인 tvList를 printAll 메서드에 매개변수로 넣는데 에러가 발생한다.
그 이유로는 printAll 메서드는 Product 클래스를 타입 변수로 사용하는 제네릭 클래스이므로 Tv 클래스와 타입이 다르기 때문에 타입 에러가 발생하게 된다.
Product 클래스로 변경하거나 ArrayList를 <E> 타입 변수로 변경하여 사용해야 에러가 발생하지 않게 된다.
제한된 제네릭 클래스
앞서 설명한 제네릭 클래스에서 문제점은 Tv 클래스가 Product 클래스를 상속받았지만 타입 에러가 발생한다는 문제가 있다. 따라서 상속받은 클래스도 사용할 수 있게 하기 위해서 제한된 제네릭 클래스를 사용해야 한다.
class MartCart<T extends Product1 & Buyable> extends Cart<T> { }
class Cart<T> {
ArrayList<T> list = new ArrayList<>();
void add(T item) { list.add(item); }
T get(int i) { return list.get(i); }
int size() { return list.size(); }
public String toString() { return list.toString(); }
T[] arr; // T타입의 배열을 위한 참조변수를 만들 수 있다.
// T[] arr1 = new T[5]; // 배열을 생성할 때는 제네릭의 타입 변수를 사용할 수 없다.
}
타입 변수를 선언할 때 <E>만 선언했다면 이제는 <T extends 조상 클래스 & 인터페이스> 이렇게 선언할 수 있다.
Cart 클래스를 보면 Product1의 자손 타입으로 넘어오면 타입 변수 T가 전부 넘어온 자손 타입으로 변경된다.
단, 여기서 주의할 점으로는 제네릭 타입은 배열을 위한 참조 변수를 만들 수 있지만 생성은 불가능하기 때문에 주의해서 사용해야 한다.
MartCart<Product1> productCart = new MartCart<>();
MartCart<Tv1> tvCart = new MartCart<>();
MartCart<Laptop> laptopCart = new MartCart<>();
Product1 클래스를 Tv1 클래스와 Laptop 클래스가 상속받고 있다면 해당 코드와 같이 작성할 수 있다. 여기서 주의할 점은 조상과 자손 간은 가능하지만 형제 관계에서는 사용할 수 없다.
와일드 카드
와일드카드는 <?>로 표현하며 하나의 참조 변수로 대입된 타입이 다른 객체를 참조할 수 있게 해 준다.
<? extends T>: 와일드카드의 상한 제한을 표현. (T와 그 자손들만 가능)
<? super T>: 와일드 카드의 하한 제한을 표현. (T와 그 조상들만 가능)
<?>: 모든 타입이 가능하며 제한이 없음. (<? extends Object>와 동일함)
참고로 메서드의 매개변수에 와일드카드를 사용할 수 있다.
MartCart2<? extends Product2> laptop = new MartCart2<Product2>();
laptop = new MartCart2<Laptop2>(); // 자손을 타입으로 받을 수 있게 된다.
예제 코드와 같이 와일드카드를 적용하면 Product2 클래스의 자손들을 사용할 수 있게된다. 따라서 참조 변수 laptop을 통해서 자손의 객체를 생성할 수 있다.
제네릭 타입의 형변환
Box b = null;
Box<String> bStr = null;
b = (Box) bStr; // 원시타입을 제네릭 타입으로 형변환 할 수는 있지만 경고가 발생한다.
원시타입을 제네릭 타입으로 형변환 할 수는 있지만 경고가 발생하게 된다.
Box<? extends Object> box = new Box<String>(); // 와일드 카드를 사용한 제네릭 타입은 형변환이 가능하다.
따라서 사용하기 위해서는 해당 코드와 같이 와일드 카드를 사용하면 제네릭 타입으로 형변환이 가능해진다.
'자바' 카테고리의 다른 글
Java - Optional (0) | 2023.11.19 |
---|---|
Java - 람다 (0) | 2023.11.17 |
Java - 컬렉션 (0) | 2023.11.14 |
Java - 애너테이션 (0) | 2023.11.13 |
Java - 내부 클래스 (0) | 2023.11.11 |