본문 바로가기

JAVA

Java 제네릭 기본

출처:

https://www.baeldung.com/java-generics

 

The Basics of Java Generics | Baeldung

A quick intro tot he basics of Java Generics.

www.baeldung.com

리액티브 프로그래밍을 스터디 중인데.. 함수형 프로그래밍, 제네릭 개념에 대한 이해가 부족하다는 것을 알게 되었다. 기본부터 다시 공부하고자 위 사이트를 방문하게 되었다.

1. 소개

Java Generic (자바 제네릭)은 JDK5.0 에서 추가되었는데 버그를 줄이고 타입들 위에 추상레이어를 추가하는 것이 목적이었다.

이 기사는 제네릭에 대한 간단히 소개하고. 제네릭의 목적과 코드의 질을 향상시키기 위해 어떻게 사용해야 하는지에 대해 다룬다.

 

2. 제네릭의 필요성

우리가 자바로 Integer를 저장하는 리스트를 생성한다고 생각해 보자. 우리는 다음과 같이 작성할 수 도 있다.

List list = new LinkedList();
list.add(new Integer(1)); 
Integer i = list.iterator().next();

컴파일러는 마지막 라인에 대해 어떤 데이터 타입이 반환되는지 알지 못하여 컴파일 오류를 표시 할 것이다. 컴파일러는 다음과 같이 명시적인 캐스팅을 필요로 한다.

Integer i = (Integer) list.iterator.next();

리스트가 반환하는 타입이 Integer라는 것을 책임질 수 있는 조건은 없다. 위의 리스트는 어떤 객체도 담을 수 있다. 우리는 단지 상황을 확인하고서 리스트의 객체를 조회하게 되는 것이다. 타입을 보면 Object만 보장될 수 있으며 타입 안정성을 보장하기위해 명시적인 캐스팅이 필요하다.

캐스팅하는 것은 성가신 작업일 수 있다. 우리는 리스트에 있는 데이터 타입이 Integer라는 것을 알고 있다. 캐스트는 우리의 코드를 지저분하게 만들고 있다. 프로그래머가 명시적인 캐스팅에 실수를 할 경우에는 타입관련 런타입 오류가 발생될 것이다.

프로그래머들이 특정 타입들을 사용해서 자신의 의도를 표현할 수 있고 컴파일러가 그 타입의 정확성을 보장할 수 있다면 더 쉬워질 것이다. 이것이 제네릭이면의 핵심 아이디어다.

이전 코드의 첫 번째 라인을 다음과 같이 수정해보자.

List<Integer> list = new LinkedList<>();

타입을 포함하고 있는 다이아몬드 연산자 <>를 사용해서 이 리스트가 Integer 타입에만 한정 되도록 범위를 좁힌다. 예를들어 리스트내에 포함 될 타입을 우리가 기술 하는 것이다. 컴파일러가 컴파일 시점에 타입을 강제할 수 있게 된다. 작은 프로그램들에서는 소소한 추가처럼 보일 수 있으나 더 큰 프로그램들에게는 프로그램의 견고성을 추가하고 더 읽기 쉽게 만들어 준다.

3. 제네릭 메소드

제네릭 메소드들은 하나의 메소드 선언으로 만들어지며 다른 타입들을 인자로 받아 호출 될 수 있는 메소드들이다. 컴파일러는 어느 타입이 사용되던 그 정확성을 보장할 것이다. 다음은 제네릭 메소드들의 일부 특성들이다.

  • 제네릭 메소드들은 메소드 선언부분에서 리턴 타입의 전 위치에 타입 파라미터를 놓는다.
  • 타입 파라미터들은 바운드 될 수 있다.(바운드에 대해서는 아래에서 설명한다.)
  • 제네릭 메소드들은 메소드 시그니쳐내에 컴마로 구분한 다른 타입의 파라미터들을 가질 수 있다.
  • 제네릭 메소드 바디는 일반 메소드 바디와 같다.

배열을 리스트로 변환하는 제네릭 메소드를 정의하는 예제는 다음과 같다.

public <T> List<T> fromArrayToList(T[] a) {   
    return Arrays.stream(a).collect(Collectors.toList());
}

위의 메소드에서 첫번째로 표시된 <T> 는 이 메소드가 제네릭 타입의 T 를 처리하겠다는 것을 암시하며, 메소드가 void 이더라도 표시해야 한다.

