동글이기가 레포

제네릭 사용법


■ 제네릭이란?

Java에서 데이터 타입을 일반화하는 것을 의미

제네릭은 클래스나 메소드에서 사용할 내부 데이터 타입을 컴파일 시 지정


■ 제네릭을 사용하는 이유

클래스나 메소드 내부에서 사용되는 객체의 타입 안정성을 높임

반환 값에 대한 타입 변환 및 타입 검사에 들어가는 노력 감소


JDK 1.5 이전에는 여러 타이비을 사용하는 클래스나 메소드에 인수나 반환 값으로 Object 타입을 사용
> 이 경우 반환된 Object 객체를 다시 원하는 타입으로 타입 변환을 해야하며, 이를 통한 오류가 발생할 가능성이 있음


따라서 제네릭을 사용하면 컴파일 시 미리 타입이 정해지므로, 타입 검사나 타임 변환의 작업을 생략 가능


■ 제네릭의 선언 및 생성

Java에서 제네릭은 클래스와 메소드에만 선언할 수 있다.

class Test<T> { 
    T element; 
    void setTest(T element) { 
        this.element = element; 
    } T getElement() { 
        return element; 
    } 
}


위의 예제에서 사용한 T를 '타입 변수'라고 하며, 임의의 참조형 타입을 의미한다.


※ 타입 변수

타입 변수는 임의의 참조형 타입을 의미하며, 정해져 있는 단어를 써야하는 것은 아니다.

여러 개의 타입 변수는 쉼표로 구분하여 명시하고

클래스 뿐 아니라 메소드의 매개변수나 반환 값으로 사용할 수 있다.

위에서 정해져 있는 단어를 써야하는 것은 아니지만, 왠만하면 아래의 네이밍 규칙을 지키는 것이 일종의 약속이긴 하다.

(1) T : 타입
(2) V : 값
(3) N : 숫자
(4) K : 키
(5) E : 요소(Element, Java 컬렉션에서 주로 사용)
(6) S : 두 번째에 선언 된 타입
(7) U : 세 번째에 선언 된 타입
(8) V : 네 번째에 선언 된 타입



위에서 생성한 제네릭 클래스를 생성할 때에는 타입 변수 자리에 사용할 실제 타입을 명시해야 한다.

Test<Integer> test1 = new Test<Integer>();


이것은 Integer 타입을 사용하는 예제이며, 이처럼 사용할 실제 타입을 명시하면, 내부적으로 정의된 타입 변수가 명시된 실제 타입으로 변환(T -> Integer)되어 처리가 된다.


그리고 Java SE 7부터 인스턴스 생성 시 타입을 추정할 수 있는 경우에는 타입을 생략할 수 있다.

Test<Integer> test1 = new Test<>();



이러한 지식을 가지고 사용 예제를 만들어보자

public class Main { 
    public static void main(String[] args){ 
        Test<Integer> test1 = new Test<>(); 
        Test<String> test2 = new Test<>(); 
        
        test1.setTest(123); 
        test2.setTest("안녕하세요"); 
        
        System.out.println(test1.getElement()); 
        System.out.println(test2.getElement()); 
        
        System.out.println(test1.getElement().getClass().getName()); 
        System.out.println(test2.getElement().getClass().getName()); 
    } 
}


결과

123
안녕하세요 
java.lang.Integer 
java.lang.String


test1은 Integer를 사용하고 test2는 String을 사용하였다.

각각 데이터를 넣고 출력하였으며, getClass().getName()을 통해 타입을 살펴보았을 때 정상적으로 Integer와 String을 출력하는 것을 볼 수 있다.


■ 제네릭의 제거 시기

Java 코드에서 선언되고 사용된 제네릭 타입은 컴파일 시 컴파일러에 의해 자동으로 검사되어 타입 변환이 된다.

