제네릭이란?

  • JDK1.5부터 도입
  • Generics add stability to your code by making more of your bugs detectable at compile time.
    • 제네릭은 컴파일시에 더 많은 버그를 감지하게 하여 안정성을 더해줍니다.
    • Oracle Javadoc
  • 제네릭은 클래스 내부에서 사용할 데이터 타입을 외부에서 지정하는 기법을 의미한다.
    • 생활코딩
  • 제네릭은 다양한 타입의 객체들을 다루는 메서드나 컬렉션 클래스에 컴파일 시의 타입 체크를 해주는 기능이다.
    • 자바의 정석
  • 무슨 뜻?
    • 런타임때 잡는 타입 체크를 컴파일때 잡도록 해준다.

사용법

  • 클래스 선언시 <>에 타입 파라미터 표시

public class Class_Name{}

  • 클래스 생성시<>에 사용할 타입을 지정

Class_Name a = new Class_Name();

  • 타입 파라미터?

    • T,E,K,V등으로 사용하지만, 특별한 의미로서 메타적인 정보를 포함하는게 아니고 임의의 참조형타입이며 변수로 생각하면 된다.
    • 변수의 규칙을 따른다.
      • 앞에 숫자가 아닌 글자로 시작해야한다.
      • 한글자로된 대문자 뿐만 아니라 1개이상의 문자면 소문자도 전부 가능하다.
    • 당연히 한글도 가능
    public class Gtest<한글> {
        private 한글 some;
        public 한글 getSome() {
            return some;
    
        }
        public void setSome(한글 some) {
            this.some = some;
        }
    

왜 제네릭을 사용하는지

그냥 Object를 써도 되지 않나?

  • Object는 모든 객체의 상위 객체 = 어떤 객체든 받을 수 있다.
  • 그렇다면 Object를 받는 클래스나 인터페이스로 모든 객체를 받도록 하면 되지 않을까
  • 간단한 5개의 Object를 받는 arrayList이다.
public class SimpleArrayList {
    private int size;
    private Object[] elementData = new Object[5]; // Object로 5개를 받음

    public void add(Object value) { //Object를 받아서 배열에 추가
        elementData[size++] = value;
    }

    public Object get(int idx) {
        return elementData[idx];
    }
}
  • 50과 100을 입력을 받아서 출력 해보자.
SimpleArrayList list = new SimpleArrayList();
list.add(50);
list.add(100);

Integer value1 = (Integer) list.get(0);
Integer value2 = (Integer) list.get(1);
System.out.println(value1 + value2); // 150
  • 이번엔 “50”과 “100”을 입력 받아서 출력해보자.

SimpleArrayList list = new SimpleArrayList();
list.add("50");
list.add("100");

Integer value1 = (Integer) list.get(0);
Integer value2 = (Integer) list.get(1);
System.out.println(value1 + value2);
  • 어떻게 될까? Untitled 컴파일시에 나오지 않고 런타임시에 이러한 오류를 출력한다. 해결을 위해서는 Integer.parseInt((String)list.get(0))을 해주어야 하지만, 오직 String으로 값을 넣었을때만 이러한 방식이 가능하다
  • Object로 값을 받을 때 문제
    • 해당 값이 String일지, Integer일지 아니면 다른 클래스일지는 추가적으로 확인이 필요하다.
    • 이러한 과정에서 컴파일 시에는 아무런 오류를 잡을 수 없어서 실제 실행을 해야 발견되는 에러이다.

인터페이스의 관점에서 보기

  • 사용법이라기엔 잘 이해가 안된다면?
public interface InterfaceA{
    public void insert(String x);
    public Object search(String x);
    public void remove(String x);
}
  • 위는 하나의 인터페이스가 String을 받는 메소드를 가진 인터페이스
    • 이는 하나의 인터페이스로 String타입이 아니면 사용이 안되며, Object로 반환타입이 존재하지만, 타입의 유효성을 확인해야 하며 타입관리가 어렵다.
  • String말고 Integer도 필요하다면?
public interface InterfaceB{
    public void insert(Integer x);
    public Object search(Integer x);
    public void remove(Integer x);
}
  • 또다른 타입을 받는 새로운 인터페이스를 만들거나, 오버라이드하여 오버로드를 다시 해줘야한다.

    • 단순히 타입만 바뀌는데에 기존의 코드가 중복이 된다.
  • 하지만 제네릭을 사용한다면?

public interface InterfaceA<E,T>{
    public void insert(E x);
    public T search(E x);
    public void remove(E x);
}
  • 해당 인터페이스를 사용할때, 타입을 정해주면 된다.
    • 메소드의 반환타입과 내부의 타입들을 지정하는게 가능해진다.
public class GenericArrayList<T> {

    private Object[] elementData = new Object[5];
    private int size;

    public void add(T value) {
        elementData[size++] = value;
    }

    public T get(int idx) {
        return (T) elementData[idx];
    }
}
  • 다시 위의 arrayList를 제네릭으로 사용한다면 위와 같을 것이다.

쓰는이유 정리

  • 하나의 클래스, 인터페이스에서 타입만 바꾼 새로운 인스턴스로 사용가능
  • 컴파일시에 이러한 타입에러를 발견이 가능

컴파일러 시점이라는건?

  • 컴파일러 시점에 타입을 확인한다 하면 컴파일 시점을 확인해 보자
public class Gtest<한글> {
    private 한글 some;
    public 한글 getSome() {
        return some;

    }
    public void setSome(한글 some) {
        this.some = some;
    }


    public static void main(String[] args) {
        // TODO Auto-generated method stub
        Gtest<String> a = new Gtest<>();
        a.setSome("test");

        Gtest<Integer> b = new Gtest<>();
        b.setSome(1234);

        String str = a.getSome();
        int num = b.getSome();

        System.out.println(a.getSome());
        System.out.println(b.getSome());
    }

}
  • 위는 간단하게 제네릭으로 된Gtest를 String/Integer 두가지로 받아서 출력하는코드이다.
  • 이 Gtest.java→Gtest.class→디 컴파일하여 결과를 확인해 보자
public class Gtest<한글> // 실제 클래스는 변함이 없다.
{
  private 한글 some;

  public 한글 getSome()
  {
    return this.some;
  }

  public void setSome(한글 some) {
    this.some = some;
  }

  public static void main(String[] args)
  {
    Gtest a = new Gtest();//제네릭이 없어졌다.
    a.setSome("test");

    Gtest b = new Gtest();
    b.setSome(Integer.valueOf(1234));

//자동으로 형변환이 이뤄지고 있따.
    String str = (String)a.getSome();
    int num = ((Integer)b.getSome()).intValue();

    System.out.println((String)a.getSome());
    System.out.println(b.getSome());
  }
}
  • 알아서 형변환을 적용해주고 있는것을 확인 할 수 있으며, 이러한 컴파일 과정으로 타입의 오류를 잡아낸다는것을 볼 수 있다.

Bounded Type Parameter (type parameter의 제한)

  • 파라미터의 범위를 제한
  • Number의 서브 클래스만 타입으로 가지고 싶은경우
    • Class_Name
    • String은 담을 수 없다.
    • Integer, Float, Double, Long은 가능

와일드카드 <?> - ?가 들어간것

  • 객체를 할당 받을때 와일드 카드를 사용
  • Gtest<?> wild = b;
    • Object처럼 받아진다.
  • Gtest<? extends Number> wild = b;
    • Number를 포함해 Number의 하위 자손들, 상속받은 타입들만 사용가능
  • Gtest<? super Number> wild = b;
    • Number를 포함해 Number의 조상 타입만 사용 가능
  • 좀더 와일드 카드를 이해해 보자 2020-12-15-java-generic-wildcard_105337.png
  • 위 사진과 같이 List는 List의 상위 타입이 아니다.
  • printCollection이라는 collection을 받아서 출력하는 코드를 만들었다면.
static void printCollection(Collection<Object> c) {
    for (Object e : c) {
        System.out.println(e);
    }
}

public static void main(String[] args) {
    Collection<String> c = new ArrayList<>();
    c.add("hi");
    printCollection(c); // 작동도 안한다.
//Collection<Object>은 Collection<String>의 상위가 아니니까
}
  • 위의 코드를 작동 하고 싶으면? <?>를 사용하면 된다.
static void printCollection(Collection<?> c) {
    for (Object e : c) {
        System.out.println(e);
    }
}

public static void main(String[] args) {
    Collection<String> c = new ArrayList<>();
    c.add("hi");
    printCollection(c);
}
  • ?를 넣음으로서 상하관계가 아닌 어떤 타입의 값이 들어갔다는 표시가 된다.
  • 여기서 좀더 제한을 둬서 Number를 상속한 것으로 바꾼다면?
static void printCollection(Collection<? extends Number> c) {
    for (Number e : c) {
        System.out.println(e);
    }
}

public static void main(String[] args) {
    Collection<String> c = new ArrayList<>();
    c.add(123);
    printCollection(c)
}
  • 이런식으로의 활용도 가능해진다.

제네릭을 사용하지 못하는 경우

  • private T[] test = new T[10];

    • 제네릭을 활용한 배열생성은 안된다.
    • 배열을 만든다는것은 해당 타입만큼의 공간을 만들어내는것이다. T라는 공간을 만들지 못한다.
      • new를 통해 heap의 공간을 확보해야 하지만, 제네릭은 정확한 타입이 지정된게 아니므로 공간 확보가 안된다.
    • 만약 쓰고 싶다면? Array.newInstance를 이용하여 해당 타입의 크기를 제공하면서, 가능하다고 한다.
    class DataSet<T>
    {
      T[] array;
      public DataSet ( Class<T> clazz , int size )
      {
         array = (T[])Array.newInstance ( clazz , size );
      }
    }
    
    //사용시
    DataSet<Integer> dataSet = new DataSet<Integer> ( Integer.class , 10 );
    [출처] [Java] 제네릭(Generic)으로 배열(Array) 생성하기|작성자 공감 프로그래머
    
  • 위와 마찬가지로 static변수로 사용할 수 없다.

    • 제네릭 메소드는 static 메소드가 가능하다.

제네릭 메서드?

  • 파라미터와 리턴 타입으로 type parameter를 갖는 메서드
  • 사용법
public <T,E> T m1(E p) {
        System.out.println(p);
        return p;
}

instance.<String>m1("sring");//사용시 <타입>을 넣어서 지정한다.
  • 는 제네릭 메서드라는 표식이후, 반환 타입, 입력 타입을 지정이 가능하다.
  • 리턴 타입에 관계 없이 제네릭 메서드라는 것을 컴파일러에게 알려주는 과정이 필요
  • 일반 클래스 내에서도 사용가능
    • 클래스의 지정하는 타입파라메터와 제네릭 메서드에 정의 된 타입 파라미터는 상관이 없다는 의미도 됨
public class Gtest2<한글> {
    private 한글 some;
    public 한글 getSome() {
        return some;

    }
    public void setSome(한글 some) {
        this.some = some;
    }

//제네릭 메서드 - 여기 한글은 클래스의 한글일까? 아니다.
    public <한글> 한글 m1(한글 p) {
        System.out.println(p);
        return p;
    }


    public static void main(String[] args) {
        // TODO Auto-generated method stub
        Gtest2<String> a = new Gtest2<>(); //String이라고 클래스 타입 지정

        Gtest2<Integer> b = new Gtest2<>();

        a.m1("String");
        a.m1(1123213323);//a는 String이라 되어있지만, 실제로는 잘 동작한다.
        a.<String>m1("sring");
        a.<String>m1(123);//해당 메서드는String을 받기때문에 여기서 에러가 생긴다.
    }

}
  • static을 사용할 수 있는 이유?
    • static메서드의 경우 메서드의 틀만 공유된다.
    • 이러한 틀안에서 지역변수처럼 타입 파라미터가 다양하게 오가는 형태로 사용이 가능하다 한다.
 static public void setSome(한글 some) { // 제네릭 메서드가 아니므로 static이 안된다.
        this.some = some;
    }

    static public <한글> 한글 m1(한글 p) {// static이 가능
        System.out.println(p);
        return p;
    }

제네릭이 활용된 곳

  • 가장 큰 활용 Collection
    • Queue, Queue,…
    • ArrayList, ArrayList,…

마무리 - 제네릭이란?

  • 하나의 클래스,인터페이스에서 여러 타입을 사용하게 해준다.
    • 이를 컴파일 시에도 알 수 있도록 체크를 해주는 기능
  • 재사용성을 높여준다.
  • 타입의 안정성을 높여준다.

출처