공부/whiteship-java

6주차: 상속

chulphan 2020. 12. 24. 19:24

▶ 백기선님께서 주최하시는 자바 스터디

github.com/whiteship/live-study

 

whiteship/live-study

온라인 스터디. Contribute to whiteship/live-study development by creating an account on GitHub.

github.com

 

어느덧 6주차가 되었다.

 

객체지향의 프로그램의 핵심 중 한 부분을 배우게 되었다

 

생각보다 시간이 많이 걸렸고 이해가 안되는 부분들이 많았다 (오버라이드 된 메소드가 감춰지는 부분이라던가..)

 

반면에 대충 알고 있던 것에 대한 부분이 명확해진 것도 있었다 

(슈퍼 클래스의 부분 클래스들이 어떻게 타입을 추론해서 그들의 메소드를 실행하는지)

 

시작

 

 

▶ 자바 상속의 특징

먼저 자바 상속의 특징에 대해 알아보기 전에 상속이 무엇인지 알아보자

 

먼저 저번 주차 과제(클래스) 에서 Circle 이라는 클래스를 정의한 적이 있다

 

이번에는 원은 원인데 원의 크기와 원의 위치를 둘 다 갖는 원을 표현하고 싶다고 가정해보자

 

저 원에 대해서 표현하기 위해서 우리는 새로운 클래스인 PlaneCircle 을 정의한다

 

그런데 새로운 원을 정의하지만 기존에 Circle 클래스가 가지는 기능은 잃어버리지 않으면서

원의 위치를 표현하는 능력을 추가하고 싶다

 

이렇게 하기 위해서 새로 정의할 PlaneCircle 클래스를 Circle 클래스의 부분 클래스로 정의해서

PlaneCircle 클래스는 Circle 클래스가 가지고 있는 필드들과 메소드들을 물려 받는다(inherit)

 

이러한 객체지향 프로그래밍의 패러다임을 상속이라고 한다

 

◎ 이제 Circle 클래스를 확장하는 (상속 받는) 코드를 작성해보면서 상속의 특징을 알아보자

public class PlaneCircle extends Circle {
    private final double cx, cy;

    public PlaneCircle(double r, double x, double y) {
        super(r);
        this.cx = x;
        this.cy = y;
    }

    public double getCentreX() {
        return cx;
    }

    public double getCentreY() {
        return cy;
    }

    public boolean isInside(double x, double y) {
        double dx = x - cx;
        double dy = y - cy;
        double distance = Math.sqrt(dx * dx + dy * dy);

        return distance < r;
    }
}

 

♠ 특징1: 슈퍼 클래스로 부터 필드들과 메소드들을 물려 받는다

위 코드의 첫번째 줄에 'extends' 키워드를 사용함으로써 Java 에게 PlaneCircle 은 Circle 클래스를 확장한다고 얘기해준다

확장이라는 의미는 슈퍼 클래스인 Circle 클래스에 정의 되어 있는 필드들과 메소드들을 물려 받는다는 것이다

 

PlaneCircle 에 정의된 isInside() 메소드의 내부를 보면 Circle 에 정의 되어있는 필드 r 을 가져다 쓰고 있는데

이는 필드 상속을 보여주는 예이다

 

    public boolean isInside(double x, double y) {
        double dx = x - cx;
        double dy = y - cy;
        double distance = Math.sqrt(dx * dx + dy * dy);
        
        // 필드 r 을 이 클래스에 정의된 것처럼 사용하고 있다
        return distance < r;
    }

 

그리고 PlaneCircle 은 Circle 클래스에 정의된 메소드들을 물려 받았다

그러므로 변수 pc에 의해 참조되는 PlaneCircle 객체를 가지고 있다면 다음과 같이 

Circle 에 정의된 메소드를 사용할 수 있다

 

PlaneCircle pc = new PlaneCircle(1.0, 1.0, 1.0);
// 메소드가 부분 클래스인 PlaneCircle 에 정의된 것처럼 사용할 수 있다
double ratio = pc.circumference() / pc.area();

 

♠ 특징2: 슈퍼 클래스를 확장하는 모든 부분 클래스들은 슈퍼 클래스이다

위에서 살펴봤듯이 PlaneCircle 클래스는 Circle 클래스를 확장하였다

이제 PlaneCircle 클래스를 인스턴스화 하여 얻는 객체는 완벽히 Circle 객체가 된다

