기존의 클래스를 재사용하여 새로운 클래스를 작성하는 것.
class Parent{
}
class Child extends Parent{
//Parent클래스를 상속 받겠다는 뜻
}
상속을 하면, 부모 클래스의 멤버변수나 메소드를 사용할 수 있지만, 생성자나 초기화 블럭은 상속되지 않는다.
class Parent{
int a = 10;
void printMessage(){
System.out.println("Hello");
}
}
class Child extends Parent{
}
public class playGround {
public static void play(){
Child child1 = new Child();
child1.printMessage(); //Hello
}
}
상속은 여러 세대를 통해 이루어질 수 있으며, 상속받은 자손 클래스는 모든 조상의 멤버를 사용할 수 있다.
단일 상속이 원칙인 자바에서 여러 클래스를 상속받은 것처럼 사용할 수 있다.
class Parent{
int a = 10;
void printMessage(){
System.out.println("Hello");
}
}
class Parent2{
int b = 20;
}
class Child extends Parent{
Parent2 one = new Parent2()
}
public class playGround {
public static void play(){
Child child1 = new Child(); //Hello
child1.printMessage(); //20
}
}
명확한 상속과 포함의 관계를 통해 코드의 재사용성을 높일 수 있다.
~은 ~이다
와 어울리면 상속을,
~은 ~을 가지고 있다
와 어울리면 포함을 선택하는 것이 권장된다.과일, 바나나, 껍질 이라는 클래스들이 있다면,
과일 -> 바나나(껍질) 이 적합할 것이다.
자바가 다중상속을 포기한 이유는 두 개 이상의 조상 클래스가 같은 이름의 멤버변수나 메소드를 가진다면, 상속의 이유중 하나인 재사용성을 기대하기 어렵기 때문이다.
사실 상속을 하지 않은 클래스들도 이미 상속을 하고 있다.
class Person{
int age;
Person(int a){
this.age = a;
}
}
위 코드가 컴파일 되면, 선언부가 class Person extends Obejct
가 된다. 자동으로 Object()
라는 클래스를 상속받는다.
그래서, 클래스는 기본적으로 Object()
가 가지는 메소드들을 따로 정의하지 않아도 사용할 수 있다.
ex) toString(), equals(), ...
class Parent{
int a = 10;
void printMessage(){
System.out.println("Here is Parent");
}
}
class Child extends Parent{
void printMessage(){
System.out.println("Here is Child");
}
}
public class playGround {
public static void play(){
Child child1 = new Child(); //Hello
child1.printMessage(); // ?? --1
}
}
--1
의 결과는 Here is Child
일 것이다.
자손이 조상와 같은 이름의 메소드를 가지면, 조상의 메소드를 덮어쓰고(override) 자손의 메소드를 실행한다.
앞서 배운 오버로딩과 비슷하게 생각할 수 있다. 같은 이름의 메소드에 대해 어떤 처리가 이루어지기 때문이다.하지만 명확히 다르며 그 차이는,
오버로딩은 기존의 없는 메서드를 정의하고,
오버로딩은 이름만 같을 뿐, 매개변수에 대한 정보가 다르다.
오버라이딩은 상속받은 메소드를 변경하고,
오버라이딩은 상속받은 메소드와 이름 뿐만아니라, 매개변수, 반환타입모두 동일하여야 한다.
완전히 다르게 생각하자. 오버라이딩의 장점은 자손의 필요에 따라 알맞게 메소드를 변경할 수 있다는 것이다. 오버라이딩은 메소드이름을 통해 파악되므로, 오타가 있어도 원하는 오버라이딩 없이 정상작동 할 수 있다.
그래서 @Override
라는 어노테이션을 사용하는 것이 권장된다.
참조변수 super
와 메서드 super()
에 대해 살펴보자.
참조변수 super
는 this
와 유사하게 객채를 가리키지만 그 대상의 차이가있다.
package playground;
class Parent{
int a = 10;
void printParentMessage(){
System.out.println("Here is Parent");
}
}
class Child extends Parent{
int a = 20;
void printNumber(){
System.out.println(super.a); // --1
System.out.println(this.a); // --2
}
@Override
void printParentMessage(){
System.out.println("Here is Child");
super.printParentMessage();
}
}
public class playGround {
public static void play() {
Child c = new Child();
c.printNumber();
c.printParentMessage();
//Here is Child --3
//Here is Parent --4
}
}
--1
과 --2
를 보면, 같이 a
를 지칭하지만 결과가 다르다. 그 이유가 super
와 this
라는 참조변수가 서로 가리키는 부분이 다르기 때문이다. super
는 조상 클래스인 Parent
의 a
를, this
는 자기 자신의a
를 지칭 한 것이다.
--3
과 --4
의 결과를 보면, 조상의 메소드가 아닌 Child
클래스의 printParentMessage()
가 실행된 것을 보면 오버라이딩이 의도에 맞게 수행되었다는 것을 알 수 있다. super.printParentMessage();
참조변수super
가 있기에, 조상의 메소드가 실행된 것을 알 수 있다.
package playground;
class Parent{
int a;
Parent(int aInit){
this.a = aInit; // --1
}
}
class Child extends Parent{
int a;
int b;
Child(int aInit, int bInit){
super(aInit + bInit); // --2
this.a = aInit;
this.b = bInit;
}
void printMessage(){
System.out.println(super.a); //3
System.out.println(this.a); //2
System.out.println(this.b); //1
}
}
public class playGround {
public static void play() {
Child c = new Child(1,2);
c.printMessage();
}
}
super()
도 this()
처럼 생성자의 역할을 하지만, 가리키는 대상이 다르다.
--1
에서 조상클래스인 Parent
의 생성자가 존재하기 때문에 컴파일러는 자동생성자를 생성하지 않는다.
그래서 Parent
의 멤버 변수에 대한 초기화가 필요한데, 이를 상속받은 Child
가 그 동작을 super()
을 통해 수행한다.
--2
가 존재하지 않으면 컴파일 에러가 발생할 것이다. 과정을 살펴보면,
컴파일러는 생성자 첫줄에 생성자(thi(),super())가 존재하지 않으면 자동으로 super()
를 삽입한다.
이때, Person
의 생성자는 int aInit
이라는 매개변수를 받는데, super()
과 일치 하지 않는다.
자동 생성될super()
가 생성되지 않게 -> super(a)
로 수정하여 매개변수를 보내거나,
Parent(int aInit)
- > null
로 하여 자동생성자를 통해 일치시켜 해결할 수 있다.