Java

자바의 Generic이란?

마닐라 2022. 5. 1. 21:12

자바에서 제네릭이란 데이터 타입의 일반화(Generalization)를 의미합니다.

클래스 내부에서 사용할 데이터 타입을 외부에서 지정하는 기법을 의미합니다.

 

기본적으로 개발이나 알고리즘 문제를 풀다보면 컬렉션을 아래와 같이 자주 사용하게 됩니다.

ArrayList<String> list = new ArrayList<String>();

HashMap<String, Integer> map = new HashMap<String, Integer>();

 

위에서 보듯 클래스 내부에서 사용할 데이터 타입을 <> 안에 String, Integer로 지정하고 있습니다.

따라서 ArrayList나 HashMap은 제네릭을 이용하여 클래스를 구현한 것을 확인할 수 있습니다.

ArrayList 클래스 선언부

 

제네릭이 없었다면 Integer, String 등 모든 레퍼런스 타입에 대한 클래스들을 IntegerArrayList,StringArrayList와 같이 각각 만들어주어야 했을 겁니다.

 

보통 제네릭은 아래 표의 타입들이 많이 쓰입니다.

 

타입 설명
<T> Type
<E> Element
<K> Key
<V> Value
<N> Number

 

타입들은 절대적인 규약은 아니고 다른 문자들도 사용할 수는 있습니다.

하지만 묵시적인 약속이므로 특별한 이유가 없다면 위와 같이 작성하는게 좋습니다.

 

제네릭 클래스를 만드는 방법을 알아보겠습니다.

 

1.클래스 및 인터페이스 선언

public class ClassName <T> { ... }
public Interface InterfaceName <T> { ... }

기본적으로 제네릭 타입의 클래스나 인터페이스의 경우 위와 같이 선언합니다.

 

public class ClassName <K, V> { ... }
public Interface InterfaceName <K, V> { ... }

위와 같이 제네릭 타입은 하나가 아닌 둘 이상을 둘 수도 있습니다.하나의 예로 HashMap의 경우 아래와 같이 제네릭 타입이 선언되어 있음을 알 수 있습니다.

HashMap 클래스 선언부

 

위와 같이 선언된 ClassName을 사용하고 싶을 때는 HashMap 사용법과 동일하게 선언합니다.

class ClassName <K, V> {
    public K key;
    public V value;

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

public class Main {
    public static void main(String[] args) {
        ClassName<String, Integer> a = new ClassName<String, Integer>("문자열", 0);
    }
}

따라서 위와 같이 객체를 생성했을 경우에는 K가 String 타입이 되고 V가 Integer 타입이 됩니다.

 

2.제네릭 클래스

제네릭 클래스를 선언했으니 선언한 타입을 클래스 내부에서 사용해보겠습니다.

class ClassName<K, V> {
    private K first;   // K 타입(제네릭)
    private V second;  // V 타입(제네릭) 

    void set(K first, V second) {
        this.first = first;
        this.second = second;
    }

    K getFirst() {
        return first;
    }

    V getSecond() {
        return second;
    }
}

// 메인 클래스 
public class Main {
    public static void main(String[] args) {
        ClassName<String, Integer> a = new ClassName<String, Integer>();
    }
}

 

위의 코드는 String과 Integer 타입의 제네릭 클래스를 생성했습니다.

따라서 변환된 코드는 아래와 같습니다.

class ClassName<String, Integer> {
    private String first;  
    private Integer second;  

    void set(String first, Integer second) {
        this.first = first;
        this.second = second;
    }

    String getFirst() {
        return first;
    }

    Integer getSecond() {
        return second;
    }
}

 

3.제네릭 메소드

클래스의 제네릭 타입 외에 따로 사용하고 싶은 타입의 메소드가 있을 수 있습니다.

선언은 아래와 같습니다.

public <T> T genericMethod(T o) {   // 제네릭 메소드
        ...
        }

