0 + 프로그래밍/0 + Java

[10분 테코톡] 나는 제너릭을 모르고 개발했다.

힘들면힘을내는쿼카 2023. 4. 19. 15:03
728x90
반응형

[10분 테코톡] 나는 제너릭을 모르고 개발했다.

Java로 개발을 진행하다 보면 제너릭이라는 표현을 들어보셨을 것 입니다.
(<T>, <? Extends T>, <? Super T> 등과 같은 문법)

 

저는 List<String>, Queue<Integer> 등과 같이 <>안에 데이터 타입을 정해서 사용하는 거구나!
정도로만 이해하고 넘겼습니다.

 

하지만, 이제는 제너릭에 대해서 제대로 이해하고, 왜 등장하게 되었는지 알아보려고 합니다!

 

 

제너릭

Java에서 제너릭은(Generic)은 클래스 내부 또는 메소드 내부에서 사용할 데이터 타입을 외부에서 지정하는 기법을 의미 합니다.

 

제너릭 클래스

말이 조금 어려운데 코드를 통해 알아봅시다.

 

이런 방식으로 클래스 내부에서 사용할 데이터 타입을 외부에서 지정할 수 있습니다.

class Member<T> {
    private T t;
}

public class Generic {
    Member<String> member1 = new Member<>();
    Member<StringBuilder> member2 = new Member<>();
}

 

결과적으로 member1.tmember2.t의 데이터 타입은 다음과 같습니다.

  • member1.t: String
  • member2.t: StringBuilder

 

데이터 타입은 각각의 인스턴스를 생성할 때 사용한 <>사이에 어떤 데이터 타입을 사용했느냐에 따라서 변하게 됩니다.

 

클래스를 정의 할 때 t의 데이터 타입을 확정하지 않고,
인스턴스를 생성할 때 데이터 타입을 지정
하는 기능이 제네릭이다.

 

근데 제너릭을 왜 사용하는 걸까요? 🤔

 

제너릭을 사용하는 이유

타입 안정성

타입 안전성?
코드를 통해 타입 안정성을 느껴봅시다.

class StudentInfo {
    public int grade;
    public StudentInfo(int grade) {
        this.grade = grade;
    }
}

class StudentPerson {
    public StudentInfo info;

    public StudentPerson(StudentInfo info) {
        this.info = info;
    }
}

class EmployeeInfo {
    public int rank;

    public EmployeeInfo(int rank) {
        this.rank = rank;
    }
}

class EmployeePerson {
    public EmployeeInfo info;

    public EmployeePerson(EmployeeInfo info) {
        this.info = info;
    }
}

public class GenericDemo {
    public static void main(String[] args) {
        StudentInfo studentInfo = new StudentInfo(2);
        StudentPerson studentPerson = new StudentPerson(studentInfo);
        System.out.println(studentPerson.info.grade);

        EmployeeInfo employeeInfo = new EmployeeInfo(1);
        EmployeePerson employeePerson = new EmployeePerson(employeeInfo);
        System.out.println(employeePerson.info.rank);
    }
}

 

이 코드에서 StudentPersonEmployeePerson은 동일한 구조를 갖고 있습니다.
중복이 발생하고 있습니다!!


중복을 제거해 볼까요?

 

StudentPersonEmployeePerson공통 부모 클래스를 사용하여 중복을 제거하면 됩니다.
하지만, 현재 부모 클래스가 존재하지 않습니다.
따라서 모든 클래스의 조상인 Object를 사용했습니다.

class Person {
    public Object info;

    public Person(Object info) {
        this.info = info;
    }
}

 

Person 클래스안에 멤버 변수인 인스턴스 변수 info를 보고
아래와 같이 main문을 수정 했습니다.

public class GenericDemo {
    public static void main(String[] args) {
        /**
         * 1. person.info는 Object 타입
         * 2. "사장"을 선언하여 String 타입으로 선언(런타임 시점에 String으로 사용)
         * 3. person.info를 EmployeeInfo로 형변환
         * 4. 컴파일 시점에서는 Person의 인자는 Object타입인 String이기 때문에 문제가 없음
         * 5. 런타임 시점에 person.info가 String이 아니고 EmployeeInfo이기 때문에 에러 발생
         */
        Person person = new Person("사장");
        EmployeeInfo employeeInfo = (EmployeeInfo) person.info;
        System.out.println(employeeInfo.rank);
    }
}

 

