자바 상속

Jeongmin Yeo (Ethan)·2020년 12월 25일
3

STUDY HALLE

목록 보기
6/13
post-thumbnail

자바의 상속에 대해 정리합니다.

백기선님과 함께하는 자바 6주차 스터디 과정입니다.

학습할 내용은 다음과 같습니다.

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

Reference: Oracle Java Document


1. 자바 상속의 특징

상속(inheritance)이란 기존의 클래스에 기능을 추가하거나 재정의하여 새로운 클래스를 정의하는 것을 의미합니다.

이러한 상속은 캡슐화, 추상화와 더불어 객체 지향 프로그래밍을 구성하는 중요한 특징 중 하나입니다.

상속을 이용하면 기존에 정의되어 있는 클래스의 모든 필드와 메소드를 물려받아, 새로운 클래스를 생성할 수 있습니다.

이때 기존에 정의되어 있던 클래스를 부모 클래스(parent class) 또는 상위 클래스(super class), 기초 클래스(base class)라고도합니다.

그리고 상속을 통해 새롭게 작성되는 클래스를 자식 클래스(child class) 또는 하위 클래스(sub class), 파생 클래스(derived class)라고도 합니다.

클래스는 다른 클래스를 통해 파생될 수 있고 궁극적으로는 최상위 클래스인 Object에서 파생될 수 있습니다.

What You Can Do in a SubClass

  • private 접근 제어자로 된 필드를 제외한 상속된 필드들은 직접적으로 사용이 가능합니다.

  • superClass에서 선언된 필드와 같은 이름으로 subClass에서 필드로 정의할 수 있습니다. (Hiding it, not recommended)

  • 새로운 필드와 메소드를를 subClass에서 추가할 수 있습니다. 이건 superClass에는 적용이 되지 않습니다.

  • 상속된 메소드는 직접 사용할 수 있고 superClass에서 정의한 메소드를 subClass에서 재정의할 수 있습니다. 이때 동일한 Signature를 가지게됩니다. (Overriding it)

  • subClass에서 constructor()를 작성할 수 있고 super keyword를 통해 superClass의 constructor()를 불러올 수 있습니다.

  • superClass의 static method와 동일한 이름을 subClass의 instance method 이름으로 할 수 없습니다.

  • superClass의 instance method와 동일한 이름을 subClass의 static method 이름으로 할 수 없습니다.

Casting Objects

예시를 붙여서 설명하겠습니다.

    // 상속을 명시적으로 지정하지 않았으므로 Object를 기본으로 상속하고 있습니다. 
    class Bicycle {
      ...
    }
    
    // Byclce을 상속합니다. 
    class MountainBike extend Bicycle {
      ... 
    }
    
    public static void main(String[] args) {
        // Ok 
        MountainBike mountainBike = new MountainBike();
   		
        // Ok
        Bicycle Bicycle = new MountainBike(); 
        
        // Ok 
        Object obj = new MountainBike();
        
        // Compile Error 
        MountainBike mountainBike = new Bicycle();
  
        // Compile Error   
        Bicycle bycicle = new Object(); 
        
    }

MountainBike는 Bicycle과 Object로부터 물려받은 클래스입니다. 그러므로 MountainBike는 Bicycle과 Object가 될 수 있습니다.

하지만 이 역은 성립하지 않습니다. Bicycle을 MountainBike처럼 사용할 수 없습니다.

캐스팅은 상속 및 구현에서 사용되는 오브젝트 중에서 다른 타입의 오브젝트를 사용하는 것을 보여주는 예시입니다.

    // Casting Case 
    Object obj = new MountainBike(); // This is called implicit casting.
        
    MountainBike myBike = obj; // Compile Error

    MountainBike myBike = (MountainBike)obj; // OK

    // logical Test using the instanceof operator. 
    if (obj instanceof MountainBike) {
         MountainBike myBike = (MountainBike)obj;
    }

2. super 키워드

Accessing Superclass Members

super 키워드는 부모 클래스로부터 상속받은 필드나 메소드를 자식 클래스에서 참조하는 데 사용하는 참조 변수입니다.

인스턴스 변수의 이름과 지역 변수의 이름이 같을 경우 인스턴스 변수 앞에 this 키워드를 사용하여 구분할 수 있었습니다.

이와 마찬가지로 부모 클래스의 멤버와 자식 클래스의 멤버 이름이 같을 경우 super 키워드를 사용하여 구별할 수 있습니다.

이렇게 자바에서는 super 참조 변수를 사용하여 부모 클래스의 멤버에 접근할 수 있습니다.

this와 마찬가지로 super 참조 변수를 사용할 수 있는 대상도 인스턴스 메소드뿐이며, 클래스 메소드에서는 사용할 수 없습니다

super() 메소드

