제네릭
정의
- JDK 1.5에 처음 도입, 다양한 타입의 객체들을 다루는 메서드나 컬렉션 클래스에 컴파일 시의 타입 체크해주는 기능이다. 추가로 에러는 언제나 런타임보다 컴파일 타임에 잡는것이 좋다(런타임 에러는 프로그램이 실행되는 동안 발생하는데 이는 프로그램 실행 중단으로 이어질 수 있다)
제네릭의 장점
- 타입 안정성을 제공
- 리스트의 형을 정함으로써 컴파일러가 들어오는 데이터의 타입을 자동으로 체크해준다(리스트에 타입이 다른 데이터가 못들어오게함 = 안정성)
- 타입체크와 형변환을 생략 할 수 있으므로 코드가 간결해진다
- ex. 원래는 list의 객체를 가져올때 object()형으로 반환되서(String)list.get(0)이런식으로 문자열을 가져와야하는게 이제는 쓸 필요가 없다
예시
//제네릭 미사용
List stringList = new ArrayList<>();
stringList.add("문자열");
stringList.add(1);
String result = (String)stringList.get(0) + (String) stringList.get(1);
String만 담을려고 했는데 타입지정을 안해서 정수가 들어와도 컴파일 단계에서 오류가 안터지고 런타임시에 오류가 터진다
//제너릭 사용
List<String> stringList = new ArrayList<>();
stringList.add("문자열");
stringList.add(1);
컴파일 에러 발생을 통해 사용자가 의도한 대로 적절한 데이터 타입만을 받을 수 있다
제네릭 용어
바운디드 타입, 와일드 카드
바운디드 타입
특정 범위 내에서 제네릭 타입을 제한하는 기능이다. 이를 통해 특정 클래스나 인터페이스를 상속하거나 구현하는
클래스만을 제네릭 타입으로 사용할 수 있도록 제한할 수 있다.
예시
public class ExClass<T extends Fruit>{
}
이렇게 사용하면 'T'는 'Fruit' 클래스 또는 'Fruit'클래스를 상속받는 클래스만을 대입할 수 있다.
와일드 카드
와일드 카드(?)는 제네릭 클래스나 메서드에서 더 유연하게 타입을 다룰 수 있도록 도와준다. 와일드 카드는 알 수 없는 타입을 나타내며, extends와 super 키워드를 사용하여 상한과 하한을 제한할 수 있다. 이러한 와일드 카드는 PECS(생산자 - extends - 데이터를 꺼냄 - 반공변, 소비자 - super - 데이터를 넣음 - 공변) 공식과 관련이 있다.
- 공변(Covariance): <? extends T>는 상한 제한을 나타내며, 타입 T와 그 자손들만이 가능하다.
- 반공변(Contravariance): <? super T>는 하한 제한을 나타내며, 타입 T와 그 조상들만이 가능하다.
- 무공변(Invariance): <?>는 제한이 없는 와일드 카드를 의미하며, 모든 타입이 가능하다. <? extends Object>와 동일하다
그림 예시
1. <?>
2. <? extends ToyotaCar>
3.<? super ToyotaCar>
사용이유
- 이러한 와일드 카드를 사용하면 메서드나 클래스에서 제네릭 타입의 유연성을 높일 수 있다. 이를 통해 상한과 하한을 제한하여 자연스럽게 다양한 타입을 다룰 수 있다
- 와일드 카드를 사용하는 경우에는 조금 더 유연한 제네릭 타입을 다룰 수 있지만, 타입 안정성을 유지하기 위해서는 사용에 주의가 필요하다. 그러므로 사용할 때는 PECS 공식과 함께 적절한 상황에 맞게 사용하는 것이 중요하다
제네릭 사용법
제네릭 클래스 선언하기
class Box<T> {//클래스 내용}
- 괄호(< >)로 구분 된 타입 매개 변수는 클래스 이름 뒤에 온다. 객체가 생성될 때 타입 파라미터를 받는 부분이다.
- 타입 파라미터와 일반 클래스 또는 인터페이스 이름의 차이를 구분하기 하기 위해서 정해진 규칙에 따라 타입 파라미터는단일 대문자를 사용한다.
- 타입이 유동적으로 변하기에 클래스 내에서 static 선언이 불가능하다
- Box<String>과 Box<Integer> 선언가능, 같은 클래스임 단지 타입만 다른. 추가로 컴파일 후에는 원시 타입인 Box로 바뀜(제네릭 타입 제거)
Box<Apple> appleBox = new Box<Apple>(); //Apple객체만 가능
Box<Grape> grapeBox = new Box<Grape>(); //Grape객체만 가능
- 배열 생성도 불가능하다. 이유는 컴파일 시에 배열을 생성할때 제네릭 타입에 대한 정보를 알 수없으므로 배열 생성 이 불가능하다. new 연산도 같은 이유로 불가능하다.
- 소스코드에서 바이트코드로 바꿀때 타입소거(Type erasure)가 이루어짐 따라서 ArrayList<String>이던 ArrayList<Integer>던 런타임에는 그냥 ArrayList로 인식
- JDK1.7부터는 추정이 가능한 경우 타입 생략 가능
Box<Apple> appleBox = new Box<>();
- 일반적으로 사용하는 타입매개 변수들
타입 | 설명 |
<T> | Type |
<E> | Element |
<K> | Key |
<V> | Value |
<N> | Number |
- raw 타입
- 제네릭을 사용하지 않았던 과거 자바 버전과의 호환성을 위해서 존재하는 타입 매개변수가 없는 제네릭 타입이다. raw 타입에 매개변수화된 제네릭 타입을 할당 할 수 있다.
- 쉽게 말하면 raw타입은 제네릭 타입이 없는 상태여도 제네릭 타입을 사용한 객체를 할당 할 수 있다.(상호 운용성 제공)
public class Box<T> {
private T t;
public void set(T t) {this.t = t;}
public T get() {return t;}
public static void main(String[] args) {
//raw 타입 생성
Box rawBox = new Box();
Box<String> box = new Box<>();
//raw type에 parameterized type 대입
rawBox = box;
}
}
사용유의
- 참조변수와 생성자에 대입된 타입이 일치해야한다.
Box<Apple> appleBox = new Box<Grape>(); //에러
- 두 타입이 상속관계에 있는것은 안된다. 대신 두 클래스의 타입이 상속관계에 있는것은 가능하다.(제네릭은 다른 제네릭 타입으로 할당할 수 없다)
Box<Fruit> appleBox = new Box<Apple>(); //에러
Box<Apple> appleBox = new FruitBox<Apple>(); //통과, 다형성 FruitBox는 Box의 자손이라고 가정
- 한 종류의 타입만 담을 수 있지만 특정 타입의 자손만 가능하다.(제한 추가)
- 추가로 클래스가 아니라 인터페이스를 구현해야한다는 제약이 필요하면 이때도 extends를 사용해야한다
제네릭 메소드
- 제네릭 메소드는 타입 매개변수를 사용하는 메소드이다.
- 제네릭 타입을 선언하는 것과 비슷하지만 제네릭 메소드에서 타입 매개변수의 scope는 선언 된 메소드로 제한된다.
public class Example {
public <T> void printArray(T[] array) {
for (T element : array) {
System.out.print(element + " ");
}
System.out.println();
}
}
위 코드에서 타입 매개변수 'T'의 스코프(변수나 함수등의 유효한 범위)는 printArray로 한정된다.
이것을 이제 '제네릭 메소드의 타입 매개변수의 스코프가 선언된 메소드 내로 제한된다' 라고 하는거다.
생성
- 선언 위치는 반환타입 바로 앞이다.
- 제네릭 클래스에 정의된 타입 매개변수와 제네릭 메서드에 정의된 타입 매개변수는 전혀 별개의 것이다. 같은 타입 문자 T를 사용해도 같은 것이 아니라는 것에 주의해야 한다
- 컴파일러가 제네릭 메소드의 반환 대상의 타입을 미리 검사하는 타입 추론 기능에 의해서 타입 파라미터는 생략이 가능하다.
- Java 8부터는 컴파일러의 타입 추론 개념이 확장되어 메소드 인자에 포함된 매개변수화된 타입까지 검사한다(메서드 선언시 생략했던 타입을 호출시에 타입 지정을 해주면 언급된 타입으로 추론한다.)
- ex. public WitchPot(T merail) 라는 반환 타입 파라미터 생략 이후 main에서
WichPot<String> pot = Util.put(frog); 이런식으로 언급해주면 반환 대상이 WitchPot<String> 인 것을 확인하고 String으로 추론한다
- ex. public WitchPot(T merail) 라는 반환 타입 파라미터 생략 이후 main에서
Erasure
- 제네릭은 컴파일때 타입을 검사하고 런타임에는 타입을 소거해서 무슨 타입인줄 알 수 없다. → 따라서 기존 코드를 사용하고자 제너릭 타입 소거 등장!
- 이를 실체화가 되지 않는다라고 한다
- 타입 매개변수의 경계까 없는 경우에는 Object로, 경계가 있는 경우에는 경계 타입으로 파라미터를 변경
예시
public class MyGenericClass<T> {
private T value;
public void setValue(T value) {
this.value = value;
}
public T getValue() {
return value;
}
}
위 코드의 경우 컴파일 시간에는 제네릭 타입 'T'가 사용되지만, 실행시간에는 'T'에 대한 정보가 소거되에 실제로는 Object타입으로 취급된다. 즉, 제네릭 타입 'T'는 컴파일 시간에만 사용되고, 실행시간에는 해당 정보가 지워지는 것이 타입소거의 예시이다.
타입소거 예시
이미지 참고: github.com/cmg1411/effectiveJava/blob/master/src/main/java/Chapter5/Day29/item29.md
참고
- 자바의 정석(저자 남궁성)
- https://alkhwa-113.tistory.com/entry/%EC%A0%9C%EB%84%A4%EB%A6%AD
- https://durtchrt.github.io/blog/java/generics/9/
- https://rockintuna.tistory.com/102
'자바(Java)' 카테고리의 다른 글
[자바] 터미널에서 Spring프로젝트 빌드 시 자바 버전 오류 해결 방법 (0) | 2023.12.01 |
---|---|
[자바] 람다식(lamda) (3) | 2023.11.09 |
[자바] 자바 I/O (1) | 2023.10.14 |
[자바] 자바 어노테이션 (1) | 2023.10.05 |
[자바] 자바 Enum (0) | 2023.09.27 |