在Java中,子对象是如何构造的?

huangapple go评论57阅读模式
英文:

How is a child object constructed in Java?

问题

在Java中,子对象是如何构造的?
我刚开始学习继承,以下几点对我来说不太清楚:

子对象仅取决于子类的构造函数吗?还是它还取决于父类的构造函数?我需要一些关于这一点的详细信息。

另外,在子类构造函数中super()是否总是默认被调用?

关于这个主题的任何其他信息都会被赞赏。

英文:

In java, how is a child object constructed?
I just started inheritance and these few points are not very clear to me:

Does the child object depend only on the child class' constructor, or does it also depend on the parent's constructor? I need some details about that point.

Also, is super() always called by default in a child constructor?

Any other information regarding this topic is appreciated.

答案1

得分: 2

好的,以下是翻译好的部分:

我认为 "一个子对象" 不是一个好的思考方式。

你正在创建一个对象。与所有对象一样,它是某个特定类的实例,毕竟 new SomeInterface() 是不能编译通过的,而且像几乎所有对象一样,它是因为某个地方(当然不一定是你的代码)运行了 Java 表达式 new SomeSpecificClass(args);

我们可以说它是一个 "子对象",因为 SomeSpecificClass 是另一个类的子类。

但这相当无用。这意味着唯一创建一个新的 "非子对象" 的方法是编写 new Object(); - 毕竟,除了 java.lang.Object 之外的所有类都是子类:如果你写 public class Foo {},Java 会将其解释为完全相同,就像你写了 public class Foo extends java.lang.Object {} 一样,毕竟。

因此,除了无用的*无关紧要的内容,所有对象都是子对象,因此作为一个术语,"子对象",我不会使用它。

这也意味着_所有_对象的创建都要经过这个 "构造函数的执行顺序如何,构造函数的工作原理是什么" 的歌舞表演。

它的工作原理可能通过展开全部来解释。Javac(编译器)会在您选择省略它们时注入一些内容,因为许多看起来是可选的东西(例如构造函数、super 调用或扩展子句)在类文件/JVM 级别上并不是**可选的。

糖 #1 - 扩展子句

已经涵盖:如果在类定义中没有 extends 子句,javac 会为您注入 extends java.lang.Object

糖 #2 - 构造函数中没有 super 调用

构造函数要么在其第一行调用某个特定的父级构造函数,要么在其第一行调用同一类中的其他构造函数(this(arg1, arg2);)。如果不这样做,Java 会为您注入它:

public MyClass(String arg) { this.arg = arg; }
// 被处理为:
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(); // 所有非常量初始值都计数
    {
        x = 10;
        // 这种奇怪的构造函数是合法的 Java,也算作初始化器。
    }
}

然后将所有这些内容移到类获取的唯一初始化器中,按照您看到的顺序。

顺序

因此,通过糖规则,我们将所有类简化为遵循以下规则:

  1. 所有类都有一个父类。
  2. 所有类都至少有一个构造函数。
  3. 所有构造函数要么调用另一个构造函数,要么调用父类的构造函数。
  4. 存在一个“初始化器”代码块。

现在唯一的问题是,以什么顺序执行这些内容?

答案是__疯狂的__。系好安全带。

这是顺序:

首先,将整个“构造”(该构造涵盖从 Child 到 Object 的所有字段,当然包括)的所有字段设置为 0/false/null。

从在 Child 上调用的实际构造函数开始。直接运行它,这意味着从第一行开始,该行__必然__是一个 this()super() 调用。

评估整行,特别是评估作为参数传递的所有表达式。即使那些本身是其他方法的调用。但是,javac 将会尽一些努力来阻止您访问字段(因为这些字段都未初始化!我还没有提到初始化器!)。

是的,真的。这意味着这个:

public class Example {
    private final long x = System.currentTimeMillis();

    public Example() {
        super(x); // x 将是 .... 0
        // 那么 "final" 是怎么样的?
    }
}

