보통 현실에서 상속이라고 하면 부모가 자식에게 무언가를 물려주는 것으로 알고 있을 것이다. 이는 Java와 같은 언어에서도 상속은 부모가 자식에게 물려주는 것과 유사하다고 보면 된다. 하지만 현실에서는 부모가 자식을 선택해서 상속을 해주는 거라면 프로그래밍에서는 자식이 부모를 선택해서 상속을 받는 것이다.
정확하게 말하자면, 부모 클래스(상위 클래스)와 자식 클래스(하위 클래스)가 있을 때, 자식 클래스는 부모 클래스를 선택해서 그 부모의 멤버를 상속받아 그대로 쓸 수 있게 되는 것이다.
첫째, 부모 클래스의 private 접근 제한을 갖는 필드 및 메소드는 자식이 물려받을 수 없으며 부모와 자식 클래스가 서로 다른 패키지에 있다면, 부모의 default 접근 제한을 갖는 필드 및 메소드도 자식이 물려받을 수 없다.
둘째, 다른 상속을 지원하는 언어와 달리 Java는 다중 상속을 지원하지 않는다. 즉, 자식이 여러 부모를 가질 수 없다는 것이다. 이는 다중 상속으로 인해 발생할 수 있는 모호성을 방지하기 위함이다.
상속을 받는 방법은 자식 클래스에 extends를 붙여 상속을 받을 수 있다.
public class Animal {
String species;
public void eat() {
System.out.println("먹이를 먹다");
}
public void sleep() {
System.out.println("잠을 잔다");
}
}
public class Bird extends Animal {
String name;
int age;
int height;
int weight;
public Bird(String species, String name, int age, int height, int weight) {
this.species = species;
this.name = name;
this.age = age;
this.height = height;
this.weight = weight;
}
public void chirp() {
System.out.println("새가 운다");
}
}
public class ExampleMain {
public static void main(String[] args) {
// 자식 객체 생성
Bird bird = new Bird("chicken", "삐약이", 2, 70, 5);
// 부모로부터 상속 받은 필드
System.out.println(bird.species); // chicken
// 자식 클래스의 필드
System.out.println(bird.name); // 삐약이
System.out.println(bird.age); // 2
System.out.println(bird.height); // 70
System.out.println(bird.weight); // 5
// 부모로부터 상속받은 메소드 호출
bird.eat(); // 먹이를 먹다
bird.sleep(); // 잠을 잔다
// 자식 클래스의 메소드 호출
bird.chirp(); // 새가 운다
}
}
위의 코드를 보면, extends 키워드를 통해 상속을 받은 자식 클래스는 자신의 필드 및 메서드뿐만 아니라, 부모 클래스의 필드와 메서드에도 직접 접근할 수 있음을 알 수 있다.
그렇다면 방금의 코드에서는 자식의 생성자만 호출한 다음 부모의 필드와 메소드에 접근을 할 수 있었는데 부모에 접근을 하려면 부모도 생성이 되어야 하지 않나라고 의문을 품을 수 있다. 결론부터 말하면 부모도 생성이 되어있는 상태이다.
그러면 부모가 어떻게 생성이 되는지 궁금할 것이다. 그것은 자식 생성자의 맨 첫 줄에 super()라는 것이 생략되어 있으며 이로 인해 부모 클래스의 기본 생성자가 자동으로 호출되기 때문이다.
public class Bird extends Animal {
String name;
int age;
int height;
int weight;
public Bird(String species, String name, int age, int height, int weight) {
super(species); // 이것이 생략 되어있음.
this.species = species;
this.name = name;
this.age = age;
this.height = height;
this.weight = weight;
}
public void chirp() {
System.out.println("새가 운다");
}
}
위의 코드처럼 super(species);가 생략되어 있는 것이다.
super란, 부모 클래스로부터 상속받은 필드나 메소드를 자식 클래스에서 참조하는 데 사용하는 참조 변수를 말한다. 주로 부모 클래스의 생성자와 메소드를 호출하거나 필드에 접근할 때 사용된다. this의 경우 자기 자신을 가리킨다고 하면 super의 경우 부모를 가리킨다고 보면 된다.
생성자의 경우는 위의 코드처럼 호출하는 것이다.
메소드와 필드의 경우는 아래의 코드로 보자.
public class Animal {
String species;
// 이것이 없으면 컴파일 오류가 발생된다.
public Animal(String species) {
this.species = species;
}
public void eat() {
System.out.println("먹이를 먹다");
}
public void sleep() {
System.out.println("잠을 잔다");
}
}
public class Bird extends Animal {
String name;
int age;
int height;
int weight;
public Bird(String species, String name, int age, int height, int weight) {
super(species); // 이것이 생략 되어있음.
this.name = name;
this.age = age;
this.height = height;
this.weight = weight;
}
public void chirp() {
System.out.println("새가 운다");
}
public void oneDayLife() {
super.eat();
this.chirp();
super.sleep();
}
public void printSpecies() {
System.out.println(super.species);
}
}
public class ExampleMain {
public static void main(String[] args) {
// 자식 객체 생성
Bird bird = new Bird("chicken", "삐약이", 2, 70, 5);
// 메소드 접근 결과
bird.oneDayLife(); // 먹이를 먹다 - 새가 운다 - 잠을 잔다
// 필드 접근 결과
bird.printSpecies(); // chicken
}
}
여기서 주의할 점으로는 만약 부모 클래스에 매개변수를 받는 생성자만 정의되어 있을 경우, 자식 클래스에서는 반드시 super(매개변수)를 통해 부모 생성자를 명시적으로 호출해야 한다. 그렇지 않으면 컴파일 오류가 발생하기에 이 점에 유의해야 한다.
또한 상속에서는 자식에서 부모로부터 받은 메소드를 재정의 할 수 있는데 이것을 메소드 오버라이딩이라고도 한다. 주로 부모로 받은 메소드가 적절하지 않을 때 자식 클래스의 현 상황에 맞게 재정의하여 사용하는 기능이다.
코드로 알아보자.
public class Animal {
String species;
public void eat() {
System.out.println("먹이를 먹다");
}
public void sleep() {
System.out.println("잠을 잔다");
}
}
public class Bird extends Animal {
String name;
int age;
int height;
int weight;
public Bird(String species, String name, int age, int height, int weight) {
super(species); // 이것이 생략 되어있음.
this.species = species;
this.name = name;
this.age = age;
this.height = height;
this.weight = weight;
}
public void chirp() {
System.out.println("새가 운다");
}
@Override
public void eat() {
System.out.println("새가 먹이를 먹다");
}
}
public class ExampleMain {
public static void main(String[] args) {
// 자식 객체 생성
Bird bird = new Bird("chicken", "삐약이", 2, 70, 5);
bird.eat(); // 새가 먹이를 먹다.
}
}
위의 코드에서처럼 보통은 어노테이션을 활용하여 @Override를 사용해서 메소드 재정의를 한다.
물론 어노테이션을 사용하지 않아도 되지만 @Override를 사용하게 되면 메소드가 정확히 재정의 된 것인지 컴파일러가 확인해 주기 때문에 실수를 줄여준다.
하지만 이렇게 메소드를 재정의할 때 주의해야 할 점이 있다.
첫째, 부모의 메소드와 동일한 리턴 타입, 메소드 명, 매개 변수 목록을 가져야 한다.
둘째, 접근 제한을 더 강하게 정의할 수 없다. 반대로 약하게는 가능하다.
셋째, 새로운 예외를 throw 할 수 없다.
이 세가지에 잘 유의해서 메소드 재정의를 해야한다.
참조
https://chanhuiseok.github.io/posts/java-1/
https://wikidocs.net/280
https://erinh.tistory.com/entry/Java-%EC%9E%90%EB%B0%94-7-%EC%83%81%EC%86%8D-%ED%81%B4%EB%9E%98%EC%8A%A4-%EC%83%81%EC%86%8D-super-%EB%A9%94%EC%86%8C%EB%93%9C-%EC%98%A4%EB%B2%84%EB%9D%BC%EC%9D%B4%EB%94%A9
혼자 공부하는 자바(한빛 미디어) - 신용권