본문 바로가기

프로그래밍 언어/자바

[Java] 상속

학습할 것

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

 

자바 상속의 특징

 

 상속을 받는 Subclass는 Superclass의 public or protected 메소드와 필드를 상속받게 된다.
만약, Subclass가 Superclass와 동일한 패키지에 있다면, Superclass의 package-private 멤버도
상속받게 된다.

  • 상속받은 필드와 메소드는 직접 접근할 수 있다.
  • Superclass의 필드와 동일한 이름의 필드를 Subclass에서 선언할 수 있다. 그러나, 이 경우 hiding이 발생한다. hiding된다고 해서 Superclass의 해당 필드가 사라지는 것은 아니다.
  • Superclass의 instance 메소드와 동일한 이름의 instance 메소드를 Subclass에서 선언할 수 있다. 이를 overrideing이라한다.
  • Superclass의 staic 메소드와 동일한 이름의 static 메소드를 Subclass에서 선언할 수 있다. 이 경우, instance 메소드와는 다르게 hiding이 발생한다.
  • Subclass의 생성자에서 Superclass의 생성자를 호출할 수 있다. 이때 super 키워드를 사용한다.
  • Subclass에서는 Superclass의 private한 field를 직접 접근할 수 없다. 그러나, 만약 Superclass에서 해당 field에 접근할 수 있는 public or protected한 메소드를 정의했을 떄는 Subclass에서 Superclass의 필드를 간접적으로 사용할 수 있다.

Object클래스를 제외하고, 자바의 모든 클래스는 하나의 클래스를 상속받으며, 오직 하나의 직접적인

Superclass로 부터 상속된다. 모든 클래스는 암묵적으로 Object의 Subclass이다.

 

자바의 클래스는 다중 상속이 불가능하다. 인터페이스의 경우에는 가능하지만 클래스는 안된다.

왜냐하면, 인터페이스는 field를 가지고 있지 않지만, 클래스는 field를 가지고 있기 때문이다.

만약, 어느 클래스가 다중 상속을 했다고 하자. 해당 클래스의 Superclass가 동일한 이름의 field

가지고 있으며, Superclass들의 메소드들에서 동일한 이름의 field에 대해서 초기화 작업을 수행하면

어떻게 될까? 어느 클래스의 field인지 알 수 없기 때문에 오류가 발생할 것이다.

반면, 인터페이스는 애초에 field를 가지고 있지 않기 때문에 이러한 걱정을 할 필요가 없다.

 

super 키워드

Superclass의 멤버로 접근할 경우

만약, Subclass에서 Superclass의 메소드를 오버라이딩했을 경우, 오버라이딩 당한 Superclass의 메소드를

호출할 수 있을까? 그렇다! 또한, hiding된 Superclass의 필드 또한 접근할 수 있다. 이때 super 키워드를

사용한다.

public class Subclass extends Superclass{
    private int field = 20;
    @Override
    public void print(){
        super.print();
        System.out.println("Subclass's method");
    }

    private void printFieldAndSuperFiled(){
        System.out.println(Integer.toString(field)+" "+ Integer.toString(super.field));
    }

    public static void main(String[] args) {
        Subclass subclass = new Subclass();
        subclass.print();
        subclass.printFieldAndSuperFiled();
    }
}
Superclass's method
Subclass's method
20 10

 

Subclass의 생성자에서 Superclass의 생성자를 호출할 경우

 

Subclass의 생성자에서 Superclass의 생성자를 호출하는 것은 명시적인 방법과 암시적인 방법이 있다.

명시적인 방법으로 호출하는 것은 super()를 사용하는 것이다. 해당 키워드는 Superclass의 생성자를

호출한다. argument가 있는 생성자를 호출하고 싶다면 super(a,b)와 같은 형태로 호출할 수 있다.

이때 주의할 점은 Superclass의 생성자가 Subclass의 생성자의 첫줄에 호출되어야 한다는 것이다.

 

public class ConstructorSuperclass {
    public int a;
    public  int b;

