본 글은 김영한 님의 『김영한의 실전 자바 - 기본편』 강의를 학습하며 정리한 내용입니다.
강의 자료에 포함된 일부 코드와 이미지를 참고하여 발췌·활용하였습니다.
자바 기본기를 제대로 다지고 싶으시다면, 아래 링크에서 강의를 확인해 보세요
본 게시물은 파트너스 활동의 일환으로 작성되었으며, 구매 시 소정의 수수료를 받을 수 있습니다.
다형성(Polymorphism)
다형성은 이름 그대로 “다양한 형태”, “여러 형태” 를 뜻한다.
프로그래밍에서 다형성은 한 객체가 여러 타입의 객체로 취급될 수 있는 능력을 뜻한다.
보통 하나의 객체는 하나의 타입으로 고정되어 있다.
그런데 다형성을 사용하면 하나의 객체가 다른 타입으로 사용될 수 있다는 뜻이다.
다형적 참조
다형적 참조란 “부모는 자식을 품을 수 있다.” 이다.
package poly.basic;
public class Parent {
public void parentMethod() {
System.out.println("Parent.parentMethod");
}
}
package poly.basic;
public class Child extends Parent {
public void childMethod() {
System.out.println("Child.childMethod");
}
}
public class PolyMain {
public static void main(String[] args) {
//부모 변수가 부모 인스턴스 참조
System.out.println("Parent -> Parent");
Parent parent = new Parent();
parent.parentMethod();
//자식 변수가 자식 인스턴스 참조
System.out.println("Child -> Child");
Child child = new Child();
child.parentMethod();
child.childMethod();
//부모 변수가 자식 인스턴스 참조(다형적 참조)
System.out.println("Parent -> Child");
Parent poly = new Child();
poly.parentMethod();
//Child child1 = new Parent(); 자식은 부모를 담을 수 없다.
//자식의 기능은 호출할 수 없다. 컴파일 오류 발생
//poly.childMethod();
}
}
# 실행 결과
Parent -> Parent
Parent.parentMethod
Child -> Child
Parent.parentMethod
Child.childMethodParent -> Child
Parent.parentMethod
코드를 분석해보자.
부모 타입의 변수가 부모 인스턴스 참조
Parent -> Parent : parent.parentMethod()
- 부모 타입의 변수가 부모 인스턴스를 참조한다.
Parent parent = new Parent()
Parent인스턴스를 만들었다.- 이 경우 부모 타입인
Parent를 생성했기 때문에 메모리 상에Parent만 생성된다.
- 이 경우 부모 타입인
- 생성된 참조값을
Parent타입의 변수인parent에 담아둔다. parent.parentMethod()를 호출하면 인스턴스의Parent클래스에 있는parentMethod()가 호출된다.
자식 타입의 변수가 자식 인스턴스 참조
Child Child: child.childMethod()
- 자식 타입의 변수가 자식 인스턴스를 참조한다.
Child child = new Child()
Child인스턴스를 만들었다.- 이 경우 자식 타입인
Child를 생성했기 때문에 메모리 상에Child 와 Parent가 모두 생성된다.
- 이 경우 자식 타입인
- 생성된 참조값을
Child타입의 변수인child에 담아둔다. child.childMethod()를 호출하면 인스턴스의Child클래스에 있는childMethod()가 호출된다.
다형적 참조 : 부모 타입의 변수가 자식 인스턴스 참조
Parent -> Child : poly.parentMethod()
- 부모 타입의 변수가 자식 인스턴스를 참조한다.
Parent poly = new Child()
Child인스턴스를 만들었다.- 이 경우 자식 타입인
Child를 생성했기 때문에 메모리 상에Child,Parent가 모두 생성된다.
- 이 경우 자식 타입인
- 생성된 참조값을
Parent타입의 변수인poly에 담아둔다.
부모는 자식 타입을 담을 수 있다.
Parent poly는 부모 타입이다.new Child()를 통해 생성된 결과는Child타입이다.- 자바에서 부모 타입은 자식 타입을 담을 수 있다.
- 반대로 자식 타입은 부모 타입을 담을 수 없다.
Child child1 = new Parent()
poly.parentMethod()- 참조값을 사용해서 인스턴스를 찾는다.
poly는Parent타입이기 때문에Parent클래스부터 시작해서 필요한 기능을 찾는다.- 인스턴스의
Parent클래스에parentMethod()가 있어 호출된다.
즉, 자바에서 부모 타입은 자신은 물론이고, 자신을 기준으로 모든 자식 타입을 참조할 수 있다.
이것이 바로 다양한 형태를 참조할 수 있다고 해서 다형적 참조라 한다.
다형적 참조의 한계
Parent poly = new Child() 이렇게 자식을 참조한 상황에서 poly 가 자식 타입인 Child 에 있는childMethod() 를 호출하면 어떻게 될까?
poly.childMethod()를 실행하면 먼저 참조값을 통해 인스턴스를 찾는다.poly는Parent타입이기 때문에,Parent클래스부터 시작해서 필요한 기능을 찾느다.- 상속관계는 부모 방향으로 찾아 올라갈 수만 있기 때문에 컴파일 오류가 발생한다.
다형성과 캐스팅
위에서 봤듯이, Parent poly = new Child() 와 같이 부모 타입의 변수를 사용하게 되면 poly.childMethod() 와 같이 자식 타입에 있는 기능은 호출할 수 없다.
상속 관계는 부모로만 찾아서 올라갈 수 있기 때문에, 자식 타입에 있는 메서드는 호출 할 수 없어 컴파일 오류가 발생한다.
이때는 다운캐스팅이라는 기능을 사용해서 부모 타입을 잠깐 자식 타입으로 변경하면 된다.
Child child = (Child) poly //Parent poly
(타입) 처럼 괄호와 그 사이에 타입을 지정하면 참조 대상을 특정 타입으로 변경할 수 있다.
이렇게 특정 타입으로 변경하는 것을 캐스팅이라고 한다.
실행 순서는 다음과 같다.
- Child child = (Child) poly
- 다운캐스팅을 통해 부모 타입을 자식 타입으로 변환한 다음에 대입 시도
- Child child = (Child) x001
- 참조값을 읽은 다음 자식 타입으로 지정
- Child child = x001
여기서 캐스팅을 한다고 해서 Parent poly 의 타입이 변하는 것은 아니다.
해당 참조값을 꺼내고 꺼낸 참조값이 Child 타입이 되는 것이다.
기존 poly 의 타입은 Parent 로 기존과 같이 유지된다.
캐스팅
캐스팅에서는 업캐스팅(부모 타입으로 변경), 다운캐스팅(자식 타입으로 변경) 이 있다.
다운캐스팅
자식 타입의 기능을 사용하려면 다음과 같이 다운캐스팅 결과를 변수에 담아두고 이후에 기능을 사용하면 된다.
Child child = (Child) poly
child.childMethod();
하지만 이 과정이 번거롭다.
다음 코드를 보자.
public class CastingMain2 {
public static void main(String[] args) {
//부모 변수가 자식 인스턴스 참조(다형적 참조)
Parent poly = new Child();
//단 자식의 기능은 호출할 수 없다. 컴파일 오류 발생
//poly.childMethod();
//일시적 다운캐스팅 - 해당 메서드를 호출하는 순간만 다운캐스팅
((Child) poly).childMethod();
}
}
위 과정을 일시적 다운 캐스팅 이라고 한다.
- 다운 캐스팅을 통해 부모타입을 자식타입으로 변환 후 기능 호출
- 참조값을 읽은 다음 자식 타입으로 다운캐스팅
이렇게 일시적으로 다운캐스팅을 사용하면 별도의 변수 없이 인스턴스의 자식 타입의 기능을 사용할 수 있다.
업캐스팅
public class CastingMain {
public static void main(String[] args) {
Child child = new Child();
Parent parent1 = (Parent) child; //업캐스팅은 생략 가능, 생략 권장
Parent parent2 = child; //업캐스팅 생략
parent1.parentMethod();
parent2.parentMethod();
}
}
부모 타입으로 변환하는 경우에는 (타입) 을 생략할 수 있다.
다운캐스팅과 주의점
다운캐스팅은 잘못하면 심각한 런타임 오류가 발생할 수 있다.
public class CastingMain {
public static void main(String[] args) {
Parent parent1 = new Child();
Child child1 = (Child) parent1;
child1.childMethod(); //문제 없음
Parent parent2 = new Parent();
Child child2 = (Child) parent2; //런타임 오류 - ClassCastException
child2.childMethod(); //실행 불가
}
}
실행 결과를 보면 child1.childMethod() 는 잘 호출되었지만, child2.childMethod() 는 실행되지 못하고,
그 전에 오류가 발생했다
왜 오류가 발생하는지 알아보자.
Parent parent2 = new Parent()new Parent()로 부모 타입으로 객체를 생성한다.- 메모리 상에 자식 타입은 전혀 존재하지 않는다.
- 생성 결과를
parent2에 담아둔다.
Child child2 = (Child) parent2parent2는Parent로 생성이 되었다.- 따라서 메모리 상에
Child자체가 존재하지 않아 사용할 수 없다.
자바에서는 이렇게 사용할 수 없는 타입으로 다운캐스팅하는 경우에 ClassCastException 이라는 예외를 발생시킨다.
업캐스팅이 안전하고 다운캐스팅이 위험한 이유
업캐스팅은 위와 같은 문제가 발생하지 않는다.
왜냐하면 객체를 생성하면 해당 타입의 상위 부모 타입은 모두 생성된다.
따라서 위로만 타입을 변경하는 업캐스팅은 메모리 상에 인스턴스가 모두 존재하기 때문에 항상 안전하다.
반면에 다운캐스팅의 경우 인스턴스에 존재하지 않는 하위 타입으로 캐스팅하는 문제가 발생할 수 있다.
왜냐하면 객체를 생성하면 부모 타입은 모두 함께 생성되지만 자식 타입은 생성되지 않는다.
컴파일 오류 vs 런타임 오류
컴파일 오류
변수명 오타, 잘못된 클래스 이름 사용 등 자바 프로그램을 실행하기 전에 발생하는 오류이다.
이런 오류는 IDE 에서 즉시 확인할 수 있기 때문에 안전하다.
런타임 오류
프로그램이 실행되고 있는 시점에 발생하는 오류로 매우 좋지 않은 오류이다.
instanceof
참조하는 대상이 다양하기 때문에 어떤 인스턴스를 참조하고 있는지 확인하려면 어떻게 해야 할까?
변수가 참조하는 인스턴스의 타입을 확인하고 싶다면 instanceof 키워드를 사용하면 된다.
public class CastingMain {
public static void main(String[] args) {
Parent parent1 = new Parent();
System.out.println("parent1 호출");
call(parent1);
Parent parent2 = new Child();
System.out.println("parent2 호출");
call(parent2);
}
private static void call(Parent parent) {
parent.parentMethod();
if (parent instanceof Child) {
System.out.println("Child 인스턴스 맞음");
Child child = (Child) parent;
child.childMethod();
}
}
}
# 실행 결과
parent1 호출
Parent.parentMethod
parent2 호출
Parent.parentMethod
Child 인스턴스 맞음
Child.childMethod
call(Parent parent) 메서드를 살펴보자.
이 메서드는 매개변수로 넘어온 parent 가 참조하는 타입에 따라서 다른 명령을 수행한다.
해당 메서드를 처음 호출할 때 parent 는 Parent 의 인스턴스를 참조한다.
parent instanceof Child //parent는 Parent의 인스턴스
new Parent() instanceof Child //false
parent 는 Parent 의 인스턴스를 참조하므로 false 를 반환한다.
해당 메서드를 다음으로 호출할 때 parent 는 Child의 인스턴스를 참조한다.
parent instanceof Child //parent는 Child의 인스턴스
new Child() instanceof Child //true
parent 는 Child 의 인스턴스를 참조하므로 true 를 반환한다.
쉽게 판단하는 방법은
오른쪽에 있는 타입에 왼쪽에 있는 인스턴스의 타입이 들어갈 수 있는지 대입해보면된다.
parent instanceof Parent //parent는 Child의 인스턴스
new Parent() instanceof Parent //parent가 Parent의 인스턴스를 참조하는 경우: true
new Child() instanceof Parent //parent가 Child의 인스턴스를 참조하는 경우: true
new Parent() instanceof Parent
Parent p = new Parent() //같은 타입 true
new Child() instanceof Parent
Parent p = new Child() //부모는 자식을 담을 수 있다. true
new Parent() instanceof Child
Child c = new Parent() //자식은 부모를 담을 수 없다. false
new Child() instanceof Child
Child c = new Child() //같은 타입 true
다형성과 메서드 오버라이딩
메서드 오버라이딩에서 꼭 기억해야 할 점은 오버라이딩 된 메서드가 항상 우선권을 가진다 는 점이다.
package poly.overriding;
public class Parent {
public String value = "parent";
public void method() {
System.out.println("Parent.method");
}
}
package poly.overriding;
public class Child extends Parent {
public String value = "child";
@Override
public void method() {
System.out.println("Child.method");
}
}
package poly.overriding;
public class OverridingMain {
public static void main(String[] args) {
//자식 변수가 자식 인스턴스 참조
Child child = new Child();
System.out.println("Child -> Child");
System.out.println("value = " + child.value);
child.method();
//부모 변수가 부모 인스턴스 참조
Parent parent = new Parent();
System.out.println("Parent -> Parent");
System.out.println("value = " + parent.value);
parent.method();
//부모 변수가 자식 인스턴스 참조(다형적 참조)
Parent poly = new Child();
System.out.println("Parent -> Child");
System.out.println("value = " + poly.value); //변수는 오버라이딩X
poly.method(); //메서드 오버라이딩!
}
}
# 실행 결과
Child -> Child
value = childChild.method
Parent -> Parent
value = parent
Parent.method
Parent -> Child
value = parent
Child.method
코드를 분석해보자.
- child 변수는 Child 타입이다.
- child.value, child.method() 를 호출하면 인스턴스의 Child 타입에서 기능을 찾아서 실행한다.
- parent 변수는 Parent 타입이다.
- parent.value, parent.method() 를 호출하면 인스턴스의 Parent 타입에서 기능을 찾아서 실행한다.
- poly 변수는 Parent 타입이다.
- poly.value , poly.method()를 호출하면 인스턴스의 Parent 타입에서 기능을 찾아서 실행한다.
- poly.value : Parent 타입에 있는 value 값을 읽는다.
- poly.method() : Parent 타입에 있는 method() 를 실행하려고 하는데 하위 타입인 Child.method() 가 오버라이딩 되어 있다. 오버라이딩 된 메서드는 항상 우선권을 가진다.
따라서 Child.method() 가 실행된다.
다형성 활용
개,고양이,소의 울음 소리를 테스트하는 프로그램을 작성해보자.
package poly.ex1;
public class Dog {
public void sound() { System.out.println("멍멍");
}
}
package poly.ex1;
public class Cat {
public void sound() {
System.out.println("냐옹");
}
}
package poly.ex1;
public class Caw {
public void sound() {
System.out.println("음매");
}
}
package poly.ex1;
public class AnimalSoundMain {
public static void main(String[] args) {
Dog dog = new Dog();
Cat cat = new Cat();
Caw caw = new Caw();
System.out.println("동물 소리 테스트 시작");
dog.sound();
System.out.println("동물 소리 테스트 종료");
System.out.println("동물 소리 테스트 시작");
cat.sound();
System.out.println("동물 소리 테스트 종료"); System.out.println("동물 소리 테스트 시작");
caw.sound();
System.out.println("동물 소리 테스트 종료");
}
}
단순히 개,고양이,소 동물들의 울음 소리를 출력하는 프로그램이다.
여기서 새로운 동물이 추가되면 어떻게 될까?
기존 코드에 소가 없었다고 가정해보자, 소가 추가된다고 가정하면 Cow 클래스도 만들고 코드도 추가해야 한다.
또한 중복을 제거하기 위해 for 문을 아래와 같이 사용한다고 해보자.
Caw[] cawArr = {cat, dog, caw}; //컴파일 오류 발생!
System.out.println("동물 소리 테스트 시작");
for (Caw caw : cawArr) {
cawArr.sound();
}
System.out.println("동물 소리 테스트 종료");
배열과 for문 사용해서 중복을 제거하려고 해도 배열 타입을 하나로 지정해야 한다.
같은 Caw 들을 배열에 담아서 처리하는 것은 가능하지만 타입이 서로 다른 클래스들을 하나의 배열에 담는 것은 불가능하다.
문제의 핵심은 바로 타입이 다르다는 점이다.
이 문제를 해결 하기 위해 다형적 참조와 메서드 오버라이딩을 활용하면 된다.
문제 해결 - 다형성 활용
다형성을 사용하기 위해 상속 관계를 사용하면 된다.Animal 이라는 부모 클래스를 만들고 sound() 메서드를 정의하면 된다.
이 메서드는 자식 클래스에서 오버라이딩 할 목적으로 만들었다.
코드를 살펴보자.
public class Animal {
public void sound() {
System.out.println("동물 울음 소리");
}
}
public class Dog extends Animal {
@Override
public void sound() {
System.out.println("멍멍");
}
}
public class Cat extends Animal {
@Override
public void sound() {
System.out.println("냐옹");
}
}
public class Caw extends Animal{
@Override public void sound() {
System.out.println("음매");
}
}
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)을 호출하면soundAnimal(Animal animal)에Dog인스턴스가 전달된다.Animal animal = dog로 이해하면 된다.Animal은Dog의 부모이기 때문에, 부모는 자식을 담을 수 있다.- 메서드 안에서
animal.sound()메서드를 호출한다.

