본문 바로가기
프로그래밍/JAVA

[JAVA] Java TPC 강의 정리 (PART 2) - 상속

by 소소로드 2020. 9. 18.

  • 1. 상속
    (1) 상속의 개념
    (2) 상속의 수평적 / 수직적 설계
    (3) 상속의 재정의 (Override)

PART 1에서는 클래스의 설계에 대해서만 이야기를 했으나

PART 2에서는 동작(행위)과 관계의 측면에서 구조 설계를 이야기한다.

1. 상속

(1) 상속의 개념

상속은 계층화를 통해 부모, 자식이라는 관계를 수직적으로 설계하는 기술이다.

공통된 부분을 Animal 클래스에서 구현하고 자식은 해당 동작을 쉽게 가져다 쓸 수 있도록

extends를 통해서 상속관계를 만들어주는 원리이다.

자바에서 최상위 Root 클래스는 Object 클래스이다. 

눈에 보이지 않지만 모든 클래스에는 부모로 명시된 extends Object가 붙어 있다.

또한, super()도 눈에 보이지 않는 기본 생성자로 사용되므로 하나의 클래스가 실행될 때

super()는 자식이 아닌 부모부터 생성하기 때문에 Object가 생성되고, Animal이 생성된다. 

Animal을 생성한 뒤에 Dog를 생성하는 것도 마찬가지이다.

[ Object - Animal - Dog == Cat ]

이런 상속체이닝 구조로 구성되어 있다.

(2) 상속의 수평적 / 수직적 설계

수평적 설계(앞서 하던 기본적인 설계)

 Dog 

 Cat

 eat()      eat()

1
2
3
4
public class Dog {
    public void eat() {
        System.out.println("개는 먹는다");
    }
cs

1
2
3
4
public class Cat {
    public void eat() {
        System.out.println("고양이는 먹는다");
    }
cs

1
2
3
4
5
6
7
8
9
10
11
public class Basic {
    public static void main(String[] args) {
 
    Dog dog = new Dog();
    dog.eat();
 
    Cat cat = new Cat();
    cat.eat();
 
   }
}
cs

클래스 설계시 Dog, Cat를 만든다고 가정을 하면 eat()라는 행위는 중복된다.

그러면 Dog에도 eat(), Cat에도 eat() 등 모든 동물에 eat()를 따로 만든다는 건데

이런 설계는 수도 많아지면 관리가 어렵고 수정하기도 쉽지 않다.


수직적 설계(계층화 / 상속구조)

 Animal

 Dog 

 Cat

 eat()      eat()

1
2
3
4
5
6
public class Animal {
    public void eat() {
        System.out.println("동물은 먹는다.");
    }
 
}
cs

1
2
3
4
5
public class Dog extends Animal {
 
}
// Cat도 동일
 
cs

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Basic {
    public static void main(String[] args) {
 
    // 1. 자식으로 받음        
    // Dog dog = new Dog();
    // dog.eat();
          // 2. 부모로 받음
// 부모-자식간의 자동 형변환, 업캐스팅이 된다.
    Animal ani = new Dog();
    ani.eat();
        
    ani = new Cat();
    ani.eat();   }
}
cs

만약 Dog, Cat의 중복적인 속성을 밖으로 빼서 하나의 클래스로 만들면 어떨까.

즉, 이들의 부모인 Animal에만 eat()를 만들어서 상속한다면

코드는 복잡해지지만 유지보수 측면에서 훨씬 이점이 많을 것이다.

상속을 할 때는 자식 클래스에서 extends를 붙이는데 '나를 확장해서 부모까지 영향을 행사한다' 정도로 볼 수 있다.

이 클래스를 사용할 때는 보통 자식 클래스에서 생성하여 받는 것보다 부모 클래스에서 생성한다.


(3) 상속의 재정의 (Override)

상속의 재정의(Override)란

상속받은 자식 클래스가 부모 클래스의 동작을 수정하는 것

=> (2)에서 아쉬운 점을 찾아보면 나는 Dog는 "개는 먹는다." Cat은 "고양이는 먹는다."

     라고 표현하고 싶은데 공통점으로 묶는다는 이유로 "동물은 먹는다."로 썼다.

     재정의를 하지 않으면 부모가 가진 내용을 변화없이 오직 그대로 쓰게된다.


- 자식으로 받음 : Dog클래스의 모든 동작을 알고 있는 경우

1. Dog dog = new Dog();로 객체를 만들면 super()를 통해

   부모인 Animal() 객체가 메모리에 먼저 생성된 뒤에 Dog()객체가 생성된다.

2. Dog() 객체는 상속받아 확장된 상태기 때문에 Animal까지 접근가능한 Dog타입의 객체라 볼 수 있다.

   dog가 메모리를 가리키게 될 때 당연히 Animal ~ Dog까지의 범위에 도달할 수 있다.

3. 만약 재정의를 할 경우 같은 메서드라면 Animal이 무시되고 Dog의 메서드가 실행된다.


- 부모로 받음 : Dog클래스의 동작 방식을 몰라서 부모에 의존하는 경우

1. Animal ani = new Dog();로 객체를 만들면 마찬가지로 super()를 통해

   부모인 Animal() 객체가 메모리에 먼저 생성된 뒤에 Dog()객체가 생성된다.

2. Dog() 객체는 Animal타입이기 때문에 ani가 메모리를 가리키게 될 때 Animal의 범위에만 도달할 수 있다.

3. 만약 재정의를 할 경우 같은 메서드라면 재정의를 했는지 찾아가게 되어있고

   재정의가 되어있을 경우에는 Dog의 메서드가 실행된다. (동적 바인딩)

=> ani.eat(); 의 컴파일 시점에서는 부모의 eat()인데 실행될 때는 동적 바인딩을 통해서

     자식에게 재정의된 메서드를 찾고 Dog()에 eat()가 재정의되어 있다면 그걸 선택한다.

4. 즉, Dog의 기능을 몰라도 부모의 타입으로 Dog의 메서드를 재정의할 수 있다.


1
2
3
4
5
6
7
8
public class Dog extends Animal {
 
    @Override
    public void eat() {
        System.out.println("개는 먹는다.");
    }
}
 
cs

1
2
3
4
5
6
7
8
public class Cat extends Animal {
 
    @Override
    public void eat() {
        System.out.println("고양이는 먹는다.");
    }
}
 
cs

수직적 설계의 내용과 다 동일하며 Dog를 Animal의 내용을 재정의해주면 된다.


(4) 다운 캐스팅(down casting)

이런 생각을 해볼 수 있다. 만약 자식의 메서드 중에 재정의 되지 않은 메서드는

부모타입으로 만들 때 어떻게 해야 할까.

1
2
3
4
5
6
7
8
9
10
11
12
public class Cat extends Animal {
 
    @Override
    public void eat() {
        System.out.println("고양이는 먹는다.");
    }
    
    public void night() {
        System.out.println("고양이는 야행성이다.");
    }
}
 
cs

이런 경우 ani = new Cat(); ani.night();로 하면 에러가 난다.

이럴 때 다운 캐스팅이 필요하다. 부모의 타입을 자식의 타입으로 형변환 해야 한다.

((Cat)ani).night();

바로 이렇게 다운 캐스팅을 강제적으로 해주면 된다.