class Base {
int num = 10;
Base() {
num++;
System.out.println("[Base 생성자] num = " + num); // ?
}
void print() {
System.out.println("[Base.print()] num = " + num++); // ?
}
}
class Derived extends Base {
int num = 100;
Derived() {
num += 5;
System.out.println("[Derived 생성자] num = " + num); // ?
}
@Override
void print() {
System.out.println("[Derived.print()] num = " + num++); // ?
}
}
public class Test {
public static void main(String[] args) {
Base b = new Derived();
b.print();
}
}
new Derived() 호출Derived 생성자 호출 → 먼저 Base 생성자 호출
Base 생성자 안에서 num++
출력: [Base 생성자] num = 11
[Derived 생성자] num = 105b.print() 호출Derived.print() 호출됨[Derived.print()] num = 105 (후치 연산자니까 출력 후 1 증가 → Derived.num = 106)[Base 생성자] num = 11
[Derived 생성자] num = 105
[Derived.print()] num = 105
✅ 필드 오버라이딩이 아닌 숨김이므로 Base.num과 Derived.num은 별개
✅ 생성자 호출 순서: Base → Derived
✅ 오버라이딩된 메서드 호출은 실제 객체 타입 기준
✅ 후치 연산자는 출력 후 증가
🎯 이런 문제의 키포인트:
- Base와 Derived에 같은 이름의 필드가 있으면 별도로 관리됨
- 오버라이딩된 메서드 호출은 반드시 실제 객체 메서드 호출
num++출력 타이밍 반드시 생각
좋아요! 🔥 이번에는 인터페이스 + 상속 + 전치/후치 연산자 트릭을 한꺼번에 섞은 고난도 문제를 드릴게요.
이건 정말 실전 시험 트릭 문제 수준입니다. 반드시 한 줄 한 줄 디버깅해보며 풀어보셔야 합니다.
interface Printer {
void print();
}
class Base implements Printer {
int num = 1;
Base() {
num++;
System.out.println("[Base 생성자] num = " + num++); // ?
}
@Override
public void print() {
System.out.println("[Base.print()] num = " + num++); // ?
}
}
class Derived extends Base {
int num = 10;
Derived() {
num += 10;
System.out.println("[Derived 생성자] num = " + num--); // ?
}
@Override
public void print() {
System.out.println("[Derived.print()] num = " + num++); // ?
}
}
public class Test {
public static void main(String[] args) {
Printer p = new Derived();
p.print();
}
}
new Derived() 호출생성자 호출 과정:
Derived() 호출 → Base() 호출
Base의 필드 초기화: Base.num = 1
Base 생성자 안:
[Base 생성자] num = 2Derived의 num = 10 초기화됨 (Base.num과 별개 필드)
Derived 생성자 본문:
[Derived 생성자] num = 20p.print() 호출[Derived.print()] num = 19[Base 생성자] num = 2
[Derived 생성자] num = 20
[Derived.print()] num = 19
✅ 인터페이스(Printer) 참조이지만 오버라이딩된 메서드 호출됨
✅ 부모와 자식이 **같은 이름의 필드(num)**을 가질 때 별도로 관리
✅ 후치 연산자(num++, num--)의 출력 타이밍 반드시 주의
✅ 생성자 호출 순서: 부모(Base) → 자식(Derived)
✅ 생성자 안에서 초기화 순서:
좋아요! 🔥 이번에는 static + 오버라이딩 + 인터페이스 트릭을 한데 섞은, 정말 심화된 문제를 드릴게요.
이 문제를 꼼꼼히 디버깅하지 않으면 분명 헷갈릴 겁니다. 🎯
interface Speaker {
static int num = 1;
void speak();
static void staticMethod() {
System.out.println("[Speaker.staticMethod()] num = " + num++);
}
}
class Super implements Speaker {
static int num = 10;
Super() {
num++;
System.out.println("[Super 생성자] num = " + num++);
}
@Override
public void speak() {
System.out.println("[Super.speak()] num = " + num++);
}
static void staticMethod() {
System.out.println("[Super.staticMethod()] num = " + num++);
}
}
class Sub extends Super {
static int num = 100;
Sub() {
num--;
System.out.println("[Sub 생성자] num = " + num--);
}
@Override
public void speak() {
System.out.println("[Sub.speak()] num = " + num--);
}
static void staticMethod() {
System.out.println("[Sub.staticMethod()] num = " + num--);
}
}
public class Test {
public static void main(String[] args) {
Speaker sp = new Sub();
sp.speak();
Speaker.staticMethod();
Super.staticMethod();
Sub.staticMethod();
}
}
new Sub() 호출Super() 호출Super.num = 10
Super() 생성자:
[Super 생성자] num = 11Sub() 생성자Sub.num = 100[Sub 생성자] num = 100sp.speak()[Sub.speak()] num = 99Speaker.staticMethod()[Speaker.staticMethod()] num = 1Super.staticMethod()[Super.staticMethod()] num = 12Sub.staticMethod()[Sub.staticMethod()] num = 98[Super 생성자] num = 11
[Sub 생성자] num = 100
[Sub.speak()] num = 99
[Speaker.staticMethod()] num = 1
[Super.staticMethod()] num = 12
[Sub.staticMethod()] num = 98
✅ static 메서드는 오버라이딩되지 않음 → 호출한 클래스나 인터페이스 기준 출력
✅ 인스턴스 메서드(speak())는 오버라이딩됨 → 동적 바인딩
✅ 인터페이스의 static int num은 인터페이스의 상수처럼 쓰임 → 따로 관리
✅ 같은 이름의 static 필드가 Super, Sub, 인터페이스 각각 따로 존재
✅ 후치연산자는 출력할 때 증가/감소하기 전 값 출력
좋아요! 🔥 이번에는 람다 + 지역클래스 + final 트릭을 섞은, 매우 까다로운 문제를 드릴게요.
이 문제를 꼼꼼히 디버깅하면서 final 캡쳐, 유효 범위, 람다 트릭을 확실히 잡아봅시다!
@FunctionalInterface
interface Printer {
void print();
}
public class Test {
static int staticNum = 100;
public static void main(String[] args) {
int localNum = 10; // effectively final
final int finalNum = 20; // explicitly final
int modifiableNum = 30;
class LocalClass {
int localNum = 200;
void show() {
System.out.println("[LocalClass.show()] localNum = " + localNum);
}
}
LocalClass lc = new LocalClass();
lc.show();
Printer lambda = () -> {
staticNum++;
// localNum++; // ❌ 컴파일 오류 (캡쳐된 지역변수는 final 이어야 함)
// finalNum++; // ❌ 컴파일 오류
// modifiableNum++; // ❌ 컴파일 오류 (람다 안에서 수정 불가)
System.out.println("[Lambda] staticNum = " + staticNum + ", localNum = " + localNum + ", finalNum = " + finalNum);
};
lambda.print();
// 바깥쪽 modifiableNum은 여기서만 수정 가능
modifiableNum++;
System.out.println("[Main End] modifiableNum = " + modifiableNum);
}
}
localNum++, finalNum++, modifiableNum++ 안에서 수정)에서 왜 컴파일 오류가 나는지 설명해 주세요.localNum = 200(이건 지역 클래스 안의 필드) → show() 호출 → [LocalClass.show()] localNum = 200staticNum) 자유롭게 수정 가능 → 호출 전에 staticNum = 100이었음 → 람다 안에서 staticNum++ → staticNum = 101localNum은 main 내부에서 수정 안 했으므로 effectively final → 출력됨finalNum은 명시적 final → 출력됨modifiableNum은 수정된 적 있으므로 람다 안에서 캡쳐 불가 → 주석 부분 에러[Lambda] staticNum = 101, localNum = 10, finalNum = 20[Main End] modifiableNum = 31[LocalClass.show()] localNum = 200
[Lambda] staticNum = 101, localNum = 10, finalNum = 20
[Main End] modifiableNum = 31
✅ 람다는 외부 지역 변수를 캡쳐할 때 반드시 final 또는 effectively final 이어야 함 → 그렇지 않으면 컴파일 오류
✅ 람다 안에서 지역변수를 수정 불가 → final 트릭
✅ static 필드는 람다 안에서 자유롭게 수정 가능
✅ 지역 클래스 내부의 필드는 외부 지역변수와 별개 (여기서 localNum 필드와 지역변수 localNum 혼동 주의!)
✅ effectively final: 한 번 초기화 후 값이 변하지 않는 경우 final처럼 취급