Java

[점프 투 자바] 5장 객체지향 프로그래밍2

seonggu 2024. 1. 1. 17:54

상속

자바에는 자식 클래스가 부모 클래스의 기능을 그대로 물려받을 수 있는 상속(inheritance)기능이 있다.

Animal 클래스를 상속하는 Dog 클래스를 만들어보자

class Animal {
    String name;

    void setName(String name) {
        this.name = name;
    }
}

class Dog extends Animal { // Animal 클래스 상속
}

public class Sample {
    public static void main(String[] args) {
				Dog dog = new Dog();
				dog.setName("poppy");
        System.out.println(dog.name);
    }
}

 

클래스를 상속하기 위해서는 extends라는 키워드를 사용함.

Dog 클래스에 객체 변수인 name과 메서드인 setName은 만들지 않았지만

Animal 클래스를 상속했기 때문에 예제에서 보듯이 그대로 사용가능.

 

  • 자식 클래스의 기능 확장하기
class Animal {
    String name;

    void setName(String name) {
        this.name = name;
    }
}

class Dog extends Animal {
    void sleep() {
        System.out.println(this.name+" zzz");
    }
}

public class Sample {
    public static void main(String[] args) {
        Dog dog = new Dog();
        dog.setName("poppy");
        System.out.println(dog.name);
        dog.sleep();
    }
}

 

  • IS-A 관계란?

Dog 클래스는 Animal 클래스를 상속했다.

즉, Dog는 Animal의 하위개념이라고 할 수 있음.

Dog는 Animal에 포함되기 때문에 ‘개(Dog)는 동물(Animal)’이라고 할 수 있다.

이와 같이 말할 수 있는 관계를 IS-A 관계(상속관계)라고 한다.

자식 클래스의 객체는 부모 클래스의 자료형인 것 처럼 사용할 수 있다.

그리하여 다음과 같은 것도 가능함.

Animal dog = new Dog();

 

🔴주의할점

Dog 객체를 Animal 자료형으로 사용할 경우 Dog 클래스에만 존재하는 sleep 메서드는 사용할 수 없음.

반대의 경우는 불가능함 .

Dog dog = new Animal();

 

✅ Object 클래스란?

자바에서 만드는 모든 클래스는 Object 클래스를 상속받는다.

사실 우리가 만든 Animal 클래스는 다음 코드와 기능적으로 완전 동일하다.

하지만 굳이 다음 코드처럼 Object 클래스를 상속하도록 코딩하지 않아도.

자바에서 만들어지는 모든 클래스는 Object 클래스를 자동으로 상속받게끔 되어 있음.

 class Animal extends Object {
    String name;

    void setName(String name) {
        this.name = name;
    }
}

따라서 모든 객체는 Object 자료형도 될 수 있음.

Object animal = new Animal();  // Animal is a Object
Object dog = new Dog();  // Dog is a Object

 

  • 메서드 오버라이딩

Dog 클래스를 좀 더 구체화 시키는 HouseDog 클래스를 만들어보자.

class Animal {
    String name;

    void setName(String name) {
        this.name = name;
    }
}

class Dog extends Animal {
    void sleep() {
        System.out.println(this.name+" zzz");
    }
}

class HouseDog extends Dog {
}

public class Sample {
    public static void main(String[] args) {
        HouseDog houseDog = new HouseDog();
        houseDog.setName("happy");
        houseDog.sleep();  // happy zzz 출력
    }
}

sleep 메서드가 호출되어 이와 같은 결과가 출력된다.

 

HouseDog 클래스로 만들어진 객체들을 sleep 메서드로 호출시 ‘happy zzz’가 아닌

‘happy zzz in house’로 출력하고 싶다.

 

HouseDog클래스를 수정해보자

class Animal {
    String name;

    void setName(String name) {
        this.name = name;
    }
}

class Dog extends Animal {
    void sleep() {
        System.out.println(this.name + " zzz");
    }
}

class HouseDog extends Dog {
    void sleep() {
        System.out.println(this.name + " zzz in house");
    }
}