이 코드는 문법상에는 아무 문제가 없지만(컴파일 시점에서 오류가 발생하지 않지만),
실행하면 ClassCastException라는 런타임 예외가 발생합니다.

 

왜 그런것 일까요? 🤔
Person 생성자의 인자는 Object 입니다.
위 코드에서 우리는 Person의 인자에 사장 이라는 String 객체를 선언 했습니다.
StringObject 이기 때문에 컴파일 시점에는 아무 이상이 없는 것이죠.

 

하지만, 런타임 시점에는 상황이 다릅니다.
앞에서 우리는 사장이라는 String을 넣었기 때문에 person.infoString 입니다.
그런데 우리가 EmployeeInfo로 형변환을 했습니다.

그래서 런타임 시점에 예외가 발생하는 것 입니다.
(String과 EmployeeInfo는 아무런 관계가 없습니다.!)

 

이러한 상황을 "타입이 안전하지 않다." 라고 이야기 합니다.

 

정리

  • person.infoObject 타입
  • 사장을 선언하여 String 타입으로 선언(런타임 시점에 String으로 사용)
  • person.infoEmployeeInfo로 형변환
  • 컴파일 시점에서는 Person의 인자는 Object타입인 String이기 때문에 문제가 없음
  • 런타임 시점에 person.infoString이 아니고 EmployeeInfo이기 때문에 런타임 시점에 에러 발생

 

이러한 문제를 해결할 수 있는 방법은 없을까요? 🤔

 

제너릭의 등장

이러한 문제를 해결하기 위해서 제너릭이 등장한 것입니다.
데이터 타입에 대해서 개발자의 실수, 착오를 예방하기 위해서 등장 했다고 할 수 있습니다.

 

즉, 컴파일 시점에러를 타입을 지정하여 런타임 시점에서 안정성을 보장받기 위해 등장!

 

위 코드를 제너릭화 해봅시다.

class Person<T> {
    public T info;

    public Person(T info) {
        this.info = info;
    }
}

class EmployeeInfo {
    public int rank;

    public EmployeeInfo(int rank) {
        this.rank = rank;
    }
}

public class GenericDemo {
    public static void main(String[] args) {
          // Person에는 EmployeeInfo타입의 데이터만 들어오도록 한다.
        Person<EmployeeInfo> person1 = new Person<>(new EmployeeInfo(1));
        EmployeeInfo info1 = person1.info;
        System.out.println(info1.rank);

        Person<String> person2 = new Person<>("사장");
        String info2 = person2.info;
        System.out.println(info2.rank); // 컴파일 에러
    }
}

 

person2컴파일 에러가 발생한 것을 알 수 있습니다.
Pserson2.infoString인데 Stringrank 필드가 없는데 호출하고 있기 때문입니다.

 

정리

// Person<EmployeeInfo>의 의미는 
// Person에EmployeeInfo타입의 데이터만 들어오도록 한다.
Person<EmployeeInfo> person1 = new Person<>(new EmployeeInfo(1));
  • 컴파일 단계에서 오류를 검출할 수 있다. 👍
  • 중복 제거타입 안정성을 챙길 수 있다. 👍

 

제너릭의 복수화

그런데, 이런 생각을 할 수 있습니다.
지정해야하는 데이터 타입이 여러개면 어떻게 해야할까요? 🤔

 

간단합니다.


데이터 타입(타입 파라미터)을 여러개를 만들면 됩니다!
코드를 보면 다음과 같습니다.

// S는 T와 구분하기 위해 다른 문자
class Person<T, S> {
    public T info;
    public S id;

    public Person(T info, S id) {
        this.info = info;
        this.id = id;
    }
}

 

여기서 눈치 채신분들도 있을 텐데 👀
제너릭에는 반드시 참조형 데이터 타입 올 수 있습니다.

 

그러면 기본 데이터 타입은 사용이 불가능한가요?🤔
아닙니다.
바로 래퍼(wrapper) 클래스를 사용 하면 됩니다.^^

class EmployeeInfo {
    public int rank;

    public EmployeeInfo(int rank) {
        this.rank = rank;
    }
}

// S는 T와 구분하기 위해 다른 문자
class Person<T, S> {
    public T info;
    public S id;

    public Person(T info, S id) {
        this.info = info;
        this.id = id;
    }
}

