영권's

6주차 과제: 상속 본문

스터디/백기선 라이브 스터디(자바)

6주차 과제: 상속

ykkkk 2021. 2. 17. 20:53

목표

자바의 상속에 대해 학습하세요.

학습할 것 (필수)

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

 

  • 자바 상속의 특징

현실에서 상속은 부모가 자식에게 물려주는 행위를 말하는 것처럼 자바에서 상속 또한 자식이 부모의 멤버를 물려받아 사용하며 확장시킬 수 있다.

부모 클래스를 상위 클래스, 자식 클래스를 하위 클래스 또는 파생 클래스라고 한다.

상속은 이미 잘 개발된 클래스를 재사용하여  새로운 클래스를 만들기 때문에 코드의 중복을 줄여준다.

 

도서) 이것이 자바다.

field1,field2,method1(),method2()를 가지는 클래스를 작성했을 때, 4개를 모두 처음부터 작성하는 것보다는 field1과 method1()을 가지고 있는 클래스가 있으면 이것을 상속하고 field2과 method2()만 추가 작성하는 것이 효율적이고 개발 시간을 줄여준다.

A클래스를 상속받은 B클래스의 바이트 코드를 보면 A를 호출하고 있다.

실제로 b 클래스를 객체 생성해서 다음과 같이 사용할 때는 마치 B가 field1과 method1()을 가지고 있는 것처럼 보인다.

 

상속을 해도 부모 클래스의 모든 필드와 메서드를 물려받는 것은 아니다.

부모 클래스에서 접근 제한을 갖는 필드와 메서드는 상속 대상에서 제외된다.

그리고 클래스가 다른 패키지에 존재한다면 default 접근 제한을 갖는 필드와 메서드도 상속 대상에서 제외된다.

그 이외의 경우는 상속의 대상이 된다.

 

상속을 이용하면 클래스의 수정을 최소화 시킬 수도 있다. 부모 클래스의 수정으로 모든 자식 클래스들의 수정 효과를 가져오기 때문에 유지 보수 시간을 최소화 시켜준다.

 

자바 상속의 특징

  1. 다중상속을 지원하지 않는다. 즉, extends 뒤에는 단 하나의 부모클래스만 올 수 있다.
  2. 부모의 생성자와 초기화 블록은 상속되지 않는다.
  3. 자식 클래스는 부모 클래스가 가진 멤버변수와 메소드를 모두 상속받는다.
  4. 부모 클래스 내에서 멤버 변수 또는 메소드가 private 접근 제한자 를 사용하면 멤버변수는 상속받으나 → 바로 접근이 불가능하다. 메소드는 상속되지 않는다.
  5. static 메서드 또는 변수도 상속이 된다.
  6. 동일한 이름의 변수가 부모 클래스와 자식 클래스에 둘 다 존재할 경우 부모 클래스의 변수는 가려진다.
  7. 상속에 대한 횟수를 제한하지 않는다.
  8. 최상위 클래스는 [Object] 클래스 이며, Object 클래스만이 유일하게 super class(부모 클래스)를 가질 수 없다. 즉, 모든 클래스들은 Object 클래스의 자식 클래스이다.

A 클래스의 바이트 코드를 보면 Object 클래스를 상속 받는것을 볼 수 있다.

 

  • super 키워드

this,this()와 비슷한데 this가 자신을 가르키는 참조변수면 super는 상속받은 super클래스를 가르키는 참조변수이다.

그리고 this()가 자기 자신의 생성자를 호출했다면,super() 상속받은 super 클래스의 생성자를 호출한다.

 

super 키워드는 자식클래스에서 상속받은 부모클래스의 멤버를 참조할때 사용되는 키워드이다.

super.변수명 , super.메서드명() 과 같은 형식이다.

 

super()

super()는 조상 클래스의 생성자를 호출한다.

this()와 마찬가지로 생성자의 첫 줄에 적어줘야하며, 생략하면 컴파일러가 첫줄에 super();를 삽입한다. 

