JAVA 지네릭스(2) Real Time Programming 에서의 지네릭스
Java Generics: Generics in Real Life Programming 을 통해 지네릭 익히기
들어가기전에
이 포스팅은 JDK 5 이상의 버전을 사용하면서도 제네릭을 안사용하는 사람들을 위한 포스팅이다.
실제로 우리 프로젝트 내에선? 앵간한 라이브러리들과 프로젝트에서 지네릭스는 굉장히 유용하다. 위에서는 지네릭스의 개념에 배웠다면, 이번 강의에서는 실전? 정도로 보면 될것 같다.
이 포스팅에선 Java 제네릭에 대해 이야기하고 그것이 얼마나 유용하고, 그리고 몇가지 특이한 케이스와 우리를 짜증나게 하는 케이스에 대해서도 다루고 있다.
시작하기 전에 제네릭을 사용하지 않으면 발생하는 문제 부터 알아보자,
List list = new ArrayList();
만약 당신이 이러한 코드를 사용한다면, 당신의 코드는 오류를 만들 가능성이 높아진다.
클래스안 모든 제네릭 타입 정보를 버리기 떄문이다. 정확히 무슨일이 일어나는지 아래서 살펴보자.
public class SomeType<T> {
public <E> void test(final Collection<E> collection) {
for (final E object : collection) {
System.out.println("E: " + object);
}
}
public void test(final Set<T> set) {
for (final T t : set) {
System.out.println("T from set: " + t);
}
}
public void test(final List<Integer> integerList) {
int result = 0;
for (final Integer integer : integerList) {
result += integer.intValue();
}
System.out.println("result: " + result);
}
// 실행코드
public static void main(String[] args) {
final SomeType someType = new SomeType();
final List<String> list = Arrays.asList("some", "test", "value");
someType.test(list);
}
}
위 코드가 있을때, 어떤 test 메서드를 실행할지 감이 오는가?
일단 실행을 해보도록 하자.
Exception in thread "main" java.lang.ClassCastException: java.lang.String cannot be cast to java.lang.Integer
at adjacentlist.SomeType.test(SomeType.java:23)
at adjacentlist.SomeType.main(SomeType.java:32)
런타임에서 ClassCastException 이 발생하였다. 왜 컴파일 타임이 아니고?
이유는 간단하다. 위 실행 코드는 __public void test(final List
SomeType 은 generified 하지만 이것이 생성될때 제네릭이 사용되지 않았기 때문이다. 제네릭 타입 정보를 지정하지 않고 제네릭 타입을 사용하면 자바 컴파일러는 제네릭이 없는 JDK 5.0 이전의 타입으로 만들어 버린다.
결국엔 위 메서드들은 아래와 같이 변환되게 된다.
public void test(final Collection collection)
public void test(final Set set)
public void test(final List integerList)
따라서 String List에 가장 가까운 타입은 List이고 이것이 __public void test(final List
그리고 public
타입이 제네릭화 되어도 사용할때 제네릭 타입이 따로 지정되지 않으면 제네릭 메서드에 있는 것을 포함한 모든 제네릭 정보가 사라지게 된다.
이부분을 조금 어려워할 수도 있는데, 이러한 문제를 해결하기 위해서 우린 genric 타입을 지정하면 해결되었다. 간단하게 와일드 카드 타입을 사용하기만 하면 된다.
final SomeType<?> someType = new SomeType<Object>();
final List<String> list = Arrays.asList("some", "test", "value");
someType.test(list);
이제 모든 문제가 해결되었다.
제네릭을 사용하면 얻는 이점이 조금은 보였을것이다. 제네릭을 사용하게되면 배열과 관련된 문제를 피할 수 있다. Java의 참조 타입 배열은 공변 하므로 이와 같은 문제가 발생하기 쉬운데, 이를 사전에 예방할수 있다.
아래 코드를 보자
final Integer[] integerArray = { 1, 2, 3, 4, 5 };
final Object[] objectArray = integerArray;
objectArray[0] = "test";
딱 봐도 문제가 있어보인다. 하지만 컴파일 타입에는 아무런 에러가 없다. 이 코드를 실행할때 아래와 같은 경고가 발생한다.
java.lang.ArrayStoreException: java.lang.String when objectArray[0] = "test"; is executed.
배열과는 다르게 제네릭은 불변이므로 다음과 같이 제네릭을 사용하는 경우
final List<Integer> integerList = Arrays.asList(1, 2, 3, 4, 5);
final List<Object> objectList = integerList;
컴파일 단계에서 에러가 발생한다. Good
제네릭은 컴파일 타임 타입 안정성과 함께 사용될 떄까지 타입 정보 지정을 연기해주는 편리함을 제공한다.
모두가 알고있는 한가지 예로 Java의 컬렉션 프레임을 살펴보자, 제네릭이 나오기전 컬렉션을 사용하던 프로그래머는 컬렉션이 생성될 때 컬렉션에 어떤 유형이 저장될지 몰랐기 때문에 Object type만을 저장하였다. 자바가 정적 타입 언어임에도 정적 타이핑을 사용하지 못하는 단점이 생겼다.
그러나 제네릭이 도입된 후 컬렉션 사용자는 컴파일 타임에 타입 안정성을 가지게 되었다.
아이러니하게도 컬렉션은 제네릭이 얼마나 유용한지 알수 있는 대표적인 예이지만 Neal Gafter에 따르면 제네릭이 타입 삭제와 와일드 카드를 사용하여 구현되어 제네릭을 더 복잡하게 만드는 이유중 하나가 된다고 한다.
다시말해 제네릭이 컴파일 시간에 타입 안정성을 제공하지만 컴파일러가 컴파일 할때 모든 제네릭 타입 정보를 삭제 하기 때문에 우리가 제네릭 타입의 객체나 배열을 못만드는 이유중 하나이다.
// 불가
T t = new T();
// 불가
T[] t = new T[10];
위의 코드는 앞서 말한대로 불가능 하다.
그럼 아래 코드를 살펴보자, 아래 코드도 불가능한 코드일까?
public <E> E[] copyArray(final E[] array) {
@SuppressWarnings("unchecked")
final E[] copiedArray = (E[]) Array
.newInstance(array
.getClass()
.getComponentType(),
array.length);
System.arraycopy(array, 0, copiedArray, 0, array.length);
return copiedArray;
}
위 메서드 안의 지역변수 copiedArray는 E 배열을 받아 모든 배열의 값을 복사 생성이 가능하다. 이건 어떻게 가능할까?
이것이 가능한 이유는 우리는 지금 E의 배열 객체를 매개변수로 직접 처리하는 것이기 때문이다 제네릭 타입 E를 바로 사용하는 것이 아닌,
따라서 먼저 런타임때 E[] 의 객체를 가져오면 해당 구성 요소 유형을 가죠오는 것이 가능한 것이다.
그럼 제네릭을 이용하는 다른 좋은 예를 찾아보도록 하자
만약 우리가 List안에 다른 타입을 저장 하려고 한다고 할때, 우리는 아래처럼 각 타입마다 메서드를 작성하여 개발을 해야할 것이다.
public List<BigDecimal> takePricesOf(List<Product> list) {
List<BigDecimal> newList = new ArrayList<BigDecimal>();
for (Product product : list) {
newList.add(product.getPrice());
}
return newList;
}
public List<DiscountedProduct> toDiscountedProducts(
int discountRate, List<Product> list
) {
List<DiscountedProduct> newList = new ArrayList<DiscountedProduct>();
for (Product product : list) {
newList.add(new DiscountedProduct(product, discountRate));
}
return newList;
}
public static class Product {
private BigDecimal price;
public BigDecimal getPrice() {
return price;
}
}
public static class DiscountedProduct {
private Product product;
private int discountRate;
public DiscountedProduct(Product product, int discountRate) {
this.product = product;
this.discountRate = discountRate;
}
}
제네릭이 완전 안쓰인건 아니다. List에서는 사용을 한다,
위 코드를 콜백 함수 object와 재사용 가능한 메서드로 다시 만들어보자.
public interface Mapper {
Object map(Object input);
}
public static List<Object> mapWithoutGenerics (Mapper mapperWithoutGenerics, List<?> list) {
List<Object> newList = new ArrayList();
for (Object object: list) {
newList.add(mapperWithoutGenerics.map(object));
}
return newList;
}
public static void main(String[] args) {
List<Product> productList = Arrays.asList(new Product(BigDecimal.ZERO));
List<Object> productPriceList = mapWithoutGenerics(new Mapper() {
public Object map(Object input) {
Product product = (Product) input;
return product.getPrice();
}
}, productList);
System.out.println(productPriceList);
}
재생산성 있는 코드는 얻었지만 뭔가 부족해 보인다, 무엇일까? 위 코드는 우리가 강조하던 컴파일 타임시에 타입 안정성이 없는 코드이다. 우리가 productList말고 어떤 List를 던지더라도 컴파일 시에는 위 코드의 에러를 잡아주지 않는다.
List<Object> productPriceList = mapWithoutGenerics(new Mapper() {
public Object map(Object input) {
Product product = (Product) input;
return product.getPrice();
}
}, Arrays.asList("Some", "String", "List"));
그래서 좀더 제네릭 하게 만들어 보도록 한다.
public static <T, R, F extends Function1<T, R>> List<R> map(F mapper, List<T> list) {
final List<R> newList = new ArrayList<>();
for (final T t : list) {
newList.add(mapper.apply(t));
}
return newList;
}
public static void main(String[] args) {
List<Product> productList = Arrays.asList(new Product(BigDecimal.ZERO));
List<String> productStringList = new ArrayList<>();
final List<BigDecimal> productPriceList =
map(
new Function1<Product, BigDecimal>() {
@Override
public BigDecimal apply(final Product product) {
return product.getPrice();
}
}
, productList
);
System.out.println(productPriceList);
}
그리고 function 객체는 아래와 같이 재사용 되어 사용할수도 있다.
private static final Function1<Product, BigDecimal> PRODUCT_TO_PRICE_MAPPER =
new Function1<Product, BigDecimal>() {
@Override
public BigDecimal apply(final Product product) {
return product.getPrice();
}
};
//...
final List<BigDecimal> productPriceList =
map(PRODUCT_TO_PRICE_MAPPER, productList);
final List<BigDecimal> anotherProductPriceList =
map(PRODUCT_TO_PRICE_MAPPER, anotherProductList);
그리고 이전에 보았던 discountproduct 메서드는 아래와 같이 사용이 가능하다.
final int discountRate = 15;
final List<DiscountedProduct> discountedProductList =
map(
new Function1<Product, DiscountedProduct>() {
@Override
public DiscountedProduct apply(final Product product) {
return new DiscountedProduct(product, discountRate);
}
}
, productList
);
보여준것 처럼 List of Product 의 개체에서 모든 가격을 가져오는 메서드를 만들었다. Java Collection에는 이러한 메서드가 따로 존재하지는 안흔다.
하지만 위에서 만든 코드도 불완전하고 Java 컬렉션을 사용하는 기존 코드와 호환이 되지는 않는다. 그래서 Java 컬렉션을 계속 사용하기 위해 컬렉션의 모든 요소에 다른 종류의 기능을 적용하는 하나의 일반 메서드를 갖는 목표를 달성하기 위해 몇가지 도우미 메서드를 만들었다.
귀찮게 왜 그랬을까? for 또는 foreach 문을 그냥 사용하면 되지 않을까 싶다.
물론가능하다. 하지만 실제 문제에 집중하기 위해 이러한 방법을 사용한것이다. 제품 목록에서 모든 가격을 얻는것에서 중요한것은 for foreach 루프를 어떻게 사용하는것이 아닌 각 제품의 가겨을 얻는것이 중요하다는 것이기 때문이다.
다음 예를 살펴보자,
먼저 최종 목적지 부터 확인해 보자.
final List<Product> anotherPositiveIntegerList = selector()
.fromIterable()
.toArrayList()
.select(greaterThan20, productList);
final Set<Product> anotherPositiveIntegerList = selector()
.fromIterable()
.toHashSet()
.select(greaterThan20, productList);
Product[] productArray = // ...
final List<Product> anotherPositiveIntegerList = selector()
.fromArray()
.toArrayList()
.select(greaterThan20, productArray);
위와 같은 코드를 만들기 위해 차근차근 나아가 보자, 아래 문제에는 문제가 조금 있다..
public R select(final C condition, final T source) {
final R result = // <- How can I get the Collection instance of R???
for (final E element : source) {
if (condition.isMet(element)) {
result.add(element);
}
}
return result;
}
만약 Collecion 타입이 딱 정해져 있다면 굉장히 쉽게 해결될 문제이다. 하지만 만약 List, Set 어떤게 올지 모른다면 어떻게 될까?
사용될때까지 인스턴스 생성을 연기해야 할까?, 더 정확히는 Selector가 인스턴스화 될때 말이다. 그래서 제네릭을 사용하여 Collection 중 하나를 만들기 위해 다른 타입을 하나 생성할 것이다.
그 타입은 아래와 같다.
public interface CollectionCreator<E, T extends Collection<? extends E>> {
T createCollection();
}
이를 통해서 Collection의 모든 하위 유형을 만들수 있게 되었다.
- for ArrayList
public class ArrayListCreator<E> implements CollectionCreator<E, ArrayList<E>> { @Override public ArrayList<E> createCollection() { return new ArrayList<E>(); } }
출처
https://blog.kevinlee.io/blog/2012/12/06/java-generics-generics-in-real-life-programming/