티스토리 뷰

Daylogs/Java

Generic 소개

ohgyun 2009. 1. 19. 23:58

http://cafe.naver.com/sqler.cafe?iframe_url=/ArticleRead.nhn%3Farticleid=830



java Generics 1 - 소개

이 글은 http://java.sun.com/j2se/1.5/pdf/generics-tutorial.pdf 에 대한 손을 좀 본 번역본입니다. 손을 봤다는 게 직역하지 않고, 뺄 거는 빼고 더할 거는 더하고 풀어 쓸 거는 풀어 쓰고 했단 소립니다. 사실 원본이 말이 쓸데없이 많아서 많이 짤라냈습니다.


자바 1.5에서는 자바 언어에 대한 몇가지 확장 기능이 추가되었다. 그 중 하나가 Generics이다.
이 글은 generics를 소개하기 위한 글이다. C++의 템플릿과 같은 다른 언어와 비슷하다.
Generics는 타입에 대한 추상성을 제공한다. Collection에서 쓰이는 컨테이너 타입이 가장 일반적인 예가 될 것이다.
예전에 작성하던 일반적인 코드는 다음과 같다.

List myIntList = new LinkedList(); // 1
myIntList.add(new Integer(0)); // 2
Integer x = (Integer) myIntList.iterator().next(); // 3


3 번째 줄에서 캐스팅을 하는 것은 다소 번거롭다. 대개의 경우 프로그래머는 어떤 리스트에 어떤 타입의 데이터가 들어갈 것인지 알고있지만, 캐스팅은 필수적인 작업이다. 컴파일러는 iterator에 의해 Object 가 리턴될 것이라는 것까지 밖에 보장하지못한다. Integer 타입이란 것을 명확히 하기 위해서는 캐스팅을 할 수 밖에 없다. 물론, 캐스팅 작업은 프로그램을난잡하게할 뿐만 아니라, 런타임 에러의 가능성을 발생시킨다.
만약 프로그래머가 만들고자 하던 의도대로 리스트에 특정한 타입만 들어갈 수 있도록 강제할 수 있다면 얼마나 좋겠는가! 이것이 바로 generics의 핵심 아이디어다. generics를 이용한 프로그램의 예는 다음과 같다.

List<Integer> myIntList = new LinkedList<Integer>(); // 1’
myIntList.add(new Integer(0)); //2’
Integer x = myIntList.iterator().next(); // 3’


myIniList 변수에 타입에 대한 정의를 했다는 것이 중요하다. List<Integer>라고 명확하게 적어줌으로써 이 인스턴스가아무 타입이나 들어갈 수 있는 리스트가 아니라 Integer 리스트라는 것을 명확하게 한다. 이런 경우 List는타입인자(예제의 경우는 Integer)를 받는 generic 인터페이스라한다. list 객체를 생성하는 시점에 타입인자를 명확히해준다.
또 한가지 주의할 점은 3'에서 보는 것과 같이 타입 캐스팅이 사라졌다는 것이다.
3 번째 줄에서 타입캐스팅을 하는 대신 1'에서 타입 파라미터로 Integer를 설정해 줌으로 인해 프로그램이 한결 간결해졌다. 매우 중요한 차이는컴파일러가 이제 타입을 체크할 수 있기 때문에 인자의 정확성을 컴파일 타임에 알 수 있게 되었다는 것이다. myIntList가List<Integer>로 정의되었을 경우 그게 언제 어디서 쓰이던 타입 안정성에 대해 컴파일러로 부터 보장받을 수있다. 즉, 타입이 명확하지 않으면, 컴파일이 되지 않는다. 또한 이렇게 코딩함으로 개발자는 그 인스턴스에 들어가는 타입을 더직관적으로 알 수 있다.


java Generics 2 - 간단한 Generics 정의하기

다음은 java.util 패키지에 있는 List와 Iterator의 선언부를 발췌한 것이다.

public interface List<E> { 
    void add(E x);
    Iterator<E> iterator();
}
public interface Iterator<E> { 
    E next();
    boolean hasNext();
}
<> 안에 들어간 요소를 제외하면, 위의 코드는 유사하다. 이게 List와 Iterator에서 타입인자를 정의하는 방법이다.
타입인자는 정해진 타입을 사용하는 generic의 선언부를 통해 정의할 수 있다.(여기에는 몇 가지 제한 사항이 있으며, 7장을 참고하라.)
List<Integer> 와 같은 List의 generic 방식의 선언을 이미 살펴보았다. parameterized type 이라고 불리는 이런 방식의사용법은 일반적인 타입인자(이 경우는 E라고 정의된 부분) 에서 모두 사용되며, 사용시에 특정한 타입(이 경우에는Integer)으로 바뀔 수 있다.
다시 말해 List<Integer>는 위의 코드에서 E라고 정의된 부분이 Integer로 바뀐 것이라고 생각하면 된다.

public interface IntegerList { 
    void add(Integer x)
    Iterator<Integer> iterator();
}


이렇게 쓰는 것은 매우 직관적이지만, 오해의 소지가 좀 있다.
parameterized type인 List<Integer>는 이런 식의 확장의 경우 굉장히 명확해 보인다. 그러나, 이런 식으로 인터페이스가 선언되어 있다면, 확장을 할 수가 없다.
generic type의 선언은 한번만 컴파일 되면, 아무데서나 쓸 수 있다. 즉, 다른 일반적인 클래스나 인터페이스와 같이 하나의 클래스 파일만 생기게 된다.