public class Sample {
    public static void main(String[] args) {
        HouseDog houseDog = new HouseDog();
        houseDog.setName("happy");
        houseDog.sleep();  // happy zzz in house 출력
    }
}

Dog 클래스에 있는 sleep 메서드를 HouseDog 클래스에 다시 구현하여 이와 같이 원하던 결괏값을 얻을 수 있다.

 

HouseDog 클래스에 Dog 클래스와 동일한 형태의 sleep 메서드를 구현하면

HouseDog 클래스의 sleep 메서드가 우선순위를 갖게 되어 HouseDog의 sleep 메서드가 호출되게 된다.

 

위와 같이 부모 클래스의 메서드를 자식 클래스가 동일한 형태로 또다시 구현하는 행위를

메서드 오버라이딩(method overriding, 메서드 덮어쓰기)

 

  • 메서드 오버로딩

HouseDog 클래스에 다음과 같은 메서드를 추가해보자.

변경이 아니고 추가임!!

void sleep(int hour) {
    System.out.println(this.name+" zzz in house for " + hour + " hours");
}

 

sleep이라는 메서드가 있지만 sleep 메서드를 또 생성할 수 있다.

단, 입력 항목이 다를 경우(매개 변수)만 가능하다.

새로 만든 sleep 메서드는 입력 항목의 hour라는 int 자료형이 추가되었다.

이렇듯 입력 항목이 다른 경우 동일한 이름의 메서드를 만들 수 있는데

이를 메서드 오버로딩(method overloading)이라고 부른다.

 

확인을 위해 아래의 코드를 실행해보자

class Animal {
    String name;

    void setName(String name) {
        this.name = name;
    }
}

class Dog extends Animal {
    void sleep() {
        System.out.println(this.name + " zzz");
    }
}

class HouseDog extends Dog {
    void sleep() {
        System.out.println(this.name + " zzz in house");
    }

    void sleep(int hour) {
        System.out.println(this.name + " zzz in house for " + hour + " hours");
    }
}

public class Sample {
    public static void main(String[] args) {
        HouseDog houseDog = new HouseDog();
        houseDog.setName("happy");
        houseDog.sleep();  // happy zzz in house 출력
        houseDog.sleep(3);  // happy zzz in house for 3 hours 출력
    }
}

 

  • 다중상속

클래스 가 동시에 하나 이상의 클래스를 상속받는 것을 뜻함.

C++, python은 지원함

자바는 다중 상속을 지원하지 않는다.

만약 자바가 다중상속을 지원한다면 아래와 같은 코드가 만들어질 수 있음.

class A {
    public void msg() {
        System.out.println("A message");
    }
}

class B {
    public void msg() {
        System.out.println("B message");
    }
}

class C extends A, B {
    public void static main(String[] args) {
        C test = new C();
        test.msg();
    }
}

 

다중상속을 지원한다고 가정하고 C클래스가 A,B 클래스를 동시에 상속하도록 하였다.

main 메서드에서 test.msg();를 실행하면

A클래스의 msg(), B클래스의 msg() 중 어떤 것을 실행해야 할까?

자바는 이러한 불명확한 부분을 애초에 없앴다.

파이썬과 같이 다중 상속을 지원하는 언어들은 이렇게 동일한 메서드를 상속받는 경우 우선순위를 정하는 규칙이 있다.

 

 

생성자

지금까지 만든 클래스들이 준비되고 main메서드를 다음과 같이 수정한 후 실행해보자.

(... 생략 ...)

public class Sample {
    public static void main(String[] args) {
        HouseDog dog = new HouseDog();
        System.out.println(dog.name); // null
    }
}

name 변수에 아무런 값도 설정하지 않았기 때문에 null이 출력됨.

이렇듯 HouseDog 클래스는 코딩하기에 따라 객체 변수 name에 값을 설정할 수도 있고

설정하지 않을 수도 있다.

 

중요중요

🔴 name이라는 객체 변수에 값을 무조건 설정해야만 객체가 생성될 수 있도록 강제할 수 있는 방법은?

생성자를 이용하기.

 

HouseDog 클래스를 수정해보자.

