이번 글은 위 질문에 대한 검증을 위해 작성하였다. stackoverflow에 위 질문을 검색하면 많은 답변이 나온다.
JLS(Java Language Spec)에서는, 인터페이스가 상위 인터페이스를 상속받지 않고 독립적인 인터페이스라면, 암묵적으로 Object의 public instance method를 인터페이스의 public abstract method로 추가해준다고 한다.
JLS - 9.2 Interface Members
If an interface has no direct superinterfaces, then the interface implicitly declares a public abstract member method m with signature s, return type r, and throws clause t corresponding to each public instance method m with signature s, return type r, and throws clause t declared in Object, unless a method with the same signature, same return type, and a compatible throws clause is explicitly declared by the interface.
그런가 보다하고, 넘어갔었다.
이후 클래스 로더를 공부하는 중 당황해버렸다... JVM Specification에서는 JLS와는 다른 명세가 있었다.
JVM Specification - 5.3.5. Deriving a Class from a class File Representation
[...]
If C has a direct superclass, the symbolic reference from C to its direct superclass is resolved using the algorithm of §5.4.3.1. Note that if C is an interface it must have Object as its direct superclass, which must already have been loaded. Only Object has no direct superclass.
[...]
(C : 클래스 로더의 Loading 대상이 되는 class or interface)
JVM 명세에서는, 인터페이스가 반드시 그것의 직속 상위클래스로 Object를 가지고 있어야 된다고 한다.
각자의 의견을 정리하면 다음과 같다.
- StackOverFlow : 인터페이스는 Object를 상속하지 않습니다.
- Java Lang Spec : 암묵적으로 Object의 public instance method를, 인터페이스에 public abstract method로 생성합니다.
- JVM Spec : 인터페이스는 반드시 Object 클래스를 상위클래스로 갖고 있어야 합니다.
물론 Specification은 말그대로 명세이기 때문에 기술적으로 JLS와 JVM Spec이 일치하지 않을 수 있다는것은 인정한다. 하지만, 각각의 문서에서 말하는 것이 서로 달랐기 때문에, JVM의 구현체에서는 어떻게 동작하는지 내 눈으로 직접 확인하고 싶었다.
검증 1 : 인터페이스 변수에서 Object 클래스의 method를 호출
- 인터페이스 래퍼런스 변수에 null을 assign한 경우
public class Main {
public static void main(String[] args) {
TestInterface testInterface = null;
testInterface.equals(null);
}
}
interface TestInterface {}
결과 : Exception in thread "main" java.lang.NullPointerException at org.example.Main.main(Main.java:6)
인스턴스 메소드는 객체가 존재해야만 호출이 가능하기 때문에, 인스턴스를 생성할 수 없는 인터페이스의 특성상,
Object 클래스의 인스턴스 메소드를 호출할 경우 NullPointerException을 throw할 것이다.
- 인터페이스 래퍼런스 변수에 인터페이스의 구현체를 assign한 경우
public class Main {
public static void main(String[] args) {
TestInterface testInterface = new TestClass();
System.out.println(testInterface.equals(testInterface));
}
}
class TestClass implements TestInterface {}
interface TestInterface {}
결과 : true
정상적으로 Object 클래스의 메소드를 호출하기 위해,
인터페이스 래퍼런스 변수에 인터페이스의 구현체를 할당하고 메소드를 호출하였다.
위 결과를 통해 "Interface는 Object 클래스의 메소드를 호출할 수 있다." 를 증명하였다.
검증 2 : 인터페이스의 바이트코드 확인
바이트코드를 통해 확인하고 싶은 것은 다음과 같다.
- (JSL)Object 클래스의 public instance method가 public abstract method의 형태로 추가되었는가?
- (JVM Spec)인터페이스는 Object 클래스를 상속하는가?
컴파일한 코드 디컴파일한 바이트코드는 다음과 같다.
public interface TestInterface {
void foo();
}
(base) PS C:\Users\A\Desktop\project\DesktopAlarmApplication\alarm_application\build\classes\java\main\org\example> javap -v .\TestInterface.class
Classfile /C:/Users/A/Desktop/project/DesktopAlarmApplication/alarm_application/build/classes/java/main/org/example/TestInterface.class
Last modified 2023. 11. 30; size 139 bytes
MD5 checksum fe7cca9bf6b2d39cde172d193f089da2
Compiled from "TestInterface.java"
public interface org.example.TestInterface
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_INTERFACE, ACC_ABSTRACT
Constant pool:
#1 = Class #7 // org/example/TestInterface
#2 = Class #8 // java/lang/Object
#3 = Utf8 foo
#4 = Utf8 ()V
#5 = Utf8 SourceFile
#6 = Utf8 TestInterface.java
#7 = Utf8 org/example/TestInterface
#8 = Utf8 java/lang/Object
{
public abstract void foo();
descriptor: ()V
flags: ACC_PUBLIC, ACC_ABSTRACT
}
SourceFile: "TestInterface.java"
위 바이트코드에서 "void foo()" 는 "public abstract void foo()" 로 변환되었다. 추상 메소드는 바이트 코드에 표시된다.
하지만, Object의 인스턴스 메소드들이 추상 메소드로 추가되지 않은 것을 보아,
실제 구현체에서는 JLS에서 명세한 것과는 다르게
Object 클래스의 public instance method가 public abstract method의 형태로 추가하지 않음
을 확인하였다.
그렇다면, 인터페이스는 Object 클래스를 암묵적으로(그리고 내부적으로) 상속받은 것일까?
다른 클래스나 인터페이스를 상속받을 경우, 위와 같이 바이트코드에 "extends org.example.TestInterface" 형태로 표시된다. 필자는 이를 근거로 인터페이스는 Object를 상속받지 않는다고 결론 지으려고 했다.
그러나 Object 클래스를 기본적으로 상속받는 클래스의 경우에도 "extends java.lang.Object" 와 같은 표시는 되어있지 않았다. 즉, 애초에 Object 클래스를 상속받는다는 표시는 바이트코드에 드러나지 않는 것이다.
public class TestClass{}
자바에서 모든 클래스가 Object 클래스를 상속받는다는 사실은 너무나 당연한 사실인데, 이 부분에서 막혔다.
그러던 중, 인터페이스와 클래스의 바이트코드 사이에 공통점을 발견하였다.
검증 3 : 인터페이스와 클래스 바이트코드 상 공통점
위 이미지는 두 바이트코드의 Constant pool을 나타낸 것이다. 공통점은 Object 클래스에 대한 네이밍을 Constant pool에 담고 있다는 것이다. 해당 클래스 or 인터페이스에서 Object의 인스턴스 메소드를 호출한다면, Constant pool에 있는 Object 클래스의 네이밍을 활용하여 함수를 호출할 것이다.
그럼 둘 다 Object 클래스를 상속받은 것일까? 그건 아니라고 본다.
클래스의 경우, constant pool의 첫번째 인덱스에 'java/lang/Object."<init>"' 이라는 Object 클래스의 생성자를 갖고 있다
(실제 보유하는 것이 아니라 네이밍을 갖고 있음을 의미함). 상속이라는 관점에서 봤을 때, 상속받은 SubClass는 SuperClass의 생성자를 호출할 수 있어야 한다. 왜냐하면 SubClass의 인스턴스를 생성한다는 것은 SuperClass의 인스턴스 또한 내부에 생성함을 의미하기 때문이다.
이러한 근거로 위 이미지에서 "클래스"는 Object 클래스를 상속받았다고 말할 수 있다.반면에, "인터페이스"는 Object 클래스의 생성자를 호출할 수 없기 때문에, "인터페이스"는 Object 클래스를 상속받지 않았다고 말할 수 있다.
검증 1~3 내용 정리
- StackOverFlow : 인터페이스는 Object를 상속하지 않습니다.
- Java Lang Spec : 암묵적으로 Object의 public instance method를, 인터페이스에 public abstract method로 생성합니다.
- JVM Spec : 인터페이스는 반드시 Object 클래스를 상위클래스로 갖고 있어야 합니다.
글의 초반에, 필자는 질문에 대해 위와 같은 답변을 얻었다고 말하였다. 그럼 위 말들은 전부 잘못된 것일까?
전부 맞는 말이라고 생각한다. 하나씩 설명하겠다.
- StackOverFlow : 인터페이스는 Object를 상속하지 않습니다.
인터페이스는 Object 클래스의 생성자를 호출할 수 없기 때문에, 위 말은 맞는 말이다.
- Java Lang Spec : 암묵적으로 Object의 public instance method를, 인터페이스에 public abstract method로 생성합니다.
문장만 본다면 잘못된 말이라고 볼 수 있다. 왜냐하면, 바이트코드에서 Object 클래스의 인스턴스 메소드들이 추상 메소드의 형태로 추가되어 있지 않았기 때문이다. 하지만, JLS는 명세일 뿐, 구현체가 아니다. 즉, 실제 구현해야할 대상에 대한 설계도 혹은 청사진이라고 판단해야한다.
이러한 관점에서, 인터페이스가 Object 클래스의 메소드를 갖는다는 것의 의미는, 추상 메소드를 활용하여 구현 클래스들이 해당 메소드를 overriding 해야함을 강제하기 위함이 아니다.
인터페이스가 Object 클래스의 메소드를 갖는다는 것의 의미는, 인터페이스 래퍼런스 변수에 구현 클래스의 인스턴스가 할당되었을 때 Object 클래스가 갖고 있는 메소드을 해당 변수에서 호출할 수 있게 해준다는 것이 더 중요한 포인트이다.
public class Main {
public static void main(String[] args) {
TestInterface testInterface = new TestImple();
testInterface.hashCode();
}
}
class TestImple implements TestInterface{}
interface TestInterface{}
위와 같은 케이스에서, 만약 TestInterface 인터페이스가 Object 클래스의 네이밍을 Constant pool에 가지고 있지 않다면, "hashCode()" 와 같은 Object 클래스의 인스턴스 메소드는 사용할 수 없을 것이다.
- JVM Spec : 인터페이스는 반드시 Object 클래스를 상위클래스로 갖고 있어야 합니다.
물론 인터페이스가 Object 클래스를 상속받지 않는다는 것은 자명하다. 그러나, 다시 JVM Spec을 확인해보니 필자가 문서를 잘못이해했다는 생각이 들었다.
JVM Specification - 5.3.5. Deriving a Class from a class File Representation
[...]
If C has a direct superclass, the symbolic reference from C to its direct superclass is resolved using the algorithm of §5.4.3.1. Note that if C is an interface it must have Object as its direct superclass, which must already have been loaded. Only Object has no direct superclass.
[...]
(C : 클래스 로더의 Loading 대상이 되는 class or interface)
파란색 표시된 부분은 처음에 필자가 집착했던 문장이고, 빨간색 표시된 문장은 다시 생각해보았을 때 핵심이라고 생각된 문장이다. 모든 클래스와 인터페이스는 반드시 Object 클래스를 상속받거나 최소한 Object 클래스의 메소드를 호출할 수 있어야 한다. 그렇지 않으면 객체지향에서 말하는 다형성을 구현할 수 없다.
이러한 관점에서 본다면, 비록 인터페이스가 Object 클래스를 상속받지는 않지만, Object 클래스가 인터페이스의 상위클래스라는 말은 맞다고 생각한다.
오늘 아침까지만 해도 Enum 공부하고 있었는데, Enum -> static initialization -> 클래스 로더 -> Object 순서로 글을 읽다보니, 위 내용들을 정리하는 동안 하루가 전부 지나버렸다...
그래도 객체지향이 자바에 어떻게 적용된 것이지 쬐끔은 알게 된 것 같아서 흥미로웠다.
결론
인터페이스는 컴파일 시 바이트코드의 constant pool에 Object 클래스의 네임을 넣음으로써,
Object 클래스의 인스턴스 메소드 호출을 가능하게 하였다.
제가 아직 모르는게 많아서, 어떠한 피드백이라도, 주신다면 감사하겠습니다~! : )
참고자료
- https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-5.html#jvms-5.4.3.1
- https://docs.oracle.com/javase/specs/jls/se7/html/jls-12.html#jls-12.4.1
- https://stackoverflow.com/questions/6056124/do-interfaces-inherit-from-object-class-in-java
- https://geekexplains.blogspot.com/2008/06/do-interfaces-really-inherit-from-class.html
'프로그래밍 언어 > 자바' 카테고리의 다른 글
[Java] 애노테이션 (3) | 2023.12.04 |
---|---|
[Java] 클래스로더 이해하기 (1) | 2023.12.01 |
[Java] ClassFile 포맷 (0) | 2023.11.30 |
[Java] 멀티쓰레드 프로그래밍 (1) | 2023.11.28 |
[Java] 예외 처리 (1) | 2023.11.28 |