public class GenericDemo {
    public static void main(String[] args) {
          // 제너릭에는 반드시 참조형 데이터 타입만 올 수 있습니다.
        Person<EmployeeInfo, Long> person = new Person<>(new EmployeeInfo(1), 1L);

        Long id = person.id;
        EmployeeInfo info = person.info;
        System.out.println("info.rank = " + info.rank);
        System.out.println("id = " + id);
    }
}

 

Spring Data JPA를 사용자라면 다음과 같은 인터페이스를 작성한 경험이 있을 것 입니다.^^

public interface MemberRepository extends JpaRepository<Member, Long> {
}

 

제너릭의 생략

제너릭은 생략이 가능합니다.

// 제너릭 생략 X
Person<EmployeeInfo, Long> person1 = new Person<>(new EmployeeInfo(1), 1L);

// 제너릭 생략 O
EmployeeInfo employeeInfo = new EmployeeInfo(1);
Long id = 1L;
// employeeInfo, id라는 매개변수를 통해 제너릭 생략 가능
Person person = new Person<>(employeeInfo, id);

 

개인적으로는 제너릭을 생략하면 가독성이 오히려 나빠진다고 생각하여 굳이 생략하지 않습니다!

 

제너릭 메소드

제너릭은 😎 놀랍게도 메소드에서도 사용이 가능합니다.

정의를 하자면 매개변수 타입리턴 타입으로 타입 파라미터를 갖는 메소드 입니다.

 

 

말이 조금 어려운데 쉽게 이야기 하면,
메소드에서 사용할 데이터 타입을 외부에서 지정하는 방법입니다.

// S는 T와 구분하기 위해 다른 문자
class Person<T, S> {
    public T info;
    public S id;

    public Person(T info, S id) {
        this.info = info;
        this.id = id;
    }

    // 제너릭 메소드
    // 매개변수 데이터 타입을 확정하고 싶지 않아서 사용
    public <T> void printInfo(T methodInfo) {
        System.out.println(methodInfo);
    }
}

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

        EmployeeInfo employeeInfo = new EmployeeInfo(1);
        Long id = 1L;
        // employeeInfo, id라는 매개변수를 통해 제너릭 생략 가능
        Person person = new Person<>(employeeInfo, id);

        person.<EmployeeInfo>printInfo(employeeInfo);
        // employeeInfo라는 매개변수를 통해 제너릭 생략 가능
        person.printInfo(employeeInfo.rank);

        // 제너릭 메소드의 타입 매개변수 우선 처리
        person.printInfo("이지금");
    }
}

 

person은 제너릭 클래스의 <T, S>EmployeeInfoLong으로 선언되었는데, person.printInfo(“이지금”)을 보면 String가 선언된 것을 볼 수 있습니다.
아니…. 같은 <T>인데 어떻게 실행 되는 거지?

 

제너릭 메소드의 타입 매개변수와 제너릭 클래스의 타입 배개변수가 같은 경우
제너릭 메소드의 <T>와 제너릭 클래스의 <T>가 같다면(같다는 것은 알파벳 T가 같다는 의미 입니다.) 제너릭 메소드의 타입 매개변수(<T>에 들어온 타입)을 우선적으로 처리 합니다.

 

왜 제너릭 메소드를 사용하는거야? 😲
메소드를 정의 할 때 methodInfo의 데이터 타입을 확정하지 않고 메소드를 사용할 때 데이터 타입을 지정할 수 있습니다.

 

다시 말해서, 메소드 안에서 매개변수의 데이터 타입을 확정하고 싶지 않기 위해서 사용 합니다.
제너릭 메소드는 여러 타입을 받아서 처리하려는 의도로 사용하는 것입니다.

 

참고
위와 같이 제너릭 클래스의 타입 파라미터와, 제너릭 메소드의 타입 파라미터가 동일한 문자(<T>)를 사용하면 가독성이 떨어지기 때문에 다른 문자를 사용하는 것을 권장 드립니다.

// 제너릭 메소드
// 매개변수 데이터 타입을 확정하고 싶지 않아서 사용
// T 대신 U 사용
public <U> void printInfo(U info) {
    System.out.println(info);
}

 

제너릭의 제한

제너릭이라는 것은 클래스나 메소드에 내부 데이터 타입을 외부에서 지정하도록 하는 기법 입니다.

이 말은 클래스나 메소드가 정하지 않은 데이터 타입이 외부에서 지정한 데이터 타입으로 인스턴스화 될 때 확정된다는 의미 입니다.

 