(... 생략 ...)

class HouseDog extends Dog {
    HouseDog(String name) {
        this.setName(name);
    }

    void sleep() {
        System.out.println(this.name + " zzz in house");
    }

    void sleep(int hour) {
        System.out.println(this.name + " zzz in house for " + hour + " hours");
    }
}

(... 생략 ...)

메서드명이 클래스명과 동일하고 리턴 자료형을 정의하지 않는 메서드를 생성자라고 한다.

 

생성자의 규칙

  1. 클래스명과 메서드명이 같다.
  2. 리턴 타입을 정의하지 않는다.(void도 사용하지 않는다.)

 

생성자는 객체가 생성될 때 호출된다.

즉, 생성자는 다음과 같이 new 키워드가 사용될 때 호출된다.

new 클래스명(입력인수, ...)

 

생성자는 메서드와 마찬가지로 다양한 입력을 받을 수 있다.

우리가 HouseDog 클래스에 만든 생성자는 다음과 같이 입력값으로 문자열을 필요로 하는 생성자이다.

HouseDog(String name) {
	this.setName(name);
}

 

따라서 다음과 같이 new 키워드로 객체를 만들 때 문자열을 전달해야만 한다.

HouseDog dog = new HouseDog("hapyy"); 
// 생성자 호출 시 문자열을 전달해야함

 

만약 전달하지 않으면 오류가 발생하는데

이는 객체 생성 방법이 생성자의 규칙과 맞지 않기 때문이다.

생성자가 선언된 경우 생성자의 규칙대로만 객체를 생성할 수 있다.

 

이렇듯 생성자를 사용하면 setName(”happy”) 와 같은 필수적인 행동을 객체 생성시에 제어할 수 있다.

 

  • 디폴트 생성자

디폴트 생성자(default constructor)를 알아보기 위해 다음 코드를 먼저 살펴보자

class Dog extends Animal {
    void sleep() {
        System.out.println(this.name + " zzz");
    }
}

그리고 다음코드

class Dog extends Animal {
    Dog() {
    }

    void sleep() {
        System.out.println(this.name + " zzz");
    }
}

 

첫 번째와 두 번째 코드의 차이점은?

두 번째는 생성자가 구현되어 있다.

생성자의 입력 항목이 없고 생성자 내부에 아무 내용이 없는 이와 같은 생성자를 디폴트 생성자 라고 한다.

디폴트 생성자를 구현하면 new Dog()로 Dog 클래스의 객체가 만들어질 때 디폴트 생성자 Dog() 실행

 

클래스에 생성자가 하나도 없다면 컴파일러는 자동으로 이와 같은 디폴트 생성자를 추가함.

하지만 하나라도 구현되어 있으면 컴파일러는 디폴트 생성자를 추가하지 않는다.

 

하여 HouseDog 클래스에 name을 입력으로 받는 생성자를 만든 후에 new HouseDog()는 사용할 수 없다.

이미 생성자를 만들었으므로 컴파일러가 디폴트 생성자를 추가하지 않기 때문.

new HouseDog() 도 가능하게 하려면 디폴트 생성자를 직접 작성하여 추가해야 한다.

 

 

생성자 오버로딩

메서드에 오버로딩이 있는 것과 마찬가지로 생성자에도 오버로딩이 있다.

다음과 같이 입력항목이 다른 생성자를 만들 수 있다.

class Animal {
    String name;

    void setName(String name) {
        this.name = name;
    }
}

class Dog extends Animal {
    void sleep() {
        System.out.println(this.name + " zzz");
    }
}

class HouseDog extends Dog {
    HouseDog(String name) {
        this.setName(name);
    }

    HouseDog(int type) {
        if (type == 1) {
            this.setName("yorkshire");
        } else if (type == 2) {
            this.setName("bulldog");
        }
    }

    void sleep() {
        System.out.println(this.name + " zzz in house");
    }

    void sleep(int hour) {
        System.out.println(this.name + " zzz in house for " + hour + " hours");
    }
}

