心安

Java对象创建过程细节探秘

字数统计: 2.8k阅读时长: 10 min
2019/04/28 Share

一、概述

在看阿里中间件团队的一篇关于”Java对象初始化顺序“的文章中,博文中有一段代码是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class B {
public B() {
System.out.println(((A)this).a);
}
}

public class A extends B {

public int a = 100;

public A() {
super();
System.out.println(a);
a = 200;
}

public static void main(String[] args) {
System.out.println(new A().a);
}
}

当时就疑惑了,在B类的构造函数里面this指代的不应该是当前吗?那就应该是B的实例,强转成A类型,这个可以吗?(小弟才疏学浅,可能各位读者大佬们不会有这么脑残的想法,请忽略。)带着这个疑问,就开始了自己的Google之旅。ps:这里我们先抛开原本博客提出的问题。

本文首发于心安-XinAnzzZ的个人博客,欢迎访问小站~

二、问题一

我的第一个疑惑就是,在B类的构造函数中,this指代的是谁?是谁的实例?带着这个疑问,我找到了另外一个问题,代码如下:

1
2
3
4
5
6
7
8
9
10
11
class Super {
Super() {
System.out.println(this.getClass() == super.getClass());
}
}

class Sub extends Super {
public static void main(String[] args) {
System.out.println(new Sub());
}
}

看到这个问题,第一反应答案当然是false,这个问题还有什么悬念吗?super是父类,this是子类,类型怎么可能相等?但是第二反应,不对,如果这么简单,还会有人提出来吗?于是我就带着疑问运行了一下,竟然输出了true!!!WTF???

三、问题二

这就让我无法理解了,上面的代码怎么输出了true呢?类型相等,那么他们是什么类型呢?会是Super类型吗?还是java.lang.Objec类型?那就赶紧打印一下看看吧,于是修改了父类代码,如下:

1
2
3
4
5
6
7
class Super {
Super() {
System.out.println(this.getClass() == super.getClass());
System.out.println(super.getClass());
System.out.println(this.getClass());
}
}

输出结果如下:

1
2
3
4
true
class com.yuhangma.xinan.sample.Sub
class com.yuhangma.xinan.sample.Sub
com.yuhangma.xinan.sample.Sub@13221655

结果又让我意外了,怎么是Sub类型啊,万万没想到啊!!!好吧,继续疑惑。。

四、问题三

它们俩既然是同类型,那么他们是相同的对象吗?代码说明一切,打印一下对象,看一下哈希码是不是一样就知道了。修改代码如下:

1
2
3
4
5
6
7
class Super {
Super() {
System.out.println(this.getClass() == super.getClass());
System.out.println(super.toString());
System.out.println(this.toString());
}
}

输出结果如下:

1
2
3
4
true
com.yuhangma.xinan.concurrent.sample.Sub@13221655
com.yuhangma.xinan.concurrent.sample.Sub@13221655
com.yuhangma.xinan.concurrent.sample.Sub@13221655

结果说明,它们不仅是同一个对象,而且就是我们在子类中实例化的那个对象,这个结果也就能解释上面的所有疑惑了。

首先,第一个疑问,this本来就是子类对象,所以强转子类是完全没问题的。

第二个疑问,thissuper都是同一个对象,类型当然相等。

But!!!Why?为什么在父类的构造函数中,superthis指代的都是子类对象?

那么又来一个疑问,请看下面代码,如果说this指代的是子类对象,那么子类对象怎么可以访问父类私有成员变量呢?抱歉,可能我这个人有问题,所以我的问题特别特别多。。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Sup {
private int i;

public Sup(int i) {
// 通过上面的结果,我们知道,在子类构造函数通过super()访问父类构造函数的时候,
// 这里的this指代的是子类的对象,那么怎么可以通过子类对象来访问父类的私有成员变量嘞!!
this.i = i;
}
}

class Sub extends Sup {
public Sub() {
super(0);
}
}

关于这个问题,请看逼乎大佬的解答。什么?你懒得看?那行,笔者去把逼乎大佬总结的结论贴过来。

从继承的概念来说,private和final不被继承。Java官方文档上是这么说的。


从内存的角度来说,父类的一切都被继承(从父类构造方法被调用就知道了,因为new一个对象,就会调用构造方法,子类被new的时候就会调用父类的构造方法,所以从内存的角度来说,子类拥有一个完整的父类)。子类对象所引用的内存有父类变量的一份拷贝


作者:KobeFan

链接:https://www.zhihu.com/question/51345942/answer/145388196

来源:知乎

著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

再贴一下Oracle官方文档的说明:

Private Members in a Superclass

A subclass does not inherit the private members of its parent class. However, if the superclass has public or protected methods for accessing its private fields, these can also be used by the subclass.


// google翻译:

子类不继承其父类的私有成员。但是,如果超类具有访问其私有字段的公共或受保护方法,则子类也可以使用这些方法。

仁者见仁,智者见智。上面两种说法都有一定的道理,但是我个人的观点是同意第二个看法,因为笔者自己试验了一下,通过debug打断点查看this对象的成员,是包括父类的私有成员变量的,各位看官也可以去debug看一下。

