Week 14
2022. 9. 10. 00:45
728x90
목표
자바의 제네릭에 대해 학습하세요.
학습할 것
- 제네릭 사용법
- 제네릭 주요 개념 (바운디드 타입, 와일드 카드)
- 제네릭 메소드 만들기
- Erasure
1.제네릭 사용법
제네릭스는 다양한 타입의 객체들을 다루는 메서드나 컬렉션 클래스에 컴파일 시의 타입체크를 해주는 기능이다. 객체의 타입을 컴파일 시에 체크하기 때문에 객체의 타입 안정성을 높이고 형변환의 번거로움이 줄어든다.
class Box {
Object item;
public void setItem(Object item) {
this.item = item;
}
public Object getItem() {
return item;
}
}
* 위의 코드에 제네릭을 적용시키려면 아래처럼 타입을 지정해줘야 한다.
class Box<T> {
T item;
public void setItem(T item) {
this.item = item;
}
public T getItem() {
return item;
}
}
* 무조건 T를 쓰는 것보다 가능하면, 상황에 맞게 의미있는 문자를 선택하여 사용하는 것이 좋다
Map<K,V>처럼 쓰는 경우도 있는데 기호의 종류만 다를 뿐 임의의 참조형 타입을 의미하는 것은 모두 같다.
---------------------------------------------------------------------------------
public static void main(String[] args) {
Box<String> box = new Box<String>();
box.setItem(new Object()); //에러가 난다. String 이외의 타입은 지정불가.. 위의 setItem메서드 정의한 걸 보자
box.setItem("abc"); //OK 가능 또는 new String()으로?
String item = ~~(String)~~b.getItem(); //String타입으로 형변환을 할 필요가 없어짐
}
* 위에서 에러가 나는 부분은 제네릭을 사용하지 않는다면 컴파일 단계에서 아마 찾기가 힘들 것이다. 그래서 사용하는 것이 제네릭
public static void main(String[] args) {
Box<Apple> appleBox = new Box<Apple>();
Box<Grape> grapeBox = new Box<Grape>();
}
* 위 처럼 객체별로 다른 타입을 지정하는 것은 적절하다! 제네릭이 이처럼 인스턴스별로 다르게 동작하도록
하려고 만든 기능이니까! (담을 때 딱딱 나눠서 담아라!)
class Box<T> {
static T item; //에러
static int compare(T t1, T t2) {...} //에러
}
* static 멤버는 타입 변수가 허용되지 않는다. 타입 변수에 지정된 타입, 즉 대입된 타입의 종류에 관계없이
동일한 것이어야 하기 때문이다. 무엇보다 static 멤버는 클래스가 메모리에 로딩이 될 때 생성되기 때문에
new연산자를 사용하여 타입 변수를 적용하고 이러는 행위가 일어나기 전에 이미 생성이 되어야 하는 멤버..들
다만, static 메서드에서 선언하고 사용하는 것은 괜찮다.
class FruitBox<T> {
...
static <T> void sort(List<T> list, Comparator<? super T> c) { //메서드 선언부에서 제네릭 타입이 선언된 메서드를 제네릭 메서드라고 함.
...
}
}
class Box<T> {
T[] itemArr; //일단 OK.... 선언은 돼..
T[] toArray() {
T[] tmpArr = new T[itemArr.length]; //에러... 제네릭 배열 생성 불가
...
return tmpArr;
}
}
* 제네릭 배열을 생성할 수 없는 이유는 new연산자 때문인데, 이 연산자는 컴파일 시점에 타입 T가 뭔지 정확히 알아야 한다.
(이렇게 정의를 하고 이러는 걸로..는 안 되지..) 그런데 위의 코드에 정의된 Box<T>클래스를 컴파일 하는 시점에서 T가 어떤 타입이 될지 전혀 알 수 없다.
instanceof연산자도 new연산자와 같은 이유로 T를 피연산자로 사용할 수 없다.
---------------------------------------------------------------------------------
* 제네릭 사용법
1.객체를 생성할 때는 참조변수와 생성자에 대입된 타입(매개변수화된 타입)이 일치해야 한다. 일치하지 않으면 에러가 발생한다.
단, 두 제네릭 클래스의 타입이 상속 관계에 있고, **대입된 타입**이 같으면 괜찮다
2.제네릭 타입에 'extends'를 사용하면, 특정 타입의 자손들만 대입할 수 있게 제한할 수 있다. extends 뒤에 구현되지 않은 인터페이스가 오더라도 extends를 써야 함
class FruitBox<T extends Fruit> {
ArrayList<T> list = new ArrayList<T>();
...
}
public static void main(String[] args) {
FruitBox<Apple> appleBox = new FruitBox<Apple>();
FruitBox<Toy> toyBox = new FruitBox<Toy>(); //에러 발생
}
****
2.제네릭 주요 개념 (바운디드 타입, 와일드 카드)
바운디드 타입은 extends와 super를 가지고 상한과 하한을 제한하는 것이고, 와일드 카드는 상한과 하한의 제한 없이 모든 타입이 다 들어올 수 있도록 하는 것
<? extends T> 와일드 카드의 상한 제한. T와 그 자손들만 가능
<? super T> 와일드 카드의 하한 제한. T와 그 조상들만 가능
<?> 제한 없음. 모든 타입이 가능. <? extends Object>와 동일함
static Juice makeJuice(FruitBox<Fruit> box) {
String tmp = "";
for(Fruit f : box.getList()) tmp += f + " ";
return new Juice(tmp);
}
static Juice makeJuice(FruitBox<Apple> box) {
String tmp = "";
for(Fruit f : box.getList()) tmp += f + " ";
return new Juice(tmp);
}
* 위와 같이 오버로딩을 하게 되면, 컴파일 에러가 발생한다. 제네릭 타입이 다른 것만으로 오버로딩이 성립되지 않음
제네릭 타입은 컴파일할 때만 사용하고 제거해버리기 때문이다. 이런 경우는 메서드 중복 정의가 되어버린다.
이러한 상황에서 쓸 수 있는 것이 와일드 카드인데 와일드 카드는 어떠한 타입도 될 수 있다.
static Juice makeJuice(FruitBox<? extends Fruit> box) {
String tmp = "";
for(Fruit f : box.getList()) tmp += f + " ";
return new Juice(tmp);
}
* 위와 같이 정의를 한다면, 타입 변수로 Fruit도 올 수 있고, Apple도 올 수 있을 것이다.
3.제네릭 메소드 만들기
메서드의 선언부에 제네릭 타입이 선언된 메서드를 제네릭 메서드라고 한다. 제네릭 타입의 선언 위치는 반환타입의 바로 앞이다.
class FruitBox<T> {
...
static <T> void sort(List<T> list, Comparator<? super T> c) { //메서드 선언부에서 제네릭 타입이 선언된 메서드를 제네릭 메서드라고 함.
...
}
}
public static void main(String[] args) {
FruitBox<Fruit> fruitBox = new FruitBox<Fruit>();
FruitBox<Apple> fruitBox = new FruitBox<Apple>();
System.out.println(Juicer.<Fruit>makeJuice(fruitBox));
System.out.println(Juicer.<Apple>makeJuice(appleBox));
}
* 메서드를 호출할 때 위와 같이 타입 변수에 타입을 대입해야 한다. 그러나 대부분의 경우 컴파일러가 타입을 추정할 수 있기 때문에 생략해도 된다.
한 가지 주의할 점은 제네릭 메서드를 호출할 때, 대입된 타입을 생략할 수 없는 경우에는 참조변수나 클래스 이름을 생략할 수 없다는 것이다.
같은 클래스 내에 있는 멤버들끼리 참조변수나 클래스이름, 즉 'this'이나 '클래스이름'을 생략하고 메서드 이름만으로 호출이 가능하지만, 대입된 타입이 있을 때는 반드시 써 줘야 한다.
이것은 단지 기술적인 이유에 의한 규칙이므로 그냥 지키기만 하면 된다.
4.Erasure
컴파일러는 제네릭 타입을 이용해서 소스파일을 체크하고, 필요한 곳에 형변환을 넣어준다. 그리고 제네릭 타입을 제거한다. 즉 , 컴파일된 파일에는 제네릭 타입에 대한 정보가 없는 것이다. 이렇게 하는 주된 이유는 제네릭이 도입되기 이전의 소스 코드와의 호환성을 유지하기 위해서이다.
1.제네릭 타입의 경계를 제거한다.
->제네릭 타입이 <T extends Fruit>라면 T는 Fruit로 치환된다. <T>인 경우는 T는 Object로 치환된다. 그리고 클래스 옆의 선언은 제거된다.
2.제네릭 타입을 제거한 후에 타입이 일치하지 않으면, 형변환을 추가한다.
->List의 get()은 Object 타입을 반환하므로 형변환이 필요하다.
'Java & Spring > WhiteShip Study' 카테고리의 다른 글
Week 13 (0) | 2022.09.03 |
---|---|
Week 12 (0) | 2022.08.27 |
Week 11 (0) | 2022.08.20 |
Week 10 (0) | 2022.08.13 |
Week 09 (0) | 2022.08.06 |