ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Java] 제네릭 (Generic)
    언어/Java 2021. 1. 30. 19:16

    제네릭은 JDK 1.5에서 첫 도입이 되었고 클래스 내부에서 사용할 데이터 타입을 외부에서 지정하는 기법이며 다양한 타입의 객체들을 다루는 메서드나 컬렉션 클래스에 컴파일 시의 타입체크를 해줍니다. 즉, 타입을 외부에서 동적으로 정의할 수 있고 런타임에서 발생할 오류들을 컴파일 타임에 발견할 수 있도록 해줍니다.

    처음에는 제네릭에 대해 잘 몰랐을 때, 최상위 객체로 값을 받아서 형변환만 해주면 되지 않나? 라고 생각했는데 아래와 같은 한계를 보고 제네릭이 무엇인지 감을 잡았습니다.

    public class CustomArrayList {
        private int size;
        private Object[] data = new Object[3];
    
        public void add(Object value) {
            data[size++] = value;
        }
    
        public Object get(int i) {
            return data[i];
        }
    }
    
    
    
    CustomArrayList customArrayList = new CustomArrayList();
    
    customArrayList.add(10);
    customArrayList.add(100);
    
    Integer i = (Integer) customArrayList.get(0); // 강제 형변환으로 Integer 타입으로 변환


    위와 같이 Object 객체 타입으로 받아서 처리하도록 하면 형변환이 귀찮지만 오류가 발생하지 않습니다. 

    CustomArrayList customArrayList = new CustomArrayList();
    
    customArrayList.add("10");
    
    Integer i = (Integer) customArrayList.get(0); // 강제 형변환으로 Integer 타입으로 변환


    그러나 위와 같이 입력하는 값을 문자열 타입으로 한다면 컴파일에서는 오류가 발생하지 않지만 (String 타입을 Object로 변경하기 때문) 런타임에서 String 타입을 Integer로 강제 형변환을 하려고 하여 오류가 발생합니다. 이러한 문제를 해결하려면 타입 별로 CustomIntegerArrayList, CustomStringArrayList와 같이 만들어야 합니다.

    제네릭 타입

    이를 해결하기 위해서 나온 기법이 제네릭입니다. 아래는 제네릭을 사용해 위의 문제를 해결한 예시입니다.

    public class CustomArrayList<T> {
        private int size;
        private Object[] data = new Object[3];
    
        public void add(T value) {
            data[size++] = value;
        }
    
        public T get(int i) {
            return (T) data[i];
        }
    }
    
    
    
    
    // 외부에서 타입 주입
    CustomArrayList<Integer> customArrayList = new CustomArrayList<>();
    customArrayList.add(10);
    Integer i = customArrayList.get(0);
    String i1 = customArrayList.get(0); // 컴파일 에러
    
    // 외부에서 타입 주입
    CustomArrayList<String> customArrayList1 = new CustomArrayList<>();
    customArrayList1.add("10");
    String s = customArrayList1.get(0);


    <T>와 같이 타입 파라미터를 주입하는 형식이 제네릭입니다. 컴파일러는 T의 위치에 지정된 타입이 대체되는 것으로 인식합니다. 이러한 방식의 이점은 사용하는 측에서 형변환이 필요 없다는 점과 잘못된 타입의 변수에 저장하려 하면 컴파일 에러가 발생한다는 점입니다.


    다시 말해 제네릭은 타입을 파라미터로 가지는 클래스와 인터페이스를 말하며 위에서 설명한 것과 같이 <타입 파라미터> 가 붙습니다.

    public class CustomArrayList<T> { ... }
    public interface CustomArrayList<T> { ... }


    타입 파라미터는보통 알파벳 대문자 1개로 표현하며 Integer, String과 같이 의미가 없습니다(코드 내에서 통일된 타입 파라미터면 모두 가능한 것으로 보임). 아무렇게나 사용할 수 있기 때문에 Type의 T나 Key의 K와 같이 사용하는 것이 관행으로 보입니다.

    멀티타입 파라미터

    위의 형태를 두 개 이상 사용할 수 있습니다.

    public class CustomMap<K, V> { ... }

    제네릭 메소드

    매개변수 타입과 반환 타입으로 타입 파라미터를 갖는 메소드를 말합니다. 제네릭 클래스 뿐만 아니라 일반 클래스 내에서도 선언하여 사용할 수 있습니다.

    public <타입파라미터> 리턴타입 메소드명(매개변수) { ... }
    
    
    
    
    public class Main {
        public <T> boolean test(T t) {
            return t instanceof String;
        }
    }
    
    
    var aa = test("S");
    System.out.println(aa); // true
    
    var bb = test(100);
    System.out.println(bb); // false

    static 제네릭 메소드

    제네릭 메소드에 static 을 부착하여 사용할 수 있습니다. 메소드의 틀만 공유하고 그 틀 안에서 지역 변수처럼 타입 파라미터가 다양하게 오가는 형태로 사용될 수 있습니다. 

    public class CustomArrayList {
        public static <T> boolean test(T t) {
            return t instanceof String;
        }
    }
    
    
    
    
    var b = CustomArrayList.<String>test("a");
    var a = CustomArrayList.test("a"); // 컴파일러가 파라미터의 타입을 보고 추측할 수 있어서 생략 가능

    제한된 타입 파라미터

    제네릭으로 사용될 타입 파라미터의 범위를 제한할 수 있습니다.


    만약 List 하위 클래스만 타입으로 받고 싶을 경우, 아래와 같이 extends를 선업합니다.

    public class CustomArrayList <T extends List>{
        public T temp;
    }
    
    
    
    
    CustomArrayList<ArrayList> customArrayList = new CustomArrayList<>();
    CustomArrayList<Integer> customArrayList1 = new CustomArrayList<>(); // List의 하위 클래스가 아니라서 컴파일 에러


    만약 제한을 하위가 아니라 상위로 두고 싶을 경우, 아래와 같이 super를 선언합니다.

    public class CustomArrayList <T super ArrayList>{
        public T temp;
    }
    
    
    CustomArrayList<List> customArrayList = new CustomArrayList<>();

    (라고 나와 있지만 왜인지는 모르게 에러가 나고 있음..)

    와일드 카드 타입

    와일드카드 타입에는 총 세가지의 형태가 있으며 ? 키워드로 표현됩니다. 제네릭 타입을 매개 변수나 반환 타입으로 사용할 때, 타입 파라미터를 제한하는 목적으로 사용합니다. 

    제네릭타입<?> : 타입 파라미터를 대치하는 것으로 모든 클래스나 인터페이스타입이 올 수 있습니다.

    제네릭타입<? extends 상위타입> : 와일드카드의 범위를 특정 객체의 하위 클래스만 올 수 있습니다.

    제네릭타입<? super 하위타입> : 와일드카드의 범위를 특정 객체의 상위 클래스만 올 수 있습니다.

    public class CustomArrayList{
        public static void test(List<?> list) { }
    
        public static void test1(List<? extends ArrayList> list) { }
    
        public static void test2(List<? extends List> list) { }
    }

    제네릭 타입 상속과 구현

    제네릭 타입을 선언한 클래스가 부모인 경우

    자식 클래스에도 작성을 해줘야 하며, 타입 파라미터를 추가할 수 있습니다.

    public class ChildList<T,M> extends ParentList<T,M> { }
    public class ChildList2<T,M,K> extends ParentList<T,M> { }

    제네릭 인터페이스를 구현할 경우

    구현 클래스에도 타입 파라미터를 추가해야 합니다.

    public class Impl<T> implements TestInterface<T> { }


    댓글