즉, 슈퍼 클래스인 Circle 타입의 변수에 부분 클래스인 PlaneCircle 객체를 할당할 수 있다는 것이다

 

PlaneCircle pc2 = new PlaneCircle(2.0, 1.0, 1.0);
Circle c = pc2;

※ 부분 클래스의 객체를 슈퍼 클래스의 변수로 할당할 때에 타입 변환 문법을 따로 추가하지 않았다

※ 이때 Circle c 로 할당이 되면 우리가 부분 클래스에서 정의했던 필드나 메소드에 대한 정보는 모두 잃어버린다

 

위의 코드에서 Circle 변수 c 에 저장된 값은 여전히 PlaneCircle 객체로 유효하다

하지만 자바 컴파일러는 이에 대해서 알지 못하므로 반대의 변환에 대해서는 타입 변환 문법을 추가해주지 않으면

에러를 일으킨다 (Narrow Type Conversion, 큰 타입에서 작은 타입으로 변환하는 것)

 

// 슈퍼 클래스 타입인 Circle 에서 부분 클래스 타입인 PlaneCircle 타입으로 변환했다
// Narrow Type Conversion
PlaneCircle pc3 = (PlaneCircle) c;

// 이때, 부분 클래스에서 정의한 메소드에 대해 접근이 가능하고 사용 가능해진다
boolean originInSide = pc3.isInside(1.0, 1.0);

 

♠ 특징3: 두 개 이상의 클래스를 확장하지 못한다. 즉, 하나의 슈퍼 클래스만 확장할 수 있다

자바를 제외한 어떤 언어에서는 두 개 이상의 클래스를 확장할 수 있다고 들은 것 같다

하지만 Java 에서는 이를 허용하지 않는다.

 

▶ super 키워드

위에서 PlaneCircle 클래스에 대한 성성자를 다시 한 번 살펴보자

public PlaneCircle(double r, double x, double y) {
    super(r);
    this.cx = x;
    this.cy = y;
}

 

PlaneCircle 클래스 생성자가 하는 일은 다음과 같다

  • 이 클래스에 새롭게 정의된 필드 cx, cy 를 초기화 하고 있다
  • 상속 받은 클래스의 필드들을 슈퍼 클래스인 Circle 생성자에 의존하여 초기화 하고 있다
    => 여기서 슈퍼 클래스 생성자를 호출하기 위해서 super() 를 호출하고 있다

¿ super 키워드??

  • super 키워드는 자바의 예약어이다

  • 부분 클래스 생성자의 내부에서 슈퍼 클래스의 생성자를 실행하기 위해 주로 사용된다

super() 를 사용하는 것과 this() 를 사용하는 것은 같은 제약을 가진다

  • super() 는 생성자 내부에서만 사용할 수 있다
  • 슈퍼 클래스 생성자를 호출하기 위한 목적이므로 반드시 생성자 몸체 내에 첫번째 statement 로 나타나야한다

 

슈퍼 클래스에 하나 이상의 생성자가 정의 되어 있으면 super() 는 어떠한 인자들이 전달되었는지에 의존해서

슈퍼 클래스의 생성자 중 하나가 실행된다 (this() 를 사용한 것과 같은 이치)

 

♣ 생성자 체이닝과 기본 생성자

이제 super() 가 어떻게 동작하는지 알아보자

 

Java 는 클래스의 인스턴스가 생성될 때 마다 클래스의 생성자가 호출됨을 보장한다

또, 부분 클래스의 인스턴스가 생성될 때 마다 부분 클래스의 생성자가 호출됨을 보장한다

 

=> 즉, Java 는 반드시 모든 생성자가 슈퍼 클래스의 생성자를 호출하는 것을 보장한다

=> 그러므로 생성자 내부에서 다른 생성자 실행을 위한 this() 또는 super() 를 명시하지 않으면

      javac 컴파일러가 super() 에 대한 호출을 삽입한다 (인자가 없는 슈퍼 클래스의 생성자를 호출)

 

만약에 슈퍼 클래스에 아무 인자도 없는 생성자가 보이지 않을 경우(정의되어 있지 않을 경우) 이러한 암묵적인

실행은 컴파일 오류를 일으킨다

 

코드로 한 번 보자

 

먼저 A 클래스에는 하나의 인자를 받는 생성자를 정의해놓고, 

A 클래스의 부분 클래스 B를 정의한 다음에, B 클래스의 생성자를 정의해놓으면

