7-5. 다형성

Hyun Jun·2022년 1월 22일
0

자바의 정석

목록 보기
30/52
post-thumbnail
post-custom-banner

다형성

개념

객체지향 개념에서 다형성이란, "여러 가지 형태를 가질 수 있는 능력"을 의미함.

Java에서는 한 타입의 참조변수로 여러 타입의 객체를 참조할 수 있도록 허용하여 다형성을 구현함.

 

class Parent {
    int parentMemVar;

    void parentMethod() { ... }
}

class Child extends Parent {
    int childMemVar;

    void childMethod() { ... }
}

Child 클래스는 Parent 클래스를 상속 받았으므로 Parent의 모든 멤버 변수와 메서드를 가짐.

Parent p = new Parent();
Child c = new Child();

위와 같이 인스턴스 변수와 메서드를 사용하기 위해 그동안은 참조변수의 타입과 인스턴스의 타입을 동일하게 맞추어 생성했었는데,

Parent p = new Child(); // 부모 타입의 참조변수에 자식 타입 인스턴스를 참조시킴

다형성의 원리를 통해 이렇게도 쓸 수 있음.

ChildParent 보다 더 확장된 개념이기 때문에 가능. (반대의 경우는 사용 불가)

Parent p = new Child();
Child c = new Child();

대신, 이 둘은 분명한 차이점이 존재.

  • 참조변수 p: Parent 클래스 타입이기 때문에, Child 클래스에만 있고 Parent 클래스에는 없는 변수나 메서드는 사용할 수 없음

  • 참조변수 c: Child 클래스 타입이기 때문에, Child 클래스에 있는 모든 변수와 메서드 사용 가능

→ 인스턴스의 타입은 같아도 참조변수의 타입에 따라 사용 가능한 멤버의 범위가 달라짐

 

참조변수의 형변환

서로 상속 관계에 있는 클래스 사이에서는 참조변수를 형변환 시킬 수 있음 (조상 타입 ↔ 자손 타입)

Up-casting하는 경우에는 형변환 생략 가능 (기본형 변수의 형변환 시, "작은 자료형 → 큰 자료형"은 형변환 생략이 가능한 것과 유사)

  • Up-casting: 자손 타입 → 조상 타입

  • Down-casting: 조상 타입 → 자손 타입

캐스트 연산자인 괄호()를 사용해 변환하고자 하는 타입을 안에 적으면 됨

Parent p = null;
Child c = new Child();
Child c2 = null;

p = c; // (Parent)c -> Up-casting. 생략 가능
c2 = (Child)p; // Down-casting. 생략 불가

개념 파트에서 보았던 예시도 원래는 이렇게 형변환을 해준 것인데 생략된 것.

Parent p = (Parent)new Child();
// 또는
Child c = new Child();
Parent p = (Parent)c;

형변환은 참조변수의 타입을 바꿔줄 뿐, 인스턴스 자체에는 아무런 영향을 끼치지 않음.

→ 형변환을 통해서는 단지 사용가능한 멤버의 범위를 줄였다 늘렸다 할 수 있는 것

 

instanceof 연산자

참조변수가 가리키는 인스턴스의 실제 타입을 확인하는 용도

varName instanceof ClassName

처럼 사용하고, 반환값은 boolean(true 또는 false)인 점을 활용하여 조건문의 조건식에 주로 사용함.

parent instanceof Parent // true
child instanceof Parent // true
parent instanceof Child // false
child instanceof Child // true

결과가 true이면 검사한 클래스의 타입으로 형변환이 가능하다는 의미

void someMethod(Parent p) {
    if (p instanceof Child1) {
        Child1 c1 = (Child1) p; // Down-casting 이므로 생략 불가
        c1.childOneMethod();
    }
    if (p instanceof Child2) {
        Child2 c2 = (Child2) p;
        c2.childTwoMethod();
    }
}

참조변수 p에는 Parent 타입이 오거나, Child1 또는 Child2 타입이 올 수 있음

Parent 타입이 온다면, 아무 조건문도 만족하지 못할 것이고,

Child1 타입이 온다면 첫번째 조건문을, Child2 타입이 온다면 두번째 조건문을 만족하게 될 것임.

 

참조변수와 인스턴스의 연결

Parent p = new Child();
Child c = new Child();

위에서 이 둘의 차이점에 대해서 짚었는데, 한가지 더 차이가 있음