이렇다 보니 제너릭에는 오만가지 참조형 데이터 타입이 들어올 수 있는 문제가 발생합니다.🤯

 

그래서 나온 개념이 제너릭의 제한 입니다. 🤔

 

extends

extends를 사용하면 제너릭으로 올 수 있는 데이터 타입을 특정 클래스의 자식 클래스로 제한할 수 있습니다.

// Info는 class를 사용해도 되고 interface를 사용해도 됩니다.
abstract class Info {
    public abstract int getLevel();
}

class EmployeeInfo extends Info {
    public int rank;

    public EmployeeInfo(int rank) {
        this.rank = rank;
    }

    @Override
    public int getLevel() {
        return this.rank;
    }
}

// T로 올 수 있는 데이터 타입을 Info또는 Info의 자식 클래스로 제한
class Person<S, T extends Info> {
    public T info;
    public S id;

    public Person(T info, S id) {
        this.info = info;
        this.id = id;
          // T를 Info로 제한했기 때문에 사용 가능
          info.getLevel();
    }

    // 제너릭 메소드
    // 매개변수 데이터 타입을 확정하고 싶지 않아서 사용
    public <U> void printInfo(U info) {
        System.out.println(info);
    }
}

 

이제 제너릭 클래스를 사용해보겠습니다.

public class GenericDemo {
    public static void main(String[] args) {
        // EmployeeInfo는 Info의 자식 입니다.
        Person<Long, EmployeeInfo> person = new Person<>(new EmployeeInfo(1), 1L);
        // String 은 Info의 자식이 아닙니다.
        // 컴파일 에러
        Person<Long, String> stringPerson = new Person<Long, String>("이지금");
    }
}

 

StringInfo의 자식이 아니기 때문에 컴파일 에러가 발생합니다.

 

공변과 불공변

제너릭을 깊게 공부하다보면 공변, 불공변이라는 개념을 마주쳐야 합니다. 😭

 

공변

AB의 하위 타입일 때, T<A>T<B>의 하위 타입이면 T는 공변 입니다.

말이 조금 어려운데 배열로 예를 들어 보겠습니다.
아래 코드는 정상적으로 실행이 됩니다.
왜냐하면 Integer의 부모는 Object이기 때문 입니다.

void genericTest() {
    Integer[] integers = new Integer[]{1, 2, 3};
    printArray(integers);
}

void printArray(Object[] arr) {
    for (Object e : arr) {
        System.out.println(e);
    }
}

 

따라서 IntegerObject의 하위 타입일 때, 배열 Integer[]배열 Object[]의 하위 타입이면 배열공변 입니다.

 

불공변

AB의 하위 타입일 때, T <A>T<B>의 하위 타입이 아니면 T는 불공변 입니다.

이것도 말이 어려운데, 제너릭으로 예를 들어보겠습니다.
아래 코드는 컴파일 에러가 발생합니다.
그 이유는 IntegerObject의 자식 클래스 이지만, List<Integer>List<Object>의 자식 클래스가 아니기 때문이죠!

void genericTest() {
    List<Integer> list = Arrays.asList(1, 2, 3);
    printCollection(list);   // 컴파일 에러 발생
}

void printCollection(Collection<Object> c) {
    for (Object e : c) {
        System.out.println(e);
    }
}

 

<Object> 타입이 필요한데 <Integer> 타입이 왔기 때문입니다.

 

따라서 IntegerObject의 하위 타입일 때, List< Integer>List<Object>의 하위 타입이 아니면 List불공변 입니다.

 

와일드 카드<?>

위에서 보신것 처럼 제너릭은 불공변이라서 모든 타입에 공통적으로 사용되는 메소드를 만들 방법이 없습니다… 😲

다시 이야기 하면, printCollection의 타입을 Integer에서 Object로 변경하여도 제네릭이 불공변이기 때문에 Collection<Object>Collection<Integer>의 하위타입이 아니기 때문에 컴파일 에러가 발생하는 것 입니다.

 

제너릭을 공변처럼 사용하기 위해서 와일드 카드가 등장합니다. 👍
즉, 모든 타입을 대신할 수 있는 와일드카드 타입(<?>)을 추가했습니다.

 

와일드 카드는 정해지지 않은 unknown type이기 때문에 Collection<?>로 선언하여 모든 타입에 대한 호출이 가능해졌습니다.

// 와일드 카드<?> 적용
void printCollection(Collection<?> c) {
    for (Object e : c) {
        System.out.println(e);
    }
}

 