    public ConstructorSuperclass(int a, int b){
        this.a = a;
        this.b = b;
    }
}
public class ConstructorSubclass extends ConstructorSuperclass {
    public int c;

    ConstructorSubclass(int a, int b, int c){
        super(a,b);
        this.c = c;
    }

    public static void main(String[] args) {
        ConstructorSubclass constructorSubclass = new ConstructorSubclass(1,2,3);
    }
}

 

암시적인 호출방법은 말 그대로 Subclass의 생성자에서 super키워드를 사용하여 Superclass의 생성자를

호출하지 않는 것이다. 자바 컴파일러는 자동으로 Superclass의 생성자를 호출한다. 그런데, no-argument인

Superclass의 생성자를 호출하기 때문에, Superclass에서 no-argument인 생성자가 없다면 컴파일 에러가

발생할 것이다.

 

따라서 위 코드처럼 Superclass의 no-argument인 생성자가 없다면 명시적으로 Subclass의

생성자에서 Superclass의 생성자를 호출해줘야 한다.

 

메소드 오버라이딩

Subclass에서 Superclass의 메소드를 오버라이딩할 때 @Override어노테이션을 사용한다. 이 어노테이션을 사용하면 컴파일러는 Subclass의 어노테이트된 메소드가 Superclass에 있는 지 확인하고, 만약 없다면 error를 발생시킨다.

 

메소드는 instance method와 static method로 나뉜다. Superclass의 instance method와 동일한 형태의 메소드를 Subclass에서 정의하면 메소드 오버라이딩이 진행되고, static method의 경우에는 메소드 하이딩이 진행된다.

  • 오버라이딩 된 instance method가 호출될 경우, Subclass의 메소드가 호출된다.
  • 하이딩 된 static method가 호출될 경우, 누가 호출했냐에 따라 호출되는 메소드가 달라진다. Superclass의 래퍼런스에서 호출할 경우, Superclass의 메소드가 호출되고, Subclass에서 호출되면 Subclass의 것이 호출된다.

 

다이나믹 메소드 디스패치

다이나믹 메소드 디스패치는 오버라이딩된 메소드가 호출될 때, 어떤 메소드가 호출되는 지 컴파일타임이 아닌, 런타임에 결정되는 메커니즘을 말한다.

public class Dispatch {
    static class A{
        void m1(){
            System.out.println("Inside A's m1 method");
        }
    }
    static class B extends A{
        void m1(){
            System.out.println("Inside B's m1 method");
        }
    }
    static class C extends A{
        void m1(){
            System.out.println("Inside C's m1 method");
        }
    }

    public static void main(String[] args) {
        A a = new A();
        B b = new B();
        C c = new C();

        A ref;
        ref = a;
        ref.m1();
        ref = b;
        ref.m1();
        ref = c;
        ref.m1();
    }
}

 

위에서 m1()메소드를 호출하면 왠지 A의 메소드가 호출될 것 같다. 왜냐하면 ref는 class A 타입의 래퍼런스 변수이기 때문이다. 하지만, 자바는 dynamic method dispatch를 지원하기 때문에, 런타임에 해당 래퍼런스 변수가 가리키고 있는 객체의 메소드가 호출된다. 이때, 래퍼런스 변수는 쌩뚱맞은 객체가 아닌 상속관계에 있는 객체를 가리키고 있어야 한다.

 

그렇다면, 왜 자바는 dynamic method dispatch를 지원하는 것일까? polymorphism(다형성)을 지원하기 위함이다. 이게 무슨 말일까?

 

예를 들어, virtual function을 생각해보자. 자바는 c++과는 다르게 virtual키워드를 사용 하지 않고, virtual function을 나타낸다. derived class에서 overridding 가능한 모든 메소드는 virtual function이다. 그런데 만약, 어는 Subclass가 interface를 implements했다고 하자.