만약 생성자에 super와 this를 둘 다 써야한다면 this를 먼저 사용하고 this에 해당하는 생성자에 super를 사용하면 된다.

자식 클래스의 인스턴스를 생성하면 super()로 최상위 클래스인 object까지 거슬러 올라가야 생성자 호출이 끝난다.

 

 

메소드 오버라이딩

메서드 오버라이딩이란 상속된 메서드의 내용이 자식 클래스에 맞지 않을 경우, 자식 클래스에서 동일한 메서드를 재정의하는것을 말한다.

 

메서드 오버라이딩의 조건

  • 부모의 메서드와 동일한 시그니처(메서드 이름, 매개 변수 리스트)를 가져야 한다.
  • 접근 제한을 더 강하게 오버라이딩 할 수 없다.
  • 새로운 예외를 throws할 수 없다.

아래 코드는 getName을 재정의 한 코드 이다.

public class A {
    String name = "A";

    void getName(){
        System.out.println(name + "method");
    }

}

public class B extends A {

    String name = "B";

    @Override
    public void getName() {
        System.out.println(name + "method");
    }
    
}

B클래스의 getName() 메서드 위에 @Override라는 어노테이션은 해당 메서드가 오버라이딩한 메서드임을 알려주고 오버라이딩이 맞는지 컴파일 시점에서 검증해준다.

 

추상 클래스

추상은 사전적 의미로 실체 간에 공통되는 특성을 추출한 것을 말한다.

예를 들어 새, 곤충, 물고기 등의 실체에서 공통되는 특성을 추출해보면 동물이라는 공통점이 있다.

이와같이 구체적인 실체라기 보다는 실체들의 공통되는 특성을 가지고 있는 추상적인 것이라고 볼 수 있다.

 

클래스에서도 추상 클래스가 존재한다.

객체를 직접 생성할 수 있는 클래스를 실체 클래스라고 한다면 이 클래스의 공통적인 특성을 추출해서 선언한 클래스를 추상 클래스라고 한다. 추상 클래스와 실체 클래스는 상속의 관계를 가지고 있다. 추상 클래스가 부모이고 실체 클래스가 자식으로 구현되어 실체 클래스는 추상 클래스의 모든 특성을 물려받고 추가적인 특성을 가질 수 있다.

 

추상클래스 선언

 

public abstract class 클래스명{
	// 생성자
    // 필드
    // 메서드
}

추상 클래스를 선언 할 때는 클래스 선언에 abstract 키워드를 붙여야한다. abstract를 붙이게 되면 new 연산자를 이용해서 객체를 만들지 못하고 상속을 통해 자식 클래스만 만들 수 있다.

 

추상클래스도 일반 클래스와 마찬가지로 필드, 생성자, 메서드 선언을 할 수 있다. new 연산자로 직접 생성자를 호출 할 수 없지만 자식 객체가 생성될 때 super()를 호출해서 추상 클래스 객체를 생성하므로 추상 클래스도 생성자가 반드시 있어야한다.

 

abstract 클래스는 abstract  메서드를 포함한다.

  - 추상 메서드는 선언부만 적고, 구현부는 비워둔다.

 

final 키워드

final 키워드는 클래스, 필드, 메서드 선언 시에 사용할 수 있다. final 키워드는 해당 선언이 최종 상태이고 수정될 수 없을을 뜻한다.

final 키워드가 클래스, 필드, 메서드 선언에 사용될 경우 조금씩 해석이 달라진다.

 

출처 - https://www.notion.so/6-dcaa52b834464c1eb1a4600a3812627c

final 필드는 값을 변경할 수 없는 상수가 된다.

final 클래스는 이 클래스는 최종적인 클래스이므로 더 이상 확장할 수 없는 클래스가 된다. 즉 다른 클래스의 조상이 될 수 없다.

