1. 제네릭스 (Generics)
1) 컴파일시 타입을 체크해 주는 기능.
2) 클래스를 설계할 때 타입 파라미터로 대체했다가 실제 사용될 때 구체적인 타입을 지정함으로써 타입 변환을 최소화.
3) 선언시 클래스 또는 인터페이스 이름 뒤에 "<>" 부호가 붙음. <> 사이에는 타입 파라미터가 들어감.
4) 타입 파라미터
- 일반적으로 대문자 알파벳 한 문자로 표현.
- 개발 코드에서는 타입 파라미터 자리 <T> 자리에 구체적인 타입을 지정해야 함.
public class 클래스명<T> {....}
public interface 인터페이스명<T> {....}
2. 제네릭스를 사용하는 이유
1) 컴파일 시 미리 타입을 체크해서 런타임 에러를 사전에 방지
2) 불필요한 타입 변환을 생략할 수 있어 코드가 간결해짐.
3) 비제네릭을 사용할 경우 : Object 타입을 사용함으로써 빈번한 타입 변환 발생 -> 성능 저하 원인
- 예시 1 (실행시 발생하는 런타임 에러는 어떻게 컴파일 단계에서 발견할 수 있을까?)
//문제 제시
ArrayList list = new ArrayList();
list.add(10);
list.add(20);
list.add("30"); //String 추가
//Integer i = list.get(2); //인덱스 2는 Integer 30이 아닌 String 이기 때문에 컴파일 에러.
Integer i = (Integer)list.get(2); //에러를 막기 위해 강제형변환을 해주면 실행 전 컴파일 에러는 없음.
System.out.println(list); //하지만 실행하면 "ClassCastExcepion" 런타임 에러가 발생함.
//문제 해결
ArrayList<Integer> list = new ArrayList<Integer>();
list.add(10);
list.add(20);
//list.add("30"); //맨처음 객체 생성시 제네릭스로 Integer 를 타입으로 지정하면
//String 을 넣을 경우 컴파일 에러가 발생함.
list.add(30); //그래서 이렇게 다시 수정할 수 있는 기회가 생김.
Integer i = list.get(2); //컴파일 OK. 강제 형변환 생략 가능
//Integer i = (Integer)list.get(2); //제네릭스로 Integer을 선언했기 때문에 강제 형변환 생략 가능
System.out.println(list);
- 예시 2 (형변환 생략)
//기존 방법은 총 2번의 타입변환이 필요함.
List list = new ArrayList();
list.add("hello"); //String 타입이 강제로 Object 타입으로 변환되어 배열에 추가됨.
String str = (String)list.get(0); //그래서 꺼내올 때는 다시 String 타입으로 강제변환 시켜야 함.
//제네릭 사용 후
//List에 저장되는 요소를 String으로 국한하기 때문에 가져올 때 타입 변환이 필요없음.
List<String> list = new ArrayList<String>(); //List컬렉션에 <String> 타입만 저장하겠다는 의미
list.add("hello"); //String 타입으로 저장되어 형변환 필요 없음.
String str = list.get(0); //그대로 String 타입이 꺼내짐
3. 타입 변수
1) 클래스를 작성할 때, Object 타입 대신 타입 변수(T)를 선언해서 사용. (알파벳은 무엇을 쓰든 상관 없음)
2) 객체를 생성시, 타입변수(E) 대신 실제 타입을 대입해야 함.
- 타입변수 적용한 예시
class Tv {}
class Audio {}
public class GenericTest {
public static void main(String[] args) {
ArrayList<Tv> list = new ArrayList<Tv>(); //Tv 타입의 객체만 저장 가능
list.add(new Tv());
//list.add(new Audio()); //Tv 타입의 객체만 저장 가능하기때문에 에러.
Tv t = list.get(0); //타입 일치하므로 형변환 불필요.
4. 제네릭스 용어
1) Box<T> : 제네릭스 클래스. 'T의 Box' 또는 'T Box' 라고 읽음.
2) T : 타입변수 또는 타입 매개변수. (T는 타입 문자)
3) Box : 원시 타입 (raw type)
5. 제네릭스 타입과 다형성
1) 제네릭스 클래스간의 다형성은 성립. (대입된 타입은 당연히 일치해야 함.)
List<Tv> list = new ArrayList<Tv>(); //다형성. ArrayList가 List를 구현.
List<Tv> list = new LinkedList<Tv>(); //다형성. LinkedList가 List를 구현.
2) 매개변수의 다형성도 성립.
ArrayList<Product> list = new ArrayList<Product>();
list.add(new Product());
list.add(new Tv()); //매개변수의 자손도 가능.
list.add(new Audio()); //매개변수의 자손도 가능.
3) 여러개의 타입 변수가 필요한 경우, 콤마를 구분자로 선언. HashMap<K, V>
public class HashMap extends AbstractMap {
....
public V get(Object key) { //내용생략 }
public V put(K key, V value) { //내용생략 }
public V remove(Object key) { //내용생략 }
....
}
//이 타입 변수에 타입을 지정해 주면 아래와 같음
public class HashMap extends AbstractMap {
HashMap<String, Student> map = new HashMap<>();
....
public Student get(Object key) { //내용생략 }
public Student put(String key, Student value) { //내용생략 }
public Student remove(Object key) { //내용생략 }
....
}
//get으로 읽어오는 코드가 형변환 없이 간결해짐.
Student s1 = map.get("1-1");
6. 제한된 제네릭스 클래스
1) 메소드 안에서 타입 파라미터 변수로 사용 가능한 것은 상위 타입의 멤버로 제한됨. (extends로 대입할 수 있는 타입을 제한.)
- 인터페이스인 경우에도 extends를 사용.
//인터페이스도 가능하며 똑같이 extends를 씀
public <T extends 상위타입> 리턴타입 메소드(매개변수){}
- 타입 제한 예시
class FruitBox<T extends Fruit> { //Fruit의 자손만 타입으로 지정가능하도록 제한
ArrayList<T> list = new ArrayList<>();
...
}
FruitBox<Apple> appleBox = new FruitBox<Apple>(); //컴파일 OK
FruitBox<Toy> toyBox = new FruitBox<Toy>(); //에러. Toy는 Fruit의 자손이 아님.
//인터페이스의 경우 타입 제한
interface Eatable {}
class FruitBox<T extends Eatable> { ... }
2) 제한된 타입 파라미터 지정
- 숫자를 연산하는 메소드는 매개값으로 Number 타입 또는 하위 타입(Byte,Short 등)의 인스턴스만 가져와야 함. (타입 파라미터 제한)
3) static 멤버에 타입 변수는 사용 불가능.
class Box<T> {
static T item; //에러
static int compare(T t1, T t2) { ... } //에러
}
4) 배열 생성시 타입 변수 사용 불가. 타입 변수로 배열 선언은 가능.
class Box<T> {
T[] itemArr; // OK. T 타입의 배열 생성은 가능.
....
T[] toArray() {
T[] tmpArr = new T[itemArr.length]; //에러. new 다음에는 제네릭스 사용 불가.
}
}
7. 제네릭스 메서드
1) 제네릭 타입이 선언된 메서드(타입 변수는 메서드 내에서만 유효함)
static <T> void sort(List<T> list, Comparator<? super T> c)
2) 클래스의 타입 매개변수 <T>와 메서드의 타입 매개변수 <T>는 별개임.
class FruitBox<T> { //클래스의 <T>와 아래 메서드의 <T>는 다른 타입 변수임.
...
static <T> void sort(List<T> list, Comparator<? super T> c) {
... //메서드 <T>는 메서드 안에서만 사용 가능함.
}
}
3) 제네릭 메서드는 메서드를 호출할 때마다 타입을 대입. (하지만 대부분 생략 가능)
8. 와일드 카드 타입 <?>
1) 하나의 참조 변수로 대입된 타입이 다른 객체를 참조 가능한 것.
ArrayList<Product> list = new ArrayList<Tv>(); //컴파일 에러. 대입된 타입 불일치
ArrayList<? extends Product> list = new ArrayList<Tv>(); //와일드 카드를 써서 에러가 나지 않음.
ArrayList<? extends Product> list = new ArrayList<Audio>(); //와일드 카드를 써서 에러가 나지 않음.
2) 제네릭타입 < ? >
- Unbounded Wildcards (제한 없음)
- < ? extends Object > 와 동일함.
- 모든 타입이 올 수 있음.
3) 제네릭타입 < ? extends T(상위타입) >
- Upper Bounded Wildcards (상위 클래스 제한)
- 상위 타입과 그 자손인 하위 타입만 올 수 있음.
4) 제네릭타입< ? super T(하위타입) >
- Lower Bounded Wildcards (하위 클래스 제한)
- 하위 타입과 그 조상인 상위 타입만 올 수 있음.
5) 메서드의 매개 변수에도 와일드 카드를 사용할 수 있음.
9. 와일드 카드 vs 제네릭 메서드
1) 제네릭 메서드는 메서드를 호출 할 때마다 다른 제네릭 타입을 대입할 수 있게 한 것.
<T extends Fruit>
2) 와일드 카드는 하나의 참조변수로 서로 다른 타입이 대입된 여러 제네릭 객체를 다루기 위한 것.
< ? extends T(상위타입) >
3) 와일드카드가 안될 때 제네릭 메서드를 사용하는 경우가 많음.
11. 제네릭 타입의 형변환
1) 제네릭 타입과 원시 타입 간의 형변환은 바람직하지 않음. (경고 발생)
Box<Object> objBox = null;
Box box = (Box)objBox; //가능은 하지만 추천하지 않음. 제네릭 타입 → 원시 타입. 경고 발생.
objBox = (Box<Object>)box; //가능은 하지만 추천하지 않음. 원시 타입 → 제네릭 타입. 경고 발생.
2) 와일드 카드가 사용된 제네릭 타입으로는 형변환 가능
12. 제네릭스 타입의 상속과 구현
1) 자식 제네릭 타입은 추가적으로 타입 파라미터를 가질 수 있음.
public class ChildProduct<T, M, C> extends Product<T, M>{}
2) 제네릭 인터페이스로 구현한 클래스도 제네릭 타입이 됨.
public class StorageTest<T> implements Storage<T>{
private T[] array;
public StorageTest(int capacity){
//타입 파라미터로 배열을 생성하려면 (T[])(new Object[n])으로 해야 함.
this.array = (T[])(new Object[capacity]);
}
}
참고 : [한빛미디어] 이것이 자바다 (신용권의 Java 프로그래밍 정복) Chapter 13.제네릭
참고 : [도우출판] JAVA의 정석(3ND EDITION)-자바의 정석 최신 Java 8.0 포함 Chapter 12.지네릭스
'JAVA' 카테고리의 다른 글
Java - Arrays 클래스 (0) | 2022.10.29 |
---|---|
Java - 다형성, instanceof (0) | 2022.10.29 |
예외 처리 (Exception) (1) | 2022.09.10 |
오버로딩(Overloading), 오버라이딩(Overriding) (0) | 2022.09.09 |
인터페이스 (Interface) (0) | 2022.09.02 |
댓글