들어가며
자바에서 동적으로 배열의 크기를 변경하기 위해 배열 대신 List를 사용하곤 한다. 그런데 클래스 선언 문법에 <>로 되어있는 코드를 보았을 것이다. 이걸 제네릭(Generic) 이라고 부르며, 제네릭 파라미터는 꺽쇠안에 포함하여 전달한다. 제네릭이 하는게 무엇이고, 왜 사용할까? 한번 알아보자.
제네릭 (Generics) 이란?
ArrayList<String> list = new ArrayList<>();
제네릭(Generics)는 다양한 타입의 객체를 다루는 메서드나 컬렉션 클래스에 컴파일 시의 타입 체크(compile-time type check)를 해주는 기능이다. 객체의 타입을 컴파일 시에 체크하면, 객체의 타입 안정성을 높이고, 형변환의 번거로움을 줄여준다.
쉽게 말하면, 제네릭스를 사용하면 다루고자 하는 객체의 타입을 미리 지정해 줄 수 있고 이를 통해, 코드에서 복잡한 형변환을 줄일 수 있다는 말이다.
예를 들어, ArrayList<String>처럼 컬렉션 클래스에 String 타입을 명시하게 되면, 해당 컬렉션에는 String 타입의 객체만 저장될 수 있다. 이런 방식으로 타입 안정성이 보장되며, 컬렉션에서 객체를 꺼낼 때 이미 String 타입으로 인식되기 때문에, 개발자가 매번 꺼낸 객체가 String 타입인지 확인할 필요가 없어져 형변환의 번거로움도 줄여준다.
제네릭 클래스의 선언
제네릭 타입은 클래스와 메서드에 선언할 수 있다. 먼저 클래스에 선언하는 제네릭 타입에 대해 살펴보자.
class Box {
Object item;
void setItem(Object item) {
this.item = item;
}
Object getItem() {
return item;
}
}
위 코드는 제네릭을 사용하지 않은 Box 클래스이다. 제네릭 클래스로 변경하려면 클래스 옆에 '<T>'를 붙이면 된다.
이제 제네릭 클래스로 변환해보자.
class Box<T>{
T item;
void setItem(T item) {
this.item = item;
}
T getItem() {
return item;
}
}
여기서 T를 타입 변수라고 한다.
타입 변수는 Type의 첫 글자에서 따온 것으로 T가 아닌 다른 것을 사용해도 된다. 보통 아래표와 같은 타입변수가 주로 사용된다.
기호의 종류만 다를 뿐 "임의의 참조형 타입"을 의미한다는 것은 모두 같다. 따라서 무조건 "T"만 사용하기 보다 상황에 맞게 의미있는 문자를 사용해서 사용하는 것이 좋다.
Box<String> b = new Box<String>(); //타입 T 대신, 실제 타입 String을 지정
b.setItem(new Object()); // 에러발생 제네릭이 String이므로 String타입만 가능
b.setItem("ABC"); // String 타입이므로 가능
String item = b.getIgem(); // 제네릭의 다른 장점으로 형변환 생략가능
이렇게 제네릭은 컴파일 시 타입을 체크해준다.
제네릭스의 용어
class Box<T>
Box<String> box = new Box<String>();
제네릭스의 용어들은 헷갈리기 쉬운 용어들이 많다. 확실하게 정리하자.
- Box<T> : 제네릭 클래스, T의 Box or T Box 라고 읽는다.
- T : 타입 변수 or 타입 매개변수
- Box : 원시 타입(raw type)
- Box<String> : 제네릭 타입 호출
- <String> : 매개변수화된 타입(대입된 타입)
- box : Box<String> 타입의 인스턴스를 참조하는 변수
- new Box<String>() : String 타입의 객체만을 담을 수 있는 Box 클래스의 새 인스턴스를 생성
제네릭스의 제한
Box<Apple> appleBox = new Box<Apple>(); Apple객체만 저장이 가능
Box<Grape> appleBox = new Box<Grape>(); Grape객체만 저장이 가능
제네릭 클래스 Box의 객체를 생성할 때, 객체별로 다른 타입을 지정하는 것은 적절하다. 제네릭스는 인스턴스별로 다르게 동작하도록 만든 기능이기 때문이다.
다음은 제네릭스가 제한되는 예시들이다.
class Box<T> {
static T item; //에러발생
static int compare(T t1,T t2){...} //에러발생
}
모든 객체에 대해 동일하게 동작하는 static멤버에는 타입 변수 T를 사용할 수 없다. T는 인스턴스 변수로 간주되기 때문이다.(static 멤버는 인스턴스 변수를 참조불가)
class Box<T> {
T[] itemArr;
T[] toArray() {
T[] tmpArr = new T[imemArr.length]; //에러. 제네릭 배열 생성불가
}
}
또한 제네릭 배열 타입의 참조변수를 선언하는 것은 가능하지만, 제네릭 타입의 배열을 생성할 수 없다.
그 이유는 new 연산자 때문이다. new 연산자는 컴파일 시점에 타입 T가 무엇인 지 알아야 하나, Box<T> 클래스의 컴파일 시점에서는 T가 어떤 타입이 될 지 전혀 알 수가 없다. (instance of도 같은 이유로 타입 T를 피연산자로 사용할 수 없음.)
제네릭 클래스의 객체 생성과 사용
// 참조변수와 생성자에 대입된 타입
Box<Apple> appleBox = new Box<Apple>();
Box<Apple> appleBox = new Box<Grape>(); // 에러
// 참조변수와 생성자에 대입된 타입 - 상속 관계
Box<Fruit> appleBox = new Box<Apple>(); // 에러
Box<Apple> appleBox = new FruitBox<Apple>();
// 객체 추가
Box<Apple> appleBox = new Box<Apple>();
appleBox.add(new Apple());
appleBox.add(new Grape()); // 에러
// 객체 추가 - 상속 관계
Box<Fruit> appleBox = new Box<Fruit>();
fruitBox.add(new Fruit());
fruitBox.add(new Apple());
제네릭 클래스를 생성할때 Box<Apple> applebox = new Box<Grape>() 같이 타입이 일치하지 않으면 에러가 발생한다.
두 타입(Apple,Grape)이 상속관계에 있어도 마찬가지이다.
단, Box<Apple> appleBox = new FruitBox<Apple>()와 같이 두 제네릭 클래스이 타입이 상속관계이고 타입이 같은 것은 허용한다. (Box class가 FruitBox class의 부모 클래스)
void add(T item)으로 객체를 추가할 때, appleBox.add(new Grape())와 같이 대입된 타입과 다른 타입의 객체는 추가할 수 없다.
단, 타입 T가 'Fruit'이면 void add(Fruit item)이 되므로 fruitBox.add(new Apple())과 같이 Fruit의 자손들은 매개변수가 될 수 있다.
제네릭의 범위 설정
제네릭을 단순히 이런 식으로 설정할 수도 있지만 범위를 주고 싶을 수도 있다. 예를 들어 Interger, Byte, Double 등과 같은 숫자를 다루는
래퍼 클래스들만 범위를 주고 싶을 때 extends라는 키워드를 사용한다. 설정 시에는 숫자 관련된 래퍼 클래스와 그 자손들만 담을 수 있다.
와일드 카드
제네릭 타입이 다른 것만으로는 오버로딩이 성립하지 않기 때문에 와일드 카드가 고안됐다. 와일드 카드는 ?
로 표현하는데, 와일드 카드는 어떠한 타입도 될 수 있다. <?>만으로는 Object타입과 다를 게 없으므로, extends와 super로
상한과 하흔을 제한할 수있다.
<? extends T> 와일드 카드의 상한 제한. T와 그 자손들만 가능
< ? Super T> 와일드 카드이 하한 제한. T와 그 조상들만 가능
< ? > 제한 없음. 모든 타입이 가능 < ? extends Object >와 동일
< K extends T>, <? extends T>는 차이가 있다 K는 특정 타입으로 지정이 되지만, ?는 타입이 지정되지 않는다는 의미가 있다.
제네릭 메소드(Generic method)
메서드에도 제네릭을 적용할 수 있다. 제니릭 메소드의 제네릭 타입변수의 위치는 반환형 앞에 위치하며 제네릭 클래스에
정의된 타입 매개변수와는 별개의 것이다. 같은 타입의 문자를 사용했더라도 같은 것이 아니니 주의해야한다.
static Juice makeJuice(FruitBox<? extends Fruit> box) {} 를 지네릭 메서드로 바꾸면
static <T extends Fruit> Juice makeJuice(FruitBox<T> box) {} 로 바꿀수 있다
또 다른 예로는
public static <T extends Comparable<? super T>> void sort(List<T> list) 메서드를 보자면 List의 요소가 Comparable을
구현한 것이여야하며 구현한 T또는 T의조상이 올수 있다는 것이다. T가 Orange이고 조상인 Fruit가 있다면 T에는 Orange Fruit Object 가 올 수있다.
참고
'JAVA' 카테고리의 다른 글
[JAVA] this와 this()의 차이점 (0) | 2024.04.09 |
---|---|
[JAVA] 문자열 비교 방법의 차이 (==, equals, matches) (1) | 2024.02.25 |