목차
- 다형성
1-1. 다형성의 정의
1-2. 다형성의 특징 - 동적바인딩
- 연산자 instanceof
- 클래스 형변환
- 다형성 활용 예시
5-1. 다형성과 객체배열
5-2. 다형성과 매개변수
5-3. 다형성과 리턴 타입 - 추상클래스와 인터페이스
6-1. 추상클래스
6-2. 인터페이스
6-3. 추상클래스와 인터페이스 비교
- 다형성은 캡슐화, 상속과 함께 객체 지향 프로그래밍(OOP, Object Oriented Programming)의 3대 특징 중 하나이다.
1. 다형성
1-1. 다형성의 정의
- 하나의 인스턴스가 여러 타입을 가질 수 있다는 의미이다. Student는 Person이자 Object이기도 하다.
- 상속의 정의를 다룰 때 멤버(필드, 메소드) 외에 타입 또한 상속이 된다고 표현했다. 여기서는 그 타입의 상속을 다룬다.
Person
△
↑
Student
- 위 같은 예시상에서 Student는 Person이라는 타입도 상속 받은 것이다.
- Student는 Person이기도 하다.
Parent
△
↑
Child
❗ Parent p = new Child();
- 상위 타입으로 하위 타입의 객체를 사용할 수 있다.
- 부모 타입의 레퍼런스 변수로 자식 타입의 인스턴스를 다룰 수 있는 것이 다형성이다.
1. Parent pA = new Parent(); 사용 가능!
2. Child cA = new Child(); 사용 가능!
3. Parent pB = new Child(); 다형성이 적용돼 가능! Child는 Parent이기도 한 것이다. 이 구문을 기억할 필요가 있다.
4. Child cB = new Parent(); 사용 불가! Parent는 독립적으로 Parent이다.
- 아래는 자식클래스가 여럿일 때의 예시이다.
Parent
△
↑
Child1 | Child2 | Child3
Parent[] arr = new Parent[3];
arr[0] = new Child1();
arr[1] = new Child2();
arr[2] = new Child3();
- Parent 타입을 상속 받은 자식클래스이기 때문에 이와 같은 구조로도 사용이 가능하다.
- new 연산자 뒷자리가 갑자기 다른 자식으로 바뀐다 하더라도 전혀 문제가 없을 것이다.
- 이처럼 다형성은 상속을 기반으로 한 기술이다.
1-2. 다형성의 특징
- 여러 타입의 객체를 하나의 타입으로 관리하기 때문에 유지보수성과 생산성이 증가된다. 앞선 예시에서처럼 Child 1, 2, 3 대신 Parent 하나로 관리할 수가 있는 것이다.
- 상속 관계에 있는 모든 객체는 동일한 메시지(==method)를 수신할 수 있다. Parent가 가진 멤버(필드, 메소드) 모두 Child 1, 2, 3도 가지고 있기 때문이다.
- 다형성을 통해 확장성이 좋은 코드, 결합성을 낮춰 유지보수성이 높은 코드를 만들 수 있다.
✅ 다형성에 대해 이해할 수 있다.
✅ 다형성의 목적에 대해 이해할 수 있다.
2. 동적바인딩
- 바인딩은 연결을 의미한다. 연결은 Parent 타입의 메소드에 정적 연결돼 있으나, 실행 시에는 Child가 가지고 있는 메소드로 동적 실행된다.
- 즉 컴파일 당시에는 부모 타입의 메소드와 연결되어 있다가, 런타임 시에는 실제 객체가 가진 오버라이딩 된 메소드로 바인딩이 바뀌어 동작하는 것이다. 이를 동적바인딩이라고 한다.
Parent methodA "부모" |
△ ↑ Child @Override methodA "자식" |
Parent p = new Child();
p.methodA();
==============
자식
- 여기서 methodA 클릭하면 참조하고 있는 것으로 Parent 메소드를 보여준다.
- 하지만 정작 실행하면 오버라이딩 된 Child의 메소드로 동작한다.
- 이 과정을 동적바인딩이라고 일컫는다.
❗ 동적바인딩 성립 조건
A. 상속 관계로 이루어져
B. 다형성이 적용된 경우에
C. 메소드 오버라이딩 되어 있어야 한다.
즉 세 가지 조건 모두 만족한 경우에 동작한다.
✅ 동적바인딩에 대해 이해할 수 있다.
✅ 동적바인딩의 성립 조건에 대해 이해할 수 있다.
3. 연산자 instanceof
- 레퍼런스 변수가 해당 클래스 타입의 객체 주소를 참조하고 있는지 확인할 때 사용한다.
- true or false 조건식 결과에 따라 값을 반환한다.
Car 부모클래스
Sonata | Avante | Grandure 자식클래스
Car c = new Grandure();
- 레퍼런스 변수 c에 저 셋 중 누가 들었는지 모르니 이를 확인하는 기능이다.
❗ 레퍼런스변수명 instanceof 클래스타입
if(c instanceof Grandure) {
((Grandure)c).moveGrandure();
}
- if문을 사용해 레퍼런스 변수 c가 Grandure 클래스 타입을 참조하고 있다면 중괄호 {} 안의 문장을 실행하라고 한 것이다.
✅ 연산자 instanceof을 이해하고 활용할 수 있다.
4. 클래스 형변환(class type casting)
- 부모클래스, 자식클래스간에 형변환은 업캐스팅(up-casting)과 다운캐스팅(down-casting)로 구분된다.
업캐스팅(up-casting)
- 상위 타입으로의 형변환을 말한다.
- 업캐스팅의 경우는 명시적, 묵시적 형변환이 모두 가능하다. 즉 자동 형변환이 적용되는 상황이다. 코드 구현 시 굳이 작성하지 않고 생략 가능하다.
Animal animal1 = (Animal) new Rabbit(); *up-casting 명시적 형변환*
Animal animal2 = new Rabbit(); *up-casting 묵시적 형변환*
다운캐스팅(down-casting)
- 하위 타입으로의 형변환을 의미한다.
- 자식 타입만이 가지는 기능들이 있을 때 그것을 사용하기 위해서는 다운캐스팅이 필요하다.
- 다운캐스팅은 명시적 형변환을 통해서만 실현된다.
Rabbit rabbit1 = (Rabbit) animal1; *down-casting 명시적 형변환*Rabbit rabbit2 = animal2;*down-casting 묵시적 형변환 불가*
Animal a = new Rabbit();
stack | heap |
a | eat | run | cry | jump ■ ■ ■ □ |
((Rabbit)a).jump();
stack | heap |
a | eat | run | cry | jump ■ ■ ■ ■ |
- 이처럼 객체별 고유한 기능을 동작시키기 위해서는 레퍼런스 변수를 형변환하여 Rabbit과 Tiger로 명시해야 메소드 호출이 가능하다.
❗(클래스명)레퍼런스변수명.잘못 쓴 예시이다. 여기서 참조연산자는 계속해서 변수 a를 가리킨다.
❗ ((클래스명)레퍼런스변수명). 소괄호 반드시 써야 한다. ((Rabbit)a).
ClassCastException
- 타입 형변환 시 실제 인스턴스와 타입이 일치하지 않는 경우에는 ClassCastException이 발생할 수 있다.
- 타입 형변환을 잘못하는 경우 컴파일 시에는 문제가 되지 않지만 런타임 시 Exception 에러가 발생한다. 즉 이클립스에서 빨간 줄이 가진 않지만, 실행하거든 콘솔 창에 다음과 같은 오류 메시지가 나타난다: java.lang.ClassCastException
❗ 따라서 instanceof 연산자를 이용해 해당 타입이 맞는지 확인한 후 → 클래스 형변환을 적용한다.
즉, 클래스 형변환에 앞서 instanceof 연산자 사용이 우선되어야 하는 것이다.
for(int i=0; i < animals.length; i++) {
if(animals[i] instanceof Rabbit) {
((Rabbit)animals[i]).jump(); 토끼면 점프하고
} else if(animals[i] instanceof Tiger) {
((Tiger)animals[i]).bite(); 호랑이면 물어라
} else {
System.out.println("호랑이나 토끼가 아닙니다."); 토끼 또는 호랑이가 아니면 출력
}
}
- 이처럼 레퍼런스 변수가 참조하는 실제 인스턴스가 원하는 타입과 맞는지 비교하는 연산자가 바로 instanceof이다.
- 다형성 특성에 따라 상속 받은 부모 클래스 타입도 함께 가지고 있다: a instanceof Animal==true
- 또한, Object 타입인 것도 확인할 수가 있다. 모든 클래스는 Object의 후손이기 때문이다: a instanceof Object==true
✅ 타입형변환에 대해 이해할 수 있다.
5. 다형성 활용 예시
5-1. 다형성과 객체배열
- 다형성과 객체배열을 복합하면 여러 인스턴스를 하나의 레퍼런스 변수로 연속 처리할 수 있다.
- 즉 선언한 타입은 부모 타입이지만, 그 안에 자식 타입의 인스턴스를 참조하도록 만들 수 있는 것이다.
=== 1. 상위 타입의 레퍼런스 배열 선언 ===
Car[] car = new Car[3];
=== 2. 각 인덱스에 인스턴스 생성 및 대입 ===
car[0] = new Sonata();
car[1] = new Avante();
car[2] = new Grandure();
=== 3. 메소드 호출(동적바인딩 적용) ===
for(int i=0; i < car.length; i++) {
car[i].move();
}
- 예시에서 move를 ctrl + 클릭하면 정적바인딩 따라 Car 타입의 move 메소드를 가리키고 있다고 보여준다.
- 하지만 정작 실행하거든 동적바인딩 따라 각각이 오버라이딩 한 move 메소드가 실행된다.
- 실행 순서는 다음의 예시와 같다.
① 상위 타입의 레퍼런스 배열을 만든다.
Animal[] animals = new Animal[3];
② 각 인덱스에 인스턴스들을 생성해서 대입한다.
animals[0] = new Rabbit();
animals[1] = new Tiger();
animals[2] = new Rabbit();
③ index별로 호출한다.
(바로 이때 오버라이딩 한 하위 타입 메소드들이 호출돼 동적바인딩이 일어난다.)
for(int i=0; i < animals.length; i++) {
animals[i].cry();
}
5-2. 다형성과 매개변수
- 매개변수 부분에 (상위 타입)을 작성함으로써 하위 타입들이 알맞게 작동할 수 있도록 한다.
- 묵시적 형변환을 이용해 매개변수가 다양한 자식클래스 자료형을 받아줄 수 있다.
public void buy(Sonata s) {}
public void buy(Avante a) {}
public void buy(Grandure g) {}
.
.
- 예를 들면 매 하위 타입마다 새롭게 정의해야 했을 메소드를 하나로 통합해 사용할 수가 있다.
public void buy(Car c) {
c.pay();
}
- 앞선 예시를 매개변수로 명시한 예이다.
- 동적바인딩 따라 pay는 각각의 하위 타입에서 선언한 대로 동작할 것이다.
Application app = new Application();
app.buy(new Sonata());
app.buy(new Avante());
app.buy(new Grandure());
- 호출 구문에서 인자로 넘긴 예시이다.
- 인자 값이 new Sonata()라는 것은 Car c = new Sonata();를 일컫는다. 따라서 buy(Sonata s)와 같은 결과를 낳는다.
- 자식 타입마다 각각의 메소드를 선언해 처리해야 했을 기능들을 매개변수에 부모 타입을 선언함으로써 누구든 적용될 수 있도록 한 것이다.
Rabbit animal3 = new Rabbit();
Tiger animal4 = new Tiger();
app3.feed((Animal)animal3); *명시적 형변환 업캐스팅(생략 가능)*
app3.feed(animal4);
- 이때 물론 하위 타입들을 호출 구문과 함께 각각의 레퍼런스 변수로 담았다가 전달할 수 있다.
app3.feed(new Rabbit());
app3.feed(new Tiger());
- 인스턴스를 생성하며 바로 묵시적 형변환을 이용해 전달할 수도 있다.
5-3. 다형성과 리턴 타입
- 다형성을 활용한 코드는 확장성을 포용한다.
- 예를 들어 Animal이라는 부모 타입 밑으로 또 다른 동물이 추가되더라도 선언해둔 메소드들은 큰 변경 없이 계속 사용이 가능한 것이다.
public Animal getRandomAnimal() {
int random = (int)(Math.random() * 2);
return random == 0 ? new Rabbit() : new Tiger();
}
Application app = new Application();
- non-static 메소드이기 때문에 클래스명을 활용해 호출 구문 작성한다.
Animal randomAnimal = app.getRandomAnimal();
- 메소드 통해 리턴된 랜덤한 값을 변수에 담는다.
randomAnimal.cry();
- 변수를 호출한다.
- 다형성을 적용하지 않고 반환 받으려면 Tiger를 리턴 받는 메소드와 Rabbit을 리턴 받는 메소드를 따로 만들어야 한다.
- 만약 각각 따로 만들어야 했다면, 랜덤 값으로 추출하는 메소드는 아마 활용되기 어려웠을 것이다.
✅ 객체배열에 다형성을 적용하여 사용할 수 있다.
✅ 매개변수에 다형성을 적용하여 사용할 수 있다.
✅ 리턴타입에 다형성을 적용하여 사용할 수 있다.
6. 추상클래스와 인터페이스
6-1. 추상클래스
추상클래스
- 추상클래스는 미완성 된 클래스라고 할 수 있다.
- 추상클래스 사용 목적은 오버라이딩 강제화에 있다.
public class Computer extends Product
- 추상클래스 Product에 대고 extends 선언하자마자 컴파일 에러 발생한다.
- 해결 방안은 그 추상클래스가 가진 추상메소드들을 완성시키는 것이다. 이를 두고 오버라이딩을 반드시 행하게끔 한 경우, 즉 오버라이딩의 강제성이 부여된 것이라고 표현한다.
*필드*
private int nonStaticField;
private static int staticField;
*생성자* 인스턴스 생성 불가!
public Product() {}
- 추상클래스는 클래스와 마찬가지로 필드, 생성자, 일반 메소드를 가질 수 있다.
- 단, 생성자의 경우 직접적으로 인스턴스를 만들 수는 없다!
- 추상클래스로는
인스턴스를 생성할 수 없다. heap 영역에 메모리를 할당할 수 없는 것이다. - 추상메소드(미완성 메소드)를 0개 이상 포함하는 클래스이다. 꼭 추상메소드만 취급하는 공간이 아니기 때문이다. 달리 말해 이 클래스로는 객체 생성하지 말라는 의미로 의도적으로 abstract 키워드를 쓸 수도 있다.
추상메소드
abstract
❗ public abstract void abstMethod();
- 메소드 선언부만 있고 구현부가 없는 메소드이다. 메소드 바디를 중괄호 {} 없이 세미콜론 ; 으로 명명한다.
- 메소드 헤드의 접근제한자와 반환형 사이에 abstract 키워드를 작성한다.
- 해당 클래스 이름에도 abstract 추가한다: public abstract class Product
- 이 같은 추상메소드를 가진 클래스는 무조건 추상클래스이다.
- 추상클래스의 추상메소드는 오버라이딩에 대한 강제성을 만든다.
- 다형성에 기반해 추상클래스를 상속 받은 하위 클래스는 미완성인 것을 완성시키고 인스턴스를 생성한다.
- 즉, 추상클래스는 가이드라인만 부여할 뿐이다.
- 이를 상속 받은 하위클래스들이 가이드라인 따라서 인스턴스를 만들어간다.
Animal animal = new Animal();추상클래스는 이처럼 완성해 쓸 것이 목적이 아니다!
Animal a1 = new Tiger();
Animal a2 = new Rabbit();
- 상위 타입인 Animal 클래스에서는 public abstract void eat(); public abstact void run(); public abstract void cry(); 메소드들이 있어야 한다고 표시만 해놓는 것이다.
- 하위 타입인 Tiger 클래스는 Animal 클래스로부터 상속을 받게 된 즉시 위 추상메소드들을 완성시켜야 한다.
- 따라서 추상클래스는 개발 시 일관된 인터페이스(interface, 사용법; 접근 방식; 메소드 호출)를 제공할 수 있도록 한다.
뒤에서 다룰 인터페이스와는 다른 의미이니 혼용하지 말자! - 여러 클래스들을 그룹화하고, 필수 기능을 추상메소드로 정의한 후 강제성을 부여하는 그 특성 때문이다.
6-2. 인터페이스
- 추상메소드와 상수 필드만 취급하는 클래스의 변형체이다. 즉 일반 메소드와 일반 필드는 가질 수 없다.
- 사용 목적은 추상클래스와 비슷하다: 필요한 기능을 공통화해서 강제성을 부여할 목적이다. 표준화한 기능을 만들기 위함이다.
- 자바의 단일 상속이라는 단점을 극복하게 한다. 다중 상속을 가능케 하는 것이다.
implements
public class Application implements InterOne, InterTwo {}
- 사용을 위해서는 implements 키워드를 쓴다.
- implements 인터페이스명 선언하는 순간 클래스명에 빨간 줄이 생긴다. 추상메소드를 작성하라는 지시이다.
public class Product extends java.lang.Object implements InterProduct, java.io.Serializable {}
- 인터페이스는 콤마 찍고 다중 상속 가능하다.
- extends가 implements보다 먼저 선언되어야 한다.
public interface InterProduct extends java.io.Serializable, java.util.Comparator {}
- 인터페이스가 인터페이스를 상속 받을 때는 extends 키워드 사용한다. 이때도 여러 인터페이스를 다중 상속 받을 수 있다.
❗ 클래스 다이어그램에서 실선이 아닌 점선으로 표현됐다면 인터페이스를 말한다.
△
ː
public interface InterA {}
- 파일 생성 시 클래스와 다른 점이다. class로 명시되던 곳에 interface가 자리한다.
상수
public static final
public static final int MAX_NUM = 100;
int MIN_NUM = 10; *묵시적 상수 취급*
- public static final 제어자 조합을 상수 필드라고 부른다. 반드시 선언과 동시에 초기화해야 한다.
- 인터페이스에서는 일반 필드는 취급하지 않으며, 상수 필드만 작성 가능하다.
- 때문에 모든 필드는 묵시적으로 public static final이다.
System.out.println(InterProduct.MAX_NUM);
System.out.println(InterProduct.MIN_NUM);
- 상수 필드는 인터페이스명.필드명 으로 접근한다. 인터페이스명.메소드명으로 접근하는 static 메소드 호출과 닮아있다.
추상메소드
abstract
- 인터페이스의 메소드는 내용을 정의하기 위함이 아니라 규칙을 만들기 위해 존재하는 공간이다.
- 인터페이스에서는 일반적으로 추상메소드만 작성이 가능하다.
public abstract void nonStaticMethod();
void abstMethod(); *public abstract 생략 가능*
- 인터페이스 안에 작성한 메소드는 묵시적으로 public abstract의 의미를 가진다.
- 따라서 인터페이스의 메소드를 오버라이딩 하는 경우, 반드시 접근제한자를 public으로 해야 오버라이딩이 가능하다.
// public InterProduct() {}InterProduct interProduct = new Product(); *레퍼런스 타입으로만 가능*
// InterProduct interProduct = new InterProduct();
interProduct.nonStaticMethod();
interProduct.abstMethod();
- 인터페이스는
생성자를 가질 수 없다: Interfaces cannot have constructors - 인터페이스는 인스턴스를 생성하지 못하고, 생성자 자체가 존재하지 않는다.
- 다형성이 작용해 레퍼런스 타입으로만 사용이 가능하다. 결과는 동적바인딩에 의해 호출된다.
// public void nonStaticMethod() {}
- 인터페이스는 구현부가 있는
non-static 메소드를 가질 수 없다: Abstract methods do not specify a body
public static void staticMethod() {}// @Override// public static void staticMethod() {}InterProduct.staticMethod();
- 하지만 static 메소드는 작성이 가능하다. 이는 JDK 1.8 추가된 기능으로, 바디가 있어도 선언이 된다.
- static 메소드는 정적 메모리 영역에 할당되므로 객체가 되어야 하는 상황들이 필요 없기 때문이다.
- static 메소드는 오버라이딩 할 수 없다. 프로그램 실행 시에 다녀가는 static 성질 때문이다.
- static 메소드는 인터페이스명.메소드명(); 으로 호출한다.
public default void defaultMethod() {}// @Override// public default void defaultMethod() {}
- 또한, default 키워드를 사용한 non-static 메소드도 작성 가능하다. 역시 JDK 1.8에서 추가된 기능이다.
- default 메소드는 인터페이스에서만 작성 가능하다.
@Override
public void defaultMethod() {}
interProduct.defaultMethod();
- default 키워드를 제외하면 오버라이딩 가능하다!
- 여기서 오버라이딩 하지 않았다면 인터페이스의 default 메소드가 호출된다.
6-3. 추상클래스와 인터페이스 비교
- 면접 질문이나 시험에서 자주 다루는 부분이므로 명확하게 구분해 암기할 필요가 있다.
구분 | 추상클래스 | 인터페이스 |
상속 | 단일 상속 | 다중 상속 |
구현 | extends | implements 단, 인터페이스간 상속에는 extends |
추상메소드 | abstract 메소드 0개 이상 | 모든 메소드가 abstract |
abstract | 명시적 사용 클래스 안에 일반 메소드도 섞어 쓰기 때문 |
묵시적 사용 생략 가능 |
객체 | 객체 생성 불가 | |
용도 | 참조 타입 |
- 이처럼 추상클래스와 인터페이스 모두 참조 타입이기 때문에 다형성이 가진 이점들을 활용할 수 있는 것이다.
✅ 추상클래스의 특징에 대해 이해할 수 있다.
✅ 추상클래스의 사용 목적에 대해 이해할 수 있다.
✅ 인터페이스에 대해 이해할 수 있다.
✅ 인터페이스의 특징에 대해 이해할 수 있다.
✅ 인터페이스의 사용 목적에 대해 이해할 수 있다.
✅ 추상클래스와 인터페이스의 공통점과 차이점에 대해 이해할 수 있다.
'Java' 카테고리의 다른 글
[JAVA/수업 과제 practice] 다형성 Lv. 1~2 (0) | 2022.01.06 |
---|---|
[JAVA/수업 과제 practice] 상속 Lv. 1~2 (0) | 2022.01.06 |
[JAVA] 8. 상속 | super | 오버라이딩 (0) | 2022.01.04 |
[JAVA/수업 과제 practice] 객체배열 Lv. 1~2 (0) | 2022.01.03 |
[JAVA] 7. 객체배열 (0) | 2022.01.03 |