[자바] | 제네릭(Generic)

제롬·2022년 1월 24일
0

제네릭(Generic)이란?

클래스 내부에서 데이터 타입을 지정하는 것이 아닌 외부에서 사용자에 의해 데이터 타입이 지정되는 것을 말한다.

한마디로 제네릭은 특정 데이터 타입을 필요에 의해 지정할 수 있도록 일반화하는 것을 의미한다. 클래스나 메소드에서 사용할 내부 데이터 타입을 컴파일 시에 미리 지정하는 방법이다.

제네릭(Generic)의 장점

  • 클래스나 메소드 내부에서 사용되는 객체의 타입 안정성을 높일 수 있다.
  • 반환값에 대한 타입 변환 및 타입 검사에 들어가는 노력을 줄일 수 있다.
  • 비슷한 기능을 지원하는 경우 코드의 재사용성을 높일 수 있다.

제네릭(Generic) 사용방법

보통 제네릭은 아래 그림과 같은 표의 타입들이 많이 사용된다.

반드시 위 표처럼 한글자로 타입을 사용할 필요도 없고 설명과 일치할 필요도 없다. 예를들어 <E><Ele>라고 사용해도 전혀 무관하다.

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

[기본적인 제네릭 타입의 클래스 및 인터페이스 선언]

public class GenericClass <T>{
	...   
}

public interface GenericInterface <T>{
	...   
}

T 타입은 해당 블럭{...}안에서까지 유효하다. 위 코드처럼 제네릭 타입을 하나만 사용하는것 뿐만 아니라 제네릭 타입을 두개로 두는것도 가능하다. (대표적으로 HashMap이 있다.)

[제네릭 타입을 두개 두는 경우]

public class GenericClass <T, K>{
	...   
}

public interface GenericInterface <T, K>{
	...   
}

public class HashMap <T, K>{
	...   
}

이렇게 생성된 제네릭 클래스를 사용할 경우 구체적인 타입을 명시해 주어야 한다.

[제네릭 클래스 사용방법 예시]

public class GenericClass <T, K>{
	...   
}

public class UsingGeneric {
    public static void main(String[] args) {
        GenericClass<Integer, String> genericClass = new GenericClass<>();
    }
}

TInteger이 되고, KString이 된다.

이때 주의해야 할 점이 있는데 타입 파라미터로 명시할 수 있는 것은 Integer, String 같은 참조 타입(Reference Type)만 가능하다. 즉, int, float, char 같은 primitive type은 올 수 없다.

또한, 참조 타입이 가능하다는것은 사용자 정의 클래스도 타입으로 명시할 수 있다는 것을 의미한다.
[사용자 정의 클래스 사용]

public class Person {
	...
}
... // 코드생략
public static void main(String[] args) {
    List<Person> people = new ArrayList<>();
}

제네릭 클래스 사용

[제네릭 클래스]


public class Main {
    public static void main(String[] args) {
        ClassName<String> className1 = new ClassName<>("10");
        ClassName<Integer> className2 = new ClassName<>(10);

        System.out.println("className1: " + className1.getElement());
        System.out.println("className1 Type: " + className1.getElement().getClass().getName());

        System.out.println("className2: " + className2.getElement());
        System.out.println("className2 Type: " + className2.getElement().getClass().getName());
    }
}

class ClassName<E> {
    private final E element; // 제네릭 타입 변수

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

    E getElement() { // 제네릭 타입 반환 메서드
        return element;
    }
}
// 실행결과
className1: 10
className1 Type: java.lang.String
className2: 10
className2 Type: java.lang.Integer

ClassName 객체를 생성할 때 꺽새괄호(<>)안에 타입 파라미터를 지정하는데 className1E 제네릭 타입은 String으로 className2E 제네릭 타입은 Integer로 모두 변환된다.

두 개의 제네릭 타입 사용

[제네릭 타입을 두개 사용할 경우]

public class Main {
    public static void main(String[] args) {
        ClassName2<String, Integer> className3 = new ClassName2<>("10", 10);

        System.out.println("first: " + className3.getFirst());
        System.out.println("K type: " + className3.getFirst().getClass().getName());

        System.out.println("second: " + className3.getSecond());
        System.out.println("V type: " + className3.getSecond().getClass().getName());
    }
}

class ClassName2<K, V> {
    private final K first;
    private final V second;

    public ClassName2(final K first, final V second) {
        this.first = first;
        this.second = second;
    }

    K getFirst() { // 제네릭 타입 반환 메서드
        return first;
    }

    V getSecond() { // 제네릭 타입 반환 메서드
        return second;
    }
}
// 실행결과
first: 10
K type: java.lang.String
second: 10
V type: java.lang.Integer

