일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | |||
5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 |
- 게시글 목록 api
- 스프링 mvc1 - 스프링 mvc
- 스프링 고급 - 스프링 aop
- 자바의 정석 기초편 ch1
- 스프링 db1 - 스프링과 문제 해결
- 자바의 정석 기초편 ch2
- @Aspect
- 스프링 mvc2 - 검증
- 2024 정보처리기사 시나공 필기
- 스프링 mvc1 - 서블릿
- 2024 정보처리기사 수제비 실기
- 자바의 정석 기초편 ch13
- jpa - 객체지향 쿼리 언어
- 스프링 mvc2 - 타임리프
- 스프링 db2 - 데이터 접근 기술
- 자바의 정석 기초편 ch7
- 스프링 mvc2 - 로그인 처리
- 코드로 시작하는 자바 첫걸음
- 자바의 정석 기초편 ch11
- 자바의 정석 기초편 ch9
- 자바의 정석 기초편 ch3
- 자바의 정석 기초편 ch14
- 자바의 정석 기초편 ch4
- jpa 활용2 - api 개발 고급
- 자바의 정석 기초편 ch12
- 자바의 정석 기초편 ch5
- 스프링 입문(무료)
- 자바 기본편 - 다형성
- 자바의 정석 기초편 ch8
- 자바의 정석 기초편 ch6
- Today
- Total
나구리의 개발공부기록
다형성, 다형성 활용, 추상 클래스, 인터페이스, 인터페이스 - 다중 구현, 클래스와 인터페이스 활용 본문
다형성, 다형성 활용, 추상 클래스, 인터페이스, 인터페이스 - 다중 구현, 클래스와 인터페이스 활용
소소한나구리 2025. 1. 14. 17:44출처 : 인프런 - 김영한의 실전 자바 - 기본편 (유료) / 김영한님
유료 강의이므로 정리에 초점을 두고 코드는 일부만 인용
1. 다형성 활용
1) 예제1 - 다형성을 사용하지 않는 프로그램
(1) Dog, Cat, Cow
- 다형성을 왜 사용하는지 장점을 알아보기위해 우선 다형성을 사용하지 않고 개, 고양이, 소의 울음소리를 테스트하는 프로그램을 작성
- 단순히 개, 고양이 소 동물의 울음 소리를 출력하는 sound()메서드를 가진 클래스들
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 Cow {
public void sound() {
System.out.println("음머");
}
}
(4) AnimalSoundMain
- 위에서 만든 동물들의 클래스의 인스턴스를 생성하고 메서드를 출력하는 클래스
- 만약 여기서 새로운 동물이 추가되려면 새로운 동물 클래스를 만들고 해당 인스턴스를 생성하고 테스트 시작, 종료라는 출력메서드와 함께 sound()메서드를 추가해야함
- Cow를 생성하는 부분은 당연히 필요하니 크게 상관없지만 테스트 시작, 종료의 출력문과 메서드를 호출하는 부분은 계속 중복이 증가함
package poly.ex1;
public class AnimalSoundMain {
public static void main(String[] args) {
Dog dog = new Dog();
Cat cat = new Cat();
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("동물 소리 테스트 종료");
}
}
/* 실행결과
동물 소리 테스트 시작
멍멍
동물 소리 테스트 종료
동물 소리 테스트 시작
야옹
동물 소리 테스트 종료
동물 소리 테스트 시작
음머
동물 소리 테스트 종료
*/
2) 중복 제거 시도
(1) 메서드로 중복 제거 시도
- 메서드를 사용하면 매개변수의 클래스를 Cow, Dog, Cat 중에 하나로 정해야하기 때문에 Cow 전용 메서드가 되고 Dog, Cat은 인수로 사용할 수 없음
- 해당 메서드에 cat을 인수로 입력하고 메서드를 호출하면 'java: incompatible types: poly.ex1.Cat cannot be converted to poly.ex1.Cow' 에러가 발생함
- 즉, Dog, Cat, Cow의 타입(클래스)이 서로 다르기 때문에 soundCow()메서드를 함께 사용하는 것은 불가능함
package poly.ex1;
public class AnimalSoundMain {
public static void main(String[] args) {
// ... 기존 코드 동일 생략
soundCow(cat);
soundCow(cow);
}
private static void soundCow(Cow cow) {
System.out.println("동물 소리 테스트 시작");
cow.sound();
System.out.println("동물 소리 테스트 종료");
}
}
(2) 배열과 for 문을 통한 중복 제거 시도
Cow[] cowArr = {dog, cat, cow}; // 컴파일 오류 발생
- 같은 Cow들을 배열에 담아서 처리는 가능하지만 타입이 서로 다른 Dog, Cat, Cow를 하나의 배열에 담는 것은 애초에 불가능함
(3) 지금의 코드로는 불가능함
- 지금 상황에서는 모든 시도가 Dog, Cat, Cow의 타입이 서로 다르기 때문에 해결방법이 없음
- 문제의 핵심은 타입이 다르다는 점인데, 반대로 이야기하면 Dog, Cat, Cow가 모두 같은 타입을 사용할 수 있는 방법이 있다면 메서드와 배열을 활용하여 코드의 중복을 제거할 수 있다는 뜻임
- 다형성의 핵심인 다형적 참조와 메서드 오버라이딩을 활용하면 이를 모두 같은 타입을 사용하고 각자 자신의 메서드도 호출할 수 있음
3) 다형성 활용 - 문제 해결
(1) 상속
- 다형성을 활용하기 위해 상속 관계를 이용하여 Animall이라는 부모 클래스를 만들고 sound()메서드를 정의 후 자식클래스에서 오버라이딩을 진행
- Dog, Cat, Cow는 Animal 클래스를 상속을 받음
(2) Animal
package poly.ex2;
public class Animal {
public void sound() {
System.out.println("동물 울음 소리");
}
}
(3) Dog, Cat, Cow - 수정
- Animal 클래스를 상속받도록 수정하고 부모의 sound()메서드를 오버라이딩
package poly.ex2;
public class Dog extends Animal {
@Override
public void sound() {
System.out.println("멍멍");
}
}
package poly.ex2;
public class Cow extends Animal {
@Override
public void sound() {
System.out.println("음머");
}
}
package poly.ex2;
public class Cat extends Animal {
@Override
public void sound() {
System.out.println("야옹");
}
}
(3) AnimalPolyMain1
- soundAnimal()메서드의 매개변수 타입을 부모타입으로 지정하여 작성하고, 해당 메서드를 호출할 때 Dog, Cat, Cow 인스턴스의 참조값을 가리키는 변수들을 인수로 입력
- 실행해보면 다형성을 활용하기전과 결과는 똑같지만 코드가 매우 간결해진 것을 확인할 수 있음
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);
}
private static void soundAnimal(Animal animal) {
System.out.println("동물 소리 테스트 시작");
animal.sound();
System.out.println("동물 소리 테스트 종료");
}
}
/* 실행 결과
동물 소리 테스트 시작
멍멍
동물 소리 테스트 종료
동물 소리 테스트 시작
야옹
동물 소리 테스트 종료
동물 소리 테스트 시작
음머
동물 소리 테스트 종료
*/
(4) 코드 분석 - soundAnimal(dog)
- soundAnimal(Animal animal)에 Dog 인스턴스가 전달되고 해당 메서드 안에서 animal.sound() 메서드를 호출함
- animal 변수의 타입은 Animal(부모클래스)이기 때문에 Dog 인스턴스에 있는 Animal 클래스 부분을 찾아서 sound()메서드 호출을 시도하지만 하위클래스인 Dog에서 sound() 메서드를 오버라이딩 했으므로 Dog의 sound()메서드가 우선권을 가지게 됨
- 결국 Dog클래스의 sound()메서드가 호출이되고 Animal클래스의 sound()는 실행되지 않음
(5) 핵심은 Animal animal
- 다형적 참조 덕분에 animal 변수는 자식인 Dog, Cat, Cow의 인스턴스를 참조할 수 있게됨
- 메서드 오버라이딩 덕분에 animal.sound()를 호출해도 오버라이딩된 각 자식 인스턴스의 메서드를 호출할 수 있었음
- 만약 오버라이딩이 없었다면 모두 Animal클래스의 sound()가 호출되었을 것임
- 다형성 덕분에 새로운 동물을 추가할 때 Animal클래스를 상속받고 sound()메서드를 오버라이딩하면 기존 코드를 재사용하여 매우 수월하게 확장할 수 있게됨
4) 다형성 활용 - 최적화(중복 제거)
(1) 배열과 for문을 활용
- Animal타입 배열을 생성하고 다형적 참조를 활용하여 자식 클래스의 인스턴스들을 배열에 추가
- animalArr의 값들을 반복문을 활용하여 하나씩 꺼낸 후 오버라이딩된 sound()메서드와 출력문을 반복하여 실행하도록하면 기존과 동일하게 출력되는 것을 확인할 수 있음
package poly.ex2;
public class AnimalPolyMain2 {
public static void main(String[] args) {
Dog dog = new Dog();
Cat cat = new Cat();
Cow cow = new Cow();
Animal[] animalArr = {dog, cat, cow};
for (Animal animal : animalArr) {
System.out.println("동물 소리 테스트 시작");
animal.sound();
System.out.println("동물 소리 테스트 종료");
}
}
}
(2) 배열과 메서드 모두 활용 - 한번더 개선
- 부모타입 배열 변수를 선언과 동시에 자식 인스턴스들을 생성하여 참조하도록 코드를 수정
- 반복문으로 출력하던코드를 soundAnimal()메서드로 별도로 추출하여 해당 메서드만 반복문으로 호출되도록 수정하면 코드를 사용하는 부분(main메서드 부분)의 코드가 훨씬 깔끔해진 것을 확인할 수 있음
- 새로운 동물이 추가되어도 soundAnimal()메서드는 Animal이라는 추상적인 부모를 참조하기 때문에 추가된 동물 클래스가 Animal 클래스를 상속받기만 하면 코드의 변경 없이 유지할 수 있음
- 코드를 잘 보면 main() 부분은 코드가 변하고 soundAnimal()은 코드가 변하지 않는데, 새로운 기능이 추가되었을 때 변하는 부분을 최소화 하는것이 잘 작성된 코드임
- 이렇게 하기 위해서는 코드에서 변하는 부분과 변하지 않는 부분을 명확하게 구분하는 것이 좋음
package poly.ex2;
public class AnimalPolyMain3 {
public static void main(String[] args) {
Animal[] animalArr = {new Dog(), new Cat(), new Cow()};
for (Animal animal : animalArr) {
soundAnimal(animal);
}
}
private static void soundAnimal(Animal animal) {
System.out.println("동물 소리 테스트 시작");
animal.sound();
System.out.println("동물 소리 테스트 종료");
}
}
5) 남은 문제
(1) Animal 클래스를 생성할 수 있는 문제
- 그러나 생각해보면 조금 어색한데 개, 고양이, 소는 실제 존재하는 것이기 때문에 생성하는 것은 당연하지만 동물이라는 추상적인 개념이 실제로 존재한다는 것은 조금 이상함
- 동물이라는 클래스는 다형성을 위해서 필요한 것이지 직접 인스턴스를 사용할일은 없지만 Animal은 클래스이므로 당연히 인스턴스를 생성하는데 제약이 없으므로 누군가 new Animal()을 사용하여 인스턴스를 생성할 수 있음
- 이렇게 생성된 인스턴스는 작동은 하지만 제대로된 기능을 수행하지 않음
(2) Animal 클래스를 상속 받는 곳에서 sound() 메서드를 오버라이딩 하지 않을 가능성이 있는 문제
- Animal을 상속 받은 자식 클래스가 있다고 가정했을 때 개발자가 실수로 자식클래스에서 Animal()의 메서드를 오버라이딩 하지 않아 기대하는 바와 다르게 sound()메서드가 동작할 수 있음
- 이런문제는 코드상에서는 아무런 문제가 발생하지 않고 코드를 실행하면 문제가 발생되므로 치명적일 수 있음
(3) 해결
- 좋은 프로그램은 제약이 있는 프로그램
- 추상 클래스와 추상메서드로 위의 문제를 한번에 해결할 수 있음
2. 추상 클래스
1) 설명
(1) 추상 클래스
abstract class AbstractAnimal {...}
- 위의 예제에서 동물(Animal)과 같이 부모 클래스는 제공하지만 실제 생성되면 안되는 클래스를 추상 클래스라함
- 이름 그대로 추상적인 개념을 제공하는 클래스이며 실체인 인스턴스가 존재하지 않는 대신 상속을 목적으로 사용되고 부모 클래스 역할을 담당함
- 클래스를 선언할 때 앞에 추상이라는 의미의 abstract 키워드를 붙여주면 됨
- 추상 클래스는 기존 클래스와 완전히 같지만 new 키워드를 사용하여 직접 인스턴스를 생성하지 못하는 제약이 추가된 것임
(2) 추상 메서드
public abstract void sound();
- 부모 클래스를 상속 받는 자식 클래스가 반드시 오버라이딩 해야 하는 메서드를 부모 클래스에 정의할 수 있는데 이것을 추상 메서드라 하며 이름 그대로 추상적인 개념을 제공하는 메서드로 실체가 존재하지안혹 메서드 바디가 없음
- 메서드 선언 시 앞에 abstract 키워드를 붙여주면 됨
- 추상 메서드가 하나라도 있는 클래스는 추상 클래스로 선언해야 하며 그렇지 않으면 컴파일 오류가 발생함
- 추상 메서드는 메서드 바디가 없는 작동하지 않는 메서드를 가진 불완전한 클래스로 볼 수 있으므로 직접 생성하지 못하도록 클래스를 추상 클래스로 선언해야 함 - 추상 메서드는 상속 받는 자식 클래스가 반드시 오버라이딩 해서 사용해야하며 그렇지 않으면 컴파일 오류가 발생함
- 추상 메서드는 메서드 바디 부분이 없으므로 자식 클래스가 반드시 오버라이딩 해야하며, 바디 부분을 만들면 컴파일 오류가 발생함
- 만약 오버라이딩 하지 않으면 자식도 추상 클래스가 되어야 함 - 추상 메서드는 기존 메서드와 완전히 같지만 메서드 바디가 없고 자식 클래스가 해당 메서드를 반드시 오버라이딩 해야한다는 제약이 추가된 것임
2) 예제
(1) AbstractAnimal
- AbstractAnimal은 abstract가 붙은 추상클래스이므로 직접 인스턴스를 생성할 수 없음
- sound()는 abstract가 붙은 추상메서드이므로 자식클래스는 반드시 이 메서드를 오버라이딩 해야함
- move() 메서드는 추상 메서드가 아닌데, 추상 클래스는 자식 클래스가 오버라이딩하지 않고 그대로 상속하여 사용할 수 있도록 일반 메서드도 가질 수 있음
- 참고로 예제에서는 클래스 이름에 Abstract... 처럼 작성하였는데 실무에서는 추상 클래스라고 꼭 이렇게 써야할 필요없이 Animal이라고 사용해도 되며 예제에서만 구분을 위해 적용한 것임
package poly.ex3;
public abstract class AbstractAnimal {
public abstract void sound();
public void move() {
System.out.println("동물이 움직입니다");
}
}
(2) Dog, Cat, Cow
- 추상 클래스를 상속받고 부모의 메서드를 오버라이딩, 기존과 코드가 동일하므로 생략
- 추상 메서드는 반드시 오버라이딩 해야하므로 자식에서 오버라이딩 메서드를 만들지 않으면 컴파일 오류가 발생함
package poly.ex3;
public class Dog extends AbstractAnimal {
// ... 나머지 코드 동일 생략
}
public class Cat extends AbstractAnimal {
// ...
}
public class Cow extends AbstractAnimal {
// ...
}
(3) AbstractMain
- 추상클래스는 생성이 불가하므로 new AbtractAnimal()은 컴파일 오류가 발생함
- 지금까지 설명한 제약을 제외하고 나머지는 모두 일반적인 클래스와 동일하며 메모리 구조, 실행결과가 모두 기존의 다형성을 활용한 것과 동일함
package poly.ex3;
public class AbstractMain {
public static void main(String[] args) {
// 추상 클래스 생성 불가
// AbtractAnimal abtractAnimal = new AbtractAnimal();
Dog dog = new Dog();
Cat cat = new Cat();
Cow cow = new Cow();
cat.sound();
cat.move();
soundAnimal(dog);
soundAnimal(cat);
soundAnimal(cow);
}
private static void soundAnimal(AbstractAnimal animal) {
System.out.println("동물 소리 테스트 시작");
animal.sound();
System.out.println("동물 소리 테스트 종료");
}
}
/* 실행 결과
야옹
동물이 움직입니다
동물 소리 테스트 시작
멍멍
동물 소리 테스트 종료
동물 소리 테스트 시작
야옹
동물 소리 테스트 종료
동물 소리 테스트 시작
음머
동물 소리 테스트 종료
*/
(4) 정리
- 추상 클래스 덕분에 실수로 Animal 인스턴스를 생성할 문제를 근본적으로 방지할 수 있음
- 추상 메서드 덕분에 새로운 동물의 자식 클래스를 만들 때 실수로 sound()를 오버라이딩 하지 않을 문제를 근본적으로 방지해줌
3) 순수 추상 클래스
(1) 모든 메서드가 추상 메서드인 추상 클래스
- 앞서 만든 예제에서 move()도 추상 메서드로 만들어야 한다고 가정해보면 AbstractAnimal 클래스의 모든 메서드가 추상 메서드가되는데 이런 클래스를 순수 추상 클래스라함
- move()도 추상 메서드가 되었으니 자식들은 AbstractAnimal의 모든 기능을 오버라이딩 해야함
(2) AbstractAnimal
- 추상 메서드만 가지고 있는 추상 클래스
package poly.ex4;
public abstract class AbstractAnimal {
public abstract void sound();
public abstract void move();
}
(3) Dog, Cat, Cow
- 생성한 순수 추상클래스를 상속받는 클래스들은 추상 메서드를 오버라이딩 해야만 함
package poly.ex4;
public class Dog extends AbstractAnimal {
@Override
public void sound() {
System.out.println("멍멍");
}
@Override
public void move() {
System.out.println("강아지 이동");
}
}
package poly.ex4;
public class Cat extends AbstractAnimal {
@Override
public void sound() {
System.out.println("야옹");
}
@Override
public void move() {
System.out.println("고양이 이동");
}
}
package poly.ex4;
public class Cow extends AbstractAnimal {
@Override
public void sound() {
System.out.println("음머");
}
@Override
public void move() {
System.out.println("소 이동");
}
}
(4) AbstractMain
- 오버라이딩한 sound()와 move()를 모두 출력하기 위해 추상클래스 타입을 매개변수로 하는 메서드들을 생성
- 자식 인스턴스를 인수로하여 해당 메서드를 호출하면 정상적으로 오버라이딩한 메서드들이 출력되는 것을 확인할 수 있음
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(dog);
soundAnimal(cat);
soundAnimal(cow);
moveAnimal(dog);
moveAnimal(cat);
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("동물 이동 테스트 종료");
}
}
/* 실행 결과
동물 소리 테스트 시작
멍멍
동물 소리 테스트 종료
동물 소리 테스트 시작
야옹
동물 소리 테스트 종료
동물 소리 테스트 시작
음머
동물 소리 테스트 종료
동물 이동 테스트 시작
강아지 이동
동물 이동 테스트 종료
동물 이동 테스트 시작
고양이 이동
동물 이동 테스트 종료
동물 이동 테스트 시작
소 이동
동물 이동 테스트 종료
*/
(5) 순수 추상 클래스
- 모든 메서드가 추상 메서드인 순수 추상 클래스는 코드를 실행로직을 담당하는 바디 부분이 전혀 없고 단지 다형성을 위한 부모 타입으로써 껍데기 역할만 제공할 뿐임
- 인스턴스를 생성할 수 없고 상속시 자식은 모든 메서드를 오버라이딩해야하며 주로 다형성을 위해 사용함
- 상속하는 클래스는 모든 메서드를 오버라이딩 해야 한다라는 특징은 상속 받는 클래스 입장에서 보면 부모의 모든 메서드를 구현해야 하는데 이런 특징은 마치 어떤 규격을 지켜서 구현해야 하는 것 처럼 느껴짐
- 위의 예제에서 보면 AbstractAnimal의 경우 sound(), move()라는 규격에 맞추어서 구현을 해야했음
- 일반적으로 이야기하는 인터페이스(연결 장치)와 비슷하다고 느껴질 수 있는데 USB 인터페이스를 생각해보면 정해진 규격에 맞추어서 제품을 개발해야 USB 인터페이스를 통해 제품과 제품끼리 연결이 가능할 것임
- 이런 순수 추상 클래스의 개념은 프로그래밍에서 매우 자주 사용되는데 사실 바자에는 순수 추상 클래스라는 용어는 없으며 순수 추상 클래스의 개념을 더욱 편리하게 사용할 수 있도록 인터페이스라는 개념을 제공함
3. 인터페이스
1) 설명
(1) 인터페이스
public interface InterfaceAnimal {
void sound();
void move();
}
- 인터페이스는 calss가 아니라 interface라는 키워드를 사용하면 됨
- 앞서 설명한 순수 추상 클래스의 모든 특징과 동일한 특징을 가지며 여기에 약간의 편의기능이 추가됨
- 메서드는 모두 public, abstract 이므로이를 생략할 수 있으며 생략이 권장이됨
- 다중 구현(다중 상속)을 지원함
(2) 인터페이스와 멤버 변수
public interface InterfaceAnimal {
public static final double MY_PI = 3.14;
double MY_PI = 3.14;
}
- 인터페이스에서 멤버 변수는 public, static, final이 모두 포함되었다고 간주됨
- 즉, 인터페이스의 멤버 변수는 상수가 되며 관례상 대문자에 언더스코어로 구분되며 인스턴스와 전혀 무관함
- 메서드 처럼 키워드들을 생략할 수 있으며 생략이 권장됨
2) 예제
(1) 그림
- 클래스 상속 관계는 UML에서 실선을 사용하지만 인터페이스 구현(상속)관계는 UML 점선을 사용함
- 클래스는 extends로 상속 하지만 인터페이스는 implements로 구현을 함
(2) InterfaceAnimal - interface
- sound(), move() 메서드를 가진 인터페이스
- 해당 메서드는 앞에 public abstract가 생략되어 있으므로 상속 받는 곳에서 모든 메서드를 오버라이딩 해야함
- 예제이므로 클래스 이름에 Interface라고 명시한 것이지 실무에서는 이렇게 작성하지 않고 Animal이라고 해도 됨
package poly.ex5;
public interface InterfaceAnimal {
void sound();
void move();
}
(3) Dog, Cat, Cow
- 인터페이스를 구현한 클래스들은 인터페이스의 모든 메서드를 직접 구현해야 함
package poly.ex5;
public class Dog implements InterfaceAnimal {
@Override
public void sound() {
System.out.println("멍멍");
}
@Override
public void move() {
System.out.println("강아지 이동");
}
}
package poly.ex5;
public class Cat implements InterfaceAnimal {
@Override
public void sound() {
System.out.println("야옹");
}
@Override
public void move() {
System.out.println("고양이 이동");
}
}
package poly.ex5;
public class Cow implements InterfaceAnimal {
@Override
public void sound() {
System.out.println("음머");
}
@Override
public void move() {
System.out.println("소 이동");
}
}
(4) InterfaceMain
- 인터페이스는 추상 클래스와 마찬가지로 생성은 불가능함
- 간단하게 구현 클래스에서 구현한 sound()메서드만 호출할 수 있도록 soundAnimal()메서드를 InterfaceAnimal 타입을 매개변수로 지정
- 해당 메서드를 호출하여 각 구현한 인스턴스의 참조변수를 인수로 입력하면 정상적으로 해당 클래스에서 구현한 메서드들이 출력되는 것을 확인할 수 있음
package poly.ex5;
import poly.ex2.Animal;
public class InterfaceMain {
public static void main(String[] args) {
// 인터페이스는 생성 불가
// InterfaceAnimal interfaceAnimal = new InterfaceAnimal();
Dog dog = new Dog();
Cat cat = new Cat();
Cow cow = new Cow();
soundAnimal(dog);
soundAnimal(cat);
soundAnimal(cow);
}
private static void soundAnimal(InterfaceAnimal animal) {
System.out.println("동물 소리 테스트 시작");
animal.sound();
System.out.println("동물 소리 테스트 종료");
}
}
/* 실행 결과
동물 소리 테스트 시작
멍멍
동물 소리 테스트 종료
동물 소리 테스트 시작
야옹
동물 소리 테스트 종료
동물 소리 테스트 시작
음머
동물 소리 테스트 종료
*/
(5) 클래스, 추상 클래스, 인터페이스는 모두 같음
- 앞서 설명한 순수 추상 클래스의 예제와 거의 유사하며 동작하는 방식도 순수 추상 클래스가 인터페이스가 되었을 뿐임
- 클래스, 추상 클래스, 인터페이스는 프로그램 코드와 메모리 구조상 모두 똑같이 자바에서는 .class로 다루어지며 작성할 때도 .java에 작성되며 다만 제약이 조금씩 들어갔을 뿐임
- 인터페이스는 순수 추상 클래스와 비슷하다고 생각하면 됨
(6) 상속 vs 구현
- 부모 클래스의 기능을 자식 클래스가 상속 받을 때 클래스는 상속 받는다고 표현하지만 부모 인터페이스의 기능을 자식이 상속 받을 때는 인터페이스를 구현한다고 함
- 상속은 이름 그대로 부모의 기능을 물려 받는 것이 목적이며 인터페이스는 모든 메서드가 추상 메서드이므로 물려받을 수 있는 기능이 없고 오히려 인터페이스에 정의한 모든 메서드를 자식이 오버라이딩 해서 기능을 구현해야 하므로 구현한다고 표현함
- 인터페이스는 메서드 이름만 있는 설계도이며 이 설계도가 실제 어떻게 작동하는지는 하위 클래스에서 모두 구현해야함
- 상속과 구현은 사람이 표현하는 단어만 다를 뿐이지 자바 입장에서는 똑같은 구조로 동일하게 작동함
(7) 인터페이스를 사용해야 하는 이유
- 모든 메서드가 추상 메서드인 순수 추상 클래스를 만들어도 되고 인터페이스를 만들어도 되지만 단순히 편리하다는 이유를 넘어서 인터페이스를 사용해야만 함
- 제약: 인터페이스를 만드는 이유는 인터페이스를 구현하는 곳에서 인터페이스의 메서드를 반드시 반드시 구현하라는 규약(제약)을 주는 것임
추상 클래스의 경우 순수한 추상 클래스라고해도 미래에 누군가 실행 가능한 메서드를 끼워 넣어서 추가된 기능은 자식 클래스에서 오버라이딩 그대로 상속하여 사용할 수 반면 인터페이스는 이런 문제가 발생할 수 있는 자체를 원천 차단할 수 있음 - 다중 구현: 클래스 상속은 부모를 하나만 지정할 수 있지만 인터페이스는 부모를 여러명 두는 다중 구현(다중 상속)이 가능함
- 이러한 개념은 처음부터 나온것이 아니며 다형성만 이용하면 클래스만으로도 문제를 해결할 수 있었으나 오랜 시간을 거쳐 다양한 문제에 직면했기 때문에 이러한 개념드로가 제약사항이 추가되고 문제를 방지하게 된 것임
- 다시한번 강조하면 좋은 프로그램은 제약이 있는 프로그램임
** 참고
- 자바 8에서 등장한 default 메서드를 사용하면 인터페이스도 메서드를 구현할 수 있음, 그러나 이것은 예외적으로 아주 특별한 경우에만 사용해야 함
- 자바 9에서 등장한 인터페이스의 private 메서드도 마찬가지이며 학습 단계에서는 이부분을 고려하지 않는 것이 좋음
- 해당 부분은 나중에 따로 다룰 예정
4. 인터페이스 - 다중 구현
1) 인터페이스의 다중 구현
(1) 허용 이유
- 클래스는 상속에서 배운대로 다중 상속이 안되는데 인터페이스의 다중 구현은 허용하는 이유는 인터페이스가 모두 추상 메서드로 이루어져 있기 때문임
- InterfaceA, InterfaceB는 둘다 같은 methodCommon()을 가지고 있고 Child는 두 인터페이스를 구현함
- 상속 관계의 겨우 두 부모 중에 어떤 한 부모의 methodCommon()을 사용해야 할지 결정해야 하는 다이아몬드 문제가 발생함
- 하지만 인터페이스 자신은 구현을 가지지 않고 인터페이스를 구현하는 곳에서 해당 기능을 모두 구현해야함
- InterfaceA, InterfaceB는 같은 이름의 methodCommon()메서드를 제공하지만 이것의 기능은 Child가 구현하고 오버라이딩에 의해 Child에 있는 methodCommon()이 호출됨
- 결과적으로 두 부모 중에 어떤 한 부모의 methodCommon()을 선택하는 것이 아니라 그냥 인터페이스들을 구현한 Child에 있는 methodCommon()이 사용되기때문에 인터페이스는 다이아몬드 문제가 발생되지 않으며 다중 구현을 허용함
(2) InterfaceA - 인터페이스, InterfaceB - 인터페이스, Child - 구현클래스
- implements InterfaceA, InterfaceB와 같이 다중 구현을 할 수 있으며 ,(콤마)로 여러 인터페이스를 구분하면 되며 개수 제한은 없음
- methodCommon()의 경우 양쪽 인터페이스에 다 있지만 같은 메서드이므로 구현은 하나만 하면 됨
package poly.diamond;
public interface InterfaceA {
void methodA();
void methodCommon();
}
package poly.diamond;
public interface InterfaceB {
void methodB();
void methodCommon();
}
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");
}
}
(3) DiamondMain
- 다형적 참조를 통해 InterfaceA와 InterfaceB의 타입으로 선언된 변수에 Child의 인스턴스를 생성하고 참조값을 대입하여 각 메서드들을 호출하면 구현클래스에서 구현한 메서드들이 호출되는 것을 확인할 수 있음
- InterfaceA 타입 변수인 a는 methodA()를, InterfaceB 타입 변수인 b는 methodB()를 호출가능하며 methodCommon()은 두 변수가 모두 가지고 있으므로 둘다 호출이 가능함
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();
}
}
/* 실행 결과
Child.methodA
Child.methodCommon
Child.methodB
Child.methodCommon
*/
(4) 그림으로 설명
- a.methodCommon()을 호출하면 먼저 Child 인스턴스를 찾고 변수 a가 InterfaceA타입이므로 해당 타입에서 methodCommon()을 찾음
- 하위 타입인 Child에서 오버라이딩이 되어있으므로 Child의 methodCommon()이 호출됨
- b.methodCommon()도 위와 동일한 방식으로 동작하여 결국엔 오버라이딩된 메서드가 호출됨
- 즉, 인터페이스의 다중 구현은 다이아몬드 문제가 발생하지 않음
5. 클래스와 인터페이스 활용
1) 클래스 상속과 인터페이스 구현을 함께 사용
(1) 설명
- AbstractAnimal은 추상클래스로 추상 메서드인 sound()와 상속이 목적인 메서드 바디가 있는 move()메서드를 가지고 있음
- Fly는 인터페이스로 나는 동물은 해당 인터페이스를 구현하여 fly() 메서드를 구현해야 함
(2) AbstractAnimal - 추상클래스, Fly - 인터페이스
- 위 그림과 같이 AbstractAnimal 추상 클래스에는 sound() 추상 메서드와 move() 일반 메서드가 있음
- Fly 인터페이스에는 fly 추상 메서드가 있음
package poly.ex6;
public abstract class AbstractAnimal {
public abstract void sound();
public void move() {
System.out.println("동물이 이동합니다.");
}
}
package poly.ex6;
public interface Fly {
void fly();
}
(3) Dog, Bird, Chicken
- Dog클래스는 AbstractAnimal만 상속 받고 sound()메서드를 구현
- Bird와 Chicken은 AbstractAnimal을 상속 받아 sound()메서드를 구현하고 Fly 인터페이스를 구현하여 fly()메서드를 구현함
- extends를 통한 상속은 하나만 할 수 있고 implements를 통한 인터페이스는 다중 구현을 할 수 있기 때문에 둘이 함께 나온경우 extends로 상속을 먼저 작성하고 다음 인터페이스들을 구현을 하면 됨
package poly.ex6;
public class Dog extends AbstractAnimal {
@Override
public void sound() {
System.out.println("멍멍");
}
}
package poly.ex6;
public class Bird extends AbstractAnimal implements Fly {
@Override
public void sound() {
System.out.println("짹짹");
}
@Override
public void fly() {
System.out.println("새 날기");
}
}
package poly.ex6;
public class Chicken extends AbstractAnimal implements Fly {
@Override
public void sound() {
System.out.println("꼬끼오");
}
@Override
public void fly() {
System.out.println("닭 날기");
}
}
(4) SoundFlyMain
- AbstractAnimal 추상 클래스가 매개변수 타입인 soundAnimal()메서드와 Fly 인터페이스가 매개변수 타입인 flyAnimal()메서드를 생성
- 해당 추상클래스와 인터페이스를 구현한 클래스들의 인스턴스를 생성하고, 각 메서드의 인수로 입력하여 메서드들을 호출해보면 모든 클래스가 AbstractAnimal을 상속 받았으므로 soundAnimal()메서드는 모두 호출 가능하지만 flyAnimal()은 Bird와 Chicken만 호출 가능함
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);
}
private static void soundAnimal(AbstractAnimal animal) {
System.out.println("동물 소리 테스트 시작");
animal.sound();
System.out.println("동물 소리 테스트 종료");
}
private static void flyAnimal(Fly fly) {
System.out.println("날기 테스트 시작");
fly.fly();
System.out.println("날기 테스트 종료");
}
}
/* 실행 결과
동물 소리 테스트 시작
멍멍
동물 소리 테스트 종료
동물 소리 테스트 시작
짹짹
동물 소리 테스트 종료
동물 소리 테스트 시작
꼬끼오
동물 소리 테스트 종료
날기 테스트 시작
새 날기
날기 테스트 종료
날기 테스트 시작
닭 날기
날기 테스트 종료
*/
(5) 그림으로 설명
- soundAnimal(bird)를 호출하면 먼저 Bird 인스턴스를 찾고 호출한 메서드의 타입은 AbstractAnimal 타입이므로 AbstractAnimal에 있는 sound()메서드를 찾음
- 해당 메서드는 오버라이딩이 되어있으므로 Bird에 있는 sound() 메서드가 호출됨
- fly(bird)도 위와 동일하며 메서드의 인수로 들어온 변수가 Fly 타입이므로 Fly에 있는 fly()메서드를 찾고 해당 메서드는 Bird에 오버라이딩 되어있으므로 Bird에 있는 fly()메서드가 호출됨