interface에 해당하는 래퍼런스 변수를 사용해서 Subclass가 overridding한 메소드를 호출하고 싶을 때, dynamic method dispatch를 지원하지 않으면 Subclass의 메소드를 호출할 수 없게 된다. 심지어 interface에서 default method를 정의하지 않았으면 not sense하게 된다.

 

따라서, 컴파일 타임에는 어느 메소드가 호출되는 지 몰라도 런타임에 생성되는 메소드를 호출하려면, dynamic method dispatch를 지원해야 하는 것이다. 이는 스프링과도 관련이 많다. 스프링에서 IoC를 구현할 때, interface를 통해서 메소드를 호출한다. 왜냐하면 실제 호출될 메소드는 외부에서 주입하기 때문이다.

 

토비의 봄 1화 : 재사용성과 다이나믹 디스패치, 더블 디스패치

interface Post{void postOn(SNS 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");
            }
        }
    }

    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());

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

 

instanceof는 안티 패턴으로 지정되있기 때문에 위와같은 코드는 지양해야 한다. SNS 인터페이스를 parameter로 받아서
dynamic dispatch를 구현한 것은 잘했지만, instanceof를 통해 넘겨받은 클래스의 종류를 확인하는 것은 수정되야 한다.

 

    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");
        }
    }

    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(), new GooglePlus());

        posts.forEach(p->sns.forEach((SNS s)->p.postOn(s)));
    }
    // polymorphism의 type을 결정하는 것을 runtime이 아닌
    // compile time에 결정하려고 하였기 때문에 발생한 문제이다.
    // dynamic method dispatch는 parameter type을 기준으로
    // 하지 않는다. parameter type을 기준으로 하는 것은
    // overriding이다. 얘는 static dispatch에서 사용하는 것이다.

 

위 코드는 에러가 발생하는 코드이다. 메소드 오버로딩은 정적 메소드 디스패치를 사용하기 때문에, 컴파일 타임에
메소드의 parameter와 변수의 type이 일치해야 한다. forEach문에서 Text or Picture의 객체를 넘겨주지 않고,
SNS 객체를 넘겨주기 때문에, 컴파일러는 어떤 메소드가 호출되어야 하는 지 알 수 없다. 이 때문에 에러가 발생한다.

 

    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");
        }
    }
    static class GooglePlus implements SNS{
        @Override
        public void post(Text post) {
            System.out.println("text-GooglePlus");
        }

        @Override
        public void post(Picture post) {
            System.out.println("picture-GooglePlus");
        }
    }

    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 GooglePlus());

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

 

위 코드는 더블 디스패치를 구현한 것이다. 더블 디스패치를 사용하면서 목표는 다음과 같았다.

  1. 메소드 오버로딩을 사용하지 않는다. 동적 디스패치를 사용하도록 한다.
  2. instanceof를 사용하지 않는다. Post 인터페이스의 구현체는 SNS의 구현체가 무엇인지 신경쓰지 않도록 한다.
    1번을 수행하기 위해 Post 인터페이스의 postOn(s) 메소드는 SNS 구현체를 parameter로 받는다. 이때 parameter로
    어떤 SNS의 구현체가 들어올 지는 런타임에 정해진다. 즉 첫번째 동적 디스패치가 수행된다.

2번을 수행하기 위해 Post 인터페이스의 구현체는 parameter로 받은 SNS 구현체의 post(this)메소드를 호출한다.
이때 parameter로 자기자신(this)를 넘겨준다. 컴파일 타임에는 넘겨받은 sns가 SNS의 어느 구현체인지 알 수 없다.
이는 런타임에 결정된다. 두번째 디스패치가 수행된다.

 

추상 클래스

추상 클래스는 abstract 키워드로 정의되는 클래스이다. 추상 클래스는 abstract method를 가지고 있을 수도 있고 없을 수도 있다. 추상 클래스는 초기화(객체로 생성)될 수 없다. 오직, Subclass로 상속될 수 밖에 없다.

 

