[java] 제네릭(Generic)

반응형

1. 제네릭(Generic) 이란?

사전적 의미 : 일반적, 포괄적

자바(java)에서 제네릭(generic)은 데이터 타입을 일반화한다는 의미이다.

List, Map 을 사용할때 <> 괄호안에 타입을 지정해서 다음과 같이 생성한다.

List<String> memberList = new ArrayList<>();
List<Integer> numList = new ArrayList<>(); 
    
Map<String, String> monthMap = new HashMap<>();
Map<Integer, Member> memberMap = new HashMap<>();
    
class Member {
    String name;
    String mailAddress;
}

 

타입으로 String, Integer, 직접만든 Member 클래스등을 지정해서 생성해주었다.
이와같이 List, Map 등을 생성할때 <>괄호안에 여러 타입들을 지정해서 생성할 수 있다.

어떻게 여러 타입을 지정해서 생성할 수 있을까?

만약, 이렇게 간단히 타입선언이 아니라, String List 따로, Integer List 따로, 직접 만든 Member List 따로.. 
각각 필요할때마다 List를 만들어줘야 한다면? 생각만해도 불편하다. 

 

그래서 사용되는 것이 제네릭(generic) 이다.

사실 과거의 JDK 1.5 이전에서는 Object 타입을 사용했다.
Object 타입을 사용하면 다시 원하는 객체로 타입을 변환해주어야 하는 과정이 추가될수 있고, 이 과정에서 에러가 발생할 가능성도 있다.
제네릭이 도입된 JDK 1.5 부터는 컴파일 시에 미리 타입이 지정되므로, 타입변환과 같은 과정을 안해도 되게 되었다. 

즉, 제네릭(generic)은 클래스나 메소드에서 사용될 데이터 타입을 컴파일 할때 미리 지정하는 방식이다.
컴파일 할때에 미리 타입을 지정하므로 다음과 같은 장점이 있다.
1. 컴파일 단계에서 잘못된 타입이 지정될 위험을 방지할 수 있다.
2. 외부에서 타입을 지정해주므로, 타입변환이나 타입검사 등을 생략할 수 있다.

2. 제네릭 생성 예

public class MyClass<T> { ... }
public interface MyInterface<T> {
    T call();
}


자바에서의 예

public interface List<E> extends Collection<E> {
    int size();
    boolean isEmpty();
    Iterator<E> iterator();
    boolean add(E e);
    ...
}
public interface Map<K, V> {
    V get(Object key);
    V put(K key, V value);
    V remove(Object key);
    Set<K> keySet();
    ...
}

 

 

3. 제네릭 타입 매개 변수

타입 매개 변수로 사용하는 문자는 관례적으로 하나의 대문자를 사용한다.
물론 여러문자로 사용할 수도 있다.

 

* 관례적으로 많이 사용하는 타입매개변수

T  Type
E  Element
K  Key
V  Value

 

4. 제네릭의 사용

// 제네릭 클래스 생성
public class MyClass<T> {...}

public class GenericClass {
    public static void main(String[] args) {
        // 제네릭 클래스 사용 : 사용할 때 타입을 지정한다.
        MyClass<String> stringMyClass = new MyClass<>();
        MyClass<Integer> integerMyClass = new MyClass<>();
    }   
}

제네릭을 사용하면 위의 예제와 같이 타입은 외부에서 사용할 때 지정하도록 할 수 있다.

이때 주의해야 할 점은, 파라미터로 참조 타입 (Reference Type) 만 사용할 수 있다.

 

즉, int, long, char, boolean 등과 같은 기본 타입 (Primitive Type) 는 선언할 수 없기 때문에, 기본 타입을 객체로 감싸주는 Integer, Long, Character, Boolean 등의 래퍼 클래스(Wrapper Class) 를 사용해야 한다.

 

참조 타입을 사용할 수 있다는 의미는 바꿔말하면 사용자가 정의한 클래스도 사용할 수 있다는 의미이다.

그렇기 때문에, 아래와 같이 List, Map 등을 생성할 수 있었던 것이다.

// 사용자 정의 Member 클래스
class Member {
    String name;
    String mailAddress;
}

// K(key) = Integer, V(Value) = Member 로 HashMap 생성
Map<Integer, Member> memberMap = new HashMap<>();
// E(Element) = Member 로 생성
List<Member> memberList = new ArrayList<>();

 

5. 타입 변수 (Type Variable) 의 제한

제네릭을 생성할때 extends(상속) 을 이용하면 타입변수(type variable) 를 제한할 수 있다.

public class Beverage {...}
// Beverage 를 상속받아 생성한 coffee, tea 클래스
public class Coffee extends Beverage {...}
public class Tea extends Beverage {...}

// 제네릭 : 타입변수 제한 (상속 이용, Beverage 와 그 하위클래스만 지정가능)
public class BeverageList<T extends Beverage> {...}

// 제네릭 사용
public class GenericClass {
    public static void main(String[] args) {
        // Beverage를 상속받아 생성한 클래스만 타입변수에 선언할 수 있다.
        BeverageList<Coffee> coffeeBeverageList = new BeverageList<>();
        BeverageList<Tea> teaBeverageList = new BeverageList<>();
        // 아래의 경우 컴파일 에러
        // BeverageList<String> stringBeverageList = new BeverageList<>();
    }
}

 

6. 제네릭 메소드 (Generic Method)

메소드의 선언부에서 반환 타입 바로 앞에 타입 변수를 사용한 메소드를 의미한다.

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

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