그런데 데이터를 삽입할 때 문제가 있습니다….

void genericTest() {
    Collection<?> collection = new ArrayList<>();
      collection.add(new Integer(1));
}

 

와일드 카드로 선언된 타입은 unknown type이기 때문에 컴파일 문제가 발생 합니다.

 

컬렉션의 add로 값을 추가하면 제너릭 타입인 E 또는 E의 자식을 넣어줘야 합니다.
그런데 지금 E?인 와일드 카드를 넣었습니다.

 

와일드 카드는 unknown type이므로 Integer, String 또는 개발자가 추가한 클래스까지 될 수 있기 때문에 범위가 무제한(♾️)입니다.
와일드카드의 경우 add로 넘겨주는 파라미터가 unknown type의 자식이여야 하는데, 정해지지 않았으므로 어떠한 타입을 대표하는지 알 수 없어서 자식 여부를 검사할 수 없는 것입니다.

 

근데 왜 와일드 카드의 데이터를 꺼낼때는 문제가 없을까요? 🤔
값을 꺼낸 결과가 unknown type 이여도 해당 타입이 어떤 타입의 자식 타입인지 확인 할 필요가 없기 때문 입니다.
(최소한 Object 타입임을 보장)

 

이러한 문제를 해결하기 위해 한정적 와일드 카드를 제공하고 있습니다.

 

한정적 와일드 카드에 들어가기에 앞서….

먼저 아래와 같이 클래스가 정의되었다고 합시다.

class MyGrandParent {}
class MyParent extends MyGrandParent {}
class MyChild extends MyParent {}

 

한정적 와일드 카드를 이해하기 전에 다음과 같은 코드를 먼저 살펴 봅시다.

public void printAndAdd(Collection<MyParent> c) {
    // 자식(MyParent)은 부모(MyGrandParent)로 표현할 수 있다.
    for(MyGrandParent e : c) {
        System.out.println(e);
    }

    // 부모(MyParent)는 자식(MyChild)으로 표현할 수 없다.
      // 컴파일 에러
    for(MyChild e : c) {
        System.out.println(e);
    }

    // 부모(MyParent)는 자식(MyChild)을 저장할 수 있다.
    c.add(new MyChild());

    // 자식(MyParent)은 부모(MyGrandParent)를 저장할 수 없다.
    // 컴파일 에러
    c.add(new MyGrandParent());
}

 

객체지향에서 자식은 부모로 표현될 수 있습니다.
부모는 자식으로 표현이 불가능 하죠.

따라서 부모는 자식을 저장할 수 있습니다.(자식은 부모로 표현될 수 있기 때문입니다.)
반대로 자식은 부모를 저장할 수 없습니다.

 

한정적 와일드 카드

자, 이제 한정적 와이드 카드에 대해서 알아봅시다.!
한정적 와일드 카드를 사용하면 특정 타입을 기준으로 상한 범위와 하한 범위를 지정할 수 있습니다.
그에 따라서 호출 범위를 확장하거나 제한할 수 있습니다.👍

 

상한 경계
<? extends MyParent>를 사용하면 MyParent를 최상위 클래스로 제한할 수 있습니다.
이를 상한 경계라고 부릅니다.

// 와일드 카드의 최상위 타입을 MyParent로 설정했습니다.
public void printCollection(Collection<? extends MyParent> c) {
    // 자식은 부모로 표현할 수 있다.
    // c는 MyParent 또는 MyParent의 자식 클래스 이다.

    // 컴파일 에러
    // 자식(MyChild)은 부모로 표현할 수 없다.
    for(MyChild e : c) {
        System.out.println(e);
    }

    // 자식(MyParent)은 부모로 표현 가능하다.
    for(MyParent e : c) {
        System.out.println(e);
    }

    // 자식(MyParent)은 부모로 표현 가능하다.
    for(MyGrandParent e : c) {
        System.out.println(e);
    }

    // 자식(MyParent)은 부모로 표현 가능하다.
    for(Object e : c) {
        System.out.println(e);
    }
}

 

아니.. 와일드 카드는 unknown type인데 최상위 타입을 MyParent로 설정 했으니 자식 클래스인 MyChild가 호출 가능해야하는 거 아니야? 🤔

 

