상속이란 친족간의 어떠한 권리나 의무를 계승, 물려주는 것을 말한다.
자바에서도 상속을 통해 상위 클래스의 속성과 기능들을 하위 클래스에게 물려줄 수 있다.
이를 통해 프로그램의 생산성을 크게 증가 시킬 수 있다.
상속 방법
클래스 상속 방법
class 자식클래스 extends 부모클래스 {
}
//ex
class Doctor extends Job {
}
자식 클래스를 선언할때 extends 예약어를 이용하여 부모 클래스를 뒤에 붙여주면
부모 클래스에 있는 모든 메소드와 변수를 자식 클래스에 존재하는 것 처럼 사용할 수 있다.
예제 코드를 통해 살펴보자
class Job {
public int type;
public void setType(int type) {
this.type = type;
System.out.println(type);
}
}
class Developer extends Job{
public void greetingDev() {
System.out.println("hello developer");
}
}
public class Sample {
public static void main(String[] args) {
Developer dev = new Developer();
dev.setType(1);
// 1 출력
dev.greetingDev();
// hello developer 출력
}
}
Developer(자식 클래스)에 extends 예약어를 사용하여 Job(부모클래스)에 있는 메소드, 변수를 물려받았다.
실제로 Developer에 선언하지 않은 메소드와 변수를 Developer의 객체에서 사용할 수 있다는 것을 알 수 있다.
물론, 자식 클래스에만 선언되어 있는 메소드도 사용할 수 있다.
이것처럼 직업에 해당하는 Developer 클래스 뿐만 아니라, Teacher, Doctor등 '직업'이라는 범주 안에서 공통적으로 필요한 변수와 메소드가 있을 때, 간편하게 Job이라는 클래스를 상속받는 것만으로도 그 기능들을 물려받을 수 있다.
그렇다면 자식클래스 부모클래스에 선언된 변수와 메소드를 전부 다 사용할 수 있을까?
상속의 제약
자바에서는 '캡슐화'라는 아주 중요한 개념이 있다.
이를 정보 은닉이라고도 하는데, 이를 통해 다른 클래스에서 변수나 메소드의 접근을 제한할 수 있다.
이러한 개념을 실현하는 것이 public, protected, private과 같은 '접근 제어자'이다.
자바에는 4가지의 접근 제어자가 존재한다.
- public - 누구나 접근 가능
- protected - 상속받거나 같은 패키지에 있을때만 접근 가능
- package-private - 같은 패키지 내에 있을 때만 접근 가능
- private - 선언된 클래스에서만 접근 가능
따라서 자식 클래스에서는 부모 클래스에서 public과 protected로 선언된 변수와 메소드만 접근 가능하다.
또한, 자바에서는 다중 상속을 지원하지 않는다.
즉, extends 예약어 뒤에 여러개의 클래스를 쓸 수 없고 하나의 클래스만 존재해야한다.
여러 클래스를 상속받을때 메소드 충돌이 일어날 수 있기 때문이다.
(이와 같은 문제를 다이아몬드 문제라고 한다.)
생성자
생성자 호출 순서
자식 객체가 생성될 때, 호출되는 생성자는 다음과 같다.
부모 객체 생성자 -> 자식 객체 생성자
class Job{
Job(){
System.out.println("Job Parent constructor");
}
}
class Developer extends Job {
Developer(){
System.out.println("Dev constructor");
}
}
public class Sample {
public static void main(String[] args) {
Developer dev = new Developer();
}
}
자식 객체를 생성하였더니, 자식 클래스의 생성자가 호출되기 전에 부모의 생성자가 먼저 호출되는 모습이다.
클래스에 생성자를 따로 선언하지 않으면 객체가 생성될때 컴파일러가 자동으로 기본 생성자(매개변수가 없는 생성자 "Job(){ }")를 생성한다.
자식 객체를 생성할때도 마찬가지이다. 자식 클래스의 생성자에 부모 클래스의 생성자를 명시적으로 지정하지 않으면, 부모 클래스의 기본 생성자를 컴파일러가 실행한다.
그래서 Developer 클래스의 생성자에 부모 클래스 생성자를 지정하지 않았음에도, 자동으로 "Job(){ }" 코드가 실행된 것이다.
생성자 호출 시 유의점
만약, 부모 클래스에 생성자가 선언되어있지 않다면?
이 경우는 문제가 발생하지 않는다. 클래스에 생성자가 선언되어있지 않으면 컴파일러가 알아서 클래스에 기본 생성자를 추가하기 때문이다.
만약, 부모 클래스에 기본 생성자가 없고 매개 변수가 있는 생성자만 선언되어있다면?
이 경우에 문제가 발생한다.
class Job{
Job(String name){
System.out.println("Job name is" + name);
}
}
class Developer extends Job {
Developer(){
System.out.println("Dev constructor");
}
}
자식 생성자에 부모 생성자가 명시되어있지 않으면, 컴파일러가 자동으로 기본 생성자(Job() { })을 호출한다고 했다.
그리고 클래스에 생성자가 선언되어있다면 기본 생성자(Job() { })를 자동으로 만들어주지 않으므로, 컴파일 오류가 발생한다.
이럴때 두가지 방법이 있다.
- 부모 클래스에 기본 생성자(Job() { })를 만들어준다.
- 자식 생성자에 super 예약어로 부모 클래스의 생성자를 명시해준다.
부모 클래스를 호출하는 super
자식에서 부모 클래스의 객체를 super 예약어를 통해 접근할 수 있다.
class Job{
Job(String name){
System.out.println("Job name is" + name);
}
}
class Developer extends Job {
Developer(){
super("Developer");
System.out.println("Dev constructor");
}
}
위 코드에서와 같이 super("Developer") 코드를 추가해, 명시적으로 String 자료형을 매개변수로 받는 부모 생성자를 호출할 수 있다. 자식보다 부모 생성자가 먼저 호출되어야 하므로 생성자 블록 최상단에 super 예약어를 사용해야 한다.
super 예약어는 생성자 뿐만 아니라, 부모 클래스의 메소드와 변수에도 접근할 수 있다.
super."변수명" / super."메소드()" 로 별도의 객체 생성 없이 부모 클래스의 기능을 사용할 수 있다.
오버라이딩 (Overriding)
상속은 부모 클래스의 변수와 메소드를 물려 받는다.
부모에게서 물려받은 메소드를 자식 클래스에서 재 정의하는 것을 오버라이딩(Overriding)이라고 한다.
class Job{
public void methodOverriding() {
System.out.println("Parent Method");
}
}
class Developer extends Job {
@Override
public void methodOverriding() {
System.out.println("Child Method");
}
}
public class Sample {
public static void main(String[] args) {
Developer dev = new Developer();
dev.methodOverriding();
}
}
Job이라는 부모 메소드에서 메소드를 물려 받고 자식에서 메소드 내용을 바꿔주었다.
그 결과, 부모에서 선언된 메소드의 코드가 실행되는 것이 아니라 자식에서 재정의한 코드가 실행된다.
메소드를 Overriding 할 때 주의해야할 점은 다음과 같다.
- 부모 클래스의 메소드와 동일한 리턴타입, 함수명, 매개변수를 갖는 자식 클래스의 메소드가 존재할 때 Overriding이 발생한다.
- Overriding 하려는 메소드의 리턴타입, 함수명, 매개변수가 같아야 한다.
- 접근 제어자는 접근 제한이 확대되는 경우만 가능하다.
접근 제어자의 접근 권한은 public > protected > package-private > private 이다.
예를 들어 부모 클래스에서 메소드가 protected로 선언되어 있다면 자식 클래스에서 Override 될 때, protected나 public으로 메소드를 선언하는 것은 가능하다는 것이다.
추가적으로 부모 클래스의 메소드를 그대로 실행하되, 몇가지 코드만 더 추가하고 싶다면 위에서 이야기한 super 예약어를 사용하면 된다.
class Job{
public void methodOverriding() {
System.out.println("Parent Method");
}
}
class Developer extends Job {
@Override
public void methodOverriding() {
super.methodOverriding();
System.out.println("Child Method");
}
}
public class Sample {
public static void main(String[] args) {
Developer dev = new Developer();
dev.methodOverriding();
}
}
오버로딩 (Overloading)
클래스 내의 같은 메소드를 매개 변수를 다르게 하여 중복해서 메소드를 선언하는 것을 오버로딩(Overloading)이라고 한다.
class Job{
public void printParameter() {
System.out.println("Overloading Parameter :: ");
}
public void printParameter(int a) {
System.out.println("Overloading Parameter :: " + a);
}
public void printParameter(int a, String name) {
System.out.println("Overloading Parameter :: " + a + " / " + name);
}
public void printParameter(String name, int a) {
System.out.println("Overloading Parameter :: " + name + " / " + a);
}
}
public class Sample {
public static void main(String[] args) {
Job sample = new Job();
sample.printParameter();
sample.printParameter(3);
sample.printParameter(10, "kiw");
sample.printParameter("Mu", 5);
}
}
이와 같이, 같은 메소드를 매개변수만 다르게 하여 여러개 선언할 수 있다.
이 때 매개 변수의 순서가 달라도 다른 메소드로 취급하며, 메소드 호출 시 자동으로 매개 변수에 맞는 메소드가 호출된다.
IS-A관계
앞서 예시로 사용한 코드를 다시 보자.
class Job {
public int type;
public void setType(int type) {
this.type = type;
System.out.println(type);
}
}
class Developer extends Job{
public void greetingDev() {
}
}
Devloper 클래스는 Job 이라는 클래스를 상속했다. 즉, Job이라는 큰 범주 안에 Developer이라는 명칭이 포함되어 있는 것이다. 따라서, "Developer은 Job이다"라고 할 수 있다.
이와 같은 관계를 IS-A 관계라고 한다. Developer is a Job 이기 때문이다.
자바에서는 이러한 IS-A 관계로 인해 자식클래스의 객체를 생성할 때 부모클래스의 자료형으로 선언할 수도 있고, 자식클래스 자신의 자료형으로도 선언할 수 있다.
이 두 클래스의 관계를 UML의 클래스 다이어그램으로 살펴보면 다음과 같다.
예시 코드를 통해 자세히 알아보자.
class Job {
public int type;
public void setType(int type) {
this.type = type;
System.out.println(type);
}
}
class Teacher extends Job {
}
class Developer extends Job {
public void greetingDev() {
System.out.println("hello developer");
}
}
public class Sample {
public static void main(String[] args) {
Job devParent = new Developer();
Developer dev = new Developer();
devParent.setType(3);
dev.setType(3);
devParent.greetingDev(); //컴파일 에러 발생
dev.greetingDev();
}
}
Sample 클래스에서 Developer 객체를 부모클래스(Job)의 자료형으로 생성하고 (devParent) 자기 자신의 클래스(Developer)의 자료형으로도 생성했다. (dev)
부모클래스를 자료형으로 한 자식의 객체는 부모클래스에서 상속받은 메소드(자식에서 Overriding된 메소드 포함)와
변수만 사용할 수 있다.
따라서, 자식클래스에서만 정의되어 있는 greetingDev()메소드를 사용하려 할 때 에러가 발생하는 것이다.
자식 자료형으로 자식 객체를 생성했을 경우에는 자식 클래스의 모든 변수와 메소드를 사용할 수 있다.
이러한 특징은 배열을 생성할 때 유용하게 사용될 수 있다.
class Job {
public int type;
public void setType(int type) {
this.type = type;
System.out.println(type);
}
}
class Teacher extends Job {
}
class Developer extends Job {
}
public class Sample {
public static void main(String[] args) {
Job[] jobArray = new Job[3];
jobArray[0] = new Developer();
jobArray[1] = new Teacher();
jobArray[2] = new Job();
}
}
다음과 같이 부모 클래스의 자료형으로 배열을 선언하면,
해당 배열 안에 자신의 객체 뿐만 아니라, 자식 객체도 넣을 수 있다.
결론
상속을 이용하면 부모 클래스의 기능을 자식 클래스가 물려받을 수 있다.
이후, Overriding으로 부모 클래스의 메소드를 덮어 쓸수도 있고, super 라는 예약어를 통해서 부모 클래스의 변수와 메소드에 쉽게 접근할 수 있다.
이와 같은 상속의 여러 기능들을 통해 개발 시간을 단축하고, 코드가 간결해지며, 확장성이 용이해지는 등 다양한 장점이 있다.
여기서 유의 해야할 점은 자식 객체를 생성할 때 부모 클래스의 생성자가 호출된다는 점과 클래스의 다중 상속이 불가능하다는 점. (인터페이스에서는 가능하다) 부모의 자료형으로 자식 객체를 생성할 수 있다는 점. public, protected로 선언되지 않은 변수와 메소드는 상속받을 수 없다는 점이다.
자바에서 상속은 매우 중요한 개념이기에 한 번에 이해가 되지 않는다면 여러번 반복하여 개념을 잡는 것이 중요하다.
참고
ㆍ"자바의 신 vol.1", 이상민 저자
ㆍ https://scshim.tistory.com/210
[Java] 자바 상속의 특징 - extends, super, 오버라이딩, instanceof, 추상 클래스와 메소드, final
상속이란? 상속은 부모가 자식에게 물려주는 행위다. 객체 지향 프로그램에서도 부모(상위) 클래스의 멤버를 자식(하위) 클래스에 물려주어 자식 클래스가 갖고 있는 것처럼 사용할 수 있다. 상
scshim.tistory.com