13. 제네릭

제네릭이란?

  • 클래스의 인터페이스, 메소드를 정의할 때, 타입(type)을 파라미터로 사용할 수 있도록 해준다.

  • JAVA 5 부터 생겼다.

public T genericMethod(T genericTypeObject)

제네릭을 사용하는 이유

컴파일 시 강한 타입 체크가 가능하다.

컴파일 시에 미리 타입을 강하게 체크하여 에러를 사전에 방지할 수 있다.

타입 변환(casting)을 제거한다.

제네릭 이전

List list = new ArrayList();
list.add("hello");
String str = (String) list.get(0); // 타입 변환이 필요하다.

제네릭 이후

List<String> list = new ArrayList<String>();
list.add("hello");
String str = list.get(0); // 타입 변환이 필요없다.

제네릭 타입(class<T> interface<T>)

타입을 파라미터로 가지는 클래스와 인터페이스를 생성할 수 있다.

public class 클래스명<T> { ... }
public interface 인터페이스명<T> { ... }

타입 파라미터의 네이밍은 변수명과 동일한 규칙에 따라 작성할 수 있다. 단, 관례적으로 영문 대문자 한글자로 작성한다.

모든 종류의 객체를 저장하면서 타입 변환이 일어나지 않는 마법을 부릴 수 있다.

제네릭 예제

Box 클래스 작성

제네릭 타입 파라미터를 받는 클래스이다.

public class Box<T> {
    public T t;

    public T getT() {
        return t;
    }

    public void setT(T t) {
        this.t = t;
    }
}
public class Main {
    public static void main(String[] args) {
        Box<String> stringBox = new Box<>();
        stringBox.setT("문자열");
        String stringBoxT = stringBox.getT();
        System.out.println("stringBoxT = " + stringBoxT); // 저장된 문자열 출력

        Box<Integer> integerBox = new Box<>();
        integerBox.setT(100);
        Integer integerBoxT = integerBox.getT();
        System.out.println("integerBoxT = " + integerBoxT); // 저장된 숫자 출력
    }
}

위와 같이 Box<T> 클래스의 제네릭 타입 파라미터를 이용하여 어떤 값이든 삽입하고 반환할 수 있게 되었다.

타입에 구애받지 않는 비슷한 로직이 필요한 경우에 기존엔 Object 타입으로 받아준 뒤에 캐스팅을 하였는데, 제네릭이 나온 이후로는 제네릭을 이용하면 캐스팅도 필요 없고 컴파일 시 강한 타입 체크도 하기 때문에 실수를 최소한으로 할 수 있다.

제네릭 멀티 타입 파라미터(class, interface)

제네릭 타입은 두 개 이상의 멀티 타입 파라미터를 사용할 수 있고, 이 경우 콤마로 구분한다.

멀티 타입 파라미터 예제 코드

public class Product <M, P>{
    private M model;
    private P price;

    public M getModel() {
        return model;
    }

    public void setModel(M model) {
        this.model = model;
    }

    public P getPrice() {
        return price;
    }

    public void setPrice(P price) {
        this.price = price;
    }
}

위와 같은 형식으로 멀티 타입 파라미터를 받는 Product 클래스를 만들 수 있다.

Product<String, Integer> product1 = new Product<>();
product1.setModel("K-11");
product1.setPrice(10000);

위는 M 타입으로는 String을 주고 P 타입으로는 Integer를 준 예제이다.

public class Laptop {
    String cpu;
    String ram;
    String vga;

    public String getCpu() {
        return cpu;
    }

    public void setCpu(String cpu) {
        this.cpu = cpu;
    }

    public String getRam() {
        return ram;
    }

    public void setRam(String ram) {
        this.ram = ram;
    }

    public String getVga() {
        return vga;
    }

    public void setVga(String vga) {
        this.vga = vga;
    }

    @Override
    public String toString() {
        return "Laptop{" +
                "cpu='" + cpu + '\'' +
                ", ram='" + ram + '\'' +
                ", vga='" + vga + '\'' +
                '}';
    }
}
public class DiscountablePrice {
    double discountRate;
    double price;

    public double getDiscountRate() {
        return discountRate;
    }

    public void setDiscountRate(double discountRate) {
        this.discountRate = discountRate;
    }

    public double getPrice() {
        return price - (price * discountRate);
    }

    public void setPrice(double price) {
        this.price = price;
    }

    @Override
    public String toString() {
        return "DiscountablePrice{" +
                "discountRate=" + discountRate +
                ", price=" + price +
                ", discountedPrice=" + this.getPrice() +
                '}';
    }
}

위와 같이 사용자 정의 타입 LaptopDiscountPrice 등을 만들어서 해당 제네릭 타입에 대입할 수도 있다.

public class Main {
    public static void main(String[] args) {
        Product<Laptop, DiscountablePrice> product2 = new Product<>();
        Laptop laptopModel = new Laptop();
        laptopModel.setCpu("i7");
        laptopModel.setRam("16gb");
        laptopModel.setVga("Geforce 1070");
        product2.setModel(laptopModel);

        DiscountablePrice discountablePrice = new DiscountablePrice();
        discountablePrice.setPrice(10000);
        discountablePrice.setDiscountRate(0.2);
        product2.setPrice(discountablePrice);

        System.out.println(product2.getModel() + " + " + product2.getPrice());
    }
}

제네릭 메소드( R method(T t))

  • 제네릭 메소드는 파라미터와 리턴 타입으로 제네릭 타입을 갖는 메소드를 말한다.

  • 제네릭 메소드 선언법은 리턴 타입 앞에 <> 기호를 추가하고 타입 파라미터를 기술한 다음, 리턴 타입과 파라미터 타입으로 제네릭 타입을 사용하면 된다.