타 입인자는 일반적인 메쏘드나 생성자에서 사용하는 보통 인자들과 비슷하다. 메쏘드에서 사용하는 일반적인 인자와 마찬가지로generic은 선언부에 타입인자를 정의한다. 메쏘드가 호출되면, 실제값으로 그 인자가 치환되어 메쏘드의 내부가 실행되는 것과마찬가지로 generics의 선언이 실행되면 실제 인자는 타입 파라미터 값을 대체하게 된다.

이름을 짓는 데는 몇가지 약정이 있다. 타입인자는 어떤 의미를 내포하는(가능하면 한 글자로) 하기를 권고한다. ( Map의 경우 key, value시스템이기 때문에 <K, V> 라고 표현하고 있다.) 또 소문자를  타입인자로 사용하지 말아야 일반적인 클래스나인터페이스에서 쓰이는 인자와 명확하게 구분할 수 있다. element라는 의미로 많은 경우 E를 사용하는 게 좋은 예이다.



java Generics 3 - Generics과 하위 타입

아래 코드를 보자.

List<String> ls = new ArrayList<String>(); //1
List<Object> lo = ls; //2


첫 번째 줄은 당연히 맞다. 두 번째 줄이 문제다. 다시 정리하면 다음과 같은 문제가 된다. String의 리스트는 Object의 리스트가 될 수 있는가? 보통은 "당근 빠따다!"라고 생각할 거다.
그럼 추가적으로 다음과 같은 코드가 있을 때는?

lo.add(new Object()); // 3
String s = ls.get(0); // 4: Object를 String에 대입하려고 한다!


Stirng 의 리스트인 ls를 Object 리스트인 lo에 대입하고, lo에 Object를 넣었다. 그래서 lo에는 이제 String만들어있는 게 아니다. 위와 같은 문제를 방지하기 위해 컴파일러는 두 번째 줄에서 에러를 발생시킨다.

일반적으로 Foo가 Bar의 하위 타입이고 G는 어떤 generic 타입일 경우 G<Foo>는 G<Bar>의 하위 타입이 아니다.



java Generics 4 - Wildcards

collection의 요소들을 루프를 돌면서 찍는 코드를 생각해보자. 이전 버전에서는 다음과 같은 코드가 될 것이다.

void printCollection(Collection c) { 
    Iterator i = c.iterator();
    for (k = 0; k < c.size(); k++) { 
        System.out.println(i.next());
    }
}

다음은 generics을 이용한 새로운 방법이다.

void printCollection(Collection<Object> c) { 
    for (Object e : c) { 
        System.out.println(e);
    }
}


문 제는 이런 새로운 방식이 이전 방식에 비해 별로 쓸모가 없다는 것이다. 예전 방식의 코드는 모든 종류의 collection을 쓸수 있었지만, 새로운 방식은 Collection<Object>만 쓸 수 있다. chap 3에서 본 것과 같이Collection<Object>는 모든 Collection의 상위 타입이 아니다.

다시 잠시 정리를 하고 넘어가면.

void foo(Collection<Object> arg){

}
와 같이 선언된 메쏘드에 대해

foo(Collection<String>타입의 머시기)

와 같은 방법으로 호출할 수 없다는 것이다.

그러면, 모든 타입의 collection에 대한 상위타입은 무엇일까? Collection<?>라고 쓰면 된다. 이를 wildcard 타입이라 부른다. wildcard 타입을 이용한 메쏘드는 다음과 같이 쓰면 된다.

void printCollection(Collection<?> c) { 
    for (Object e : c) { 
        System.out.println(e);
    }
}

이 렇게하면 모든 타입의 collection에 대해서 이 메쏘드를 사용할 수 있다. printCollection() 메쏘드 안을 잘보면 여전히 인자를 Object 타입으로 사용하는 것을 볼 수 있다. 이렇게 쓰는 것은 항상 안전하다.
그러나 다음과 같은 코드는 문제가 있다.

Collection<?> c = new ArrayList<String>();
c.add(new Object()); // 컴파일 에러!

c 가 가지는 요소의 타입이 무엇인지 불확실하기 때문에 Object 인스턴스를 추가할 수가 없다. add() 메쏘드는 인자로 E라는인자 타입을 가지는데, 이 경우 ?로 정의되어 있기 때문에 여기서는 여전히 알 수 없는 타입이기 때문이다. 이런 경우add()에 들어갈 수 있는 유일한 값은 null 뿐이다.
반대로 List<?>에서 get()을 생각해보자. 리스트의 타입이 무엇인지 알 수 없지만, Object의 하위 타입이란 건 알고 있다. 그래서 이런 경우 get()은 Object 타입을 리턴한다.

제한된 wildcards

사각형과 원과 같은 어떤 모양을 그리는 간단한 애플리케이션을 생각해보자. 프로그램에서 이런 모양을 표현하기 위해서는 다음과 같은 상속관계가 정의될 것이다.

public abstract class Shape { 
    public abstract void draw(Canvas c);
}
public class Circle extends Shape { 
    private int x, y, radius;
    public void draw(Canvas c) { ... }
}
public class Rectangle extends Shape { 
    private int x, y, width, height;
    public void draw(Canvas c) { ... }
}


이들을 그리기 위한 캔버스는 다음과 같다.

public class Canvas { 
    public void draw(Shape s) { 
        s.draw(this);
    }
}

Shape들을 한번에 그리기 위해서는 다음과 같은 메쏘드가 있을 법하다.

public void drawAll(List<Shape> shapes) { 
    for (Shape s: shapes) { 
        s.draw(this);
    }
}