this() 메소드가 같은 클래스의 다른 생성자를 호출할 때 사용된다면, super() 메소드는 superClass의 생성자를 호출할 때 사용됩니다.

subClass의 인스턴스를 생성하면, 해당 인스턴스에는 subClass의 고유 멤버뿐만 아니라 superClass의 모든 멤버까지도 포함되어 있습니다.

따라서 superClass의 멤버를 초기화하기 위해서는 subClass의 생성자에서 superClass의 생성자까지 호출해야만 합니다.

이러한 superClass의 생성자 호출은 모든 클래스의 superClass인 Object 클래스의 생성자까지 계속 거슬러 올라가며 수행됩니다.

따라서 자바 컴파일러는 superClass의 생성자를 명시적으로 호출하지 않는 모든 subClass의 생성자 첫 줄에 자동으로 다음과 같은 명령문을 추가하여, superClass의 멤버를 초기화할 수 있도록 해줍니다.

Subclass Constructors

super Keyword는 또 subClass의 constructor에서 superClass의 생성자를 호출해 초기화 작업을 할 수 있습니다.

subClass의 생성자에서 super를 통해 superClass의 constructor를 명시적으로 지정하지 않는다면 Java Compiler에 의해 no-argument constructor가 호출되게 됩니다. 만약 superClass에서 no-argument constructor이 없다면 컴파일 에러가 발생합니다.

그리고 subClass에서 super를 통해 명시적으로 superClass의 생성자를 호출한다면 constructor chaining을 생각해볼필요가 있습니다.


3. 메소드 오버라이딩

자바에서 subClass는 superClass의 private 멤버를 제외한 모든 메소드를 상속받습니다.

이렇게 상속받은 메소드는 그대로 사용해도 되고, 필요한 동작을 위해 재정의하여 사용할 수도 있습니다.

즉, 메소드 오버라이딩이란 상속받은 superClass의 메소드를 재정의하여 사용하는 것을 의미합니다.

스태틱 메소드의 경우에도 subClass에서 동일한 Signature와 리턴 타입을 가진 메소드를 정의할 수 있고 superClass의 스태틱 메소드를 hiding 할 수 있습니다.

Overriding Condition

자바에서 메소드를 오버라이딩하기 위한 조건은 다음과 같습니다.

  1. 오버라이딩이란 메소드의 동작만을 재정의하는 것이므로, 메소드의 선언부는 기존 메소드와 완전히 같아야 합니다. 이 말은 Signature가 같다는 말로 메소드 이름과 매개변수를 말합니다.

    하지만 메소드의 반환 타입은 부모 클래스의 반환 타입으로 타입 변환할 수 있는 타입이라면 변경할 수 있습니다.

  2. 부모 클래스의 메소드보다 접근 제어자를 더 좁은 범위로 변경할 수 없습니다

  3. 부모 클래스의 메소드보다 더 큰 범위의 예외를 선언할 수 없습니다.

Interface methods

인터페이스의 default method와 abstract method도 클래스의 인스턴스 메소드처럼 상속받을 수 있고 재정의할 수 있습니다.이런 경우 superClass의 instance method가 interface의 default 메소드가 동일한 Signature를 가지는 상황이 발생할 수 있습니다. 이 경우 다음의 룰을 따릅니다.

1. Instance methods are preferred over interface default methods.

public class Horse {
    public String identifyMyself() {
        return "I am a horse.";
    }
}

public interface Flyer {
    default public String identifyMyself() {
        return "I am able to fly.";
    }
}

public interface Mythical {
    default public String identifyMyself() {
        return "I am a mythical creature.";
    }
}

public class Pegasus extends Horse implements Flyer, Mythical {
    public static void main(String... args) {
        Pegasus myApp = new Pegasus();
        System.out.println(myApp.identifyMyself()); // The method Pegasus.identifyMyself returns the string I am a horse.
    }
}

2. Methods that are already overridden by other candidates are ignored. This circumstance can arise when supertypes share a common ancestor.

public interface Animal {
    default public String identifyMyself() {
        return "I am an animal.";
    }
}

public interface EggLayer extends Animal {
    default public String identifyMyself() {
        return "I am able to lay eggs.";
    }
}

public interface FireBreather extends Animal { }

public class Dragon implements EggLayer, FireBreather {
    public static void main (String... args) {
        Dragon myApp = new Dragon();
        System.out.println(myApp.identifyMyself()); // The method Dragon.identifyMyself returns the string I am able to lay eggs.


    }
}

만약 두 개 이상의 독립적인 인터페이스 default 메소드로 인해 충돌이 발생하는 경우 Java Compiler는 에러를 발생시킵니다. 이 경우 상속받는 클래스에서 명시적인 Overriding을 해줘야합니다.

public interface EggLayer {
    default public String identifyMyself() {
        return "I am able to lay eggs.";
    }
}

