Skip to content

Instantly share code, notes, and snippets.

@unluckyjung
Last active March 2, 2021 15:54
Show Gist options
  • Save unluckyjung/222356008f3a4b0f212d1d81917aaeb7 to your computer and use it in GitHub Desktop.
Save unluckyjung/222356008f3a4b0f212d1d81917aaeb7 to your computer and use it in GitHub Desktop.
제너릭.md

제너릭 상속시 주의해야할 점에 대해서 정리해봅니다.

오해

public class MyClass{}

class Parent<T> {
    public T print(T arg) {
        System.out.println("1");
        return arg;
    }
}

class Child<T extends MyClass> extends Parent {
    public T print(T arg) {
        System.out.println("2");
        return arg;
    }
}

class App {
    public static void main(String[] args) {
        Parent<MyClass> p1 = new Parent<>();
        Parent<MyClass> p2 = new Child<>();
        Child<MyClass> c1 = new Child<>();

        p1.print(new MyClass());
        p2.print(new MyClass());
        c1.print(new MyClass()); 
    }
}
  • 이 코드의 결과값은 무엇이 나올것 같은가?
  • p1은 자명하게 1이 나올것이다.
  • c1역시 자명하게 2가 나올것이다.
  • 문제는 p2에서 발생한다.
    • 한번 보면, Child 클래스는 Parent를 상속했다.
    • public T print(T arg) 는 오버라이딩 될것을 기대한다.
c1.print(new MyClass()); 
  • 해당 메소드의 결과는 2를 기대한다.
  • 하지면 결과는 1이 나오게된다.

왜?

1이 불렸다는것은 Parent의 메소드가 불렸다는건데? 어째서?

  • 뜨거운 감자가 된 p2만 남기고 다 지워보자.
public class MyClass{}

class Parent<T> {
    public T print(T arg) {
        System.out.println("1");
        return arg;
    }
}

class Child<T extends MyClass> extends Parent {
    public T print(T arg) {
        System.out.println("2");
        return arg;
    }
}

class App {
    public static void main(String[] args) {
        Parent<MyClass> p2 = new Child<>();
        p2.print(new MyClass());
    }
}

image

  • 우리의 똑똑한 인텔리제이는 Child의 print는 아예 사용되지 않고 있다고 이야기해준다.
  • 어째서 이런일이 발생하게 된것일까?

잘못된 상속

  • 근본적인 이유는 상속이 잘못 이루어졌기 때문이다.
  • 우리는 Parent를 상속할때 제너릭타입을 같이 가져오지 않았다.
// 잘못된 상속
class Child<T extends MyClass> extends Parent {
    public T print(T arg) {
        System.out.println("2");
        return arg;
    }
}

// 올바른 상속
class Child<T extends MyClass> extends Parent<T> {
    public T print(T arg) {
        System.out.println("2");
        return arg;
    }
}
  • 아래와 같이 올바른 상속을 하게 된다면, 정상적으로 오버라이딩이 되어 1이 아닌 2의 결과를 얻을 수 있다.
public class MyClass{}

class Parent<T> {
    public T print(T arg) {
        System.out.println("1");
        return arg;
    }
}

class Child<T extends MyClass> extends Parent<T>{
    public T print(T arg) {
        System.out.println("2");
        return arg;
    }
}

class App {
    public static void main(String[] args) {
        Parent<MyClass> c1 = new Child<>();
        c1.print(new MyClass());    //  2가 호출된다.
    }
}

아니 그러면 처음 컴파일 타임에서부터 에러가 났어야 했던거 아니야?

잘못된 상속이라매, Parent<MyClass> c1 = new Child<>(); 이게 그러면 불가능 해야 하는거아니야?

  • 이어 설명하기 전에 확실히 짚고 넘어갈 부분이 있다.

    • 잘못된 상속 이라고 했지, 상속이 되지 않았다 라고 하지 않았다.
    • 잘못된 상속 의 정의가 좀 모호하긴 하지만, 애초에 맨 위의 방식으로 짜는것 자체가 잘못된 방식이기때문에, 그냥 잘못된 상속이라고 하고 넘어가겠다.
    • 지금 우리가 궁금한것은, 왜 이런일이 벌어졌냐지 다른곳에 촛점을 맞추진 말자
  • 다시 잘못된 코드로 돌아가보자.

    • 위로 올려서 보기 귀찮을것 같으므로 친절하게 다시 복붙해주겠다.
