자바 공부 <8> - 제네릭스(Generics)

Posted by lib oimb
2018. 7. 27. 16:52 JAVA

오늘은 제네릭스에 대해서 정리 해보겠다. 무엇인가 공부를 하기전에  그것을 쓰는 이유와 효과(결과)를 먼저 찾고 하는데, 제네릭스는 그 답을 찾기 힘들었다. 

물론 책에는 그 이유에 대한 설명을 잘 해주셨지만 , 그 이유에 대해 나는 수긍하지도 이해 되지도 않았다. 그래도 학습의 목적으로 공부한 내용을 소개 해보겠다.


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


당연한 결과다.  하지만 내가 말하고 싶은점은 저런식으로 문자열만 넣는 객체로 생각하고 후에 이를 비교하기 위해 equals 로 비교를 하였는데 false가 나와 당혹스러울 수있다는 것이다. 컴파일 및 런타임 에러가 아닌 단순하게 코드에 대한 논리 오류이기 때문에 찾기 어려울 수 있다. 

이를
1
2
3
4
Box_Generic<String> bg = new Box_Generic<>();
 
        bg.setItem("ABC");
        // bg.setItem(new Integer(1)); 에러 발생
cs

이렇게 쓴다면 컴파일 에러를 바로 잡을수 있고 실수할 확률도 적어진다. 즉 이로 인해 타입의 안정성을 제공한다.

이제 2 번째 장점을 살펴보자.

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

이러한 캐스팅 과정이 필요없어서 코드가 간결해진다고 한다.
하지만 이후에 보면 코드가 간결해진다고는 보기 힘들고 또 코드가 길어지며 이해하기 더욱 힘든 부분이 있다.


2. 제네릭스 주의 

제네릭스 사용함에 있어 주의사항이 있다.

1. static 
제네릭 클래스는 앞서 보듯이 객체별로 다른 타입의 객체를 만들 수 있다. 즉 따로 동작하게 된다.
따라서 동일하게 동작해야되는 static멤버 들은 당연히 타입 변수 T를 사용할 수 없다. 
어디서는 static String 이고 또 어디서는 static Integer 로 쓰여선 안되기 때문이다. 
즉 static T item  또는 static int eat(T t1 , T t2) {}  이러한 형식으로 쓰는것은 안된다.

2. new

타입 매개변수는 new로 생성할 수 없다. new 연산자는 컴파일 시점에 그 타입이 무엇인지 정확하게 알아야 하는데 T는 당시 클래스에서는 정해지지 않은 매개변수 이므로 사용할 수 없다.
클래스 내에서 T의 인스턴스를 만드는 방법은 있다. (https://www.thecodingforums.com/threads/how-to-create-an-instance-of-type-t.593591/ )


3. 제네릭 클래스 생성시 주의

 제네릭 클래스 객체를 생성시에 참조변수와 생성자에 대입된 타입이 반드시 일치해야 한다.
Box_Generic<Strng> bg = new BoxGeneric<Integer>();  // 에러 발생
Box_Generic<Strng> bg = new BoxGeneric<String>();  //  가능

이는 클래스의 상속 관계에서도 마찬가지이다. Parent  - Child 인 상황에서

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를 담을수 있다는것 만을 표시하는것이다)

)

즉 이러한 예기치 못한 상황이 발생할 수 있기 때문에 에러가 나는 것이다.


그렇다면 언제써야 되요? 라고 묻겠지


여기 좋은 그림이 있다.





위 그림은 상속 관계를 나타낸다.


즉  타입 매개변수가 상속관계에 있다해서 제네릭클래스도 그 상속관계를 이용해서 사용 할 수 없다는 것이다. 

즉 제네릭의 상속관계는 별개 이다. 허나 이를 사용할 수 있게 만들어주는것이 ? 이다.

즉 코드로 보면

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
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.setList(new ArrayList<B_Grade>());
        btr.setList(new ArrayList<C_Grade>());
        btr.setList(new ArrayList<D_Grade>());
    }
}
 
class Box_T_Random<T> {
 
    ArrayList<extends T> list;
 
    void setList(ArrayList<extends T> list) {
        this.list = list;
    }
}
 
 
cs


이런 형태로 쓸 수 있다는 것이다.

위 코드를

void setList(ArrayList<T> list) {

this.list = list;

}

이렇게 바꾸면 바로 에러가 뜰 것이다. T 는 위에서 A_Grade 를 뜻하지만 상속관계를 이용할 수없기 때문에 에러가 나는것 이다.


즉 이를 이용하면 이러한 형태를 짤 수 있다.


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.setList(testList);
        newList.add("ss");
        newList.add("tt");
        for (String string : newList) {
            System.out.println(string);
        }
    }
    @SuppressWarnings("unchecked")
    public <T> ArrayList<T> setList(List<?> 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)






이 댓글을 비밀 댓글로
    • 호이짜
    • 2018.07.29 00:17
    인터넷 뒤져보니 잘 모르는 부분에 대해서 http://happinessoncode.com/2017/05/21/java-generic-and-variance-1/ 여기에 잘정리되어있네요

    참고했음합니다.
    • Pizu
    • 2020.09.15 06:25
    설명이 매우 이상하네요.
    와일드카드를 캡쳐 메서드를 곁들여서 설명해야지 이런 식으로 설명하면 기초 열심히 했든 안했든 애매하게 해석될
    여지만 남네요.