Generics 특징

업데이트:

Generics를 사용하는 이유을 알아보고자 합니다.

제네릭은 자바의 가장 늦게 도입 된 기능 중 하나 입니다. 이 기능이 도입되기까지 많은 고민과 필요성이 있었을 것입니다. 제네릭을 사용하는 이유 및 방법에 대해 알아 보겠습니다.

Usage

제네릭이란?

제네릭(Generic)은 클래스 내부에서 사용할 데이터 타입을 외부에서 지정하는 기법을 의미합니다. 코드로 살펴보겠습니다.

class Person<T> {
    public T info;
}

public class GenericDemo {
    public static void main(String[] args) {
        Person<String> p1 = new Person<String>();
        Person<StringBuilder> p2 = new Person<StringBuilder>();
    }
}

p1.info와 p2.info의 데이터 타입은 아래와 같습니다. 이것은 인스턴스를 생성할 때 <> 사이에 어떤 데이터 타입을 사용했는지에 달려 있게 됩니다.

  • p1.info : String
  • p2.info : StringBuilder

제네릭을 사용하는 이유

  • 타입 안정성을 보장하기 위해 사용 합니다. (e.g. Person Class는 StudentInfo, EmployeeInfo Class를 모두 담을 수 있습니다. 중복을 제거 하기 위해 사용 한 것 입니다. StudentInfo, EmployeeInfo를 모두 처리 할 수 있게, 생성자에 상위 클래스인 Object를 사용한다고 가정 합니다. 만약 “부장”이라는 String 값을 넣을 때는 에러가 발생 하지 않지만, 꺼내 쓸때에는 Type Casting 에러가 발생하게 됩니다.)
    class StudentInfo {
      public int grade;
      StudentInfo(int grade){ this.grade = grade; }
    }
    class EmployeeInfo {
      public int rank;
      EmployeeInfo(int rank){ this.rank = rank; }
    }
    class Person {
      public Object info;
      Person(Object info){ this.info = info; }
    }
    public class GenericDemo {
      public static void main(String[] args) {
          Person p1 = new Person("부장");
          EmployeeInfo ei = (EmployeeInfo)p1.info;
          System.out.println(ei.rank);
      }
    }
    

    위의 코드는 컴파일은 되지만, 아래와 같은 runtime error가 발생합니다. Type Casting 에러가 쉽게 발생 할 수 있는 것 입니다.

    java.lang.ClassCastException: java.lang.String cannot be cast to com.example.bagic.EmployeeInfo
    

    제네릭의 제한

  • extends를 사용하여, generics으로 생성 할 객체를 제한 한다.
    interface class Info {
      int getLevel();
    }
    class EmployeeInfo implements Info {
      public int rank;
      EmployeeInfo(int rank){ this.rank = rank; }
      public int getLevel() {
          return this.rank;
      }
    }
    class Person<T extends Info> {
      public T info;
      Person(T info){ this.info = info; }
    }
    public class GenericDemo {
      public static void main(String[] args) {
          Person p1 = new Person(new EmployeeInfo(1));
          Person<String> p2 = new Person<String>("부장"); // error 발생
      }
    }
    

    위 코드의 Generics 형태의 Person Class는 “부장”이라는 String 파라미터 값의 인스턴스를 생성할 수 없습니다. String 타입의 Person을 생성할때, T 는 info 타입인지를 확인하여, info의 자식으로 제한 하기 때문입니다. ___

제네릭 특징

class EmployeeInfo {
    public int rank;
    EmployeeInfo(int rank){ this.rank = rank; }
}
class Person<T, S> {

    public T info;
    public S id;

    Person(T info, S id) {
        this.info = info;
        this.id = id;
    }

