| 다형성 (사용 편의성)
다형성이란, 하나의 객체가 여러가지 타입을 가질 수 있는 것을 의미한다.
다형성은 오버로딩과 오버라이딩을 통해 구현할 수 있다.
오버라이딩이 가능한 이유는 뭘까? 그건 동적 바인딩 때문이다.
여기서 동적 바인딩이란 메서드가 실행 시점에서 성격이 결정되는 것을 말한다.
종류 | 정적바인딩(static binding) | 동적바인딩(Dynamic binding) |
정의 | 컴파일 시간에 성격이 결정 | 실행 시간(런타임)에 성격이 결정 |
예시 | C언어 컴파일 시간에 변수의 데이터타입이 결정 ex. int *p; -- 미리 데이터타입 지정 |
javascript 런타임에 값에 따라 변수의 데이터타입 결정 ex. var c = 1; -- 미리 데이터타입 지정x ex. Person p = new Person(), p.method() -- p 생성 전엔 p가 가리키는 곳 어딘지 알 수 없음 |
장단점 | 컴파일 시간에 많은 정보가 확정되어있어 실행 효율↑ 값이 변하지 않아서 안정적 |
런타임에 자유롭게 성격이 바뀌므로 적응성↑ 메모리 공간의 낭비, 느린 속도 |
- 자바에서는 static 메소드의 경우 정적바인딩을, 일반 메소드의 경우 동적바인딩을 사용한다.
| 다형성의 장단점
장점 | 유지보수가 쉽다 | 하나의 타입으로 여러 가지의 객체를 관리할 수 있어 유지보수가 쉽다. |
재사용성이 증가 | 객체를 재사용하기가 쉬워 개발자의 코드 재사용성이 높아진다. | |
느슨한 결합 | 부모타입으로 자식인스턴스를 참조할 경우 클래스간 의존성이 낮아지며, 확장성이 높고, 결합도가 낮아진다. |
|
단점 | 프로그램의 가독성이 낮아짐 | 실제 인스턴스가 무엇인지 instanceof가 없으면 알기 어렵다 |
디버깅의 어려움 | - |
| 다형성 구현 - 오버로딩과 오버라이딩
다형성을 개념적으로, 구현적으로 나누기도 하는 걸 봤었는데, 결국 핵심은 오버로딩과 오버라이딩이었다.
여기에 어떤 분들은 추가적으로 함수형 인터페이스를 함께 소개하기도 해서, 아래와 같이 넣어보았다.
오버로딩(중복 정의) | 같은 이름의 메소드를 매개변수를 다르게 해서 적재 |
오버라이딩(재정의) | 똑같은 선언부의 메소드를 다른 내용으로 재정의 |
함수형 인터페이스 | 한 개의 추상 메소드를 가지고 있는 인터페이스 |
1. 오버로딩
오버로딩의 조건 1. 메소드의 이름이 같아야 한다. 2. 매개 변수의 갯수 또는 타입이 달라야한다. 3. 반환 타입이 다른 것은 관계 없다. |
- 오버로딩의 대표적인 예로, PrintStream의 println()이 있다.
public class PrintStream extends FilterOutputStream
implements Appendable, Closeable
{
public void println() {
newLine();
}
public void println(boolean x) {
synchronized (this) {
print(x);
newLine();
}
}
public void println(char x) {
synchronized (this) {
print(x);
newLine();
}
}
public void println(int x) {
synchronized (this) {
print(x);
newLine();
}
}
public void println(long x) {
synchronized (this) {
print(x);
newLine();
}
}
}
2. 오버라이딩
오버라이딩의 조건 1. 상위 클래스를 상속 또는 구현해야한다. 2. 물려 받은 메소드를 구현할 때는 선언부가 동일해야 한다. 3. 하위 클래스는 상위 클래스보다 접근제어자가 더 공개적이거나 같아야 한다. (ex. 상위가 public -> 하위도 public, 상위가 protected -> 하위는 protected or public) 4. final과 private인 메소드는 오버라이딩이 불가하다. |
[1] 상위 클래스를 상속하고 메소드를 오버라이딩하는 경우
- 지난번 상속에서 사용한 Vehicle를 조금 변형해서 '움직인다(move)' 기능만 만들어 보았다.
- Vehicle을 상속하는 Car, Train, Airplane은 move를 오버라이딩하여 저마다의 소리를 낸다.
package polymorphism;
class Vehicle {
public void move() {
System.out.println("unknown");
}
}
class Car extends Vehicle{
public void move() {
System.out.println("부릉부릉");
}
}
class Train extends Vehicle{
public void move() {
System.out.println("칙칙폭폭");
}
}
class Airplane extends Vehicle{
public void move() {
System.out.println("위잉위잉");
}
}
- 메인 함수에서의 출력
각 하위 인스턴스들을 부모 참조변수인 Vehicle로 통일하여 참조하는 걸 아래에서 볼 수 있다.
이렇게 부모 참조변수로 자식 인스턴스를 참조하는 것을 업캐스팅이라고 한다.
업캐스팅 | |
의미 | 상위 객체 참조 변수로 하위 인스턴스를 참조하는 것 |
제약사항 | 상위 객체 참조 변수를 쓰면, 하위 객체의 맴버를 참조할 수 없다. (= 하위 객체 변수를 car.xxx식으로 쓸 수 없다) ex. Vehicle을 상속받은 Car객체엔 Car만의 맴버 "gasDisplacement"와 "drive()"가 있다. Vehicle car = new Car(); <-- 이 경우에, car.gasDisplaement와 car.drive() 를 쓸 수가 없다 |
package polymorphism;
public class Test {
public static void main(String[] args) {
Vehicle car = new Car();
car.move();
System.out.println();
Vehicle train = new Train();
train.move();
System.out.println();
Vehicle air = new Airplane();
air.move();
System.out.println();
}
}
부릉부릉
칙칙폭폭
위잉위잉
[2] 인터페이스를 사용하는 경우
- 위와 같이만 만들 경우에는 사실 다형성의 장점인, '느슨한 결합'을 온전히 지키기 어렵다.
- 왜냐하면 여전히 부모의 객체를 수정할 때, 자식에게 미치는 영향이 있기 때문에 (객체 간 의존성 ↑) 자식 입장에서 부모의 정보를 잘 알고 있어야 하기 때문이다.
- 그렇기에 공통 기능, '움직인다(move)'를 뽑아 movable인터페이스를 만드는 것이 느슨한 결합을 만들기에 더 좋다.
package polymorphism;
public interface Movable {
public abstract void move();
}
class Car implements Movable{
@Override
public void move() {
System.out.println("부릉부릉");
}
}
class Train implements Movable{
@Override
public void move() {
System.out.println("칙칙폭폭");
}
}
class Airplane implements Movable{
@Override
public void move() {
System.out.println("위이위잉");
}
}
- 메인 함수에서의 출력
package polymorphism;
import java.util.Arrays;
import java.util.List;
public class Test {
public static void main(String[] args) {
List<Movable> movables = Arrays.asList(new Car(), new Airplane(), new Train());
for (Movable one : movables) {
one.move();
}
}
}
3. 함수형 인터페이스
함수형 인터페이스는 한 개의 추상 메소드를 가진 인터페이스를 말한다.
함수형 인터페이스는 람다식을 통해 간편하게 사용할 수 있는데,
앞서 쓴 movable 인터페이스를 람다식으로 바꿔 쓰면 아래와 같다.
// 함수형 인터페이스 사용
public class Test {
public static void main(String[] args) {
List<Movable> movables = Arrays.asList(new Car(), () -> System.out.println("다그닥다그닥"));
for (Movable one : movables) {
one.move();
}
}
}
4. 객체 타입을 확인하는 instanceof, 지양해야할까?
instanceof 를 통해 부모참조변수가 참조하는 인스턴스가 무엇인지 알 수 있다.
그런데 instanceof를 사용하는 것을 지양하라는 이야기가 있다. 왜 그런 걸까?
아래의 코드를 보면, instanceof를 사용해서 실제 참조하는 인스턴스가 무엇인지 볼 수 있다.
class Test {
public static void main(String[] args) {
List<Movable> movables = Arrays.asList(new Car(), new Airplane(), new Train());
for (Movable one : movables) {
if (one instanceof Car) {
System.out.println("This is a Car");
one.move();
} else if (one instanceof Airplane) {
System.out.println("This is an Airplane");
one.move();
} else if (one instanceof Train) {
System.out.println("This is a Train");
one.move();
}
}
}
}
이렇게 실제 인스턴스를 보게 주게 되면 외부의 객체에 정보를 고스란히 노출하는 것이기 때문에, 캡슐화가 깨지게 된다.
캡슐화란, 객체가 가진 상태나 행위를 다른 이가 사용하거나 보지 못하도록 숨기는 것
[출처] https://tecoble.techcourse.co.kr/post/2021-04-26-instanceof/
따라서, 되도록 instanceof는 사용하는 것을 지양하는 것이 좋다.
[ 참고 ]
자바의 정석
스프링 입문을 위한 자바 객체 지향의 원리와 이해
instanceof의 지양, https://tecoble.techcourse.co.kr/post/2021-04-26-instanceof/
https://tecoble.techcourse.co.kr/post/2020-10-27-polymorphism/
https://m.blog.naver.com/PostView.naver?isHttpsRedirect=true&blogId=next7885&logNo=130068105493
https://life-with-coding.tistory.com/485
http://www.tcpschool.com/java/java_polymorphism_concept
'Language > Java' 카테고리의 다른 글
JAVA 라이브러리 - Java.lang 패키지 (0) | 2022.08.08 |
---|---|
객체지향 언어의 특징 4_캡슐화 (정보 은닉) (0) | 2022.07.22 |
객체지향 언어의 특징 2_상속 (재사용 + 확장) (0) | 2022.07.20 |
객체지향 언어의 특징 1_추상화 (모델링) (0) | 2022.07.20 |
객체지향 프로그래밍이란? (0) | 2022.07.18 |