我们先看一下 Java 的帮助文档对于 Object 的描述:
Class Object is the root of the class hierarchy. Every class has Object as a superclass. All objects, including arrays, implement the methods of this class.
Object 类是类层次结构的根类. 每个类都使用 Object 作为超类. 所有对象 (包括数组) 都实现这个类的方法.
注意: 描述是 Every class(所有的类). 有这句话可以猜想一下, 抽象类是继承了 Object.
对于继承, 我们知道 C++ 语言支持多继承, Java 语言只支持单继承. 那么 Java 语言为什么不支持多继承呢? 我们先看一看多继承中最典型的钻石问题(菱型缺陷), 如下图(图片来源于 https://www.cnblogs.com/sddai/p/6516668.html):
其中 A,B,C,D 是四个类, B 继承 A,C 也继承 A,D 又同时继承了 B 和 C. 如果 B 和 C 都有 test 方法, 看如下代码
- D d = new D();
- d.test();
第一句中当 new D(); 的时候会不会调用两次 A 的构造函数?
第二句中调用的是 B 里面的 test 方法还是 C 里面的 test 方法?
为了避免以上的问题, Java 采用了折衷的方法, 只允许单继承, 但可以实现多个接口. 所以我们可以以 java 语言是单继承这个前提, 来推导一下接口和抽象类是否继承 Object. 如下:
对于抽象类而言: 一个普通类肯定是继承了 Object, 如果一个抽象类再继承这个普通类, 这个时候抽象类肯定也是继承了 Object 的. 而对于没有继承任何类的抽象类而言, 如果它没有继承 Object, 那么当一个普通类继承这个抽象类的时候, 这个普通类也肯定没有继承 Object, 悖论. 所以抽象类肯定是继承了 Object.
对于接口而言呢: 如果接口继承了 Object 类. 那么当一个类实现多个接口的时候, 那不就相当于继承了多遍 Object? 又变成了多继承? 这个问题先放一放.
到目前为止, 以上的言论还都处于猜想阶段, 现在我们就来深入一点, 找一下确凿的 "证据". 我们都知道 Java 源文件会先编译成 class 文件, 然后再被 jvm 执行. 那么如果我们能够知道父类在 class 文件中是怎么存储的, 然后看一下接口编译成的 class 文件, 不就知道接口是否继承 Object 了吗? 以下内容涉及字节码, 来源于《深入理解 Java 虚拟机》第二版的 6.3 节(核心是 6.3.4 节).
Java 文件编译而成的 class 文件是二进制文件, 没有任何分隔符, 所以无论是顺序还是数量都是被严格规定的.
class 文件开始的 4 个字节是 CAFEBABE, 表示这是一个能被虚拟机接受的 class 文件; 紧跟着 4 个字节表示 class 文件的版本号; 紧接着后面是常量池, 前两个字节是常量中的常量数量, 后面是常量池的内容; 常量池后面的 2 个字节代表访问标志, 比如是否 public, 接口, 注解, 枚举等; 紧接着 2 个字节代表类的索引; 类索引后面两个字节代表父类索引; 父类索引后面是接口索引集合, 前两个字节代表集合的大小, 后面跟具体的接口索引. 如下图所示:
注:
1. 由于常量池中常量的数量是用两个字节存储的, 也就是说单个 class 文件中的常量池中常量的个数不会超过 2 个字节.
2. "索引" 是指在常量池中的第几项常量(从 1 开始), 占两个字节(和常量池中的常量数量占用空间一样). 比如类索引为 5, 表示类的全类名在常量池中的第 5 个常量处.
3. 父类索引只使用了两个字节, 这也说明了在 class 文件中父类最多存在一个(除了 Object 类的父类索引为 0 外, 其他都有值).
可见, 我们只需找出常量池的结尾, 即可找出父类索引, 从而确定一个类的父类是谁? jdk 中有一个 javap 的命令(javap -v xxx). 可以查看一个类的常量池, 从而查看常量池中最后一个常量的值, 然后再根据 class 文件找出对应的值, 即可确定常量池的末尾.
例: TestJ1.java 如下:
- public class TestJ1 {
- }
使用 UltraEdit 打开 TestJ1.class 文件, 使用命令行输入命令:"javap -v TestJ1". 如下图所示:
由图中可知常量池最后一个常量为 "java/lang/Object" (Constant pool 为常量池), 在 class 文件中对应的位置为 0x0069~0X0078. 所以访问标志的位置为 0x0079~0x007a, 值为: 0x0021; 同理类索引的值为: 0x0002; 父类索引值为: 0x0003; 接口索引集合长度为: 0x0000(该类没有实现接口).
类索引为: 0x0002, 换算成 10 进制是 2, 找常量池中为 #02(#02 表示常量池中的第二项常量)的值, 为 #11, 再找 #11, 为 Test1(此处为类的全类名. 由于 TestJ1 类没有包, 所以是类名. 格式如 java/lang/Object). 同理父类为: 0x0003 --> #3 --> #12 --> java/lang/Object. 所以 TestJ1 继承 Object 类.
接下来我们写一个最简单的接口如下:
- public interface InterSuper1 {
- }
class 文件和常量池如下:
由上图可以看出在 class 文件中 InterSuper1 接口的父类标识符指向的也是 Object 类. 不止如此, 如果一个接口有父接口. 那么此接口的父类标识符指向的也是 Object 类. 可以说对于 class 文件而言所有接口的父类都是 Object(同理也可证明 Object 类也是所有抽象类的父类).
现在我们再回过头看一看上面遗留的问题: 如果接口继承了 Object 类. 那么当一个类实现多个接口的时候, 那不就相当于继承了多遍 Object? 又变成了多继承? 首先不会继承多遍 Object, 因为在 class 文件而言, 只能存储一个父类. 这个类还是直接或者间接的继承 Object. 也是单继承, 由于接口不能实例化, 所以也不会出现上面的菱形缺陷.
至于网上流传的 Java 的标准 --"Java Language Specification" 中的 9.2 节, 如下(来源于 http://www.cnblogs.com/softnovo/articles/4546418.html):
我的理解是: 首先这段话没有明确说明接口不继承 Object; 其次它是出自于 java 语言规范中, 所以它的目的是让人们更加容易使用 Java, 所以故意省略了这个细节也是有可能的; 再者如果接口继承 Object, 上面的观点也能说得通.
还有一个是如下代码, 为什么不输出 Object 中的方法? 这个我也无法解释.
- public interface SuperInter {
- public void test();
- public String getString();
- }
- public static void main(String[] args) {
- Method[] methods = SuperInter.class.getMethods();
- for (Method method : methods) {
- System.out.println(method.getName());
- }
- }
参考资料:
《深入理解 Java 虚拟机》第二版
- https://www.cnblogs.com/sddai/p/6516668.html
- http://www.cnblogs.com/softnovo/articles/4546418.html
- https://blog.csdn.net/xidiancoder/article/details/78011148
- https://blog.csdn.net/tengfeixiaoao/article/details/79586949
来源: https://www.cnblogs.com/wind-snow/p/10003772.html