public class Sample {
    public static void main(String[] args) {
        HouseDog happy = new HouseDog("happy");
        HouseDog yorkshire = new HouseDog(1);
        System.out.println(happy.name);  // happy 출력
        System.out.println(yorkshire.name);  // yorkshire 출력
    }
}

이와 같이 입력항목이 다른 생성자를 여러 개 만들 수 있는데

이런 것을 생성자 오버로딩(constructor overloading)이라고 한다.

 

 

인터페이스

  • 인터페이스는 왜 필요한가?

어떤 동물원의 사육사가 하는 일이다.

난 동물원(zoo)의 사육사(zookeeper)이다.
육식동물(predator)이 들어오면 난 먹이를 던져준다(feed).
호랑이(tiger)가 오면 사과(apple)를 던져준다.
사자(lion)가 오면 바나나(banana)를 던져준다.

 

코드로 구현해보기.

class Animal {
    String name;

    void setName(String name) {
        this.name = name;
    }
}

class Tiger extends Animal {
}

class Lion extends Animal {
}

class ZooKeeper {
    void feed(Tiger tiger) {  // 호랑이가 오면 사과를 던져 준다.
        System.out.println("feed apple");
    }

    void feed(Lion lion) {  // 사자가 오면 바나나를 던져준다.
        System.out.println("feed banana");
    }
}

public class Sample {
    public static void main(String[] args) {
        ZooKeeper zooKeeper = new ZooKeeper();
        Tiger tiger = new Tiger();
        Lion lion = new Lion();
        zooKeeper.feed(tiger);  // feed apple 출력
        zooKeeper.feed(lion);  // feed banana 출력
    }
}

Dog 클래스와 마찬가지로 이번엔 Animal을 상속한 Tiger와 Lion이 등장했다.

그리고 ZooKeeper 클래스를 정의하였다.

Zookeeper 클래스는 tiger가 왔을 때, lion이 왔을 때, 각각 다른 feed 메서드가 호출된다.

실행하면 아래와 같은 결과가 나옴

feed apple
feed banana

 

Tiger와 Lion 뿐이라면 Zookeeper 클래스는 더 이상 할 일이 없겠지만.

Crocodile, Leopard 등이 계속 추가된다면 ZooKeeper 클래스가 추가될 때마다

매번 다음과 같은 feed 메서드를 추가해야 한다.

(... 생략 ...)

class ZooKeeper {
    void feed(Tiger tiger) {
        System.out.println("feed apple");
    }

    void feed(Lion lion) {
        System.out.println("feed banana");
    }

    void feed(Crocodile crocodile) {
        System.out.println("feed strawberry");
    }

    void feed(Leopard leopard) {
        System.out.println("feed orange");
    }
}

(... 생략 ...)

이렇게 클래스가 추가될 때마다 메서드를 추가해야 한다면

Zookeeper 클래스가 복잡해진다.

이를 위해 인터페이스를 도입해보자.

 

  • 인터페이스 작성하기

다음과 같이 코드 상단에 Predator

interface Predator {
}

class Animal {
    String name;

    void setName(String name) {
        this.name = name;
    }
}

(... 생략 ...)

이 코드와 같이 인터페이스는 interface 키워드로 작성하기.

참고로 class와 같이 interface도 Predator.java와 같이 단독 파일로 저장하는 것이 일반적이다.

 

Tiger, Lion 클래스는 작성한 인터페이스를 구현하도록 다음과 같이 implements라는 키워드를 사용해서 수정하자.

(... 생략 ...)

class Tiger extends Animal implements Predator {
}

class Lion extends Animal implements Predator {    
}

(... 생략 ...)

 

또한 ZooKeeper 클래스도 변경해주자.

(... 생략 ...)

class ZooKeeper {
    void feed(Predator predator) {
        System.out.println("feed apple");
    }
}

(... 생략 ...)

feea 메서드의 입력으로 Tiger, Lion 각각 필요했지만 이것을

Predator라는 인터페이스로 대체할 수 있게 되었다.

 

tiger, lion은 각각 Tiger, Lion의 객체이기도 하지만 Predator 인터페이스의 객체이기도 하기 때문에

