티스토리 뷰

Java

Generics

LichKing 2016. 5. 29. 21:56

자바에 추가된지 참 오래된기술이지만 아직도 익숙치않은 기술이 하나있다.

Generics(제네릭)이 바로 그것인데 제네릭에대한 포스팅을 간략히 진행하려한다.

사실 제네릭이란 단어는 생소하더라도 초보개발자들도 당연하게 사용하고있는 부분이있는데 콜렉션프레임워크에서 타입을 제한할때 주로 사용하고있다.


List<Integer> numbers = new ArrayList<Integer>();


ArrayList는 선형 자료구조로 배열과 유사한 자료구조이다. 배열과 다른점은 동일한 자료형만 관리할 수 있다는 점과 크기변환이 자유롭지않다는 것이다.

제네릭을 사용하지않을때는 이런식으로 사용한다.


List numbers = new ArrayList();


numbers.add(100);

numbers.add(200);


Object element = numbers.get(0);

Integer number = null;


if( element instanceof Integer ){

number = (Integer) element;

}


System.out.println(number);


ArrayList 객체를 만들고 Integer타입의 값을 집어넣은 후 다시 꺼내는 별거없는 소스다.

이 자료구조에는 Integer 타입의 값만 들어가고있지만 get() 메서드가 항상 Object를 반환하기때문에 Integer 변수에 값을 저장하려면 타입체크하는 코드가 빠질수가없다. 또한 이 리스트가 Integer 타입만 들어가리란걸 보장할 수가없다. add() 메서드에 문자열을 집어넣어도 아무런 문제없이 값이 들어가게된다.

제네릭은 리스트에 들어갈 타입을 제한하여 원치 않는 타입의 값이 들어가는 경우엔 컴파일에러를 발생하고, 값을 꺼내올때는 자동으로 형변환을 처리해주어 반복적인 형체크, 형변환 코드를 없애주는 역할을 한다.

사실 이경우만 놓고보면 ArrayList 가 배열보다 나은점중 하나는 타입을 가리지않는다는 점인데, 타입을 고정해버리면 배열과 같아지는게 아닌가.. 하는 생각을 할수도있지만 ArrayList는 동일타입을 다루는 경우가 많기때문에 코드의 중복을 제거하는게 훨씬 효율성이 높다.


List<Integer> numbers = new ArrayList<Integer>();


numbers.add(100);

numbers.add(200);


int number = numbers.get(0);


1. 제네릭 클래스

ArrayList 클래스가 관리하는 객체의 타입을 Integer로 고정했기때문에 값을 꺼내올때 타입을 체크하는 코드가 필요하지않게된다.

사용법은 이정도로만 하고 그럼 클래스를 어떻게 구현하는지 살펴보자


class MyList<T>{
Object[] elements;

public MyList(){
this.elements = new Object[10];
}

public MyList(int arraySize){
this.elements = new Object[arraySize];
}

public boolean add(T element){
this.elements[0] = element;
return true;
}

public T get(int index){
return (T) this.elements[index];
}
}

(헐...코드를 그대로 복사해오면 코드하이라이트기능이 유지되는걸 이제 처음알았다...)


아주 간단한 리스트 구현체다. 근본없이 작동하는거니 리스트 구현에는 크게 신경쓰지말자. 먼저 사용할때와 마찬가지로 <>연산자를 이용해서 구현한다. <T>는 타입에 대한 '변수' 다. 정식명칭은 타입 파라미터(Type Parameter, 형인자)이다. 변수이기때문에 굳이 T일 필요는 없다.

보통 T를 쓰는이유는 Type의 약자를 따온것이고 ArrayList같은 경우는 E(element), HashMap 같은경우는 K, V(Key, Value) 인것처럼 의미있는 명칭을 사용하니 적절히 사용하자.


MyList 클래스의 사용법은 알고있는대로 사용하면 된다.