그 후, 코드 내의 모든 제네릭 타입을 제거되어 컴파일된 class 파일에는 어떠한 제네릭 타입도 포함되지 않게 된다.

이렇게 동작되는 이유는 제네릭을 사용하지 않는 코드와 호환성을 유지하기 위함이다.


■ 제네릭 사용 시 주의할 점

1. 참조 변수와 생성자에 대입된 타입 파라미터가 일치해야 함

// 정상 
Test<Integer> test1 = new Test<Integer>(); 

// 에러 
Test<Integer> test2 = new Test<String>();


2. 타입 파라미터가 상속 관계에 있어도 에러 발생

// Child는 Parent의 자손 
Test<Parent> test1 = new Test<Child>();


3. 두 제네릭 클래스의 타입이 상속 관계에 있고, 대입된 타입이 동일

// SubTest는 Test의 자손 
Test<Integer> test1 = new SubTest<Integer>();



제네릭 주요 개념(바운디드 타입, 와일드 카드)

 

바운디드 타입


바운디드 타입은 제네릭 타입의 범위를 제한할 수 있다.

제네릭 타입에 extends를 사용하면, 특정 타입의 자손들만 대입할 수 있게 제한이 가능하다.

처음에 사용한 예시에서 java.lang.Number 클래스를 이용해보자
> 서브 클래스로 Byte, Double, Float, Integer, Long, Short 등이 있다.

class Test<T extends Number> { // extends Number 추가 
    T element;
    void setTest(T element) { 
        this.element = element; 
    } 
    T getElement() { 
        return element; 
    } 
}


Main 클래스의 변화를 살펴보자


Number 클래스의 서브 클래스로 String이 없기 때문에 에러가 발생한 것을 볼 수 있다.


와일드 카드


와일드 카드란 이름에 제한을 두지 않음을 표현하는 데 사용되는 기호를 의미한다.

Java의 제네릭에서 물음표 기호를 사용하여 이러한 와일드 카드를 사용할 수 있다.

<?> // 타입 변수에 모든 타입을 사용할 수 있음
<? extends T> // T 타입과 T 타입을 상속받는 자손 클래스 타입만을 사용할 수 있음 
<? super T> // T 타입과 T 타입이 상속받은 조상 클래스 타입만을 사용할 수 있음


와일드 카드를 이용하여 예제를 만들어보자

class Animal {
    public void cry() { 
        System.out.println("동물"); 
    } 
} 
class Cat extends Animal {
    public void cry() { 
        System.out.println("냐옹");
    }
}
class Dog extends Animal { 
    public void cry() { 
        System.out.println("멍멍"); 
    }
}

class WildCard<T> {
    ArrayList<T> a = new ArrayList<T>();
    public static void crying(WildCard<? extends Animal> list) { 
        Animal b = list.get(0);
        b.cry();
    }
    void add(T animal) {
        a.add(animal); 
    }
    T get(int index) {
        return a.get(index);
    } 
}

public class Test {
    public static void main(String[] args) { 
        WildCard<Cat> catList = new WildCard<Cat>(); 
        WildCard<Dog> dogList = new WildCard<Dog>(); 
        
        catList.add(new Cat()); 
        dogList.add(new Dog()); 
        
        WildCard.crying(catList); 
        WildCard.crying(dogList); 
    } 
}


결과

냐옹 멍멍


위의 예제에서 Animal클래스와 Animal클래스를 상속받는 Cat, Dog 클래스가 있다.

WildCard라는 제네릭을 생성하고 ArrayList를 하나 생성한다.

WildCard안에 static으로 crying이라는 메소드를 만드는데, 매개변수로 Upper Bounded Wildcard를 이용한다.
※ Upper Bounded Wildcard는 <? extends T>를 의미

따라서, crying메소드의 매개변수로는 WildCard<Animal의 자손 클래스>만 가능하다.