final 메서드는 최종적인 메서드가 되므로 오버라이딩 할 수 없는 메서드가 된다. 즉, 부모 클래스를 상속해서 자식 클래스를 선언할 때 부모 클래스에 선언된 final 메서드는 자식 클래스에서 재정의 할 수 없다는 것이다.

 

final 이 붙은 멤버변수는 값을 한 번만 할당할 수 있는데 선언과 함께 초기화하거나 생성자에서 초기화한다.

 

 

  • Object 클래스

java.lang 패키지

java.lang 은 자바 프로그램에서 가장 많이 사용되는 패키지로서 자바 프로그램내에 import하지 않아도 자동으로 포함된다.

그만큼 기본이 되는 클래스들과 인터페이스들이 포함되어 있다.

java.lang 패키지 클래스의 구조

출처) https://www.notion.so/e5c33507880b4d098f83a2c4f8f02c04

 

java.lang.Object 클래스는 자바 API의 모든 클래스와 사용자가 정의한 모든 클래스의 최상위 클래스이다.

즉, 모든 클래스는 Object 클래스로부터 상속받는다.

 

사용자가 클래스를 정의할 때 선언부에 명시적으로 extends 하지 않아도 자동으로 상속받게 된다.

 

  • 다이나믹 메소드 디스패치 (Dynamic Method Dispatch)

Java는 객체지향 프로그래밍언어로서 객체들간의 메시지 전송을 기반으로 문제를 해결해나간다.

메세지 전송이라는 표현은 결국 메서드를 호출하는 것인데, 이것을 Dispatch라 부른다.

 

Dispatch는 static dispatchdynamic dispatch가 있는데

static은 구현 클래스를 이용해 컴파일 시점에서부터 어떤 메서드가 호출될 지 정해져 있는 것이고,

dynamic은 인터페이스를 이용해 참조함으로서 호출되는 메서드가 동적으로 정해지는 것을 의미한다.

 

Static Dispatch

자바에서 객체 생성은 Runtime 시에 호출된다.

즉, 컴파일 시점에 알 수 있는 것은 타입에 대한 정보이다.

타입자체가 Dispatch라는 구현 클래스이기 때문에 해당 메서드를 호출하면 어떤 메서드가 호출될지 정적으로 정해진다.

이에 대한 정보는 컴파일이 종료된 후 바이트코드에도 드러나게 된다.

public class StaticDispatch {
    public static void main(String[] args) {
        Dispatch dispatch = new Dispatch();
        dispatch.print();
    }
}
class Dispatch{
    public void print(){
        System.out.println("dispatch");
    }
}
// class version 52.0 (52)
// access flags 0x21
public class me/study/week6/StaticDispatch {

  // compiled from: StaticDispatch.java

  // access flags 0x1
  public <init>()V
   L0
    LINENUMBER 3 L0
    ALOAD 0
    INVOKESPECIAL java/lang/Object.<init> ()V
    RETURN
   L1
    LOCALVARIABLE this Lme/study/week6/StaticDispatch; L0 L1 0
    MAXSTACK = 1
    MAXLOCALS = 1