다음과 같이 Predator를 자료형으로 사용할 수 있는 것이다.

IS-A 관계도 성립된다. Tiger is a Predator’, ‘Lion is a Predator’가 성립된다.

 

tiger: Tiger 클래스의 객체이자 Predator 인터페이스의 객체

lion: Lion 클래스의 객체이자 Predator 인터페이스의 객체

 

중요 클래스(ZooKeeper)를 작성하는 시점에서 클래스(Animal)의 구현체(Tiger, Lion)가 몇 개가 될지 알 수가 없으므로

인터페이스(Predator)를 정의하여 인터페이스를 기준으로 메서드를(feed)를 만드는 것이 효율적이다.

 

  • 인터페이스의 메서드

그런데 앞서 살펴몬 ZooKeeper 클래스에 feed 메서드를 보면

객체가 어떤 것이 오냐에 따라 다르게 문자열을 출력했다.

하여 그것과 맞게 코드를 수정해주자.

 

Predator 인터페이스에 다음과 같은 getFood 메서드를 추가해보자

interface Predator {
    String getFood();
}

(... 생략 ...)

인터페이스의 메서드는 메서드의 이름과 입출력에 대한 정의만 있고 내용이 없다.

 

그 이유는 인터페이스는 ‘규칙’이기 때문이다.

즉, getFood 메서드는 인터페이스를 implements한 클래스들이 강제적으로 구현해야하는 규칙이 된다.

이제 getFood 메서드를 추가하면 컴파일 오류가 발생한다

 

오류를 해결하려면 다음과 같이 Tiger, Lion 클래스를 구현해주자.

(... 생략 ...)

class Tiger extends Animal implements Predator {
    public String getFood() {
        return "apple";
    }
}

class Lion extends Animal implements Predator {
    public String getFood() {
        return "banana";
    }
}

(... 생략 ...)

 

이어서 ZooKeeper 클래스도 변경해주자

(... 생략 ...)

class ZooKeeper {
    void feed(Predator predator) {
        System.out.println("feed "+predator.getFood());
    }
}

(... 생략 ...)

 

feed 메서드가 feed apple 대신 “feed “ + predator.getFood() 를 출력하도록 코드를 수정했다.

predator.getFood() 메서드를 호추랗면 Predator 인터페이스를 구현한 구현체(Tiger, Lion)의 getFood() 메서드가 호출된다.

feed apple
feed banana

 

  • 인터페이스 더 파고들기

인터페이스가 왜 필요한지 이해하고 가자

동물 클래스의 종류만큼 feed 메서드가 필요했던 ZooKeeper 클래스를 Predator 인터페이스를 이용하여 구현했더니 단 한개의 feed 메서드로 구현이 가능했다.

 

🔴 여기서 중요한 점

메서드의 개수가 줄어들었다는 점이 아니라

ZooKeeper 클래스가 동물 클래스에 의존적인 클래스에서

동물 클래스와 상관없는 독립적인 클래스가 되었다는 점이다.

이것이 인터페이스의 핵심이다.

 

USB 포트. 이 포트에 연결할 수 있는 기기들은 하드디스크, 메모리 스틱, 스마트폰 등 다양하다.

USB 포트가 물리적 세계의 인터페이스라고 할 수 있다.

USB 포트의 규격만 알면 어떤 기기도 연결할 수 있다.

또, 컴퓨터는 USB 포트만 제공하고 어떤 기기가 연결되는지 신경 쓸 필요가 없다.

바로 이 점이 자바의 인터페이스와 매우 비슷하다.

앞서 만든 ZooKeeper 클래스가 어떤 동물 클래스(Tiger, Lion, ...)이든 상관하지 않고 feed 메서드를 구현한 것처럼 말이다.

 

 

✅ 상속과 인터페이스

Predator 인터페이스 대신 Animal 클래스에 getFood 메서드를 추가하고 Tiger, Lion 등에서 getFood() 메서드를 오버라이딩 한 후 Zookeeper의 feed 메서드가 Predator 대신 Animal을 입력 자료형으로 사용해도 동일한 효과를 거둘 수 있다.