<? extends MyParent>로 인해서 MyParentunknown typeMyParent의 자식 클래스들이 가능합니다.
아까 앞에서 자식은 부모로 표현이 가능한 것을 우리는 인지했습니다.
따라서 MyParent의 부모 클래스로 호출이 가능합니다!

 

이말은 즉, c에는 최소한 MyParent가 저장되어 있다는 의미 입니다.

 

참고1
자식👶🏻이 잘못을 하여, 부모👩👨를 모셔오라고 하는 경우라고 생각하시면 쉽습니다. 👍
자식은 부모로 표현이 가능합니다.

 

참고2
unknown typeMyParent자식 클래스가 정확히 어떤 타입인지 알수 있을까요?
그 타입이 진짜로 MyChild 일 수도 있고, 아닐 수도 있습니다.

 

예를 들어 MyParent의 자식 클래스인 AnotherChild가 있다고 해볼까요?

class AnotherChild extends MyParent {}

 

컬렉션 c에서 꺼내서 만들어지는 객체가 MyChild 타입이 아닌 AnotherChild가 될 수도 있습니다…..😭

 

하지만, 컬렉션 c에서 꺼내서 만들어지는 객체가 적어도 MyParent 임은 확실하므로 MyParent와 그 부모 타입(MyGrandParent)으로 꺼내는 것은 문제가 없습니다.

 

 

그런데 말입니다…
컬렉션 c에 원소를 저장하는 경우에는 상황이 달라집니다.

// 와일드 카드의 최상위 타입을 MyParent로 설정했습니다.
public void addElement(Collection<? extends MyParent> c) {
    // 부모는 자식을 저장할 수 있다.
    // c는 MyParent 또는 MyParent의 자식 클래스이다.

    // c가 MyChild랑 관계 없는 MyParent의 자식클래스이면?
    // 저장 불가
    c.add(new MyChild());

    // c가 MyParent의 자식클래스이면?
    // 자식은 부모를 저장할 수 없다.
    c.add(new MyParent()); // 컴파일 에러

    // 자식은 부모를 저장할 수 없다.
    c.add(new MyGrandParent()); // 컴파일 에러

    // 자식은 부모를 저장할 수 없다.
    c.add(new Object()); // 컴파일 에러        
}

 

앞에서 우리는 부모는 자식을 저장할 수 있다는 사실을 알고 있습니다.
그러면 c는 현재 MyParent가 최상위 타입이기 때문에
MyParent의 자식인 MyChild는 저장될 수 있을 것 같습니다.

 

하지만, c가 정말로 MyParent 인것을 우리는 장담할 수 있을까요?
만약 cMyParent의 다른 자식 클래스인 AnotherChild라고 하면 어떤가요?
MyChild를 저장하는 건 말이 안됩니다.!!

 

또한 MyGrandParent와 같이 부모 클래스는 자식 클래스에 저장 될 수 없습니다!

 

하한 경계
원소를 소모하는 경우에는 상한 경계가 아닌 하한 경계를 지정하여 최소한 MyParent 타입 임을 보장하면 문제를 해결할 수 있습니다.

// 와일드 카드의 최하위 타입을 MyParent로 설정했습니다.
public void addElement(Collection<? super MyParent> c) {
    // 부모는 자식을 저장할 수 있다.
    // c는 MyParent 또는 부모 클래스 이다.

    // 부모는 자식을 저장할 수 있다.
    c.add(new MyChild());

    // 자기 자신을 저장할 수 있다.
    c.add(new MyParent());

    // 만약 c가 MyParent이면?
    // 자식은 부모를 저장할 수 없다.
    // 컴파일 에러
    c.add(new MyGrandParent());

    // 컴파일 에러
    c.add(new Object());
}

 

하지만, 출력할 때는 상황은 반대 입니다.

// 와일드 카드의 최하위 타입을 MyParent로 설정했습니다.
public void printCollection(Collection<? super MyParent> c) {
    // 자식은 부모로 표현할 수 있다.
    // c는 MyParent 또는 MyParent의 부모 클래스 이다.

    // 부모는 자식(MyChild)으로 표현될 수 없습니다.
    // 컴파일 에러
    for(MyChild e : c) {
        System.out.println(e);
    }

    // c가 MyGrandParent라면?
    // 부모는 자식(MyParent)으로 표현될 수 없습니다.
    // 컴파일 에러
    for(MyParent e : c) {
        System.out.println(e);
    }

    // c가 Object라면?
    // 부모는 자식(MyGrandParent)으로 표현될 수 없습니다.
    // 컴파일 에러
    for(MyGrandParent e : c) {
        System.out.println(e);
    }

    // Object는 모든 객체의 부모이다.
    for(Object e : c) {
        System.out.println(e);
    }
}

 

