Java 제네릭

임준영·2021년 4월 10일
0
post-custom-banner

제네릭이란?

자바 5부터 제네릭(Generic) 타입이 새롭게 추가 되었는데, 제네릭 타입을 이용함으로써 잘못된 타입이 사용 될 수 있는 문제를 컴파일 과정에서 제거할 수 있습니다. 사실 API 문서를 볼때마다 제네릭 표현이 많기 때문에 제네릭을 이해하지 못하면 API 도큐먼트를 정확히 이해할 수 없습니다. 이러한 이유로 제네릭 타입에 대해서 알아보고 간단한 예제코드를 작성하였습니다.

제네릭은 클래스와 인터페이스, 그리고 메소드를 정의할 때 타입(type)파라미터(parameter)로 사용할 수 있도록 합니다. 타입 파라미터는 코드 작성 시 구체적인 타입으로 대체되어 다양한 코드를 생성하도록 해줍니다.

1. 제네릭을 사용하는 이유?

  • 컴파일 시 강한 타입 체크를 할 수 있습니다.
    자바 컴파일러 코드에서 잘못 사용된 타입 때문에 발생하는 문제점을 제거하기 위해 제네릭 코드에 대한 강한 타입 체크를 합니다. 실행 시 타입 에러가 나는 것보다는 컴파일 시에 미리 타입을 강하게 체크해서 에러를 사전에 방지하는 것이 좋습니다.

  • 타입 변환을 제거합니다.
    비제네릭 코드는 불필요한 타입 변환을 하기 때문에 프로그램 성능에 악영향을 미칩니다. 다음 아래 코드를 보면 List에 문자열 요소를 저장했지만, 요소를 찾아올 때는 반드시 String으로 타입 변환을 해야합니다.

List list = new ArrayList();
list.add("hello");
String str = (String) list.get(0);

다음과 같이 제네릭 코드로 수정하면 List에 저장되는 요소를 String 타입으로 국한하기 때문에 요소를 찾아올 때 타입 변환을 할 필요가 없어 프로그램 성능이 향상됩니다.

List<String> list = new ArrayList<String>();
list.add("hello");
String str = list.get(0); // 타입 변환을 하지 않습니다.

2. 제네릭 타입(class, interface)

제네릭 타입은 타입을 파라미터로 가지는 클래스와 인터페이스를 말합니다. 제네릭 타입은 클래스 또는 인터페이스 이름 뒤에 "<>"부호가 붙고, 사이에 타입 파라미터가 위치합니다.

ex)

public class 클래스명<T> {...}
public interface 인터페이스명<T> {...}

타입 파라미터는 변수명과 동일한 규칙에 따라 작성할 수 있지만, 일반적으로 대문자 알파벳 한 글자로 표현합니다. 제네릭 타입을 실제 코드에서 사용하려면 타입 파라미터 구체적인 타입을 지정해야 합니다.

제네릭을 이용한 Box 클래스 예제

public class Box<T>{
    // 클래스 뒤에 <T> 타입 파라미터를 명시했기 때문에 변수의 타입으로 사용 가능합니다.
    private T t; 
    public T get() { return t; }
    public void set(T t){ this.t = t; }
}

이제 구체적인 타입으로 변경하는 코드를 작성하겠습니다.

// 타입 파라미터를 String 타입으로 변경
Box<String> box = new Box<String>();

// 타입 파라미터 T는 String 타입으로 변경되어 Box 클래스의 내부는 다음과 같이 자동으로 재구성 됩니다.
public class Box<String>{
    private String t;
    public String get() { return t; }
    public void set(String t) { this.t = t; }
}

타입 파라미터 T를 Integer 타입으로 변경한다고 하면 마찬가지로 같습니다.

Box<Integer> box = new Box<Integer>();
box.set(6) // 자동 Boxing
int value = box.get(); // 자동 UnBoxing

이와 같이 제네릭은 클래스를 설계할 때 구체적인 타입을 명시하지 않고, 타입 파라미터로 대체했다가 실제 클래스가 사용될 때 구체적인 타입을 지정함으로써 타입 변환을 최소화 시킵니다.

3. 멀티 타입 파라미터(class<K,V,...>, interface<K,V,...>)

제네릭 타입은 두개 이상의 멀티 타입 파라미터를 사용할 수 있습니다. 이 경우 각 타입 파라미터를 콤마로 구분합니다.

멀티 타입 파라미터를 가진 제네릭 타입을 정의하고 호출하는 예제

public class Product<T, M> {

    private T t;
    private M m;

    public T getT() {
        return t;
    }

    public void setT(T t) {
        this.t = t;
    }

    public M getM() {
        return m;
    }

    public void setM(M m) {
        this.m = m;
    }
}

public class GenericExample {
    public static void main(String[] args) {
        Product<Tv, String> product1 = new Product<>();

        product1.setT(new Tv("삼성전자Tv"));
        product1.setM("디젤");

        Tv tv = product1.getT();
        String name = product1.getM();

        System.out.println(tv.getName() +" " + name);
    }
}

4. 제네릭 메소드(<T,R> R method(T t))

제네릭 메소드는 매개 타입과 리턴 타입으로 파라미터를 갖는 메소드를 말합니다. 제네릭 메소드를 선언하는 방법은 리턴 타입 앞에 <>기호를 추가하고 타입 파라미터를 기술한 다음, 리턴 타입과 매개 타입으로 타입 파라미터를 사용하면 됩니다.