외부에서 제네릭 클래스를 생성할 때 꺽새괄호(<>) 안에 타입을 파라미터로 보내 제네릭 타입을 지정해 준다.

제네릭 메서드

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

제네릭 클래스는 클래스 이름 옆에 <E>라는 제네릭 타입을 붙여 클래스 내에서 사용할 수 있는 E타입으로 일반화 했다. 이 외에 메서드에 한정한 제네릭도 사용이 가능하다.

[제네릭 메서드 선언방법]

public <T> T genericMethod(T t){
    ...
}

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

클래스와는 다르게 반환타입 이전에 제네릭 타입을 선언한다.

public class Main {
    public static void main(String[] args) {
        System.out.println(className1.genericMethod(5).getClass().getName());
        System.out.println(className1.genericMethod("5").getClass().getName());
        System.out.println(className1.genericMethod(className2).getClass().getName());
    }
}
class ClassName<E> {
    private final E element; // 제네릭 타입 변수

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

    E getElement() { // 제네릭 타입 반환 메서드
        return element;
    }

    public <T> T genericMethod(T t) {
        return t;
    }
}
// 실행결과
java.lang.Integer
java.lang.String
ClassName

위 코드를 보면 genericMethod()는 파라미터 타입에 따라 T 타입이 결정된다.
즉, 클래스에서 지정한 제네릭 유형과 별도로 메서드에서 독립적으로 제네릭 유형을 선언하여 사용할 수 있다.

이렇게 메서드에서 독립적으로 제네릭 유형을 사용하는 경우는 정적 메서드 를 선언할 때 필요하기 때문이다.

정적 메소드와 제네릭

static 키워드가 붙은 정적 메서드는 기본적으로 프로그램 실행시 메모리에 이미 올라가있다. 그렇기 때문에 객체를 생성할 필요없이 클래스 이름을 통해 해당 메서드를 바로 사용할 수 있다.

하지만, 여기서 문제는 static 메서드는 객체생성 이전에 메모리에 올라가기 때문에 클래스로부터 타입을 얻어올 수 없다는데 있다.

[제네릭 클래스와 별도로 독립적인 제네릭 사용]

public class Main {
    public static void main(String[] args) {
        int result = ClassName3.genericMethod(3);
        System.out.println(result);
    }
}

class ClassName3<E> {
    private final E element;

    public ClassName3(final E element) {
        this.element = element;
    }
    
    E get(){
        return element;
    }

    static <T> T genericMethod(T t) { // 제네릭 메서드
        return t;
    }
}
// 실행결과
3

위 코드에서 볼 수 있듯이 제네릭 메서드는 제네릭 클래스 타입과 별도로 지정된다.

제한된 제네릭(Generic)과 와일드 카드

T와 같은 타입 변수를 사용하여 타입을 제한한다.

특정 범위 내로 좁혀서 제네릭을 사용하고 싶다면 extends, super 그리고 물음표(?) 를 사용하면 된다. 물음표는 쉽게 말해 알 수 없는 타입 을 나타낸다.

<K extends T> // T와 T의 자손 타입만 가능 (K는 들어오는 타입으로 지정)
<K super T> // T와 T의 부모 타입만 가능 (K는 들어오는 타입으로 지정)
<? extends T> // T와 T의 자손 타입만 가능
<? super T> // T와 T의 부모 타입만 가능
<?> // 모든 타입 가능. <? extends Object> 랑 동일한 의미

간단히 말하면

  • extends T -> 상한 경계
  • ? super T -> 하한 경계
  • <?> -> 와일드 카드

여기서 주의해야 할 부분이 하나 있는데 K extends T? extends T이다. K는 특정 타입으로 지정되지만 ? 는 타입이 지정되지 않았다는 것을 의미한다.

<K extends Number>
    /*
     * Number와 이를 상속하는 Integer, Double, Long 등의 타입이 지정될 수 있으며
     * 객체 혹은 메서드를 호출 할 경우 K는 지정된 타입으로 변환된다.
     */
<? extends T> 
    /*
     * Number와 이를 상속하는 Integer, Double, Long 등의 타입이 지정될 수 있으며
     * 객체 혹은 메서드를 호출 할 경우 지정 되는 타입이 없어 타입 참조가 불가능.
     */

따라서 만약 특정 타입의 데이터를 조작해야 할 경우에는 K 같이 특정 제네릭 인수로 지정해주어야 한다.

상속 관계에 따른 제한적 제네릭 타입

다음 그림과 같이 클래스들이 상속관계를 갖는다고 가정해보자.

1. <K extends T><? extends T>

이 경우 T 타입을 포함한 자식 타입만 가능하다는 의미이다.

