제네릭이란?

제네릭이란 클래스 내부에서 사용할 데이터 타입을 구체적으로 지정하는 것이 아닌 추후에 외부에서 지정할 수 있도록 일반화해두는 것이다.

데이터 타입은 <>안에 적어 지정하는데 여기에는 '객체' 타입만이 들어 올 수 있다. 그리고 <> 사이에 특정 타입만 다루지 않고 여러 종류의 타입으로 변신할 수 있도록 일반화시킨 타입를 제네릭 타입이라고 한다. 그리고 보통 여기에는 E(element), T(type), V(value), K(key)를 관례로 많이 쓴다.

이런 제네릭은 크게 클래스, 인터페이스, 메서드에 사용할 수 있다. 각 상황에 대해서 예제를 통해 살펴보자.

객체 타입이기 때문에 int와 같은 데이터 타입이 아닌 Integer와 같은 객체타입이 들어갈 수 있다. 자세한 내용은 여기를 참고하세요

제네릭 클래스

다음과 같이 클래스 선언문 옆에 제네릭 타입 매개변수가 쓰이면, 이를 제네릭 클래스라고 한다.

public class Box<T> {

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

    void save(T sample) {
        list.add(sample);
    }

    T getSample(int idx) {
        return list.get(idx);
    }
}

위의 Box라는 클래스는 자신이 어떤 타입의 데이터를 저장할지 직접 선언하지 않고 다음과 같이 Main이라는 외부에서 Integer과 Float라는 타입이 주입된다.

public class Main {
    public static void main(String[] args) {
        Box<Integer> integerBox = new Box<>();
        integerBox.save(1);

        Box<Float> floatBox = new Box<>();
        floatBox.save(1F);

        System.out.println(integerBox.getSample(0));
        System.out.println(floatBox.getSample(0));

    }
}

그럼 제네릭 클래스에선 다음과 같이 T에 들어온 타입이 대입이 되어 외부에서 주입받은 타입으로 정해진다.

Box<Integer> integerBox = new Box<>(); 에서 생성자에서 <>처럼 타입을 넣지 않은 이유는 자바가 내부적으로 앞에 선언된 Integer로 추론할 수 있기 때문에 생략이 가능하다.

제네릭 인터페이스

인터페이스도 클래스와 비슷하게 다음과 같이 제네릭 인터페이스를 만들 수 있다.

public interface IBox<T> {
    T getSample(int idx);
}

이 인터페이스는 일반 클래스와 제네릭 클래스 모두 참조할 수 있다. 먼제 제네릭 클래스의 참조를 보자.

제네릭 클래스의 참조

public class Box<T> implements IBox<T>{

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

    void save(T sample) {
        list.add(sample);
    }

    @Override
    public T getSample(int idx) {
        return list.get(idx);
    }
}

이번 예제도 마찬가지로 Main이라는 외부에서 타입을 지정한다.

public class Main {
    public static void main(String[] args) {
        Box<Integer> integerBox = new Box<>();
        integerBox.save(1);

        Box<Float> floatBox = new Box<>();
        floatBox.save(1F);

        System.out.println(integerBox.getSample(0));
        System.out.println(floatBox.getSample(0));

    }
}

그럼 그림처럼 Box에 주입에 타입이 인터페이스로 인터페이스에서 메소드의 리턴값을 대체하는 것을 볼 수 있다.

일반 클래스의 참조

일반 클래스는 다음과 같이 인터페이스의 타입 매개변수를 특정 타입으로 고정해야 한다. 이렇게 일반 클래스도 제네릭 인터페이스를 참조할 수 있지만 이는 제네릭의 장점인 타입 유연성과 재사용성을 충분히 살리지 못할 수 있다.

public class IntegerBox implements IBox<Integer>{

    List<Integer> list = new ArrayList<>();

    void save(Integer sample) {
        list.add(sample);
    }

    @Override
    public Integer getSample(int idx) {
        return list.get(idx);
    }
}

제네릭 메서드

제네릭 메소드는 일단 다음과 같이 만들수 있다.

    public <T> T printInfo(T type) {
        System.out.println("type : " + type);
        return type;
    }

이 제네릭 메서드는 일반 클래스와 제네릭 클래스에서 모두 사용가능하다. 먼저 일반 클래스의 경우를 보자.

