상속

이상민·2021년 7월 7일
0
post-thumbnail

1. 자바의 상속

1-1. 상속

  • 객체 지향 프로그래밍의 기본 아이디어 중 하나로, 객체들 간의 관계를 구축하는 방법이다

  • 고전적으론 기존에 있던 클래스로부터 속성과 동작을 상속받아 새로운 클래스를 만드므로서, 객체 간 관계를 설정하고, 코드의 중복을 최소화 할 수 있다.

  • 아래는 클래스 간 관계를 표현할 때 자주 사용하는 클래스 다이어그램이다. SUV와 SEDAN 클래스는 Car 클래스의 모든 속성과 동작을 가지고, 추가로 새로운 속성과 동작을 가진다.

class Car {
    var mSpeed;
    void accelerate(float value) {...}
    void break(float value) {...}
}

class SUV extends Car {
    boolean misOffRoadMode;
    void setOffRoadMode(boolean mode) {...}
    boolean isOffRoadMode() {...}
}

class SEDAN extends Car {
    int mDrivingMode;
    void setDrivingMode(int mode) {...}
}

1-2. 자바 상속의 특징

  • extends 예약어를 통해 상속을 한다

  • 다중 클래스의 상속을 지원하지 않는다

  • 부모 클래스의 생성자는 상속되지 않는다

  • private 접근 지정자를 가진 변수나 메소드가 있다면, 변수는 상속 받으나 바로 접근이 불가능하고, 메소드는 상속되지 않는다

  • 정적 변수 및 메소드도 상속이 된다

  • Object 클래스가 최상위 클래스이며, 유일하게 Object 클래스만 부모 클래스를 가질 수 없다


2. super 키워드

super : 부모 클래스를 가리키는 참조 변수

  • super.variable 또는 super.method()와 같이 부모 클래스의 멤버에 접근 가능하다

  • super()는 부모 클래스의 생성자를 호출한다. 생성자 내에서 사용하려면, this()와 마찬가지로 생성자의 맨 첫줄에 작성해야한다

자식 클래스의 인스턴스는 부모 클래스의 멤버까지 포함하고 있기 때문에, 부모 클래스의 생성자까지 호출해야한다. 모든 클래스는 Object 클래스의 자식 클래스이기 때문에, Object 클래스의 생성자까지 계속 거슬러 올라가 호출한다. 생성자에 super()가 호출되지 않으면, 컴파일러가 자동으로 생성자 첫줄에 추가한다. 인자를 받지 않는 기본 생성자도 신경써야하는 이유가 여기에 있다. 만약 기본 생성자가 없다면, 해당 클래스를 상속하는 클래스에서 super()를 호출할 경우 오류가 발생하기 때문이다.

// 오류 발생
class Parent {
    int a;
    Parent(int n) { a = n; }
}

class Child extends Parent {
    int b;
    Child() {
        super();
        b = 20;
    }
// 정상 작동
class Parent {
    int a;
    Parent() { a = 10; }
    Parent(int n) { a = n; }
}

class Child extends Parent {
    int b;
    Child() {
        super();
        b = 20;
    }

3. 메소드 오버라이딩

OOP의 다형성을 구현하기 위한 방법 중 하나로 자식 클래스에서 동일한 시그니쳐를 같는 메소드를 재정의 해서 다르게 작동하게 하는 것

3-1. 오버라이딩의 조건

  1. 메소드의 선언부는 기존 메소드와 완전히 동일해야한다. 단 메소드의 반환 타입이 부모 클래스의 반환 타입으로 변환될 수 있다면, 변경할 수 있다

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

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

3-2. 자바의 오버라이딩

class Parent {
    void smth() {...}
}

class Child {
    @Override 
    void smth() {...}
}

@Override 애너테이션은 컴파일러에게 해당 메소드가 부모 클래스의 메소드를 오버라이딩한다는 것을 알려준다. 애너테이션을 달지 않아도 오버라이딩은 가능하지만, 가독성 측면에서나 컴파일 타임 검증 및 오류 확인한다는 점에서 쓰는게 좋다.


4. 다이나믹 메소드 디스패치

오버라이딩 + 업캐스팅으로 실행시간 다형성 구현

