ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [JAVA] 제네릭
    study/java 2020. 8. 8. 00:31

    제네릭 ( Generic )

    클래스 내부에서 사용할 데이터 타입을 외부에서 지정하는 기법

    다양한 타입의 객체들을 다루는 메소드나 컬렉션 클래스에 컴파일 시의 타입체크를 해주는 기능

     

     

    제네릭이 필요한 이유

    public class ArrayListEx {
        private int size;
        private Object[] elements = new Object[5];
    
        public void add(Object value){
            elements[size++] = value;
        }
    
        public Object get(int index){
            return elements[index];
        }
    }
    

     위의 코드는 ArrayList를 직접 만든 예시다.

    public class Main {
        public static void main(String[] args) {
            ArrayListEx list = new ArrayListEx();
    
            list.add(10);
            list.add(25);
    
            Integer value1 = (Integer) list.get(0);
            Integer value2 = (Integer) list.get(1);
    
            System.out.println(value1 + value2);
        }
    }
    

    위의 코드를 실행시켜보면 컴파일도 잘되고 문제없이 동작한다.

     

    add() 메소드는 파라미터로 Object 타입을 받는다. 

    모든 클래스는 Object 클래스의 상속을 받기 때문에 Object 타입으로 받으면 그 어떤 타입이라도 받을 수 있다.

    그러므로 get() 메소드를 호출할 때 형변환만 잘 해주면 어떤 데이터 타입도 저장할 수 있다.

     

    public class Main {
        public static void main(String[] args) {
            ArrayListEx list = new ArrayListEx();
    
            list.add("10"); // 변경
            list.add("25"); // 변경
    
            Integer value1 = (Integer) list.get(0);
            Integer value2 = (Integer) list.get(1);
    
            System.out.println(value1 + value2);
        }
    }
    

     

    위의 코드는 어떨까?

    add() 메소드는 Object 타입은 모두 받을 수 있기 때문에 위의 코드처럼 String을 받아도 문제가 없다.

    컴파일도 문제가 없지만 위 코드는 런타임때 ClassCastException이라는 예외를 발생시킨다.

    이는 잘못된 타입 변환이 이루어졌다는 오류메세지이다.

    list에 String 타입의 값을 넣어놓고 Integer로 형변환했기 때문에 발생한 오류다.

     

    위의 방식처럼 하는 경우 어떤 타입으로 형변환을 할 수 있는지 모호한 경우가 많기 때문에 잠재적인 오류를 가지고 있다.

    이를 컴파일 시점에서 발견할 수 있다면 더욱 좋을 것이다.

     

    물론 ArrayListEx클래스를 Integer를 위한 클래스, String을 위한 클래스들로 만들 수도 있을 것이다.

    하지만 add()메소드와 get()메소드의 구현내용과 필드가 모두 같기 때문에 코드의 중복이 발생하게 된다.

     

    이때 제네릭을 사용하면 문제를 해결할 수 있다.

    public class GenericArrayListEx<T> {
    
        private Object[] elements = new Object[5];
        private int size;
    
        public void add(T value) {
            elements[size++] = value;
        }
    
        public T get(int index) {
            return (T) elements[index];
        }
    }

    <T> 로 표현한 것이 제네릭이다.

    GenericArrayListEx라는 클래스를 객체로 생성할 때 타입을 지정하면 생성되는 오프젝트 안에서 T의 위치에 지정한 타입이 대체되어서 들어가는 것처럼 컴파일러가 인식한다.

    List<String> 을 사용하는 것을 생각하면 된다.

    public class Main {
        public static void main(String[] args) {
            GenericArrayListEx<Integer> list = new GenericArrayListEx<>();
            
            list.add(1);
            list.add(2);
    
            int value1 = list.get(0); // 형변환이 필요없다
            int value2 = list.get(1); // 형변환이 필요없다
    
            System.out.println(value1 + value2);
        }
    }
    

    위의 코드처럼 사용하면 된다.

    제네릭을 사용할 때의 이점은 두 가지이다.

    1. 형변환할 필요가 없다는 것

    2. 타입을 지정해줄 때의 문제를 컴파일 시점에서 발견할 수 있는 것

     

     

    한정적 타입 매개변수 ( Bounded Type Parameter )

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

    위에서 만든 GenericArrayListEx가 Number의 서브클래스만을 타입으로 갖도록 하고 싶을 때 아래와 같이 타입을 제한할 수 있다.

    public class GenericArrayListEx<T extends Number>{
    	...
    }

    위처럼 정의했다면 String은 담길 수 없다.

    Number의 상위 클래스만으로 제한하고 싶은 경우는

    public class GenericArrayListEx<T super Number>{
    	...
    }

    이런식으로 할 수 있다.

     

    한정적 타입 매개변수가 사용되는 가장 흔한 예시는 Comparable을 적용하는 경우다.

    T extends Comparable<T> 처럼 정의를 하면 Comparable 인터페이스의 서브 클래스들만 타입으로 사용하는 것이다.

    Comparable 인터페이스를 구현하기 위해서는 반드시 compareTo() 메소드를 정의해야 하기 때문에 Comparable 인터페이스를 구현하는 클래스들은 비교가 가능한 타입이 된다.

    public class Pair implements Comparable<Pair>{
            private int num;
    
            public Pair(int num) {
                this.num = num;
            }
    
            @Override
            public int compareTo(Pair o) {
                return this.num > o.num ? 1 : -1;
            }
    }

     

     

    제네릭을 사용할 수 없는 경우

    GenericArrayListEx 를 정의할 때 다른 부분에는 모두 T를 사용했는데 배열을 생성하는 부분에서는 T를 사용하지 않고 Object를 사용했고 get() 메소드를 호출할 때 T 타입으로 형변환을 하는 코드를 사용했다.

    GenericArrayListEx가 가지는 elements도 new T[5]처럼 하면 되는데 왜 get() 메소드에서 형변환을 통해 반환을 할까?

     

    그 이유는 new 연산자 때문이다.

    new 연산자는 먼저 heap 영역에 충분한 공간이 있는지 확인을 한 후에 메모리를 확보하는 역할을 한다.

    충분한 공간이 있는지 확인을 하기 위해선 먼저 타입을 알아햐 하는데 컴파일 시점에서 타입 T가 무엇인지 알 수 없으므로 new T[5]와 같이 제네릭으로 배열을 생성할 수 없다.

     

    static 변수에도 제네릭을 사용할 수 없다.

    static 변수는 인스턴스에 종속되지 않는 클래스 변수로 모든 인스턴스가 공통된 저장공간을 공유하게 된다.

    static 변수에 제네릭을 사용하려면  GenericArrayListEx<Integer> 에서는 Integer 타입으로, GenericArrayListEx<String> 에서는 String 타입으로 사용될 수 있어야 하는데 이를 공유변수로 두고 생성되는 인스턴스에 따라서 타입이 바뀐다는 것은 말이 안된다.

    하지만 static 메서드에는 제네릭을 사용할 수 있다.

     

    제네릭 메서드

    제네릭 메서드를 정의할 때는 반환하는 타입이 어떤 것인지와는 관계없이 개발자가 직접 제네릭 메서드라는 것을 컴파일러에게 알려야한다.

    그러기 위해서는 반환 타입을 정의하기 전에 제네릭 타입에 대한 정의가 반드시 필요한다.

    또 제네릭 클래스가 아닌 일반 클래스 내부에서도 제네릭 메서드를 정의할 수 있다.

    즉 클래스에 지정된 타입 파라미터와 제네릭 메서드에 정의된 타입 파라미터는 상관이 없다는 것이다.

    제네릭 클래스에서 <T>를 사용하고 같은 클래스의 제네릭 메서드에서 <T>로 같은 이름을 가진 타입 파라미터를 사용하더라도 둘은 전혀 상관이 없다는 것이다.

     

    static 메서드에 제네릭을 사용할 수 있는 이유는 static 메서드의 경우 메서드의 틀만 공유되기 때문이라고 생각하면 된다.그리고 그 틀 안에서 지역변수처럼 타입 파라미터가 다양하게 오가는 형태로 사용될 수 있다.이는 static 메서드 뿐만아니라 다른 인스턴스 메서드도 마찬가지다.

    클래스에 정의된 타입 파라미터와는 별개로 제네릭 메서드는 자신만의 타입 파라미터를 가진다.

     

    이때 주의 사항이 있다.

    public static void printFirstChar(T param) {
        System.out.println(param.charAt(0));
    }

    위 코드를 보면 두 가지 오류를 확인할 수 있다.

    1. param의 타입인 T를 알 수 없다는 점

    2. T의 타입을 알 수 없기 때문에 charAt() 메서드 호출이 불가능하다는 점

     

    허용되지 않은 이유는 위의 메서드가 제네릭 메서드가 아니기 때문이다.반환 타입 앞에 제네릭에 대한 선언이 없다.

    public static <T extends CharSequence> void printFirstChar(T param) {
        System.out.println(param.charAt(0));
    }

    static 메서드에서 제네릭을 사용하려면 위처럼 해야한다.

    T가 CharSequence의 서브클래스만을 허용한다는 것을 보면 charAt() 메서드를 사용할 수 있다는 것을 알 수 있다.

    위 메서드를 호출하면 아래와 같이 된다.

    GenericArrayListEx.<String>printFirstChar("JAEHO");

    위의 경우 파라미터를 통해 타입 추론이 가능하므로 타입은 생략할 수 있다.

    GenericArrayList.printFirstChar("JAEHO");

     

     

    결론

    제네릭을 사용하는 이유는 두 가지

    1. 형변환이 필요없고 타입 안정성이 보장

    2. 코드의 재사용성

     

    참고

    https://yaboong.github.io/java/2019/01/19/java-generics-1/

    'study > java' 카테고리의 다른 글

    [JAVA] hashCode & equals  (0) 2020.09.02
    [JAVA] Exception  (0) 2020.08.21
    [JAVA] GC  (0) 2020.08.06
    [JAVA] Jar,War  (0) 2020.08.04
    [JAVA] static 키워드  (0) 2020.07.30
Designed by Tistory.