위에서 말했듯이 메소드는 하나이상의 제네릭 타입을 처리할 수 있으며, 모든 제네릭 타입들이 메소드의 리턴타입 전 위치에 추가되어야 한다. 예를들어 위의 메소드를 타입 T와 G를 처리하는 메소드로 수정하고 싶다면 다음과 같이 만들 수 있다.

public static <T, G> List<G> fromArrayToList(T[] a, Function<T, G> mapperFunction) {
    return Arrays.stream(a)
      .map(mapperFunction)
      .collect(Collectors.toList());
}

타입 T를 요소를 갖는 배열을 타입 G요소를 갖는 리스트로 변환시키는 함수를 전달하고 있다. 예를들어 아래와 같이 Integer 를 String으로 변환하도록 할 수 있다.

@Test
public void givenArrayOfIntegers_thanListOfStringReturnedOK() {
    Integer[] intArray = {1, 2, 3, 4, 5};
    List<String> stringList
      = Generics.fromArrayToList(intArray, Object::toString);
  
    assertThat(stringList, hasItems("1", "2", "3", "4", "5"));
}

오라클은 제네릭 타입 표기시 대문자를 사용할 것과 더 의미적인 표현을 사용할 것을 추천하는데 Java Collections에서는 T 는 타입으로 K 는 키로 V 는 값의 의미로 사용되고 있다.

3-1. 바운디드 제네릭 (Bounded Generics)

타입 파라미터들은 바운드(bound) 될 수 있다. 바운드 된다는 의미는 제한된다는 의미인데 메소드가 받을 수 있는 타입을 제한 할 수 있다는 것이다.

예를들면, 어떤 타입과 그 타입의 모든 서브 클래스들을 허용하거나 어떤 타입과 그 타입의 모든 부모클래스들을 허용하도록 메소드를 작성할 수 있다.

public <T extends Number> List<T> fromArrayToList(T[] a) {
    ...
}

위의 코드에서 extends 키워드는 클래스의 경우 타입 T 가 상위클래스를 상속받은 타입만 허용 한다는 의미이며, 인터페이스의 경우에는 상위 인터페이스를 구현하는 타입을 허용한다는 의미이다.

3-2. 다중 바운드 (Multiple Bounds)

하나의 타입은 아래와 같이 상위의 여러 타입들을 상속받은 타입만 허용하도록 제한 할 수 있다.

<T extends Number & Comparable>

타입 T 가 상속받은 타입이 클래스인 경우(Number 클래스를 상속 받는다면) 클래스 타입을 먼저 표기해 해야 한다. 순서가 바뀔경우 컴파일 오류가 발생한다.

4. 와일드 카드와 함께 사용하기

자바에서 와일드 카드는 물음표 "?" 로 표시하며 알 수 없는 타입을 의미할 때 사용된다. 와일드 카드는 특히 제네렉 타입을 사용할 때 유용하며, 파라미터로 사용될 수 있으나 사용시 먼저 알고 있어야 할 것이 있다.

Object는 모든 자바 클래스들의 부모 타입이긴 하지만 Object 컬렉션이 다른 컬렉션들의 부모타입이 되는것은 아니다.

예를들어 List<Object>는 List<String>의 부모타입이 아니며, 따라서 List<Object> 타입의 변수에 List<String> 변수를 할당할 경우 컴파일 오류가 발생하게 된다. 같은 컬렉션에 서로 다른 타입들을 추가할 경우 발생 할 수 있는 오류를 막기 위한 것이다.

모든 컬렉션과 컬렉션의 서브타입들에 대해 동일한 규칙이 적용된다. 다음 예제를 보자

public static void paintAllBuildings(List<Building> buildings) {
    buildings.forEach(Building::paint);
}

Building의 서브타입인 House 를 떠올려 보면, House 가 Building의 서브타입이긴 하지만 그렇다고 House 리스트에 이 메소드를 사용할 수 없다. 이 메소드가 Building 타입과 Building의 모든 서브타입들에 대해서도 사용되어야 할 경우 바운디드 와일드 카드가 다음과 같이 사용될 수 있다.

public static void paintAllBuildings(List<? extends Building> buildings) {
    ...
}

이제 위의 메소드는 Building과 Building의 모든 서브타입들에 대해서 동작하게 된다. 이것을 "upper bounded wildcard(상위제한와일드카드)" 라고 부른다.

와일드카드에는 lower bounded wildcard(하위제한와일드카드)도 있는데 ? 타입은 정의된 타입 자체가 되거나 부모 타입이 되어야 한다. 표기는 super 라는 키워드를 사용하며 <? super T> 로 표기하는데 ? 는 T 타입이거나 T를 부모로 갖는 모든 타입을 의미한다.