  • 기존 메소드와 오버라이딩된 메소드들 중 어떤 메소드를 실행할지는 런타임에 결정된다
class Car {
    void printType() {
        System.out.println("자동차");
    }
}

class SEDAN extends Car {
    @Override
    void printType() {
        System.out.println("세단");
    }
}

class SUV extedns Car {
    @Override
    void printType() {
        System.out.println("SUV");
    }

}

public class Main {
    public static void main(String[] args) {
        Car ref = new Car();
        ref.printType  // 자동차
        // 업캐스팅
        ref = new SEDAN();
        ref.printType  // 세단
        // 업캐스팅
        ref = new SUV();
        ref.printType  // SUV
    }
}

다이나믹 메소드 디스패치가 중요한 이유는 클라이언트 --> 서플라이어 의존관계의 클라이언트 클래스의 재사용성을 높이기 위해서이다. 서플라이어를 클래스가 아닌 인터페이스로 하므로써 재사용성을 높일 수 있는데 List<Integer> list = new ArrayList<Integer>(); 처럼 흔히 인터페이스 참조변수로 사용하는 이유이기도 하다.


5. 더블 디스패치


6. 추상 클래스

추상 메소드를 하나 이상 가진 클래스

  • 객체의 설계도인 클래스의 설계도
  • 클래스의 설계도이기 때문에 객체를 생성할 수 없다
  • 객체를 생성할 수는 없지만, 자식 객체가 생성될 때 객체화 되므로 super()로 생성자를 호출할 수 있다
  • 공통적인 부분은 추상 메소드로 작성하고 자식 클래스에서 구현하면 되므로 관계가 더 명확하다

6-1. 추상 클래스의 용도

  • 설계와 구현을 분리
abstract class Calculator {
    public abstract int add(int a, int b);
    public abstract int subtract(int a, int b);
    public abstract double average(int[] a);
}

public class GoodCalc extends Calculator {
    @Override
    public int add(int a, int b) { 
        return a + b;
    }
    
    @Override
    public int subtract(int a, int b) { 
        return a - b;
    }
    
    @Override
    public double average(int[] a) { 
        double sum = 0;
        for (int i = 0; i <a.length; i++)
            sum += a[i];
        return sum/a.length;
    }
}
  • 계층적 상속 관계를 갖는 클래스 구조의 구현
abstract class Animal {
    abstract void sound();
}

class Dog extends Animal {
    @Override
    void sound() {
        System.out.println("멍멍");
    }
}

class Cat extends Animal {
    @Override
    void sound() {
        System.out.println("야옹");
    }
}

7. final 키워드

  • 종단 요소들을 생성하기 위한 키워드

7-1. final 클래스

  • 종단 클래스를 생성한다
  • 주로 보안상 이유로 종단 클래스가 필요하다
final class FinalClass {
   ...
}

class SubClass extends FinalClass {  // 컴파일 오류
   ...
}

7-2. final 메소드

  • 오버라이딩이 불가능한 종단 메소드를 생성한다
class SuperClass {
    final int finalMethod() {...}
}

class SubClass extends Superclass {
    @Override  // 컴파일 오류
    int finalMethod() {...}
}

7-3. final 필드

  • 상수를 선언할 때 사용한다
  • 상수 필드는 선언시에 초기 값을 지정해야하고, 런타임 중에 변경할 수 없다
class SharedClass {
    final double PI = 3.14;
}

8. Object 클래스

부모를 가지지 않는 최상위 클래스

  • 모든 클래스를 Object 클래스를 상속한다
  • 자주 사용하는 Object 클래스가 제공하는 공통 메소드들에 익숙해지자

8-1. toString

  • 객체를 프린트 했을 때 출력하는 내용의 정의하는 메소드
  • 기본적으로는 어떤 클래스의 인스턴스인지, 고유 식별 값은 무엇인지를 출력한다
  • 아래처럼 변경하여 조금 더 유용하게 사용할 수 있다
class Dog {
    @Override
    public String toString() {
        return "Dog 객체" + '@' + Integer.toHexString(hashCode());
    }
}

8-2. clone

  • 해당 객체를 복제해 새로운 객체를 생성한다
  • clone 하기 위해선 Cloneable 인터페이스를 구현해야한다

8-3. equals

  • 객체의 값을 비교하는 메소드
  • 단순히 ==로 객체를 비교하면 동일한 객체인지를 확인하기 때문에 값을 비교하기 위해선 equals 메소드를 사용해야한다
  • equals()를 직접 구현 시 hashCode()도 함께 구현해야한다
class Student {
    String name;
    Student (String name) {
        this.name = name;
    }
    
    @Override
    public boolean equals(Object obj) {
        Student _obj = (Student)obj;
        return name == _obj.name;
    }
}
profile
편하게 읽기 좋은 단위의 포스트를 추구하는 개발자입니다

0개의 댓글