제네릭스(Generics)란?
다양한 타입의 객체들을 다루는 메서드나 컬렉션 클래스에 컴파일 시 타입체크를 해주는 기능이다.
객테의 타입을 컴파일 시 체크하기에 객체의 타입 안정성을 높인다.
지정되지 않은 타입의 객체가 저장되는 것을 막는다.
ArrayList같은 컬렉션 클래스는 다양한 종류의 객체를 담을 수 있다.
만약 한 종류의 객체만 담기를 원할 때 제네릭스를 사용하면
약속한 형식대로만 사용하도록 개발하다보니 이후에 꺼낼때 형변환이 필요 없어진다.
- 제네릭스(Generics)를 사용하지 않을 때
public static void main(String[] args) {
ArrayList list = new ArrayList();
list.add(10);
list.add("10");
String word = (String)list.get(1);
}
list 는 ArrayList로 이루어져 있으며 Object 타입이 들어가다보니, 객체를 꺼낼 때마다 필요한 형으로 변환이 필요하다.
- 제네릭스(Generics)를 사용할 때
public static void main(String[] args) {
// 제네릭 사용
ArrayList<String> list = new ArrayList<String>();
list.add("10");
list.add("20");
String word = list.get(0);
}
제네릭스를 사용한다면 타입지정을 해주고, 원하는 곳에 사용할 때 형변환을 사용하지 않아도 된다.
제네릭스(Generics) 사용 장점
- 타입 안정성을 제공한다. ( 코드를 잘못 입력하게 되면 컴파일 에러가 발생하므로 오류를 바로바로 체크 할 수 있다.)
- 타입체크와 형변환을 생략할 수 있으므로 코드가 간결해 진다.
1. 제네릭스 클래스 선언
제네릭 타입은 클래스와 메소드에 선언할 수 있다.
클래스에 선언하는 제네릭 타입이란?
1
2
3
4
5
6
|
class Box{
Object item;
void setItem(Object item){}
Object getItem(){}
}
|
cs |
위의 클래스는 일반적인 클래스이다.
이를 제네릭 클래스로 변경하고자 한다면 '<T>' 를 붙이고 Object는 모두 T로 변경해야한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
public class Box<T> {
ArrayList<T> list = new ArrayList<T>();
void add(T item) {
list.add(item);
}
T get(int i) {
return list.get(i);
}
int size() {
return list.size();
}
@Override
public String toString() {
return list.toString();
}
}
|
cs |
- Box<T>에서 'T'를 타입변수( type variable )라고 하며 'Type'에서 첫글자를 따온 것이다.( 타입변수는 T가 아닌 다른 것을 사용해도 된다. )
- 타입변수가 여러개인 경우에는 Map<K,V>와 같이 콤마를 구분자로 나열하면 된다.
- T, K, V 이들은 기호의 종류만 다를 뿐, '임의의 참조형 타입'을 의미한다는 것은 모두 같다.
- 기존에는 다양한 종류의 타입을 다루는 메소드의 매개변수나 리턴 타입으로 Object타입의 참조변수를 많이 사용했고, 그로 인해 형변환이 불가피했지만, 이젠 Object타입 대신 원하는 타입을 지정하기만 하면 되는 것이다.
b.setItem(new Object()); // 정상
b.setItem("ABC"); // 정상, Object Type이다보니 어느 형이든 들어가는게 가능
※ 제네릭스 용어 정리
class Box {} | |
Box<T> | 제네릭클래스, 'T의 Box' 또는 'T Box'라고 읽는다. |
T | 타입변수 또는 타입 매개변수( T는 타입문자 ) |
Box | 원시타입( raw type ) |
- 타입문자 T는 제네릭 클래스 Box<T>의 타입변수 또는 타입 매개변수라고 하는데 메소드의 매개변수와 유사한 점이 있기 때문이다.
- 그래서 아래와 같이 타입 매개변수에 타입을 지정하는 것을 '제네릭 타입 호출'이라고 하고 지정된 타입을 '매개변수화된 타입( parameterized type )'이라고 한다.
1
2
|
Box<String> b = new Box<String>();
Box<Integer> b1 = new Box<Integer>();
|
cs |
- 위처럼 Box<String>, Box<Integer>는 서로 다른 타입을 대입하여 객체를 만든 것이다.
Box라는 같은 클래스를 활용해서 다른 타입의 객체를 만들 수 있음.
※ 제네릭스의 제한
- 제네릭 클래스 Box의 객체를 생성할 때는 객체 별로 다른 타입을 지정해주는 것이 적절하다.
왜? 제네릭은 인스턴스 별로 다르게 동작하도록 만든 것이기 때문이다. - 그러나 모든 객체에 대해 동일하게 동작해야 하는 static멤버에 타입변수 T를 사용할 수 없다.
왜? T는 인스턴스 변수로 간주되기 때문이다. - static 멤버는 타입 변수에 지정된 타입의 종류에 관계없이 동일한 것이어야 하기 때문이다.
- 또한 제네릭 타입의 배열을 생성하는 것도 허용되지 않는다.
- 제네릭 배열 타입의 참조변수를 선언하는 것은 가능하지만 new T[10]; 과 같은 배열을 생성하는 것은 안된다.
==> new 연산자로 인해 불가능한 것인데, new 연산자는 컴파일 시점에 타입 T가 무엇인지 정확히 알아야 하기 때문이다.
1
2
3
4
5
|
class Box<T>{
static T item; // 에러
static int compare(T t1, T t2){}; // 에러
}
|
cs |
- 타입문자로 사용할 타입을 명시하면 한 종류의 타입만 지정할 수 있도록 제한할 수 있지만,
그래도 여전히 모든 종류의 타입을 지정할 수 있다는 것에는 변함이 없다. - 타입 매개변수 T에 지정할 수 있는 타입의 종류를 제한할 수 있는 방법도 있다.
Box<Toy> toyBox = new Box<>();
toyBox.add(new Toy()); // Ok, 과일상자에 장난감을 담을 수 있다. - 아래와 같이 제네릭 타입에 extends를 사용하면 특정 타입의 자손들만 대입할 수 있게 제한할 수 있다.
class FruitBox<T extends Fruit>{/* 내용 생략 */}
- 여전히 한 종류의 타입만 담을 수 있지만, Fruit 클래스의 자손들만 담을 수 있다는 제한이 걸린다.
2. 와일드 카드( wild card ? 물음표 = 와일드카드 )
매개변수에 과일박스를 대입하면 주스를 만들어 반환하는 Jucier라는 클래스가 있고
이 클래스에는 과일을 주스로 만들어서 반환하는 static 메소드 makeJuice()가 있다고 가정하자.
1
2
3
|
class Juicer{
static Juice makeJuice(FruitBox<T> box){}
}
|
cs |
Juicer 클래스는 제네릭 클래스가 아닌데다가 제네릭 클래스라고 해도 static 메소드에는 타입 매개변수 T가 사용할 수 없으므로 아예 제네릭스를 적용하지 않던가 아래와 같이 매개변수 T 대신 특정 타입 Fruit를 정해줘야 한다.
1
2
3
4
5
6
7
8
9
10
|
class Juicer{
static Juice makeJuice(FruitBox<Fruit> box){}
// static Juice makeJuice(FruitBox<Apple> box){} // 에러
}
FruitBox<Fruit> fruitBox = new FruitBox<Fruit>();
FruitBox<Apple> appleBox = new FruitBox<Apple>();
Juicer.makeJuice(fruitBox); // 성공
//Juicer.makeJuice(appleBox); // 에러
|
cs |
이렇게 제네릭 타입을 'FruitBox<Fruit>'로 고정해 놓으면 위의 코드에서도 알 수 있듯이 'FruitBox<Apple>' 타입의 객체는 메소드의 매개변수가 될 수 없다.
여기서 FruitBox<Apple>' 도 매개변수로 사용하려면 오버로딩을 해야한다. 그러나.....
"제네릭 타입이 다른 것만으로는 오버로딩이 성립되지 않는다."라는 규칙때문에 우리가 생각한 것처럼 오버로딩을 하면 에러가 발생한다.
이럴 때 사용하는 것이 와일드카드이다. 와일드카드는 기호 ?를 사용한다.
1
2
3
4
5
6
7
8
9
|
class Juicer{
static Juice makeJuice(FruitBox<? extends Fruit> box){} // Fruit 상속 받은 애 다 할 수 있음
}
FruitBox<Fruit> fruitBox = new FruitBox<Fruit>();
FruitBox<Apple> appleBox = new FruitBox<Apple>();
Juicer.makeJuice(fruitBox); // 성공
Juicer.makeJuice(appleBox); //
|
cs |
- 와일드카드는 기호 ?로 표현하는데 와일드카드는 어떠한 타입도 될 수 있다.
- ? 만으로는 Object 타입과 다를게 없으므로 아래와 같이 제한을 할 수 있다.
<? extends T> 와일드카드 상한제한, T와 그 자손들만 가능 <? super T> 와일드카드 하한제한, T와 그 조상들만 가능 <?> 제한 없음 모든 타입 가능( <? extends Object> )
3. 제네릭 메소드
- 메소드 선언부에 제네릭 타입이 선언된 메소드를 제네릭 메소드라 한다.
- 제네릭 타입의 선언 위치는 반환 타입 바로 앞이다.
- 보통 메소드는 제네릭스를 쓸 일이 없을텐데, 제네릭 메소드라는 것도 존재하긴한다.
- 하나의 파라미터로 여러 파라미터가 들어오거나, 리턴을 여러 형식이 되도록 설정하는데 쓰인다.
1static<T> void sort(List<T> list, Comparator<? super T> c);cs - 제네릭 클래스에 정의된 타입 매개변수T와 제네릭 메소드에 정의된 타입 매개변수T는 전혀 별개인 것이다.
- 같은 타입문자 T를 사용해도 같은 것이 아니다. 혼용되어 사용되는 경우도 있으니 주의해야 한다.
- 메소드에 선언된 제네릭 타입은 지역변수를 선언한 것과 같다고 생각하면 편한데, 이 타입 매개변수는 메소드 내에서만 지역적으로 사용될 것이므로 메소드가 static이건 아니건 상관없다.
1
2
3
4
5
6
7
8
9
|
static Juice makeJuice(FruitBox<? extends Fruit> box){}
static<T extends Fruit> Juice makeJuice(FruitBox<T> box){}
FruitBox<Fruit> fruitBox = new FruitBox<>();
FruitBox<Apple> appleBox = new FruitBox<>();
Juicer.<Fruit>makeJuice(fruitBox);
Juicer.<Apple>makeJuice(appleBox);
|
cs |
static<T extends Fruit> Juice 부분을 주목하자.
제네릭 메소드를 만들 때, 제네릭 타입의 선언 위치는 반환타입(Juice) 바로 앞이다.
이상한 위치인데, 쨋든 선언하면 파리미터 영역에서 Object T
여기서 extends Fruit 이면 메소드를 통해 Fruit 상속된 모든 객체들로 생성 가능하다는 뜻이다.
* 두 개는 같은 것이다.
static Juice makeJuice(FruitBox<? extends Fruit> box){}
static<T extends Fruit> Juice makeJuice(FruitBox<T> box){}
참조
https://devlog-wjdrbs96.tistory.com/201
https://tadaktadak-it.tistory.com/24
https://dahliachoi.tistory.com/m/46
'IT기술 > JAVA' 카테고리의 다른 글
[Tomcat] 서버 재시작 없이 catalina.out 초기화 (0) | 2023.07.31 |
---|---|
[Java] 자바로 폴더(디렉토리) 생성하기 (0) | 2023.07.31 |
[JAVA] 자바에서 REST API 구현하기 (0) | 2023.07.20 |
[Java] 객체지향에 대한 5원칙 정리 (0) | 2023.07.20 |
[java] pojo란 무엇인가?(Plain Old Java Object) (0) | 2023.07.20 |