하지만 상속은 자식 클래스가 부모 클래스의 메서드를 오버라이딩 하지 않고

사용할 수 있기 때문에 해당 메서드를 반드시 구현해야한다는 ‘강제성’을 갖지 못한다.

→ 아하 그러니까 자식 클래스에서 오버라이드 하지 않아도 되니까 !!

그래서 상황에 맞게 상속을 사용할 것인지 인터페이스를 구현할 것인지 결정해야한다.

인터페이스는 반드시 구현해야한다는 강제성!!!!

 

  • 디폴트 메서드

자바 8버전 부터 디폴트 메서드(default method)를 사용할 수 있다.

인터페이스의 메서드는 구현체를 가질 수 없지만

디폴트 메서드를 사용하면 구현된 형태의 메서드를 가질 수 있다.

interface Predator {
    String getFood();

    default void printFood() {
        System.out.printf("my food is %s\\n", getFood());
    }
}

메서드명 가장 앞에 default 라고 표기해야한다.

이렇게 디폴트 메서드를 구현하면 Predator 인터페이스를 구현한 Tiger, Lion 등의 실제 클래스는 printFood를 구현하지 않아도 된다.

그리고 디폴트 메서드는 오버라이드 가능하다.

즉, printFood 메서드를 실제 클래스에서 다르게 구현하여 사용할 수 있다

 

 

다형성

객체 지향 프로그래밍의 특징 중에는 다형성(polymorphism)이 있다.

다형성은 무엇이고 왜 필요할까?

아래 코드 추가하기.

interface Predator {
    (... 생략 ...)
}

class Animal {
    (... 생략 ...)
}

class Tiger extends Animal implements Predator {
    (... 생략 ...)
}

class Lion extends Animal implements Predator {
   (... 생략 ...)
}

class ZooKeeper {
    (... 생략 ...)
}

class Bouncer {
    void barkAnimal(Animal animal) {
        if (animal instanceof Tiger) {
            System.out.println("어흥");
        } else if (animal instanceof Lion) {
            System.out.println("으르렁");
        }
    }
}

public class Sample {
    public static void main(String[] args) {
        Tiger tiger = new Tiger();
        Lion lion = new Lion();

        Bouncer bouncer= new Bouncer();
        bouncer.barkAnimal(tiger);
        bouncer.barkAnimal(lion);
    }
}

Bounder 클래스를 보면 barkAnimal 메서드는 입력으로 받은 객체가 Tiger의 객체라면 어흥, Lion의 객체라면 으르렁을 출력함.

 

instanceof는 어떤 객체가 특정 클래스의 객체인지를 조사할 때 사용되는 자바 내장 명령어이다.

여기서 animal instanceof Tiger는 ‘animal 객체는 Tiger 클래스로 만들어진 객체인가?’를 묻는 조건문이고, 참이면 어흥을 출력하게 되는 것이다.

 

🔴 IS-A 관계 되짚어 보기

barkAniaml 메서드의 입력 자료형은 Tiger나 Lion이 아닌 Animal 이다.

하지만 BarkAnimal 메서드를 호출할 때 tiger 또는 lion객체를 전달할 수 있다.

이게 가능한 이유는 두 클래스가 Animal 이라는 부모 클래스를 상속한 자식 클래스 이기 때문이다.

자식 클래스에 의해서 만들어진 객체는 언제나 부모 클래스의 자료형을 사용할 수 있다.

하여 다음과 같이 코딩이 가능하다.

Animal tiger = new Tiger();  // Tiger is a Animal
Animal lion = new Lion();  // Lion is a Animal

 

Bouncer 클래스의 barkAnimal 메서드를 고쳐보자.

클래스가 추가되면 barkAnimal 메서드는 다음처럼 수정되어야 한다.

class Bouncer {
    void barkAnimal(Animal animal) {
      if (animal instanceof Tiger) {
            System.out.println("어흥");
        } else if (animal instanceof Lion) {
            System.out.println("으르렁");
        } else if (animal instanceof Crocodile) {
          System.out.println("쩝쩝");
        } else if (animal instanceof Leopard) {
          System.out.println("캬옹");
        }
    }
}

