상속을 받은 클래스가 인스턴스를 생성할 때 어떤 일이 벌어지는가 알아본다.
public class OOPTest {
public static void main(String[] args) {
B1 b1 = new B1();
System.out.println("Good Day, dear my lord!");
}
}
class A1 {
int ID;
String name;
int naturalNumber;
A1(){
System.out.println("A1() Called");
}
}
class B1 extends A1{
double realNumber;
B1(){
// super();
System.out.println("B1() Called");
}
}
클래스 B1은 클래스 A1을 상속한다. main 메소드에서 상속한 B1을 생성했다.
결과는 부모클래스 A1이 먼저 생성되고 B1이 생성된다.
힙메모리에서 보면 A1이 먼저 생성되고 그 다음에 B1의 멤버변수가 들어온다. 상속을 하게 되면
*부모 클래스의 멤버변수 + 자손 클래스의 멤버변수 = 클래스의 멤버변수
가 되고 자손 멤버변수의 개수는 항상 부모와 같거나 많게 된다.
class A1 {
int ID;
String name;
int naturalNumber;
A1(){
System.out.println("A1() Called");
}
}
class B1 extends A1{
double realNumber;
B1(){
super(); // A1 () 생성자를 호출한다.
System.out.println("B1() Called");
}
}
B1은 상위 클래스의 생성자를 호출하는데 컴파일러가 자동으로 super ( ); 를 추가한다. super 는 this 와 관계가 있다. this 는 자기 자신을 참조하는 것이고 super 는 상위클래스 (부모)를 참조하는 것이다.
즉 다음과 같이 생성자가 호출된다.
super ( ) ;
B1 ( ) ;
실질적으로
super( );
this( );
라 볼 수도 있다. 이제 부모의 매개변수를 바꿔 새로운 생성자를 만들어 보자.
부모의 생성자 A1 ( int,String,int)를 새로 정의했다. 그랬더니 컴파일 오류가 난다. B1을 생성하려고 할 때 super( ) 생성자를 호출하는데 매개변수가 없기 때문이다. 이제 B1에서 생성자를 정의하고, main 메서드에서 매개변수를 추가한다. 코드가 정상으로 작동하는 것을 볼 수 있다.
public class OOPTest {
public static void main(String[] args) {
B1 b1 = new B1(2020,"my Lord",100,101.5);
B1 b2 = new B1();
System.out.println("Good Day, dear my lord!");
}
}
class A1 {
int ID;
String name;
int naturalNumber;
A1(){
System.out.println("old A1");
}
A1(int ID, String name, int naturalNumber){
this.ID = ID;
this.name = name;
this.naturalNumber = naturalNumber;
System.out.println("A1() new Called");
}
}
class B1 extends A1{
double realNumber;
B1(){
System.out.println("old B1");
}
B1(int ID, String name, int naturalNumber,double realNumber){
super(ID, name, naturalNumber);
this.realNumber = realNumber;
System.out.println("B1() new Called");
}
}
결과를 비교해보면 확실히 알 수 있다.
super인 A1( ) new 가 먼저 생성되고 B1( ) 이 따른다.
매개변수가 없는 기존 B1은 여전히 super( )는 생략되어 있으나 super( )를 먼저 생성한다.
결론적으로 자손이 생성될때 부모가 먼저 생성된다는 것을 알 수 있다. super( ) -> this( )
super 연산자로 부모의 멤버변수나 메소드를 참조할 수 있다. 예를 들어 super.ID 멤버변수에 접근, super.메소드( ) 처럼 사용가능하다. 하나의 인스턴스에서 멤버 변수는 동일 참조값이다. 메소드의 경우 자손을 오버라이드 했다면 부모와 다를 수 있다. 예전 메서드를 사용하기 위해서 super 연산자를 사용할 수 있다. 이 부분은 오버라이드에서 다룰 것이다.
여기까지 봤으면 왜 자손클래스의 범위가 부모클래스의 범위보다 큰지 이해할 수 있을 것이다. B1 extends A1은 A1보다 확장한다는 뜻이다. 멤버변수와 메서드 측면에서 확장된다.
그렇다는 것은 B1의 인스턴스를 생성하면 A1 참조변수로 사용할 수 있다는 것이다. B1의 확장된 영역은 사용하지 못하겠지만 자기 범위인 A1클래스의 멤버면수와 메소드에는 접근할 수 있다. 다시 말해
A1 a1 = new B1( );
이 가능하다. 이것을 클래스 형 변환이라고 한다.
반대로 A1의 인스턴스를 B1이 접근할 수는 없다. double 형 참조는 가리킬 곳이 없다. 이것은 불가능하다. 아무리 A1의 인스턴스를 만들어도 B1의 double 참조공간은 계속 비어있을텐데 사용할 이유가 없다.
A1을 참조 변수로 B1의 인스턴스를 생성했더니 기존 A1 클래스의 변수들만 사용할 수 있다. B1의 double realNumber는 사용 못한다.
그런데 다시 B1형으로 형변환을 하니 realNumber가 사용가능해졌다. 옆에 붙어있는 표시가 어느 클래스의 변수인지를 말해준다. A1, B1, Object
최초 new B1( ) 생성시기에 메모리상에 double realNumber도 이미 생성되었다는 것을 의미한다. 참조 자료형이 A1으로 범위가 B1보다 적었기 때문에 접근을 못한 것 뿐이었다. 다시 B1형으로 변환하면 접근할 수 있다. (다시 자손으로 내려가서 다운캐스팅이라 한다)