상한 경계, 하한 경계 개념이 헷갈리지 않나요?
저는 그렇습니다.. ㅠㅠ

 

도대체 언제 extends를 사용하고, super를 사용해야 하는지 헷갈려요..😵‍💫

 

PECS

그래서 이펙티브 자바에서는 PECS라는 공식을 만들었습니다.

produce - extends, consumer-super의 줄임말 입니다.

이말은 컬렉션으로 부터 와일드 카드 타입의 객체를 생성(produce)하면 extends를 사용하고
객체를 소비(consumer)하면 super를 사용하라는 것 입니다.^^

public void produce(Collection<? extends MyParent> c) {
    for(MyGrandParent e : c) {
        System.out.println(e);
    }
}

public void consumer(Collection<? super MyParent> c) {
    c.add(new MyChild());
}

개인적으로 저는 와일드 카드의 객체를 표현해야할 때는 extends, 저장해야 할 때는 super를 사용한다고 이해하고 있습니다.

 

Type Erasure

제너릭은 JDK 1.5 부터 지원되었습니다.
따라서 JDK 1.5이전 소스와 호환성을 유지해야 했습니다.

그래서 Type Erasure라는 프로레스를 도입하게 되었습니다.
타입 이레이저는 컴파일 시점에만 타입에 대한 제약 조건을 설정하고, 런타임에는 해당 정보를 소거하는 프로세스 입니다.

 

다시 말하면, Java에서는 제너릭 클래스를 인스턴스화 할때 해당 타입을 지워버린다.
그 타입은 컴파일시에만 존재하고 컴파일된 바이트 코드에서는 어떠한 타입파라미터의 정보를 찾아볼 수 없다.!!!!!!!!!!!!!!!!!!!!!!!!!

정말일까요? 🤔

 

타입 이레이저 확인 👀

타입 이레이저의 동작 과정입니다.

1. 제너릭 타입의 경계 제거
2. 타입이 일치하지 않으면 형변환 추가

 

정말로 컴파일 시점에만 타입 파라미터의 정보가 있고,
컴파일된 바이트 코드에서는 타입 파라미터의 정보가 없는지 확인해봅시다.!!

 

제너릭 선언
아래와 같은 제너릭 클래스가 있다고 합시다.

class Admin {}
class Member<T extends Admin> {
    List<T> list = new ArrayList<>();

    public void add(T t) {
        list.add(t);
    }

    public T get(int index) {
        return list.get(index);
    } 
}

 

제너릭 타입의 경계 제거
위의 예시에서 Member 옆에 있는 <>는 지워지고 TAdmin로 치환됩니다.
만약 extends가 없다면 TObject로 변환됩니다.

class Member {
    List<Admin> list = new ArrayList<>();

    public void add(Admin t) {
        list.add(t);
    }

    public Admin get(int index) {
        return list.get(index);
    }
}

 

그리고 위의 List<Admin>List<E>입니다.
이도 당연히 타입 이레이저에 의해서 다음과 같이 바뀌게 됩니다.^^

class Member {
    List list = new ArrayList();

    public void add(Admin t) {
        list.add(t);
    }

    public Admin get(int index) {
        return list.get(index);
    }
}

 

타입이 일치하지 않으면 형변환 추가
마지막으로 get 메소드list.get(index)를 반환하고 있습니다.
이는 Object를 반환하기 때문에 Admin형으로 형변환 해야 합니다.

class Member {
    List list = new ArrayList();

    public void add(Admin t) {
        list.add(t);
    }

    public Admin get(int index) {
        // 형변환
        return (Admin) list.get(index);
    }
}

 

바이트 코드
와우😲 정말로 타입 파라미터에 대한 정보가 없습니다….

 

참고

제네릭 - 생활코딩
Java 제네릭과 와일드카드 타입에 대해 쉽고 완벽하게 이해하기(공변과 불공변, 상한 타입과 하한 타입) - MangKyu’s Diary

https://ttl-blog.tistory.com/282#%F0%9F%A7%90%20%ED%83%80%EC%9E%85%20%EC%9D%B4%EB%A0%88%EC%9D%B4%EC%A0%80%20(Type%20Erasure)-1

 

 

728x90
반응형