public static void main(String[] args) {
MyList<Integer> myList = new MyList<>();

myList.add(100);
System.out.println(myList.get(0));

myList.add("hello"); // 제네릭으로 제한을 건 Integer이외의 타입은 컴파일 에러 발생!
}

그냥 사용할줄만 알았던 상황에서 이제 간단히 클래스에 구현까지 하게되었다.

제네릭은 이런식으로 클래스단위에 적용이 가능하며, 메서드에도 부여가 가능하다. 개인적으로는 이때부터 좀 멘탈이 붕괴됐다.


2. 제네릭 메서드


class MyList{
Object[] elements;

public MyList(){
this.elements = new Object[10];
}

public MyList(int arraySize){
this.elements = new Object[arraySize];
}

public <T> boolean add(T element){
this.elements[0] = element;
return true;
}

public <T> T get(int index){
return (T) this.elements[index];
}
}

기존에 MyList 클래스 자체에 제네릭을 부여했던것을 add() 메서드와 get() 메서드에 메서드레벨로 부여했다. 클래스에 부여할때와 마찬가지로 반환타입앞에 <T> 형태로 부여가 가능하다. 나처럼 그냥 콜렉션프레임워크를 사용할때만 간간히 제네릭을 사용하는 천민들에게 메서드레벨의 제네릭은 상당히 생소한데 사용법도 똑같이 사용이 가능하다.


public static void main(String[] args) {
MyList myList = new MyList();

myList.<Integer>add(100);
Integer num = myList.<Integer>get(0);

System.out.println(num);
}

제네릭 메서드를 공부하다보니 메서드의 제네릭이 생소했던 이유가 있었다. 클래스에 제네릭을 사용할 경우 객체 생성시 타입 파라미터 혹은 다이아몬드 연산자가 코드에 명시적으로 등장하지만 제네릭 메서드는 몇몇 추론이 불가능한 경우를 제외하고는 다이아몬드 연산자까지도 필요없는 타입 추론을 지원하고있다. 때문에 제네릭 메서드를 호출하는 쪽에서는 이게 제네릭 메서드인지도 알 수가없는 것이다. 예제 코드에서는 예제를 위해 메서드에 타입 파라미터를 Integer로 명시했지만 제네릭을 지워도 코드는 정상적으로 작동한다.


주로 ArrayList로 설명을 하고있는데 콜렉션의 타입을 제한할때 제네릭을 사용하지만 타입을 제한하고싶지 않을때도 있을것이다. Integer도 받고 String도 받아야할때가 있을수있다. 제네릭 클래스는 기본적으로 제네릭을 지원하지않는 예전 코드와의 호환성을 위해 제네릭 없이도 여전히 사용이 가능하다. 그럼 타입제한을 원치않을땐 제네릭없이 사용하면될까?


public static void main(String[] arg){
List list = new ArrayList();
}

(제네릭 없이도 여전히 사용가능하다.)


3. 와일드 카드

제네릭 클래스를 제네릭없이 사용하는건 어디까지나 예전코드와의 호환성때문이다. 현재 사용중인 jdk가 1.5미만이 아니라면 제네릭 클래스를 제네릭없이 쓰는건 지양해야한다. 참고로 저 코드는 컴파일이 되기는 하지만 warnning이 뜨게된다.


public static void main(String[] arg){
List<Object> list = new ArrayList<>();
}

타입제한이 필요치 않을땐 이런식으로 사용하자. 모든 클래스의 조상인 Object로 타입제한을 걸면 모든 객체를 받을 수 있다.


메서드의 파라미터로 제네릭 클래스를 받을때도 제네릭을 사용할 수 있는데 들어오는 타입이 일정하지않은 경우도 존재할 수 있다.


public static void main(String[] arg){
List<String> list = new ArrayList<>();
method01(list);
}

public static void method01(List<String> stringList){

}

(항상 String 타입만 받는게 아닐 수도 있다.)