public <타입 파라미터,...> 리턴타입 메소드명(매개변수,...){...}

다음 boxing() 제네릭 메소드 <> 기호 안에 타입 파라미터 T를 기술한 뒤, 매개 변수 타입으로 T를 사용했고, 리턴 타입으로 제네릭 타입 Box를 사용했습니다.

public <T> Box<T> boxing(T t) {...}

제네릭 메소드는 두가지 방식으로 호출할 수 있습니다. 코드에서 타입 파라미터의 구체적인 타입을 명시적으로 지정해도 되고, 컴파일러가 매개값의 타입을 보고 구체적인 타입을 추정하도록 할 수 도 있습니다.

리턴타입 변수 = <구체적인 타입> 메소드명(매개 값);
리턴타입 변수 = 메소드명(매개 값);

제네릭 메소드 호출 예제

public class Util {

    //제네릭 메소드 선언방법: 리턴 타입 앞에 타입파라미터 기술 후에 리턴 타입과, 매개타입으로 타입 파라미터를 사용하면 됩니다.
    public static <T> Box<T> boxing(T t){
        Box<T> box = new Box<>();
        box.set(t);
        return box;
    }
}

public class BoxingMethodExample{
    public static void main(String[] args){
        // 매개 값의 타입으로 자바 컴파일러에서 타입을 추정합니다.
        Box<Integer> box = Util.boxing(100);
        int initValue = box.get();
        System.out.println(initValue);
    }
}

다음 예제는 Util 클래스에 정적 제네릭 메소드로 compare()를 정의하고 CompareMethodExample 클래스에서 호출했습니다. 타입 파라미터는 K,V로 선언되었는데, 제네릭 타입 Pair가 K와 V를 가지고 있기 때문입니다. compare() 메소드는 두 개의 Pair을 매개값으로 받아 K와 V 값이 동일한지 검사하고 boolean 값을 리턴한다.

public class Util{
    public static <K,V> boolean compare(Pair<K,V> p1, Pair<K,V> p2){

        boolean keyCompare = p1.getKey().equals(p2.getKey());
        boolean valueCompare = p1.getValue().equals(p2.getValue());
        return KeyCompare && valueCompare;
    }
}

public class Pair<K,V> {


    private K key;
    private V value;

    public Pair(K key, V value) {
        this.key = key;
        this.value = value;
    }

    public void setKey(K key) {
        this.key = key;
    }

    public void setValue(V value) {
        this.value = value;
    }

    public K getKey() {
        return key;
    }

    public V getValue() {
        return value;
    }

}

public class CompareMethodExample{
    public static void main(String[] args){
          Pair<Integer, String> p1 = new Pair<Integer, String>(1, "사과");
          Pair<Integer, String> p2 = new Pair<Integer, String>(1, "사과");
        
          // 구체적 타입을 명시적으로 지정합니다.
          boolean result1 = Util.<Integer, String>compare(p1,p2);
          if(result1){
               System.out.println("논리적으로 등등한 객체입니다.");
          }else{
              System.out.println("논리적으로 등등하지 않는 객체입니다.");
          }

          Pair<String, String> pair = new Pair<>("user1","홍길동");
          Pair<String, String> pair = new Pair<>("user2","홍길동");
          // 구체적인 타입을 주정합니다.
          boolean result2 = Util.compare(p3,p4);

          f(result2){
               System.out.println("논리적으로 등등한 객체입니다.");
          }else{
              System.out.println("논리적으로 등등하지 않는 객체입니다.");
          }

    }
}

5. 제한된 타입 파라미터(<T extends 최상위타입>)

타입 파라미터에 지정되는 구체적인 타입을 제한할 필요가 종종 있습니다. 예를 들어 숫자를 연산하는 제네릭 메소드는 매개값으로 Number 타입 또는 하위 클래스 타입(Byte, Short, Double, Long, Integer)의 인스턴스만 가져와야 합니다. 이것이 제한된 타입 파라미터가 필요한 이유입니다.

// 제한된 타입 파라미터 정의
public <T extends 상위타입> 리턴타입 메소드(매개변수,...){...}

타입 파라미터에 지정되는 구체적인 타입은 상위 타입이거나 상위 타입의 하위 또는 구현 클래스만 가능합니다.

주의할점은 메소드의 중괄호 {} 안에서 타입 파라미터 변수로 사용 가능 한 것은 상위 타입의 맴버(필드, 메소드)로 제한됩니다. 하위 타입에만 있는 필드와 메소드는 사용할 수 없습니다. 아래 코드는 숫자 타입만 구체적인 타입으로 갖는 제네릭 메소드 compare() 입니다. 두 개의 숫자 타입을 매개 값으로 받아 차이를 리턴합니다.

// 제한된 타입 파라미터 정의 구체적인 타입을 제한하기 위해 사용합니다.
public static <T extends Number> int compare(T t1, T t2){

   double v1 = t1.doubleValue();
   double v2 = t2.doubleValue();

   return Double.compare(v1,v2);
}

doubleValue() 메소드는 Number 클래스에 정의되어 있는 메소드로 숫자를 double 타입으로 변환합니다. Double.compare() 메소드는 첫 번째 매개값이 작으면 -1을, 같으면 0을, 크면 1을 리턴합니다.

post-custom-banner

0개의 댓글