animal변수의 타입은Animal이므로Dog인스턴스에 있는Animal클래스 부분을 찾아서sound()메서드 호출을 시도한다.- 하위 클래스인
Dog에서sound()메서드를 오버라이딩 했다. - 따라서 오버라이딩한 메서드가 우선권을 갖는다.
- 하위 클래스인
Dog클래스에 있는sound()메서드가 호출된다.
이렇게 다형성 덕분에 Animal을 상속받는다면 새로운 동물을 추가해도 다음 코드를 그대로 재사용 할 수 있다.
이제 위 코드를 배열과 메서드 모두 활용해서 완성해보자.
public class AnimalPolyMain {
public static void main(String[] args) {
Animal[] animalArr = {new Dog(), new Cat(), new Caw()};
for (Animal animal : animalArr) {
soundAnimal(animal);
}
}
//동물이 추가 되어도 변하지 않는 코드
private static void soundAnimal(Animal animal) {
System.out.println("동물 소리 테스트 시작");
animal.sound();
System.out.println("동물 소리 테스트 종료");
}
}
지금까지 코드에는 2가지 문제가 있다.
Animal클래스를 생성할 수 있는 문제Animal클래스를 상속 받는 곳에서sound()메서드 오버라이딩을 하지 않을 가능성
Animal 클래스를 생성할 수 있는 문제
Animal animal = new Animal();
개, 고양이 등 실제 존재하는 것은 당연하지만, 동물이라는 추상적인 개념이 실제로 존재하는 것은 이상하다.
이 클래스는 다형성을 위해서 필요한 것이지 직접 인스턴스를 생성해서 사용할 일은 없다.
하지만 누군가 실수로 인스턴스를 생성할 수 있다는 것이다.
이렇게 생성된 인스턴스는 작동은 하지만 제대로된 기능을 수행하지 않는다.
Animal 클래스를 상속 받는 곳에서 sound() 메서드 오버라이딩을 하지 않을 가능성
예를 들어 Animal 을 상속 받은 Pig 클래스를 만든다고 가정해보자.
그런데 개발자가 실수로 sound() 메서드를 오버라이딩 하는 것을 빠트릴 수 있다.
이렇게 되면 부모의 기능을 상속 받고, 프로그램을 실행하면 기대와 다르게 부모 클래스에 있는 Animal.sound() 가 호출될 것이다.
추상 클래스
추상 클래스
Animal 클래스와 같이 부모 클래스는 제공하지만, 실제 생성되면 안되는 클래스를 추상 클래스라 한다.
추상 클래스는 이름 그대로 추상적인 개념을 제공하는 클래스이다.
따라서 실체인 인스턴스가 존재하지 않는다. 대신에 상속을 목적으로 사용되고, 부모 클래스 역할을 담당한다.
abstract class AbstractAnimal {...}
특징
- 클래스를 선언할 때 앞에 추상이라는 의미의
abstract키워드를 붙여준다. - 직접 인스턴스를 생성하지 못한다.
추상 메서드
부모 클래스를 상속 받는 자식 클래스가 반드시 오버라이딩 해야 하는 메서드를 부모 클래스에 정의할 수 있는데 이것을 추상 메서드라고 한다.
추상 메서드는 이름 그대로 추상적인 개념을 제공하는 메서드이다.
따라서 실체가 존재하지 않고, 메서드 바디가 없다.
public abstract void sound()
특징
- 선언할 때 메서드 앞에 추상이라는 의미의
abstract키워드를 붙여준다. - 추상 메서드가 하나라도 있는 클래스는 추상 클래스로 선언해야 한다.
- 추상 메서드는 메서드 바디가 없기 때문에 작동하지 않는 메서드를 가진 불완전한 클래스로 볼 수 있다.
- 따라서 직접 생성하지 못하도록 추상 클래스로 선언해야 한다.
- 추상 메서드는 상속 받는 자식 클래스가 반드시 오버라이딩 해서 사용해야 한다.
활용
이제 추상클래스, 메서드를 활용한 코드를 살펴보자.
public abstract class AbstractAnimal {
public abstract void sound();
public void move() {
System.out.println("동물이 움직입니다.");
}
}
public class Dog extends AbstractAnimal {
@Override
public void sound() {
System.out.println("멍멍");
}
}
public class Cat extends AbstractAnimal {
@Override
public void sound() {
System.out.println("냐옹");
}
}
public class Caw extends AbstractAnimal {
@Override
public void sound() {
System.out.println("음매");
}
}
public class AbstractMain {
public static void main(String[] args) {
//추상클래스 생성 불가
//AbstractAnimal animal = new AbstractAnimal();
Dog dog = new Dog();
Cat cat = new Cat();
Caw caw = new Caw();
cat.sound();
cat.move();
soundAnimal(cat);
soundAnimal(dog);
soundAnimal(caw);
}
//동물이 추가 되어도 변하지 않는 코드
private static void soundAnimal(AbstractAnimal animal) {
System.out.println("동물 소리 테스트 시작");
animal.sound();
System.out.println("동물 소리 테스트 종료");
}
}
코드를 살펴보자.
- 추상 클래스는 생성이 불가능하다.
- 코드의 주석을 풀고 실행하면 컴파일 오류가 발생한다.
- 추상 메서드는 반드시 오버라이딩 해야 한다.
정리를 해보자면,
- 추상 클래스 덕분에 실수로
Animal인스턴스를 생성할 문제를 근본적으로 방지해준다. - 추상 메서드 덕분에 새로운 동물의 자식 클래스를 만들 때 실수로
sound()를 오버라이딩 하지 않을 문제를 근본적으로 방지해준다.
순수 추상 클래스
모든 메서드가 추상 메서드인 순수 추상 클래스는 코드를 실행할 바디 부분이 전혀 없다.
public abstract class AbstractAnimal {
public abstract void sound();
public abstract void move();
}
이러한 순수 추상 클래스는 실행 로직을 전혀 가지고 있지 않다.
단지 다형성을 위한 부모 타입으로써 껍데기 역할만 제공할 뿐이다.
특징
- 인스턴스를 생성할 수 없다.
- 상속시 자식은 모든 메서드를 오버라이딩 해야 한다.
- 주로 다형성을 위해 사용된다.
이런 특징을 잘 생각해보면 순수 추상 클래스는 마치 어떤 규격을 지켜서 구현해야 하는 것 처럼 느껴지고,
또한 이것은 우리가 일반적으로 이야기하는 인터페이스와 같이 느껴진다.
인터페이스
자바는 순수 추상 클래스를 더 편리하게 사용할 수 있도록 인터페이스라는 개념을 제공한다.
인터페이스는 class 가 아니라 interface 키워드를 사용하면 된다.
인터페이스
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;
double MY_PI = 3.14;
}
인터페이스에서 멤버 변수는 public,static,final 이 모두 포함되었다고 간주된다.
자바에서 static final 을 사용해 정적이면서 고칠 수 없는 변수를 상수라고 한다.
인터페이스 활용
이제 인터페이스를 활용해 코드를 작성해보자.
인터페이스를 상속 받을 때는 implements 라는 구현 이라는 키워드를 사용해야 한다.
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("개 이동");
}
}
public class Cat implements InterfaceAnimal {
@Override
public void sound() {
System.out.println("냐옹");
}
@Override
public void move() {
System.out.println("고양이 이동");
}
}
public class Caw implements InterfaceAnimal {
@Override
public void sound() {
System.out.println("음매");
}
@Override
public void move() {
System.out.println("소 이동");
}
}
public class InterfaceMain {
public static void main(String[] args) {
//인터페이스 생성 불가
//InterfaceAnimal interfaceMain1 = new InterfaceAnimal();
Cat cat = new Cat();
Dog dog = new Dog();
Caw caw = new Caw();
soundAnimal(cat);
soundAnimal(dog);
soundAnimal(caw);
}
//동물이 추가 되어도 변하지 않는 코드
private static void soundAnimal(InterfaceAnimal animal) {
System.out.println("동물 소리 테스트 시작");
animal.sound();
System.out.println("동물 소리 테스트 종료");
}
}
앞서 공부한 순수 추상 클래스 예제와 거의 유사한 것을 알 수 있다.
상속 vs 구현
부모 클래스의 기능을 자식 클래스가 상속 받을 때, 클래스는 상속 받는다고 표현하지만,
부모 인터페이스의 기능을 자식이 상속 받을 때는 인터페이스를 구현한다고 표현한다.
상속은 이름 그대로 부모의 기능을 물려 받는 것이 목적이다.
인터페이스는 모든 메서드가 추상 메서드이기 때문에 물려받을 수 있는 기능이 없고,
오히려 인터페이스에 정의한 모든 메서드를 자식이 오버라이딩 해서 기능을 구현해야 하기 때문에, 구현이라고 표현한다.
인터페이스는 메서드 이름만 있는 설계도이고, 이 설계도가 실제 어떻게 작동하는지는 하위 클래스에서 모두 구현해야 한다.
인터페이스를 사용해야 하는 이유
- 제약
- 인터페이스의 규약(제약)은 반드시 구현해야 하는 것이다.
- 그런데 순수 추상 클래스의 경우 미래에 누군가 그곳에 실행 가능한 메서드를 끼워 넣을 수 있다.
- 이렇게 되면 추가된 기능을 자식 클래스에서 구현하지 않을 수도 있고, 또 더는 순수 추상 클래스가 아니게 된다.
- 인터페이스는 모든 메서드가 추상 메서드이다. 따라서 이런 문제를 원천 차단할 수 있다.
- 다중 구현
- 자바에서 클래스 상속은 부모를 하나만 지정할 수 있는 반면에 인터페이스는 부모를 여러면 두는 다중 구현이 가능하다.
인터페이스 다중 구현
인터페이스는 다중 구현이 가능한 이유는 무엇일까?
바로 인터페이스는 모두 추상 메서드로 이루어져 있기 때문이다.
InterfaceA, InterfaceB 는 둘 다 methodCommon() 을 가지고 있다.
그리고 Child 는 두 인터페이스를 구현했다. 이때 다이아몬드 문제가 발생한다.
하지만 인터페이스는 자신은 구현을 가지지 않는다. 대신에 인터페이스를 구현하는 곳에서 해당 기능을 모두 구현해야한다.
두 인터페이스 모두 같은 메서드를 제공하지만 이것의 기능은 Child 가 구현한다.
그리고 오버라이딩에 의해 Child 에 있는 methodCommon() 이 호출된다.
결과적으로 두 부모 중에 어떤 한 부모의 메서드를 선택하는 것이 아니라 그냥 인터페이스들을 구현한 Child 에 있는 메서드가 사용된다.
코드로 살펴보자.
public interface InterfaceA {
void methodA();
void methodCommon();
}
public interface InterfaceB {
void methodB();
void methodCommon();
}
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");
}
}
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();
}
}

