티스토리 뷰

반응형

 클래스 설계할 때마다 제네릭 문법을 항상 까먹는다. 제네릭을 직접 사용할 일은 많지 않을 수 있다. 하지만 각종 라이브러리들에서 자주 쓰이므로 꼭 알아두는 것이 좋다. 그래서 정리해보는 제네릭 문법 포스팅.



사진출처: https://www.nisdon.com/2017/09/java-parametric-polymorphism-generics-udemy-course.html



 제네릭(Generic)은 코드블럭 내부에서 쓸 자료형을 외부에서 지정하는 기법을 뜻한다. 여러가지 자료형을 허용하고 싶을 때 Object로 선언해버리면 깔끔하지만, 그렇게하면 원하지 않는 자료형이 입력되었을 때의 오류를 컴파일 시점에 잡아낼 수 없다.



1. Generic Class

 클래스 내부에서 사용될 자료형을 지정하는 것이다. 대표적인 적용 예는 ArrayList가 있다. 사용 예를 보면 어떤 의미인지 알게 될듯.


class Foo<T> {
    private T t;

    public void set(T t) {
        this.t = t;
    }

    public T get() {
        return t;
    }
}


2. Generic Method

 굳이 클래스에 제네릭을 선언하지 않고, 메소드에만 제네릭을 선언하는 방법도 있다. 이때 주의해야할 점이 있는데, 파라미터 타입 또는 리턴타입에 제네릭을 선언했으면 메소드의 리턴타입 앞에도 똑같이 선언해 주어야 한다. 글로 표현하니 애매하니까, 아래 예제를 함께 보면 좋다.


class Koo {
    // 두개 이상의 파라미터에도 각각 제네릭을 설정할 수 있다.
    public <N1, N2> Integer exampleOne(N1 t, N2 e) {
        return (Integer)t + (Integer)e;
    }

    // 먼저 나왔던 Foo 클래스를 리턴타입으로 정의한 메소드
    public <String> Foo<String> exampleTwo() {
        return new Foo<>();
    }
}



3. Generic Interface

 제네릭 클래스와 비슷하다. 클래스들이 인터페이스를 조금 더 유연하게 구현(implement)할 수 있다.


interface Roo <T1, T2, T3> {
    T1 implementThis(T1 t1);
    T2 implementThis();
    T3 maintainGeneric(T3 t3);
}

// 제네릭에 어떤 자료형의 정의하느냐에 따라 유연하게 구현 가능.
class roo <String, Integer, T3> implements Roo<String,Integer,T3> {

    @Override
    public String implementThis(String s) {
        return s;
    }

    @Override
    public Integer implementThis() {
        return null;
    }

    // 이렇게 제네릭을 유지하는 방법도 있음.
    @Override
    public T3 maintainGeneric(T3 t3) {
        return null;
    }
}



4. Generic을 더 멋지게 써보자

 지금까지 제네릭에 대해 어느정도 알아봤다. 그런데 Generic Interface를 제외하면 Object를 사용하는 것과 큰 차이가 없어보인다. 와일드카드를 사용하지 않아서 그렇다. 아까 소개했던 예제를 다시한 번 째려보자.


class Koo {
    public <N1, N2> Integer exampleOne(N1 t, N2 e) {
        // t와 e에 숫자가 아닌 것이 들어오면 오류 발생!!
        return (Integer)t + (Integer)e;
    }
}

public class Main {
    public static void main(String [] args) {
        Koo koo = new Koo();
        int result = koo.exampleOne("abc","efg");
        System.out.println(result);
    }
}


 exampleOne 메소드에는 사실상 아무거나 들어올 수 있는 상황인데, 메소드 내부에서는 Integer로 강제 형변환하고 있다. 위 소스코드는 오류 없이 멀쩡하게 실행시킬 수 있지만, 실행하면 아래와 같은 오류가 발생한다. (컴파일 시점에 오류를 잡아내지 못한다.)



 개발자의 실수를 줄이기 위해서는 exampleOne의 파라미터 타입을 숫자의 형태로 제한할 필요가 있어보인다. 이렇게 하면 된다.


class Koo {
    public <N1 extends Integer, N2 extends Integer> Integer exampleOne(N1 t, N2 e) {
        return Integer.valueOf(t) + Integer.valueOf(e);
    }
}
 




 IDE에서 확인하면 친절하게 틀렸다고 알려주는 것을 확인할 수 있다. 이렇게 하니까
파라미터 타입을 Object로 선언하는 것과 차이를 보여주는 것 같다.

 이렇게 타입을 제한하는 용법은 <T {extends or super} {클래스 or 인터페이스}> 의 형태로 쓰인다. 그러니까,

<T extends String> - 클래스 String자신 또는 String을 상속하는 아무 타입

<T extends List> - 인터페이스 List자신 또는 List를 상속하는 아무 타입

<T super HashMap> - 클래스 HashMap자신 또는 HashMap이 상속하는 아무 타입

와 같은 형태로 사용가능하다는 뜻이다.

 마지막으로.. 제네릭에 쓰인 T V N 등의 알파벳 대문자는 무슨 뜻일까? 별 뜻은 없고 임의의 알파벳 대문자 사용한 것이다. 이렇게 제네릭에 알파벳 대문자를 임의로 사용할 수 있지만, 그래도 일종의 약속이 있다. 주로 아래와 같은 의미로 사용된다.

E - Element


K - Key


N - Number


T - Type


V - Value




-끝-




«   2021/12   »
      1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31  
글 보관함
Total
759,110
Today
0
Yesterday
253