참조변수 super, 생성자 super()
사실 객체지향언어의 대한 개념을 다시 제대로 잡아야겠다고 생각했던 포인트가 이 부분이다. 생성자가 어떤 원리로 어떻게 이루어지는데에 있어서 개념이 러프하다고 생각했고 정말 새로운 것들도 많이 알게되며 까먹었던 것들도 많이 있었다.
우선 참조변수 super와 생성자 super() 는 인스턴스 내에서 사용하던 this 참조변수와 this() 생성자와 같이 전혀 다르다는 걸 인지하고 들어가도록 하자.
참조변수 super
먼저 참조변수 super는 조상을 가리키는 참조 변수이다. 이것도 this와 동일하게 인스턴스 메서드(생성자 포함) 내에서만 사용이 가능하다. 예시를 한 번 살펴보자
class Parent { int x = 10;}
class Child extends Parent {
int x = 20;
void method(){
System.out.println("x=" + x); // 20
System.out.println("this.x=" + this.x); // 20
System.out.println("super.x=" + super.x); // 10
}
}
여기서 놀라운 사실은 int x 를 두 번 선언한 것이었다. 어떻게 이게 이렇게 되는거지? 라고 의문이 들었다. 실제로 테스트도 해봤고 정상적으로 동작하는 것을 볼 수 있었다. 이럴때는 메모리 구조를 살펴보는 것이 제일이다.
이렇게 생성이 된다고 한다. 그럼 여기서 더 궁금해진 부분이 있다. 그럼 한번 더 상속받아서 같이 선언하면 뭘 가리키게 되는거지? 같은 의문이 들었다.
class Parent { int x = 10;}
class Child extends Parent {
int x = 20;
void method(){
System.out.println("x=" + x); // 20
System.out.println("this.x=" + this.x); // 20
System.out.println("super.x=" + super.x); // 10
}
}
class GrandChild extends Child {
int x = 30;
}
여기서 void method()는 상속받았기 때문에 그대로 사용한다고 가정해보자. 그리고 결과를 확인해보았다. 결과는 다음과 같았다.
x = 20, this.x = 20, super.x = 10
여기서 x는 기본적으로 this.x 이기 때문이다. 변수만 있다면 this가 생략된 것
어? 예상했던 결과인 x = 30, this.x = 30, super.x = 20 과 좀 다르다 상속받은 메서드를 그대로 사용했을 때 Child 메소드에 있는게 그대로 사용이 된건가? 라고 생각이 들어서 한번 오버라이딩을 해보기로 했다.
class Parent { int x = 10;}
class Child extends Parent {
int x = 20;
void method(){
System.out.println("x=" + x); // 20
System.out.println("this.x=" + this.x); // 20
System.out.println("super.x=" + super.x); // 10
}
}
class GrandChild extends Child {
int x = 30;
void method(){
System.out.println("x=" + x); // 30
System.out.println("this.x=" + this.x); // 30
System.out.println("super.x=" + super.x); // 20
}
}
코드에 전혀 차이가 없는 오버라이딩이다. 한 번 더 동작을 확인해봤다. 이번 테스트로 확실해졌다. 실제로 오버라이딩을 하지 않으면 조상의 메소드에서 조상의 인스턴스 변수를 가지고 this와 super가 동작한다.
또 오버라이딩을 했을 때 super가 가리키는 대상은 제일 맨위에 있는 조상이 아닌 바로 위에 있는 부모의 인스턴스 변수를 가리킨다는 사실을 확실히 알게 되었다. 또 그렇다면 super.super.x 도 가능하지않을까? 싶어서 한 번 시도해봤다.
class Parent {
int x = 10;
int y = 30;
}
class Child extends Parent {
int x = 20;
void method(){
System.out.println("x=" + x); // 20
System.out.println("this.x=" + this.x); // 20
System.out.println("super.x=" + super.x); // 10
}
}
class GrandChild extends Child {
int x = 30;
void method(){
System.out.println("x=" + x); // 30
System.out.println("this.x=" + this.x); // 30
System.out.println("super.x=" + super.x); // 20
System.out.println("super.super.x=" + super.super.x); // 에러 발생
System.out.println("super.y=" + super.y); // 30
}
}
아쉽게도 동작이 하지 않는 것을 볼 수 있었다. 그럼 최고 조상에 다른 변수가 있으면 어떻게 될까? 정상적으로 출력이 되는 모습을 확인할 수 있었다. 아마도 내가 생각하기에는 super는 바로 윗조상을 가리키는데 상속을 받으면서 Child 메소드에 인스턴스 변수 y가 상속되었고 그걸 가리키고 있는 것이다 라고 생각이 든다. 이렇게 헷갈릴 수 있는 부분이 많았는데 전부 말끔하게 해소가 되었다.
super의 용도는 다음과 같다. 조상의 멤버를 자신의 멤버와 구별 할 때 사용하는 것 즉, 보통은 같은 변수를 선언하는 일이 많진 않겠지만, 어쩔 수 없이 구별해야 하는 상황일때에는 다음과 같이 super를 잘 사용해보자.
super() 조상의 생성자
정말 많이 헷갈렸던 부분이고 정말 놀라운 사실을 알게 된 파트이다. this() 생성자가 자신의 인스턴스 객체에 있는 생성자를 호출하듯이, super()는 조상의 생성자를 호출할 때 사용한다. super를 그럼 대체 언제 쓰지? 라는 의문이 많이 들 수 있다. 이런 것도 예시로 살펴보는 것이 간단하다.
class Point {
int x, y;
Point(int x, int y){
this.x = x;
this.y = y;
}
}
class Point3D extends Point {
int z;
Point3D(int x, int y, int z){
this.x = x; // super.x = x 도 물론 가능하다.
this.y = y;
this.z = z;
}
}
위와 같은 상황에서 조상의 x, y를 직접 자손에서 가지고 와서 초기화를 해주는 것을 볼 수 있다. super는 위와 같은 상황일 때 사용하는 것이 좋은데 다음 예시를 보자.
// 위에 Point 그대로 사용
class Point3D extends Point {
int z;
Point3D(int x, int y, int z){
super(x, y);
this.z = z;
}
}
조상의 생성자를 호출해서 조상에 있는 인스턴스 변수들을 초기화 해줄 수 있다.
이제부터가 중요하다. 숨겨져 있던 컴파일러의 super() 호출부분이다. 생성자는 생성자의 첫 줄에 반드시 생성자를 호출해야 한다. 만약 생성자가 없으면 컴파일러가 알아서 생성자의 첫 줄에 super(); 를 삽입한다.
모든 클래스의 조상은 Object이다. 만약에 어떤 클래스가 상속을 받지 않은 상태에서 다음과 같이 구성되어있다고 하자.
class Point {
int x, y;
}
이런 구조를 가지고 있었다고 하자. 저번 파트에서도 얘기했지만 아무것도 상속하지 않은 클래스는 자연스럽게 Object 클래스를 상속받게 된다. 그리고 지금보면 기본 생성자가 없는 것을 볼 수 있다. 따라서 보이지 않지만 컴파일러에서는 다음처럼 보이게 될것이다.
class Point {
int x, y;
// 컴파일러가 생성
Point(){
}
}
위와 같이 기본 생성자가 추가되어있는 상태일 것이다. 그런데 위에서 말한 조건을 살펴보자. 생성자는 생성자의 첫줄에 반드시 생성자를 호출해야 한다. → 그런데 위를 보면 기본 생성자의 첫 줄에 생성자가 없다. 그리고 그 다음을 읽어보면 만약 생성자가 없다면 컴파일러가 알아서 첫 줄에 super()를 삽입한다. 그렇다 컴파일러가 확인하는 코드는 다음과 같이 변경될것이다.
class Point {
int x, y;
// 컴파일러가 생성
Point(){
super(); // Object 클래스 기본 생성자
}
}
실제로 Object 클래스 생성자의 기본 생성자가 명시되어있는 것을 Object 클래스에서 확인할 수 있다. 따라서 우리가 지금까지 봐왔던 코드들을 잠깐 살펴보자
class Point {
int x, y;
Point(int x, int y){
this.x = x;
this.y = y;
}
}
class Point3D extends Point {
int z;
Point3D(int x, int y, int z){
// super(); 기존에 없었지만 컴파일할 때 컴파일러가 자동으로 추가해준다.
this.x = x;
this.y = y;
this.z = z;
}
}
실제로 저런 동작이 발생한 것이다. 근데 잘 살펴봐라 super()를 호출하는데, Point 클래스에는 기본 생성자가 없는 것을 볼 수 있다. 매개변수가 있는 생성자가 있기 때문에 자연스럽게 기본 생성자는 생성되지 않았다. 그런데 자식 클래스에서 super() 라는 조상의 기본 생성자를 호출 하고 있다. 실제로 실행하면 다음과 같은 오류를 반환하는 것을 볼 수 있다.
constructor Point in class Point cannot be applied to given types;
Point3D 객체를 생성했는데 Point 클래스에 주어진 생성자가 없다는 의미이다. 즉 super()를 호출했는데, 기본 생성자가 없기 때문에 위와 같은 오류가 발생하는 것이다.
이런 질문을 할 수도 있다. 그럼 꼭 super()를 추가해줘야되는건가? 꼭 그렇지는 않다. 다만 클래스를 생성할 떄 기본 생성자를 꼭 만들어두도록 하자.
class Point3D extends Point {
int z;
Point3D(){}
Point3D(int x, int y, int z){
this();
this.x = x;
this.y = y;
this.z = z;
}
}
위 처럼 작성을 해줘도 생성자의 첫 줄에 생성자가 있기 때문에 super()를 컴파일러가 추가해주지 않는다. 그래서 생성자에 대해서 정리를 하면 다음과 같이 될것이다.
생성자 내부에서 생성자를 호출할 때는 첫 줄에 와야한다. 만약 첫줄에 생성자를 호출하지 않으면 super()를 컴파일러가 삽입한다. 따라서, 클래스를 생성할 때에는 오류를 방지하기 위해 꼭 기본 생성자를 추가해주도록 하자. 많은 부분을 공부하게 된 파트였다. 역시 실무를 몇 번 해보다가 개념을 다시 보니까 신기하다. 아는 만큼 보인다는 것이 정말 맞는 말 같다. 나중에 더 배우고 다시 보게되었을 때 더 놀라울지도..
'Language > Java' 카테고리의 다른 글
7-4 제어자 (0) | 2023.03.23 |
---|---|
7-3 패키지 (0) | 2023.03.22 |
7-1 상속과 오버라이딩 (0) | 2023.03.22 |
6. 객체지향언어 1 (0) | 2023.03.22 |
3. 연산자 (0) | 2023.03.21 |