Java/STUDY HALLE

[Java] 상속

무토(MUTO) 2020. 12. 22. 07:37

0. 학습 목표

  • 자바 상속의 특징
  • super 키워드
  • 메소드 오버라이딩
  • 다이나믹 디스패치
  • (더블 디스패치)
  • 추상클래스
  • final 키워드
  • Object 클래스

1. 자바 상속의 특징

1-1. 상속이란?

상속이란 상위클래스에서 정의한 필드와 메서드를 하위클래스도 동일하게 사용할 수 있게 물려받는 것이다.

1-2. 상속을 사용하는 이유

코드를 재사용하기에 편하고 클래스 간 계층구조를 분류하고 관리하기 쉬워진다.

  • 영웅이 빌런들을 물리치며 세계 평화를 지키는 게임을 만든다고 가정하자.
  • 영웅은 hp, 공격력, 레벨을 가지며 상대방을 공격할 수 있고 레벨만큼 상대방의 공격력을 방어할 수 있다.
  • 빌런은 hp, 공격력 방어력을 가지며 상대방을 공격할 수 있고 단단한 방어력으로 상대방의 공격을 방어력만큼 막을 수 있다.

위 요구사항을 1차적으로 구현한 코드는 다음과 같다.

Unit.java

package study.moon.test;

public class Unit {

    int hp;

    int attackPoint;

    public Unit(int hp, int attackPoint) {
        this.hp = hp;
        this.attackPoint = attackPoint;
    }

    public void attack(Unit unit) {
        unit.attackedBy(this);
    }

    public void attackedBy(Unit unit) {
        this.hp -= unit.attackPoint;
    }

}

Hero.java

package study.moon.test;

public class Hero extends Unit{

    int level;

    public Hero(int hp, int attackPoint, int level) {
        super(hp, attackPoint);
        this.level = level;
    }

}

Villain.java

package study.moon.test;

public class Villain extends Unit {

    int defensePoint;

    public Villain(int hp, int attackPoint, int defensePoint) {
        super(hp, attackPoint);
        this.defensePoint = defensePoint;
    }

}

위 요구사항을 보면 히어로와 빌런 둘 모두가 공통적으로 포함하고 있는 부분이 있다.

  1. hp를 가지고 있다.
  2. 공격력을 가지고 있다.
  3. 공격을 할 수 있다.
  4. 공격을 당할 수 있다.

따라서 위의 공통적인 부분을 Unit 으로 묶고 히어로와 빌런은 각각 Unit을 상속하고, 나머지를 구현하는것이 코드를 재사용할 수 있는 방법이다.

1-3. 자바 상속의 특징

1-3-1. 다중상속 금지

자바는 다중 상속을 허용하지 않는다. 예를들어 히어로와 빌런간의 이종 교배를 통하여 힐런이라는 종이 탄생했다고 치자. 힐런은 히어로와 빌런의 능력을 모두 이어받아 레벨도 있고 강력한 방어력도 가질 수 있다. 하지만 자바에서는 이러한 다중 상속을 허용하지 않는다.

public class Hellain extends Villain, Hero {
    "컴파일 에러"
}

1-3-2. 최상위 클래스 Object

자바의 모든 클래스는 최상위 클래스 Object의 서브클래스이다.

public class Main {

    public static void main(String[] args) {
        Object hero = new Hero(100,10,3);// Hero 는 Object의 서브클래스
        Object villain = new Villain(150,5,5);//Villain도 Object의 서브클래스
    }

}

2. super 키워드

2-1. super

super키워드를 사용하면 서브클래스가 수퍼클래스에 접근이 가능하다. super는 수퍼클래스의 참조변수라고 볼 수 있다.

Villain.java

package study.moon.test;

public class Villain extends Unit {

    int defensePoint;

    int hp; // 보통 이렇게 하지 않지만 super를 설명하기 위해 상위클래스에서 사용한 변수명을 다시 사용한다.

