Post

Java 문법 (12) - 다형성2

Java 문법 (12) - 다형성2




다형성 활용


다형성을 사용하지 않은 코드

다형성을 사용하지 않은 코드를 변경해서 다형성을 적용시켜보겠습니다. 단순한 동물 소리를 테스트하는 코드입니다.

1
2
3
4
5
6
7
8
// Dog.java
package poly.ex1;  
  
public class Dog {  
    public void sound(){  
        System.out.println("멍멍");  
    }  
}
1
2
3
4
5
6
7
8
// Cat.java
package poly.ex1;  
  
public class Cat {  
    public void sound(){  
        System.out.println("야옹");  
    }  
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// AnimalSountMain.java
package poly.ex1;  
  
public class AnimalSountMain {  
    public static void main(String[] args) {  
        // 다형성이 없을 때 중복되는 코드들  
        Dog dog = new Dog();  
        Cat cat = new Cat();  
        
		// 중복되는 코드
        System.out.println("동물소리 테스트 시작");  
        dog.sound();  
        System.out.println("동물소리 테스트 종료");  
  
        System.out.println("동물소리 테스트 시작");  
        cat.sound();  
        System.out.println("동물소리 테스트 종료");  
    }  
}

다형성을 사용하지 않으면 새로운 동물을 추가할 때, main에서 변경해야할 부분들이 많고, 중복되는 코드가 많습니다. 중복을 해결하고자 메서드, 반복문을 사용하려고 해도 타입이 다르기 때문에 사용할 수 없습니다.

다형성 적용

Animal 이라는 클래스를 만들어서 다형성을 사용해보겠습니다.

1
2
3
4
5
6
7
8
9
10
// Animal.java
package poly.ex2;  
  
public class Animal {  
    // 동물은 추상적인 개념이라 실제 인스턴스가 생기는 것은 어색함.  
    // 따라서 추상 클래스가 필요로 함  
    public void sound(){  
        System.out.println("동물소리 테스트 시작");  
    }  
}
1
2
3
4
5
6
7
8
9
10
// Dog.java
package poly.ex2;  
  
public class Dog extends Animal{  
    @Override  
    public void sound(){  
  
        System.out.println("멍멍");  
    }  
}
1
2
3
4
5
6
7
8
9
package poly.ex2;  
  
public class Cat extends Animal{  
    @Override  
    public void sound(){  
  
        System.out.println("야옹");  
    }  
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package poly.ex2;  
  
public class AnimalSoundMain2 {  
    public static void main(String[] args) {  
	    // 새로운 동물도 new Caw()만 추가해주면 가능
        Animal[] animals = {new Dog(), new Cat(), new Caw()};  
  
        for(Animal animal: animals){  
            soundAnimal(animal);  
        }  
    }  
  
    private static void soundAnimal(Animal animal){  
        System.out.println("동물소리 테스트 시작");  
        animal.sound();  
        System.out.println("동물소리 테스트 종료");  
    }  
}

다형성을 사용해서 Animal 이라는 부모 클래스를 만들어 타입을 같게 만들어 줌으로써 중복되는 코드들을 제거할 수 있습니다. 새로운 동물을 추가할 때에도 기존과 달리 main에서 객체만 생성해주면 됩니다.




추상 클래스


추상 클래스

예제에서 Animal 클래스는 동물이라는 추상적인 개념입니다. 이 클래스를 직접 인스턴스를 생성해서 사용할 일은 없습니다. 따라서 인스턴스를 생성할 수 없게 제약을 주는 것이 추상 클래스입니다.

추상 클래스는 클래스를 선언할 때 앞에 abstract 키워드를 붙여주면 됩니다. 기존 클래스와 완전히 같지만, 직접 인스턴스를 생성하지 못하는 제약만 추가된 클래스입니다. new AbstractAnimal()으로 인스턴스를 만들려고 하면 컴파일 오류가 발생합니다.

추상 메서드

부모 클래스를 상속받는 자식 클래스가 반드시 오버라이딩 해야 하는 메서드를 부모 클래스에 정의할 수 있습니다. 이를 추상 메서드라고 합니다.

이름 그대로 추상적인 개념을 제공하는 메서드로, 실체가 존재하지 않아서 메서드 바디가 없습니다. 선언할 때 메서드 앞에 abstract 키워드를 붙여주면 됩니다.

추상 클래스, 메서드 예제

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// AbstractAnimal.java
package poly.ex3;  
    // 추상 클래스는 클래스에서 인스턴스를 생성하지 못한다는 제약이 추가되는 것
public abstract class AbstractAnimal {  
    // 추상 메서드 :
    // 바디를 가질 수 없음  
    // 추상 메서드를 하나라도 가지고 있으면 그 클래스도 추상 클래스여야 함  
    // 추상 메서드는 무조건 오버라이딩해야 함  
    // 오버라이딩 하지 않으면 자식도 추상 클래스가 되어야 함  
    public abstract void sound();  
  
