我不认为“子对象”是考虑这个问题的好方法。
你正在制作一个物体。像所有对象一样,它是某个特定类的实例,(毕竟,new SomeInterface() 不会编译)并且像(几乎)所有对象一样,它是因为 some code 在某个地方(不必须是你的代码,当然)在某处运行 java 表达式 new SomeSpecificClass(args);。
我们可以说它是一个“子对象”,因为SomeSpecificClass 是某个其他类的子类。
但这没什么用。这意味着创建一个新的“非子”对象的唯一方法是编写new Object(); - 毕竟,除了java.lang.Object 之外的所有类都是子类:如果你写public class Foo {},java 会准确地解释它毕竟,就好像你写了public class Foo extends java.lang.Object {}一样。
因此,除非无用*无关紧要,所有对象都是子对象,因此作为术语“子对象”,我不会使用它。
这也意味着 ALL 对象的创建要经过这个“好的,以什么顺序以及构造函数如何工作”的歌舞例程。
脱糖可能最容易解释它的工作原理。如果您选择省略它们,Javac(编译器)会注入一些东西,因为在类文件/JVM 级别,很多感觉是可选的东西(例如构造函数、超级调用或扩展子句)不是* *.
糖 #1 - 扩展子句
已经涵盖:如果你的类 def 中没有 extends 子句,javac 会为你注入 extends java.lang.Object。
糖 #2 - 构造函数中没有超级调用
构造函数必须在其第一行调用某个特定的超级构造函数,或,它必须在其第一行调用来自同一类的其他构造函数 (this(arg1, arg2);)。如果你不这样做,java 会为你注入它:
public MyClass(String arg) { this.arg = arg; }
// is treated as:
public MyClass(String arg) {
super();
this.arg = arg;
}
如果您的父类没有可用的零参数构造函数,则尤其包括编译器错误。
糖 #3:没有构造函数
如果你编写一个没有构造函数的类,那么 java 会为你做一个:
public YourClass() {}
它将是公开的,没有参数,也没有代码。但是,根据糖 #2 规则,这会进一步扩展为:
public YourClass() {super();}
字段初始化和代码块被重写为单个块。
构造函数并不是创建新对象时唯一运行的东西。想象一下这段代码:
public class Example {
private final long now = System.currentTimeMillis();
}
此代码有效;你可以编译它。您可以创建Example 的新实例,now 字段将保持调用new Example() 时的时间。那么这是如何工作的呢?这感觉像构造函数代码很多,不是吗?
嗯,它是这样工作的:从上到下浏览源文件,找到你能找到的每一个非静态初始化代码:
public class Example {
int x = foo(); // all non-constant initial values count
{
x = 10;
// this bizarre constructor is legal java, and also
// counts as an initializer.
}
}
然后按照您看到它们的顺序将所有这些移到类获得的唯一初始化程序中。
订购
因此,通过糖规则,我们减少了所有类以遵守以下规则:
- 所有类都有一个父类。
- 所有类都至少有 1 个构造函数。
- 所有构造函数都调用另一个构造函数或父构造函数。
- 有一个“初始化程序”代码块。
现在唯一的问题是,事情的执行顺序是什么?
答案是疯狂。抓住你的帽子。
这是顺序:
首先,将整个“构造”的所有字段设置为 0/false/null(当然,该构造涉及从 Child 一直到 Object 的每个字段)。
从在Child 上调用的实际构造函数开始。 直接运行,这意味着从第一行开始,必然是this() 或super() 调用。
计算整行,特别是计算所有作为参数传递的表达式。即使这些本身就是对其他方法的调用。但是,javac 会做一些小的努力来阻止您访问您的字段(因为这些字段全部未初始化!我还没有提到初始化程序!!)。
是的,真的。这意味着:
public class Example {
private final long x = System.currentTimeMillis();
public Example() {
super(x); // x will be .... 0
// how's that for 'final'?
}
}
这将最终调用您的其他构造函数的第一行(它本身也是 this() 或 super() 调用)。要么我们永远无法离开这个森林,堆栈溢出错误中止了我们创建这个对象的尝试(因为我们有一个构造函数循环不断地相互调用),或者,在某些时候,我们遇到了super() 调用,这意味着我们现在去我们的家长班,再次重复整个歌舞套路。
我们继续前进,一直到 java.lang.Object,通过硬编码,它根本没有 this() 或 super() 调用,并且是唯一的调用。
然后,我们先停下来。现在的工作是运行 j.l.Object 的构造函数中的其余代码,但首先,我们运行 Object 的初始化程序。
然后,对象的构造函数运行其中的所有其余代码。
然后,Parent 的初始化程序运行。然后是使用的父构造函数的其余部分。并且如果 parent 一直在横向移动(this() 在其构造函数中调用),则它们都在方法调用中以正常的相反顺序运行。
我们最终以 Child 结束;它的初始化程序运行,然后构造函数按顺序运行,最后我们完成了。
给我看!
class Parent {
/* some utility methods so we can run this stuff */
static int print(String in) {
System.out.println("@" + in);
return 0;
// we use this to observe the flow.
// as this is a static method it has no bearing on constructor calls.
}
public static void main(String[] args) {
new Child(1, 2);
}
/* actual relevant code follows */
Parent(int arg) {
print("Parent-ctr");
print("the result of getNow: " + getNow());
}
int y = print("Parent-init");
long getNow() { return 10; }
}
class Child extends Parent {
Child(int a, int b) {
this(print("Child-ctr1-firstline"));
print("Child-ctr1-secondline");
}
int x = print("Child-init");
Child(int a) {
super(print("Child-ctr2-firstline"));
print("Child-ctr2-secondline");
}
final long now = System.currentTimeMillis();
@Override long getNow() { return now; }
}
现在是伟大的益智游戏。应用上述规则并尝试弄清楚这将打印什么。
@Child-ctr1-firstline
@Child-ctr2-firstline
@父初始化
@Parent-ctr
@getNow 的结果:0
@Child-init
@Child-ctr2-secondline
@Child-ctr1-secondline
- 构造函数执行顺序是有效的:第一行在前,其余的在最后。
- final 字段为 0,尽管它似乎永远不应该为 0。
- 你总是最终运行你父母的构造函数。
--
*) 您可以将它们用于锁或哨兵指针值。假设“几乎没用”。
**) 你可以破解一个类文件,以便它描述一个没有父类的类(甚至不是 j.l.Object);这就是java.lang.Object 的类文件的工作方式。但是你不能让javac 做这个,你必须一起破解它,这样的事情会很疯狂,没有真正有用的目的。