    public Villain(int hp, int attackPoint, int defensePoint) {
        super(hp, attackPoint);
        this.defensePoint = defensePoint;
        this.hp = 1000; // 빌런만 가지고있고 유닛은 없는 hp를 1,000으로 설정한다.
        super.hp = 10000; //유닛이 공통으로 가지고있는 hp를 10,000으로 설정한다.
    }

}

2-2. super()

super()를 사용하면 수퍼클래스의 생성자를 호출할 수 있다.

다시 정상적인 빌런과 유닛의 코드를 보자.

Unit.java

package study.moon.test;

public class Unit {

    int hp;

    int attackPoint;

    public Unit(int hp, int attackPoint) { // 해당 생성자가 빌런에서 super(hp,attackPoint)로 호출된다.
        this.hp = hp;
        this.attackPoint = attackPoint;
    }

    public void attack(Unit unit) {
        unit.attackedBy(this);
    }

    public void attackedBy(Unit unit) {
        this.hp -= unit.attackPoint;
    }

}

Villain.java

package study.moon.test;

public class Villain extends Unit {

    int defensePoint;

    public Villain(int hp, int attackPoint, int defensePoint) {
        super(hp, attackPoint); //Unit의 생성자를 호출한다.
        this.defensePoint = defensePoint;
    }

}

빌런이 super(hp,attackPoint)를 호출하였다.
즉, 수퍼클래스의 Unit(hp, attackPoint)생성자를 호출하여 초기화했다는 뜻이다.
그러면 이제 빌런은 추가적인 방어력 부분만 의존성으로 받아와서 새롭게 셋팅하면 된다.

!!! 수퍼클래스의 생성자의 인자가 없다면 서브클래스에서 super()를 작성하지 않아도 자동으로 컴파일시에 추가된다.

3. 메소드 오버라이딩

수퍼클래스가 가지고있는 메서드를 서브클래스에서 새롭게 다른 로직으로 정의하고 싶을 때 사용하는 문법이다.
상속관계에 있는 클래스간에 같은 이름의 메서드를 정의하는 문법을 오버라이딩이라고 한다.
오버라이딩 어노테이션은 생략할 수도 있다.

빌런은 공격을 받을때 히어로와는 다르게 방어력의 수치만큼 공격피해가 덜 들어와야 한다.
따라서 유닛이 공통으로 가지고있는 공격받는 로직을 새롭게 작성해주어야 한다.

Villain.java

package study.moon.test;

public class Villain extends Unit {

    int defensePoint;

    public Villain(int hp, int attackPoint, int defensePoint) {
        super(hp, attackPoint);
        this.defensePoint = defensePoint;
    }

    @Override
    public void attackedBy(Unit unit) {// Unit에 정의되어있는 메서드를 재정의했다.
        this.hp -= (unit.attackPoint - this.defensePoint); //빌런은 일반적인 유닛과는 다르게 방어력만큼 공격이 덜 들어온다.
    }

    @Override
    public String toString() {
        return "Villain{" +
            "hp=" + hp +
            ", attackPoint=" + attackPoint +
            ", defensePoint=" + defensePoint +
            '}';
    }
}

4. 다이나믹 디스패치

컴파일타임에는 알 수 없는 메서드의 의존성을 런타임에 늦게 바인딩 하는것이다.

다음 코드를 보도록 하자.

Main.java

package study.moon.test;

public class Main {

    public static void main(String[] args) {
        Hero hero = new Hero(100,10,3);
        Villain villain = new Villain(150,5,5);
        hero.attack(villain);
        villain.attack(hero);
        System.out.println(hero);
        System.out.println(villain);
    }

}

다음 코드는 각각의 객체가 어떤 메서드를 호출할 지 정확하게 예측이 가능하다.
히어로는 Hero의 attack()을 호출 할 것이고
빌런은 Villain의 attack()을 호출 할 것이다.
이것은 모두 컴파일 타임 에 파악할 수 있다.
컴파일된 class파일을 통해 확인해보도록 하자.