어떤 일이 일어나는지 보는 것이다

 

class A {
    // 이 클래스는 인자가 없는 생성자가 정의되어 있지 않다
    public A(int a) {
    
    }
}

class B extends A {
    // 여기서 There is no default constructor available... 에러가 난다
    public B() {
    
    }
}

 

 

위에 적어놨듯이 java 컴파일러는 슈퍼 클래스 호출을 위한 super() 를 부분 생성자의 첫 부분에 넣는다

그런데 슈퍼 클래스 A 에서는 기본 생성자가 정의가 되어 있지 않으므로 에러를 내뱉는 것이다

 

★ 생성자 체이닝

 

☞ 이제 PlaneCircle 클래스의 새로운 인스턴스가 생성될 때 무슨 일이 일어나는지 알아보자

  1. PlaneCircle 생성자가 실행된다
  2. 이 생성자는 명시적으로 슈퍼 클래스 생성자 실행을 위한 super(r) 을 호출한다
  3. 이제 슈퍼 클래스인 Circle 의 생성자는 이 클래스의 슈퍼 클래스인 Object의 생성자 실행을 위한 super() 를 암묵적으로 호출한다
    (Object 는 생성자를 하나만 가지고 있다)
  4. 이 때에 클래스 계급의 최상위에 (Object) 도달했고 생성자들은 실행을 시작한다
  5. Object 생성자의 몸체가 처음으로 실행된다
  6. Object 생성자가 실행을 끝내면 Circle 생성자의 몸체가 실행된다
  7. 마지막으로 2번에서 호출했던 super(r) 에 대한 실행이 끝나면 PlaneCircle 의 생성자 나머지 부분의 statement들이 실행된다

위에서 설명한 일련의 과정들이 바로 생성자 체인이다

객체가 생성 될 때에는 생성자들의 수열이 실행되며 순서는 최하위의 부분 클래스에서 클래스 계급의 최상위로 올라간다

그리고 항상 부분 클래스의 생성자 첫번째 statement 에서 super() 가 실행되기 때문에 Object 생성자의 몸체가 처음으로 실행되고

그 아래로 내려가며 부분 클래스들의 생성자가 실행되고 인스턴스화 되는 것이다

 

★ 기본 생성자

Java 는 생성자 없이 선언된 클래스에 암묵적으로 클래스에 생성자를 추가한다.

이 기본 생성자는 슈퍼 클래스 생성자를 실행하는 것 외에 아무것도 하지 않는다

 

예를 들어, 우리가 A 라는 클래스에 아무 생성자도 명시하지 않으면, Java 는 암묵적으로 생성자를 추가한다

 

class A {

}

// 위와 같이 정의된 클래스는 Java 에서 암묵적으로
// 아래와 같이 처리한다

class A {
    public A() {
        super();
    }
}

 

※ 일반적으로 슈퍼 클래스에 기본 생성자를 정의해놓지 않으면 서브 클래스에서는 반드시 슈퍼 클래스 생성자와 그에 맞는 인자들에 대한         실행을 명시해줘야 한다

 

 

▶ 메소드 오버라이딩 (Method Overriding)

객체지향 프로그래밍에서 중요하고 유용한 기술인 메소드 오버라이딩에 대해 알아보자

 

어떤 클래스의 인스턴스 메소드가 그의 슈퍼 클래스와 같은 이름, 같은 리턴 타입, 같은 파라미터 타입 및 개수로 정의되어 있다면

이 인스턴스 메소드(부분 클래스의) 는 슈퍼 클래스의 메소드를 오버라이드(override) 했다고 한다

 

클래스의 객체에 대한 메소드가 실행할 때, 새로 정의한 메소드가(오버라이드 한 메소드) 가 실행된다. 

(슈퍼 클래스의 메소드가 실행되는 것이 아님!!)

 

클래스 메소드는 (static 과 함께 정의된) 오버라이드 될 수 없다. 클래스 필드와 마찬가지로, 클래스 메소드는 부분 클래스에 의해

가려지지 오버라이드 되지 않는다. 그러므로 클래스 필드나 메소드를 사용할 때에는 사용하고자 하는 클래스의 이름을 반드시 명시해서 사용해야한다 (ClassName.field / ClassName.method())

 

♣ 오버라이딩 한 메소드는 숨겨지지 않는다