public interface FireBreather {
    default public String identifyMyself() {
        return "I am an animal.";
    }
}

public class Dragon implements EggLayer, FireBreather{

    public static void main(String[] args) {
        Dragon dragon = new Dragon();
        System.out.println(dragon.identifyMyself()); // Compile Error 
    }
}

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

메소드 오버라이딩은 Java가 런타임 다형성을 지원하는 방법 중 하나라면 다이나믹 메서드 디스패치는 컴파일 타임이 아닌 런타임에 오버라이드 메서드에 대한 호출이 해결되는 메커니즘입니다.

  • 오버라이드 메소드가 superClass 참조를 통해 호출될 때 Java는 호출 시 참조되는 객체를 보고 실행할 메소드의 버전(슈퍼 클래스인지 하위 클래스인지)을 결정합니다. 따라서 이 결정은 런타임에 수행됩니다.

  • 런타임때 실행될 재정의된(overriden) 메서드의 버전을 결정할때는 참조 변수 타입이 아닌 참조할 개체의 타입에 따라 다릅니다.

  • superClass의 참조 변수는 subClass 객체를 참조할 수 있습니다. 이건 업캐스트라고 합니다. Java는 이걸 이용해 런타임때 어떤 메소드를 호출할지 해결합니다.

그러므로, 만약 superClass에 있는 메소드를 subClass가 오버라이딩 하고있고 superClass 참조 변수가 서브 클래스를 참조하고 있다면 다른 버전의 메소드가 실행될 수 있습니다.

다음은 다이나믹 메서드 디스패치를 보여 주는 예제입니다.


public class Bicycle {

    // the Bicycle class has three fields
    public int cadence;
    public int gear;
    public int speed;
    protected String testPrivate;

    public Bicycle(int cadence, int gear, int speed) {
        this.cadence = cadence;
        this.gear = gear;
        this.speed = speed;
    }

    public void printDescription(){
    	System.out.println("\nBike is " + "in gear " + this.gear
        	+ " with a cadence of " + this.cadence +
        	" and travelling at a speed of " + this.speed + ". ");
    }

}

public class MountainBike extends Bicycle {
    private String suspension;

    public MountainBike(
               int startCadence,
               int startSpeed,
               int startGear,
               String suspensionType){
        super(startCadence,
              startSpeed,
              startGear);
        this.setSuspension(suspensionType);
    }

    public String getSuspension(){
      return this.suspension;
    }

    public void setSuspension(String suspensionType) {
        this.suspension = suspensionType;
    }

    @Override
    public void printDescription() {
        super.printDescription();
        System.out.println("The " + "MountainBike has a" +
            getSuspension() + " suspension.");
    }
} 

public class RoadBike extends Bicycle{
    // In millimeters (mm)
    private int tireWidth;

    public RoadBike(int startCadence,
                    int startSpeed,
                    int startGear,
                    int newTireWidth){
        super(startCadence,
              startSpeed,
              startGear);
        this.setTireWidth(newTireWidth);
    }

    public int getTireWidth(){
      return this.tireWidth;
    }

    public void setTireWidth(int newTireWidth){
        this.tireWidth = newTireWidth;
    }

    @Override
    public void printDescription(){
        super.printDescription();
        System.out.println("The RoadBike" + " has " + getTireWidth() +
            " MM tires.");
    }
}


public class TestBikes {
  public static void main(String[] args){
    Bicycle bike01, bike02, bike03;

    bike01 = new Bicycle(20, 10, 1);
    bike02 = new MountainBike(20, 10, 5, "Dual");
    bike03 = new RoadBike(40, 20, 8, 23);

    bike01.printDescription(); // Bike is in gear 1 with a cadence of 20 and travelling at a speed of 10. 
    bike02.printDescription(); // Bike is in gear 5 with a cadence of 20 and travelling at a speed of 10. The MountainBike has a Dual suspension.
    bike03.printDescription(); // Bike is in gear 8 with a cadence of 40 and travelling at a speed of 20. The RoadBike has 23 MM tires.
  }
}

5. 추상 클래스

자바에서는 하나 이상의 추상 메소드를 포함하는 클래스를 가리켜 추상 클래스(abstract class)라고 합니다.

이러한 추상 클래스는 객체 지향 프로그래밍에서 중요한 특징인 다형성을 가지는 메소드의 집합을 정의할 수 있도록 해줍니다.

즉, 반드시 사용되어야 하는 메소드를 추상 클래스에 추상 메소드로 선언해 놓으면, 이 클래스를 상속받는 모든 클래스에서는 이 추상 메소드를 반드시 재정의해야 합니다.

추상 클래스는 인스턴스화 할 수 없다는 특징이 있습니다.

추상 메소드는 자식 클래스에서 반드시 오버라이딩해야만 사용할 수 있는 메소드를 의미하며 일반 클래스에서는 쓸 수 없습니다. 그리고 추상 메소드는 구현부는 작성되지 않습니다.

