[Java] 다형성
10. 다형성1
다형성 (Polymorphism)
- 다형성 : 한 객체가 여러 타입의 객체로 취급될 수 있는 능력
package poly.basic;
public class Parent {
public void parentMethod() {
System.out.println("Parent.parentMethod");
}
}
public class Child extends Parent {
public void childMethod() {
System.out.println("Child.childMethod");
}
}
package poly.basic;
public class PolyMain {
public static ovid main(String[] args) {
System.out.println("Parent->Child");
Parent poly = new Child();
poly.parentMethod();
// Child chlid1 = new Parent(); 자식은 부모를 담을 수 없다.
// poly.childMethod(); 자식 기능은 호출할 수 없다.
}
}
다형적 참조
- 자바의 다형적 참조 : 부모 타입의 변수가 자식 인스턴스를 참조할 수 있다. 다양한 형태를 참조할 수 있음에 다형적 참조라고 한다.
- Parent -> Child : poly.parentMethod()
- Child 인스턴스를 새로 만들어서 부모 타입의 변수인 poly에 참조값을 담는다.
- 인스턴스에는 child와 parent 클래스가 생성된다.
- 인스턴스에서 메서드 호출자와 동일한 parent 클래스에 접근해 parentMethod()를 호출한다.
- 자바에서 부모 타입은 자식 타입을 참조할 수 있지만, 자식 타입은 부모 타입을 참조할 수 없다.
- 상속 관계에서 자식 -> 부모 방향으로 찾아 올라갈 수 있지만, 부모 -> 자식 방향으로 찾아 내려갈 수 없다.
poly.childMethod()
를 실행하면 컴파일 오류가 발생한다.
캐스팅
- 다운 캐스팅 : 현재 타입을 자식 타입으로 변경하는 것
package poly.basic; public class CastingMain1 { public static void main(String[] args) { Parent parent1 = new Child(); // 다운 캐스팅 (부모 타입 -> 자식 타입) Child child1 = (Child) parent1; child.childMethod(); Parent parent2 = new Parent(); // Child child2 = (Child) parent2; 런타임 오류 // child2.childMethod(); 실행불가 } }
Child child1 = (Child) parent1
- (타입) 참조대상 -> 참조대상을 특정 타입으로 변경한다.
- Parent 타입인 parent1를 (Child)를 사용해서 일시적으로 자식 타입인 Child 타입으로 변경하여 child1 변수에 대입한다.
- 인스턴스에서 호출자와 동일한 타입인 Child 클래스에 접근해 childMethod()를 호출한다.
- parent1은 Parent 타입을 유지!! 해당 참조값이 Child 타입이 되는 것!!
((Child) parent1).childMethod()
와 같이 별도의 변수 없이 일시적으로 다운 캐스팅을 할 수 있다.
- 자바에서는 인스턴스에 존재하지 않는 하위 타입으로 다운캐스팅하는 경우 ClassCastException이라는 런타임 예외를 발생시킨다.
Child child2 = (Child) parent2
- parent2는 Parent 클래스만 존재하는 부모 타입의 인스턴스 참조값을 저장한다.
- parent2를 Child 타입으로 다운캐스팅할 때, 메모리 상에 Child 클래스가 존재하지 않기 때문에 Child를 사용할 수 없다.
- 여기서 ClassCastException 예외가 발생하여 프로그램이 종료된다.
- 업 캐스팅 : 현재 타입을 부모 타입으로 변경하는 것
package poly.basic; public class CastingMain2 { public static void main(String[] args) { Child child = new Child(); Parent parent1 = (Parent) child; Parent parent2 = child; // 업캐스팅 생략 권장 parent1.parentMethod(); parent2.parentMethod(); } }
- (타입) 생략 가능하며, 생략이 권장된다.
- Child 타입인 child 참조변수의 타입을 부모 타입인 Parent로 변환한다.
instanceof
instanceof : 변수가 참조하는 인스턴스의 타입을 확인하기 위해 사용하는 키워드이다.
package poly.basic; public class CastingMain { public static void main(String[] args) { Parent parent1 = new Parent(); System.out.println("parent1 호출"); call(parent1); Parent parent2 = new Child(); System.out.println("parent2 호출"); call(parent2); } private static void call(Parent parent) { parent.parentMethod(); if(parent instanceof Child) { System.out.println("Child 인스턴스 맞음"); Child child = (Child) parent; child.childMethod(); } } }
parent instanceof Child
- 다운 캐스팅을 수행하기 전 매개변수로 넘어온 parent 인스턴스가 참조하는 타입이 Child인지 확인한다.
- true면 parent를 Child 타입으로 다운 캐스팅 후, childMethod()를 호출하도록 한다.
- 자바 16부터는
parent instanceof Child child
처럼 instanceof를 사용하면서 동시에 변수를 선언하면서 직접 다운캐스팅 하는 코드를 생략할 수 있다.if (parent instanceof Child child) { System.out.println("Child 인스턴스 맞음"); child.childMethod(); }
11. 다형성2
다형성 활용
package poly.ex2;
public class Animal {
public void sound() {
System.out.println("동물 울음 소리");
}
}
package poly.ex2;
public class Dog extends Animal {
@Override
public void sound() {
System.out.println("멍멍");
}
}
public class Cat extends Animal {
@Override
public void sound() {
System.out.println("야옹");
}
}
public class Cow extends Animal {
@Override
public void sound() {
System.out.println("음매");
}
}
package poly.ex2;
public class AnimalPolyMain1 {
public static void main(String[] args) {
Dog dog = new Dog();
Cat cat = new Cat();
Cow cow = new Cow();
soundAnimal(dog);
soundAnimal(cat);
soundAnimal(cow);
}
public static void soundAnimal(Animal animal) {
System.out.println("동물 소리 테스트 시작");
animal.sound();
System.out.println("동물 소리 테스트 종료");
}
}
soundAnimal(Animal animal)
soundAnimal(dog)
호출 시,Animal animal = dog
로 Dog 인스턴스가 전달된다.- (디형적 참조) Animal은 부모타입이고 Dog, Cat, Cow는 자식타입이다. 부모는 자식을 담을 수 있다.
animal.sound()
메서드 호출 시, 호출자와 동일한 Animal 클래스에 먼저 접근하여sound()
메서드를 실행한다.- Dog 클래스에서
sound()
메서드를 오버라이딩하였기 때문에 “멍멍”이 출력된다.
다형성 활용 (배열과 for문 사용)
- 위의 코드를 배열과 for문을 사용하여 중복을 제거한다.
package poly.ex2; public class AnimalPolyMain2 { public static void main(String[] args) { Dog dog = new Dog(); Cat cat = new Cat(); Cow cow = new Cow(); Animal[] animalArr = {dog, cat, cow}; for (Animal animal : animalArr) { soundAnimal(animal); } } }
- 배열은 같은 타입의 데이터를 나열할 수 있다.
Animal[] animalArr = {new Dog(), new Cat(), new Cow()}
도 가능하다.
- for문을 사용해서
soundAnimal(anima)
메서드를 반복 호출하면 메서드 오버라이딩에 의해 각 인스턴스의 오버라이딩 된sound()
메서드가 호출된다.
- 배열은 같은 타입의 데이터를 나열할 수 있다.
추상클래스
- 추상 클래스 : 상속을 목적으로 부모 클래스를 제공하지만, 실체인 인스턴스가 존재하지 않는 클래스이다.
abstract
키워드를 사용한다.- 추상클래스는 다형성을 위해 존재하는 클래스(ex. Animal)의 인스턴스를 생성할 문제를 근본적으로 방지한다.
- 추상 메서드 : 부모 클래스를 상속 받은 자식 클래스가 반드시 오버라이딩 해야 하는 메서드를 부모 클래스에 정의한 메서드이다.
abstract
키워드를 사용한다.- 메서드 바디가 없다.
- 추상 메서드가 하나라도 있는 클래스는 추상 클래스로 선언해야 한다.
- 추상 메서드를 오버라이딩 하지 않으면 자식 클래스도 추상 클래스가 되어야 한다.
- 실수로 메서드를 오버라이딩 하지 않을 문제를 근본적으로 방지한다.
package poly.ex3; public abstract class AbstractAnimal { // 추상 클래스 public abstract void sound(); // 추상 메서드 public void move() { System.out.println("동물이 움직입니다."); } }
package poly.ex3; // Dog, Cat, Cow 하위 클래스 생성 public class Dog extends AbstractAnimal { @Override public void sound() { System.out.println("멍멍"); } }
package poly.ex3; public class AbstractMain { public static void main(String[] args) { // 추상 클래스 생성 불가 AbstractAnimal animal = new AbstractAnimal(); Dog dog = new Dog(); Cat cat = new Cat(); Cow cow = new Cow(); cat.sound(); cat.move(); soundAnimal(cat); soundAnimal(dog); soundAnimal(cow); } private static void soundAnimal(AbstractAnimal animal) { System.out.println("동물 소리 테스트 시작"); animal.sound(); System.out.println("동물 소리 테스트 종료"); } }
순수 추상 클래스
- 모든 메서드가 추상 메서드인 클래스이다.
public abstract class AbstractAnimal { public abstract void sound(); public abstract void move(); }
- 인스턴스를 생성할 수 없다.
- 상속하는 자식 클래스는 모든 메서드를 오버라이딩 해야 한다.
- 주로 다형성을 위해 사용된다.
인터페이스
- 인터페이스 : 순수 추상 클래스를 더 편리하게 사용하기 위한 기능을 제공한다.
interface
키워드를 사용한다.- 인터페이스를 구현하는 클래스는
implements
키워드를 사용한다.- 인터페이스는 물려받을 수 있는 기능이 없어서 상속 말고 구현한다라고 표현한다.
- 인터페이스의 모든 메서드는
public abstract
이며 생략이 권장된다. - 인터페이스의 멤버 변수는
public static final
키워드를 모두 포함하며 생략이 권장된다.public interface InterfaceAnimal { public static final int MY_PI = 3.14; // public static final 생략 가능 }
package poly.ex5; public interface InterfaceAnimal { void sound(); void move(); }
package poly.ex5; public class Dog implements InterfaceAnimal { @Override public void sound() { System.out.println("멍멍"); } @Override public void move() { System.out.println("개 이동"); } } public class Cat implements InterfaceAnimal { @Override public void sound() { System.out.println("야옹"); } @Override public void move() { System.out.println("고양이 이동"); } } public class Cow implements InterfaceAnimal { @Override public void sound() { System.out.println("음매"); } @Override public void move() { System.out.println("소 이동"); } }
package poly.ex5; public class InterfaceMain { public static void main(String[] args) { // 인터페이스 생성 불가 // InterfaceAnimal interfaceMain1 = new InterfaceAnimal(); Cat cat = new Cat(); Dog dog = new Dog(); Cow cow = new Cow(); soundAnimal(cat); soundAnimal(dog); soundAnimal(cow); } private static void soundAnimal(InterfaceAnimal animal) { System.out.println("동물 소리 테스트 시작"); animal.sound(); System.out.println("동물 소리 테스트 종료"); } }
인터페이스 - 다중 구현
- 자바는 다중 상속을 지원하지 않는다.
- 여러 부모 클래스에 동일한 이름의 메서드가 있다면, 자식 클래스는 어떠한 부모의 메서드를 사용해야할 지에 대한 문제가 발생하기 때문이다. (다이아몬드 문제)
- 자바는 인터페이스의 다중 구현을 허용한다.
- 인터페이스는 모두 추상 메서드로 이루어져있다. 인터페이스를 구현하는 클래스에서는 모든 기능을 구현해야 한다.
- 즉, 인터페이스들을 구현한 클래스에 있는 메서드를 사용하는 것이기 때문에 다이아몬드 문제가 발생하지 않는다.
public class Child extends Parent implements InterfaceA, InterfaceB { @Override ... }
implements
키워드 뒤에,
로 여러 인터페이스를 구분한다.extends
가implements
보다 먼저 나와야 한다.
12. 다형성과 설계
다형성의 실세계 비유
- 객체를 설계할 때 역할과 구현을 분리한다.
- 역할 = 인터페이스
- 구현 = 인터페이스를 구현한 클래스, 구현 객체
- 객체 클라이언트와 객체 서버는 서로 요청과 응답이라는 협력 관계를 가진다.
- 다형성으로 인터페이스를 구현한 객체를 실행 시점에서 유연하게 변경할 수 있다.
- 오버라이딩
- 클라이언트를 변경하지 않고 서버의 구현 기능을 변경한다.
- 한계 : 역할(인터페이스)가 변하면, 클라이언트와 서버 모두에 큰 변경이 발생한다.
- 인터페이스를 안정적으로 잘 설계하는 것이 중요하다.
- 인터페이스를 안정적으로 잘 설계하는 것이 중요하다.
다형성 - 역할과 구현 예제
Driver
: 클라이언트Driver
클래스는Car car
을 멤버 변수를 가진다.Driver
는Car
인터페이스에만 의존하고 구현(K3Car
,Model3Car
에는 의존하지 않는다.
package poly.car1; public interface Car { void startEngine(); void offEngine(); void pressAccelerator(); }
package poly.car1; public class K3Car implements Car { @Override public void startEngine() { System.out.println("K3Car.startEngine"); } @Override public void offEngine() { System.out.println("K3Car.offEngine"); } @Override public void pressAccelerator() { System.out.println("K3Car.pressAccelerator"); } }
package poly.car1; public class Model3Car implements Car { @Override public void startEngine() { System.out.println("Model3Car.startEngine"); } @Override public void offEngine() { System.out.println("Model3Car.offEngine"); } @Override public void pressAccelerator() { System.out.println("Model3Car.pressAccelerator"); } }
package poly.car1; public class Driver { private Car car // 멤버 변수로 Car car을 가진다. public void setCar(Car car) { // 멤버 변수에 자동차를 설정한다. System.out.println("자동차를 설정합니다: " + car); this.car = car; } public void drive() { System.out.println("자동차를 운전합니다."); car.startEngine(); car.pressAccelerator(); } }
package poly.car1; public class CarMain1 { public static void main(String[] args) { Driver driver = new Driver(); //차량 선택 (k3) Car k3Car = new K3Car(); driver.setCar(k3Car); driver.drive(); // 차량 변경 (model) Car model3Car = new Model3Car(); driver.setCar(model3Car); driver.drive(); } }
실행결과
자동차를 설정합니다: poly.car.K3Car@24d46ca6 자동차를 운전합니다. K3Car.startEngine K3Car.pressAccelerator K3Car.offEngine 자동차를 설정합니다: poly.car.Model3Car@372f7a8d 자동차를 운전합니다. Model3Car.startEngine Model3Car.pressAccelerator Model3Car.offEngine
K3Car
를 생성한다driver.setCar(k3Car)
을 호출해서K3Car
의 인스턴스를 참조한다.driver.drive()
호출 시x001
을 참조한다.- 호출자 타입인
car
을 찾아서 실행하지만, 메서드 오버라이딩된K3Car
의 기능이 호출된다.
Model3Car
를 생성한다.driver.setCar(model3Car)
를 호출해서Model3Car
의 인스턴스를 참조하도록 한다.driver.drive()
호출 시x002
을 참조한다.- 위와 동일하게 호출자 타입과 동일한
car
을 찾아서 실행하지만, 메서드 오버라이딩된Model3Car
의 기능이 호출된다.
OCP(Open-Closed Principle) 원칙
- OCP 원칙 : 객체 지향 설계 원칙 중 하나.
- Open for extension : 새로운 기능의 추가나 변경 사항이 생겼을 때, 기존 코드를 확장할 수 있어야 한다.
- Closed for modification : 기존의 코드는 수정되지 않아야 한다.
- 즉, 기존 코드의 수정 없이 새로운 기능을 추가할 수 있어야 한다.
- 새로운 차량을 추가해도
Driver
의 코드는 변경되지 않는다.main()
일부를 제외한 프로그램의 핵심 코드는 전혀 수정되지 않아도 된다.- 기능 추가 :
Car
인터페이스를 사용해서 새로운 차량을 자유롭게 추가할 수 있다. - 확장 가능 : 클라이언트인
Driver
은Car
인터페이스를 통해 추가된 차량을 자유롭게 호출 가능하다. - 새로운 차를 생성하고,
Driver
에게 차를 전달해주는 역할인main()
은 코드 수정이 발생하지만, 핵심 코드는 그대로 유지된다.
- 기능 추가 :
김영한의 실전 자바 기본편을 참고하였습니다