클래스의 필드는 가려지지만 오버라이딩 된 메소드는 가려지지 않는다.

 

예를 들어, 슈퍼 클래스의 인스턴스 필드 이름과 그의 서브 클래스의 인스턴스 필드 이름이 같다고 하자

그리고 서브 클래스의 객체에서 슈퍼 클래스 인스턴스 필드의 값을 참조하고 싶다면 간단하게 

서브 클래스의 객체를 슈퍼 클래스의 객체 타입으로 변환하면 된다.

하지만 오버라이드 된 메소드는 이러한 테크닉으로 실행시킬 수가 없다는 것이다

 

코드를 보자

 

class A {
    int a = 1;
    
    // instance method
    int f() {
        return a;
    }
    
    // class method
    static String g() {
        return "Class A";
    }
}

class B extends A {
    int a = 2;
    
    // 오버라이드 한 메소드
    int f() {
        return -a;
    }
    
    // B의 클래스 메소드
    static g() {
        return "Class B";
    }
}

 

이렇게 정의된 클래스가 있다고 할 때에 부분 클래스 B의 객체를 사용하는 것은 간단하다

그런데 B의 객체에서 A에 정의된 필드나 클래스 메소드를 사용하려고 한다면 아래와 같이 하면 된다

 

B b = new B();
A a = b;

System.out.println(a.i); // A 의 인스턴스 변수 i
System.out.println(a.f()); // 그럼 얘는??
System.out.println(a.g()); // A의 클래스 메소드 g
System.out.println(A.g()); // A의 클래스 메소드 g

a.f() 는 어디에 정의되어 있는 메소드를 실행시킬까?? -_-? 한 번 생각해보시길

 

♣ 오버라이드 된(overridden) 메소드 실행시키기

오버라이드 된 메소드는 슈퍼 클래스의 객체의 메소드를 말한다

 

먼저 숨겨진 필드의 같은 경우에는 저번 주차 과제에서 살펴봤듯이 두 가지 방법이 있었다

1. super 키워드를 이용한 방법 => super.field

2. this 키워드를 슈퍼 클래스의 타입으로 변환하여 접근하는 방법 => ((A) this.i)

 

실제로 오버라이드 된 메소드는 위의 1번 방법대로 super 키워드로 접근하여 사용하는 것이 가능하다

super.f() 이런식으로....

 

하지만 2번 방법 같은 경우, 시도를 해보면 Stackoverflow 에러를 볼 수 있다...

 

그렇다면 field 에서 잘 쓰던 방법이 method 에서는 왜 안먹히는지 알아보자

 

인터프리터가 super 문법과 함께 쓰인 인스턴스 메소드를 실행할 때에 virtual lookup method 의 수정된 형식으로 실행된다

첫번째로 보편적인 virtual method lookup 처럼 메소드가 실행된 실제 객체의 클래스를 결정한다

보통은 런타임이 이 클래스와 함께 시작 될 적절한 클래스를 탐색한다

하지만 메소드가 super 문법과 같이 호출이 될 경우에 이 클래스의 슈퍼 클래스 부터 탐색하기 시작한다

만약에 슈퍼 클래스의 메소드가 직접적으로 구현이 되어 있다면 슈퍼 클래스의 메소드가 실행될 것이다.

만약에 슈퍼 클래스가 메소드를 물려줬으면 물려 준 버전 즉, 오버라이드 한 메소드가 실행될 것이다.

 

그런데 참고로, super 키워드는 가장 직접적으로 오버라이드 된 메소드를 실행한다

더 알아보기 위해서 클래스 A 는 클래스 B를 부분 클래스로, 클래스 B는 클래스 C를 부분 클래스로 갖고,

이 3개의 클래스 모두 f() 라는 메소드를 가지고 있다고 가정해보자

 

이 세 클래스의 계층구조를 그려보면 아래와 같다

 

 

C 의 메소드 f() 에서 B 의 메소드 f() (오버라이드 된 메소드) 를 super.f() 표현식을 통해 실행시킬 수 있다.

하지만 C 의 메소드 f() 에서 직접적으로 A 의 메소드 f() 를 실행시킬 수 있는 방법은 없다.

왜냐하면 Java 에서 super.super.f() 처럼 super 를 중첩해서 참조하는 것은 허용하지 않기 때문이다

 

▶ 다이나믹 메소드 디스패치(virtual method lookup)