Abstract Classes Compared to Interfaces

추상 클래스와 인터페이스는 유사한 점이 많습니다. 둘 다 인스턴스화 할 수 없으며 구현부가 없는 메소드를 선언할 수 있습니다.

하지만 추상 클래스의 경우 static과 final이 아닌 여러가지 필드들을 선언할 수 있고 메소드에도 여러 접근 제어자를 붙일 수 있습니다.

반면 인터페이스의 경우 필드는 반드시 public static final이 붙고 메소드의 접근 제어자는 클래스와 다르게 기본적으로 public이 붙습니다.

또 다른 차이점은 자바에서는 인터페이스는 여러개 상속받아 구현할 수 있지만 추상 클래스의 경우 클래스로 간주하므로 오직 하나만 상속받을 수 있습니다.

Which should you use, abstract classes or interfaces?

  • Consider using abstract classes

    • 관련된 클래스가 많아서 코드를 공유하는게 나은 경우

    • 추상 클래스를 상속받을 클래스의 경우 공통된 기능과 필드들이 많아 보이는 경우

    • static field가 아닌 field를 사용할 수 있으므로 메소드가 이런 객체의 필드에 접근해서 바꿀 수 있는 경우가 필요한 경우

  • Consider using interfaces

    • 인터페이스와 관련없는 클래스가 구현해도 괜찮은 경우 (e.g Comparable Cloneable )

    • Multiple Inheritance의 이점을 이용하고 싶은 경우

abstract class example AbstractMap

AbstractMap의 하위 클래스는 HashMap, TreeMap, and ConcurrentHashMap 등이 있습니다. 이런 Collection에서 공통적인 메소드( get, put, isEmpty, containsKey, and containsValue)는 AbstractMap에 정의되어 있습니다.

interface example HashMap

HashMap의 경우 여러 인터페이스를 구현하고 있습니다. (Serializable, Cloneable)

Serializable, Cloneable 이 인터페이스를 보면 알겠지만 HashMap이 아니더라도 저런 인터페이스를 사용하는 클래스들은 많을 것입니다.


6. final 키워드

자바의 모든 클래스 메소드에 final 키워드를 붙일 수 있습니다. 이 경우 메소드는 subClass에서 재정의할 수 없습니다.

Object Class에 보면 수 많은 final 키워드가 붙은 메소드를 볼 수 있습니다.

변경해서는 안되는 메소드 구현이 있다면 final 키워드를 메소드에 붙여서 사용하면 됩니다. 예를들어 ChessAlgorithm Class에서 getFirstPlayer() 메소드에 final 키워드를 붙이는 경우가 적합한 케이스 입니다.


7. Object 클래스

Object 클래스는 java.lang 패키지 중에서도 가장 많이 사용되는 클래스이며 모든 자바 클래스의 최고 조상 클래스 입니다.

따라서 자바의 모든 클래스는 Object 클래스의 모든 메소드를 바로 사용할 수 있습니다.

이러한 Object 클래스는 필드를 가지지 않으며, 총 11개의 메소드만으로 구성되어 있습니다.

Object 클래스의 메소드는 다음과 같습니다.

메소드설명
protected Object clone()해당 객체의 복제본을 생성하여 반환함.
boolean equals(Object obj)해당 객체와 전달받은 객체가 같은지 여부를 반환함.
protected void finalize()해당 객체를 더는 아무도 참조하지 않아 가비지 컬렉터가 객체의 리소스를 정리하기 위해 호출함.
Class getClass()해당 객체의 클래스 타입을 반환함.
int hashCode()해당 객체의 해시 코드값을 반환함.
void notify()해당 객체의 대기(wait)하고 있는 하나의 스레드를 다시 실행할 때 호출함.
void notifyAll()해당 객체의 대기(wait)하고 있는 모든 스레드를 다시 실행할 때 호출함.
String toString()해당 객체의 정보를 문자열로 반환함.
void wait()해당 객체의 다른 스레드가 notify()나 notifyAll() 메소드를 실행할 때까지 현재 스레드를 일시적으로 대기(wait)시킬 때 호출함.
void wait(long timeout)해당 객체의 다른 스레드가 notify()나 notifyAll() 메소드를 실행하거나 전달받은 시간이 지날 때까지 현재 스레드를 일시적으로 대기(wait)시킬 때 호출함.
void wait(long timeout, int nanos)해당 객체의 다른 스레드가 notify()나 notifyAll() 메소드를 실행하거나 전달받은 시간이 지나거나 다른 스레드가 현재 스레드를 인터럽트(interrupt) 할 때까지 현재 스레드를 일시적으로 대기(wait)시킬 때 호출함.
profile
좋은 습관을 가지고 싶은 평범한 개발자입니다.

0개의 댓글