drawAll() 은 정확히 Shape의 리스트에 의해 호출된다. 따라서 List<Circle>과 같은 것은 인자로 넣을 수 없다.즉, 인자로 들어가는 것은 Shape의 하위객체까지 들어갈 수 있어야 한다. 이런 경우 메쏘드는 다음과 같이 정의되면 된다.

public void drawAll(List<? extends Shape> shapes) { ... }


List<Shape>가 List<? extends Shape>로 바뀐 것이 포인트다. 이렇게 하면 List<Circle> 타입도 인자로 사용할 수 있다.
List<?extends Shape> 이 제한된 wildcard의 예제다. ?는 불분명한 타입을 표현하는 것인데, ?가 Shape의하위타입으로 제한을 하는 것이다. 이런 경우 Shape는 wildcard의 upper bound라 부른다.
wildcard의 유연함을 사용하는 데는 대가가 따른다. 메쏘드 안에서 shapes라는 인자에 추가하는 것은 안 된다. 예를 들면 다음과 같은 코드는 안 된다.

public void addRectangle(List<? extends Shape> shapes) { 
    shapes.add(0, new Rectangle()); // compile-time error!
}


이 메쏘드를 호출하는 시점을 생각해보자. 인자로 List<Circle>이 들어올 수도 있다. 그렇기 때문에List<? extends Shape>로 선언된 변수에 Shape의 하위 클래스인 Rectangle을 넣을 수 없다.


다음은 허용된다. (예제에서 Driver는 Person의 하위타입이다.)

public class Census { 
    
public static void
        addRegistry(Map<String, ? extends Person> registry) { ...}
}...


Map<String, Driver> allDrivers = ...;
Census.addRegistry(allDrivers);





java Generics 5 - Generic 메쏘드

어떤 array의 모든 요소를 Collection에 추가하는 메쏘드를 만들고자 한다.
첫번째 생각은 이러하다.

static void fromArrayToCollection(Object[] a, Collection<?> c) { 
    for (Object o : a) { 
        c.add(o); // 컴파일 에러!
    }
}


자, 이제 초보자들이 흔히 하는 실수인 Collection<Object>를 메쏘드에 인자로 쓰는 것과 같은 실수를 피하는법을 알아보자. 역시 Collection<?>도 이 경우 통하지 않는다. 이런 경우에 사용하는 것이 바로generic 메쏘드다. 타입 정의와 마찬가지로 메쏘드 정의에서도 generic을 사용할 수 있다.

static <T> void fromArrayToCollection(T[] a, Collection<T> c) { 
    for (T o : a) { 
        c.add(o); // 옳다
    }
}


이제 array 요소의 상위타입으로 구성된 collection을 이용하여 이 메쏘드를 쓸 수 있다.

Object[] oa = new Object[100];
Collection<Object> co = new ArrayList<Object>();
fromArrayToCollection(oa, co);// T는 Object
String[] sa = new String[100];
Collection<String> cs = new ArrayList<String>();
fromArrayToCollection(sa, cs);// T는  String
fromArrayToCollection(sa, co);// T는 Object
Integer[] ia = new Integer[100];
Float[] fa = new Float[100];
Number[] na = new Number[100];
Collection<Number> cn = new ArrayList<Number>();
fromArrayToCollection(ia, cn);// T는 Number
fromArrayToCollection(fa, cn);// T는 Number
fromArrayToCollection(na, cn);// T는 Number
fromArrayToCollection(na, co);// T는 Object
fromArrayToCollection(na, cs);// 컴파일 에러

실제 인자가 어떤 타입인지 메쏘드를 호출할 때 넘겨주지 않는다는 것에 유의하자. 컴파일러가 넘어오는 인자에 따라 알아서 다 처리해준다. 
여기서 한 가지 의문점이 생긴다. 어떤 때 generic 메쏘드를 사용할 것이며, 어떤 때 wildcard 타입을 사용할 것인가? 다음의 Collection 라이브러리를 살펴보자.

interface Collection<E> { 
    public boolean containsAll(Collection<?> c);
    public boolean addAll(Collection<? extends E> c);
}


이것을 generic 메쏘드를 사용하면 다음과 같이 된다.

interface Collection<E> { 
    public <T> boolean containsAll(Collection<T> c);
    public <T extends E> boolean addAll(Collection<T> c);
    //타입 변수도 정해져야 한다구!
}


그 러나 conatainsAll과 addAll 모두 타입인자 T는 한번만 사용된다. 리턴 타입은 타입인자에 무관하며, 메쏘드의인자와도 상관없다. 즉 여기서 인자의 타입은 다형성을 위해 사용된 것이다. 다시 정리하면, generic 메쏘드는 두 개 이상의인자 사이의 타입 연관성을 위해 정의된 것이다. 또, wildcard는 인자의 하위유연성을 위해 만들어졌다.
물론, generic 메쏘드와 wildcard를 동시에 사용하는 케이스도 있을 수 있다. 다음은 Collections.copy() 메쏘드 이다.

class Collections { 
    public static <T> void copy(List<T> dest, List<? extends T> src){...}
}


두개의 파라미터 간의 연관성을 살펴보자. 원본 array인 src로부터 복사된 객체 dest는 반드시 T로 할당될 수 있어야 한다. 그러므로, src의 타입은 반드시 T의 하위 타입이어야 한다.
위의 선언부를 wildcard를 쓰지 않고 정의하는 방법도 있다.