먼저, 다이나믹 메소드 디스패치에 대해 찾아보니, 내가 이해한 바로는 책(Java in Nutshell) 에서 Virtual Method Lookup 이라고 설명해주는 것과 같은 개념인 것 같아서 정리해본다 이후에 찾아보고 틀린 개념이면 수정해야겠다

 

먼저 앞에서 적었듯이 슈퍼 클래스 ↔ 부분 클래스 관계에 있다면, 부분 클래스는 슈퍼 클래스의 타입이 된다

그러므로 부분 클래스는 타입 캐스팅 필요 없이 슈퍼 클래스의 타입의 변수에 할당 될 수 있다

 

PlaneCircle 이라는 클래스가 Circle 이라는 클래스를 확장한다고 하고, Circle[] 배열에 PlaneCircle 객체와 Circle 객체를 담았다고 하자

그리고 name() 이라는 메소드를 둘 다 가지고 있다고 가정하자 (PlaneCircle 에서 오버라이딩)

 

그리고 Circle[] 배열을 순회하면서, name() 이라는 메소드를 호출할 때에 javac 는 이 메소드가 Car 클래스의 것인지, PlaneCircle 클래스의 것인지 알 수 있는걸까?? 실제로, 소스코드 컴파일러는 이 것에 대해 컴파일 타임에는 알지 못한다

 

대신에 javac 는 실행 시에 virtual method lookup 을 사용하는 바이트 코드를 만든다

인터프리터가 저 코드를 실행할 때에, 배열 내에 각 객체에 대해 적당한 name() 이라는 메소드를 찾는다

다시 말하면, 인터프리터는 표현식 c.name() 을 해석하고 실제 실행 시에 변수 c 에 의해 참조되는 객체의 타입을 확인해서

그 타입에 적당한 name() 메소드를 찾아내는 것이다

 

JVM 은 정적 타입에 관련된 메소드를 (메소드 오버라이딩을 허락하지 않는 것처럼) 간단히 사용할 수 없다

 

이 말이 무슨 말인가 싶어서 코드를 통해 간단히 실험해보았다.

class A {
    int i = 1;
    
    int f() {
        return i;
    }
    
    static String g() {
        return "Class A";
    }
}

class B {
    int i = 1;
    
    int f() {
        return -i;
    }
    
    static String g() {
        return "Class B";
    }
}

A[] aa = {new A(), new B()};

for (int i = 0; i < aa.length; i++) {
    // 인스턴스 메소드 호출
    System.out.println(aa[i].f());
    
    // 클래스 메소드 호출
    System.out.println(aa[i].g());
}

위의 for 문에서 인스턴스 메소드를 호출하면 각 객체의 타입에 맞는 메소드가 호출되었지만지만

클래스 메소드 같은 경우 슈퍼 클래스에 정의된 것만 호출하는 현상을 확인했다

 

즉, 클래스 메소드의 경우에는 virtual method lookup 을 실행하지 않는 것으로 생각된다

 

Virtual Method Lookup 은 Java 인스턴스 메소드에 대해서 기본으로 적용된다

 

 

▶ 추상 클래스

몇 개의 도형에 대한 클래스 Rectangle, Square, Hexagon, Triangle 등등을 구현한다고 가정해보자

그리고 저 클래스들에 대해 기본적으로 모두 area()와 circumference() 메소드를 주어준다.

 

이제 도형들의 배열들에 대해 작업하기 쉽게 해주기 위해서 도형들에 대한 공통적인 슈퍼 클래스인 Shape 클래스를 만든다

 

이런 식의 계층 구조를 통해서 모든 도형 객체들은 실제로 표현되는 클래스가 무엇인지에 관계 없이 Shape 타입의 변수나 필드에

할당될 수 있고 Shape 타입의 배열의 원소가 될 수 있다

 

그리고 Shape 클래스가 저 도형들의 클래스들이 공통적으로 가지는 (예를 들어, area(), circumference()) 특징들을 캡슐화 하고 싶다

 

그러면서도 Shape 클래스는 실제로 어떤 도형인지를 표현하게 하고 싶지 않고 메소드에 대한 유용한 구현도 하고 싶지 않다

 

Java 는 이런 상황을 조작하기 위해 추상 메소드(abstract method) 를 제공한다

추상 메소드는 몸체를 가지고 있지 않다. 단지 메소드에 대한 서명과 그 뒤에 세미콜론(;) 만 붙는다.

 