这将最终调用您的另一个构造函数的第一行(该构造函数本身也是 this()super() 调用)。要么我们永远无法摆脱这个循环,构造函数的循环调用无休止地互相调用,从而中止我们创建此对象的尝试(因为我们有一

英文:

I don't think "A child object" is a good way to think about this.

You're making an object. Like all objects, it is an instance of some specific class, (After all, new SomeInterface() does not compile) and like (almost) all objects, it is made because some code someplace (doesn't have to be your code, of course) ran the java expression new SomeSpecificClass(args); somewhere.

We could say it is a 'child object' because SomeSpecificClass is a child class of some other class.

But that's rather useless. That means the only way to ever make a new 'non-child' object would be to write new Object(); - after all, all classes except java.lang.Object are a child class: If you write public class Foo {}, java will interpret that exactly the same as if you had written public class Foo extends java.lang.Object {}, after all.

So, barring useless* irrelevancies, all objects are child objects, and therefore as a term, 'child object', I'd not use that.

That also means that ALL object creation goes through this 'okay and in what order and how do the constructors work' song and dance routine.

How it works is probably most easily explained by desugaring it all. Javac (the compiler) injects things if you choose to omit them, because a lot of things that feel optional (such as a constructor, a super call, or an extend clause), at the class file / JVM level, aren't**.

Sugar #1 - extends clause

Already covered: if you have no extends clause on your class def, javac injects extends java.lang.Object for you.

Sugar #2 - no super call in constructor

A constructor must either call some specific super constructor on its very first line, or, it it must call some other constructor from the same class on its very first line (this(arg1, arg2);). If you don't, java will inject it for you:

public MyClass(String arg) { this.arg = arg; }
// is treated as:
public MyClass(String arg) {
    super();
    this.arg = arg;
}

Notably including a compiler error if your parent class has no zero-arg constructor available.

Sugar #3: No constructor

If you write a class that has no constructor, then java makes one for you:

public YourClass() {}

It will be public, it will have no args, and it will have no code on it. However, as per sugar #2 rule, this then gets expanded even further, to:

public YourClass() {super();}

Field inits and code blocks get rewritten to a single block.

The constructor isn't the only thing that runs when you make new objects. Imagine this code:

public class Example {
    private final long now = System.currentTimeMillis();
}

This code works; you can compile it. You can make new instances of Example, and the now field will hold the time as it was when you invoked new Example(). So how does that work? That feels a lot like constructor code, no?

Well, this is how it works: Go through the source file top to bottom and find every non-static initializing code you can find:

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.
    }
}

and then move all that over to the one and only initializer that classes get, in the order you saw them.

Ordering

So, via sugar rules we have reduced ALL classes to adhere to the following rules:

  1. ALL classes have a parent class.
  2. ALL classes have at least 1 constructor.
  3. ALL constructors invoke either another constructor or a constructor from parent.
  4. There is one 'initializer' code block.

Now the only question is, in what order are things executed?

The answer is crazy. Hold on to your hats.

This is the order:

First, set all fields to 0/false/null of the entire 'construct' (the construct involves every field from Child all the way down to Object, of course).

Start with the actual constructor invoked on Child. Run it directly, which means, start with the first line, which neccessarily is either a this() or a super() invocation.

Evaluate the entire line, notably, evaluate all expressions passed as arguments. Even if those are themselves invocations of other methods. But, javac will do some minor effort to try to prevent you from accessing your fields (because those are all uninitialized! I haven't mentioned initializers yet!!).

Yeah, really. This means this:

public class Example {
    private final long x = System.currentTimeMillis();

    public Example() {
        super(x); // x will be .... 0
        // how's that for 'final'?
    }
}

This will either end up invoking the first line of some other constructor of yours (which is itself also either a this() or a super() call). Either we never get out of this forest and a stack overflow error aborts our attempt to create this object (because we have a loop of constructors that endlessly invoke each other), or, at some point, we run into a super() call, which means we now go to our parent class and repeat this entire song and dance routine once more.

We keep going, all the way to java.lang.Object, which by way of hardcoding, has no this() or super() call at all and is the only one that does.

Then, we stop first. Now the job is to run the rest of the code in the constructor of j.l.Object, but first, we run Object's initializer.

Then, object's constructor runs all the rest of the code in it.

Then, Parent's initializer is run. And then the rest of the parent constructor that was used. and if parent has been shifting sideways (this() invokes in its constructors), those are all run in reverse order as normal in method invocations.

We finally end up at Child; its initializer runs, then the constructor(s) run in order, and finally we're done.

Show me!

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; }
}

