https://surrealcode.tistory.com/52
이전에 다형성에 대해서 알아보았다.
이번엔 학습한 다형성이 왜 필요하고 사용해야 하는지, 그 장점에 대해 알아보도록 한다.
public class Dog {
public void sound(){
System.out.println("멍멍");
}
}
public class Cow {
public void sound(){
System.out.println("음매");
}
}
public class Cat {
public void sound(){
System.out.println("야옹");
}
}
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("동물 소리 테스트 종료");
}
위처럼 동물 소리를 내는 예제를 만들었다고 가정한다.
Main문을 보면
System.out.println("동물 소리 테스트 시작");
~~~.sound();
System.out.println("동물 소리 테스트 종료");
이 부분이 중복이 되어 자주 사용된다
이 부분의 중복을 제거할 순 없을까?
배열과 for문을 사용하려 하지만 각 동물마다의 타입이 달라 사용을 할 수 없다.
문제의 핵심은 타입이 다르다는 점이다. 반대로 얘기하자면 Dog, Cat, Cow가 모두 같은 타입을 사용할 수 있는 방법이 있다면, 메서드와 배열을 활용해서 코드의 중복을 제거할 수 있다는 것이다.
앞서 배운 다형성을 사용하기 위해 상속 관계를 사용한다.
Animal이라는 부모클래스를 만들고 sound()메서드를 정의한다.
public class Animal {
public void sound(){
System.out.println("동물 울음 소리");
}
}
public class Cat extends Animal{
@Override
public void sound() {
System.out.println("냐옹");
}
}
public class Cow extends Animal{
@Override
public void sound() {
System.out.println("음매");
}
}
public class Dog extends Animal{
@Override
public void sound() {
System.out.println("멍멍");
}
}
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("동물 소리 테스트 종료");
}
private static void soundAnimal의 매개변수를 보면 Animal 타입이다.
Main에서 자식 타입인 dog, cat cow를 매개변수로 집어넣으면
Animal 타입에 자식인 dog, cat, cow가 담기게 된다.
상속때 배웠던 것처럼 부모타입은 자식타입을 담을 수 있다.
그런데 왜 animal.sound();에서는 각각의 동물 울음 소리가 나는 걸까?
그 이유는 메서드 오버라이딩 때문인데, 앞서 말했듯이 메서드 오버라이딩을 했을 경우 오버라이딩 된 메서드가 우선권한을 갖기 때문이다.
이 코드의 핵심은 Animal animal 부분이다.
다형적 참조 덕분에 animal 변수는 자식인 Dog, Cat, Cow의 인스턴스를 참조할 수 있다.(부모는 자식을 담을 수 있기에)
메서드 오버라이딩 덕분에 animal.sound()를 호출해도 오버라이딩 된 각각의 동물 울음소리가 난다.
이제 위와 같은 문제는 배열과 for문을 활용해서 중복을 효과적으로 제거할 수 있게 된다.
public static void main(String[] args) {
Dog dog = new Dog();
Cat cat = new Cat();
Cow cow = new Cow();
Animal[] arr = {dog, cat, cow};
for (Animal animal : arr) {
System.out.println("동물소리테스트 시작");
animal.sound();
System.out.println("동물소리테스트 종료");
}
}
배열은 "같은 타입의 데이터를 나열할 수 있다."
Dog, Cat, Cow는 모두 "Animal의 자식이므로 Animal 타입이다."
Animal 타입의 배열을 만들고, 다형적 참조를 사용하면 된다.
Animal[] arr = new Animal[]{dog, cat, cow};
Animal[] arr = {dog, cat, cow};
//둘 다 동일한 array코드이다
다형적 참조 덕분에 Dog, Cat, Cow의 부모 타입인 Animal 타입으로 배열을 만들고, 각각을 배열에 포함했다.
새로운 동물이 추가 되어도 soundAnimal() 메서드는 코드 변경 없이 유지할 수 있다. 이렇게 할 수 있는 이유는 이 메서드가 Animal이라는 추상적인 부모를 참조하기 때문이다. 따라서 Animal을 상속받은 새로운 동물이 추가되어도, 이 메서드의 코드는 변경없이 유지할 수 있다.
**새로운 기능이 추가되었을 때 변하는 부분을 최소화 하는 것이 잘 짜여진 코드이다.
위의 예제에서도 사실 2가지의 문제가 있다.
1. Animal 클래스를 생성할 수 있는 문제
2. Animal 클래스를 상속 받는 곳에서 sound() 메서드 오버라이딩을 하지 않을 가능성
위의 문제는 "추상클래스"와 "추상메서드"를 사용하면 한번에 해결할 수 있다.
추상 클래스: Animal 와 같이 부모 클래스는 제공하지만, "실제 생성되면 안되는 클래스를 추상 클래스"라고 한다.
추상적인 개념만을 제공하는 클래스이다. 따라서 실체인 인스턴스가 존재하지 않는다.
abstract class AbstractAnimal{}
추상 클래스는 기존 클래스와 완전히 같다. 다만 "new AbstractAnimal()와 같이 직접 인스턴스를 생성하지 못하는 제약"이 추가된 것이다.
추상 메서드 : 부모 클래스를 상속받는 자식 클래스가 반드시 오버라이딩 해야하는 메서드를 부모 클래스에 정의할 수 있다.
public abstract void sound();
위와 같은 "추상 메서드가 하나라도 있는 클래스는 추상클래스로 선언"해야한다.
추상 메서드는 상속 받는 자식 클래스가 반드시 오버라이딩 해서 사용해야 한다.
추상 메서드를 오버라이드 하지 않은 채 클래스를 상속 받을 순 없다.
추상 메서드 또한 기존 메서드와 완전히 같다.
다음은 순수 추상 클래스이다.
public abstract class AbstractAnimal {
public abstract void sound();
public abstract void move();
}
순수 추상 클래스는 실행 로직을 전혀 가지고 있지 않다. 단지 다형성을 위한 부모 타입으로써 껍데기 역할만 제공할 뿐이다.
순수 추상 클래스의 특징
1. 인스턴스를 생성할 수 없다.
2. 상속시 자식은 "모든 메서드를 오버라이딩" 해야 한다.
3. 주로 다형성을 위해 사용된다.
4. 순수 추상 클래스를 상속받으면 모든 메서드를 구현해야 한다.
5. 추상 메서드는 바디 부분({})을 만들 수 없다.
위의 AbstractAnimal의 경우 sound(), move()라는 규격에 맞추어 구현을 해야한다.
이것은 우리가 일반적으로 얘기하는 인터페이스와 같이 느껴진다. 인터페이스는 분명한 규격이 있다. 이 규격에 맞추어 제품을 개발해야 연결된다. 순수 추상 클래스가 USB 인터페이스 규격이라고 한다면 USB인터페이스에 맞추어 마우스, 키보드 같은 연결 장치들을 구현할 수 있다.
이런 순수 추상 클래스의 개념은 프로그래밍에서 자주 사용된다.
자바는 더 편리하게 사용할 수 있도록 인터페이스라는 개념을 제공한다.
인터페이스
인터페이스는 class가 아니라 interface라는 키워드로 제공된다.
인터페이스는 public abstract 키워드를 생략할 수 있다.
순수 추상 클래스는 다음과 같은 특징을 가졌다.
1. 인스턴스를 생성할 수 없다 new AbstractAnimal() --> 이런거 불가
2. 상속 시 모든 메서드를 오버라이딩 해야한다.
3. 주로 다형성을 위해 사용된다.
인터페이스는 앞서 설명한 순수 추상 클래스와 같다. 여기에 약간의 편의 기능이 추가된다.
1.인터페이스의 메서드는 모두 public, abstract이다
2. 메서드에 public abstract를 생략할 수 있다.
3. 상속을 받을 시 extends가 아닌 implements를 사용한다.
4. ***인터페이스는 다중 구현(다중 상속)을 지원한다.***
"인터페이스에서 멤버 변수를 쓸 수 있으나 이 멤버변수는 public, static, final이 모두 포함되었다고 간주된다.
final은 변수의 값을 한번 설정하면 수정할 수 없다는 뜻이다."
public abstract class AbstractAnimal {
public abstract void sound();
public abstract void move();
}
public class Cat extends AbstractAnimal {
@Override
public void sound() {
System.out.println("냐옹");
}
@Override
public void move() {
System.out.println("고양이가 움직입니다.");
}
}
public class Cow extends AbstractAnimal {
@Override
public void sound() {
System.out.println("음매");
}
@Override
public void move() {
System.out.println("소가 움직입니다.");
}
}
public class Dog extends AbstractAnimal {
@Override
public void sound() {
System.out.println("멍멍");
}
@Override
public void move() {
System.out.println("개가 움직입니다.");
}
}
public static void main(String[] args) {
Dog dog = new Dog();
Cat cat = new Cat();
Cow cow = new Cow();
soundAnimal(cat);
soundAnimal(dog);
soundAnimal(cow);
}
//변하지 않는 부분
private static void soundAnimal(AbstractAnimal animal) {
System.out.println("동물 테스트 시작");
animal.sound();
animal.move();
System.out.println("동물 테스트 종료");
System.out.println();
}
인터페이스는 class 대신 interface로 선언해주고, 상속을 받을 땐 implements를 사용한다.
부모 클래스의 기능을 자식 클래스가 상속 받을 때는 "상속"받는다고 표현하지만 부모 인터페이스의 기능을 자식이 상속 받을 때는 상속이 아닌 인터페이스를 "구현"한다고 표현한다.
상속은 이름 그대로 부모의 기능을 물려받는 것이 목적이다. 하지만 인터페이스는 모든 메서드가 추상 메서드이다. 따라서 물려받을 수 있는 기능이 없고, 오히려 "인터페이스에 정의한 모든 메서드를 자식이 오버라이딩 해서 기능을 구현해야 한다." 따라서 구현한다고 표현한다.
인터페이스를 사용해야 하는 이유
제약 : 인터페이스의 메서드를 반드시 구현하라는 규약을 주는 것이다.
다중 구현 : 인터페이스는 부모를 여러명 두는 다중 구현이 가능하기 때문에 사용한다.
public abstract class AbstractAnimal {
public abstract void sound();
public abstract void move();
public void hello(){
System.out.println("hello"); //이거 추상 아닌데 왜?
}
}
public interface InterfaceAnimal {
void sound(); //public abstrace
void move(); //public abstrace
public void hello(){
System.out.println("hello");
}
}
추상클래스와 인터페이스의 차이점은 추상클래스에선 매서드를 구현할 수 있지만. 인터페이스에서는 구현된 메서드는 제공할 수 없다.
그렇기에 아래 코드는 컴파일 에러가 발생한다.
"좋은 프로그램은 제약이 있는 프로그램이다"
인터페이스 : 굉장히 강하게 안에 있는 기능을 구현하라고 만든 것
추상클래스 : 일부는 구현을 해야하나(추상클래스), 일부는 구현하지 않아도 되는 것(만들어진 메서드)
앞서 언급했듯 인터페이스는 다중 구현이 가능하다.
다중 상속의 문제는
아래 그림처럼 AirplaneCar가 Airplane과 Car를 받을때 move는 과연 어떠한 move를 사용해야할지의 문제가 발생한다.
하지만 인터페이스는 모두 추상 메서드로 이루어져 있기 때문에 이러한 문제들이 해결될 수 있다.
어짜피 move() 메서드는 구현이 안되어 있기 때문이다.
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 static void main(String[] args) {
InterfaceA childA = new Child();
childA.methodA();
childA.methodCommon();
System.out.println();
InterfaceB childB = new Child();
childB.methodB();
childB.methodCommon();
}
또한 추상 클래스와 인터페이스를 동시에상속과 구현이 동시에 이루어지기도 한다.
public abstract class AbstractAnimal {
public abstract void sound();
public void move(){
System.out.println("동물이 이동합니다.");
}
}
public interface Fly {
void fly();
}
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 Dog extends AbstractAnimal{
@Override
public void sound() {
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);
}
private static void soundAnimal(AbstractAnimal animal) {
System.out.println("동물 테스트 시작");
animal.sound();
// animal.move();
System.out.println("동물 테스트 종료");
System.out.println();
}
private static void flyAnimal(Fly fly){
System.out.println("날기 테스트 시작");
fly.fly();
System.out.println("날기 테스트 종료");
}
}
'공부 > Java' 카테고리의 다른 글
자바 다형성 3편 (Polymorphism) (1) | 2024.09.05 |
---|---|
자바 좋은 객체 지향 프로그래밍이란? (1) | 2024.09.05 |
자바 다형성 1편 (Polymorphism) (0) | 2024.09.04 |
자바 상속이란 (1) | 2024.09.03 |
자바 final (1) | 2024.09.02 |