사용법
다이아몬드 연산자 (<>)에 타입을 정의해주면 되지만 명명 규칙이 존재한다.
- E : 요소(element)
- K : 키(key)
- N : 숫자(Number)
- T : 타입(type)
- V : 값(value)
- S, U, V : 2,3,4번째 선언된 타입
장점
- 컴파일 타임때 타입추론을 통한 타입에러를 사전에 잡을 수 있다.
- 컴파일러가 타입추론을 통한 타입 캐스팅을 자동으로 해준다.
- 코드의 재사용성이 매우 높아진다.
컴파일 타임 때 타입 추론을 통한 타입 에러를 사전에 잡을 수 있다
public interface List<E> extends Collection<E> {...}
List를 사용할 때 우리는 다이아몬드 연산자 (<>) 안에 어떠한 타입을 지정하고 사용한다. Java 7 버전 이후로 도입됐다.
List<String> string = new ArrayList<String>();
Map<String, List<String>> map = new HashMap<String, List<String>>();
에서
List<String> string = new ArrayList<>();
Map<String, List<String>> map = new HashMap<>();
타입을 지정함으로써 컴파일러는 타입을 추론할수 있게된다. 따라서 List 요소는 String 인 객체만 들어올 수 있다.
해당 예제 처럼 String 타입 외 다른 타입이 들어온다면 개발자는 사전에 오류를 잡아낼 수 있다. 만약 타입을 지정하지 않고 로 타입(raw type)으로 사용시 컴파일러는 타입추론을 하지 못한다 따라서 리스트 내부에 어떠한 객체를 넣어도 개발자는 오류가 발생한걸 인지하지 못한다.
public static void main(String[] args) {
List rawTypeList = new ArrayList(); //로 타입 리스트
rawTypeList.add("String");
rawTypeList.add(10);
}
이처럼 컴파일러는 제네릭을 통한 타입추론을 할 수 없기 때문에 해당 코드는 컴파일오류가 나지않지만 제네릭을 통한 타입추론이 가능해짐으로써 우리는 리스트를 믿고 사용할 수 있게된다. 즉 안정성을 보장할 수 있게된다는 뜻인데 만약 로 타입 리스트를 사용하게된다면 리스트 내부에 어떠한 객체가 있는지 알 수 없게된다. 이러한 문제점은 안정성이 없고 얼마든지 개발자의 실수로 인해 리스트내부에 다른 객체가 혼합하여 들어갈 수 있다.
컴파일러가 타입 추론을 통한 타입 캐스팅을 자동으로 해준다
public static void main(String[] args) {
List rawTypeList = new ArrayList();
rawTypeList.add("String");
rawTypeList.add(10);
for (Object o : rawTypeList) {
System.out.println(o);
}
}
컴파일러는 제네릭을 보고 타입추론을 하지만 현재 예제의 리스트는 로 타입 리스트이기 때문에 컴파일러는 타입 추론을 하지 못한다 따라서 for-each 문을 사용할 때 Object의 객체를 내보내게 된다. (어떠한 객체가 있는지 모르기 때문에) 이러한 문제점은 만약 리스트 내부에 서로 다른 타입의 객체가 들어가 있으면
for (Object o : rawTypeList) {
if(o instanceof String){
System.out.println((String) o);
}
if(o instanceof Integer){
System.out.println(o);
}
}
일일이 비교를 통한 타입캐스팅을 해줘야하지만 제네릭을 사용한다면
List<String> stringList = new ArrayList<>();
for (String s : stringList) {
System.out.println(s);
}
코드의 재사용성이 매우 높아진다
public static void main(String[] args) {
List<Integer> intList = new ArrayList<>(List.of(1,2,3,4,5));
List<String> stringList = new ArrayList<>(List.of("a","b","c","d","f"));
for (String s : stringList) {
System.out.println(s);
}
for (Integer integer : intList) {
System.out.println(integer);
}
}
해당 코드는 단순히 리스트를 순회하면서 콘솔에 찍는 행위만 하지만 타입이 서로 다르기 때문에 어쩔 수 없이 반복하는 코드가 보인다. 해당 문제점은 제네릭 메소드를 만들어서 사용하면 해결할 수 있다.
public class MainRunner {
static <E> void ListForEach(List<E> list){
for (E e : list) {
System.out.println(e);
}
}
public static void main(String[] args) {
List<Integer> intList = new ArrayList<>(List.of(1,2,3,4,5));
List<String> stringList = new ArrayList<>(List.of("a","b","c","d","f"));
ListForEach(intList);
ListForEach(stringList);
}
}
제네릭 주요 개념 (바운디드 타입, 와일드 카드)
바운디드 타입 (Bounded Generics)
간단하게 타입을 제한할 수 있는 제네릭이다. 만약 리스트 안에 Number의 하위클래스만 (자기포함)을 받고 싶다면 다음과 같이 해주면된다.
class List<E extends Number>{
...생략
}
이와 반대로 super는 하위클래스가 아닌 상위클래스만 (자기포함) 받고 싶을 때 사용한다
와일드 카드 (WildCard)
void Foo(List<?>list list){} // List 내부의 요소들을 Object 타입으로 취급하게됨
즉 타입을 신경쓰지 않으니 타입캐스팅을 해주지 않는다 (Object 타입으로 취급) 하지만 바운디드 타입과 같이 사용하게 된다면
void Foo(List<? extends Number > list){} // 내부의 요소를 Number 타입으로 취급하게됨
Number로 취급하게 된다. 와일드카드는 구체적인 데이터 타입이 필요하지 않을 때 사용한다. 즉 구체적인 타입보단 각각의 데이터가 가지고 공통적인 부분 즉 추상적인 데이터를 필요할 때 사용하는거 같다.
Erasure (소거)
Erasure 란 원소 타입을 컴파일 타임에만 검사하고 런타임에는 해당 정보를 알 수 없는 것이다. 즉 컴파일 타임에만 타입에 대한 제약조건을 적용하고 런타임에는 타입에 대한정보를 소거하는 행위다. 소거는 타입 파라미터가 unbounded 이면 타입파라미터를 object 타입으로 변경한다.
- Example<T> ->unbounded 임으로
- Example<Object>으로 변경
- Example<? extends Number> -> bounded임으로
- Example<Number> 으로 변경
'프로그래밍 언어 > Java' 카테고리의 다른 글
[Java] Comparable과 Comparator : 비교를 위한 인터페이스 (0) | 2024.10.22 |
---|---|
[Java] Collections 클래스 (0) | 2024.10.22 |
[Java] BigDecimal (0) | 2024.09.04 |
[Java] long float의 값 뒤에 L, F을 붙여야 하는 이유 (0) | 2024.09.04 |
[Java] 자료형 정리 (0) | 2024.09.04 |