동물 클래스가 추가될 때 마다 분기문이 추가되어야 하므로 좋지 않다.

우리는 이미 인터페이스를 배웠으므로 좀 더 나은 해법을 찾을 수 있다.

 

다음처럼 Barkable 인터페이스를 작성하고 두 클래스가 이 인터페이스를 구현하도록 변경해보자.

interface Predator {
    (... 생략 ...)
}

interface Barkable {
    void bark();
}

class Animal {
    (... 생략 ...)
}

class Tiger extends Animal implements Predator, Barkable {
    public String getFood() {
        return "apple";
    }

    public void bark() {
        System.out.println("어흥");
    }
}

class Lion extends Animal implements Predator, Barkable {
    public String getFood() {
        return "banana";
    }

    public void bark() {
        System.out.println("으르렁");
    }
}

class ZooKeeper {
    (... 생략 ...)
}

class Bouncer {
    void barkAnimal(Barkable animal) {  // Animal 대신 Barkable을 사용
        animal.bark();
    }
}

public class Sample {
    (... 생략 ...)
}

콤마를 이용하여 인터페이스를 여러개 implements 할 수 있다.

Tiger, Lion 클래스는 두개의 인터페이스를 다 implement하였고

이어서 두 클래스는 bark 메서드를 구현해야한다.

그리하여 이렇게 bark 메서드를 구현하면 Bouncer 클래스의 barkAnimal메서드와 인터페이스를 사용해 수정한 barkAnimal 메서드를 비교한 것이다.

 

barkAnimal 메서드의 입력 자료형이 Animal에서 Barkable로 변경되었다.

그리고 animal의 객체 타입을 체크하여 ‘어흥’ 또는 ‘으르렁’을 출력하던 부분을 그냥

bark메서드를 호출하도록 변경했다. 이렇게 변경했더니 복잡하던 조건 분기문도 사라지고

누가봐도 명확한 코드가 됐다.

 

예제에서 사용한 tiger, lion 객체는 각각 Tiger, Lion 클래스의 객체이면서 Animal 클래스의 객체이기도 하고,

Barkable과 Predator 인터페이스의 객체이기도 하다.

이러한 이유로 barkAnimal 메서드의 입력 자료형을 Animal에서 Barkable로 바꾸어 사용할 수 있는 것이다.

 

이렇게 하나의 객체가 여러 개의 자료형 타입을 가질 수 있는 것을 다형성 이라고 한다.

다형성을 이용하면 복잡한 형태의 분기문을 간단하게 처리할 수 있는 경우가 많다.

 

그러므로 Tiger 클래스의 객체는 다음과 같이 여러 가지 자료형으로 표현할 수 있다.

Tiger tiger = new Tiger();  // Tiger is a Tiger
Animal animal = new Tiger();  // Tiger is a Animal
Predator predator = new Tiger();  // Tiger is a Predator
Barkable barkable = new Tiger();  // Tiger is a Barkable

여기서 중요한점!

각 기 다른 객체로 선언된 객체들은 사용할 수 있는 메서드들도 당연히 서로 다르다.

predator 객체는 getFood() 메서드가 선언된 Predator 인터페이스의 객체이므로 getFood 메서드만 호출이 가능하다.

이와 마찬가지로 Barkable로 선언된 barkable 객체는 bark 메서드만 호출이 가능하다.

 

만약에 getFood 메서드와 bark 메서드를 모두 사용하고 싶다면??

Predator, Barkable 인터페이스를 구현한 Tiger로 선언된 tiger 객체를 그대로 사용하거나 다음과 같이 getFood, bark 메서드를 모두 포함하는 새로운 인터페이스를 만들면 된다.

interface Predator {
    (... 생략 ...)
}

interface Barkable {
    void bark();
}

interface BarkablePredator extends Predator, Barkable {
}

(... 생략 ...)

기존의 인터페이스를 상속하여 BarkablePredator를 만들었다.