일반 클래스에서 제네릭 함수

public class returnBox {

    public <T> T printInfo(T type) {
        System.out.println("type : " + type);
        return type;
    }
}

위처럼 일반 클래스의 경우에 다음과 같이 메소드를 사용하기 전에 <>에 타입을 지정해주면 된다.

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

        returnBox box = new returnBox();

        box.<Integer>printInfo(3);
        box.<String>printInfo("str");
        box.<Float>printInfo(4F);
    }
}

예제에선 <>를 사용하여 타입을 지정해줬지만 사실 <>를 사용하지 않아도 된다. 그 이유는 위에서 언급했던 것처럼 자바는 타입을 추론할 수 있는데 제네릭 메소드에서 사용한 제네릭 타입은 T하나로 printInfo()에 들어오는 매개 변수를 스스로 판단해 제네릭 타입으로 스스로 추론할수 있기 때문에 box.info(3);으로 사용해도 무방하다.

제네릭 클래스에서 제네릭 함수

위의 경우에는 비교적 간단했지만 제네릭 클래스에서 제네릭 메소드를 선언하면 꽤 복잡할 수 있다. 먼저 알아야 할것은 클래스에서 제네릭 타입과 메소드에서의 제네릭 타입은 독립적이라는 것이다.

class ReturnBox<T> {
	
    // 클래스의 타입 파라미터를 받아와 사용하는 일반 메서드
    public T returnBox(T x, T y) {
        // ...
    }
    
    // 독립적으로 타입 할당 운영되는 제네릭 메서드
    public static <T> T returnBoxStatic(T x, T y) {
        // ...
    }
}

다음과 같이 일반 메소드의 경우에는 클래스에서 지정된 타입을 따르지만 제네릭 메소드는 제네릭 메소드에 지정된 타입을 따라야 한다.

여기서 주의할 점은 빨간색의 T와 파란색의 T가 같지 않다는 것을 잘 이해해야 한다.

제네릭 타입 범위 제한하기

제네릭에 타입을 지정해줌으로서 클래스의 타입을 컴파일 타임에서 정하여 타입 예외에 대한 안정성을 확보하는 것은 좋지만 문제는 너무 자유롭다는 점이다.

예를 들어 Integer, Long, Float와 같은 숫자들을 이용한 계산기를 만들기 위해 제네릭 타입을 사용하였는데 외부에서 String과가 같이 계산기에 적합하지 않은 타입을 지정하면 곤란할 수 있다.

이런 경우 사용하는 키워드가 extends로 다음과 같이 사용할 수 있다. 기본적인 용법은 <T extends [제한타입]> 이다. 제네릭 <T> 에 extends 키워드를 붙여줌으로써, <T extends Number> 제네릭을 Number 클래스와 그 하위 타입(Integer, Double)들만 받도록 타입 파라미터 범위를 제한 한 것이다. 

class Calculator<T extends Number> {
	T add(T x, T y) {};
    T min(T x, T y) {};
    T div(T x, T y) {};
    T mul(T x, T y) {};
   
}

느낀점

전에 블로그를 통해 정리했전 Wrapper Class와 데이터 타입에 대한 이야기가 나왔을 때 내용을 이해하는데 좀 더 편안했다. 이전에 공부했던 내용들이 다음 내용을 이해하는데 도움이 되는 것이 공부를 할 수 있는 동기부여가 되는 것 같다. 이번 계기로 제네릭이 느끼기엔 어렵게 느껴졌지만 이번 계기로 좀 더 친근해진 느낌이다. 하지만 아직 많은 연습이 필요함을 느꼈다. 또 내가 이해한 내용을 글로 표현하는게 참 어려운 것 같다....

참고 : https://inpa.tistory.com/entry/JAVA-%E2%98%95-%EC%A0%9C%EB%84%A4%EB%A6%ADGenerics-%EA%B0%9C%EB%85%90-%EB%AC%B8%EB%B2%95-%EC%A0%95%EB%B3%B5%ED%95%98%EA%B8%B0#%EC%A0%9C%EB%84%A4%EB%A6%AD_%ED%81%B4%EB%9E%98%EC%8A%A4

profile
주주주주니어 개발자

0개의 댓글

Powered by GraphCDN, the GraphQL CDN