and now for the great puzzler. Apply the above rules and try to figure out what this will print.

>! @Child-ctr1-firstline<br>
>! @Child-ctr2-firstline<br>
>! @Parent-init<br>
>! @Parent-ctr<br>
>! @the result of getNow: 0<br>
>! @Child-init<br>
>! @Child-ctr2-secondline<br>
>! @Child-ctr1-secondline<br>

  • Constructor execution ordering is effectively: the first line goes first, and the rest goes last.
  • a final field was 0, even though it seems like it should never be 0.
  • You always end up running your parent's constructor.

--

*) You can use them for locks or sentinel pointer values. Let's say 'mostly useless'.

**) You can hack a class file so that it describes a class without a parent class (not even j.l.Object); that's how java.lang.Object's class file works. But you can't make javac make this, you'd have to hack it together, and such a thing would be quite crazy and has no real useful purpose.

答案2

得分: 1

  • 在继承中,子对象的构建至少依赖于一个父类构造函数。
  • 调用 super() 方法并非强制性的。默认情况下,Java 会调用父类构造函数,无需参数,除非你指定了自定义构造函数。
  • 这里是一个示例:

Mother(母类)

public class Mother {

    int a;

    public Mother() {
        System.out.println("Mother without argument");
        a = 1;
    }

    public Mother(int a) {
        System.out.println("Mother with argument");
        this.a = a;
    }

}

Child(子类)

public class Child extends Mother {

    public Child() {
        System.out.println("Child without argument");
    }

    public Child(int a) {
        super(a);
        System.out.println("Child with argument");
    }

}

如果你执行以下操作:

Child c1 = new Child();

你会得到:

Mother without argument
Child without argument

如果你执行以下操作:

Child c1 = new Child(a);

你会得到:

Mother with argument
Child with argument

但是,如果你将第二个子类构造函数更改为删除 super(arg) 调用,父类的无参构造函数将被调用:

public Child(int a) {
    // super(a);
    System.out.println("Child with argument");
}

你会得到:

Mother without argument
Child with argument
英文:
  • In inheritance, the construction of a child object depends on at least one parent constructor.
  • Calling the super () method is not mandatory. By default, Java will call the parent constructor without argument except if you precise a custom constructor.
  • Here an example

Mother

public class Mother {

int a;

public Mother() {
    System.out.println(&quot;Mother without argument&quot;);
    a = 1;
}

public Mother(int a) {
    System.out.println(&quot;Mother with argument&quot;);
    this.a = a;
}

}

child

 public class Child extends Mother {

public Child() {
    System.out.println(&quot;Child without argument&quot;);
}

public Child(int a) {
    super(a);
    System.out.println(&quot;Child with argument&quot;);
}

}

If you do this :

Child c1 = new Child();

you will get :

Mother without argument
Child without argument

If you do this :

Child c1 = new Child(a);

You will get :

Mother with argument
Child with argument

But if you change the second child constructor to and remove the super(arg) the parent constructor without argument will be called :

    public Child(int a) {
    //        super(a);
    System.out.println(&quot;Child with argument&quot;);
    }

You will get :

Mother without argument
Child with argument

huangapple
  • 本文由 发表于 2020年9月15日 20:24:23
  • 转载请务必保留本文链接:https://go.coder-hub.com/63901774.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定