
다형성은 한 객체가 여러 타입의 객체로 취급될 수 있는 능력이다.
이브이, 메타몽인가?
보통 하나의 객체는 하나의 타입으로 고정되어 있다. 그런데 다형성을 사용하면 하나의 객체가 다른 타입으로 사용될 수 있다.
다형적 참조 : 부모 타입의 변수가 자식 인스턴스 참조
Parent poly = new Child()
부모는 자식을 담을 수 있다. 반대로 자식 타입은 부모 타입을 담을 수 없다.
자바에서 부모 타입은 자신은 물론이고, 자신을 기준으로 모든 자식 타입을 참조할 수 있다. 이것이 바로 다양한 형태를 참조할 수 있다고 해서 다형적 참조라고 한다.
poly.childMethod()를 실행하면 먼저 참조값을 통해 인스턴스를 찾는다. 그 다음으로 인스턴스 안에서 실행할 타입을 찾아야 한다. 호출자인 poly는 Parent 타입이다. 따라서 Parent 클래스부터 시작해서 필요한 기능을 찾는다. 그런데 상속 관계는 부모 방향으로 찾아 올라갈 수는 있지만 자식 방향으로 찾아 내려갈 수 없다.
그렇다면 이렇게 한계가 많은데 왜 굳이 Parent 타입으로 변수를 만들었을까? Child 타입의 변수로 Parent를 상속한 Child 인스턴스를 참조하면 parentMethod(), childMethod() 모두 사용할 수 있는데?
다운캐스팅을 한다고 해서 Parent poly의 타입이 변하는 것은 아니다. 해당 참조값을 거내고 꺼낸 참조값이 Child 타입이 되는 것이다. 따라서 poly의 타입은 Parent로 기존과 유지된다.
Child child = (Child) poly;
child.childMethod();
해당 메서드를 호출하는 순간만 다운캐스팅하는 기법
((Child) poly).childMethod();
업캐스팅은 생략 가능, 생략이 권장된다.
Parent parent = (Parent) child;
객체를 생성하면 해당 타입의 상위 부모 타입은 모두 함께 생성된다. 그렇기 때문에 위로만 타입을 변경하는 업캐스팅은 메모리 상에 인스턴스가 모두 존재하기 떄문에 항상 안전하다.
다운캐스팅을 잘못하면 심각한 런타임 오류가 발생할 수 있다.
다운캐스팅을 자동으로 하지 않는 이유이다.
Parent parent = new Parent()
Child child = (Child) parent; // 런타임 오류 발생 - ClassCastException
child.childMethod(); // 실행불가
다형성에서는 참조형 변수는 이름 그대로 다양한 자식을 대상으로 참조할 수 있다. 그런데 참조하는 대상이 다양하기 때문에 어떤 자식을 참조하는지 궁금할 수 있다.
Parent parent1 = new Parent();
Parent parent2 = new Child();
if (parnet1 instanceof Child) {
System.out.println("Child 인스턴스 맞음")
Child child = (Child) parent;
child.childMethod();
}
조건부에 instanceof를 활용하여 런타임 오류 없이 메서드를 호출할 수 있다.
Parent parent1 = new Child();
Parent 타입의 참조변수 parent1에 Child 클래스의 인스턴스를 참조값으로 대입한다.
오버라이딩된 메서드가 항상 우선권을 가진다. 이게 가장 중요한 내용이다.
변수는 오버라이딩이 안 되고, 메서드는 오버라이딩이 된다.
다형적 참조와 메서드 오버라이딩을 이해하면 다형성이 어떻게 활용되는지 알 수 있다.
코드는 같지만 타입이 다르면 중복 코드가 많아진다. 타입이 다르기 때문에 배열과 반복문으로도 해결할 수 없다. 이럴 때 다형성이 활용될 수 있다.
animal이라는 부모 클래스를 만들고 각각 상속시킨 다음에 울음소리를 오버라이딩하면
dog, cat, cow
각각 멍멍, 야옹, 음메로 출력시킬 수 있다.
하지만 animal이라는 멀쩡한 클래스를 만든다면 나중에 문제가 발생할 수 있다. 단순히 dog, cat, cow 등을 통합하기 위해 animal을 만들었지, animal 자체를 사용하려는 의도는 아니었는데, 실수로 animal 인스턴스의 메서드를 호출할 수도 있는 것이다. animal 클래스는 멀쩡한 클래스이기 때문에 human error에 노출될 수 있다.
이럴 때 추상클래스가 등장한다.
Animal과 같이 부모 클래스는 제공해주지만 실제 인스턴스로 생성되면 안 되는 클래스를 추상클래스라고 한다. 상속을 목적으로만 사용되고 부모 클래스 역할을 담당한다. 마치 SPC specific purpose company, 페이퍼컴퍼니와 같다.
클래스에 추상 메서드가 하나로도 있으면 추상클래스로 선언해야 한다.
추상메서드는 메서드 바디가 없다.
추상메서드는 상속받는 자식클래스가 반드시 오버라이딩해서 사용해야 한다.
자동차라는 추상클래스, (자동차라는 건 관념일 뿐, 현실에선 없다! 인스턴스를 만들 수 없다!)
아반떼, 소나타, 제네시스라는 자식 클래스(반드시 악셀, 브레이크, 핸들, 엔진이 있어야 한다!)
animal을 추상클래스를 만들어줬기 때문에 실수로 animal이라는 클래스의 인스턴스를 만들고, 오버라이딩 하지 않고 메서드를 호출하는 경우를 원천적으로 방지해준다.
모든 메서드가 추상메서드
다양한 제품을 호환할 수 있는 USB인터페이스로 비유할 수 있다. USB인터페이스는 분명한 규격이 있고, 이 규격을 맞춰서 제품을 개발해야만 연결이 된다.
그리고 자바는 순수 추상 클래스를 더 편리하게 사용할 수 있도록 인터페이스라는 개념을 사용한다!
인터페이스는 class 대신 interface를 사용한다.
메서드에 public abstract를 생략할 수 있다.
UML에서는
클래스 상속 관계에는 실선으로 표현하고,
인터페이스 구현(상속) 관계에는 점선으로 표현한다.
인터페이스를 구현할 때는 extends 대신 implements를 쓴다.
무조건 인터페이스의 모든 메서드를 반드시 구현해야 하는 제약이 있다. 순수추상클래스는 미래에 누군가 실수로 abstract 없이 실행 가능한 메서드를 추가할 수 있다. 이렇게 되면 추가된 기능은 자식 클래스에서 구현하지 않을 수도 있고, 더는 순수추상클래스가 아니게 된다. 인터페이스는 모든 메서드가 추상메서드이기 때문에 이러한 문제를 원천차단할 수 있다.
클래스 상속은 부모가 하나밖에 없는데, 인터페이스는 부모를 여럿 두는 다중 구현(다중 상속)이 가능하다.