제네릭이란
제네릭(generic)이란 타입을 사용하는 쪽에서 정의하여, 코드의 재사용성과 타입 안정성을 제공하는 기능입니다.
자바에서 List 자료형에는 아래와 같이 <> 괄호 안에 타입을 지정한다고 배웁니다.
List<Integer> intList = new ArrayList<>();
intList.add(10000);
이 코드는 List자료형인 intList변수에 Integer타입만 들어올 수 있다는 것을 의미합니다.
그렇지만 동일한 List 클래스를 아래와 같이 선언하면 해당 리스트에는 다른 타입을 받을 수 있습니다.
List<String> strList = new ArrayList<>();
strList.add("good");
이것이 가능한 이유는 List가 제네릭 인터페이스라서 그렇습니다.
'타입을 사용하는 쪽에서 정의'할 수 있기 때문입니다.
List 자료형을 타고 들어가보면 아래와 같이 정의되어있습니다.
public interface List<E> extends Collection<E> { ... }
컬렉션을 상속받고, E 라는 타입으로 정의되어 있는 것을 볼 수 있습니다.
여기서 E를 '타입 파라미터'라고 부르고 이를 확장하여 적용하면,
클래스나 인터페이스를 선언할 때에도 타입 파라미터를 이용해서 사용하는 쪽에서 정의할 수 있습니다.
제네릭 클래스 선언과 사용방법
public class Box<T> {
private T item;
public void setItem(T item) {
this.item = item;
}
public T getItem() {
return item;
}
}
public class Main {
public static void main(String[] args) {
Box<Integer> intBox = new Box<>();
intBox.set(10);
// intBox.set("글자불가능") 컴파일 실패
Box<String> strBox = new Box<>();
strBox.set("Hello");
// strBox.set(10000); 컴파일 실패
}
}
예를 들어 Box클래스를 선언할 때 위와같이 T 라는 타입 파라미터를 주고 선언할 수 있습니다.
그리고 아래에서 실제 사용할 때 타입을 지정해주면 컴파일 타임에 타입에 대한 처리가 가능합니다. - 타입 안정성
Box라는 하나의 클래스만으로, 여러 타입의 데이터를 처리할 수가 있습니다. - 코드의 재사용성
타입 파라미터 네이밍 방법
- E: Element (주로 컬렉션에서 사용)
- K: Key
- N: Number
- T: Type
- V: Value
- S, U, V 등: 2번째, 3번째, 4번째에 선언된 타입
타입 파라미터를 지정하는 명명규칙은 정해진 것이 없지만, 통상적으로 위와 같이 사용합니다.(convention)
와일드카드
List<Object>는 List<Integer>의 상위 타입이 아닙니다.
일반적인 클래스의 경우 상속관계를 통해 다형성을 적용할 수 있지만, 제네릭은 불가능합니다.
따라서 일종의 다형성을 적용하기 위해 와일드 카드를 사용합니다.
- <?>: 모든 타입이 가능합니다.
- <? extends 상위타입>: 하위 타입이 가능합니다.
- <? super 하위타입>: 상위 타입이 가능합니다.
public void printList(List<?> list) {
for (Object o : list) {
System.out.println(o);
}
}
List<Integer> intList = new ArrayList<>(Arrays.asList(1, 2, 3));
List<? extends Number> numberList = intList;
// 읽기는 가능
for (Number num : numberList) {
System.out.println(num);
}
// numberList.add(4); // 컴파일 에러
이렇게 선언하는 경우, Number 의 Number를 포함한 하위타입으로는 모두 받을수 있지만,
역시 add 사용이 불가능하여 리스트 수정이 불가능합니다.
컴파일러는 numberList가 적어도 Number를 상속한 타입이라는 것은 알기 때문에 읽을 수 있지만, 어떤 타입인지 알지 못하기 때문에 add가 불가능합니다.
List<Object> objList = new ArrayList<>();
List<? super Integer> superIntList = objList;
superIntList.add(1);
superIntList.add(2);
// superIntList.add("A string"); // 컴파일 에러
// 읽기는 Object 타입으로만 가능
for (Object obj : superIntList) {
System.out.println(obj);
}
이렇게 선언하는 경우 Integer 객체를 추가할 수 있지만, 읽을 때에는 Object 타입으로만 읽을 수 있습니다.
제네릭 사용시 주의사항
1. 기본타입 사용불가
// List<int> list; // 컴파일 오류
List<Integer> list; // OK
제네릭은 원시타입의 Wrapper 클래스를 통해서만 선언이 가능합니다.
제네릭은 내부적으로 Object를 사용하여 구현되어 있습니다.
이유는 컴파일 타임에 타입 정보를 검사한 후, 런타임 시에 이 정보를 제거하는 '타입 소거' 방식을 사용하기 때문입니다.
컴파일 시 모든 제네릭 타입 정보가 제거되어, 실행시점에 원시타입으로 처리되기 때문에 원시타입 선언은 불가능합니다.
2. 타입 파라미터로 인스턴스 생성 불가
public class GenericFactory<T> {
public T create() {
return new T(); // 컴파일 오류
}
}
예를 들어 이런코드가 있다면, java에서는 컴파일 타임 이후 T는 모두 Object로 변환됩니다.
런타임에는 이미 타입 정보 검사가 끝났기 때문에 JVM이 T가 구체적으로 어떤 타입인지 알 수 없습니다.
3. static 멤버에서 타입 파라미터 사용 불가
static 멤버는 클래스 레벨에 속하며, 인스턴스와 무관하게 클래스가 로드될 때 메모리에 할당됩니다.
public class Box<T> {
private T value;
// 컴파일 오류
private static T staticValue;
// 컴파일 오류
public static T getStaticValue() {
return staticValue;
}
}
예를 들어 위와같이 staticValue가 static으로 선언되어 있다면, 모든 인스턴스에서 공유되는 변수가 됩니다.
하지만, 타입 파라미터는 인스턴스마다 다를 수 있습니다. 공유되는 변수인 staticValue에 어떤 타입을 지정해 주기가 어렵습니다.
따라서 static에는 타입 파라미터 사용이 불가능합니다.
'Language > JAVA' 카테고리의 다른 글
자바 가변인자(varargs)와 힙 오염(Heap pollution) (0) | 2024.07.25 |
---|---|
JCF - 자바 컬렉션 프레임워크의 종류 (0) | 2024.07.24 |
String 클래스 파고들기 - 객체의 특성 및 더하기 연산의 버전별 차이 (0) | 2024.04.09 |
String 클래스 파고들기 - String 생성과 byte 변환시 주의점 (0) | 2024.04.05 |
자바가 돌아가는 동작 원리 (0) | 2024.03.06 |
남에게 설명할 때 비로소 자신의 지식이 된다.
포스팅이 도움되셨다면 하트❤️ 또는 구독👍🏻 부탁드립니다!! 잘못된 정보가 있다면 댓글로 알려주세요.