        [접근 제어자] <제네릭타입> [반환타입] [메소드명]([제네릭타입] [파라미터]) {
        // 텍스트
        }

 

// 제네릭 클래스
class ClassName<K, V> {
    private K first;   // K 타입(제네릭)
    private V second;  // V 타입(제네릭)

    void set(K first, V second) {
        this.first = first;
        this.second = second;
    }

    K getFirst() {
        return first;
    }

    V getSecond() {
        return second;
    }

    <T> T genericMethod(T o) { // 제네릭 메소드
        return o;
    }
}

// 메인 클래스
public class Main {
    public static void main(String[] args) {
        ClassName<String, Integer> a = new ClassName<String, Integer>();
        a.genericMethod('a');
    }
}

 

위와 같이 제네릭 메서드를 선언한 후에 해당 메서드를 호출하면 파라미터 값의 'a'를 통해 메서드의 타입(Character)이 정해집니다.

보통 제네릭 메서드는 static 키워드가 붙은 메서드에 자주 사용합니다.

static 메서드는 이미 JVM 메모리의 method 영역에 할당되어 있기 때문에 객체 생성을 통해 접근하지 않아도 됩니다.

이 말을 다시 생각해보면 객체 생성 없이 타입을 지정해주는 것이 필요합니다.

 

public class ClassName<E> {
    static E genericMethod(E o) {  // error!
        return o;
    }
}

class Main {
    public static void main(String[] args) {
        ClassName.genericMethod(3);
    }
}

 

위의 코드는 객체 생성없이 static 메서드에 접근할 수 있다는 것을 확인할 수 있지만 객체 생성에 의해 만들어지는 제네릭 타입을 알아낼 방법이 없습니다.

 

class ClassName<E> {
    public E element;

    public ClassName(E element) {
        this.element = element;
    }

    // 아래 메소드의 E타입은 제네릭 클래스의 E타입과 다른 독립적인 타입이다.
    static <E> E genericMethod1(E o) { // 제네릭 메소드
        return o;
    }