class Collections { 
    public static <T, S extends T>
        void copy(List<T> dest, List<S> src){...}
}


첫 번째 타입인자 T는 첫 번째 메쏘드 인자의 선언부와 두 번째 타입인자 S를 정의하기 위한 부분에 쓰였다. S는 src의 타입을정의하기 위해 한 번만 쓰였다. 즉 S는 두 번째 타입인자를 정의하기 위한 곳 외에는 쓰이지 않았다. 다른 곳에 쓰이지 않았기때문에 S는 wildcard로 변환될 수 있다. wildcard를 쓰는 것이 훨씬 더 명확하다. wildcard를 썼다는 것은이것이 다른 용도로 사용되지 않는다는 것을 명확히 해주기 때문이다.
wildcard는 메쏘드 선언부 밖에서 멤버 변수나 array 등의 타입을 정의할 수 있다는 이점도 있다.

section 4 에서 보았던 Shape를 그리는 문제로 돌아가보자. 그리기 전에 인자로 넘어온 Shape의 리스트의 리스트를(List가 두 겹이다.) static 멤버변수에 저장하는 상황을 가정하자.

static List<List<? extends Shape>> history =
new ArrayList<List<? extends Shape>>();
public void drawAll(List<? extends Shape> shapes) { 
    history.addLast(shapes);
    for (Shape s: shapes) { 
        s.draw(this);
    }
}


이 런 경우 T와 같이 쓰지 않고, wildcard를 사용하는 것이 바람직하다. T를 재사용할 필요가 없기 때문이다. 또 위와 같이여러 개의 ?들끼리 굳이 구별을 할 필요가 있다면 ?대신 T,S와 같은 파라미터 변수를 사용하면 된다. 위와 같이 중첩된 경우도마찬가지다.



java Generics 6 - 기존 코드와의 호환

지금까지는 generic을 지원하는 버젼에 대해서만 알아보았다. 하지만 이미 기존에 만들어진 소스가 있을 것이다. 기존에 짜여진 모든 소스 코드를 수정하는 것은 있을 수 없는 일이다.
10 장에서 기존코드를 generic을 사용하는 새로운 코드로 바꾸는 것에 대해 알아보도록 하겠다. 이번 장에서는 훨씬 간단한 문제인기존 소스 코드와의 호환성에 대해서만 알아볼 것이다. 이것은 두 부분으로 나뉘어 진다. 기존 코드를 generic이 사용된코드에서 사용하는 것과 generic이 들어간 코드를 기존 코드에서 사용하는 것이다.

기존 코드를 generic 코드에서 사용하기.

generic의 장점을 충분히 살리면서 기존 코드를 사용하는 법을 알아보겠다.
예제로 com.Fooblibar.widgets 패키지를 사용하고자 한다 치자.(Fooblibar가 뭐 하는 건지는 알 필요 없으므로 생략!)

package com.Fooblibar.widgets;
public interface Part { ...}
public class Inventory { /**
* Adds a new Assembly to the inventory database.
* The assembly is given the name name, and consists of a set
* parts specified by parts. All elements of the collection parts
* must support the Part interface.
**/
    public static void addAssembly(String name, Collection parts) {...} 
    public static Assembly getAssembly(String name) {...}
}
public interface Assembly { Collection getParts(); // Returns a collection of Parts
}


이 제 위에서 소개한 API를 사용하는 코드를 짠다고 가정하자. addAssembly() 메쏘드를 호출할 때 적절한 인자로 호출하는게 바람직할 것이다. 그러니까 Collection<Part>라고 정의된 generic type이 명시적인 인자로호출을 하고자 한다.

package com.mycompany.inventory;
import com.Fooblibar.widgets.*;
public class Blade implements Part {
...
}
public class Guillotine implements Part {
}
public class Main { 
    public static void main(String[] args) { 
        Collection<Part> c = new ArrayList<Part>();
        c.add(new Guillotine()) ;
        c.add(new Blade());
        Inventory.addAssembly(”thingee”, c);
        
Collection<Part> k = Inventory.getAssembly(”thingee”).getParts();
    }
}

addAssembly 를 호출할 때 두 번째 인자는 Collection 타입이다. 그리고 실제로 넘기는 인자는Collection<Part> 타입이다. 어떻게 작동할까? 컴파일러는 Collection이 어떤 타입으로 지정될지모른다.
Collection과 같은 generic type이 타입인자 없이 호출 될 때는 row-type으로 호출된다.
이미 살펴봤던 것처럼 Collection은 Collection<?>의 의미이지 Collection<Object>를 의미하는 게 아니다라고 생각할 것이다.
그러나 위의 코드를 보면 별로 그런 것 같지도 않다. 
Collection<Part> k = Inventory.getAssembly(”thingee”).getParts();
가 정상적으로 작동하고 있기 때문이다. Collection<?>은 Collection<Part>로 명시적인캐스팅없이 할당이 불가능하다. 즉, Collection<?>로 호출이 되었다면 에러가 났어야 마땅하다.
이 러한것은 정상적으로 작동한다. 다만 "unchecked warning"(@로 시작하는 annotation 중 하나) 이 없다면,warning이 생긴다. 컴파일러는 정합성을 보장해 줄 수 없기 때문이다. getAssembly()라는 기존에 이미 존재하던메쏘드가 Collection<Part>를 리턴할 지를 체크할 방법이 없다. 위에서 사용한 코드에서는 단지Collection이라고만 되어 있고, 그런 경우 모든 object를 사용할 수 있게 하는 방법뿐이다.
이론적으로만 봤을 때 이건 별로 합당치 못하지만, 기존에 쓰던 코드를 버리라고 할 수는 없다. 이건 개발자의 몫이다.
raw type이란 결국 wildcard와 매우 흡사하지만, raw type은 타입체크를 하지 않는 것이다. 이것은 기존 코드와의 호환성을 고려한 결정이다.
generic 코드에서 기존의 코드를 호출하는 것은 위험하다. 타입에 대한 안정성을 어떻게도 보장할 수 없기 때문이다.


