'김영한의 실전 자바 - 기본편' 강의를 들으면서 복습할만한 내용을 정리하였다.
다형성을 사용하기 위해 여기서는 상속 관계를 사용한다. Animal
(동물) 이라는 부모 클래스를 만들고 sound()
메서드를 정의한다. 이 메서드는 자식 클래스에서 오버라이딩 할 목적으로 만들었다.
Dog, Cat, Caw
는 Animal
클래스를 상속받았다. 그리고 각각 부모의 sound()
메서드를 오버라이딩 한다.
public class AnimalPolyMain1 {
public static void main(String[] args) {
Dog dog = new Dog();
Cat cat = new Cat();
Caw caw = new Caw();
soundAnimal(dog);
soundAnimal(cat);
soundAnimal(caw);
}
//동물이 추가 되어도 변하지 않는 코드
private static void soundAnimal(Animal animal) {
System.out.println("동물 소리 테스트 시작");
animal.sound();
System.out.println("동물 소리 테스트 종료");
}
}
soundAnimal(dog)
를 호출하면soundAniaml(Animal animal
메서드에 Dog
인스턴스가 전달된다.Animal animal = dog
로 이해하면 된다. 부모는 자식을 담을 수 있다. Animal
은 Dog
의 부모다.animal.sound()
메서드를 호출한다.animal
변수의 타입은 Animal
이므로 Dog
인스턴스에 있는 Animal
클래스 부분을 찾아서 sound()
메서드를 실행한다. 그런데 하위 클래스인 Dog
에서 sound()
메서드를 오버라이딩 했다. 따라서 오버라이딩한 메서드가 우선권을 가진다.
Dog
클래스에 있는 sound()
메서드가 호출된다.
이 코드이 핵심은 Animal animal
이다.
다형적 참조 덕분에 animal
변수는 자식인 Dog, Cat, Caw
의 인스턴스를 담을 수 있고, 참조할 수 있다.
메서드 오버라이딩 덕분에 animal.sound()
를 호출해도 Dog.sound(), Cat.sound(), Caw.sound()
와 같이 각 인스턴스의 메서드를 호출할 수 있다. 만약 자바에 메서드 오버라이딩이 없었다면 모두 Animal
의 sound()
가 호출되었을 것이다.
다형성 덕분에 이후에 새로운 동물을 추가해도 코드를 그대로 재사용할 수 있다.
새로운 기능이 추가되었을 때 변하는 부분을 최소화 하는 것이 잘 작성된 코드이다. 이렇게 하기 위해서는 코드에서 변하는 부분과 변하지 않는 부분을 명확하게 구분하는 것이 좋다.
Animal
클래스를 생성할 수 있는 문제Animal
클래스는 동물이라는 클래스이다. 이 클래스를 직접 생성해서 사용할 일이 있을까? 예) Animal animal = new Animal();
동물이라는 추상적인 개념이 실제로 존재하는 것은 이상하다. 사실 이 클래스는 다형성을 위해서 필요한 것이지 직접 인스턴스를 생성해서 사용할 일은 없다.
하지만 Animal
도 클래스이기 때문에 인스턴스를 생성하고 사용하는데 아무런 제약이 없다. 누군가 실수로 new Animal()
을 사용해서 Animal
의 인스턴스를 생성할 수 있다는 것이다. 이렇게 생성된 인스턴스는 작동은 하지만 제대로된 기능을 수행하지는 않는다.
Animal
클래스를 상속 받는 곳에서 sound()
메서드 오버라이딩을 하지 않을 가능성개발자가 실수로 sound()
메서드를 오버라이딩 하는 것을 빠트릴 수 있다. 이렇게 되면 부모의 기능을 상속 받는다. 따라서 코드상 아무런 문제가 발생하지 않는다. 물론 프로그램을 실행하면 기대와 다르게 Animal.sound()
가 호출될 것이다.
좋은 프로그램은 제약이 있는 프로그램이다. 추상 클래스와 추상 메서드르 사용하면 이런 문제를 한번에 해결할 수 있다.
동물(Animal
)과 같이 부모 클래스는 제공하지만, 실제 생성되면 안되는 클래스를 추상 클래스라 한다.
추상 클래스는 이름 그대로 추상적인 개념을 제공하는 클래스이다. 따라서 실제적인 인스턴스가 존재하지 앟는다. 대신에 상속을 목적으로 사용되고, 부모 클래스 역할을 담당한다.
abstract class AbstractAnimal {...}
추상 클래스는 클래스를 선언할 때 앞에 추상이라는 의미의 abstract
키워드를 붙여주면 된다.
추상 클래스는 기존 클래스와 완전히 같다. 다만 new AbstractAnimal()
와 같이 직접 인스턴스를 생성하지 못하는 제약이 추가된 것이다.
부모 클래스를 상속 받는 자식 클래스가 반드시 오버라이딩 해야 하는 메서드를 부모 클래스에 정의할 수 있다. 이것을 추상 메서드라 한다. 추상 메서드는 이름 그대로 추상적이 개념을 제공하는 메서드이다. 따라서 실체가 존재하지 않고, 메서드 바디가 없다.
public abstract void sound();
추상 메서드는 선언할 때 메서드 앞에 추상이라는 의미의 abstract
키워드를 붙여주면 된다.
추상 메서드가 하나라도 있는 클래스는 추상 클래스로 선언해야 한다.
그렇지 않으면 컴파일 오류가 발생한다.
추상 메서드는 메서드 바디가 없다. 따라서 작동하지 앟는 메서드를 가진 불완전한 클래스로 볼 수 있다. 따라서 직접 생성하지 못하도록 추상 클래스로 선언해야 한다.
추상 메서드는 상속 받는 자식 클래스가 반드시 오버라이딩 해서 사용해야 한다.
그렇지 않으면 컴파일 오류가 발생한다.
추상 메서드는 자식 클래스가 반드시 오버라이딩 해야 하기 때문에 메서드 바디 부분이 없다. 바디 부분을 만들면 컴파일 오류가 발생한다.
오버라아딩 하지 않으면 자식도 추상 클래스가 되어야 한다.
추상 메서드는 기존 메서드와 완전히 같다. 다만 메서드 바디가 없고, 자식 클래스가 해당 메서드를 반드리 오버라이딩 해야 한다는 제약이 추가된 것이다.
AbstractAnimal
public abstract class AbstractAnimal {
public abstract void sound();
public void move() {
System.out.println("동물이 움직입니다.");
}
}
AbstractAnimal
은 abstract
가 붙은 추상 클래스이다. 이 클래스는 직접 인스턴스를 생성할 수 없다.
sound()
는 abstract
가 붙은 추상 메서드이다. 이 메서드는 자식이 반드시 오버라이딩 해야 한다.
이 클래스는 move()
라는 메서드를 가지고 있는데, 이 메서드는 추상 메서드가 아니다. 상속을 목적으로 만들어진 메서드이다. 따라서 자식 클래스가 오버라이딩 하지 않아도 된다.
Dog
public class Dog extends AbstractAnimal {
@Override
public void sound() {
System.out.println("멍멍");
}
}
지금까지 설명한 제약 (추상 클래스는 직접 인스턴스 생성 불가, 추상 메서드는 자식이 오버라이딩 해야함)을 제외하고 나머지는 모두 일반적이 클래스와 동일하다. 추상 클래스는 제약이 추가된 클래스일뿐이다. 메모리 구조, 실행 결과 모두 동일하다.
Animal
인스턴스를 생성할 문제를 근본적으로 방지해준다.sound()
를 오버라이딩 하지 않을 문제를 근본적으로 방지해준다.앞서 만든 예제에서 move()
도 추상 메서드로 만들어야 한다고 가정해보자.
이 경우 AbstractAnimal
클래스의 모든 메서드가 추상 메서드가 된다. 이런 클래스를 순수 추상 클래스라 한다.
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 abstratct
를 생략할 수 있다. 참고로 생략이 권장된다.
인터페이스는 다중 구현(다중 상속)을 지원한다.
public interface InterfaceAnimal {
public static final double MY_PI = 3.14;
}
인터페이스에서 멤버 변수는 public static final
이 모두 포함되었다고 간주된다. (생략이 권장된다.)
public interface InterfaceAnimal {
void sound();
void move();
}
public class Dog implements InterfaceAnimal {
@Override
public void sound() {
System.out.println("멍멍");
}
@Override
public void move() {
System.out.println("개 이동");
}
}
인터페이스르 상속 받을 때는 extends
대신에 implements
라는 구현이라는 키워드를 사용해야 한다. 인터페이스는 그래서 상속이라 하지 않고 구현이라 한다.
클래스, 추상 클래스, 인터페이스는 프로그램 코드, 메모리 구조상 모두 똑같다. 모두 자바에서는 .class
로 다루어진다. 인터페이스를 작성할 때도 .java
에 인터페이스를 정의한다.
인터페이스는 순수 추상 클래스와 비슷하다고 생각하면 된다.
부모 클래스의 기능을 자식 클래스가 상속 받을 때, 클래스는 상속 받는다고 표현하지만, 부모 인터페이스의 기능을 자식이 상속 받을 때는 인터페이스를 구현한다고 표현한다.
상속은 이름 그대로 부모의 기능을 물려 받는 것이 목적이다. 하지만 인터페이스는 모든 메서드가 추상 메서드이다. 따라서 물려받을 수 있는 기능이 없고, 오히려 인터페이스에 정의한 모든 메서드를 자식이 오버라이딩 해서 기능을 구현해야 한다. 따라서 구현한다고 표현한다.
인터페이스는 메서드 이름만 있는 설계도이고, 이 설계도가 실제 어떻게 작동하는지는 하위 클래스에서 모두 구현해야 한다. 따라서 인터페이스의 경우 상속이 아니라 해당 인터페이스를 구현한다고 표현한다.
상속과 구현은 사림이 표현하는 단어만 다를 뿐이지 자바 입장에서는 일반 상속 구조와 동일하게 작동한다.
모든 메서드가 추상 메서드인 경우 순수 추상 클래스를 만들어도 되고, 인터페이스를 만들어도 된다. 그런데 왜 인터페이스르 사용해야 할까?
제약 : 인터페이스를 만드는 이유는 인터페이스를 구현하는 곳에서 인터페이스의 메서드를 반드시 구현해라는 규약(제약)을 주는 것이다. 인터페이스의 규약(제약)은 반드시 구현해야 하는 것이다. 그런데 순수 추상 클래스의 경우 미래에 누군가 그곳에 실행 가능한 메서드를 끼워 넣을 수 있다. 이렇게 되면 추가된 기능을 자식 클래스에서 구현하지 않을 수도 있고, 또 더는 순수 추상 클래스가 아니게 된다. 인터페이스는 모든 메서드가 추상 메서드이다. 따라서 이런 문제를 원천 차단할 수 있다.
다중 구현 : 자바에서 클래스 상속은 부모를 하나만 지정할 수 있다. 반면에 인터페이스는 부모를 여려명 두는 다중 구현(다중 상속)이 가능하다.
좋은 프로그램은 좋은 제약이 있는 프로그램이다.
자바가 다중 상속을 지원하지 않는 이유
위의 그림처럼 다중 AirplaneCar
가 다중 상속을 사용하고 있다고 해보자. 그러면 AirplanCar
입장에서 move()
를 호출할 때 어떤 부모의 move()
를 사용해야 할지 애매한 문제가 발생한다. 이것을 다이아몬드 문제라 한다. 그리고 다중 상속을 사용하면 클래스 계층 구조가 매우 복잡해질 수 있다.
InterfaceA, InterfaceB
는 둘다 같은 methodCommon()
을 가지고 있다. 그리고 Child
는 두 인터페이스를 구현했다. 상속 관계의 경우 두 부모 중에 어떤 한 부모의 methodCommon()
을 사용해야 할지 결정해야 하는 다이아몬드 문제가 발생한다.
하지만 인터페이스 자신은 구현을 가지지 않는다. 대신에 인터페이스를 구현하는 곳에서 해당 기능을 모두 구현해야 한다. 여기서 InterfaceA, InterfaceB
는 같은 이름의 methodCommon()
을 제공하지만 이것의 기능은 Child
가 구현한다. 그리고 오버라이딩에 의해 어차피 Child
에 있는 methodCommon()
이 호출된다. 결과적으로 두 부모 중에 어떤 한 부모의 methodCommon()
을 선택하는 것이 아니라 그냥 인터페이스들을 구현한 Child
에 있는 methodCommon()
이 사용된다. 이런 이유로 인터페이스는 다이아몬드 문제가 발생하지 않는다. 따라서 인터페이스의 경우 다중 구현을 허용한다.
InterfaceA
타입으로 참조할 경우InterfaceB
타입으로 참조할 경우