    static <T> T genericMethod2(T o) { // 제네릭 메소드
        return o;
    }

}

public class Main {
    public static void main(String[] args) {
        ClassName.genericMethod1("문자열");
        ClassName.genericMethod2('a');
        System.out.println(ClassName.genericMethod1("문자열").getClass()); // class java.lang.String
        ClassName<Integer> c = new ClassName<Integer>(1);
        System.out.println(c.element.getClass()); // class java.lang.Integer

    }
}

 

따라서 제네릭 메서드임을 명시하여 사용을 해야하고 제네릭 메서드임을 명시한다면 클래스 선언부에 있는 타입과 동일한 타입이어도 독립적으로 다른 타입으로 인식합니다.

위와 같이 클래스와 메서드 모두 <E> 타입으로 제네릭임을 명시했는데 출력시 다른 클래스를 가리키고 있는 것을 확인할 수 있습니다.

 

제네릭은 모든 레퍼런스 타입이 올 수 있습니다. 하지만 모든 레퍼런스가 아닌 특정 타입만을 제한하고 싶거나 그래야 할 때가 필요할 수 있습니다. 

이 때 필요한 것들이 extends, super, '?' 입니다.

 

<K extends T>   // T와 T의 자손 타입만 가능 (K는 들어오는 타입으로 지정 됨)
<K super T>    // T와 T의 부모(조상) 타입만 가능 (K는 들어오는 타입으로 지정 됨)

<? extends T>  // T와 T의 자손 타입만 가능
<? super T>    // T와 T의 부모(조상) 타입만 가능
<?>       // 모든 타입 가능. <? extends Object>랑 같은 의미

 

1.extends 키워드

K extends T와 ? extends T는 비슷해 보이지만 차이가 있습니다.

K는 특정 타입으로 지정이 되지만 '?'는 특정 타입으로 지정이 되지 않는다는 것입니다.

'?'에 대해서는 좀 더 밑에서 설명하겠습니다.

Integer, Short, Double, Long 등을 상속하는 Number 클래스를 이용한 예시를 보겠습니다.

extends는 자신(Number)과 자신을 상속받는 자식들(Integer, Double, ...)로 타입을 제한하는 것입니다.

class ClassName <T extends Number> { 
}

public class Main {
    public static void main(String[] args) {

        ClassName<Double> a1 = new ClassName<Double>();    // OK!

        ClassName<String> a2 = new ClassName<String>();    // error!
    }
}

 

Number라는 타입과 그 하위의 타입으로 제한을 걸어두었기때문에 Number를 상속받는 Double 클래스는 정상적으로 객체 생성이 되고 String은 별개의 클래스 이므로 에러가 나는 것을 볼 수 있습니다.

 

2.super 키워드

super는 자신과 자신의 부모들로 타입을 제한하는 것입니다.

super에는 <T>를 사용할 수 없으므로 정렬과 관련된 Comparable 인터페이스로 확인해보겠습니다.

class ClassName <E extends Comparable<? super E>> { ...}

E extends Comparable 부분부터 본다면 Comparable과 Comparable을 상속받는 자식들로 타입을 제한하는 것입니다.

Comparable은 인터페이스이기 때문에 E 객체는 반드시 Comparable을 구현해야 합니다.

 

class Grade <E extends Comparable<E>> {}

class Student implements Comparable<Student> {
    @Override
    public int compareTo(Student o) {
        return 0;
    }
}

public class Main {
    public static void main(String[] args) {
        Grade<Student> a = new Grade<Student>();
    }
}

위의 코드에서 Grade 클래스는 Comparable 하위 타입으로 제한을 두고 있으며 E 객체는 Comparable을 구현할 것입니다.

<? super E>가 아닌 <E>를 적용하더라도 문제없이 동작합니다.

하지만 Student 클래스가 Person 클래스를 상속받을 경우 문제가 발생합니다.

아래와 같은 코드가 있다고 하겠습니다.

 

class Grade <E extends Comparable<E>> {}

class Person {}

class Student extends Person implements Comparable<Person> {
    @Override
    public int compareTo(Person o) {
        return 0;
    }
}

public class Main {
    public static void main(String[] args) {
        Grade<Student> a = new Grade<Student>();
    }
}

 

 

Student 클래스는 Person을 상속받고 있으며 Comparable 타입을 Person으로 제한하였기에 Person 클래스의 정렬 기준에 맞게 Comparable을 구현해야 합니다.

하지만 위의 경우는 Student 타입으로 Grade를 생성하는데 Student 클래스는 Comparable을 상속받지만 Person 타입으로 상속받습니다.

그렇기 때문에 컴파일 단계에서 문제가 생기며 타입의 안정성을 더하고자 <? super E>를 사용해야하는 것입니다.

 

class Grade <E extends Comparable<? super E>> {}

class Person {}

 class Student extends Person implements Comparable<Person> {
    @Override
    public int compareTo(Person o) {
        return 0;
    }
}
public class Main {
    public static void main(String[] args) {
        Grade<Student> a = new Grade<Student>();
    }
}

 

위와 같이 제네릭 타입을 지정하면 문제없이 잘 동작하게 됩니다.

 

3.'?'(와일드 카드)

<?>은 <? extends Object> 와 같은 의미입니다. Object는 자바의 최상위 클래스입니다.

따라서 Object를 포함한 하위 타입을 모두 허용하겠다는 의미이며 하위 타입이 무엇이든지 신경쓰지 않겠다는 것을 의미합니다.

데이터가 아닌 기능의 사용에만 사용에만 관심이 있는 경우에 <?>로 사용합니다.

 

 

참고

https://opentutorials.org/course/1223/6237

http://www.tcpschool.com/java/java_generic_concept

https://st-lab.tistory.com/153?category=830901