예제

public class Beverage {...}
// Beverage 를 상속받아 생성한 coffee, tea 클래스
public class Coffee extends Beverage {...}
public class Tea extends Beverage {...}

// 제네릭 : 타입변수 제한 (상속 이용, Beverage 와 그 하위클래스만 지정가능)
public class BeverageList<T extends Beverage> {
    public <T> T genericMethod(T t){
        return t;
    }
}

// 제네릭 사용
@Slf4j
public class GenericClass {
    public static void main(String[] args) {
        BeverageList<Coffee> coffeeList = new BeverageList<>();
        // 파라미터 타입에따라 결정된다.
        String beverageListResult = coffeeList.getClass().getName();
        String integerResult = coffeeList.genericMethod(3).getClass().getName();
        String stringResult = coffeeList.genericMethod("stringTEST").getClass().getName();
        String teaResult = coffeeList.genericMethod(new Tea()).getClass().getName();

        log.info("beverageListResult = {}", beverageListResult);
        log.info("integerResult = {}", integerResult);
        log.info("stringResult = {}", stringResult);
        log.info("teaResult = {}", teaResult);
    }
}

출력결과

beverageListResult = BeverageList
integerResult = java.lang.Integer
stringResult = java.lang.String
teaResult = Tea

출력결과를 확인해보면, 제네릭 메소드의 파라미터 타입에 의해 T 타입이 결정되는 것을 확인할 수 있다.

즉, 클래스에서 지정한 제네릭 타입과 별도로 메소드에서 독립적으로 제네릭 타입을 선언하여 사용할 수 있다.

 

이는 정적 메소드 (static method) 를 생성할때 필요하다.

정적(static) 메소드는 어플리케이션이 실행될 때 메모리에 올라가있다. 즉, 객체가 생성되기 전에 메모리에 올라가 있다.

그래서 클래스와 같은 T를 선언해도 클래스로부터 T를 가져올수가 없으므로 다음과 같이 컴파일 에러가 발생한다.

public class BeverageList<T extends Beverage> {
    
    /**
     * static 메소드는 객체가 생성되기 전에 메모리에 올라간다.
     * 그 때문에 클래스와 같은 T를 선언해도
     * 클래스로부터 T를 가져올수 없어 컴파일 에러가 발생한다.
     */
    // compile error 발생
//    static T staticMethod(T t) {...}
}

제네릭이 사용되는 메소드를 정적 메소드로 하고 싶을때, 제네릭 메소드를 사용해야 한다.

 

7. 와일드 카드 (wild card) 사용

물음표(?) 기호를 사용하여 와일드 카드를 사용할 수 있다.

 

와일드카드 (Unbounded Wildcards)

<?> 		// 타입 변수에 모든 타입 사용 가능

상위 클래스 제한 와일드카드 (Upper Bounded Wildcards)

<? extends T> 	// T 타입과 T 타입을 상속받는 자식 클래스 타입만 사용 가능

하위 클래스 제한 와일드카드 (Lower Bounded Wildcards)

<? super T> 	// T 타입과 T 타입이 상속받은 부모 클래스 타입만 사용 가능

예제 코드

class Beverage {...}
// 상속 : Latte -> Coffee -> Beverage
class Coffee extends Beverage {...}
class Latte extends Coffee {...}
// 상속 : GreenTea -> Tea -> Beverage
class Tea extends Beverage {...}
class GreenTea extends Tea {...}

// 1. 와일드 카드 (Unbounded Wildcards)
// <?> 는 <? extends Object> 와 동일하다
<?>	// Object 는 최상위 타입이므로 어떤 타입이든 가능하다.
public class MyClass extends Object {...}	// <?>와 동일하다

// 2. 상위 클래스 제한 와일드카드 (Upper Bounded Wildcards)
// extends 뒤에 오는 타입이 최상위 타입이다.
<T extends Beverage>	// GreenTea, Tea, Latte, Coffee, Beverage 가능
<T extends Coffee>	// Latte, Coffee 가능
<T extends Latte>	// Latte 가능

<? extends Beverage>	// GreenTea, Tea, Latte, Coffee, Beverage 가능
<? extends Coffee>	// Latte, Coffee 가능
<? extends Latte>	// Latte 가능

// 3. 하위 클래스 제한 와일드카드 (Lower Bounded Wildcards)
// super 뒤에 오는 타입이 최하위 타입이다.
<T super Beverage>	// Beverage 가능
<T super Coffee>	// Coffee, Beverage 가능
<T super Latte>		// Latte 가능

<? super Beverage>	// Beverage 가능
<? super Coffee>	// Coffee, Beverage 가능
<? super Latte>		// Latte 가능

 

8. 제네릭 타입 제한 과 와일드카드의 차이

주의해야 할 점은 <E extends T> 와 <? extends T> 는 차이가 있다는 점이다.

E 는 특정 타입이 지정되고, ?는 지정되지 않는다는 차이가 있다.

/**
 * Number 를 포함하고 상속하는 Integer, Double, Long 등이 가능
 * 객체 또는 메소드를 호출 할 경우 E는 지정된 타입으로 반환된다.
 */
<E extends Number>

/**
 * Number 를 포함하고 상속하는 Integer, Double, Long 등이 가능
 * 객체 또는 메소드를 호출 할 경우 지정된 타입이 없어 타입참조가 불가능하다.
 */
<? extends Number>

 

반응형