首页 新闻 会员 周边

Java:子类没写构造器?编译器偷偷帮你写一个子类的,并在里面调用父类的;Python:子类没写初始化方法?那就不写了,初始化时直接去父类借一个来用

0
[已解决问题] 解决于 2026-07-22 17:43

image_497411134304020
区别一:Python 中 init 根本不是“构造函数”

核心真相:在 Python 中,init 根本不负责“创建”对象
在 Java/C++ 中,构造函数(Constructor)包揽了两件事:

在内存中开辟空间(创建)。
给属性赋初始值(初始化)。
但在 Python 中,这两件事是由两个不同的魔法方法分开完成的:

new(cls):真正的构造函数。负责在内存中开辟空间,把对象生出来。
init(self):初始化方法。不负责生对象,只负责给 new 生出来的对象穿衣服(赋值)。

在 Java 中,构造函数(Constructor)负责两件事:分配内存创建对象 + 初始化属性。 但
当你执行 mg = Manager("FangManager") 时,Python 底层其实做了两步:

第一步(创建):调用 Manager.new(Manager)。因为 Manager 没写 new,它继承自 object,所以底层 C 代码直接分配了一块内存,此时一个纯粹的 Manager 子类对象已经诞生了。
第二步(初始化):准备调用 init 给这个对象赋值。

区别二:Python 的“属性查找机制”(MRO) vs Java 的“编译器补全”
这是导致你产生疑惑的最核心原因。

Java 的做法(静态编译思维):
Java 是静态语言,编译器必须确保每个类都有自己的构造函数。如果子类没写,编译器会在编译阶段强行给你“塞”一个默认的无参构造函数,并在里面写上 super()。 👉 所以在 Java 中,你永远是在调用子类(被编译器补全的)构造函数。

Python 的做法(动态查找思维):
Python 是动态语言,没有“编译器强行补全”这种机制。Python 的核心哲学是 “消息传递与属性查找”(MRO,方法解析顺序)。 当 Python 在第二步准备调用 init 时,它的逻辑非常简单粗暴:

去 Manager 类里面找有没有 init? -> 没找到。
顺着继承链,去父类 Employee 里面找有没有 init? -> 找到了!
直接执行 Employee.init,并把刚才创建好的 Manager 对象作为 self 传进去。
👉 所以在 Python 中,如果子类没写 init,Python 不会帮你生成一个,而是*直接顺着继承链往上找,找到父类的 init 就直接用了。

*Tesla*的主页 *Tesla* | 老鸟四级 | 园豆:2062
提问于:2026-07-22 17:33
< >
分享
最佳答案
0

拆解 mg = Manager("FangManager") 的真实时间线
当你写下这行代码时,Python 底层严格按以下顺序执行:

第一步:创建对象(真正的“创建”发生在这里)
Python 首先调用 Manager.new(Manager)。 因为 Manager 没写 new,它继承了 object 的 new。底层的 C 代码在内存中开辟了一块空间。 👉 注意:就在这一刻,一个纯粹的 Manager(子类)对象已经在内存中诞生了!“创建”动作已经结束。

第二步:初始化对象(你看到的“找父类 init”发生在这里)
对象生出来后,Python 准备给它赋值。它去 Manager 里找 init,没找到,于是顺着继承链找到了 Employee.init。 然后,Python 把第一步创建好的那个 Manager 对象,作为 self 参数,传给了 Employee.init。 👉 注意:父类的 init 只是在给这个已经存在的子类对象“贴标签(赋值)”,它并没有参与“创建”这个对象的过程。

在 Python 中,所谓的“贴标签”,用专业的技术术语来说,就是绑定实例属性(Instance Attributes)。
class Employee:
def init(self, name, job=None, pay=0):
self.name = name # 标签 1
self.job = job # 标签 2
self.pay = pay # 标签 3
这里的 name、job、pay 就是所谓的“标签”。 当父类的 init 执行时,它就是在给传入的 self(此时是子类 Manager 对象)贴上这三个数据标签。name、job、pay 这三个标签,结结实实地贴在了 mg 这个子类对象的 dict 里。 父类的 init 只是提供了一个 “贴标签的动作指南”,它并没有把标签贴在一个所谓的“父类对象”上

