객체지향에서 상속이란 하나의 클래스가 다른 클래스의 자원을 그대로 받아 설계되는 것을 의미한다. 실세계의 의미와 비슷하게 상속받는 클래스를 자식 클래스, 상속해주는 클래스를 부모 클래스라고 한다.
상속은 부모클래스의 필드, 메소드, 생성자 등을 그대로 가져다 쓰기 때문에 자식 클래스간의 통일성을 주고, 오버라이딩 기법과 맞물려 다형성의 성질을 제공한다.
리터럴 타입의 변수들이 자동으로 형 변환될 수 있는 것과 마찬가지로, 클래스 타입의 변수들도 자동 형 변환이 가능하다.
int intVal = 4; // 4byte
double doubleVal = intVal; // 8byte
System.out.println(doubleVal); // 4.0
이와 같이 리터럴 타입의 변수는 더 크기가 큰 변수에 작은 변수를 대입하면 자동으로 형 변환되어서 대입된다.
마찬가지로 부모 클래스 타입 변수에 자식 클래스 인스턴스를 대입할 수 있다. 이는 객체 지향에서 객체를 큰 시스템의 부품으로 사용하기 때문에 각 객체의 부품화로 다형성을 제공하는 것이다.
Developer beDeveloper = new Developer(); // Developer extends Person
Person jaeseok = beDeveloper;
어떤 변수에 사람 객체가 들어가서 기능을 수행했는데 시스템이 바뀌어서 그 변수에 개발자나 학생, 여자, 군인등의 좀 더 세부적이며 사람의 특성을 가지는 객체가 그 역할을 대체해서 수행해야하는 경우가 발생할 수 있다. 이런 경우에 위의 코드처럼 부모 클래스 변수에 자식 클래스 인스턴스를 바로 대입할 수 있다.
자바에서 지원하는 이러한 기능은 부모 클래스 변수 하나로 다양한 형태의 인스턴스를 수용해서 사용할 수 있다는 점에서 객체지향의 다형성의 특징을 잘 나타낸다고 할 수 있다.
상속의 다형성은 메소드의 매개변수에서도 드러난다.
public static void main(String[] args) {
Developer beDeveloper = new Developer(20, 175, 72);
System.out.println(returnBMI(beDeveloper)); // 23.510204081632654
}
public static double returnBMI(Person person) {
return person.calculateBMI();
}
Person 타입의 객체를 전달받아 값을 반환하는 returnBMI라는 메소드에 Developer 타입의 객체를 전달하더라도 Person의 자원을 상속받아 가지고 있기 때문에 그대로 처리해서 반환할 수 있다.
그렇다면 자식 클래스 타입 변수에 부모 클래스를 대입할 순 없을까?
Person jaeseok = new Person();
Developer beDeveloper = (Developer) jaeseok; // Developer extends Person
할 수 있다. 그러나 위와 같이 할 수는 없다. Developer 클래스에는 language라는 String 필드가 있다고 하자. 그렇다면 jaeseok 인스턴스에는 language 필드가 포함되어 있지 않기때문에 Developer 타입으로 강제 형 변환할 수 없다.
할 수 있는 경우는 기존에 Developer 타입이었던 변수가 자동 형 변환으로 Person 변수로 바뀌어서 사용되다가 다시 Developer 타입으로 사용해야되는 경우 강제 형 변환이 가능하다.
public static void main(String[] args) {
Developer beDeveloper = new Developer(20, 175, 72, "Java");
System.out.println(((Developer)sameStatus(beDeveloper)).language); // Java
}
public static Person sameStatus(Person person) {
return person;
}
위와 같이 Person을 입력받은 후 sameStatus의 매개변수인 person은 language 필드를 사용할 수 없는 Person 객체로 자동 형 변환한다. 그것을 그대로 반환하는데 반환된 Person 객체를 출력하는 부분에서 Developer 타입으로 강제 변환하여 language를 반환할 수 있다.
접근 제한자는 내가 설계한 클래스, 메소드, 필드 등에 접근할 수 있는 권한의 범위를 명시한 것이다.
public 접근 제한자는 어디에서든 접근이 가능하다.
protected 접근 제한자는 다른 패키지에서는 접근이 불가한데, 다른 패키지더라도 나의 자식 클래스라면 접근이 가능하다.
default 접근 제한자는 따로 명시하지 않으면 자동으로 컴파일러가 처리하는 접근 제한자인데, 다른 패키지는 무조건 접근이 불가능하다.
private 접근 제한자는 나와 같은 패키지도 접근이 불가능하고 클래스 내부에서 호출하는 경우에만 접근이 가능하다.
final 필드가 리터럴 타입인 경우에는 기본적으로 값이 고정되어있고 수정이 불가능하다.
이와 마찬가지로 final로 선언된 클래스는 최종적인(직역하면) 클래스이므로, 이 클래스 이후의 자식 클래스가 생길 수 없다. 즉, 상속이 불가능하다.
public final class Person {
}
public class Developer extends Person {
}
이와 같이 Devloper가 Person을 상속하고자 할 때 Person이 final class라면 상속이 불가능해서 에러가 발생한다.
그렇다면 final 메소드는 어떨까?
public class Person {
public final void run(){
System.out.println("사람이 뛰고 있습니다.");
}
}
public class Developer extends Person {
@Override
public final void run(){
System.out.println("개발자가 뛰고 있습니다.");
}
}
이와 같은 클래스의 구조에서 run이라는 메소드가 final 메소드로 선언되어있다. 만약 final이 없다면 main 메소드에서 [Developer 인스턴스].run();
했을 때 개발자가 뛰고 있습니다.
라고 출력값이 발생할 텐데, 아예 컴파일 전에 에러가 발생한다.
final 메소드도 최종적인 메소드이기 때문에 여기서 더 이상 수정할 수 없다. 즉, Override가 불가능하다.