Erasure 와 Translation

public String loophole(Integer x) { 
    List<String> ys = new LinkedList<String>();
    List xs = ys;
    xs.add(x); // 컴파일 시점 unchecked warning
    return ys.iterator().next();
}


String 리스트를 옛날 방식의 리스트에다가 집어 넣으려 한다. 컴파일 시점에 warning이 뜬다.
이번에는 Integer를 리스트에 넣고 String으로 뽑아 쓰려는 경우를 보자. 물론, 잘못된 시도다. Warning을 무시하고 일단 실행시켜보면 잘못된 타입으로 뽑아 쓸 때 에러가 난다.

public String loophole(Integer x) { 
    List ys = new LinkedList();
    List xs = ys;
    xs.add(x);
    return (String) ys.iterator().next(); // 런 타임 에러.
}


리스트로 부터 요소를 뽑아와 String으로 캐스팅을 하려고 하면 ClassCastException이 발생하게 된다.
컴파일러는 erasure라는 generics에 대한 첫 단계 변환을 실행한다. 소스 코드 차원에서의 변환이라고 생각하면 대략 맞다. 이는 generic을 제거하는 작업이라고 볼 수 있다.
erasure는 generic type 정보를 전부 제거해버리는 작업이다. List<String>을 List로 바꿔버리는 것처럼 <> 사이에 감싸진 모든 정보를 날려버린다.

기존 코드에서 generic 코드 사용하기


반대의 경우를 생각해보자. Fooblibar.com의 API를 generics를 사용하도록 바꿔보자. 그러나 그 API를 쓰는 코드는 아직 generics를 사용하지 않는다고 하면 아래와 같은 코드가 나올 것이다.

package com.Fooblibar.widgets;
public interface Part { ...} 
public class Inventory { 
    /**
    * Adds a new Assembly to the inventory database.
    * The assembly is given the name name, and consists of a set
    * parts specified by parts. All elements of the collection parts
    * must support the Part interface.
    **/    
    public static void addAssembly(String name, Collection<Part> parts) {...} 
    public static Assembly getAssembly(String name) {...}
}
public interface Assembly { 
    Collection<Part> getParts(); // Returns a collection of Parts
}


그걸 사용하는 코드는 다음과 같을 것이다.

package com.mycompany.inventory;
import com.Fooblibar.widgets.*;
public class Blade implements Part {
}
public class Guillotine implements Part {
}
public class Main { 
    public static void main(String[] args) { 
        Collection c = new ArrayList();
        c.add(new Guillotine()) ;
        c.add(new Blade());
        Inventory.addAssembly(”thingee”, c); // 1: unchecked warning
        Collection k = Inventory.getAssembly(”thingee”).getParts();
    }
}


위 의 코드는 generics가 나오기 전에 짜여졌으며, com.Fooblibar.widgets와 collection 라이브러리(둘다 generics를 쓴다) 를 사용하고 있다. 위의 코드에서 generics가 사용되어야 할 부분은 전부 raw type으로정의되어 있다.

첫번째 줄에서 Collection<Part>를 사용해야하는데 raw type의Collection이 호출되므로 unchecked warning이 발생한다. 컴파일러 입장에서는 이 Collection이Collection<Part>라고 확인할 방법이 없다.




java Generics 7 - Fine Print

Generic 클래스는 그 클래스의 모든 객체에 공유된다.

다음 코드는 무엇을 출력할까?

List <String> l1 = new ArrayList<String>();
List<Integer> l2 = new ArrayList<Integer>();
System.out.println(l1.getClass() == l2.getClass());


false가 출력될 거라 생각하겠지만 틀렸다. true를 출력한다. 모든 generic 클래스의 인스턴스는 실제 타입이 무엇이건 간에 같은 run-time 클래스를 가지게 된다.
generic이냐 아니냐를 결정하는 것은 타입 인자에 의한 것인데, 어떤 타입인자를 쓰건 그 클래스가 다른 것이 되진 않는다.
static 변수나 메쏘드는 그 클래스의 모든 인스턴스에 공유된다. 따라서 static 메쏘드나 static initializer에서타입인자를 지정하는 것은 안 되며, static 변수의 선언부나 initializer에 사용할 수도 없다. 다음 코드는 에러투성이다.

public class Test<E> {
    public static List<E> a = null; 
    static{
        List<E> b = null;
    }
    public static List<E> c = null;
    public static List d = new ArrayList<E>();
}


Cast 와 instanceOf

특정 타입인자로 구성된 인스턴스가 타입인자를 포함한 generics의 클래스의 인스턴스인지 체크하는 것은 허용되지 않는다.

Collection cs = new ArrayList<String>();
if (cs instanceof Collection<String>) { ...} // 안된다!


마찬가지로 다음 코드도 문제가 있다.

Collection<String> cstr = (Collection<String>) cs; // unchecked warning


여기서는 unchecked warning이 발생하는데, 이건 런타임 시스템에서 체크할 수 있는 게 아니기 때문이다.
다음 코드도 마찬가지다.

