본문 바로가기
Computer Science/Java

제네릭

by eunnnn 2023. 4. 10.

바에서 제네릭(generic)이란 데이터의 타입(data type)을 일반화한다(generalize)는 것을 의미한다. 한마디로 특정(Specific) 타입을 미리 지정해주는 것이 아닌 필요에 의해 지정할 수 있도록 하는 일반(Generic) 타입이라는 것이다.

어떤 자료구조를 만들어 배포하려고 할 때 String 타입도 지원하고싶고 Integer타입도 지원하고 싶고 많은 타입을 지원하고 싶다. 그러면 String에 대한 클래스, Integer에 대한 클래스 등 하나하나 타입에 따라 만들 것인가? 이러한 문제를 해결하기 위해 우리는 제네릭이라는 것을 사용한다.

ArrayList<Integer> list1 = new ArrayList<Integer>();
ArrayList<String> list2 = new ArrayList<Integer>();
 
LinkedList<Double> list3 = new LinkedList<Double>():
LinkedList<Character> list4 = new LinkedList<Character>();


List<T> fruits = new ArrayList<>();

 

제네릭의 선언 및 생성

class MyArray<T> {
    T element;
    void setElement(T element) { this.element = element; }
    T getElement() { return element; }
}

위의 예제에서 사용된 'T'를 타입 변수(type variable)라고 하며, 임의의 참조형 타입을 의미한다. 이러한 타입 변수를 원래 타입이 들어갈 자리에 삽입하여 제네릭을 선언한다.

꼭 'T'뿐만 아니라 어떠한 문자를 사용해도 상관없으며, 여러 개의 타입 변수는 쉼표(,)로 구분하여 명시할 수 있다. 이러한 특징으로 인하여 동시에 여러 타입을 선언하여 사용이 가능하다.

public <P, R> R test(P p, Function<P, R> function){
    return function.apply(p);
}

class AnonyMap<K, V> implements Map<K, V>{
    ....
}

또한 제네릭은 와일드카드를 이용하여 타입에 대해 유연한 처리를 가능하게 한다. method2처럼 제네릭 타입을 와일드 카드로 모든 타입에 대하여 허용하게 될 경우 param의 제네릭은 최상위 클래스인 Object로 정의된다.

public void method1(List<Object> param){ // List의 제네릭타입으로 Object만 허용한다.
  // ...
}

public void method2(List<?> param){ // List의 제네릭타입으로 모든 타입을 허용한다.
  // ...

 

위와 같이 선언된 제네릭 클래스(generic class)를 생성할 때에는 타입 변수 자리에 사용할 실제 타입을 명시해야 한다.

MyArray<Integer> myArr = new MyArray<Integer>();

 

제네릭의 장점

  • 제네릭을 사용하면 잘못된 타입이 들어올 수 있는 것을 컴파일 단계에서 방지할 수 있다. 개발자가 의도하지 않은 타입의 객체가 저장되는 것을 방지할 수 있고 저장한 객체를 다시 가져올 때 기존 타입과 다른 타입으로 캐스팅되어 발생하는 오류(ClassCastException)를 줄일 수 있다.
  • 클래스 외부에서 타입을 지정해주기 때문에 따로 타입을 체크하고 변환해줄 필요가 없다. 즉, 관리하기가 편하다.
  • 비슷한 기능을 지원하는 경우 코드의 재사용성이 높아진다.
  • 형 변환(Type Casting)의 번거로움을 줄일 수 있다.
class MadPlay {
    private Object obj;

    public MadPlay(Object obj) { this.obj = obj; }
    public Object getObj() { return obj; }
}

class GenericTester {
    public void executeMethod() {
        MadPlay instance1 = new MadPlay(new String("Hello"));
        MadPlay instance2 = new MadPlay(new Integer(123));
        MadPlay instance3 = new MadPlay(new Character('a'));

        String obj1 = (String) instance1.getObj();
        Integer obj2 = (Integer) instance2.getObj();
        Character obj3 = (Character) instance3.getObj();
    }
}
class GenericMadPlay<T> {
    private T obj;

    public GenericMadPlay(T obj) { this.obj = obj; }
    public T getObj() { return obj; }
}

class GenericTester {
    public void executeMethod() {
        GenericMadPlay<String> genericInstance1 = new GenericMadPlay<>("Hello");
        GenericMadPlay<Integer> genericInstance2 = new GenericMadPlay<>(123);
        GenericMadPlay<Character> genericInstance3 = new GenericMadPlay<>('a');

        String genericObj1 = genericInstance1.getObj();
        Integer genericObj2 = genericInstance2.getObj();
        Character genericObj3 = genericInstance3.getObj();
    }
}

타입을 지정하지 않으면 최상위 Object 객체 타입으로 정의되므로 다양한 종류의 타입을 다뤄야하는 메서드의 매개변수에 Object 타입을 사용하고(첫 번째 예제 코드에서의 생성자) 그로 인한 타입 캐스팅이 불가피했지만 두 번째 코드를 보면 알 수 있듯이 제네릭을 사용하면 원하는 타입을 사전에 지정하기만 하면 된다.

 

제네릭 사용 시 주의할 점

  • 제네릭은 클래스와 인터페이스만 적용되기 때문에 자바 기본 타입(Primitive Type)은 사용할 수 없다.
  • 제네릭 타입의 객체는 생성이 불가능하다.
  • 제네릭 클래스 또는 인터페이스 타입의 배열을 선언할 수 없습니다
public void someMethod() {
    List<int> intList = new List<>(); // 기본 타입 int는 사용 불가
    List<Integer> integerList = new List<>(); // Okay!
}

--

public void someMethod() {
    // Type parameter 'T' cannot be instantiated directly
    T t = new T();
    return t;
}

-- 

public void someMethod() {
    // generic array creation
    // (자바 8이전) Cannot create a generic array of MadPlay<Integer>
    MadPlay<Integer>[] arr1 = new MadPlay<>[10];
    MadPlay<Integer>[] arr2 = new MadPlay[10]; // Okay!
}

 

 

 

출처

'Computer Science > Java' 카테고리의 다른 글

Garbage Collection  (0) 2023.04.10
JVM의 구조와 JAVA의 실행방식  (1) 2023.04.10
어노테이션  (0) 2023.04.10
String, StringBuilder, StringBuffer의 차이  (0) 2023.04.10
동일성과 동등성  (0) 2023.04.10