오늘은 제네릭스에 대해서 정리 해보겠다. 무엇인가 공부를 하기전에 그것을 쓰는 이유와 효과(결과)를 먼저 찾고 하는데, 제네릭스는 그 답을 찾기 힘들었다.
물론 책에는 그 이유에 대한 설명을 잘 해주셨지만 , 그 이유에 대해 나는 수긍하지도 이해 되지도 않았다. 그래도 학습의 목적으로 공부한 내용을 소개 해보겠다.
1. 제네릭스란?
제네릭스는 다양한 타입의 객체들을 다루는 메서드나 컬렉션 클래스에 컴파일 시의 타입체크를 해주는 기능이다. 객체의 타입을 컴파일 시에 체크하기 때문에 객체의 타입 안정성을 높이고 형변환의 번거로움이 줄어든다.
즉 , 1. 타입의 안정성을 제공하며 2. 타입체크와 형변환을 생략할 수 있으므로 코드가 간결해 진다.
정의와 설명은 이렇다. 하지만 정작 코드를 보면 의문이 들것이다. 코드가 간결해짐으로써 보기에는 좋을 수 있지만 그만큼 축약 되므로 이해에 어려움이 있을 수 있다고 생각하고 실제 코드를 봐도 나는 이해하기 어려웠다.
코드를 한번 보면서 저 2가지의 장점을 지켜 보자.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
|
import java.util.ArrayList;
public class Generics {
public static void main(String[] args) {
//앞으로 코드가 올부분
}
}
class Box_Object {
ArrayList<Object> list = new ArrayList<>();
void setItem(Object item) {
list.add(item);
}
Object getItem(int index) {
return list.get(index);
}
}
class Box_Generic<T> {
ArrayList<T> list = new ArrayList<>();
void setItem(T item) {
list.add(item);
}
T getItem(int index) {
return list.get(index);
}
}
|
cs |
위의 것이 평범?한 클래스 선언이고 아래 것이 제네릭을 이용한 클래스 이다.
먼저 변수선언을 Object를 한 이유를 살펴보자.
Object를 한 이유는 하나의 타입으로 제한 하지 않고 여러가지 타입을 쓰기 위해서 저런식으로 제한 한것이다.
즉 item 은 String 일수도 있으며 Integer 일 수도 있다. 문제는 하나의 인스턴스 객체를 하나의 타입으로 정하지 않고 무분별하게 쓸수도 있다는 점이다.
예를 들자면
1
2
3
4
5
6
7
8
9
10
11
|
Box_Object bo = new Box_Object();
bo.setItem("AMB");
bo.setItem(new Integer(1));
System.out.println(bo.getItem(0).equals("AMB"));
System.out.println(bo.getItem(1).equals("1"));
|
cs |
결과 :
true
false
1
2
3
4
|
Box_Generic<String> bg = new Box_Generic<>();
bg.setItem("ABC");
// bg.setItem(new Integer(1)); 에러 발생
|
cs |
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
Box_Generic<String> bg = new Box_Generic<>();
Box_Object bo = new Box_Object();
bg.setItem("ABC");
// bg.setItem(new Integer(1)); 에러 발생
bo.setItem("AMB");
bo.setItem(new Integer(1));
String tmp = (String) bo.getItem(0);
String tmp2 = bg.getItem(0); // 캐스팅 필요 없다.
|
cs |
Box_Generic<Parent> bg = new Box_Generic<Child>(); // 에러 발생
다만 T의 상속관계가 아닌 제네릭클래스의 상속관계 인 경우는 가능하다 Box_Generic - Box_Generic_Child 인 경우
Box_Generic<Parent> bg = new Box_Generic_Child<Parent>(); // 가능
여기서도 T 타입은 일치 시켜야만 한다.
3. T의 제한
T에 대한 타입을 제한 시킬 수 있다.
사람을 위한 제네릭 클래스를 선언 했는데 이 T타입에 뜬금없이 과일이 들어가버리면 과일이 사람 행세를 하는 꼴이 된다. 따라서 T를 제한 해야하는 경우가 있다.
class PersonAct<T extends Person> { } 이렇게 제한하게 되면 T는 Person , Person을 상속하는 객체 로 제한이 되어진다. 여기서 키워드 extends는 인터페이로 제한을 둘 경우에도 implements가 아닌 extends를 사용한다는 점에 유의 하자.
그리고 제한에 대해 더 몇가지를 두는것 역시 가능하다. 예를 들면 사람이면서 여자인 경우 또는 남자인 경우로 나눈다면
class PersonAct<T extends Person & Female> { }
class PersonAct<T extends Person & male> { }
이런식으로 하면 된다.
여기까지는 이해하는데 문제 없을 것이다. 만약 여기까지도 어렵다면 제네릭 관련 파트 책이나 강의 영상을 한번 보기를 추천한다.
이제부터 이해하는데 조금 어려울 수 있다.
4. 와일드 카드
와일드 카드를 설명하기 앞서 내 생각 기준으로 와이들 카드를 사용하는? 해야하는? 상황을 먼저 설명 해야 될것 같다.
1. 제네릭 클래스는 아닌 상황에서 매개변수 타입을 지정하지 않은 상황
2. static 매서드에서 타입 매개 변수를 사용해야하는 상황 - ( 사실 제네릭으로 사용 할수도 있긴함 )
3. T 를 사용한 상태에서 범위 지정 ( Add <? super T > )
사실 3번으로 쓰이는 경우는 잘 모르겠다...
2번도 제네릭으로 쓴다면 충분히 구현 가능하므로 1번에 대해서 생각을 해보자
자 먼저 1번의 상황을 설명 할 때 되게 헷갈려 하는 부분이 있는데 코드를 보자
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
|
import java.awt.List;
import java.util.ArrayList;
import java.util.Collection;
public class Generics {
public static void main(String[] args) {
Box_T_Random<A_Grade> btr = new Box_T_Random<>();
btr.setList(new ArrayList<A_Grade>());
btr.list.add(new B_Grade()); // 에러
}
}
class Box_T_Random<T> {
ArrayList<? extends T> list;
void setList(ArrayList<? extends T> list) {
this.list = list;
}
}
class A_Grade {
void call() {
System.out.println("A");
}
}
class B_Grade extends A_Grade {
void call() {
System.out.println("B");
}
}
class C_Grade extends B_Grade {
void call() {
System.out.println("C");
}
}
class D_Grade extends C_Grade {
void call() {
System.out.println("C");
}
}
|
cs |
자 12번 라인을 보자
이 부분에서 왜 에러가 날까? 단순하게 로직만 보면 맞지 않을까 싶다.
? extends A_Grade 라고 있는데 즉 ? 는 A_Grade의 자손으로 제한을 두겠다는 의미로볼 수있다.
그러면 list add 에서 new B_Grade는 되는거 아냐?? 라고 생각 할 수 있다!!!
만약 안되는 이유를 아신다면 기초 부터 공부를 정말 열심히 하신분이다.
자 이는 2가지 시점으로 보면 된다. ( 내 기준임)
1. 컴파일 시점
2. 런타임 시점
이 2가지 인데 앞서 말한 저 잘못 된 생각은 런타임 시점으로 보고 있다고 할 수 있다.
이를 컴파일 시점으로 보면 엄연히 틀렸다는 것을 알수 있는데
컴파일 시점에 ? 는 정해져 있지 않다. 그런데 여기서 내가 new B_Grade를 넣는 상황에 만약 ? 의 값이 B_Grade보다 자식 관계라면?
즉 ArrayList<D_Grade>라면? add로 B_Grade는 당연히 넣을 수 없게 된다.
(주의 btr.setList(new ArrayList<A_Grade>()); 했다고 해서 ? 가 A_Grade 로 치환되는것은 절대 아니라는점을 알고 가자
그냥 ? 이 부분은 치환되지않고 A_Grade 를 상속하는 객체를 담는 ArrayList를 담을수 있다는것 만을 표시하는것이다)
)
즉 이러한 예기치 못한 상황이 발생할 수 있기 때문에 에러가 나는 것이다.
여기 좋은 그림이 있다.
위 그림은 상속 관계를 나타낸다.
즉 타입 매개변수가 상속관계에 있다해서 제네릭클래스도 그 상속관계를 이용해서 사용 할 수 없다는 것이다.
즉 제네릭의 상속관계는 별개 이다.
따라서 add로 이어질수 없는것이다.
따라서 ? extends T 이런식으로 같이 쓰는 경우는 불가능하며
이러한 형태를 짤 수 있다.
package com.test;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
public class Wild {
public static void main(String[] args) {
Box_T_Random<Grade> btr = new Box_T_Random<>();
btr.setList(new ArrayList<Grade>());
btr.addBox(new A_Grade());
btr.addBox(new B_Grade());
btr.addBox(new C_Grade());
btr.addBox(new D_Grade());
Iterator<?> it = btr.getList().iterator();
while(it.hasNext()){
((Grade)it.next()).call();
}
}
}
class Box_T_Random<T extends Grade> {
ArrayList<T> list;
void setList(ArrayList<T> list) {
this.list = list;
}
ArrayList<T> getList() {
return this.list;
}
void addBox(T t) {
this.list.add(t);
}
}
abstract class Grade {
abstract void call();
}
class A_Grade extends Grade {
void call() {
System.out.println("A");
}
}
class B_Grade extends Grade {
void call() {
System.out.println("B");
}
}
class C_Grade extends Grade {
void call() {
System.out.println("C");
}
}
class D_Grade extends Grade {
void call() {
System.out.println("D");
}
}
아래는 추가적인 이해를 위한 코드이다. 참고만하자
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
|
package com.test;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
public class Wild {
public static void main(String[] args) {
Wild w = new Wild();
ArrayList<String> testList = new ArrayList<String>();
testList.add("tt");
testList.add("ee");
for (String string : testList) {
System.out.println(string);
}
System.out.println("-----------");
ArrayList<String> newList = w.addList(testList);
newList.add("ss");
newList.add("tt");
for (String string : newList) {
System.out.println(string);
}
}
@SuppressWarnings("unchecked")
public <T> ArrayList<T> addList(List<T> list){
ArrayList<T> newList = new ArrayList<>();
Iterator<?> it = list.iterator();
while(it.hasNext()) {
newList.add( (T) it.next());
}
return newList;
}
}
|
cs |
제네릭 사용하는 방법에 대해 최대한 쉽게 설명 하려고 했는데 공부하는 나도 어려워서 쉽게 설명하기 힘들었다.
설명은 여기까지이고 내 생각을 좀더 적자면
제네릭에 대한 사용은 사실 모듈단계의 설계를 할 때 주로 사용하지 비지니스 모델을 만들 때는 별로 사용하지 않는다고 하신다
단순히 프렘웤을 사용해 더 쉽게 사용하는 방법이 있다고 하신다.
제네릭을 공부하는 사람에게 좋은 재료가 되었음 좋겠다. 그리고 추가로 좋은 정리가 있어서 링크를 남기겠습니다( http://multifrontgarden.tistory.com/104)
자바공부<7> - JAVA IO (1) | 2018.07.24 |
---|---|
자바공부<6> - 컬렉션 프레임웍 (List,Set,Map,Hashing) (0) | 2018.07.17 |
parseDouble과 parseInt의 구조적 차이 (판정 유무) (1) | 2018.07.12 |
자바공부<5> - 인터페이스 (1) | 2018.07.10 |
자바공부<4> - 다형성 (0) | 2018.07.09 |