<T> T badCast(T t, Object o) {
    return (T) o; // unchecked warning
}


run time에 타입 변수라는 것은 없다. 이는 시간적으로나 공간적으로 과부하를 일으키지 않는다는 것이다. 이런 점은 바람직하지만, 믿을만한 캐스팅을 할 수는 없다.

Arrays

wildcard 타입을 제외하고는 generics의 array를 만들 수 없다. 아래 코드를 보자.

List<String>[] lsa = new List<String>[10]; // 이건 안 되지!
Object o = lsa;
Object[] oa = (Object[]) o;
List<Integer> li = new ArrayList<Integer>();
li.add(new Integer(3));
oa[1] = li; // 말도 안 되지만, run time에는 무사통과
String s = lsa[1].get(0); // run-time error - ClassCastException


만 약에 parameterized type이 허용된다면 위와 같은 문제가 생길 수 있고, unchecked warning도 없이컴파일은 성공하고, run time에 에러가 발생할 것이다. generics를 사용하면 uncheck warning 없이컴파일이 잘 되었다는 것은 모든 타입이 다 정상적이라는 것을 보장한다.

그러나 여전히 wildcard는 쓸 수 있다. 위에서 사용한 코드를 조금 바꿔보자.

List<?>[] lsa = new List<?>[10]; // ok, wildcard 타입은 괜찮아.
Object o = lsa;
Object[] oa = (Object[]) o;
List<Integer> li = new ArrayList<Integer>();
li.add(new Integer(3));
oa[1] = li; // 옳타꾸나~
String s = (String) lsa[1].get(0); // run time error 그러나 waning은 떠준다.


마찬가지로 타입변수로 된 element를 가지는 array를 만들려고 하면 컴파일 에러가 발생한다.

<T> T[] makeArray(T t) { 
    return new T[100]; // error
}


타입변수로 된 것은 run time에는 존재하지 않기 때문에, 실제로 array의 요소의 타입이 뭔지 알 방법이 없다.




java Generics 8 - Class Literals as Run-time Type Tokens

JDK 1.5에서의 변화 중 하나는 java.lang.Class가 generic이 되었다는 것이다. 다른 클래스들이 generic을 사용한다는 것보다 훨씬 재미있는 예이다.
Class는 타입 변수 T를 가지고 있는데, 왜 있는 것일까? Class가 어떤 타입을 나타내는 지를 표현하는 것이다.
예 를 들어, String.class는 Class<String>이 되고, Serializable.class는Class<Serialize>가 되는 것이다. 이렇게 하면, reflection을 이용한 코드에서 안정성을 높일 수있다.
특히 Class<T>.newInstance()는 T를 리턴한다.
예를 하나 들어보자. db query를 수행하는 유틸리티 메쏘드를 작성하는데, 그 메쏘드는 Collection의 인스턴스를 반환한다고 가정하자.
 Factory 인스턴스를 인자로 받는 코드는 대략 다음과 같을 것이다.

interface Factory<T> { T make();}
public <T> Collection<T> select(Factory<T> factory, String statement) { 
    Collection<T> result = new ArrayList<T>();
    /* run sql query using jdbc */
    for (/* iterate over jdbc results */ ) { 
        T item = factory.make();
        /* use reflection and set all of item’s fields from sql results */
        result.add(item);
    }
    return result;
}


이는 다음과 같이 호출할 수 있다.

select(new Factory<EmpInfo>(){ 
    public EmpInfo make() { 
        return new EmpInfo();
    }} , ”selection string”);


또는 다음과 같이 Factory 인터페이스의 구현체인 EmpInfoFactory 클래스를 정의할 수도 있다.

class EmpInfoFactory implements Factory<EmpInfo> {
    ...
    public EmpInfo make() { return new EmpInfo();}
}


EmpInfoFactory를 정의했을 경우 호출하는 코드는 다음과 같다.

select(getMyEmpInfoFactory(), ”selection string”);


아래의 두 가지 방법으로 정리가 된다.

-호출하는 쪽에서 factory 클래스를 익명으로 만든다.

-또는, 호출할 때 적합한 Factory 클래스들을 만들어 놓고, 각각의 인스턴스를 생성해서 호출한다.(매우 바람직하지 않다!)

reflection을 쓰면, 이런 식으로 복잡하게 factory 클래스들을 만들어 내지 않을 수 있다.

Collection emps = sqlUtility.select(EmpInfo.class, ”select * from emps”);
...
public static Collection select(Class c, String sqlStatement) { 
    Collection result = new ArrayList();
    /* jdbc를 이용한 sql 수행 */
    for ( /* sql 수행 결과를 반복 */ ) { 
        Object item = c.newInstance();
        /* 수행된 결과로 부터 만들어진 필드를 reflection을 이용해서 세팅. */
        result.add(item);
    }
    return result;
}


그러나, 이런 식으로 하면 우리가 정말 원하는 타입의 collection을 받아올 수 없다. 그래서 Class가 generic으로 선언된 것이며, 이를 이용하여 다음과 같이 코드를 수정할 수 있다.

Collection<EmpInfo> emps = sqlUtility.select(EmpInfo.class, ”select * from emps”);
...
public static <T> Collection<T> select(Class<T>c, String sqlStatement) { 
    Collection<T> result = new ArrayList<T>();
    /* jdbc를 이용한 sql 수행 */
    for ( /* sql 수행 결과를 반복 */ ) { 
        T item = c.newInstance();
        /* 수행된 결과로부터 만들어진 필드를 reflection을 이용해서 세팅. */
        result.add(item);
    } 
    return result;
}