crying은 static 메소드이기 때문에 내부적으로 Animal 객체를 생성하고 ArrayList 객체를 전달하기 위해 get메소드를 WildCard안에 생성하였다.

main 함수 내에서는 WildCard<Cat>과 WildCard<Dog> 객체를 만들어주고 add() 메소드를 이용하여
Cat과 Dog의 객체를 넣어주었다.

다음에 crying 메소드에 각 객체를 넣어주는데,
catList와 dogList에 각각 add() 메소드로 Cat과 Dog의 객체를 넣어주었기 때문에 crying() 메소드의 "Animal b = list.get(0)"을 통해 Animal의 cry() 메소드가 각각 Cat과 Dog의 cry() 메소드로 오버라이딩 되었다.

결과적으로 Animal 객체의 cry() 메소드를 실행시킴으로써 "동물"이 아닌 "야옹", "멍멍"이 출력된다.


제네릭 메소드 만들기


제네릭 메소드는 메소드의 선언부에 타입 변수를 사용한 메소드이다.

타입 변수의 선언은 반환 타입 바로 앞에 위치한다.

public static <T> void method() {}


아래의 예제는 대표적인 제네릭 메소드인 Collections.sort()이다.

아래 정의된 타입 변수 T와 제네릭 메소드에서 사용된 타입 변수 T는 전혀 별개의 것이다.

@Contract(mutates = 'param1') 
/unchecked, rawtypes/
public static <T> void sort(List<T> List, Comparator<? super T> c) { 
    list.sort(c); 
}



Erasure


제네릭은 타입의 안전성을 보장하며, 실행 시간에 오버헤드가 발생하지 않도록 하기 위해 추가되었다.

컴파일러는 컴파일 시점에 제네릭에 대하여 타입 이레이저라고 부르는 프로세스를 적용하는데,

타입 이레이저는 모든 타입의 파라미터를 제거하고나서 그 자리를 제한하고 있는 타입으로 변경하거나 타입 파라미터의 제한 타입이 지정되지 않았을 경우에는 Object로 대체한다.
※ <Object>는 보통 생략


1) 일반적인 제네릭

public class Test<T> { 
    private T data; 
    private Test<T> next;
    public Test(T data, Test<T> next) { 
        this.data = data; 
        this.next = next; 
    } 
    public T getData() { 
        return data;
    } 
}


아래로 변경

public class Test { 
    private Object data; 
    private Test next; 
    public Test(Object data, Test next) {
        this.data = data; this.next = next; 
    }
    public Object getData() {
        return data; 
    } 
}



2) 바운디드 타입을 사용할 때

public class Test<T extends Comparable<T>> {
    private T data; 
    private Test<T> next; 
    public Test(T data, Test<T> next) { 
        this.data = data;
        this.next = next;
    }
    public T getData() {
        return data;
    } 
}


아래로 변경

public class Test {
    private Object data;
    private Comparable next; 
    public Test(Object data, Comparable next) { 
        this.data = data; 
        this.next = next;
    } 
    public Object getData() { 
        return data; 
    } 
}



3) 제네릭 메소드

public static <T> int count(T[] anArray, T elem) {
    int cnt = 0; 
    for (T e : anArray) { 
        if (e.equals(elem)) { 
            cnt++; 
        }
    } 
    return cnt; 
}


아래로 변경

public static int count(Object[] anArray, Object elem) { 
    int cnt = 0; 
    for (Object e : anArray) {
        if (e.equals(elem)) { 
            cnt++;
        } 
    }
    return cnt; 
}

'스터디 > 동기 Java 스터디' 카테고리의 다른 글

[QueryDSL] QueryDSL이란?  (0) 2022.04.17
9주차 : 람다식  (0) 2021.05.26
8주차 : I/O  (0) 2021.05.19
8주차 : 애노테이션  (0) 2021.05.17
7주차 : Enum  (0) 2021.05.13

공유하기

facebook twitter kakaoTalk kakaostory naver band
loading