추상 메소드(abstract method)는 implementation없이 정의된 함수이다. 추상 메소드는 Subclass에서 implement되어야 한다. 따라서, 추상 클래스를 상속받은 클래스는 추상 클래스의 추상 메소드들을 반드시 overridding해야 한다. 그래야만 객체를 생성할 수 있다.

public abstract class GraphicObject{
    abstract void draw();
}

public class Circle extends GraphicObject{
    @Override
    void draw(){
        System.out.println("Circle draw");
    }
}

 

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

 

추상 클래스는 인터페이스와 유사하다. 단독으로 초기화될 수 없다. 그러나 추상 클래스는 인터페이스와는 다르게

static 혹은 final이 아닌 필드를 선언할 수 있으며, public 혹은 protected 혹은 private한 메소드를 선언할 수 있다.

 

인터페이스에서는, 모든 필드들은 자동으로 public하고 static하며, final이다. 그리고 모든 메소드들은 public으로 선언된다.

추가적으로 추상 클래스는 오직 하나의 클래스만 상속되는데 반해, 인터페이스는 다중 구현이 가능하다. 이러한 차이점들 때문에 추상 클래스와 인터페이스를 사용하는 경우는 다르다.

  • 추상클래스를 사용하는 경우
    • 관련된 클래스들과 코드를 공유하고 싶을 때
    • 추상 클래스를 상속받는 클래스의 메소드들과 필드가 자주 사용되며, public이 아닌 접근 지정자를 설정하고 싶을 때
    • static 혹은 final이 아닌 필드를 선언하고 싶을 때
  • 인터페이스를 사용하는 경우
    • 관련되지 않은 클래스들에서 인터페이스가 구현되는 것을 기대할 때. 예를 들어서 Comparable and Cloneable클래스가 있다.
    • 특정한 데이터 타입의 행동을 구현하고 싶지만, 누가 그 행동을 구현하지는 신경쓰고 싶지 않을 때
    • 다중 상속의 이점을 누리고 싶을 때
abstract class GraphicObject {
    int x, y;
    void moveTo(int newX, int newY){
        x = newX;
        y = newY;
    }
    abstract void draw();
    abstract void resize();
}
public class Circle extends GraphicObject{
    @Override
    void draw() {
        System.out.println("Circle draw");
    }

    @Override
    void resize() {
        System.out.println("Circle resize");
    }
}
public class Rectangle extends GraphicObject{
    @Override
    void draw() {
        System.out.println("Rectangle draw");
    }

    @Override
    void resize() {
        System.out.println("Rectangle resize");
    }
}

 

final 키워드

 

final키워드는 클래스의 메소드에 선언할 수 있다. final키워드를 메소드에 붙이면, 해당 메소드가 Subclass에서 오버라이딩되지 못하게 막을 수 있다.

메소드가 치명적인 영역을 다룰 경우 final로 지정하여야 한다. 또한, 생성자에서 호출되는 메소드들은 일반적으로 final로 지정되어야 한다.
왜냐하면, 해당 메소드가 오버라이딩될 경우, 예기치 못한 결과가 발생할 수 있기 떄문이다.

메소드 뿐만 아니라, 클래스도 final로 지정될 수 있다. immutabl한 클래스를 정의하고 싶을 때 이를 사용한다. 예를 들어 'String' 클래스가 있다.

public class FinalTestClass {
    final void testMethod(){}
}
public class DerivedClass extends FinalTestClass{
    void testMethod(){}
}

위 경우 에러가 발생한다. 왜냐하면, DerivedClass에서 FinalTestClass이 final 메소드를 오버라이딩하려고 했기 때문이다.

 

Object 클래스

Object 클래스는 5주차 : 클래스에서
설명하고 있기 때문에 생략한다.

 

참고자료

'프로그래밍 언어 > 자바' 카테고리의 다른 글

[Java] 패키지  (0) 2023.11.28
[Java] Enum  (0) 2023.11.21
[Java] 클래스  (1) 2023.11.21
[Java] 제어문  (0) 2023.11.21
[Java] 연산자  (0) 2023.11.21