11-1. 다형성 활용1
다형성을 왜 사용하는지 장점을 알아보기 위해
다형성을 사용하지 않는 프로그램 개발과
이후, 다형성을 사용한 코드 변경
(동물 소리 문제 - 다형성 사용 안함)
poly.ex1.Dog.java
package poly.ex1;
public class Dog {
public void sound() { System.out.println("멍멍"); }
}
poly.ex1.Cat.java
package poly.ex1;
public class Cat {
public void sound() { System.out.println("액옹"); }
}
poly.ex1.Cow.java
package poly.ex1;
public class Cow {
public void sound() { System.out.println("음머"); }
}
poly.ex1.AnimalSoundMain.java
package poly.ex1;
public class AnimalSoundMain {
public static void main(String[] args) {
Dog dog = new Dog();
Cat cat = new Cat(); // ctrl+alt+v 단축키(Introduce Variable)
Cow cow = new Cow();
System.out.println("동물 소리 테스트 시작");
dog.sound();
System.out.println("동물 소리 테스트 종료");
System.out.println("동물 소리 테스트 시작");
cat.sound();
System.out.println("동물 소리 테스트 종료");
System.out.println("동물 소리 테스트 시작");
cow.sound();
System.out.println("동물 소리 테스트 종료");
}
}
- 새로운 동물이 추가된다고 가정했을 때, 출력 코드가 중복된다.
- 중복 제거를 위해서는 메서드 사용, 배열&for문을 사용하면 된다.
- 하지만 Dog, Cat, Cow는 서로 완전히 다른 클래스
코드 중복 제거 시도
메서드로 중복 제거 시도
...
// 메서드로 중복 제거 시도 => 메서드가 많아져 실패
soundCat(cat);
soundCow(cow);
}
private static void soundCat(Cat cat) {
System.out.println("동물 소리 테스트 시작");
cat.sound();
System.out.println("동물 소리 테스트 종료");
}
private static void soundCow(Cow cow) {
System.out.println("동물 소리 테스트 시작");
cow.sound();
System.out.println("동물 소리 테스트 종료");
}
- 매개변수의 클래스를 Cow, Dog, Cat 중 하나로 정해야 한다.
- 매개변수를 Cow로 했을 때, 해당 메서드는 Cow의 전용 메서드가 된다.
- Dog, Cat, Cow의 타입(클래스)이 달라 soundCow메서드를 함께 사용하는 것이 불가능 하다.
배열과 for문으로 중복 제거 시도
...
// 배열과 for문으로 중복 제거 시도 => 서로 다른 타입의 클래스를 배열에 담는 것은 불가능
Cow[] cowArr = {cat, dog, cow}; // 컴파일 오류
...
- 배열 타입을 Dog, Cat, Cow 중 하나로 지정해야 함
- 타입이 서로 다른 Dog, Cat, Cow를 하나의 배열에 담을 수 없음
Dog, Cat, Cow가 모두 같은 타입을 사용할 수 있다면 코드 중복 제거 가능
11-2. 다형성 활용2
다형성을 사용하기 위해 상속 관계 사용
Animal 이라는 부모 클래스를 만들고 메서드를 자식 클래스에서 오버라이딩하게 함
(동물 소리 문제 - 다형성 사용해 변경)
poly.ex2.Animal.java (부모 클래스)
package poly.ex2;
public class Animal {
public void sound() {
System.out.println("동물 울음 소리");
}
}
poly.ex2.Dog.java
package poly.ex2;
public class Dog extends Animal{
@Override
public void sound() {
System.out.println("멍멍");
}
}
poly.ex2.Cat.java
package poly.ex2;
public class Cat extends Animal{
@Override
public void sound() {
System.out.println("에옭");
}
}
poly.ex2.Cow.java
package poly.ex2;
public class Cow extends Animal{
@Override
public void sound() {
System.out.println("음머");
}
}
poly.ex2.AnimalPolyMain1.java
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);
}
// 매개변수 'Animal animal' 이 코드의 핵심
private static void soundAnimal(Animal animal) {
System.out.println("동물 소리 테스트 시작");
animal.sound();
System.out.println("동물 소리 테스트 종료");
}
}
매개변수 Animal animal
private static void soundAnimal(Animal animal)
- 다형적 참조 덕분에 animal 변수는 자식인 Dog, Cat, Cow의 인스턴스를 참조할 수 있다.
- 메서드 오버라이딩 덕분에 animal.sound()를 호출해도,
Dog.sound(), Cat.sound() 같이 각 인스턴스의 메서드 호출 가능 - 다형성 덕분에 이후, 새로운 동물을 추가해도 soundAnimal() 메서드 코드를 그대로 재사용할 수 있다.
package poly.ex2;
public class Duck extends Animal{
@Override
public void sound() { System.out.println("꽉꽉"); }
}
11-3. 다형성 활용3
배뎔과 for문을 사용해 중복 제거
poly.ex2.AnimalPolyMain2.java
package poly.ex2;
public class AnimalPolyMain2 {
public static void main(String[] args) {
Dog dog = new Dog();
Cat cat = new Cat();
Cow cow = new Cow();
Duck duck = new Duck();
Animal[] animalArr = {dog, cat, cow, duck};
// 변하지 않는 부분
for (Animal animal : animalArr) {
System.out.println("동물 소리 테스트 시작");
animal.sound();
System.out.println("동물 소리 테스트 종료");
}
}
}
- 배열은 같은 타입의 데이터를 나열할 수 있다.
poly.ex2.AnimalPolyMain3.java (조금 더 개선)
package poly.ex2;
public class AnimalPolyMain3 {
public static void main(String[] args) {
// ctrl+alt+n 단축키(Inline Variable)
Animal[] animalArr = {new Dog(), new Cat(), new Cow()};
for (Animal animal : animalArr) {
soundAnimal(animal);
}
}
// 코드를 메서드로 만드는 단축키 => ctrl+alt+m
// 변하지 않는 부분
private static void soundAnimal(Animal animal) {
System.out.println("동물 소리 테스트 시작");
animal.sound();
System.out.println("동물 소리 테스트 종료");
}
}
- 새로운 동물이 추가되었을 때, 코드가 변하는 부분과 변하지 않는 부분이 구분된다.
- 새로운 기능이 추가되었을 때, 변하는 부분을 최소화하는 것이 잘 작성된 코드이다.
남은 2가지 문제
1. Animal 클래스를 생성할 수 있는 문제
- Animal animal = new Animal();
- 위와 같이 동물이라는 추상적인 개념이 실제로 존재하는 것은 이상하다.
- 다형성을 위해 필요하지, 직접 인스턴스를 생성해서 사용할 일은 없다.
- 하지만 Animal도 클래스이기 때문에 인스턴스를 생성하고 사용하는데 아무런 제약이 없다.
- 누군가 new Animal()로 Animal 인스턴스를 생성할 수 있다.
2. Animal 클래스를 상속 받는 곳에서 sound() 메서드 오버라이딩을 하지 않을 가능성
- Animal을 상속 받은 Pig 클래스를 만든다고 했을 때,
- 개발자가 실수로 sound() 메서드를 오버라이딩하지 않았다.
- 코드상 아무 문제도 없지만,
프로그램을 실행하면 "꾸에엑"이 아닌 부모 클래스의 Animal.sound()가 호출될 것이다.
이를 해결하려면, 추상 클래스와 추상 메서드를 사용하면 된다.
11-4. 추상 클래스1
추상 클래스
- 동물(Animal)과 같이 부모 클래스는 제공하지만, 실제 생성(인스턴스)되면 안되는 클래스
- 추상적인 개념을 제공하는 클래스로, 실체인 인스턴스가 존재하지 않음
- 상속을 목적으로 사용되고 부모 클래스 역할을 담당
- public abstractclass AbstractAnimal {...}
- 추상 클래스 선언 시 앞에 추상이라는 의미의 abstract 키워드를 입력
- 기존 클래스와 같지만, 직접 인스턴스를 생성하지 못하는 제약이 추가됨
new AbstractAnimal() => X
추상 메서드
- 부모 클래스를 상속 받는 자식 클래스가 반드시 오버라이딩 해야하는 메서드
- 추상적인 개념을 제공하는 메서드로, 실체가 존재하지 않고 바디가 없음
- public abstract void sound();
- 추상 메서드 선언시 메서드 앞에 abstract 키워드 입력
- 추상 메서드가 하나라도 있는 클래스는 추상 클래스로 선언해야 함
- 그렇지 않으면 컴파일 오류 발생
- 추상 메서드는 메서드 바디가 없음
- 작동하지 않는 메서드를 가진 불완전한 클래스로 볼 수 있다.
- 직접 생성하지 못하도록 추상 클래스로 선언
- 추상 메서드는 상속 받는 자식 클래스가 반드시 오버라이딩해서 사용
- 그렇지 않으면 컴파일 오류 발생
- 추상 메서드는 자식클래스가 반드시 오버라이딩 해야 해서 메서드 바디 부분이 없음
- 메서드 바디가 있으면 컴파일 오류 발생
- 오버라이딩 하지 않으면 자식 클래스도 추상 클래스여야 함.
- 추상 메서드는 기존 메서드와 같지만,
메서드 바디가 없고,
자식 클래스가 해당 메서드를 반드시 오버라이딩 해야 하는 제약이 추가됨
(동물 소리 문제 - 추상 클래스/메서드 사용해 변경)
poly.ex3.AbstractAnimal.java (추상)
package poly.ex3;
public abstract class AbstractAnimal {
public abstract void sound();
public void move() {
System.out.println("동물이 움직입니다.");
}
}
- AbstractAnimal은 추상 클래스로 직접 인스턴스를 생성할 수 없다.
- sound()는 추상 메서드로 자식이 반드시 오버라이딩 해야 한다.
- move()는 추상 메서드가 아니므로 자식 클래스가 오버라이딩 하지 않아도 된다.
poly.ex3.Dog.java
package poly.ex3;
public class Dog extends AbstractAnimal {
@Override
public void sound() { System.out.println("아르르르"); }
}
poly.ex3.Cat.java
package poly.ex3;
public class Cat extends AbstractAnimal {
@Override
public void sound() { System.out.println("미야옹"); }
}
poly.ex3.Cow.java
package poly.ex3;
public class Cow extends AbstractAnimal {
@Override
public void sound() { System.out.println("음머어"); }
}
poly.ex3.AbstractMain.java
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("동물 소리 테스트 종료");
}
}
추상 클래스로 실수로 Animal 인스턴스를 생성할 문제를 근본적으로 방지
추상 메서드로 자식 클래스가 sound() 메서드를 오버라이딩 하지 않을 문제를 근본적으로 방지
11-5. 추상 클래스2
순수 추상 클래스: 모든 메서드가 추상 메서드인 추상 클래스
(동물 소리 문제 - 순수 추상 클래스 사용해 변경)
poly.ex4.AbstractAnimal.java (순수 추상)
package poly.ex4;
public abstract class AbstractAnimal {
public abstract void sound();
public abstract void move();
}
poly.ex4.Dog.java
package poly.ex4;
public class Dog extends AbstractAnimal {
@Override
public void sound() { System.out.println("아르르르"); }
@Override
public void move() { System.out.println("개 이동"); }
}
poly.ex4.Cat.java
package poly.ex4;
public class Cat extends AbstractAnimal {
@Override
public void sound() { System.out.println("미야옹"); }
@Override
public void move() { System.out.println("고양이 이동"); }
}
poly.ex4.Cow.java
package poly.ex4;
public class Cow extends AbstractAnimal {
@Override
public void sound() { System.out.println("음머어"); }
@Override
public void move() { System.out.println("소 이동"); }
}
poly.ex4.AbstractMain.java
package poly.ex4;
public class AbstractMain {
public static void main(String[] args) {
Dog dog = new Dog();
Cat cat = new Cat();
Cow cow = new Cow();
soundAnimal(cat);
soundAnimal(dog);
soundAnimal(cow);
moveAnimal(cat);
moveAnimal(dog);
moveAnimal(cow);
}
// 변하지 않는 코드
private static void soundAnimal(AbstractAnimal animal) {
System.out.println("동물 소리 테스트 시작");
animal.sound();
System.out.println("동물 소리 테스트 종료");
}
// 변하지 않는 코드
private static void moveAnimal(AbstractAnimal animal) {
System.out.println("동물 이동 테스트 시작");
animal.move();
System.out.println("동물 이동 테스트 종료");
}
}
순수 추상 클래스
- 모든 메서드가 추상 메서드여서 코드를 실행할 바디 부분이 전혀 없다.
- 실행 로직이 없고, 다형성을 위한 부모 타입으로 껍데기 역할만 제공
- 특징
- 인스턴스 생성 불가
- 상속 시 자식은 모든 메서드를 오버라이딩 해야 한다.
- 주로 다형성을 위해 사용
- 순수 추상 클래스는 마치 어떤 규격을 지켜서 구현해야 하는 것처럼 느껴진다.
=> 일반적으로 이야기하는 인터페이스와 같이 느껴진다.- ex) USB 인터페이스는 분명한 규격이 있고 이 규격에 맞추어 제품을 개발해야 연결이 된다.
순수 추상 클래스가 USB 인터페이스 규격이라고 한다면
USB 인터페이스에 맞추어 마우스, 키보드 같은 연결 장치들을 구현할 수 있다.
- ex) USB 인터페이스는 분명한 규격이 있고 이 규격에 맞추어 제품을 개발해야 연결이 된다.
- 자바는 순수 추상 클래스를 더 편리하게 사용할 수 있도록 인터페이스라는 개념을 제공
순수 추상 클래스 ≒ 인터페이스
11-6. 인터페이스
순수 추상 클래스
public abstract class AbstractAnimal {
public abstract void sound();
public abstract void move();
}
인터페이스
public interface InterfaceAnimal {
public abstract void sound();
public abstract void move();
}
인터페이스 - 메서드 public abstract 키워드 생략 가능
public interface InterfaceAnimal {
void sound();
void move();
}
인터페이스 특징
순수 추상 클래스의 특징 + 약간의 편의 기능
- 인스턴스 생성 불가
- 상속 시 모든 메서드를 오버라이딩
- 주로 다형성을 위해 사용
- 인터페이스의 메서드는 모두 public, abstract
- public abstract 생략 가능, 생략 권장
- 인터페이스는 다중 구현(다중 상속)을 지원
인터페이스와 멤버 변수
public interface InterfaceAnimal {
public static final double MY_PI = 3.14;
}
- 인터페이스의 멤버 변수는 public, static, final이 모두 포함되었다고 간주
- final은 변수의 값을 한번 설정하면 수정할 수 없다는 뜻
- 자바에서 static final을 사용한 정적이면서 고칠 수 없는 변수를 상수라고 함
- 관례상 상수는 대문자에 언더스코어(_)로 구분
public interface InterfaceAnimal {
double MY_PI = 3.14;
}
- public static final 생략이 권장
클래스의 상속 관계는 UML에서 실선 사용
인터페이스 구현(상속)관계는 UML에서 점선 사용
=> 상속 받은 메서드를 다 구현해야하기 때문에 '구현'이라고 말함
(동물 소리 문제 - 인터페이스 사용해 변경)
poly.ex5.InterfaceAnimal.java (인터페이스)
package poly.ex5;
public interface InterfaceAnimal {
void sound(); // public abstract 생략 가능
void move(); // public abstract 생략 가능
}
poly.ex5.Dog.java
package poly.ex5;
public class Dog implements InterfaceAnimal {
@Override
public void sound() { System.out.println("멈머"); }
@Override
public void move() { System.out.println("산책"); }
}
poly.ex5.Cat.java
package poly.ex5;
public class Cat implements InterfaceAnimal {
@Override
public void sound() { System.out.println("냐옹"); }
@Override
public void move() { System.out.println("우다다다"); }
}
poly.ex5.Cow.java
package poly.ex5;
public class Cow implements InterfaceAnimal {
@Override
public void sound() { System.out.println("음메"); }
@Override
public void move() { System.out.println("우적우적"); }
}
poly.ex5.InterfaceMain.java
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("동물 소리 테스트 종료");
}
}
클래스, 추상 클래스, 인터페이스는 모두 똑같다.
- 프로그램 코드, 메모리 구조상 모두 똑같다.
- 모두 자바에서는 .class 루어진다.
- 인터페이스를 작성할 때도 .java 에 인터페이스를 정의
- 인터페이스는 순수 추상 클래스와 비슷
상속 vs 구현 extends vs implements
- 부모 클래스의 기능을 자식 클래스가 상속 받을 때 => 클래스는 상속 받는다고 표현
- 부모 인터페이스의 기능을 자식 클래스가 상속 받을 때 => 인터페이스를 구현한다고 표현
- 상속은 이름 그대로 부모의 기능을 물려 받는 것이 목적
- 인터페이스는 모든 메서드가 추상 메서드로 물려받을 수 있는 기능이 없다.
- 오히려 인터페이스에 정의한 모든 메서드를
자식이 오버라이딩해서 기능을 구현해야 하기에 구현한다고 표현 - 인터페이스는 메서드 이름만 있는 설계도이고,
이 설계도가 실제 어떻게 작동하는지는 하위 클래스에서 모두 구현 - 따라서 인터페이스의 경우 상속이 아니라 해당 인터페이스를 구현한다고 표현
- 상속과 구현은 사람이 표현하는 단어만 다를 뿐이지 자바 입장에서는 똑같다. 일반 상속 구조와 동일하게 작동
인터페이스를 사용해야 하는 이유
- 제약
- 인터페이스를 구현하는 곳에서 인터페이스의 메서드를 반드시 구현하라는 규약(제약)을 주는 것이다.
ex) USB 인터페이스 규약 - 순수 추상 클래스의 경우 미래에 누군가 그곳에 실행 가능한 메서드를 끼워 넣을 수 있다.
이렇게 되면 추가된 기능을 자식 클래스에서 구현하지 않을 수도 있고, 또 더는 순수 추상 클래스가 아니게 된다. - 인터페이스는 모든 메서드가 추상 메서드이기에 이런 문제를 원천 차단할 수 있다.
- 인터페이스를 구현하는 곳에서 인터페이스의 메서드를 반드시 구현하라는 규약(제약)을 주는 것이다.
- 다중 구현
- 자바에서 클래스 상속은 부모를 하나만 지정할 수 있다.
- 인터페이스는 부모를 여러명 두는 다중 구현(다중 상속)이 가능하다
자바8에 등장한 default 메서드를 사용하면 인터페이스도 메서드를 구현할 수 있다.
하지만 이것은 예외적으로 아주 특별한 경우에만 사용해야 한다.
자바9에서 등장한 인터페이스의 private 메서드도 마찬가지이다.
지금 학습 단계에서는 이 부분들을 고려하지 않는 것이 좋다. 이 부분은 뒤에서 따로 다룬다.
11-7. 인터페이스 - 다중구현
자바가 다중 상속을 지원하지 않는 이유
- 비행기와 자동차를 상속 받아 하늘을 나는 자동차를 만든다고 했을 때,
- AirplaneCar 입장에서 move()를 호출할 때, 어떤 부모의 기능을 사용해야 할지 결정해야 하는 문제가 발생
=> 다이아몬드 문제 - 또한, 다중 상속을 사용하면 클래스 계층 구조가 매우 복잡해짐
- 대신에 인터페이스의 다중 구현을 허용해 이런 문제를 피함
- 인터페이스가 모두 추상 메서드로 이루어져 있기 때문에 다중 구현을 허용함.
인터페이스 다중 구현을 허용한 이유
- InterfaceA, InterfaceB는 둘다 같은 methodCommon()를 가지고 있고, Child는 두 인터페이스를 구현
- 인터페이스는 자신이 구현을 가지지 않고 인터페이스를 구현하는 곳에서 해당 기능을 모두 구현해야 함
- InterfaceA , InterfaceB는 같은 이름의 methodCommon()를 제공하지만, 이것의 기능은 Child가 구현함
- 오버라이딩에 의해 어차피 Child에 있는 methodCommon()가 호출됨
- 결과적으로 한 부모의 methodCommon()를 선택하는 것이 아닌, 인터페이스들을 구현한 Child의 methodCommon()를 사용
- 그래서 인터페이스는 다이아몬드 문제가 발생하지 않음
poly.diamond.InterfaceA.java
package poly.diamond;
public interface InterfaceA {
void methodA();
void methodCommon();
}
poly.diamond.InterfaceB.java
package poly.diamond;
public interface InterfaceB {
void methodB();
void methodCommon();
}
poly.diamond.Child.java
package poly.diamond;
public class Child implements InterfaceA, InterfaceB {
@Override
public void methodA() {
System.out.println("Child.methodA");
}
@Override
public void methodB() {
System.out.println("Child.methodB");
}
@Override
public void methodCommon() {
System.out.println("Child.methodCommon");
}
}
- methodCommon()의 경우 양쪽 인터페이스에 다 있지만 같은 메서드이기에 구현은 하나만 하면 됨
poly.diamond.DiamondMain.java
package poly.diamond;
public class DiamondMain {
public static void main(String[] args) {
InterfaceA a = new Child();
a.methodA();
a.methodCommon();
InterfaceB b = new Child();
b.methodB();
b.methodCommon();
}
}
11-8. 클래스와 인터페이스 활용
클래스 상속과 인터페이스 구현을 함께 사용
- AbstractAnimal은 추상 클래스
sound() 는 추상 메서드
move() 는 추상 메서드가 아닌 상속을 목적으로 사용 - Fly는 인터페이스
Bird, Chicken은 날 수 있는 동물이기에 fly() 를 구현해야 함
poly.ex6.AbstractAnimal.java
package poly.ex6;
public abstract class AbstractAnimal {
public abstract void sound();
public void move() {
System.out.println("동물이 이동합니다.");
}
}
poly.ex6.Fly.java
package poly.ex6;
public interface Fly {
void fly();
}
poly.ex6.Dog.java
package poly.ex6;
public class Dog extends AbstractAnimal {
@Override
public void sound() { System.out.println("멍멍"); }
}
poly.ex6.Bird.java
package poly.ex6;
public class Bird extends AbstractAnimal implements Fly {
@Override
public void sound() { System.out.println("짹짹"); }
@Override
public void fly() { System.out.println("새 비행"); }
}
poly.ex6.Chicken.java
package poly.ex6;
public class Chicken extends AbstractAnimal implements Fly {
@Override
public void sound() { System.out.println("꼭끼오"); }
@Override
public void fly() { System.out.println("푸드득"); }
}
- extends 를 통한 상속은 하나만 할 수 있고
implements를 통한 인터페이스는 다중 구현 할 수 있기 때문에
둘이 함께 나온 경우 extends가 먼저 나와야 한다.
poly.ex6.SoundFlyMain.java
package poly.ex6;
public class SoundFlyMain {
public static void main(String[] args) {
Dog dog = new Dog();
Bird bird = new Bird();
Chicken chicken = new Chicken();
soundAnimal(dog);
soundAnimal(bird);
soundAnimal(chicken);
flyAnimal(bird);
flyAnimal(chicken);
}
// AbstractAnimal 사용 가능
private static void soundAnimal(AbstractAnimal animal) {
System.out.println("동물 소리 테스트 시작");
animal.sound();
System.out.println("동물 소리 테스트 종료");
}
// Fly 인터페이스가 있으면 사용 가능
private static void flyAnimal(Fly fly) {
System.out.println("날기 테스트 시작");
fly.fly();
System.out.println("날기 테스트 종료");
}
}