이렇게 하면, 처음의 의도대로 원하는 타입의 collection을 뽑아올 수 있다.



java Generics 9 - wildcards 심화학습

이번 장에서는 wildcard를 좀더 심도 있게 다뤄볼 생각이다. 읽기용으로 제한된 wildcard를 쓰는 것은 이미 몇 번 봐왔다. 이번에는 쓰기 전용을 생각해 보자.
Sink라는 예제 인터페이스를 생각해보자.

interface Sink<T> {
    flush(T t);
}


위 인터페이스를 사용하는 아래와 같은 코드를 생각해보자. writeAll 이라는 메쏘드는 coll이라는 변수로 정의된 Collection의 모든 원소를 Sink의 flush()에 대입시키고, 마지막 원소를 반환하는 메쏘드다.

public static <T> T writeAll(Collection<T> coll, Sink<T> snk){ 
    T last;
    for (T t : coll) { 
        last = t;
        snk.flush(last);
    }
    return last;
}
...
Sink<Object> s;
Collection<String> cs;
String str = writeAll(cs, s); // 잘못된 호출

위 에서 나타난 바와 같이 writeAll은 잘못 호출되고 있다. 왜냐하면 메쏘드 선언부에서는 T로 동일한 generic으로사용하였지만,  호출하는 부분에서는 cs(Collection의 인스턴스)와 s(String의 인스턴스)가 각각 다른generic을 사용하기 때문이다.
writeAll() 메쏘드를 다음과 같이 고쳐보겠다.

public static <T> T writeAll(Collection<? extends T>, Sink<T>){...}
...
String str = writeAll(cs, s); // 호출은 잘 되겠지만, return type이 안 맞음.


호출까지는 잘 되겠지만 잘못된 코드다. 왜냐하면 return type이 Object로 넘어오기 때문이다. 위와 같이 호출할 경우 s는 Sink<Object> 이므로, T는 Object를 의미하게 된다.

이런 경우 해답은 lower bound다. <? super T>으로 쓰면 된다.  ? extends T 가 T의 하위객체라는 뜻이라면, ? super T는 T의 상위 객체라는 의미이다. 이를 사용한 코드는 아래와 같다.

public static <T> T writeAll(Collection<T> coll, Sink<? super T> snk){...}
...
String str = writeAll(cs, s); // 아싸~~


이렇게 쓰면, 호출도 정상적으로 되고 return type도 우리가 원했던 대로 된다.
좀 더 현실적인 예제를 들어보자. java.util.TreeSet<E>는 E라는 원소 타입을 가지며 정렬된 Set을 만들수 있다. 이는 몇 개의 생성자를 가지는데, 그 중에 Comparator를 인자로 받는 생성자가 있다. 이 인자는 정렬할 방법을정의해준다. 생성자를 정의하는 부분은 다음과 같이 생겼을 것 같다.

TreeSet(Comparator<E> c)


Comparator 인터페이스의 정의는 다음과 같다.

interface Comparator<T> { 
    int compare(T fst, T snd);
}


TreeSet<String> 을 적절한 Comparator를 통해 호출하려고 하면 String 끼리 비교할 수 있는 Comparator를 만들어서 생성자에전달해주어야 한다. 즉, Comparator<String>을 만들어 주어야 한다. 그러나Comparator<Object>도 그 역할을 해낼 수 있다. 그러나,TreeSet(Comparator<E> c) 와 같은 생성자로 할 경우 E는 String을 의미하기 때문에Comparator<Object>는 받을 수가 없다. 이럴 경우 유연성을 위해 lower bound를 사용하면 된다.즉, Comparator를 인자로 받는 생성자는 다음과 같이 정의되면 된다.

TreeSet(Comparator<? super E> c)


이렇게 하면 이 생성자에 적용할 수 있는 모든 Comparator를 받을 수 있다.
lower bound의 마지막 예를 들어보자. Collections.max() 메쏘드를 살펴보자. 이 메쏘드는 인자로 받은 collection의 최대 값을 반환한다.
 최대값은 정렬 방법에 따라 달라질 수 있다. 때문에 인자로 받은 collection의 모든 원소들은 Comparable을 구현해야 한다.(Comparator가 아니다.) 게다가 그 원소들끼리는 서로 비교가 가능해야 한다.
이를 코드로 정리해 보면 다음과 같이 될 것이다.

public static <T extends Comparable<T>>
T max(Collection<T> coll)


Collection<T> 란 T를 원소로 가지는 어떤 Collection을 의미한다. 리턴 타입이 T라는 것은 인자로 받은 Collection의 원소의타입으로 리턴된다는 것이다. 또 T는 Comparable<T>를 구현해야 서로 비교가 가능하다. 그러나 이것은 너무제약이 심하다. 다음의 코드를 보면 그 이유를 알 것이다.

class Foo implements Comparable<Object> {...}
...
Collection<Foo> cf = ...;
Collections.max(cf); // 쫌 돌아가면 좋겠는걸...


cf 의 모든 원소는 cf의 다른 원소들과 비교가 가능하다. cf의 원소는 Foo의 타입이고, Foo는 Comparable을 구현하고있기 때문이다. 그러나 위에서 정의한 것처럼 Collections.max()가 정의되어 있다면, 위의 코드는 작동하지 않는다.Foo가 Comparable<Foo>를 구현한 것이 아니라 Comparable<Object>를 구현했기때문이다. 사실 정확히 Comparable<Foo>를 구현해야만 Foo의 인스턴스들이 서로 비교 가능한 것은 아니다.Foo의 상위 타입으로 비교를 해도 된다. 그래서 Collections.max()를 다음과 같이 정의하면 된다.

