1. 상속 관계
상속은 객체 지향 프로그래밍의 핵심 요소 중 하나로, 기존 클래스의 필드와 메서드를 새로운 클래스에서 재사용하게 해 준다. 이름 그대로 기존 클래스의 속성과 기능을 물려받는 것이라고 보면 된다.
부모와 자식
자식이 부모의 기능을 물려받아서 사용할 수는 있지만, 부모는 자식에 접근할 수 없다.
- 부모(슈퍼) 클래스
- 상속을 통해 자신의 필드와 메서드를 다른 클래스에 제공하는 클래스
- 자식(서브) 클래스
- 부모 클래스로부터 필드와 메서드를 상속받는 클래스
단일 상속
extends 키워드를 사용해 상속할 수 있으며, 이때 extends의 대상은 하나만 선택할 수 있다. 자바는 다중 상속을 지원하지 않는다.
- 비행기와 자동차를 상속받아서 하늘을 나는 자동차를 만든다고 가정해 보자. 만약 아래 그림처럼 다중 상속을 사용하게 되면 자식 클래스 입장에서 move() 메서드를 호출할 때 어떤 부모의 메서드를 사용해야 할지 판단하기 어렵다. 이것을 다이아몬드 문제라고 부른다.
- 추가로, 다중 상속을 지원하면 클래스 계층 구조가 매우 복잡해질 수 있다.
이런 문제점 때문에 자바는 클래스의 다중 상속을 허용하지 않는다. 대신, 이후 설명하는 인터페이스의 다중 구현을 허용해 이런 문제를 피한다.
상속과 메모리 구조
이 부분을 제대로 이해해야 한다.
- 상속 관계의 객체를 생성하면, 그 내부에는 부모와 자식이 모두 생성된다.
- 상속 관계의 객체를 호출할 때, 대상 타입을 정해야 한다. 이때 호출자의 타입을 통해 대상 타입을 찾는다.
- 현재 타입에서 기능을 찾지 못하면, 상위 부모 타입으로 기능을 찾아서 실행한다. 기능을 찾지 못하면 컴파일 오류가 발생한다.
상속과 메서드 오버라이딩
부모에게서 상속받은 기능을 자식이 재정의하는 것을 메서드 오버라이딩(Overriding)이라고 한다.
- 오버라이딩한 메서드 위에 @Override 애노테이션을 사용해, 상위 클래스의 메서드를 오버라이드한다는 것을 나타낼 수 있다.
- 컴파일러는 이 애노테이션을 보고 메서드가 정확히 오버라이드 됐는지 확인하고, 오버라이딩 조건을 만족시키지 않으면 컴파일 에러를 발생시킨다.
public class ElectricCar extends Car {
@Override
public void move() {
System.out.println("이동");
}
public void charge() {
System.out.println("충전");
}
}
메서드 오버라이딩 조건은 아래와 같다.
- 메서드 이름
- 메서드 이름이 같아야 한다.
- 메서드 매개변수(파라미터)
- 매개변수(파라미터) 타입, 순서, 개수가 같아야 한다.
- 반환 타입
- 반환 타입이 같아야 한다. 단, 반환 타입이 하위 클래스 타입일 수 있다.
- 접근 제어자
- 오버라이딩 메서드의 접근 제어자는 상위 클래스의 메서드보다 더 제한적이어서는 안 된다.
- ex. 상위 클래스의 메서드가 protected로 선언돼 있으면 하위 클래스에서 이를 public 또는 protected로 오버라이드할 수 있지만, private나 default는 불가능하다.
- 예외
- 오버라이딩 메서드는 상위 클래스의 메서드보다 더 많은 체크 예외를 throws로 선언할 수 없다.
- 더 적거나 같은 수의 예외, 또는 하위 타입의 예외는 선언할 수 있다.
- static, final, private
- 키워드가 붙은 메서드는 오버라이드 될 수 없다.
- static은 클래스 레벨에서 작동하므로, 인스턴스 레벨에서 사용하는 오버라이딩이 의미가 없다.
- final 메서드는 재정의를 금지한다.
- private 메서드는 해당 클래스에서만 접근 가능하기 때문에 하위 클래스에서 보이지 않는다. 따라서 오버라이드 할 수 없다.
- 생성자 오버라이딩
- 생성자는 오버라이딩 할 수 없다.
상속과 접근 제어
자식 클래스는 부모 클래스의 public, protected 필드나 메서드만 접근할 수 있다.
- 본인 타입에 없으면 부모 타입에서 기능을 찾는데, 객체 내부에서는 자식과 부모가 구분돼 있기 때문에 접근 제어자가 영향을 준다. 결국 자식 타입에서 부모 타입의 기능을 호출할 때, 부모 입장에서 보면 외부에서 호출한 것과 같다.
2. super
부모 참조
부모와 자식의 필드명이 같거나 메서드가 오버라이딩 돼 있으면, 자식에서 부모의 필드나 메서드를 호출할 수 없다. 이때 사용하는 게 super 키워드다. super는 이름 그대로 부모 클래스에 대한 참조를 나타낸다.
public class Child extends Parent {
public String value = "child";
@Override
public void hello() {
System.out.println("hello");
}
public void call() {
System.out.println("this.value = " + this.value); // this 생략 가능
System.out.println("super value = " + super.value);
this.hello(); // this 생략 가능
super.hello();
}
}
생성자
상속 관계의 인스턴스를 생성하면 결국 메모리 내부에는 자식과 부모 클래스가 각각 다 만들어진다. Child를 만들면 부모인 Parent까지 함께 만들어지는 것이다. 따라서 각각의 생성자도 모두 호출돼야 한다.
- 상속 관계를 사용하면 자식 클래스의 생성자에서 부모 클래스의 생성자를 반드시 호출해야 한다.
- 상속 관계에서 부모의 생성자를 호출할 때는 super(...)를 사용하면 된다.
예제를 통해 상속 관계에서 생성자를 어떻게 사용하는지 알아보자.
- 클래스를 상속받으면 생성자의 첫 줄에 super(...)를 사용해서 부모 클래스의 생성자를 호출해야 한다.
- 상속 관계에서 첫 줄에 super(...)를 생략하면 자바는 부모의 기본 생성자를 호출하는 super()를 자동으로 만들어 준다.
public class ClassA {
public ClassA() {
System.out.println("ClassA");
}
}
///
public class ClassB extends ClassA {
public ClassB(int a) {
super(); // 부모 기본 생성자 O -> 생략 가능
System.out.println("ClassB a = " + a);
}
public ClassB(int a, int b) {
super(); // 부모 기본 생성자 O -> 생략 가능
System.out.println("ClassB a = " + a + " b = " + b);
}
}
- 부모 클래스에 기본 생성자가 없는 경우엔 super()를 사용하거나 생략할 수 없다.
public class ClassC extends ClassC {
public ClassC() {
super(10,20); // 부모 기본 생성자 X -> 생략 불가능
System.out.println("ClassC");
}
}
- 이 상태에서 아래 코드를 실행하면 ClassA → ClassB → ClassC 순서로 실행된다. 생성자의 실행 순서가 결과적으로 최상위 부모부터 실행돼서 하나씩 내려오는 것이다.
public class SuperMain {
public static void main(String[] args) {
ClassC c = new ClassC();
}
}
코드 첫 줄에 this(...)를 사용하더라도 반드시 한 번은 super(...)를 호출해야 한다.
public class ClassB extends ClassA {
public ClassB(int a) {
this(a,0);
System.out.println("ClassB a = " + a);
}
public ClassB(int a, int b) {
super();
System.out.println("ClassB a = " + a + " b = " + b);
}
}
///
public class SuperMain {
public static void main(String[] args) {
// ClassC c = new ClassC();
ClassB b = new ClassB(100);
}
}