상속(Inheritance)은 OOP(Object Oriented Programming)의 네 가지 원칙 중 하나이다.

  • 두 객체 사이의 "is-a"와 "has-a", "is-a" 또는 "has-a"의 관계
  • super class(parent class) vs sub class(child class)
  • 이미 존재하는 super class의 코드를 재사용

Extending a class

class Employee {
  private String name;
  private int salary;
  public Employee() {
    this.name = "Noname";
    this.salary = 0;

  public String getName() { return this.name; }
  public void setName(String name) { this.name = name; }
  public int getSalary() { return this.salary; }
  public void setSalary(int salary) { this.salary = salary; }
  }

위와 같은 클래스가 있다고 해 보자. 우리는 이 Employee 클래스를 확장한 클래스를 구현할 수 있다.

class Manager extends Employee {
  private int bonus;
  public void setBonus(int bonus) { this.bonus = bonus; }
}
  • Manager 클래스는 Employee 클래스의 모든 멤버(변수와 메서드)를 상속한다.
  • Manager 클래스는 자신만의 클래스 정의에 정의된 새로운 멤버를 가진다.
    • bonus, setBonus()

Super class vs. Sub class

Manager 클래스는 다음과 같이 쓸 수 있다.
Employee 클래스에 정의된 메서드(getName, getSalary)가 어떻게 Manager 클래스의 인스턴스에서 호출되는지 주의하자.

public class Lecture {
  public static void main(String[] args) {
    Manager m = new Manager();
    System.out.println(m.getName() + " " + m.getSalary();
  }
}

Employee 클래스는 Manager 클래스의 superclass다.
Manager 클래스는 Employee 클래스의 subclass다.

Method overriding

만약 Manager 클래스가 getName이나 getSalary 같은 메서드를 정의하고 있지 않다면, 그의 superclass(Employee)에 있는 메서드가 사용된다.
Manager 클래스의 정의에 getSalary를 정의하는 것도 가능하다.
이 경우, Manaer 클래스의 getSalary 메서드는 Employee 클래스의 getSalary 메서드를 오버라이드(override)하고, 이 메서드가 사용된다.
이를 메서드 오버라이딩(Method Overriding)이라고 한다.

  • super 키워드는 superclass를 가리키는 지시자(directive)이다.
  • 오버라이딩 메서드는 superclass의 메서드와 같은 매개변수 타입을 가져야 한다.
class Manager extends Employee {
  private int bonus;
  public void setBonus(int bonus) { this.bonus = bonus; }
  public int getSalary() {
    return super.getSalary() + bonus;
  }

super.getSalary()를 호출하는 대신에 바로 인스턴스 변수에 접근할 수는 없을까?

  • 불가능하다. 변수 salary는 private으로 정의되어 있기 때문이다.
  • 만약 public 또는 protected 변수라면, subclass에서 접근할 수 있다.
  • protected는 같은 패키지의 클래스와 subclass에서 visible하다.
class Employee {
  private String name;
  private int salary;
  public Employee() {
    this.name = "Noname";
    this.salary = 0;
  }

  public String getName() { return this.name; }
  public void setName(String name) { this.name = name; }
  public int getSalary() { return this.salary; }
  public void setSalary(int salary) { this.salary = salary; }
 }

class Manager extends Employee {
  private int bonus;
  public void setBonus(int bonus) { this.bonus = bonus; }
  public int getSalary() {
    return this.salary + bonus // Error! salary is private in class Employee
 }

만약 subclass의 메서드가 superclass의 메서드와 같은 이름을 가졌지만 다른 매개변수 타입을 가진다면 그 메서드는 오버라이딩 메서드가 아니다.
만약 프로그래머가 오버라이딩 메서드를 구현하길 원한다면@override라는 어노테이션(annotation)을 사용할 수 있다. 이는 실수를 방지하게 해 준다.

  • 이 어노테이션이 있는 경우, 만약 메서드가 오버라이딩 메서드가 아니라면 컴파일 에러가 일어난다.
class Employee {
  private String name;
  private int salary;
  public Employee() {
    this.name = "Noname";
  }
  public boolean worksFor(Employee supervisor) {
    System.out.println("Employee.worksFor");
    return (this.supervisor == supervisor);
  }
}

class Manager extends Employee {
  @Override // this will cause compile error
  public boolean worksFor(Manager supervisor) {
    System.out.println("Manager.worksFor");
    return (this.supervisor == supervisor);
  }
}

메서드를 오버라이드할 때, 리턴 타입을 subclass로 변경할 수 있다.
아래의 getSupervisor 메서드는 Employee 대신 Manager를 반환함에도 오버라이딩 메서드이다.

class Employee {
  private String name;
  private int salary;
  public Employee() {
    this.name = "Noname";
  }
  public Employee getSupervisor() {
    System.out.println("Employee");
    return supervisor;
  }
}

class Manager extends Employee {
  @Override // this is valid
  public Manager getSupervisor() {
    System.out.println("Manager");
    return (Manager)supervisor;
}

메서드를 오버라이드할 때, 오버라이딩 메서드는 최소한 superclass의 메서드와 같은 접근 가능성을 가져야 한다.
오버라이딩 메서드는 상속된 메서드의 visibility를 줄일 수 없다.

