핵심 개념
1. 생성자 기본
- 클래스와 이름이 동일하고 반환 타입이 없음
- 객체 생성 시 자동으로 호출됨
- 기본 생성자는 명시하지 않으면 자동 생성
2. 생성자 체이닝
- this(): 같은 클래스의 다른 생성자 호출
- super(): 부모 클래스의 생성자 호출
- 생성자의 첫 줄에 위치해야 함
3. 상속과 생성자
- 자식 클래스 생성 시 부모 생성자가 먼저 호출됨
- super()가 없으면 자동으로 부모의 기본 생성자 호출
자바 생성자 연습문제
문제: 생성자 체이닝 순서 파악
질문: 출력 결과는?
class A {
int x;
A() {
this(5);
System.out.println("A()");
}
A(int x) {
this.x = x;
System.out.println("A(int): x=" + x);
}
}
public class Test1 {
public static void main(String[] args) {
A a = new A();
}
}
A(int): x=5
A()
해설: this(5)가 먼저 실행되어 A(int) 생성자가 호출된 후, A() 생성자의 본문이 실행됩니다.
문제: super() 생략 시 문제
질문: 컴파일 에러가 나는 이유는? 어떻게 고쳐야 하나?
class B {
B(int n) {
System.out.println("B: " + n);
}
}
class C extends B {
C() {
// TODO: 여기에 코드 추가 (컴파일 되도록)
System.out.println("C");
}
}
public class Test2 {
public static void main(String[] args) {
C c = new C();
}
}
에러 이유: B 클래스에 기본 생성자가 없습니다. C의 생성자에서 자동으로 super()를 호출하려 하지만, B()가 존재하지 않아 컴파일 에러가 발생합니다.
해결 방법:
class C extends B {
C() {
super(10); // 또는 다른 int 값
System.out.println("C");
}
}
문제: 2단계 상속 호출 순서
질문: 출력 결과는?
class X {
X() {
System.out.println("X");
}
}
class Y extends X {
Y() {
System.out.println("Y");
}
}
public class Test3 {
public static void main(String[] args) {
Y y = new Y();
}
}
X
Y
해설: 자식 클래스의 생성자는 항상 부모 클래스의 생성자를 먼저 호출합니다. Y의 생성자 첫 줄에 super()가 자동으로 삽입되어 X의 생성자가 먼저 실행됩니다.
문제: this()와 super() 동시 사용?
질문: 컴파일 에러가 나는가? 왜?
class D {
D(int n) {
System.out.println("D: " + n);
}
}
class E extends D {
E() {
this(10);
super(5); // 이게 가능할까?
}
E(int n) {
super(n);
}
}
컴파일 에러 발생!
이유: this()와 super()는 모두 생성자의 첫 줄에만 올 수 있습니다. 동시에 사용할 수 없으며, 하나만 선택해야 합니다.
이 코드에서는 this(10) 다음에 super(5)가 올 수 없습니다.
문제: 3단계 상속 출력 순서
질문: 출력 결과는?
class P {
P() {
System.out.print("1");
}
}
class Q extends P {
Q() {
System.out.print("2");
}
}
class R extends Q {
R() {
System.out.print("3");
}
}
public class Test5 {
public static void main(String[] args) {
R r = new R();
}
}
123
해설: 생성자 호출 순서는 최상위 부모부터 자식 순서입니다.
- R 생성자 호출 → Q 생성자 호출 → P 생성자 호출
- P 생성자 실행 (출력: 1) → Q 생성자 실행 (출력: 2) → R 생성자 실행 (출력: 3)
문제: 오버라이딩과 생성자
질문: 출력 결과는? (힌트: 필드 초기화 순서)
class F {
void show() {
System.out.println("F");
}
F() {
show();
}
}
class G extends F {
int n = 5;
void show() {
System.out.println("G: " + n);
}
}
public class Test6 {
public static void main(String[] args) {
G g = new G();
}
}
G: 0
해설:
- G 객체 생성 시 F의 생성자가 먼저 호출됩니다.
- F의 생성자에서 show() 호출 → 오버라이딩된 G의 show() 실행
- 하지만 이 시점에 G의 필드 n은 아직 초기화되지 않았습니다! (기본값 0)
- 따라서 "G: 0"이 출력됩니다.
핵심: 생성자 실행 순서는 부모→자식이지만, 메서드는 오버라이딩된 자식 메서드가 호출됩니다. 단, 자식의 필드는 아직 초기화 전입니다.
문제: 생성자에서 메서드 호출
질문: 출력 결과는?
class H {
H() {
init();
}
void init() {
System.out.println("H init");
}
}
class I extends H {
int value = 100;
void init() {
System.out.println("I init: " + value);
}
}
public class Test7 {
public static void main(String[] args) {
I i = new I();
}
}
I init: 0
해설:
- I 객체 생성 → H의 생성자 먼저 호출
- H의 생성자에서 init() 호출 → 오버라이딩된 I의 init() 실행
- 이 시점에 I의 필드 value는 아직 초기화되지 않음 (기본값 0)
- "I init: 0" 출력
주의: 생성자에서 오버라이딩된 메서드를 호출하면 예상치 못한 결과가 나올 수 있습니다!
문제: 다형성 생성자
질문: 출력 결과는?
class J {
J() {
System.out.println("J");
}
}
class K extends J {
K() {
System.out.println("K");
}
}
public class Test8 {
public static void main(String[] args) {
J j = new K();
}
}
J
K
해설:
- new K()로 실제 객체는 K 타입으로 생성됩니다.
- 참조 변수 타입이 J여도, 생성되는 객체는 K입니다.
- 따라서 K의 생성자가 호출되고, 부모인 J의 생성자가 먼저 실행됩니다.
- 다형성은 생성자 호출에 영향을 주지 않습니다!
문제: 생성자 체인 복잡
질문: 출력 순서는?
class L {
L() {
this(1);
System.out.println("L()");
}
L(int a) {
this(a, 2);
System.out.println("L(int): " + a);
}
L(int a, int b) {
System.out.println("L(int,int): " + a + "," + b);
}
}
public class Test9 {
public static void main(String[] args) {
L l = new L();
}
}
L(int,int): 1,2
L(int): 1
L()
해설: 생성자 체이닝 순서
- L() 호출 → this(1) 실행 (L()의 본문은 대기)
- L(int) 호출 → this(1, 2) 실행 (L(int)의 본문은 대기)
- L(int, int) 실행 → "L(int,int): 1,2" 출력
- L(int)의 본문 실행 → "L(int): 1" 출력
- L()의 본문 실행 → "L()" 출력
핵심: this()를 따라가서 가장 깊은 생성자가 먼저 완전히 실행된 후, 역순으로 본문이 실행됩니다.
문제: 3단계 상속 + 오버라이딩
질문: 출력 결과는?
class M {
void test() {
System.out.println("M");
}
M() {
test();
}
}
class N extends M {
void test() {
System.out.println("N");
}
}
class O extends N {
void test() {
System.out.println("O");
}
}
public class Test10 {
public static void main(String[] args) {
O o = new O();
}
}
O
해설:
- O 객체 생성 → N 생성자 호출 → M 생성자 호출
- M의 생성자에서 test() 호출
- 실제 객체 타입은 O이므로, O의 test() 메서드 실행!
- "O" 출력
핵심:
- 생성자는 부모부터 실행되지만
- 메서드는 실제 객체 타입(O)의 메서드가 실행됩니다 (동적 바인딩)
- 생성자에서 오버라이딩된 메서드를 호출하면 자식 클래스의 메서드가 실행됩니다!
문제: 기본 필드 숨김
질문: 출력 결과는?
class A {
int i;
public A(int i) { this.i = i; }
int get() { return i; }
}
class B extends A {
int i;
public B(int i) { super(2*i); this.i = i; }
int get() { return i; }
}
public class Main {
public static void main(String[] args) {
A ab = new B(7);
System.out.println(ab.i + ", " + ab.get());
}
}
14, 7
해설:
- ab.i: 참조 타입이 A이므로 A의 필드 i = 14 (super(14)로 초기화됨)
- ab.get(): 실제 객체는 B이므로 B의 get() 메서드 실행 → B의 필드 i = 7 반환
- 핵심: 필드는 참조 타입 기준, 메서드는 실제 객체 타입 기준
문제: super.필드 접근
질문: 출력 결과는?
class A {
int x = 10;
int getX() { return x; }
}
class B extends A {
int x = 20;
int getX() { return x; }
int getSuperX() { return super.x; }
}
public class Main {
public static void main(String[] args) {
A a = new B();
B b = new B();
System.out.println(a.x + ", " + a.getX());
System.out.println(b.x + ", " + b.getX() + ", " + b.getSuperX());
}
}
10, 20
20, 20, 10
해설:
- a.x: 참조 타입 A → A의 x = 10
- a.getX(): 실제 객체 B → B의 getX() → B의 x = 20
- b.x: 참조 타입 B → B의 x = 20
- b.getX(): B의 getX() → B의 x = 20
- b.getSuperX(): super.x로 명시 → A의 x = 10
문제: 생성자에서 필드 초기화
질문: 출력 결과는?
class A {
int num = 1;
public A() {
num = 10;
System.out.print(num + " ");
}
}
class B extends A {
int num = 2;
public B() {
num = 20;
System.out.print(num + " ");
}
}
public class Main {
public static void main(String[] args) {
A a = new B();
System.out.print(a.num);
}
}
10 20 10
해설:
- B 객체 생성 → A 생성자 실행 → A.num = 10, "10 " 출력
- B 생성자 실행 → B.num = 20, "20 " 출력
- a.num: 참조 타입 A → A.num = 10 출력
문제 4: 복잡한 필드 계산
질문: 출력 결과는?
class A {
int x;
public A(int x) { this.x = x; }
int calc() { return x * 2; }
}
class B extends A {
int x;
public B(int x) {
super(x + 5);
this.x = x;
}
int calc() { return x + super.x; }
}
public class Main {
public static void main(String[] args) {
A a = new B(10);
System.out.println(a.x + ", " + a.calc());
}
}
15, 25
해설:
- B(10) 호출 → super(15) → A.x = 15, B.x = 10
- a.x: 참조 타입 A → A.x = 15
- a.calc(): 실제 객체 B → B의 calc() → B.x(10) + A.x(15) = 25
문제: 3단계 상속 필드 숨김
질문: 출력 결과는?
class A {
int n = 1;
int get() { return n; }
}
class B extends A {
int n = 2;
int get() { return n + super.n; }
}
class C extends B {
int n = 3;
int get() { return n; }
}
public class Main {
public static void main(String[] args) {
A a = new C();
B b = new C();
C c = new C();
System.out.println(a.n + " " + a.get());
System.out.println(b.n + " " + b.get());
System.out.println(c.n + " " + c.get());
}
}
1 3
2 3
3 3
해설:
- a.n: 참조 A → A.n = 1
- a.get(): 객체 C → C.get() → C.n = 3
- b.n: 참조 B → B.n = 2
- b.get(): 객체 C → C.get() → C.n = 3
- c.n: 참조 C → C.n = 3
- c.get(): C.get() → C.n = 3
문제: 메서드 체이닝과 필드
질문: 출력 결과는?
class A {
int val = 5;
int getVal() { return val; }
int compute() { return getVal() * 2; }
}
class B extends A {
int val = 10;
int getVal() { return val; }
}
public class Main {
public static void main(String[] args) {
A a = new B();
System.out.println(a.val + ", " + a.compute());
}
}
5, 20
해설:
- a.val: 참조 A → A.val = 5
- a.compute(): 실제 객체 B → A의 compute() 실행
- A.compute()에서 getVal() 호출 → 오버라이딩된 B.getVal() 실행 → 10 반환
- 10 * 2 = 20
문제: static 필드와 인스턴스 필드
질문: 출력 결과는?
class A {
static int x = 1;
int y = 2;
}
class B extends A {
static int x = 10;
int y = 20;
}
public class Main {
public static void main(String[] args) {
A a = new B();
System.out.println(a.x + ", " + a.y);
System.out.println(A.x + ", " + B.x);
}
}
1, 2
1, 10
해설:
- a.x: static 필드는 참조 타입 기준 → A.x = 1
- a.y: 인스턴스 필드도 참조 타입 기준 → A.y = 2
- A.x: A의 static 필드 = 1
- B.x: B의 static 필드 = 10 (상속이 아니라 숨김
문제: 생성자 체인 + 필드 계산
질문: 출력 결과는?
class A {
int a;
public A(int x) { a = x; }
int sum() { return a; }
}
class B extends A {
int a;
public B(int x) {
super(x * 2);
a = x * 3;
}
int sum() { return a + super.a; }
}
public class Main {
public static void main(String[] args) {
A obj = new B(5);
System.out.println(obj.a + ", " + obj.sum());
}
}
10, 25
해설:
- B(5) → super(10) → A.a = 10, B.a = 15
- obj.a: 참조 A → A.a = 10
- obj.sum(): 객체 B → B.sum() → B.a(15) + A.a(10) = 25
문제: 메서드에서 this와 super
질문: 출력 결과는?
class A {
int x = 1;
void show() { System.out.print(x + " "); }
void test() { show(); }
}
class B extends A {
int x = 2;
void show() { System.out.print(x + " "); }
}
public class Main {
public static void main(String[] args) {
A a = new B();
a.test();
System.out.print(a.x);
}
}
2 1
해설:
- a.test(): A의 test() 실행 → show() 호출
- show()는 오버라이딩됨 → B.show() 실행 → B.x(2) 출력
- a.x: 참조 A → A.x = 1
문제: 최종 보스
질문: 출력 결과는?
class A {
int n = 1;
public A(int n) { this.n = n; }
int calc() { return n * 10; }
}
class B extends A {
int n = 2;
public B(int n) {
super(n + 1);
this.n = n;
}
int calc() { return super.calc() + n; }
}
class C extends B {
int n = 3;
public C(int n) {
super(n - 1);
this.n = n;
}
int calc() { return super.calc() + super.n + n; }
}
public class Main {
public static void main(String[] args) {
A a = new C(10);
System.out.println(a.n + ", " + a.calc());
}
}
10, 128
해설:
- C(10) → B(9) → A(10)
- A.n = 10, B.n = 9, C.n = 10
- a.n: 참조 A → A.n = 10
- a.calc(): 객체 C → C.calc() 실행
- super.calc() → B.calc() 실행
- super.calc() → A.calc() = A.n * 10 = 100
- 100 + B.n(9) = 109
- 109 + B.n(9) + C.n(10) = 128
- super.calc() → B.calc() 실행