참고로 제네릭을 Object로 제한하면 List에 들어가는 객체는 타입을 따지지않지만 List<String> 타입이 List<Object> 타입의 하위형이 되는건 아니다. 파라미터를 List<Object>로 선언해놓는다고 List<String>을 인자로 보낼수있는거 아니라는 뜻이다.


public static void main(String[] arg){
List<String> list = new ArrayList<>();
method01(list); //컴파일 에러
}

public static void method01(List<Object> stringList){

}

(컴파일 에러 발생!)

이를 어려운말로 제네릭은 불변타입(Invariant)이라고 한다. 저런경우엔 어떤식으로 해결이 가능할까? 이번에야말로 제네릭을 제외하고 써야하나? 라고 생각이 들 수도있다.

하지만 누누히 말하듯 1.5이상에서는 그어떤 이유라도 제네릭클래스를 제네릭없이 사용하는건 지양해야한다. 제네릭으로 안될거같으면 코드양이 좀 더 길어지더라도 되게끔 수정해야한다. 하지만 지금 이런상황은 코드양이 길어지지도않는 해결법이 있다.


제네릭에서 와일드카드라는 것을 지원하는데 말 그대로 만능 해결자다.

public static void main(String[] arg){
List<String> list = new ArrayList<>();
method01(list);
}

public static void method01(List<?> stringList){

}

<?>가 와일드카드인데 어떤 타입도 다 받을 수 있게된다.

<?>만 사용하게되면 정말 어떤 타입이든 가리지않고 다 받게되어 이런식으로 작성하는것을 비한정적 와일드카드라고 표현한다. 비한정적이라는건 한정적도 있을거란얘기일것이다.

String 클래스의 하위 클래스만 인자로받겠다고하면 이런식으로 한정지을 수 있다.


public static void main(String[] arg){
List<String> list = new ArrayList<>();
method01(list);
}

public static void method01(List<? extends String> stringList){

}

(굳이 첨언하자면 String은 확장이 불가능한 클래스이기때문에 이 메서드엔 String 제네릭밖에 못들어온다.)


예리한 사람들이라면 여기서 한가지 궁금증이 들것이다.

public static <T> void method1(List<T> list) {

}

public static void method2(List<?> list) {

}

타입 파라미터를 사용한 제네릭 메서드와 와일드 카드를 사용한 제네릭 메서드이다. 둘다 어떤 타입의 제네릭 타입이든 받아줄 수 있다.

상당수의 제네릭 메서드가 둘중 어떤 방법을 써도 문제없이 작동하는걸 볼 수 있는데 제네릭 코드가 메서드 선언부에만 등장한다면 와일드 카드를 쓰는것이 좀 더 간단하다. 하지만 제네릭 코드가 메서드 구현부에도 등장한다면 타입 파라미터를 이용하는것이 좀 더 타입안정성을 확보해줄수 있다. 개인적으로는 최대한 타입 파라미터를 활용하는 것을 선호한다.

public static <T> void method1(List<T> list) {
for (T t : list) {

}
}

public static void method2(List<?> list) {
//와일드카드는 Object로 돌려야하기때문에 제네릭의 이점을 살리기가 힘든경우가 있다.
for (Object obj : list) {

}
}


제네릭을 잘 사용하면 파라미터 타입이나 반환타입에 구애받지않는 매우 유연한 코드작성이 가능하다.

제네릭을 잘 사용하기위한 노력이 더 필요할듯하다.

'Java' 카테고리의 다른 글

Java8#01. 람다 표현식(Lambda Expression)  (25) 2016.10.02
자바8 용어정리  (1) 2016.09.19
Generics  (7) 2016.05.29
DesignPattern#04 Decorator Pattern  (0) 2016.03.10
DesignPattern#03 Template Callback Pattern  (0) 2016.03.04
DesignPattern#02 Template Method Pattern  (0) 2016.03.02
댓글
댓글쓰기 폼