Main.class

public static void main(java.lang.String[]);
    Code:
       0: new           #7                  
       3: dup
       4: bipush        100
       6: bipush        10
       8: bipush        30
      10: invokespecial #9                  
      13: astore_1
      14: new           #12                 
      17: dup
      18: sipush        150
      21: iconst_5
      22: iconst_5
      23: invokespecial #14                 
      26: astore_2
      27: aload_1
      28: aload_2
      29: invokevirtual #15         // Method study/moon/test/Hero.attack:(Lstudy/moon/test/Unit;)V
      32: aload_2
      33: aload_1
      34: invokevirtual #19         // Method study/moon/test/Villain.attack:(Lstudy/moon/test/Unit;)V
      37: getstatic     #20                 
      40: aload_1
      41: invokevirtual #26                 
      44: getstatic     #20                 
      47: aload_2
      48: invokevirtual #26                 
      51: return

장황한 주석들을 삭제하고 두 메서드가 관련된 부분만 살펴보도록 하자.
29행과 34행을 살펴보면 해당 인스턴스는 정확하게 어떤 클래스의 메서드를 호출 할 것인지 명확하게 알 수 있다.
hero는 Hero클래스의 attack을 호출, villain은 Villain클래스의 attack을 호출한다.

하지만 나는 다형성을 적용하여 코드를 조금 더 유연하게 작성하고 싶다.
다음과 같이 코드를 변경해보자

Main.java

package study.moon.test;

public class Main {

    public static void main(String[] args) {
        Unit hero = new Hero(100,10,3);// 타입을 Unit으로 변경하였다.
        Unit villain = new Villain(150,5,5);// 타입을 Unit으로 변경하였다.
        hero.attack(villain);
        villain.attack(hero);
        System.out.println(hero);
        System.out.println(villain);
    }

}

이렇게 코드가 변경된다면 과연 어떻게될까?
컴파일된 class 파일을 살펴보자.

Main.class

public static void main(java.lang.String[]);
    Code:
       0: new           #7                  
       3: dup
       4: bipush        100
       6: bipush        10
       8: bipush        30
      10: invokespecial #9                  
      13: astore_1
      14: new           #12                 
      17: dup
      18: sipush        150
      21: iconst_5
      22: iconst_5
      23: invokespecial #14                 
      26: astore_2
      27: aload_1
      28: aload_2
      29: invokevirtual #15         // Method study/moon/test/Unit.attack:(Lstudy/moon/test/Unit;)V
      32: aload_2
      33: aload_1
      34: invokevirtual #15         // Method study/moon/test/Unit.attack:(Lstudy/moon/test/Unit;)V
      37: getstatic     #21                 
      40: aload_1
      41: invokevirtual #27                 
      44: getstatic     #21                 
      47: aload_2
      48: invokevirtual #27                 
      51: return

이번에도 딱 두 부분의 메서드가 컴파일 타임에 어떻게 바인딩되어있는지 확인해보도록 하자
29행과 34행을 보면 해당 메서드는 Unit의 attack을 사용하기로 결정되어있다.
그렇다면 빌런과 히어로 모두 유닛의 attack을 사용하는것일까??
그렇지 않다. 다음 결과물을 보도록 하자.

Main.java

public class Main {

    public static void main(String[] args) {
        Unit hero = new Hero(100,10,3);// 타입을 Unit으로 변경하였다.
        Unit villain = new Villain(150,5,5);// 타입을 Unit으로 변경하였다.
        hero.attack(villain);
        villain.attack(hero);
        System.out.println(hero);
        System.out.println(villain);
    }

}
Hero{hp=98, attackPoint=10, level=3}
Villain{hp=145, attackPoint=5, defensePoint=5} // 유닛의 메서드가아닌 빌런의 메서드가 호출된 결과이다.

위의 main 함수의 결과물이다.