public static <T extends Comparable<? super T>>
T max(Collection<T> coll)


만 약에 만들고자하는 API에서 타입 인자 T를 사용만 한다면, lower bound( ? super T) 가 나을 것이고,API가 T의 타입을 리턴한다면 유연성을 위해 upper bound( ? extends T) 를 사용하는 게 나을 것이다.

Wildcard Capture


/** Set s에 어떤 element를 추가해주는 메쏘드 */
public static <T> void addToSet(Set<T> s, T t) {...}


위와 같은 코드가 있을 때, 아래와 같은 코드로 호출한다고 치자.

Set<?> unknownSet = new HashSet<String>();
addToSet(unknownSet, “abc”); // 아니되옵니다~


unknownSet은 실제로는 HashSet<String>이지만 변수를 정의할 때는 generic 타입이 무엇인지 알 수 없기 때문에 이는 정상적으로 작동하지 않는다.

다음 코드를 살펴보자.

class Collections {
    ...
    <T> public static Set<T> unmodifiableSet(Set<T> set) { ... }
}
...
Set<?> s = Collections.unmodifiableSet(unknownSet); // 이건 정상적으로 작동한다. 왜??


제대로 작동하면 안 될 것 같다. 그러나 Set<T>는 Set<?>로 캐스팅이 가능하기 때문에 정상적으로작동한다. 결국 unmodifiableSet은 요소의 타입이 무엇이든 간에 상관없이 정상적으로 작동한다.
 이 런 상황은상대적으로 많이 일어날 수 있기 때문에 안전하다는 게 보장된 코드를 받아들일 수 있도록 해야 하는 규칙이 있다. 이 규칙을wildcard capture라고 부르는데, 이는 컴파일러가 wildcard로 표현된 알 수 없는 타입을 타입 인자로 처리할 수있게 해준다.




java Generics 10 - 기존 코드를 Generic을 사용하도록 변경하기

지금까지 기존에 generic을 사용하지 않는 코드와 새로 짠 generic을 사용하는 코드 간의 상호작용에 대해서 알아보았다. 이제 기존 코드를 generic을 사용하도록 바꾸는 좀 더 어려운 작업에 들어가 보자.
generic을 사용하도록 코드를 바꾸기로 결정하면, API를 어떻게 바꿀 것인지 잘 생각해보아야 한다.
기존에 이 API를 사용하던 코드들도 여전히 정상적으로 작동해야 하므로, 지나치게 제약을 주면 안 된다. java.util.Collection를 다시 살펴보자. 아래는 generic을 사용하기 전의 코드다.

interface Collection { 
    public boolean containsAll(Collection c);
    public boolean addAll(Collection c);
}

이를 generic을 사용해서 대충 바꿔보면 아래와 같이 된다.

interface Collection<E> { 
    public boolean containsAll(Collection<E> c);
    public boolean addAll(Collection<E> c);
}


타 입이 안전하게 된 것 같아 보이지만, 사실 containsAll()은 어떤 타입의 Collection이든 다 받을 수 있어야한다. ("인자로 받을 수 있다"와 "결과가 false다"는 다른 얘기다.) 이 메쏘드가 true를 리턴하기 위해서는 인자로받은 Collection의 E 타입의 값들을 가지고 있어야 할 것이다. 그러나 인자로 넘어오는 Collection이 정확히 E의타입이란 보장은 없다. E의 서브 타입일 수도 있다.

addAll()에 있어서도 E의 서브 타입이 추가 가능하도록 만들어야 한다. section 5 에서 이 방법에 대해 다루었다.
이런 식으로 generic을 추가할 때는 기존에 이 API를 사용하는 코드들도 정상적으로 작동하도록 해야 한다. 그러니까, generic이 적용되지 않은 코드에서도 정상적으로 작동하도록 해야 한다는 것이다.

이번에는 section 9에서 보았던 Collections.max()를 다시 살펴 보자. 마지막으로 우리가 생각했던 max()의 선언부는 다음과 같다.

public static <T extends Comparable<? super T>>
T max(Collection<T> coll)


괜찮아 보인다. 하지만 generic을 제거하고 생각해 보면 아래와 같이 된다.

public static Comparable max(Collection coll)


하지만 실제로는 아래와 같이 되어야 정상이다.

public static Object max(Collection coll)


즉, 리턴 타입은 Object가 되어야 한다.
이런 경우 타입 변수 T를 이용하여 다시 정의를 하면 다음과 같이 된다.

public static <T extends Object & Comparable<? super T>>
T max(Collection<T> coll)


이 것은 multiple bound라는 것의 예다. &를 이용하는 문법이다. T1 & T2 & .. &Tn과 같이 정의된 경우 T는 T1, T2...Tn 의 서브 타입이 되며, 첫번째 T1의 타입으로 erasure가 작동한다.

마지막으로 고려해야 할 사항은 인자로 받는 Collection은 읽기가 가능해야 한다는 것이다. 그래서 JDK에서는 max()가 다음과 같이 정의되어 있다.

public static <T extends Object & Comparable<? super T>>
T max(Collection<? extends T> coll)



맨 마지막 한 페이지 정도는 generic과 직접 관련이 없는 부분이 있어서 뺐습니다. 이것으로 generic 공부 끝~~~
반응형
댓글
공지사항