  // access flags 0x9
  public static main([Ljava/lang/String;)V
   L0
    LINENUMBER 5 L0
    NEW me/study/week6/Dispatch
    DUP
    INVOKESPECIAL me/study/week6/Dispatch.<init> ()V
    ASTORE 1
   L1
    LINENUMBER 6 L1
    ALOAD 1
    INVOKEVIRTUAL me/study/week6/Dispatch.print ()V
   L2
    LINENUMBER 7 L2
    RETURN
   L3
    LOCALVARIABLE args [Ljava/lang/String; L0 L3 0
    LOCALVARIABLE dispatch Lme/study/week6/Dispatch; L1 L3 1
    MAXSTACK = 2
    MAXLOCALS = 2
}

 

 

 

Dynamic Dispatch

인터페이스를 타입으로 메서드를 호출한다.

 

컴파일러는 타입에 대한 정보를 알고있으므로 Runtime시에 호출 객체를 확인해 해당 객체의 메서드를 호출한다.

Runtime시에 호출 객체를 알 수 있으므로 바이트코드에도 어떤 객체의 메서드를 호출해야하는지 드러나지 않는다.

 

print() 메서드는 인자가 없는 메서드이지만 자바는 묵시적으로 항상 receiver parameter를 인자로 보내게된다.

receiver parameter를 인자로 보내기 때문에 this를 이용해 메서드 내부에서 호출 객체를 참조할 수 있다.

receiver paramer 알아보기

 

이것이 dynamic dispatch의 근거가 된다.

package me.study.week6;

public class DynamicTest {

    public static void main(String[] args){
        Dispatchable dispatch = new Dispatch();
        dispatch.print();
    }
}

interface Dispatchable{
    void print();
}

class Dispatch implements Dispatchable {
    public void print(){
        System.out.println("dynamic dispatch");
    }
}
// class version 52.0 (52)
// access flags 0x21
public class me/study/week6/DynamicTest {

  // compiled from: DynamicTest.java

  // access flags 0x1
  public <init>()V
   L0
    LINENUMBER 3 L0
    ALOAD 0
    INVOKESPECIAL java/lang/Object.<init> ()V
    RETURN
   L1
    LOCALVARIABLE this Lme/study/week6/DynamicTest; L0 L1 0
    MAXSTACK = 1
    MAXLOCALS = 1

  // access flags 0x9
  public static main([Ljava/lang/String;)V
   L0
    LINENUMBER 6 L0
    NEW me/study/week6/Dispatchs
    DUP
    INVOKESPECIAL me/study/week6/Dispatchs.<init> ()V
    ASTORE 1
   L1
    LINENUMBER 7 L1
    GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
    ALOAD 1
    INVOKEINTERFACE me/study/week6/Dispatchable.method ()Ljava/lang/String; (itf)
    INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V
   L2
    LINENUMBER 8 L2
    RETURN
   L3
    LOCALVARIABLE args [Ljava/lang/String; L0 L3 0
    LOCALVARIABLE dispatch Lme/study/week6/Dispatchable; L1 L3 1
    MAXSTACK = 2
    MAXLOCALS = 2
}

두 바이트 코드 중 메서드 호출 부분을 보면

 

INVOKEVIRTUAL me/study/week6/Dispatch.print ()

 

INVOKEINTERFACE me/study/week6/Dispatchable.method ()Ljava/lang/String; (itf)

 

로 다른것을 볼 수 있다.

 

토비의 봄 TV 예시 코드

package com.programmers.designpattern.begavioral.visitor.dispatch;

import java.util.Arrays;
import java.util.List;

public class Dispatch {
    static abstract class Service {
        abstract void run();
    }

    static class MyService1 extends Service {
        @Override
        void run() {
            System.out.println("run1");
        }
    }

    static class MyService2 extends Service {
        @Override
        void run() {
            System.out.println("run2");
        }
    }

    public static void main(String[] args) {
        Service service = new MyService1();

        // note : run 이라는 코드는 여기서만 봤을 때 둘중(구현클래스)에 어떤것을 실행시킬지 컴파일 타임에는 결정되어잇지 않다
        //  하지만 MyService1의 메서드가 실행되는데 이때 다이나믹 디스패치가 일어나게 된다.
        //  메서드 호출 과정에서 첫번째로 들어가는 것이 receiver parameter 라는 것이 들어간다.
        //  모든 클래스에 this가 정의되어 있는데 그것이 receiver parameter로 들어가 있다.
        //  그래서 실제 어떤것을 실행시킬지는 런타임시점에서 service에 할당되어있는 객체에 따라서 결정된다.
        service.run();

        // 예시
        List<Service> services = Arrays.asList(new MyService1(), new MyService2());
        // note : 이렇게 생성했을 떄는 어떤 클래스가 사용될지 한눈에 보이지 않는데
        //  런타임에서 실제로 어떤 객체가 할당 되어 있는지에 따라 컬렉션을 반복할 때 해당 객체의 메서드가 실행된다.
        services.forEach(Service::run);

    }
}

 

Double dispatch 란

더블 디스패치란? 말 그대로 Dynamic dispatch가 2번 일어나는 것이다.

 

  • 페이스북, 트위터에 사진과 텍스트를 올려주는 요구사항이 들어왔다고 가정하자.
  • 확장성을 고려해서 Post와 SNS를 인터페이스로 만들었다.
  • posts.forEach(p -> sns.forEach(s -> p.postOn(s))) 에서 런타임 시점에 어떤 객체인지 확인하고 알맞은 메소드를 호출해준다. 다이나믹 디스패치가 발생한다.
public class Dispatch {
    interface Post{ void postOn(SNS sns); }
    static class Text implements Post{
        @Override
        public void postOn(SNS sns){
            System.out.println("text -> " + sns.getClass().getSimpleName());
        }
    }
    static class Picture implements Post{
        @Override
        public void postOn(SNS sns) {
            System.out.println("picture -> " + sns.getClass().getSimpleName());
        }
    }

    interface SNS { }
    static class Facebook implements SNS{ };
    static class Twitter implements SNS{ };
    
    public static void main(String[] args) {
        List<Post> posts = Arrays.asList(new Text(), new Picture());
        List<SNS> sns = Arrays.asList(new Facebook(), new Twitter());
	/*for (Post post : posts) {
            for (SNS sn : sns) {
                post.postOn(sn);
            }
        }*/
        posts.forEach(p -> sns.forEach(s -> p.postOn(s)));
    }
}

  • 만약 Text와 Picture 메소드에 SNS마다 다른 비지니스 로직을 적용하려면 어떻게 해야 할까?
		static class Text implements Post{
        @Override
        public void postOn(SNS sns){
            if(sns instanceof Facebook){
                System.out.println("text - facebook");
            }
            if(sns instanceof Twitter){
                System.out.println("text -twitter");
            }
        }
    }
    static class Picture implements Post{
        @Override
        public void postOn(SNS sns) {
            if(sns instanceof Facebook){
                System.out.println("picture - facebook");
            }
            if(sns instanceof Twitter){
                System.out.println("picture -twitter");
            }
        }
    }
  • instanceof 로 객체를 구분해서 나눠주면 된다. 하지만 이 방법의 문제점이 있다. 새로운 SNS가 추가될 때마다 if문을 Text와 Picture 메소드에 계속 추가 해줘야 되는데 한쪽 메소드에 빼먹는 경우가 생길 수 있다.

 

  • 그러면 어떻게 객체지향스럽게 타입을 구분하도록 할 수 있을까?
  • 첫 번째 방법은 각각의 SNS 용 메소드를 만들어주는 것이다.
	interface Post{
        void postOn(Facebook sns);
        void postOn(Twitter sns);
    }
    static class Text implements Post{
        @Override
        public void postOn(Facebook sns) {
            System.out.println("text-facebook");
        }
        @Override
        public void postOn(Twitter sns) {
            System.out.println("text-twitter");
        }
    }
    static class Picture implements Post{
        @Override
        public void postOn(Facebook sns) {
            System.out.println("picture-facebook");
        }
        @Override
        public void postOn(Twitter sns) {
            System.out.println("picture-twitter");
        }
    }
  • 하지만 이 방법도 하나의 문제가 발생한다. 컴파일시점에서 에러가 발생한다.

출처) https://www.notion.so/6-dcaa52b834464c1eb1a4600a3812627c

 

  • postOn 메소드가 호출될 때 메소드 오버로딩이 발생하는데 컴파일시점에 파라미터의 타입을 체크해서 호출할 메소드를 정해둬야한다. 즉 스태틱 디스패칭을 한다. 그런데 위의 코드에서 s는 인터페이스 타입이고, postOn에 파라미터 타입은 더 상세한 타입으로 정의되어있다. 스태틱 디스패치를 하려면 컴파일러는 컴파일시점에 인터페이스 타입의 s의 정보로만 정확히 어떤 메소드를 호출 해야할지 결정해야하는데 어느 메소드를 호출할 지 컴파일 시점에는 정할 수 없다. 그래서 에러가 발생한다.
  • 자바는 Receiver Parameter가 하나인 싱글디스패치 언어이다. p.postOn에서 Post의 객체를 결정하기 위한 Receiver Parameter가 사용되었기 때문에 파라미터 s 에 대한 타입을 다이나믹하게 결정할 수 없었던 것이다.

두번째 방법으로 기존 코드를 변경하지 않고, 더 확장성 있게 만들기 위해서 더블디스패치를 사용해보자.

public class Dispatch {
    interface Post{
        void postOn(SNS sns);
    }
    static class Text implements Post{
        @Override
        public void postOn(SNS sns) {
            sns.post(this);
        }
    }
    static class Picture implements Post{
        @Override
        public void postOn(SNS sns) {
            sns.post(this);
        }
    }

    interface SNS {
        void post(Text post);
        void post(Picture post);
    }
    static class Facebook implements SNS{
        @Override
        public void post(Text post) {
            System.out.println("text - facebook");
        }

        @Override
        public void post(Picture post) {
            System.out.println("picture - facebook");
        }
    };
    static class Twitter implements SNS{
        @Override
        public void post(Text post) {
            System.out.println("text - twitter");
        }

        @Override
        public void post(Picture post) {
            System.out.println("picture - twitter");
        }
    };
    
    public static void main(String[] args) {
        List<Post> posts = Arrays.asList(new Text(), new Picture());
        List<SNS> sns = Arrays.asList(new Facebook(), new Twitter());

        posts.forEach(p -> sns.forEach(s -> p.postOn(s)));
    }
}

 

  • 기존의 코드를 냅두고 각 SNS에 비지니스로직을 SNS 구현 클래스에 옮겨놨다. posts.forEach(p -> sns.forEach(s -> p.postOn(s))) Post 타입이 결정되고, sns 를 파라미터로 넘겨준 다음 postOn(SNS sns) 메소드에서 SNS 타입이 결정된다. 즉 디스패치가 2번 발생해서, 더블 디스패치라 한다.

이것의 장점은 새로운 SNS가 추가되어도 코드의 변경이 적다는 점이다. 새로운 SNS를 추가하려고 할 때 밑에 코드만 추가하면 무리없이 동작한다.

 

	static class Instagram implements SNS{
        @Override
        public void post(Text post) {
            System.out.println("text - instagram");
        }

        @Override
        public void post(Picture post) {
            System.out.println("picture - instagram");
        }
    }
	public static void main(String[] args) {
        List<Post> posts = Arrays.asList(new Text(), new Picture());
        List<SNS> sns = Arrays.asList(new Facebook(), new Twitter(), new Instagram());

        posts.forEach(p -> sns.forEach(s -> p.postOn(s)));
    }

 

출처) https://www.notion.so/6-dcaa52b834464c1eb1a4600a3812627c

 

참고)

www.notion.so/e5c33507880b4d098f83a2c4f8f02c04

 

상속

목표

www.notion.so

www.notion.so/6-dcaa52b834464c1eb1a4600a3812627c

 

6주차 과제: 상속

목표

www.notion.so

토비의 봄 - https://www.youtube.com/watch?v=s-tXAHub6vg

 

도서) 이것이 자바다.

'스터디 > 백기선 라이브 스터디(자바)' 카테고리의 다른 글

8주자 과제: 인터페이스  (0) 2021.02.22
7주차 과제: 패키지  (0) 2021.02.19
5주차 과제: 클래스  (0) 2021.02.08
3주차 과제: 연산자  (0) 2021.01.18
4주차 : 제어문  (0) 2020.12.12
Comments