만약 조상 클래스와 자손 클래스에 동일한 이름의 인스턴스 메서드가 있는 경우, 자손의 것으로 오버라이딩되어 항상 현재 참조하고 있는 인스턴스 (예시 기준으로 Child 인스턴스)의 메서드가 나오는데 비해,

→ 멤버 변수는 참조변수의 타입에 따라 다른 결과를 가져옴.

class BindingTest {
    public static void main(String[] args) {
        Parent p = new Child();
        Child c = new Child();

        System.out.printf("p.x = %d%n", p.x); // 출력: 10
        System.out.printf("p.x = %d%n", c.x); // 출력: 20

        p.method(); // 출력: Child의 메서드가 실행되었습니다.
        c.method(); // 출력: Child의 메서드가 실행되었습니다.
    }
}

class Parent {
    int x = 10;
    void method() {
        System.out.println("Parent의 메서드가 실행되었습니다.");
    }
}

class Child extends Parent {
    int x = 20; // 부모의 멤버변수와 이름이 겹침
    void method() {
        System.out.println("Child의 메서드가 실행되었습니다.");
    }
}

이래서 더더욱 외부 클래스에서 참조변수를 통한 인스턴스 변수 직접 접근을 막아야 함. (참조변수 타입에 따라 상이한 결과가 나옴 = 실수의 여지)

멤버 변수들을 private으로 지정해서 직접 접근을 막고, 값을 읽어오는 메서드를 정의하여 메서드를 통해서만 접근할 수 있게 해야함.

 

매개변수의 다형성

매개변수로 참조변수(인스턴스)를 받아올 때, 매개 변수의 타입을 조상 클래스의 타입으로 놓으면 자손 클래스 타입의 참조변수 어느 것이든 받아올 수 있음

class Test {
    public static void main(String[] args) {
        Buyer b = new Buyer();
        b.buy(new Tv());
        b.buy(new Computer());
        b.buy(new Audio());
    }
}

class Product {
    int price;
    int bonusPoint;

    Product (int price) {
        this.price = price;
        this.bonusPoint = (int)(price / 10.0);
    }
}

class Tv extends Product {
    Tv() {
        super(100);
    }
}

class Computer extends Product {
    Computer() {
        super(200);
    }
}

class Audio extends Product {
    Audio() {
        super(300);
    }
}

class Buyer {
    int money = 1000;
    int bonusPoint = 0;

    Buyer() {
        System.out.printf("money: %d%nbonusPoint: %d%n", money, bonusPoint);
    }

    void buy(Product p) {
        money -= p.price;
        bonusPoint += p.bonusPoint;
        System.out.printf("money: %d%nbonusPoint: %d%n", money, bonusPoint);
    }
}

모든 제품의 조상인 Product 클래스가 있고, 자손으로는 Tv, Computer, Audio 클래스가 있음

Buyer 클래스는 구매자로서, buy 메서드를 사용해 제품을 구매함.

이 때 buy 메서드의 매개변수로 특정 제품 인스턴스를 가리키는 참조변수를 넣어야하는데, 만약 매개변수의 타입을 Tv, Computer, Audio 타입 중 하나로 설정해두었다면 그 제품밖에 구매할 수 없으므로, buyTv, buyComputer, buyAudio와 같이 개별 메서드를 만들어줘야함.

그러나 매개변수 타입을 조상 클래스인 Product 타입으로 설정해두면, 모든 자손 클래스 타입을 받을 수 있게 됨.

 

매개변수의 다형성의 또 다른 예시로 print 메서드가 있음 (아래는 실제 System.out.print()의 일부)

public void print(Object obj) {
    write(String.valueOf(obj));
}

위에서 본 원리대로, 매개변수 타입을 모든 클래스의 조상인 Object 클래스 타입으로 설정해서 모든 객체를 다 받을 수 있게됨.

 

여러 종류의 객체를 배열로 다루기

다형성의 원리에 따라 다음과 같이 쓸 수 있음

Product p1 = new Tv();
Product p2 = new Computer();
Product p3 = new Audio();

참조변수들이 모두 같은 타입이 되었으므로 배열로 묶을 수 있음

Product[] p = new Product[]{p1, p2, p3};
// 또는
Product[] p = {p1, p2, p3};

이렇게 다형성을 이용해 공통 조상을 가진 객체들을 배열로 묶어서 보다 효율적인 데이터 관리가 가능해짐.

profile
Back-end Engineer 👨‍💻
post-custom-banner

0개의 댓글