객체지향개념에서 다형성이란 '여러가지 타입을 한 가지 타입으로 처리 할 수 있는 기술'을 의미하며, 자바에서는 한 타입의 참조 변수로 여러 타입의 객체를 참조 할 수 있도록 하였다. 즉, 부모 클래스 타입의 참조 변수로 자식 클래스의 인스턴스를 참조 할 수 있다.
생성한 인스턴스를 다루기 위해, 인스턴스의 타입과 일치하는 타입의 참조 변수만 사용하기도 하지만, 상속 관계의 클래스에서는 자식 클래스 타입의 인스턴스를 생성 할 때, 부모 클래스 타입의 참조 변수를 사용 할 수 있다.
Parent.java
01 package poly.sample;
02
03 public class Parent {
04 protected int num;
05
06 public void display() {
07 System.out.println("부모 클래스 메소드");
08 }
09 }
polysample.Child.java
01 package poly.sample;
02
03 public class Child extends Parent {
04 private int x = 100;
05
06 public Child() { }
07
08 public void out() {
09 System.out.println("부모의 protected num vlfem : " + num);
10 System.out.println("자식 클래스 메소드");
11 }
12
13 @Override
14 public void display() {
15 System.out.println("상속 받아 재정의한 메소드");
16 }
17 }
test.poly.TestPolymorphism.java
01 package test.poly;
02
03 import poly.sample.*;
04
05 public class TestPolymorphism {
06 public static void main(String[] args) {
07 Child c = new Child();
08 c.display();
09 c.out();
10 System.out.println();
11
12 Parent p = new Child();
13 p.display(); // Overriding 된 perent 멤버이므로 접근 가능
14 // p.out(); // 컴파일 에러. Parent 타입으로 Child 멤버에 접근 할 수 없음.
15 }
16 }
------
100
상속 받아 재정의한 메소드
자식 클래스 메소드
자식 클래스 메소드
Child c = new Child(); | Parent p = new Child(); |
---|---|
c.x -> 접근 가능 | p.x -> 자식 객체 멤버라 접근 불가. |
c.display(); -> 접근가능 | p.display(); -> 자식 객체 오버라이딩 된 메소드에 접근 가능 |
c.out(); -> 접근 가능 | p.out; -> 자식 객체 멤버라 접근 불가 |
메소드의 매개 변수에 다형성을 적용하면, 동일한 메소드의 오버로딩(overloading) 개수를 줄일 수 있게 된다.
메소드 매개 변수 오버로딩의 한 예를 살펴보자.
test.poly.TestPolyArgument.java
01 package test.poly;
02
03 public class TestPolyArgument {
04 public static void main(String[] args) {
05 Buyer b = new Buyer();
06 b.buy(new Chair());
07 b.buy(new Desk());
08 }
09 }
10
11 class Fumiture {
12 private int price; // 제품 가격
13 public Fumiture(int price) {
14 this.price = price;
15 }
16 }
17
18 class Chair extends Fumiture {
19 public Chair() {
20 super(100); // 부모 클래스의 생성자 호출
21 }
22 @Override
23 public String toString() {
24 return "Chair";
25 }
26 }
27
28 class Desk extends Fumiture {
29 public Desk() {
30 super(200);
31 }
32
33 @Override
34 public String toString() {
35 return "Desk";
36 }
37 }
38
39 class Buyer {
40 private int money = 500;
41
42 public void buy(Fumiture f) {
43 if(money < f.price) {
44 System.out.println("잔액부족!");
45 return;
46 }
47 money -= f.price;
48 System.out.println(f + "구매성공! 잔액 : " + money + " 만원");
49 }
50 }
------
Chair 구매성공! 잔액 : 400 원
Desk 구매성공! 잔액 : 200 원
예제의 클래스 Buyer의 buy 메소드에 주목해보자. 매개변수의 타입으로 Fumiture인 변수 f를 받고 있다. 부모 타입인 Fumiture 덕분에 자식 클래스인 Chair, Desk 모두 하나의 메소드를 사용 할 수 있다. 만약 Chair, Desk를 매개 변수로 받았다면, 아래와 같이 각각 사용 할 수 있는 메소드가 필요 했을 것이다.
01 class Buyer
02 int money = 500;
03 // Chair 구매메소드
04 void buy(Chair c) {
05 if(money < c.price) {
06 System.out.println("잔액부족!");
07 return;
08 }
09 money -= c.price;
10 System.out.println(c + " 구매성공! 잔액 : " + money + " 만원");
11 }
12 // Desk 구매 메소드
13 void buy(Desk d) {
14 if(money < d.price) {
15 System.out.println("잔액부족!");
16 return;
17 }
18 money -= d.price;
19 System.out.println(d + " 구매성공! 잔액 : " + money + " 만원");
20 }
21 }
매개변수의 타입으로 부모 클래스를 선언해서 자식 클래스를 모두 사용 할 수 있는 클래스에서 각 클래스마다 다르게 동작하는 기능이 필요하다면, 각 객체의 실제 타입을 알 수 있는 instanceof 연산자를 사용 할 수 있다.
주로 조건식 안에서 사용되며, boolean 값을 리턴하는 연산자 instanceof의 문법은 다음과 같다.if(참조변수 instanceof 검사할 타입명(클래스명) { }
instanceof의 연산 결과가 true이면, 검사한 타입으로 형변환이 가능하다는 의미이다.
test.instance.TestInstanceOf.java
01 package test.instance;
02
03 public class TestInstancOf {
04 public static void main(String[] args) {
05 Chair c = new Chair();
06
07 if(c instanceof Chair) {
08 System.out.println("Chair 객체입니다.");
09 }
10
11 if(c instanceof Fumiture) {
12 System.out.println("Fumiture 객체입니다.");
13 }
14
15 if(c instanceof Object) {
16 System.out.println("Object 객체입니다.");
17 }
18
19 System.out.println("실제 타입 : " + c.getClass().getName());
20 }
21 }
22
23 class Fumiture { }
24
25 class Chair extends Fumiture { }
------
Chair 객체입니다.
Fumiture 객체입니다.
Object 객체입니다.
실제 타입 : Chair
생성된 instance는 Chair 타입이지만, Object 타입과 Fumiture 타입의 instanceof 연산결과로 모두 true 값을 얻었다. Chair는 Fumiture 클래스를 상속하고 있지만, 모든 클래스는 또한 Object 클래스의 자식 클래스이기 때문이다. 또 검사한 타입(부모 클래스)으로 형변환 할 수 있다는 의미이다.
.getClass().getName());는 참조 변수가 가리키는 인스턴스의 클래스 이름을 문자열로 반환하는 Object 클래스의 메소드이다. 모든 클래스는 Object 클래스를 상속하고 있기 때문에 이 메소드를 사용 할 수 있다.
매개변수의 타입으로 부모 클래스를 받아서, instanceof 연산을 통해 어느 후손 클래스 타입인지를 검사 후, 실제 클래스 타입으로 형변환 할 수 있다. 이 때, 명시적으로 캐스팅 해 줘야 한다. 캐스팅 후에는 부모 클래스 타입으로 사용 할 수 없었던 자식 클래스 타입(객체의 실제 타입)별 메소드를 사용 할 수 있게 된다.01 class Buyer { 02 int money = 500; 03 void buy(Fumiture f) { 04 if(f instanceof Chair) { 05 Chair c = (Chair)f; 06 c.chairMethod(); // Chair 클래스에 선언된 메소드 07 } 08 else if(f instanceof Desk) { 09 Desk d = (Desk)f; 10 d.deskMethod(); // Desk 클래스에 선언된 메소드 11 } 12 } 13 }
추상 클래스는 미완성된 클래스를 뜻하며, 선언되어 있지만 구현되지 않은 미완성의 메소드(추상 메소드)를 포함하고 있는 클래스일 때 반드시 abstract 키워드를 class 앞에 붙여 준다.
클래스로서 객체 생성은 못하지만 새로운 클래스를 작성할 때 부모 클래스로 이동되며 새 클래스 작성을 위한 템플릿이 되어 상속 받은 클래스들의 규칙을 주는 용도로 사용 가능하다.public abstract class 클래스이름 { // 필드, 생성자, 메소드 작성 // 미완성된 추상 메소드 선언 }
클래스 앞에 'abstract' 키워드를 붙이면 추상 클래스가 생성되며, 객체를 생성할 수 없다는 점만 빼면 일반 클래스와 다른 점이 없다.
메소드는 접근제어자, 리턴타입, 이름, 매개변수로 이루어진 선언부와 메소드의 기능을 정의하는 구현부로 나뉜다. 추상 메소드는 구현부('{ }' Body)를 가지지 않는다.
접근제한자 abstract 리턴타입 함수이름 ();
추상 클래스에서 추상 메소드를 제공하는 이유는, 추상 클래스를 상속 받아 만들어지는 후손 클래스들이 메소드를 처음 선언과 동일하게 재정의해서 사용하라는 의미이다. 반드시 구현부를 작성해야 하므로 강제성도 가지고 있다.
inherit.sample.Person.java
01 package inherit.sample;
02
03 public abstract class Person {
04 private String name;
05
06 public Person() { }
07 public void setName(String name) { this.name = name; }
08 public String getName() { return name; }
09 public abstract void out();
10 }
Student.java
01 package inherit.sample
02
03 public class Student extends Person {
04 private String subject
05
06 public Student () { super(); }
07 public Student(String subject) {
08 super();
09 this.subject = subject;
10 }
11
12 public Student(String name, String subject) {
13 super(name);
14 this.subject = subject;
15 }
16
17 @Override
18 public void out() { // 상속받은 부모의 추상 메소드는 반드시 오버라이딩 해야 함
19 System.out.println(getName() + " 학생입니다");
20 System.out.println(subject + "를 수강합니다");
21 }
22 }
Professor.java
01 package inherit.sample;
02
03 public class Professor extends Person {
04 private String major;
05
06 public Professor() { super(); }
07
08 public Professor(String major) {
09 super();
10 this.major = major;
11 }
12
13 public Professor(String name, String major) {
14 super(name);
15 this.major = major;
16 }
17
18 @Override
19 public void out() {
20 System.out.println(getName() + " 교수입니다");
21 System.out.println(major + "를 전공합니다");
22 );
23 }
inherit.test.AbstractEx.java
01 package inherit.java
02
03 public class AbstractEx {
04 public static void main(String[] args) {
05 Student stu = new Student("홍길동", "자바");
06 Professor prof = new Professor("김춘추", "컴퓨터과학");
07
08 stu.out();
09 prof.out();
10 }
11 }
------
홍길동 학생입니다
자바를 수강합니다
김춘추 교수입니다
컴퓨터과학를 전공합니다
추상메소드로만 구성된 추상클래스를 인터페이스(interface)라고 구분하였으며, 추상 클래스보다 추상화 정도를 높여 일반 메소드와 멤버 변수를 멤버로 가질 수 없게 하였다.
추가로 상수(public static final 필드)만 멤버로 가진다.
인터페이스는 다중 상속에 제한이 없기 때문에 여러 추상화 메소드를 인터페이스로 작성함으로써 코드의 강제성을 부여할 수 있고, 여러 클래스에서 사용되는 동일한 상수일 경우 인터페이스에 정의한 후 해당 상수를 공유할 수 있게 하면 중복을 줄일 수 있다.
추상 메소드를 작성하려면 abstract 키워드를 표시해야 하지만 인터페이스 내부에는 모든 메소드가 추상 메소드이므로 public abstract 키워드를 생략할 수 있다. 하지만 혼동할 수 있으므로 모든 인터페이스의 추상 메소드에 public sbstract를 표시해 두기도 한다.
public interface 인터페이스이름 { // 상수 또는 추상 메소드 }
클래스의 상속은 단일 상속만 가능하지만, 인터페이스는 다중 상속이 가능하다.
public interface 인터페이스 extends 인터페이스1, 인터페이스2, ... { // 상수 또는 추상 메소드 }
추상 메소드에 대한 구현(implement)과 재정의(Overriding)는 인터페이스를 상속 받은 후손 클래스에서 구현(implement)해야 한다.
인터페이스가 가진 추상 메소드를 구현하려면 'implement'키워드를 사용해서 후손 클래스가 인터페이스를 상속을 받아야 한다. 상속 받은 후손 클래스는 반드시 인터페이스의 추상 메소드를 구현(implement : 메소드 바디 추가하고 코드 작성)해야 한다.
public class 클래스이름 implements 인터페이스 { // 일반 클래스 정의 // 인터페이스 추상 메소드 구현 }
인터페이스를 이용하면 정의해야 하는 메소드(프로그램기능)을 표준화하고 강제화 할 수 있다. 또한 메소드화 시켜야 하는 기능을 분류해야 하는 고민 없이 구현만 하면 되므로 개발 시간을 단축시킬 수 있다. 일반 클래스 상속을 이용해서 자식 클래스들의 관계를 맺는 것보다 간편하게 관계를 맺을 수 있다.