  • visibility: public > protected > private
class Employee {
  private String name;
  private int salary;
  public Employee() {
    this.name = "Noname";
  }
  public Employee getSupervisor() {
    System.out.println("Employee");
    return supervisor;
  }
}

class Manager extends Employee {
  @Override // Error! the overriding method has less visibility
  protected Manager getSupervisor() {
    System.out.println("Manager");
    return (Manager)supervisor;
}

Creating a subclass

subclass를 생성할 때, 그의 생성자는 그것의 superclass의 생성자를 호출한다.

  • Manager 클래스의 인스턴스를 생성할 때, 기본 생성자가 호출된다.
  • 기본 생성자는 superclass의 생성자를 호출한다.
class Employee {
  private String name;
  private int salary;
  public Employee() {
    name = "Noname";
    salary = 50000;
  }

  public String getName() { return this.name; }
  public int getSalary() { return this.salary; }
}

class Manager extends Employee {
  private int bonus;
  public void setBonus(int bonus) { this.bonus = bonus; }
  public int getSalary() { return super.getSalary() + this.bonus; }
}
  • superclass의 생성자는 subclass 생성자의 시작에 호출된다. 따라서 superclass의 생성자가 항상 먼저 호출되고 그 다음 subclass의 생성자가 동작한다.
class Employee {
  private String name;
  private int salary;
  public Employee() {
    name = "Noname";
    salary = 50000;
  }

  public void setName(String name) { this.name = name; }
  public String getName() { return this.name; }
  public int getSalary() { return this.salary; }
}

class Manager extends Employee {
  private int bonus;
  public Manager () {
    super.setName("NoName(Manager)");
    bonus = 1000;
  }
  public void setBonus(int bonus) { this.bonus = bonus; }
  public int getSalary() { return super.getSalary() + this.bonus; }
}
  • super()를 사용해서 명시적으로 superclass의 생성자를 호출할 수 있다.
    • super()문은 생성자의 첫 번째 문장이어야 한다. 그렇지 않으면 에러를 발생시킨다!
  • 만약 superclass의 생성자를 명시적으로 호출하지 않으면, 매개변수가 없는 superclass의 생성자를 호출하려고 할 것이다.
    • 만약 superclass에 그런 생성자가 없다면, 에러가 발생한다.
class Employee {
  private String name;
  private int salary;
  public Employee(String name, int salary) {
    this.name = name;
    this.salary = salary;
  }

  public String getName() { return this.name; }
  public int getSalary() { return this.salary; }
}

class Manager extends Employee {
  private int bonus;
  public Manager (String name, int salary) {
    //super(name, salary); // Error! Employee does not have a constructor with no parameter.
    bonus = 10000;
  }

  public void setBonus(int bonus) { this.bonus = bonus; }
  public int getSalary() { return super.getSalary() + this.bonus; }
}

Detour: Implicit Constructor

만약 클래스 정의가 생성자를 포함하지 않는다면, 암시적 생성자가 거기 있는 것으로 친다.

  • 암시적 생성자는 매개변수롤 가지지 않고, 만약 superclass가 있는 경우 superclass 생성자를 호출하는 것 외에는 아무것도 하지 않는다.

어떤 생성자가 정의되면, 암시적 생성자는 더 이상 사용되지 않는다.

  • 매개변수가 존재하는 생성자가 정의되면, 그 클래스는 매개변수 없는 생성자를 가지지 않는다.
class Employee {
  private String name;
  private int salary;
  public Employee(String name, int salary) {
    this.name = name;
    this.salary = salary;
  }
}
Employee e = new Employee(); //Error! no Employee() exists

Dynamic method lookup

subclass의 객체를 superclass 타입의 객체에 할당할 수 있다.

Manager boss = new Manager();
Employee emp1 = boss;
int salary = emp1.getSalary(); // call getSalary defined in both Employee and Manager.

만약 getSalary 메서드를 호출하면 Manager 클래스에 정의된 메서드가 호출된다.
boss가 Employee 타입의 변수에 할당되었더라도, 그것은 Manager 클래스의 객체인 것이다. empl은 Employee 타입이지만 가리키는 객체의 클래스(Manager)의 메서드를 호출하게 된다.
JVM은 객체의 클래스를 확인하고 맞는 메서드를 호출한다. 이를 Dynamic method lookup이라고 한다. Dynamic method lookup은 다음과 같은 것을 가능하게 해 준다. (Manager와 Janitor는 Employee 클래스의 subclass이다.)

Employee[] staff = new Employee[...];
staff[0] = new Employee(...);
staff[1] = new Manager(...);
staff[2] = new Janitor(...);

JVM이 런타임에 dynamic method lookup을 수행한다고 하더라도, 다음의 코드는 컴파일 에러를 발생시킨다.

Employee emp1 = new Manager("Donald", 100000);
emp1.setBonus(20000); // setBonus is only defined in class Manager

왜냐하면 컴파일러는 emp1.setBonus(20000); 문장을 프로세싱할 때 Employee 클래스에서 setBonus 메서드를 찾기 때문이다. 컴파일 에러를 피하고 싶다면, 클래스를 subclass로 변환해 줘야 한다.

Employee emp1 = new Manager("Donald", 100000);
if(emp1 instanceof Manager) {
  Manager mgr = (Manager) emp1;
  mgr.setBonus(20000);
}
System.out.println(emp1.getSalary());