Java — 바인딩 규칙이 문제의 8할이다
Java 문항은 매회 2~4개, 그리고 거의 전부가 "어느 메서드/필드가 선택되는가" 하나로 수렴한다. 오버라이딩·오버로딩·하이딩·섀도잉의 4규칙 표를 체화하면 절반은 끝난다. 이 편의 코드는 전부 2024~2026년 기출 복원이며 javac로 실행 검증했다.
1. 4규칙 표 — 무엇이 언제 결정되는가
| 대상 | 결정 시점 | 기준 | 이름 |
|---|---|---|---|
| 인스턴스 메서드 (재정의) | 런타임 | 실제 객체 타입 | 오버라이딩 (동적 바인딩) |
| 메서드 시그니처 선택 | 컴파일 타임 | 참조(선언) 타입에 보이는 것 | 오버로딩 (정적 선택) |
| static 메서드 | 컴파일 타임 | 참조 타입 | 하이딩 (메서드 숨김) |
| 필드 | 컴파일 타임 | 참조 타입 | 섀도잉 (필드 은닉) |
한 줄 요약: 인스턴스 메서드만 실제 객체를 따라가고, 나머지 전부는 참조 타입을 따른다.
1.1 하이딩+오버라이딩 복합 (2025-2 기출 → 5P)
class P {
static String print() { return "P"; }
int sum(int a) { return a + 1; }
}
class C extends P {
static String print() { return "C"; }
int sum(int a) { return a + 2; }
}
P tmp = new C();
System.out.println(tmp.sum(3) + tmp.print());
/* sum: 인스턴스 메서드 → C의 것 → 5
print: static → 참조 타입 P의 것 → "P" ⇒ 5P */
1.2 필드 섀도잉 (2024-3 기출 → 52)
class B { int x = 3; int getX() { return x * 2; } }
class A extends B { int x = 7; int getX() { return x * 3; } }
B b1 = new A();
A b2 = new A();
System.out.print(b1.getX() + b1.x + b2.getX() + b2.x);
/* b1.getX() → A의 것 21 (메서드=동적)
b1.x → B의 3 (필드=참조 타입!)
b2 → 21 + 7 ⇒ 21+3+21+7 = 52 */
함정 — 필드는 오버라이드되지 않는다. 부모/자식이 같은 이름의 필드를 각각 가질 뿐, 어느 것이 보이느냐는 참조 타입이 정한다.
1.3 오버로딩은 컴파일 타임 — 2026-1회 최다 오답 문항
class A {
String f(Object x) { return "1"; }
String g() { return f("a"); } // ← 컴파일 시 A에 보이는 f(Object)로 확정
}
class B extends A {
String f(Object x) { return "2"; } // 오버라이드 (런타임에 이것이 실행됨)
String f(String x) { return "3"; } // 새 오버로드 — 호출 후보조차 아니다
}
A a = new B();
System.out.println(a.g()); /* → 2 ("3"이 대표 오답) */
2단계로 갈라 읽어라 — ① 컴파일: 선언 타입(A)의 후보 중 시그니처 확정(f(Object)). ② 런타임: 그 시그니처를 실제 객체(B)가 재정의했으면 그쪽 실행. B의 f(String)은 A에 없던 신규 오버로드라 ①에서 이미 배제된다.
1.4 제네릭 소거 + 오버로딩 (2024-3 기출 → B0)
class Printer {
void print(Integer a) { System.out.print("A" + a); }
void print(Object a) { System.out.print("B" + a); }
void print(Number a) { System.out.print("C" + a); }
}
/* Collection<T>의 value(타입 변수 T, 상한 없음)를 print(value) 하면?
→ T는 소거(erasure) 후 Object — print(Object) 선택 ⇒ B0 */
런타임에 값이 Integer(0)라는 사실은 아무 영향이 없다. 상한이 T extends Number였다면
C0가 됐을 것 — 오버로드 선택은 철저히 컴파일 시점의 정적 타입 게임이다.
2. == vs equals — 참조 비교와 내용 비교
/* 2024-2 기출 → NNN */
int a[] = {1,2,3,4}; int b[] = {1,2,3,4}; int c[] = {1,2,3};
check(a, b); // a == b : 내용 같아도 다른 객체 → N
/* 2024-3 기출 → OOAAA */
m[0] = "A"; m[1] = "A"; m[2] = new String("A");
m[0].equals(m[1]) // 내용 비교 → O
m[1].equals(m[2]) // new String이어도 내용 같음 → O
| 비교 | 배열 | String 리터럴 | new String |
|---|---|---|---|
== | 참조 — 항상 별개 | 풀 공유로 같을 수 있음 | 항상 별개 객체 |
equals | (Object 상속) 참조 비교 | 내용 비교 | 내용 비교 |
함정 조합 — 문제가
equals면 new String도 같다. 문제가==면 내용이 같아도 다르다. 어느 쪽 연산자인지에 밑줄부터 긋는다.
3. 생성자 체이닝과 "실행 순서" 문항
/* 2024-1 기출 — ①~⑦ 실행 순서: ⑤⑥③①⑦② */
Child(int x) { // ③ 생성자에 먼저 '진입'하고
super(x + 1, x); // 첫 줄 super가 Parent ① 실행
this.x = x;
}
new Child(3): Child 생성자 진입(③) → super() → Parent 생성자(①) → 복귀. 시험 관례상 "③ → ①" 순서로 적는다 (진입이 먼저).- 자식에
getT(int n)처럼 시그니처가 다른 메서드는 오버라이드가 아니다 — 부모 참조로getT()를 부르면 부모 것이 실행된다(§1.3과 같은 원리). - 부모에 기본 생성자가 없으면 자식 생성자 첫 줄은 반드시
super(인자)(2025-3 기출 — 빈칸 정답 super). 자기 클래스의 다른 생성자는this(...).
3.1 생성자 안에서 부른 메서드도 동적 바인딩 (2025-1 기출 → 54)
class Parent {
static int total = 0;
public Parent() { total += (++v); show(); } // ← 이 show()는 Child.show()다!
public void show() { total += total; }
}
class Child extends Parent {
public Child() { v += 2; total += v++; show(); }
@Override public void show() { total += total * 2; }
}
new Child();
System.out.println(Parent.total);
total 하나(static)에 모든 단계가 누적된다 — 한 줄씩 값을 적어 굴린다:
| 단계 | 실행 | total |
|---|---|---|
| Child() → 암묵적 super() → Parent() | total += ++v (Parent.v 1→2) | 2 |
Parent()의 show() → 동적 바인딩으로 Child.show() | total += total*2 = 2+4 | 6 |
Child() 몸통: v += 2(Child.v 10→12), total += v++ | 6 + 12 | 18 |
Child()의 show() → Child.show() | 18 + 36 | 54 |
함정 2연발 — ① 부모 생성자 몸통에서 호출해도 재정의된 메서드는 자식 버전(Child.show, total의 3배)이 불린다. ② Parent와 Child가 각자
v필드를 갖는데, super()는 Parent.v(1)를, 몸통은 Child.v(10)를 쓴다(필드 섀도잉 §1.2). statictotal은 클래스당 하나라 전 단계가 쌓인다.
3.2 super.메서드() 는 이어 붙이기 (2026-2 기출 → 10a20b)
@Override void print() { super.print(); System.out.print(b + "b"); } —
부모 출력(10a)이 먼저, 자식 출력(20b)이 뒤. 순서만 정확히.
4. 예외 처리 — 매칭은 위에서부터, finally는 무조건
/* 2025-1 기출 → 출력1출력5 */
try { System.out.print(a / b); } // 5/0 → ArithmeticException
catch (ArithmeticException e) { ... } // ← 첫 매칭 하나만 실행
catch (Exception e) { ... } // 실행 안 됨
finally { ... } // 항상 실행
/* 2024-3 기출 → 101 */
static void func() throws Exception { throw new NullPointerException(); }
/* 선언이 throws Exception이어도, 매칭은 '실제 던져진 객체' NPE 기준
→ catch(NullPointerException) +1 → finally +100 → 101 */
규칙 3개면 끝난다: ① 구체 타입 catch가 먼저 선언돼 있으면 그쪽이 잡는다 ② catch는 하나만 실행된다 ③ finally는 예외 여부·return 여부와 무관하게 실행된다.
람다 버전(2025-2 기출 → 19): 함수형 인터페이스의 apply(3)이 조건에 걸려 throw하면
run()의 catch가 7을 반환 — 람다여도 예외 흐름 규칙은 동일하다.
5. 문자열 — + 는 좌에서 우로, split은 조각 배열
/* 2026-1 기출 → 1123 */
int x1 = 9, x2 = 2; String x3 = "3";
System.out.println(x1 + x2 + "2" + x3);
/* (9+2)=11 산술 → 11+"2"="112" 연결 → +"3" → "1123" */
+는 좌→우 결합. int끼리 만나는 동안만 덧셈이고, String을 만나는 순간부터 전부 연결이다. "9223"(전부 연결)과 "16"(전부 덧셈)이 양쪽 대표 오답.
/* 2024-2 기출 → S */
"ITISTESTSTRING".split("T") // {"I", "IS", "ES", "S", "RING"}
result[3] // "S" — TT 연속 구간은 사이 문자로 갈린다
split은 구분자를 버리고 조각을 담는다. 조각을 0번부터 나열한 표를 그리고 인덱스를 센다.
6. static 필드 = 클래스당 하나 (싱글톤 포함)
/* 2024-1 기출 → 4 */
static public Connection get() {
if (_inst == null) { _inst = new Connection(); }
return _inst; // 항상 같은 인스턴스
}
/* conn1·conn2·conn3 전부 동일 객체 — count() 4회 → 4 */
참조 변수가 몇 개든 인스턴스가 하나면 필드도 하나다. §3.1의 Parent.total도
같은 원리(클래스당 하나)로 모든 생성자·메서드 호출이 한 변수에 누적됐다.
7. 참조 전달 — 무엇이 호출부에 남는가
/* 2025-2 기출 → BB */
public static void change(String[] data, String s) {
data[0] = s; // 배열 '원소' 수정 → 호출부에 보인다
s = "Z"; // 매개변수 '재대입' → 호출부와 무관
}
/* 2025-2 기출 → 1a3b3 */
BO[] arr = {a, b, c};
BO t = arr[0]; arr[0] = arr[2]; arr[2] = t; // 배열 '칸'의 참조만 교환
arr[1].v = arr[0].v; // b.v = c.v = 3 (객체 내용 수정)
System.out.println(a.v + "a" + b.v + "b" + c.v); // 변수 a,b,c 기준!
구분 훈련 — ① 배열 칸의 참조 교환은 변수(a, b, c)에 아무 일도 하지 않는다. ②
.필드 = 값만이 객체 내용을 바꾸고, 그 객체를 가리키는 모든 변수에 보인다. ③ 출력이 배열 기준인지 원래 변수 기준인지 마지막에 반드시 확인.
8. enum — values()와 name()
/* 2025-3 기출 → AB */
enum Tri { A("A"), B("AB"), C("ABC"); ... }
Tri t = Tri.values()[Tri.A.name().length()];
/* name() = 상수 이름 "A" → 길이 1 → values()[1] = B → B.code() = "AB" */
values()는 선언 순서의 배열,name()은 상수 이름 그대로,ordinal()은 선언 인덱스.- 생성자 인자로 받은 필드(code)와 상수 이름(name)을 구분하는 것이 함정의 전부다.
9. 재귀·분할정복 — 호출 트리로 굴린다
/* 2025-1 기출 → 20 */
static int func(int[] a, int st, int end) {
if (st >= end) return 0;
int mid = (st + end) / 2;
return a[mid] + Math.max(func(a, st, mid), func(a, mid + 1, end));
}
/* func(0,4)=a[2]+max(func(0,2)=8, func(3,4)=12) = 8+12 = 20 */
/* 2025-1 기출 → 4 — 오버로딩 갈림 재귀 */
static int calc(int value) { ... calc(value-1) + calc(value-2); }
static int calc(String str) { ... calc(value-1) + calc(value-3); }
calc("5"); /* String 버전은 최초 1회만 — value-1, value-3은 int라
이후 전부 int 버전(피보나치형) → calc(4)+calc(2) = 3+1 = 4 */
String/int 오버로드가 섞인 재귀는 인자의 정적 타입이 어느 오버로드를 고르는지를 호출마다 표기하며 트리를 그린다. §1.3의 컴파일 타임 선택 규칙이 재귀에도 그대로 적용된다.
재귀 중복 제거(2024-2 기출 → dcba): 되돌아오며 c + result로 앞에 붙이는지
result + c로 뒤에 붙이는지, seen 배열이 무엇을 거르는지 — 붙이는 방향에 밑줄.
10. Java 문법 빠른 참조 (코드 문항 어디서든)
- 기본형 vs 참조형:
int·double·char·boolean(값 자체) vs 객체(참조). 래퍼 클래스Integer·Double·Character·Boolean. 오토박싱(int↔Integer 자동 변환). - 정수 나눗셈:
7/2=3(int끼리 몫),7.0/2=3.5.%는 나머지.char는 정수처럼 연산 ('A'+1→ 66,(char)('A'+1)→ 'B'). - 형변환: 작은 타입→큰 타입은 자동, 반대는 명시
(int)3.9=3(버림). 문자열→숫자는Integer.parseInt("5"), 숫자→문자열은String.valueOf(x)또는""+x. - 문자열 메서드:
.length(),.charAt(i),.substring(a,b)(b 미포함),.split(구분자),.equals()(내용),==(참조),.indexOf(),.replace(). String은 불변. - 배열:
int[] a = new int[5](기본값 0),a.length(괄호 없음 — 문자열의 length()와 다름),Arrays.equals()로 내용 비교. - 증감·연산자: 전위
++x/후위x++, 논리&&/||/!, 비트&/|/^/~, 시프트<</>>/>>>(부호 없는). - 접근 제어자:
public(전체) >protected(같은 패키지+자식) > (default, 같은 패키지) >private(자기 클래스).static(클래스 소속),final(상수·상속/재정의 금지). - 삼항:
조건 ? a : b. Math:Math.max/min/abs/pow/sqrt.
11. 고난도 함정 (자체 출제 hard 세트 대비)
기출엔 아직 안 나왔지만 나올 수 있는 Java 함정. 각 항목은 hard 세트의 실제 문항이다.
11.1 Integer 캐시 — ==가 값에 따라 갈린다
Integer x = 100, y = 100; // x == y → true (캐시)
Integer p = 200, q = 200; // p == q → false (캐시 밖)
오토박싱은 −128~127 범위의 Integer를 캐시해 같은 객체를 재사용한다. 그 범위 안이면
==가 true, 벗어나면 매번 새 객체라 false다. 래퍼 값 비교는 언제나.equals()로. 기본형int끼리의==는 값 비교라 이 함정과 무관하다.
11.2 Java switch도 fall-through
switch (n) { // n=2
case 1: s += 1;
case 2: s += 2; // 진입, break 없음
case 3: s += 3; // 흘러내림
default: s += 10; // 여기까지
} // s = 2+3+10 = 15
C와 똑같이 break가 없으면 아래 case로 흘러내린다. (자바 14+의 화살표
case ->는 fall-through가 없지만, 실기 코드는 전통case :형태다.)
11.3 생성자에서 부른 가상 메서드 — 자식 필드는 아직 0
class P { int x = 10; P() { x = add(); } int add() { return x + 1; } }
class C extends P { int y = 5; int add() { return y + 100; } }
new C(); // P() 실행 시 add()는 C.add() (동적 바인딩), 그러나 y는 아직 0 → x=100
초기화 순서: 부모 생성자 → 자식 필드 초기화 → 자식 생성자. 부모 생성자가 도는 시점엔 자식 필드가 아직 기본값(0/null) 이다. 그 상태에서 재정의 메서드(C.add)가 불리면 y=0으로 계산된다(100). 이후 y=5로 초기화되어 최종 x=100, y=5.
11.4 삼항 연산자의 숫자 타입 승격
Object o = true ? Integer.valueOf(1) : Double.valueOf(2.0); // o = 1.0 (Double)
삼항의 두 갈래가 Integer와 Double이면 공통 수치 타입 double로 승격된다 — 조건이 참이라 1이 선택돼도 타입은 이미 double이라 1.0으로 나온다. 값 선택보다 타입 결정이 먼저다.
11.5 자주 쓰는 라이브러리
Arrays.sort(arr)는 오름차순 in-place 정렬(반환값 없음). 정렬 후arr[0]이 최소,arr[length-1]이 최대.Arrays.equals(a, b)는 내용 비교(배열의==·equals는 참조 비교).Integer.parseInt("5"),String.valueOf(x),Math.max/min/abs.
12. 시험장 체크리스트
- 참조 타입과 실제 객체 타입을 코드 옆에 나란히 적는다 (
P tmp = new C()→ P/C). - 인스턴스 메서드만 실제 객체를 따른다. static·필드·오버로드 선택은 참조 타입.
- 자식의 같은 이름 메서드가 시그니처까지 같은지 확인 — 다르면 오버로드일 뿐.
- 생성자: 진입(자식) → super(부모) → 몸통. 그 안의 가상 호출은 자식 버전.
- 예외: 던져진 '객체'의 타입으로 위에서부터 매칭, 하나만, finally 무조건.
+연쇄는 좌→우로 타입을 추적. String 등장 이후는 전부 연결.- 출력 대상이 배열인지 변수인지, name()인지 필드인지 — 마지막 확인.
출처
- 2024-1 ~ 2026-2 실기 복원 기출 (이 트랙 수록 8회차) — 기사퍼스트·두목넷·수제비(공개 미러)·뉴비티·chobopark 교차 검증
- 본문 코드 전량 javac(21) 컴파일·실행 검증 완료