5. 타입 이레이저(Type Erasure)

제네릭은 타입의 안정성을 보장하며 실행시간에 오버헤드가 발생하지 않도록 하기 위해 추가 되었다. 컴파일러는 컴파일 시점에 제네릭에 대하여 "type erasure(타입 이레이저)"라고 부르는 프로세스를 적용한다.

타입 이레이저는 모든 타입 파라미터들을 제거하고나서 그 자리를 제한하고 있는 타입으로 변경하거나 타입 파라미터의 제한 타입이 지정되지 않았을 경우에는 Object 로 대체한다. 따라서 컴파일 후에 바이트 코드는 새로운 타입이 생기지 않도록 보장하는 일반 클래스들과 인터페이스, 메소드들만 포함한다. Object 타입도 컴파일 시점에 적절한 캐스팅이 적용된다.

다음은 이레이저의 예이다.

public <T> List<T> genericMethod(List<T> list) {
    return list.stream().collect(Collectors.toList());
}

타입 이레이저가 적용되면서 특정 타입으로 제한되지 않은 T 는 다음과 같이 Object 로 대체 된다.

// for illustration
public List<Object> withErasure(List<Object> list) {
    return list.stream().collect(Collectors.toList());
}
 
// which in practice results in
public List withErasure(List list) {
    return list.stream().collect(Collectors.toList());
}

타입이 제한되어 있을 경우 그 타입은 컴파일시점에 제한된 타입으로 교체될 것이다.

public <T extends Building> void genericMethod(T t) {
    ...
}

위의 코드는 컴파일 후 다음과 같이 변경된다.

public void genericMethod(Building t) {
    ...
}

6. 제네릭과 기본(Primitive) 데이터 타입

자바에서 제네릭 사용시 제약이 있는데 타입 파라미터는 기본 타입이 될 수 없다는 것이다.

예를들어 다음 코드는 컴파일 되지 않는다.

List<int> list = new ArrayList<>();
list.add(17);

기본 타입이 적용될 수 없는 이유를 이해하기 위해서는 "제네릭은 컴파일 시점 기능"이라는 것을 기억하자. 이 뜻은 타입 파라미터가 제거 되면 모든 제네릭 타입들이 Object 타입으로 구현된다는 의미이다.

예를들어 리스트의 add 메소드를 보자

List<Integer> list = new ArrayList<>();
list.add(17);

add 메소드의 시그니쳐는 다음과 같다.

boolean add(E e);

그리고, 다음과 같이 컴파일 된다.

boolean add(Object e);

타입 파라미터들은 Object 로 변환될 수 있어야 한다. 기본 타입은 Object 를 상속받지 않기 때문에 타입 파라미터로서 사용할 수 없는것이다.

그러나 자바는 기본 타입들을 대해 boxed type 을 제공하며 다음과 같이 autoboxing 과 unboxing (wrapping 과 unwrapping)을 한다.

Integer a = 17;
int b = a;

정수들을 갖을 수 있는 리스트를 생성하고 싶다면 우리는 다음과 같이 래퍼 클래스들을 사용할 수 있다.

List<Integer> list = new ArrayList<>();
list.add(17);
int first = list.get(0);

위의 코드가 컴파일 되면 다음과 같아 진다.

List list = new ArrayList<>();
list.add(Integer.valueOf(17));
int first = ((Integer) list.get(0)).intValue();

향후 자바 버전에서는 제네릭으로 기본 타입 데이터도 허용할 수 도 있다. Valhalla 프로젝트가 제네릭을 처리하는 방식을 향상시키는데 목표를 두고 있다. 이 개념은 JEP 218 에 기술된 대로 제네릭에 특화된 것들을 구현하는 것이다.

7. 결론

제네릭은 자바 언어에 있어 파워풀한 기능으로 프로그래머의 업무를 더 쉽게 만드며 에러 발생을 줄여준다. 제네릭은 컴파일 시점에 타입의 정확성을 강제하며 가장 중요한 점은 애플리케이션에 추가적인 오버헤드 없이 제네릭 알고리즘들을 구현가능하도록 해준다.

'JAVA' 카테고리의 다른 글

자바 함수형 프로그래밍 기술 7가지  (0) 2020.04.03
Apache Pulsar 정리 (kafka와 비교)  (0) 2019.12.15
XSD 를 Java 소스로 변환하기 - 메이븐 프로젝트 활용  (0) 2019.12.14
Calendar 예제  (0) 2011.05.28
JSON 테스트  (0) 2011.05.28