通过上面的实验也就验证了,子类对象初始化的时候访问父类对象,但是并不会产生父类对象,从始至终都只有子类对象。其实反过来想,假如说每次子类的实例化都会产生父类的实例,那么如果一个类的结构层次非常深,那么一次子类实例化可能产生成百上千的对象,并且会产生成千上万的java.lang.Object的实例,Unbelievable!!这不仅对我们内存是一个很大的挑战,还会让我们的程序运行效率变得极低。我想Java的设计师们应该不会做这么脑残的设计的。

五、解惑

在经过一番纠结,一番搜索之后,又找到了逼乎上面的一个问题和一个大佬的回答,终于让我豁然开朗。先甩原文链接,关于一个构造方法中this()和super()的执行顺序?,其中RednaxelaFX大佬的回答彻底的解除了我的疑问。

为了方便各位看官的阅读,这里把原问题都复制过来。

原问题

1
2
3
4
5
6
7
1.通过this()调用其它构造方法,必须位于本构造方法的第一句
2.构造方法中如果第一行没有显示调用super();,那么Java都会隐式调用super();
作为父类的初始化方法,那这两个在内存中到底谁先执行呢
看看下面这个程序
我在一个构造方法中调用第二个自身的构造方法,第二个自身构造方法中又包含一个super(),
那么想知道在第一个自身构造方法中有没有隐式的super(),
父类对象应该是优先于子类对象在堆内存里面出现的吧?最后究竟创建了几个父类对象

代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
class Person {
private String name;
private String location;

Person(String name) {
this.name = name;
location = "beijing";
}

Person(String name,String location) {
this.name = name;
this.location = location;
}

public String info() {
return "name: " + name + " location: " + location;
}
}


class Student extends Person {
private String school;
Student(String name, String school) {
this(name,"beijing", school);
}
Student(String name, String location ,String school) {
super(name, location);
this.school = school;
}
public String info() {
return super.info() + " school: " + school;
}
}

public class TestTeacher {
public static void main(String[] args) {
Student s1 = new Student("C", "S1");
System.out.println(s1.info());
}
}

这里提问者问了两个问题:

  1. 第一个this()构造函数中有没有隐式的super()来调用父类构造函数?
  2. this()构造函数会访问父类的构造函数,那么也就意味着会创建父类的对象,那么子类对象和父类对象谁先创建?

首先,第一个问题,肯定是没有,因为如果构造函数中用this()调用了重载的其他构造函数,那么就不会再隐式的访问父类构造函数。

第二个问题,我最开始看到的时候也疑惑这个问题,那么我们看一下大神的回答。

大神解惑

大神原文的回答比较长,这里我用自己的话说下大神回答的关键点。不过还是建议各位看官老爷们不要偷懒,去看看原文大神的回答,再看看笔者的拙见。

  1. 首先,this构造函数中会调用其他的构造函数来进行初始化,并且由于调用了其他构造函数,所以不会访问父类的构造函数。但是不管怎么调用,最后调用的构造函数一定不会再调用其他的构造函数,那么它就会隐式的访问父类的构造函数。
  2. 一个子类对象应该包含它本身以及所有基类(父类)声明的全部字段,子类的构造函数只能初始化子类本身的字段,却无法初始化基类包含的那些私有字段,super()的作用就是为了让基类去初始化自己封装的那些字段。

六、总结

什么?你说文章又臭又长?实在懒得看我那么多废话?请直接告诉你我一顿瞎比操作最后的结论是什么?

好嘞,结论就是下面几条了。

  1. 对象的构造函数会隐式的访问父类的构造函数,但是如果这个构造函数已经访问了重载的其他构造函数,那么就不会有隐式的super
  2. 访问父类的构造函数并不意味这创建了父类的实例,子类的实例化只会产生子类的实例。
  3. 在父类中使用this或者super都指代的是子类的对象,但是如果想访问父类的私有成员,只能用this
  4. 访问父类构造函数的意义在于初始化父类的私有成员。子类无法直接访问父类的私有成员,所以通过访问父类构造器来初始化父类的私有成员。
  5. 父类中私有成员也会被继承,但是子类没有直接访问权限(子类访问父类私有成员的时候编译报错信息是”private access”,而不是不存在)。
  6. 成员变量的初始化是在父类构造函数调用完之后的,在此之前,成员变量的值均是默认值。

七、参考

关于一个构造方法中this()和super()的执行顺序?

Java子类到底有没有继承父类的私有字段?

java中子类有没有继承父类的私有变量?

java中,创建子类对象时,父类对象会也被一起创建么?

阿里中间件团队博客-剖析一个java对象初始化顺序问题

原文作者:XinAnzzZ

原文链接:https://www.yuhangma.com/2019/java/2019-04-28-object-initialize/

发表日期:April 28th 2019, 12:00:00 am

更新日期:February 2nd 2021, 4:44:20 pm

版权声明:(转载本站文章请注明作者和出处 心 安 – XinAnzzZ ,请勿用于任何商业用途)

CATALOG
  1. 1. 一、概述
  2. 2. 二、问题一
  3. 3. 三、问题二
  4. 4. 四、问题三
  5. 5. 五、解惑
    1. 5.1. 原问题
    2. 5.2. 大神解惑
  6. 6. 六、总结
  7. 7. 七、参考