public class MyClass{}

class Parent<T> {
    public T print(T arg) {
        System.out.println("1");
        return arg;
    }
}

class Child<T extends MyClass> extends Parent{
    public T print(T arg) {
        System.out.println("2");
        return arg;
    }
}

class App {
    public static void main(String[] args) {
        Parent<MyClass> p2 = new Child<>();
        p2.print(new MyClass());
    }
}
  • 여기서 이야기 해주고 싶은것은 print는 전혀 다른 print이다.
    • 즉 Parent 클래스의 print와 Child 클래스의 print는 이름이 같아 헷갈린것이다.
  • 왜 달라?
    • public T print(T arg) 에는 타입변수인 T가 있다.
    • T의 경우 Parent print의 T와, Child print의 T는 다르다.
    • 또 왜?
      • 잘못된 상속을 했기 때문이다.
      • Parent에 대한 상속만 했지, 제너릭에 대한 상속은 이루어지지 않았기 때문이다.
      • 이 경우를 다시 보고 오자.

그래서 어떻게 상속이 된건데?

  • 계속 혼란을 주고 있는 메소드명만 바꿔서 보면 직관적으로 확인이 가능하다.
public class MyClass{}

class Parent<T> {
    public T parentPrint(T arg) {
        System.out.println("1");
        return arg;
    }
}

class Child<T extends MyClass> extends Parent{
    public T childPrint(T arg) {
        System.out.println("2");
        return arg;
    }
}

class App {
    public static void main(String[] args) {
        Parent<MyClass> p1 = new Child<>();
        Child<MyClass> c1 = new Child<>();
    }
}

image

image

  • p1의 경우, Parent에만 구현되어있는 ParentPrint 메소드에만 접근이 가능한걸 볼 수 있다.
  • c1의 경우, 두가지 메소드에 다 접근이 가능한것을 볼 수 있다.



image

  • 즉 메소드 이름을 다시 동일하게 print로 바꾼경우, 다음과 같이 두개가 다른 메소드로 표현되고 있음을 알 수 있다.

결론

제너릭 타입을 가지고 있는 클래스를 상속할때는 주의하자.

  • 제너릭 타입을 상속하지 않아도 물론 컴파일 에러는 나지 않는다.
  • 하지만 대부분의 상황에서 Parent 만 상속하는것이 아니고 Parent<T> 를 상속하는것이 아마 의도에 맞을것이다.

생각해볼거리

1

package generic_lecture;

class Parent<T extends Number> {
    public void print(T arg) {
        System.out.println(arg);
    }
}

class Child<T> extends Parent{
    public void print(T arg) {
        System.out.println(arg);
    }
}

class App {
    public static void main(String[] args) {
        Child c1 = new Child();
        c1.print(1234);
        c1.print("Goodgid ManSae");
    }
}
  • 다음 코드의 결과는 어떻게 될것 같은가?
  • 컴파일 에러가 날것인가?
  • 아니면 문자열을 출력하는 시점에서 런타임 에러가 날것인가?
  • 또 아니면 숫자와 문자열이 다 정상적으로 출력이 될것인가?

2

class Parent<T extends Number> {
    public void print(T arg) {
        System.out.println(arg);
    }
}

class Child<T> extends Parent<T>{
    public void print(T arg) {
        System.out.println(arg);
    }
}

class App {
    public static void main(String[] args) {
        Child c1 = new Child();
        c1.print(1234);
        //c1.print("Goodgid ManSae");
    }
}
  • 1이 컴파일 단에서 문제가 있었을거라고 생각해보자.
  • 그렇다면 child 부분을 다음과 같이 변경하고 문자열을 주석 처리한다면 컴파일이 정상적으로 될까?
  • 안된다면 이유는 무엇일까?
@unluckyjung
Copy link
Author

unluckyjung commented Mar 2, 2021

  1. 에러안남. 정상적으로 1234 굳긷만세 출력. why 다른 메소드라 오버로딩처럼 쓰여짐

  2. 변경시 컴파일 에러남. child<T>를 child<T extends Number> 로 바꾸어주어야함

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment