개발

[Java] 예제 코드로 알아보는 다형성 - 2

오렌지색 귤 2022. 1. 24. 12:06
반응형

2022.01.24 - [Java] - [Java] 예제 코드로 알아보는 다형성 - 1

 

[Java] 예제 코드로 알아보는 다형성 - 1

정의 다형성(polymorphism)이란 하나의 객체가 여러 가지 타입을 가질 수 있는 것을 의미합니다. 자바에서는 이러한 다형성을 부모 클래스 타입의 참조 변수로 자식 클래스 타입의 인스턴스를 참조

hwan33.tistory.com

이전 포스팅을 이어가도록 하겠습니다.

 

 

 

다형성

 

 

 

Quiz 1. 다음 코드의 결과를 예측하시오

 

class SuperClass {  
    String x = "super";  

    public void method() {  
        System.out.println("super class method");  
    }  
}  

class SubClass extends SuperClass {  
    String x = "sub";  

    @Override  
    public void method() {  
        System.out.println("sub class method");  
    }  
}  

public class PolyTest3 {  
    public static void main(String\[\] args) {  
        SuperClass class1 = new SuperClass();  
        System.out.println(class1.x);  
        class1.method();  

        SuperClass class2 = new SubClass();  
        System.out.println(class2.x);  
        class2.method();  
    }  
}

 

 

 

 

 

 

정답

 

 

super
super class method
super
sub class method

 

많은 분들이 헷갈려 하실 것 같은 다형성의 핵심 코드입니다!

 

List list = new ArrayList<>();

 

위의 인터페이스와 구현체의 관계를 생각하면 퀴즈의 결과가 이해되지 않을 수도 있습니다.

 

ArrayList 구현체에 List 인터페이스에서 정의되지 않은 다른 메서드가 존재한다고 가정했을 때, list는 List 타입이므로 해당 메서드를 사용하지 못합니다.

 

따라서 List 인터페이스에서는 정의되지 않고 ArrayList 구현체에서만 정의된 메서드를 사용하려면 다음과 같은 list를 선언해야 합니다.

 

ArrayList list = new ArrayList<>();

 

사실 위와 같은 이유로 3번째 System.out.println(class2.x);의 결과는 sub가 출력된 것이 아닌 super가 출력되었습니다.

 

 

그렇다면 같은 원리로 4번째 출력문도 super class method 가 출력되어야 하는 것 아닌가 하는 생각이 드실 수도 있습니다.

 

하지만 method는 오버라이딩 되었다는 것을 명심해주셔야 합니다.

 

SubClass는 SuperClass의 method를 상속받아 오버라이딩 했습니다.

 

method는 어떤 클래스의 메소드인지 런타임 시점에 결정됩니다.

 

이 개념을 동적 바인딩이라 부르며, 동적 바인딩은 런타임 시점에 해당 메소드를 구현하고 있는 실제 객체 타입을 기준으로 찾아가서 실행될 함수를 호출합니다.

 

class2 참조 변수로 접근 가능한 것은 상위 클래스의 멤버이지만, 하위 클래스에서 메서드를 오버라이딩 했기 때문에 하위 클래스의 메서드를 호출합니다.

 

 

 

 

Quiz 2. 동적 바인딩과 정적 바인딩

 

class SuperClass {  
    public String x = "super X";  
    public static String y = "super Y";  

    public void methodA() {  
        System.out.println("super method A");  
    }  

    public static void methodB() {  
        System.out.println("super method B");  
    }  
}  

class SubClass extends SuperClass {  
    String x = "sub X";  
    public static String y = "sub Y";  

    @Override  
    public void methodA() {  
        System.out.println("sub method A");  
    }  

    public static void methodB() {  
        System.out.println("sub method B");  
    }  
}  

public class PolyTest4 {  
    public static void main(String\[\] args) {  
        SuperClass class1 = new SuperClass();  
        System.out.println(class1.x);  
        System.out.println(class1.y);  
        class1.methodA();  
        class1.methodB();  

        SuperClass class2 = new SubClass();  
        System.out.println(class2.x);  
        System.out.println(class2.y);  
        class2.methodA();  
        class2.methodB();  
    }  
}

 

 

 

 

 

 

정답

 

super X
super Y
super method A
super method B
super X
super Y
sub method A
super method B

 

 

static method를 제외하고는 모두 상위 클래스인 SuperClass의 멤버가 출력되었다.

 

여기서 알 수 있는 것은 다음과 같다

 

  • 모든 인스턴스 메서드는 런타임에 결정된다
  • static 메서드와 static 변수는 컴파일 시에 결정된다
  • 따라서 상위 클래스의 멤버가 호출되는지, 하위 클래스의 멤버가 호출되는지는 instance인지, static 인지에 따라 달라진다.

 

 

한가지 더 짚고 넘어가자면, methodB에 @Override 애노테이션을 붙여주면 sub가 호출되는 것 아닌가 하는 궁금증이 들 수도 있다.

 

다만, 이미 해본 결과 컴파일 오류가 발생하고 이유는 다음과 같다.

 

static method는 컴파일 시점에 메모리에 올라가고 메서드 영역에 존재한다.

 

애초에 해당 클래스로부터 생성되는 모든 인스턴스들에 의해 공유되는 static method가 오버라이딩 된다는 것이 말이 안된다.

 

또한, 위에서도 말했듯이 런타임 시점에 사용될 메서드가 결정되는 Override와 다르게 static 메서드는 클래스가 컴파일 되는 시점에 결정된다.

 

 

 

PS

 

위에서 나온 개념들을 익히고 나면 Effective Java 아이템 2에서 나를 힘들게 했던 빌더 패턴을 쉽게 이해할 수 있다. 간단하게 다시 살펴보자면

 

public abstract class Pizza {
    ... // 생략

    abstract static class Builder<T extends Builder<T> {  
        ... // 생략  

        public T addTopping(Topping topping) {  
            toppings.add(Objects.requiredNonNull(topping));  
            return self();  
        }  

        protected abstract T self();  
    }  
    ... // 생략  
}

public class NyPizza extends Pizza {
    ... // 생략

    public static class Builder extends Pizza.Builder<Builder> {  
        ... // 생략  

        @Override protected Builder self() { return this; }  
    }  
    ... // 생략  
}

public class Test {
    public static void main(String[] args) {
        NyPizza pizza = new NyPizza.Builder(SMALL)
                    .addTopping(SAUSAGE)
                    .addTopping(ONION)
                    .build();
    }
}

 

 

위의 코드에서 상위 클래스의 빌더에서 addTopping 메서드의 반환으로 self() 메서드를 반환하는데, 이 self() 메서드는 하위 클래스에서 오버라이딩 했기 때문에 결국 실제로 반환되는 것은 하위 클래스의 빌더이다...

 

자세한 내용은 아이템 2 게시물에 가서 확인하도록 하자!

 

 

 

 

Quiz 3. eat1, eat2, eat3 중 가장 적합한 메서드를 고르시오

 

public class Test {

    static class Food {  
        void eat() {  
            System.out.println("냠");  
        }  
    }  

    static class Pizza extends Food {  
    }  

    public static void main(String\[\] args) {  
        eat3(new Pizza());  
    }  

    public static void eat1(Object obj) {  
        if (obj != null || obj instanceof Food) {  
            Food food = (Food)obj;  
            food.eat();  
        }  
    }  

    public static void eat2(Food food) {  
        if (food != null) {  
            food.eat();  
        }  
    }  

    public static void eat3(Pizza pizza) {  
        if (pizza != null) {  
            pizza.eat();  
        }  
    }  
}

 

 

 

 

 

정답

 

eat2 메서드가 가장 적합합니다.

 

상위로 올라갈수록 활용도는 물론 높아집니다.

 

실제로 eat1의 메서드의 경우 아래와 같이 모든 파라미터에 대해서 활용 가능하게 됩니다. (물론 Food와 Pizza 객체에 대해서만 메세지를 출력합니다)

 

eat1(new Object());
eat1(230);
eat1(new Food());
eat1(new Pizza());
eat1(true);

 

하지만 활용도가 높아지는 만큼 코드의 복잡성도 높아지기 때문에, Java API 처럼 공통기능의 경우에는 Object를 파라미터로 쓰겠지만, 대부분의 경우에 비지니스 로직상 최상위 객체를 파라미터로 사용하는 것을 권장합니다.

 

 

 

 

Quiz 4. 아래의 코드에서 컴파일 시 오류가 발생하는 번호를 고르시오.

 

Object[] objs = {"Hi", new Animal(), new Human(), 30, true}; // 1번
String str1 = (String)objs[0]; // 2번
String str2 = (String)objs[1]; // 3번
String str3 = (String)objs[4]; // 4번
System.out.println(str1.length()); // 5번
System.out.println(str2.length()); // 6번
System.out.println(str3.length()); // 7번

 

 

 

 

 

 

정답

 

컴파일 시점에는 아무 오류도 나오지 않는다.

 

런타임 시점에 오류가 날 3, 4, 6, 7번 모두 컴파일러는 str2와 str3 모두 String 타입으로 인지하기 때문이다.

 

 

 

 

 

Quiz 5. 그렇다면 퀴즈 4의 문제를 해결할 수 있는 방안은?

 

 

 

 

 

 

 

 

정답

 

이런 문제는 instanceof 연산자로 해결할 수 있다

 

for (Object o : objs) {
    if (o instanceof String) {
        String str = (String)o;
        System.out.println(str.length());
    } else {
        System.out.println("문자열 타입이 아닙니다");
    }
}
반응형