    public void move(){  
        System.out.println("동물이 움직입니다.");  
    }  
}
1
2
3
4
5
6
7
8
9
10
// Dog.java
package poly.ex3;  
  
public class Dog extends AbstractAnimal{  
	// 추상 메서드 오버라이딩
    @Override  
    public void sound() {  
        System.out.println("멍멍");  
    }  
}

순수 추상 클래스

예제의 AbstractAnimal에서 move()도 추상 메서드로 만들게 되면, AbstractAnimal 클래스의 모든 메서드가 추상 메서드가 됩니다. 이런 클래스를 순수 추상 클래스라고 합니다.

1
2
3
4
5
 public abstract class AbstractAnimal {
     public abstract void sound();
     public abstract void move();

}

순수 추상 클래스는 코드를 실행할 바디 부분이 전혀 없습니다. 주로 다형성을 위한 부모 타입으로 껍데기 역할만 제공합니다. 상속시 자식은 모든 메서드를 오버라이딩 해야한다는 특징이 있는데, 어떤 규격을 지켜서 구현해야하는 것 처럼 느껴집니다.

USB 인터페이스 처럼 규격을 맞추어 제품을 개발해야하는 것 처럼, 순수 추상 클래스를 USB 인터페이스 규격처럼 사용할 수 있습니다. 자바는 순수 추상 클래스를 더 편리하게 사용할 수 있도록 인터페이스라는 개념을 제공합니다.




인터페이스


인터페이스는 추상클래스의 특징에 더해서 편의 기능이 추가됩니다.

  • 인스턴스를 생성할 수 없다
  • 상속시 모든 메서드를 오버라이딩 해야 한다
  • 주로 다형성을 위해 사용된다
  • 인터페이스의 메서드는 모두 public, abstract이다.
  • 메서드에 public abstract를 생략할 수 있다. 생략 권장
  • 인터페이스는 다중 구현을 지원한다.
  • 인터페이스의 멤버변수는 public static final이 모두 포함되었다고 간주한다. 생략가능 (상수)
1
2
3
4
5
6
// InterfaceAnimal.java
public interface InterfaceAnimal {
     int MAX_COUNT = 100; // public static final 생략
     void sound(); // public abstract 생략
     void move(); // public abstract 생략
 }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// Dog.java
package poly.interfaceEx1;  
  
public class Dog implements InterfaceAnimal {  
    @Override  
    public void sound() {  
        System.out.println("멍멍");  
    }  
  
    @Override  
    public void move() {  
        System.out.println("강아지가 움직입니다.");  
    }  
}

인터페이스를 상속 받을 때는 extends 대신에 implements, 구현 이라는 키워드를 사용합니다. 상속은 부모의 기능을 물려받는 것이 목적이지만, 인터페이스는 물려받을 기능이 없고 모든 메서드를 오버라이딩 해서 기능을 구현해야 하기 때문에 구현이라고 표현합니다.

인터페이스를 사용해야 하는 이유

순수 추상 클래스를 만들어도 되고 인터페이스를 만들어도 되는데, 인터페이스를 사용하는 이유가 있습니다.

  • 제약: 인터페이스를 만드는 이유는 인터페이스를 구현하는 곳에서 인터페이스의 메서드를 반드시 구현해라는 제약을 주는 것입니다. 순수 추상 클래스의 경우 미래에 누군가 그곳에 실행 가능한 메서드를 끼워 넣을 수 있습니다. 그렇게 되면 추가된 기능을 자식 클래스에서 구현하지 않을 수도 있고, 또한 더는 순수 추상 클래스가 아니게 됩니다. 인터페이스는 제약을 주어서 이러한 문제를 원천 차단합니다.
  • 다중 구현: 자바에서 클래스 상속은 부모를 하나만 지정할 수 있습니다. 반면에 인터페이스는 부모를 여러명 두는 다중 구현이 가능합니다.

인터페이스 다중 구현

자바가 다중 상속을 지원하지 않는 이유

다이아몬드 문제 때문입니다. 만약 Airplane과 Car를 상속 받는 AirplaneCar라는 클래스가 있을 때, Airplane과 Car 모두 move()라는 기능을 가지고 있다면, AirplaneCar 입장에서 move를 호출할 때 어떤 부모의 move를 사용해야 할지 애매한 문제가 발생합니다. 이를 다이아몬드 문제라고 합니다.

인터페이스 다중 구현을 지원하는 이유

인터페이스는 모두 추상 메서드로 이루어져 있기 때문입니다. InterfaceA, InterfaceB 모두 method()라는 같은 함수를 가지고 있고, Child는 두 인터페이스를 모두 구현합니다. 이 때 Child입장에서 method()를 불러도 어차피 오버라이딩에 의해서 Child에서 구현한 method()가 호출되기 때문에 문제가 발생하지 않습니다.

1
2
3
4
5
// InterfaceA.java
public interface InterfaceA {  
    void method();  
    void methodA();  
}
1
2
3
4
5
// InterfaceB.java
public interface InterfaceB {  
    void method();  
    void methodB();  
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// Child.java 
public class Child implements InterfaceA, InterfaceB {  
    @Override  
    public void methodA() {  
        System.out.println("Child.methodA");  
    }  
  
    @Override  
    public void methodB() {  
        System.out.println("Child.methodB");  
    }  
	// 오버라이딩으로 Child의 method가 우선순위를 갖기 때문에 문제되지 않음
    @Override  
    public void method() {  
        System.out.println("Child.methodCommon");  
    }  
}

클래스 상속 + 인터페이스 구현

1
public class Child extends ParentsClass implements InterfaceA, InterfaceB{}

exdends를 통한 상속은 하나만 할 수 있고, implements를 통한 인터페이스는 다중 구현할 수 있습니다. 둘이 함께 나온 경우 extends가 먼저 나와야 합니다.