<T extends B> // B와 C 타입만 올 수 있다.
<T extends E> // E 타입**텍스트**만 올 수 있다.
<T extends A> // A, B, C, D, E 타입 모두 올 수 있다.
<? extends B> // B와 C타입만 올 수 있다.
<? extends E> // E 타입만 올 수 있다.
<? extends A> // A, B, C, D, E 타입 모두 올 수 있다.

extends 뒤에 오는 타입이 최상위 타입으로 한계가 정해지는 경우이다. 대표적으로 제네릭 클래스에서 수를 표현하는 클래스만 받고 싶은 경우가 있다. Integer, Long, Double과 같은 래퍼 클래스들은 Number 클래스를 상속받는다.

아래 코드처럼 사용할 수 있다.

public class ClassName <K extends Number> {
	...
}

이렇게 특정 타입 및 그 하위 타입만 제한 하고 싶을 경우 사용하면 된다. 만약 Number 클래스를 상속받았는데 String 클래스를 사용하려하면 에러(Bound mismatch)가 발생한다.

[제한된 범위 밖의 잘못된 제네릭 타입 사용]

public class LimitedGeneric <K extends Number> {
    
}

class LimitedGenericMain{
    public static void main(String[] args) {
        LimitedGeneric<Integer> limitedGeneric1 = new LimitedGeneric<>();
        LimitedGeneric<String> limitedGeneric2 = new LimitedGeneric<String>(); // 에러
    }
}

// 에러메시지
Type parameter 'java.lang.String' is not within its bound; should extend 'java.lang.Number'

2. <K super T><? super T>

이 경우는 T 타입의 부모 타입만 가능하다는 의미이다.

<K super B> // B와 A 타입만 올 수 있다.
<K super E> // E, D 그리고 A 타입이 올 수 있다.
<K super A> // A 타입만 가능.

<? super B> // B와 A 타입만 올 수 있다.
<? super E> // E, D 그리고 A 타입이 올 수 있다.
<? super A> // A 타입만 가능.

super 키워드는 뒤에 오는 타입이 최 하위 타입으로 한계가 정해진다.

대표적으로 업 캐스팅이 될 필요가 있을때 사용한다. 예를 들어 과일 이라는 클래스가 있고 이 클래스를 상속받는 사과, 딸기 클래스가 있다고 가정해보자.

이때 사과딸기 둘 다 과일로 보고 자료를 조작해야할 수도 있다. 그럴 때 사과과일로 캐스팅 해야 하는데 과일이 상위 타입이므로 업 캐스팅을 해야한다. 이럴 때 사용하는 것이 super이다.

[업 캐스팅 예제]

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

위와 같은 코드는 우선순위큐(PriorityQueue) 같이 값을 정렬하는 클래스에서 특정 제네릭에 대한 자기 참조 비교를 하고싶을 경우 위와 같은 형식을 취한다.

위 코드에서 E extends ComparableComparable을 구현하여 최상위 타입을 정해야 한다는 의미라고 볼 수 있다.

[Comparable<E><? super E>]

public class School <E extends Comparable<E>> { ... }	// Error가능성 있음
public class School <E extends Comparable<? super E> { ... }	// 안전성이 높음
 
public class Person {...}
 
public class Student extends Person implements Comparable<Person> {
	@Override
	public int compareTo(Person o) { ... };
}
 
public class Main {
	public static void main(String[] args) {
		School<Student> a = new School<Student>();
	}
}

위 코드처럼 Student 보다 더 큰 범주인 Person 클래스를 둔다고 가정해 보자. 만약, Comparable 구현부인 compareTo에서 Person 타입으로 업캐스팅한다면 정렬이 안되거나 에러가 날 수 있다.

왜냐하면, School 인스턴스를 생성할때 타입 파라미터로 Student를 주지만 Comparable에서는 그보다 상위 타입인 Person으로 비교하기 때문이다.

따라서 E 객체의 상위 타입으로 범위를 한정하기 위해 <? super E>를 해주어 에러 가능성을 줄일 수 있는 것이다.

<E extends Comparable<? super E>> 전체 의미는 E 자기 자신 및 조상 타입과 비교할 수 있는 E 를 말한다.

3. <?> (와일드카드)

와일드 카드 <?><? extends Object>와 같은 의미이다. Object는 자바에서 모든 API 및 사용자 클래스의 최상위 타입이다.

public class ClassName{
	...
}

public class ClassName extends Object{
	...
}

위 두 클래스는 같은 의미라고 볼 수 있다. extends Object를 묵시적으로 상속받는 것이나 다름 없다. 한마디로 <?>는 어떤 타입이든 상관 없다는 의미이다.


[Reference]
st-lab
TCPSchool

0개의 댓글