좋아요! 🔥 이번에는 훨씬 복잡하고 헷갈리는 상속 트릭 문제를 드릴게요.
이 문제를 정신 바짝 차리고 디버깅해야 합니다.
난이도 확!! 🚀
class A {
String value = "A";
A() {
System.out.println("[A 생성자 시작]");
init();
System.out.println("[A 생성자 끝]");
}
void init() {
System.out.println("A.init() 호출됨 value=" + value);
}
void print() {
System.out.println("A.print() 호출됨 value=" + value);
}
}
class B extends A {
String value = "B";
B() {
System.out.println("[B 생성자 시작]");
value = "B 생성자에서 바뀜";
System.out.println("[B 생성자 끝]");
}
@Override
void init() {
System.out.println("B.init() 호출됨 value=" + value);
}
@Override
void print() {
System.out.println("B.print() 호출됨 value=" + value);
}
}
class C extends B {
String value = "C";
C() {
System.out.println("[C 생성자 시작]");
value = "C 생성자에서 바뀜";
System.out.println("[C 생성자 끝]");
}
@Override
void init() {
System.out.println("C.init() 호출됨 value=" + value);
}
@Override
void print() {
System.out.println("C.print() 호출됨 value=" + value);
}
}
public class Test {
public static void main(String[] args) {
A obj = new C();
System.out.println("-- 이제 print 호출 --");
obj.print();
}
}
✅ init()이 생성자 안에서 호출될 때 실제 오버라이딩된 메서드가 호출된다는 점
✅ 필드 초기화 타이밍 (String value = ...) vs 생성자 호출 타이밍
✅ super() 호출 구조와 부모 클래스 생성자 호출 시점
new C() 호출→ 생성자 호출 체인: A() → B() → C()
→ 각 클래스의 필드 초기화 & 생성자 바디 실행 순서:
가장 먼저 A의 필드 value="A" 초기화
A 생성자 호출 → [A 생성자 시작] 출력
A 생성자 안에서 init() 호출됨
value 필드는 기본값 null 상태C.init() 호출됨 value=null 출력A 생성자 끝 → [A 생성자 끝] 출력
B 필드 초기화(value="B")
B 생성자 호출 → [B 생성자 시작] 출력
→ B 생성자 바디에서 value = "B 생성자에서 바뀜"; 대입
→ [B 생성자 끝] 출력
C 필드 초기화(value="C")
C 생성자 호출 → [C 생성자 시작] 출력
→ C 생성자 바디에서 value = "C 생성자에서 바뀜";
→ [C 생성자 끝] 출력
[A 생성자 시작]C.init() 호출됨 value=null (init 호출되는 시점엔 C의 value 초기화 안됨)[A 생성자 끝][B 생성자 시작][B 생성자 끝][C 생성자 시작][C 생성자 끝]obj.print();→ obj의 실제 타입 C → C의 print 호출
→ 현재 C의 value = "C 생성자에서 바뀜"
→ 출력: C.print() 호출됨 value=C 생성자에서 바뀜
[A 생성자 시작]
C.init() 호출됨 value=null
[A 생성자 끝]
[B 생성자 시작]
[B 생성자 끝]
[C 생성자 시작]
[C 생성자 끝]
-- 이제 print 호출 --
C.print() 호출됨 value=C 생성자에서 바뀜
✅ 생성자 호출 순서:
value가 부모와 자식에 각각 존재 → this.value / super.value 구분 필수좋아요! 🔥 이번에는 인터페이스 + 다형성 + 업캐스팅 트릭 문제와 static 트릭 문제를 한 문제씩 내드릴게요.
그리고 자세히 디버깅하면서 풀이해 드릴게요.
interface Animal {
void sound();
static void info() {
System.out.println("Animal interface static info");
}
}
class Dog implements Animal {
String name = "Dog";
@Override
public void sound() {
System.out.println(name + " barks");
}
static void info() {
System.out.println("Dog class static info");
}
}
class Cat implements Animal {
String name = "Cat";
@Override
public void sound() {
System.out.println(name + " meows");
}
static void info() {
System.out.println("Cat class static info");
}
}
public class Test {
public static void main(String[] args) {
Animal a1 = new Dog();
Animal a2 = new Cat();
a1.sound();
a2.sound();
a1.info();
a2.info();
Dog.info();
Cat.info();
Animal.info();
}
}
이 프로그램의 출력값을 예측하고, 왜 그런 출력이 나오는지 하나씩 디버깅해 보세요.
Animal a1 = new Dog(); → 업캐스팅. 실제 객체는 Dog.Animal a2 = new Cat(); → 업캐스팅. 실제 객체는 Cat.a1.sound();sound() 호출 → "Dog barks"a2.sound();sound() 호출 → "Cat meows"a1.info(); a2.info();info()는 인터페이스의 static 메서드이므로 오버라이딩 X.Animal이므로 Animal.info() 호출됨"Animal interface static info"Dog.info(); Cat.info(); Animal.info();"Dog class static info""Cat class static info""Animal interface static info"Dog barks
Cat meows
Animal interface static info
Animal interface static info
Dog class static info
Cat class static info
Animal interface static info
✅ 인터페이스의 static 메서드는 구현 클래스의 static 메서드와 전혀 다른 것
✅ 호출할 때 참조변수의 타입 기준 → 업캐스팅된 경우 인터페이스 쪽 호출
✅ 정적 메서드는 오버라이딩 없음!
class X {
static int num = 0;
X() {
num++;
System.out.println("X 생성자 호출 num=" + num);
}
static {
num += 10;
System.out.println("static 블록 호출 num=" + num);
}
}
class Y extends X {
static int num = 100;
Y() {
num++;
System.out.println("Y 생성자 호출 num=" + num);
}
static {
num += 50;
System.out.println("Y static 블록 호출 num=" + num);
}
}
public class Test2 {
public static void main(String[] args) {
new Y();
new Y();
}
}
이 프로그램의 출력값을 정확히 쓰고 왜 그렇게 출력되는지 디버깅해 보세요.
new Y() 호출되면 → Y 클래스 로딩static 블록 호출 num=10Y static 블록 호출 num=150new Y() 생성자 호출"X 생성자 호출 num=11""Y 생성자 호출 num=151"new Y() 생성자 호출"X 생성자 호출 num=12""Y 생성자 호출 num=152"static 블록 호출 num=10
Y static 블록 호출 num=150
X 생성자 호출 num=11
Y 생성자 호출 num=151
X 생성자 호출 num=12
Y 생성자 호출 num=152
✅ static 블록은 클래스 최초 로딩 때 1번만 호출
✅ 부모 클래스 static → 자식 클래스 static → 부모 생성자 → 자식 생성자 호출 순서
✅ 부모의 static 필드와 자식의 static 필드가 이름 겹치지 않고 별도로 존재 (X.num, Y.num 별개)
좋아요! 🎯 이번에는 **증감 연산자(++, --)**를 교묘히 섞어서 실수를 유도하는 트릭 문제를 내고, 풀이까지 자세히 해드릴게요.
이번에는 상속 계층과 static 필드를 이용하고, 거기에 증감 연산자를 섞습니다.
class A {
static int num = 0;
A() {
num += 5;
System.out.println("A 생성자 호출 num=" + num++);
}
static {
num = 10;
System.out.println("A static 블록 num=" + num++);
}
}
class B extends A {
static int num = 100;
B() {
num -= 3;
System.out.println("B 생성자 호출 num=" + num--);
}
static {
num += 10;
System.out.println("B static 블록 num=" + num--);
}
}
public class Test {
public static void main(String[] args) {
new B();
new B();
}
}
이 프로그램의 출력값을 한 줄 한 줄 정확히 써 보세요.
그리고 왜 그런 출력값이 나오는지 철저히 디버깅해 보세요!
new B() 호출 → B 클래스 초기화B 상속 부모 → A 클래스 초기화 시작
A.static { num = 10; } → num=10 출력 "A static 블록 num=10"B 클래스 초기화
B.static { num = 100; }(이미 num 선언됨) → num += 10 → num = 110"B static 블록 num=110"new B() 생성자 호출new B()부모 A 생성자 호출
"A 생성자 호출 num=16"자식 B 생성자 호출
"B 생성자 호출 num=106"new B()부모 A 생성자 호출
"A 생성자 호출 num=22"자식 B 생성자 호출
"B 생성자 호출 num=102"A static 블록 num=10
B static 블록 num=110
A 생성자 호출 num=16
B 생성자 호출 num=106
A 생성자 호출 num=22
B 생성자 호출 num=102
✅ 부모(A)와 자식(B) 클래스의 num 필드는 이름은 같지만 완전히 별개
✅ static 블록은 클래스 최초 로딩할 때만 호출
✅ num++ vs ++num, num-- 트릭 → 후위 연산자는 출력 후 증가/감소하기 때문에 출력값을 헷갈리기 쉬움
✅ 반드시 호출 순서와 현재 필드 상태를 하나씩 추적하기