빌런이 일반 유닛이라면 방어력의 영향을 받지 않고 히어로의 공격 10을 온전히 받아냈어야했다.
그러나 오버라이딩 된 빌런의 계산을 따랐고 그 결과 히어로의 공격력 10에서 방어력 5를 뺀 데미지만 입게 되었다.
히어로도 레벨의 영향을 받아 레벨만큼 공격을 덜받았다.

이처럼 컴파일 타임에는 메서드의 클래스타입이 정해져있지 않지만 런타임에 정해져서 메서드를 호출하는 것을
동적 dispatch 라고 한다.

많은 블로그 글을 둘러보면서 확인한 결과, 대부분의 블로그의 글들이 구현체가 없는 인터페이스, 혹은 추상클래스의 메서드를 호출할 때 클래스 타입에 따라 동적으로 메서드가 결정되는것이라고 설명해놓았다. 그런데 해 본 결과 굳이 추상클래스나 인터페이스가 아니어도 동적 디스패치는 발생할 수 있다. 수퍼클래스의 메서드를 오버라이딩해도 동적 디스패치가 가능하다.

5. 더블 디스패치

  • 히어로는 레벨이 올라 수퍼 히어로,하이퍼 히어로로 업그레이드할 수 있게 하고싶다.
  • 빌런도 일정 시간이 지나면 수퍼빌런, 하이퍼 빌런으로 업그레이드할 수 있게 만들고 싶다.
  • 그에 맞춰서 각각 업그레이드 한 상태에서 상대방을 공격할 때의 로직을 if문을 사용하지 않고 유기적으로 작성해주고싶다.
  • 어떻게 작성해야 할까?

ex)
수퍼히어로 -> 빌런
하이퍼히어로 -> 빌런
수퍼빌런 -> 히어로
하이퍼빌런 -> 히어로
모두 다른 로직을 작성하고싶다!

코드를 다음과 같이 수정해보자. 그러면 if문은 사용하지 않고도 분기처리를 진행할 수 있다.

주석에 적힌 부분을 따라가면 2번 동적 디스패치가 발생했다는 사실을 알 수 있다.

Unit.java

public abstract class Unit {
    int hp;
    int attackPoint;

    public Unit(int hp, int attackPoint) {
        this.hp = hp;
        this.attackPoint = attackPoint;
    }

}

Hero.java

public abstract class Hero extends Unit {

    int level;

    public Hero(int hp, int attackPoint, int level) {
        super(hp, attackPoint);
        this.level = level;
    }

    //1. goto  hyperHero.attack()  or  superHero.attack()
    public abstract void attack(Villain villain);  

    public void isAttackedBy(SuperVillain superVillain) {
        hp -= (superVillain.attackPoint * 2 - level);
    }

    public void isAttackedBy(HyperVillain hyperVillain) {
        hp -= (hyperVillain.attackPoint);
    }
}

HyperHero.java

public class HyperHero extends Hero {

    public HyperHero(int hp, int attackPoint, int level) {
        super(hp, attackPoint, level);
    }

    //2. goto villain.isAttackedBy(superHero)  or  villain.isAttackedBy(hyperHero)
    @Override
    public void attack(Villain villain) {
        villain.isAttackedBy(this);
    }

}

SuperHero.java

public class SuperHero extends Hero{

    public SuperHero(int hp, int attackPoint, int level) {
        super(hp, attackPoint, level);
    }

    //2. goto villain.isAttackedBy(superHero)  or  villain.isAttackedBy(hyperHero)
    @Override
    public void attack(Villain villain) {
        villain.isAttackedBy(this);
    }
}

Villain.java

public abstract class Villain extends Unit {

    int defensePoint;

    public Villain(int hp, int attackPoint, int defensePoint) {
        super(hp, attackPoint);
        this.defensePoint = defensePoint;
    }

    //1. goto  hyperVillain.attack()  or  superVillain.attack() 
    public abstract void attack(Hero hero);