예제 코드 1 (형식)

public static <P, R> R genericTypeMethod(P param, R paramToReturn) {
    System.out.println("param = " + param);
    System.out.println("paramToReturn = " + paramToReturn);

    return paramToReturn;
}

위와 같은 형식으로 사용할 수 있다.

public static void main(String[] args) {
    ParamType param = new ParamType();
    param.setParamString("파라미터");

    ReturnType paramToReturn = new ReturnType();
    paramToReturn.setReturnString("리턴");

    genericTypeMethod(param, paramToReturn);
}

파라미터와 리턴 값을 위한 타입을 만들어서 임의로 작성해보았다.

예제 코드 2 (Pair 클래스 응용)

public class Pair <K, V> {
    private K key;
    private V value;

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

    public K getKey() {
        return key;
    }

    public void setKey(K key) {
        this.key = key;
    }

    public V getValue() {
        return value;
    }

    public void setValue(V value) {
        this.value = value;
    }
}
public class Util {
    public static <K, V> boolean compare(Pair<K, V> p1, Pair<K, V> p2) {
        boolean keyCompare = p1.getKey().equals(p2.getKey());
        boolean valueCompare = p1.getValue().equals(p2.getValue());

        return keyCompare && valueCompare;
    }
}
public class Main {
    public static void main(String[] args) {
        Pair<String, String> p1 = new Pair<>("1", "홍길동");
        Pair<String, String> p2 = new Pair<>("1", "홍길동");

        boolean compare1 = Util.compare(p1, p2);

        if(compare1) {
            System.out.println("key와 value가 모두 일치합니다.");
        } else {
            System.out.println("key와 value가 일치하지 않습니다.");
        }

        Pair<Integer, String> p3 = new Pair<>(100, "user1");
        Pair<Integer, String> p4 = new Pair<>(100, "user2");

        boolean compare2 = Util.compare(p3, p4);

        if (compare2) {
            System.out.println("key와 value가 모두 일치합니다.");
        } else {
            System.out.println("key와 value가 일치하지 않습니다.");
        }
    }
}

위와 같이, 제네릭 타입을 받는 keyvalue를 갖는 Pair 클래스를 만들고, Util 클래스 메소드의 .compare()로 논리적 동치를 비교하는 예제를 작성해보았다.

Object 객체가 갖는 공통 메소드는 모든 객체의 타입에 무관하게 공통으로 제공되기 때문에, 타입에 무관한 연산을 수행할 수 있는게 핵심이다.

제한된 타입 파라미터()

  • 종종 구체적인 타입을 제한할 필요가 있을 때 사용한다.

    • 이를테면 숫자 연산을 할 때는 Number를 갖는 하위 타입을 받아야 한다.

    • extends 키워드를 이용하지만 인터페이스도 가능하다.

public class NumberUtil {
    public static <T extends Number> double add(T num1, T num2) {
        return num1.doubleValue() + num2.doubleValue();
    }
}

public class Main {
    public static void main(String[] args) {
        int a = 10;
        double b = 20.10;

        double result = NumberUtil.add(a, b);
        System.out.println("result = " + result); // 결과 30.1
    }
}

위는 Number의 하위 타입 값을 받아서 double 형태로 변환한 뒤에 반환하는 메소드의 예제이다.

와일드카드 타입(<?>, <? extends ...>, <? super ...>)

코드의 ?를 일반적으로 와일드카드 타입이라 한다. 제네릭 타입을 파라미터 타입으로 사용하거나 리턴 타입으로 사용할 때, 구체적 타입 대신에 와일드 카드를 다음과 같이 세가지 형태로 사용할 수 있다.

  • 제네릭타입<?>: Unbounded Wildcards (제한 없음)

    • 타입 파라미터를 대치하는 구체적인 타입으로 모든 클래스나 인터페이스 타입이 올 수 있다.

  • 제네릭타입<? extends 상위타입>: Upper Bounded Wildcards (상위 클래스 제한)

    • extends 뒤에 기재한 상위 타입의 하위 타입만 올 수 있다.

  • 제네릭타입<? super 하위타입>: Lower Bounded Wildcards (하위 클래스 제한)

    • super 뒤에 기재한 하위 타입의 상위 타입만 올 수 있다.

위의 와일드 카드 타입이 왜 나왔는지 알려면

위의 현상을 생각해보면 된다.

Object는 모든 것의 상위에 있는 자바 최상위 클래스이지만, ArrayList<Object>ArrayList<String>를 포용할 수 없다. 왜냐하면 제네릭 타입에 대해서는 상속 관계 등이 전혀 상관없기 때문이다.

그래서 말 그대로 제네릭 타입에 어떠한 타입이 들어갈지 모를 때 ?라고 정의해 놓으면 아무런 타입이나 들어갈 수 있는 것이다.

? extends A 와 같은 형식으로 타입을 지정해놓으면 A를 상속한 타입만 하위 타입만 들어갈 수 있게 된다.

? super B와 같은 형식으로 타입을 지정해 놓으면 B가 상속 받는 타입을 포함하여 그 위의 상위 타입만 올 수 있다.

제네릭 타입의 상속과 구현

제네릭 타입도 다른 타입처럼 부모 클래스가 될 수 있다.

public class ChildProduct<M, P, C> extends Product<M, P> {
    private C company;

    public C getCompany() {
        return company;
    }

    public void setCompany(C company) {
        this.company = company;
    }
}

이전에 생성했던 Product<M, P> 클래스에서 제네릭 타입의 회사 정보가 필요하다면, 위와 같이 C 타입을 추가해서 company라는 멤버를 받으면 된다.

Last updated