*Tesla* | 老鸟四级 |园豆:2062 | 2026-07-22 17:36

java在执行 new 子类() 的整个过程中,内存里从头到尾只产生了一个对象(子类对象),绝对没有额外产生一个独立的“父类对象”。super() 调用父类构造,是先在内存里造了一个“父类对象”,然后再造一个“子类对象”,最后把它们拼起来。这是完全错误的。一、 内存视角:到底发生了什么?
假设父类 Parent 有个属性 name,子类 Child 有个属性 age。 当你执行 Child c = new Child(); 时,JVM 的底层动作如下:

分配唯一内存:JVM 在堆内存中开辟了一块内存空间。这块空间里既有继承来的 name,也有子类自己的 age。这就是那个唯一的子类对象。
调用子类构造:进入子类的构造函数。
调用父类构造(super()):跳转到父类的构造函数。
关键点来了:此时父类构造函数里的代码(比如 this.name = "张三"),是在给第1步开辟的那块唯一内存空间里的 name 字段赋值。
它没有在堆内存里再开辟一块新空间去造一个独立的 Parent 对象。
回到子类构造:给那块内存空间里的 age 字段赋值。
完成:把这块内存的地址返回给变量 c。
结论:父类构造函数被调用,只是为了把子类对象中 “属于父类的那部分基因(属性)” 给初始化好,它操作的始终是那同一个子类对象。
class Parent {
public Parent() {
// 在父类构造函数中,打印 this 的内存地址和实际类型
System.out.println("父类构造中 - this的内存地址: " + this);
System.out.println("父类构造中 - this的实际类型: " + this.getClass().getName());
}
}

class Child extends Parent {
public Child() {
super(); // 调用父类构造
// 在子类构造函数中,打印 this 的内存地址和实际类型
System.out.println("子类构造中 - this的内存地址: " + this);
System.out.println("子类构造中 - this的实际类型: " + this.getClass().getName());
}
}

public class Test {
public static void main(String[] args) {
System.out.println("开始创建对象...");
Child c = new Child();
}
}
开始创建对象...
父类构造中 - this的内存地址: Child@1b6d3586
父类构造中 - this的实际类型: Child <--- 注意这里!
子类构造中 - this的内存地址: Child@1b6d3586
子类构造中 - this的实际类型: Child
铁证解析:

内存地址相同:父类构造和子类构造中的 this 地址完全一样(Child@1b6d3586),说明从头到尾只有这一个对象。
实际类型是 Child:在父类构造函数执行时,this.getClass() 打印出来的居然是 Child!这证明父类构造函数在运行时,它眼里的“当前对象”根本就是子类对象,它是在给子类对象做初始化,而不是在创建一个父类对象。当你没有为子类写任何构造函数时,Java 编译器会自动为子类生成一个默认的无参构造函数,并且在这个默认构造函数的第一行,自动偷偷插入 super();
在 Java 中,有一个铁律:你要创建什么类的对象,就必须调用什么类的构造函数。

你要创建 Child(子类)对象,就必须调用 Child 的构造函数。
父类 Parent 的构造函数,永远只能用来创建 Parent 对象,它没有能力、也没有权限去直接创建一个 Child 对象。
第 3 步:子类构造函数内部,调用父类构造函数(协助初始化)
在子类的 Child() 构造函数执行的第一行,遇到了隐藏的 super()。 此时,程序会暂停子类构造函数的执行,跳转去执行父类的构造函数 Parent()。 👉 注意:父类构造函数此时被调用,不是为了“创建”父类对象,而是为了“初始化”当前这个子类对象中继承自父类的那部分属性。
第 4 步:回到子类,完成子类自身的初始化
父类构造函数执行完毕后,程序回到子类的 Child() 构造函数,继续执行后面的代码(如果有的话),初始化子类自己独有的属性。最终,一个完整的子类对象创建完成。

*Tesla* | 园豆:2062 (老鸟四级) | 2026-07-22 17:43
清除回答草稿
   您需要登录以后才能回答,未注册用户请先注册