    public void isAttackedBy(SuperHero superHero) {
        hp -= (superHero.attackPoint + superHero.level - defensePoint);
    }

    public void isAttackedBy(HyperHero hyperHero) {
        hp -= (hyperHero.level * 3);
    }
}

HyperVillain.java

public class HyperVillain extends Villain {

    public HyperVillain(int hp, int attackPoint, int defensePoint) {
        super(hp, attackPoint, defensePoint);
    }

    //2. goto hero.isAttackedBy(superVillain)  or  hero.isAttackedBy(hyperVillain)
    @Override
    public void attack(Hero hero) {
        hero.isAttackedBy(this);
    }
}

SuperVillain.java

public class SuperVillain extends Villain{

    public SuperVillain(int hp, int attackPoint, int defensePoint) {
        super(hp, attackPoint, defensePoint);
    }

    //2. goto hero.isAttackedBy(superVillain)  or  hero.isAttackedBy(hyperVillain)
    @Override
    public void attack(Hero hero) {
        hero.isAttackedBy(this);
    }

}

Main.java

public class Main {

    public static void main(String[] args) {
        Hero superHero = new SuperHero(100,20,5);
        Hero hyperHero = new HyperHero(100,5,10);
        Villain superVillain = new SuperVillain(200,10,5);
        Villain hyperVillain = new HyperVillain(150,15,10);
        superHero.attack(superVillain);
        superHero.attack(hyperVillain);
        hyperHero.attack(superVillain);
        hyperHero.attack(hyperVillain);
        superVillain.attack(superHero);
        superVillain.attack(hyperHero);
        hyperVillain.attack(superHero);
        hyperVillain.attack(hyperHero);

        System.out.println(superHero);
        System.out.println(hyperHero);
        System.out.println(superVillain);
        System.out.println(hyperVillain);
    }
}

컴파일 된 클래스 파일을 확인하면서 어떻게 동적 디스패치가 일어났는지 확인해보자.

Main.class

public static void main(java.lang.String[]);
    Code:
       0: new           #7                  
       3: dup
       4: bipush        100
       6: bipush        20
       8: iconst_5
       9: invokespecial #9                  
      12: astore_1
      13: new           #12                 
      16: dup
      17: bipush        100
      19: iconst_5
      20: bipush        10
      22: invokespecial #14                 
      25: astore_2
      26: new           #15                 
      29: dup
      30: sipush        200
      33: bipush        10
      35: iconst_5
      36: invokespecial #17                 
      39: astore_3
      40: new           #18                 
      43: dup
      44: sipush        150
      47: bipush        15
      49: bipush        10
      51: invokespecial #20                 
      54: astore        4
      56: aload_1
      57: aload_3
      58: invokevirtual #21         // Method study/moon/test/Hero.attack:(Lstudy/moon/test/Villain;)V
      61: aload_1
      62: aload         4
      64: invokevirtual #21         // Method study/moon/test/Hero.attack:(Lstudy/moon/test/Villain;)V
      67: aload_2
      68: aload_3
      69: invokevirtual #21         // Method study/moon/test/Hero.attack:(Lstudy/moon/test/Villain;)V
      72: aload_2
      73: aload         4
      75: invokevirtual #21         // Method study/moon/test/Hero.attack:(Lstudy/moon/test/Villain;)V
      78: aload_3
      79: aload_1
      80: invokevirtual #27         // Method study/moon/test/Villain.attack:(Lstudy/moon/test/Hero;)V
      83: aload_3
      84: aload_2
      85: invokevirtual #27         // Method study/moon/test/Villain.attack:(Lstudy/moon/test/Hero;)V
      88: aload         4
      90: aload_1
      91: invokevirtual #27         // Method study/moon/test/Villain.attack:(Lstudy/moon/test/Hero;)V
      94: aload         4
      96: aload_2
      97: invokevirtual #27         // Method study/moon/test/Villain.attack:(Lstudy/moon/test/Hero;)V
     100: getstatic     #32                 
     103: aload_1
     104: invokevirtual #38                 
     107: getstatic     #32                 
     110: aload_2
     111: invokevirtual #38                
     114: getstatic     #32                 
     117: aload_3
     118: invokevirtual #38                 
     121: getstatic     #32                 
     124: aload         4
     126: invokevirtual #38                 
     129: return
}

