static 메서드는 오버라이딩 불가, 인스턴스 메서드는 가능
결론
- static 메서드는 오버라이딩이 불가능하고, 인스턴스 메서드는 오버라이딩이 가능한 이유는 “메서드가 실행되는 방식이 다르기 때문”이다.
static 메서드는 JVM의 메서드 영역에 보관된다
- 클래스가 로딩되면 static 메서드가 JVM의 메서드 영역에 보관된다.
- static 메서드는 컴파일 시간에 확인되는 메모리 주소를 지닌다.
-
JVM의 메모리 구조도
JVM Memory (Simplified) ┌─────────────────┐ │ Method Area │ ← Class metadata, static variables, static methods │ (Class Area) │ ├─────────────────┤ │ Heap │ ← Object instances, instance variables ├─────────────────┤ │ Stack │ ← Method calls, local variables, object references └─────────────────┘
static 메서드의 실행 과정
- 컴파일 시간에 컴파일러가 메서드에 있는 static 키워드를 확인하고, 변수를 선언한 타입의 클래스가 정의한 static 메서드를 실행한다.
-
코드 예시
class Animal { public static void staticMethod() { System.out.println("Animal static method"); } } class Dog extends Animal { public static void staticMethod() { System.out.println("Dog static method"); } } // Animal이 참조하는 타입이다. Animal myDog = new Dog(); myDog.staticMethod(); // Outputs "Animal static method" - NO polymorphism!
왜 static 메서드는 오버라이딩 될 수 없는가
- 메서드 오버라이딩은 런타임에 실제 객체를 확인하는 과정에 의해 이뤄진다.
- static 메서드를 호출할 때는 실제로 사용되는 객체의 클래스 자체를 확인하지 않으므로 오버라이딩될 수 없다.
- static 메서드는 클래스 그 자체에 속하기 때문에, 자식 클래스가 같은 시그니처의 static 메서드를 정의해도 부모의 static 메서드와 개별적인 메서드가 된다.
➕메서드 하이딩(Method Hiding)
- 기술적으로 자식 클래스는 부모 클래스의 시그니처와 동일한 static 메서드를 정의할 수 있다.
- 이는 메서드 오버라이딩이 아닌 메서드 하이딩이라 불린다.
-
코드로 보는 예시
class Animal { public static void describe() { System.out.println("This is a generic Animal."); } public void makeSound() { System.out.println("The animal makes a sound."); } } class Dog extends Animal { // This HIDES the parent's static method. It does NOT override it. public static void describe() { System.out.println("This is a specific Dog."); } // This OVERRIDES the parent's instance method. @Override public void makeSound() { System.out.println("The dog barks."); } } public class Test { public static void main(String[] args) { Animal myAnimal = new Dog(); // The reference is Animal, the object is Dog. // --- Static Method Call (Method Hiding) --- myAnimal.describe(); // Output: "This is a generic Animal." // ^^^ COMPILE-TIME RESOLUTION ^^^ // The compiler sees `myAnimal` is of reference type `Animal`. // It directly links this call to `Animal.describe()`. // --- Instance Method Call (Overriding) --- myAnimal.makeSound(); // Output: "The dog barks." // ^^^ RUNTIME RESOLUTION (Dynamic Dispatch) ^^^ // The JVM sees the actual object is a `Dog` at runtime. // It calls `Dog.makeSound()`, the overridden version. // The correct way to call static methods: Animal.describe(); // "This is a generic Animal." Dog.describe(); // "This is a specific Dog." } }
➕메서드 하이딩에서 “자식 클래스의 static 메서드가 부모 클래스의 static 메서드를 숨긴다”고 하는 이유
- 메서드 하이딩의 예시 코드는 대개 부모 클래스의 static 메서드가 실행되는 모습을 보여준다.
- 따라서 되려 “부모의 static 클래스가 자식 클래스의 static 메서드를 숨기는 거 아님”이라고 할 수 있다.
- “자식 클래스의 static 메스드가 부모 클래스의 static 메서드를 숨긴다”는 말의 의미는 “자식 클래스만 봐서는 부모에게 같은 시그니처의 static 메서드가 있는지 알아챌 수 없다”는 의미이다.
- static 메서드는 오버라이딩 되지 않는다. 따라서 자식이 부모 클래스의 static 메서드와 시그니처가 동일한 메서드를 만들더라도 자식 클래스만 봐서는 메서드 하이딩을 하고 있는지 알 수 없다.
인스턴스 메서드는 가상 테이블을 사용한다
- 인스턴스 메서드 또한 메서드 영역에 저장된다. 그러나 접근되는 방식이 static 메서드와 다르다.
- 인스턴스 메서드들은 가상 테이블(vtable)을 통해 접근된다.
- 생성된 객체들은 자신의 클래스가 사용하는 vtable로의 참조값을 지닌다.
-
JVM의 메모리 구조도
JVM Memory (Simplified) ┌─────────────────┐ │ Method Area │ ← Class metadata, static variables, static methods │ (Class Area) │ ├─────────────────┤ │ Heap │ ← Object instances, instance variables ├─────────────────┤ │ Stack │ ← Method calls, local variables, object references └─────────────────┘
vtable(Dispatch Table)에 대하여
- 객체 지향형 언어들이 다형성을 지원하기 위해 사용하는 컴파일러 수준의 데이터 구조이다.
- 본질적으로 메서드들의 메모리 주소로 구성된 배열이다.
- static / private / final로 선언되지 않은 메서드, 즉 virtual 메서드들을 대상으로 한다.
- 다른 말로 하면 “상속 가능한 메서드”들이다.
- 모든 객체들은 자신이 생성된 클래스의 vtable을 가리키는 포인터를 지닌다.
오버라이딩 된 인스턴스 메서드의 실행 과정
- 컴파일 시간의 과정
- 컴파일러가 변수를 선언한 타입의 클래스에 호출하려는 메서드가 있음을 확증한다. (메서드에 대한 기본적인 유효성 검사)
- “메서드가 존재한다”는 것만 확인하지 “그 메서드의 구현체가 무엇인가”는 모름
- 컴파일러가 vtable 내에 있는 메서드의 인덱스를 확인한다.
- vtable에 있는 메서드의 인덱스는 해당 메서드를 오버라이딩하는 모든 자식 클래스들의 vtable 내에서도 동일하기 때문에 중요하다.
- 컴파일러가 변수를 선언한 타입의 클래스에 호출하려는 메서드가 있음을 확증한다. (메서드에 대한 기본적인 유효성 검사)
- 런타임의 과정
- JVM이 heap 영역에 있는 객체를 확인한 뒤, 객체의 메타 데이터에 있는 해당 객체의 클래스가 사용하는 vtable을 조회한다.
- vtable에서 컴파일러가 찾은 인덱스에 해당하는 메서드 구현체를 찾은 뒤 실행한다.
오버라이딩되지 않은 인스턴스 메서드의 실행 과정
- 컴파일 시간의 과정: 오버라이딩 된 인스턴스 메서드의 실행 과정과 동일하다.
- 런타임의 과정
- JVM이 heap 영역에 있는 실제 객체를 확인한다.
- JVM이 변수를 선언한 클래스의 vtable을 조회한다. (실제 객체를 생성한 클래스를 추적하는 과정이 없다)
- vtable에서 컴파일러가 찾은 인덱스에 해당하는 메서드 구현체를 찾은 뒤 실행한다.
왜 인스턴스 메서드는 오버라이딩 될 수 있는가
- 인스턴스 메서드는 실행될 때 실제로 사용되는 객체의 타입을 확인한다.
- 실행하는 과정에서 이뤄지는 vtable의 조회 과정을 통해 런타임 다형성인 메서드 오버라이딩이 가능해진다.
코드로 보는 예시
-
Animal과 Dog 클래스
class Animal { public static void staticMethod() { /* in Method Area: Animal.staticMethod@0x100 */ } public void instanceMethod() { /* in Animal's vtable */ } } class Dog extends Animal { public static void staticMethod() { /* in Method Area: Dog.staticMethod@0x200 */ } @Override public void instanceMethod() { /* in Dog's vtable */ } } -
메모리 위치
Method Area: ┌──────────────────────┐ │ Animal Class Data │ │ ├─ staticMethod: 0x100 ← Fixed address! │ └─ vtable[0]: Animal.instanceMethod@0x300 ├──────────────────────┤ │ Dog Class Data │ │ ├─ staticMethod: 0x200 ← Fixed address! │ └─ vtable[0]: Dog.instanceMethod@0x400 ← Overridden! └──────────────────────┘ Heap: ┌──────────────────────┐ │ Animal animalRef = new Dog() │ │ ├─ object header │ │ ├─ _vptr → Dog.vtable ← Points to Dog's vtable! │ └─ instance vars │ └──────────────────────┘