이와 같이하면 위에서 원하는 두 메서드 둘다 사용할 수 있다.

지금까지 봐왔던 것처럼 인터페이스는 일반클래스와 달리 extends를 이용하여 여러 개의 인터페이스 상속을 동시에 할 수 있다.

 

이번에는 Lion 클래스를 앞서 작성한 BarkablePredator 인터페이스를 구현하도록 수정하자.

(... 생략 ...)

class Lion extends Animal implements BarkablePredator {
    public String getFood() {
        return "banana";
    }

    public void bark() {
        System.out.println("으르렁");
    }
}

(... 생략 ...)

Bounder 클래스를 실행하더라도 동일한 결과 값이 나온다.

 

상속 관계에서 처럼 자식 인터페이스로 생성한 객체의 자료형은 부모 인터페이스로 사용하는 것이 가능하다.

자식 클래스의 객체 자료형을 부모 클래스의 자료형으로 사용 가능하다는 점과 동일하다.

 

 

추상 클래스

추상 클래스(abstract class)는 인터페이스의 역할도 하면서 클래스의 기능도 가지고 있는 자바의 ‘돌연변이’ 같은 클래스이다.

추상클래스를 인터페이스로 대체하는 것이 좋다고 하는 사람도 있다.

 

일단 추상클래스를 알아보기 위해 Predator 인터페이스를 추상 클래스로 변경해보자.

abstract class Predator extends Animal {
    abstract String getFood();

    default void printFood() {  // default 를 제거한다.
        System.out.printf("my food is %s\\n", getFood());
    }
}

(... 생략 ...)

추상 클래스를 만들려면 class 앞에 abstract를 표기해야 한다.

또한 인터페이스의 메서드와 같은 역할을 하는 메서드에도 abstract를 붙여야한다.

 

abstract 메서드는 인터페이스 메서드와 동일하게 구현체가 없다.

abstract 클래스를 상속하는 클래스에서 해당 abstract 메서드를 구현해야한다.

그리고 Animal 클래스의 기능을 유지하기 위해 Animal 클래스를 상속했다.

 

인터페이스의 디폴트 메서드는 더 이상 사용할 수 없으므로 삭제. 일반 메서드로 변경

 

Predator 인터페이스를 이와 같이 추상 클래스로 변경하면

Predator 인터페이스를 상속한 BarkablePredator 인터페이스를 사용할 수 없으므로 삭제

Lion, Tiger 클래스도 Animal 클래스 대신 Predator 추상 클래스를 상속하도록 변경하자.

ed!abstract class Predator extends Animal {
    (... 생략 ...)
}

interface Barkable {
    (... 생략 ...)
}

interface BarkablePredator extends Predator, Barkable {
}

class Animal {
    (... 생략 ...)
}

class Tiger extends Predator implements Barkable {
    (... 생략 ...)
}

class Lion extends Predator implements Barkable {
    (... 생략 ...)
}

class ZooKeeper {
    (... 생략 ...)
}

class Bouncer {
    (... 생략 ...)
}

public class Sample {
    (... 생략 ...)
}

Predator 추상 클래스에 선언된 getFood 메서드는 Tiger, Lion 클래스에 이미 구현되어 있으므로

추가로 구현할 필요는 없다.

추상 클래스에 abstract로 선언된 메서드는 인터페이스의 메서드와 마찬가지로 반드시 구현해야한다.

추상 클래스에는 abstract 메서드 외에 실제 메서드도 사용할 수 없다.

추상 클래스에 실제 메서드를 추가하면 Tiger, Lion 등으로 만들어진 객체에서 그 메서드들을 모두 사용할 수 있게 된다.

원래 인터페이스에서 default 메서드로 사용했던 printFood가 추상 클래스의 실제 메서드에 해당된다.

 

🔴 인터페이스와 추상 클래스의 차이

자바 8버전부터 인터페이스의 default 메서드가 추가되면서 추상 클래스와 차이점이 모호해졌다.

하지만 추상 클래스는 인터페이스와 달리 일반 클래스처럼 객체 변수, 생성자, private 메서드 등을 가질 수 있다.