다음과 같은 방식으로 코드를 작성하면 조금 더 유연한 코드가 된다. 업그레이드 된 히어로 빌런이 추가된다면 해당 부분을 추가해서 넣어주기만 하면 된다. 디자인 패턴으로는 다음과 같은 패턴을 방문자 패턴이라고 부른다고 한다.

6. 추상클래스

구체적이지 않은 클래스를 말한다. 예를들어 구체적인 클래스가 히어로, 빌런이라면 추상적인 클래스는 유닛이 될 수 있다.
공통된 부분으로 묶기에는 적당하지만 구현을 하지는 않을 클래스를 만들 때 추상클래스를 이용한다.

public abstract class Unit {
    int hp;
    int attackPoint;

    public Unit(int hp, int attackPoint) {
        this.hp = hp;
        this.attackPoint = attackPoint;
    }

    public abstract void attack(Unit unit);

    public void attackedBy(Unit unit) {
        this.hp -= unit.attackPoint;
    }
}
  • 클래스 앞에 abstract 키워드를 이용하면 해당 클래스는 추상클래스가 된다.
  • 추상클래스는 추상메서드를 작성할 수 있다. (추상메서드란, 구현부가 없는 메서드이다.)
  • 추상메서드는 메서드의 반환형 앞에 absract를 붙이면 된다.
  • 추상클래스는 인스턴스를 생성할 수 없다.
  • 추상 클래스를 상속받은 클래스는 수퍼클래스가 가지고있는 추상메서드를구현하지 않으면 추상클래스가 된다.

7. final

final은 다시 무언가를 정의내리는것을 막는 키워드이다.

  • class
    • 클래스의 상속을 막는다.
  • variable
    • 변수의 재할당을 막는다.
  • method
    • 메서드의 오버라이딩을 막는다.

많은 사람들이 오해하고 있는 부분중에 하나가 바로 변수에 final을 사용하면 불변한다는 것이다.
그러나 대상은 사실 변할 수 있다.예제를 살펴보자.

import java.util.ArrayList;
import java.util.List;

class Main {

    public static void main(String[] args) {
        final List<Integer> list = new ArrayList<>();
        list.add(1);
        list.add(2);
        list.add(3);
        list.add(4);
        list.add(5);
        System.out.println(list); // [1, 2, 3, 4, 5]
    }
}

list 내부의 값이 0개에서 5개로 증가했다.
이처럼 final에서의 불변은 대상이 불변하는것이 아니라 새롭게 할당하는것을 막는다는것을 의미한다.

그렇다면 final 키워드는 왜 사용할까?

  • 우리의 기억력이 완벽하지 않고, 우리 코드의 의도가 분명하지 않기때문에 사용한다.
  • 우리는 항상 실수하기 때문에 final을 사용하여 미리 실수를 차단할 수 있는 방안은 모두 사용해야한다.

8. Object

  • 자바의 최상위 클래스이다.
  • 따로 어디서 상속받지 않더라도 Obejct는 모든 클래스의 최상위 클래스이기 때문에 내가 클래스를 생성하면 그 클래스에는 자동으로 object의 기본메서드가 포함되어있다.

Object 클래스의 메서드

http://www.tcpschool.com/java/java_api_object

'Java > STUDY HALLE' 카테고리의 다른 글

[Java] 인터페이스  (0) 2021.01.08
[Java] 패키지  (0) 2020.12.29
[Java] 클래스  (0) 2020.12.16
[Java] 선형 자료구조  (0) 2020.12.07
[Java] GitHub Library 사용법  (0) 2020.12.06