다음은 추상 메소드와 추상 클래스에 대한 규칙이다

  1. 추상 메소드를 갖는 모든 메소드는 자동적으로 추상 클래스이다 그리고 반드시 abstract modifier 를 붙여서 선언해줘야한다. 그렇지 않을 경우 컴파일 에러가 난다
  2. 추상 클래스는 인스턴스화 될 수 없다
  3. 추상 클래스의 부분 클래스는 슈퍼 클래스에서 제공된 모든 추상 메소드를 구현했을 때에만 객체화 될 수 있다. 이러한 부분 클래스를 concrete 부분 클래스라고 불리며 이 클래스는 추상 클래스가 아니라는 사실을 강조하기 위함이다
  4. 추상 클래스의 부분 클래스가 슈퍼 클래스로 부터 상속 받은 모든 추상 메소드를 구현하지 않았다면, 부분 클래스는 그 자체로 추상 클래스가 되며 반드시 구현하지 않은 추상 메소드를 선언해줘야 한다
  5. static, private, final 메소드들은 추상 메소드가 될 수 없다. 왜냐하면 이러한 타입의 메소드들은 부분 클래스에 의해 오버라이드 될 수 없기 때문이다. 마찬가지 이유로 final 클래스는 어떠한 추상 메소드를 포함할 수 없다
  6. 추상 메소드를 하나도 갖지 않은 클래스여도 추상 클래스로 선언할 수 있다. 이러한 클래스를 선언하는 것은 여기서는 구현이 완벽히 되지 않았지만 부분 클래스에서 완벽하게 구현할 것이라는 의미를 가지고 있다. 그리고 이러한 클래스는 인스턴스화 될 수 없다

 

▶ final 키워드

먼저 final modifier 를 붙여서 클래스를 선언하면 해당 클래스는 확장되지 않는 클래스임을 의미한다

즉, 다른 클래스에서 이 클래스를 상속 받아 활용하지 못한다는 의미이다

 

다른 것들에 붙으면 어떻게 되는지 간략하게 정리해보자

  • final + method: 이 메소드는 override 되지 않음을 나타낸다
  • final + field: 한 번 정해진 필드의 값은 바뀌지 않음을 나타낸다. 그래서 선언과 동시에 할당해주거나, 생성자에서 반드시 초기화를 해주어야한다. 특히 static final 로 선언된 변수는 컴파일 타임 상수임을 나타낸다
  • final + variable: 지역변수, 메소드 매개변수 또는 예외 매개변수는 이들 값을 바꿀 수 없음을 나타낸다

 

▶ Object 클래스

프로그래머가 정의한 모든 클래스는 슈퍼 클래스를 가진다.

만약에 슈퍼 클래스가 extends 절이 없이 선언 되었다면 이는 java.lang.Object 클래스를 

상속 받고 있다는 의미이다

 

Object 클래스는 Java 에서 클래스 계급의 최상위에 있는 클래스이다

 

Object 클래스는 다음 두 가지 이유로 특별하다

  • Java 클래스들 중에서 Object 클래스만이 슈퍼  클래스를 가지지 않는다
  • Java 의 모든 클래스들은 Object 의 메소드들을 물려 받는다

 

※ super 와 super() 의 차이

위에서 super 와 super() 를 다른 단락에 정리해놓아서 나중에 햇갈릴 것 같아 정리해놓는다

 

먼저 super() 는 서브 클래스의 생성자 내부에서 슈퍼 클래스 생성자를 호출할 때 사용된다

또 super() 를 사용할 때에는 생성자 내부의 첫번째 statement 로 명시 되어야 한다

반면에 super 는 오버라이드 한 클래스(서브 클래스) 내부 어디에서든 오버라이드 된 메소드(슈퍼 클래스) 를 실행할 수 있다

 

 

[참조 내용]

Java in a Nutshell, 7th Edition by Ben Evans, David Flanagan (www.oreilly.com/library/view/java-in-a/9781492037248/)

[Dynamic method dispatch]: www.studytonight.com/java/dynamic-method-dispatch.php

'공부 > whiteship-java' 카테고리의 다른 글

8주차: 인터페이스  (0) 2021.01.08
7주차: 패키지  (0) 2021.01.01
5주차: 클래스  (0) 2020.12.19
4주차 과제: live-study 대시보드  (0) 2020.12.12
4주차 과제: JUnit5  (0) 2020.12.11