코드를 살펴보자.
a.methodCommon()을 호출하면 먼저x001 Child인스턴스를 찾는다.- 변수 a 가
InterfaceA타입이므로 해당 타입에서methodCommon()을 찾는다. methodCommon은 하위 타입인Child에서 오버라이딩 되어있기 때문에Child의 메서드가 호출된다.
클래스와 인터페이스 활용
public abstract class AbstractAnimal {
public abstract void sound();
public void move() {
System.out.println("동물이 이동합니다.");
}
}
public interface Fly {
void fly();
}
public class Dog extends AbstractAnimal {
@Override
public void sound() {
System.out.println("멍멍");
}
}
public class Bird extends AbstractAnimal implements Fly {
@Override
public void sound() {
System.out.println("짹짹");
}
@Override
public void fly() {
System.out.println("새 날기");
}
}
public class Chicken extends AbstractAnimal implements Fly {
@Override
public void sound() {
System.out.println("꼬끼오");
}
@Override
public void fly() {
System.out.println("닭 날기");
}
}
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("날기 테스트 종료");
}
}
extends 를 통한 상속은 하나만 할 수 있고 implements 를 통한 인터페이스는 다중 구현 할 수 있기 때문에 둘이 함께 나온 경우 extends 가 먼저 나와야 한다.
'인프런 > 김영한 자바' 카테고리의 다른 글
| [김영한의 실전 자바 중급 1편] - 2. 불변 객체 (0) | 2025.10.10 |
|---|---|
| [김영한의 실전 자바 중급 1편] - 1. Object 클래스 (0) | 2025.09.20 |
| [김영한의 실전 자바 기본편] - 9. 상속 (0) | 2025.09.15 |
| [김영한의 실전 자바 기본편] - 8. final (0) | 2025.09.12 |
| [김영한의 실전 자바 기본편] - 7. 자바 메모리와 static (0) | 2025.09.11 |