    public <U> void printInfo(U info) {
        if (info instanceof EmployeeInfo) {
            System.out.println("EmployeeInfo : " + ((EmployeeInfo) info).rank); // EmployeeInfo : 1
        } else if (info instanceof StudentInfo) {
            System.out.println("StudentInfo : " + ((StudentInfo) info).grade); // StudentInfo : 99
        }
    }
}
public class GenericDemo {
    public static void main(String[] args) {
      EmployeeInfo e = new EmployeeInfo(1);
      Integer i = new Integer(10);

      /* EmployeeInfo 의 id 값을 integer 로 변환하여, generics 형태인 Person 객체 생성 */
      Person<EmployeeInfo, Integer> p1 = new Person<EmployeeInfo, Integer>(e, i);
      System.out.println("p1.info.rank : " + p1.info.rank); // rank : 1
      System.out.println("p1.id.intValue() : " + p1.id.intValue()); // id : 10

      /* EmployeeInfo 형태의 Person 생성하여, print */
      Person p2 = new Person(e, i);
      p2.<EmployeeInfo>printInfo(e);

      /* StudentInfo 형태의 Person 생성하여, print */
      Person p3 = new Person(new StudentInfo(99), new Integer(11));
      p3.printInfo(new StudentInfo(99));
    }
}

위의 코드에서 Person의 두번째 parameter 값을 Interger의 형태로 변환하여야 한다.

  • 제네릭은 primitive type(e.g. int, char…)을 넣을 수 없다.
  • 제네릭은 생략 가능하다. 1.Person p2 = new Person(e, i); 2.Person<EmployeeInfo, Integer> p1 = new Person<EmployeeInfo, Integer>(e, i)
  • 메서드 형태로 사용 가능하다.

와일드 카드(Wild Card)

  • 전체가 아닌 일부분으로 범위를 제한 할 때 사용.
public class GenericDemo {
    public static void main(String[] args) {
      List<Integer> lists = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
      System.out.println("Sum : " + sumOfList(lists)); // Sum : 55
    }

    double sumOfList(List<? extends Number> lists) {
        double number = 0.0;
        for (Number n : lists) {
            number += n.doubleValue();
        }
        return number;
    }
}

위의 코드에서, <? extends Number> 과 같이 sumOfList의 parameter 값을 정하였습니다. 그렇다면, List, List을 적용 할 수 있게 됩니다. Integer, Double 클래스는 모두 Number 클래스를 상속받기 때문 입니다. 당연히 String 배열은, sumOfList의 parameter 값으로 넘길 수 없게 됩니다. type 안정성을 보장 받게 되는 것이지요. ___

Generic를 사용한 copyOf

generics은 framework 소스를 보면, 쉽게 찾아 볼수 있습니다. 예를 들어 copyOf에서 어떻게 사용 한것인지 살펴 보겠습니다.

String[] movies = {"킹덤", "하우스오브카드","종이의집"};
String[] copyList = Arrays.copyOf(movies, 2);

for (String movie : copyList) {
    System.out.println("movie : " + movie); // movie : 킹덤, 하우스오브카드
}

위의 코드에서는 “킹덤”, “하우스오브카드”,”종이의집” 3개의 String이 담긴 배열을 생성하였고, 2개만 복사 하였습니다.


public static <T> T[] copyOf(T[] original, int newLength) {
    return (T[]) copyOf(original, newLength, original.getClass());
}

return은 타입이 T[]인 배열로 하게 됩니다. 오버로딩 되어 있는, copyOf에 parameter을 전달 할 때에는, original 값을 U[] 타입인 String[] 을 넣었습니다. 또한 newType 값을 T[] 으로 하여, original.getClass() 을 넣었습니다. 클래스 내부에서 사용할 데이터 타입을 외부에서 지정한 것입니다. 이렇게 한 이유는 copyOf의 값으로 string, int, double 배열을 전달하여도, 내부에서 U[], T[]로 정의 되어 있기때문에, string, int, double에 각각 맞는 copyOf 메서드를 만들지 않아도 되기 때문입니다. 3개의 메서드가 1개로 줄어 든 것입니다.

public static <T,U> T[] copyOf(U[] original, int newLength, Class<? extends T[]> newType) {

    T[] copy = ((Object)newType == (Object)Object[].class) ?
    (T[]) new Object[newLength] : (T[]) Array.newInstance(newType.getComponentType(), newLength);

    System.arraycopy(original, 0, copy, 0, Math.min(original.length, newLength));
    return copy; // {"킹덤", "하우스오브카드"}
}
  • original : {“킹덤”, “하우스오브카드”,”종이의집”}
  • newLength : 2
  • newType : class[Ljava.lang.String];

System.arraycopy 을 통해, copy에는 String[2] 값이 담기게 됩니다. ___

Reference

카테고리:

업데이트: