构造方法
本章定位:构造方法是对象初始化的唯一入口。理解默认构造方法的生成规则、构造方法重载的链式调用,以及
this()在同级构造器间调用的"首行"约束。这是理解对象生命周期和 Spring Bean 实例化的前置知识。
什么是构造方法
周一早会刚结束,白歌发现小崔的代码里工号为 1003 的员工姓名竟然是 null。小崔挠头:"我 new Employee() 了呀,然后才 setName(...) 的,但测试时忘记调 setter 了..."
白歌说:"这就是为什么需要构造方法——它强制要求创建对象时就必须传入必要信息,不给'半成品'对象留下任何机会。"
构造方法(Constructor) 是一种特殊的方法,在对象创建时自动调用,用于完成对象的初始化。
构造方法的四大特性
| 特性 | 说明 | 示例 |
|---|---|---|
| 名称与类名完全相同 | 包括大小写 | public Employee() {} |
| 没有返回类型 | 连 void 也不能写 | 写 void 就变成了普通方法! |
| 使用 new 调用 | 不能像普通方法那样 obj.constructor() | new Employee(1001, "大翔", 50000) |
| 自动调用 | new 关键字执行时,JVM 自动调用 | 用户无需手动调用 |
// ✅ 正确:构造方法
public class Employee {
public Employee(long id, String name) { // 无返回类型,与类同名
this.id = id;
this.name = name;
}
}
// ❌ 错误:这是普通方法,不是构造方法
public class Employee {
public void Employee(long id, String name) { // 多了 void!
// 这只是一个名叫 Employee 的普通方法,可以像普通方法调用
}
}
默认构造方法
如果一个类中没有显式定义任何构造方法,编译器会自动提供一个无参的默认构造方法。
public class Student {
private String name;
private int age;
// 程序员没有写构造方法
// 编译器自动生成:
// public Student() {
// super(); // 调用 Object 类的构造方法
// }
// getter / setter ...
}
// 使用:
Student s = new Student(); // ✅ 调用默认构造方法
重要规则:
| 场景 | 默认构造方法是否存在 |
|---|---|
| 类中没有构造方法 | ✅ 编译器自动生成无参构造方法 |
| 类中定义了任意一个构造方法 | ❌ 编译器不再生成默认构造方法 |
public class Student {
private String name;
// 自定义了有参构造方法
public Student(String name) {
this.name = name;
}
// 此时默认无参构造方法消失!
}
// 使用:
Student s1 = new Student("小崔"); // ✅ 调用有参构造方法
Student s2 = new Student(); // ❌ 编译错误:无参构造方法不存在
面试常见陷阱:父类定义了有参构造方法后,子类构造方法必须显式调用
super(参数),否则编译失败。
构造方法重载
与普通方法一样,构造方法支持重载——同一个类中可以有多个参数列表不同的构造方法,以支持不同的初始化方式。
public class Employee {
private long id;
private String name;
private double salary;
// 重载版本 1:无参 —— 创建"待填写"员工
public Employee() {
System.out.println("Employee() 被调用 —— 创建待填写员工");
}
// 重载版本 2:仅工号 —— 最简单的有效员工
public Employee(long id) {
this.id = id;
System.out.println("Employee(long) 被调用 —— 仅工号");
}
// 重载版本 3:工号 + 姓名 —— 薪资待定
public Employee(long id, String name) {
this.id = id;
this.name = name;
System.out.println("Employee(long, String) 被调用 —— 薪资待定");
}
// 重载版本 4:全参数 —— 完整信息入职
public Employee(long id, String name, double salary) {
this.id = id;
this.name = name;
this.salary = salary;
System.out.println("Employee(long, String, double) 被调用 —— 完整信息");
}
}
this() 调用本类其他构造方法
如果一个类有多个构造方法,可以通过 this(参数...) 在一个构造方法中调用另一个构造方法,从而复用初始化代码,避免重复。
核心规则
| 规则 | 说明 |
|---|---|
| 必须在第一行 | this() 必须是构造方法体中的第一条语句 |
| 最多调用一次 | 一个构造方法中最多出现一次 this() |
| 不能成环 | A 调 B,B 调 C 没问题;A 调 B,B 调 A 则编译错误 |
| 不能在普通方法中使用 | this() 只能在构造方法中使用 |
public class Employee {
private long id;
private String name;
private double salary;
// 无参构造方法 —— 通过 this() 委派给全参构造方法
public Employee() {
this(0, "待入职", 0.0); // 必须在第一行
// 这里可以写额外的初始化代码(如果有的话)
}
// 仅工号 —— 委派给三参构造方法
public Employee(long id) {
this(id, "待入职", 0.0);
}
// 工号 + 姓名 —— 委派给全参构造方法
public Employee(long id, String name) {
this(id, name, 0.0);
}
// 全参构造方法 —— 最终执行者("终点站")
public Employee(long id, String name, double salary) {
this.id = id;
this.name = name;
this.salary = salary;
}
}
这种模式被称为**"构造方法链"(Constructor Chaining)**,Spring 框架中大量使用此模式。所有重载的构造方法最终都汇聚到一个"终点站"构造方法中,确保初始化逻辑集中在一处。
完整示例:飞翔科技员工入职系统
场景
小崔为飞翔科技实现员工入职模块:不同来源的员工信息完整度不同——校招简历只有姓名和期望薪资,社招简历信息完整,内推简历通常缺薪资数据。他使用构造方法重载 + this() 链式调用来优雅地处理这些场景。
/**
* 飞翔科技 —— 员工入职系统
* 演示:构造方法重载 + this() 链式调用
*/
public class Employee {
private static long nextId = 1000; // 自动生成工号(static 成员)
private long id;
private String name;
private double salary;
private String department; // 部门
private String source; // 招聘来源:校招/社招/内推
// ========== 构造方法链 ==========
// ① 终点站构造方法 —— "唯一的真相来源"(Single Source of Truth)
public Employee(long id, String name, double salary, String department, String source) {
this.id = id;
this.name = name;
this.salary = salary;
this.department = department;
this.source = source;
}
// ② 社招:信息完整,仅缺招聘来源标注
public Employee(String name, double salary, String department) {
this(++nextId, name, salary, department, "社招");
}
// ③ 内推:缺薪资(后定级)
public Employee(String name, String department, String source) {
this(++nextId, name, 0.0, department, source);
}
// ④ 校招:仅姓名和期望薪资
public Employee(String name, double salary) {
this(++nextId, name, salary, "待分配", "校招");
}
// ⑤ 无参:占位(不推荐但有时需要——如框架反射创建)
public Employee() {
this(++nextId, "待入职", 0.0, "待分配", "未知");
}
// ========== 业务方法 ==========
public void showInfo() {
System.out.printf("工号:%d | 姓名:%s | 薪资:%.0f | 部门:%s | 来源:%s%n",
id, name, salary, department, source);
}
// getter / setter 省略
}
/**
* 入职测试 —— 模拟不同来源的员工录入
*/
public class OnboardingTest {
public static void main(String[] args) {
System.out.println("===== 飞翔科技 2025 年春季入职 =====\n");
// 社招:信息完整
Employee emp1 = new Employee("大翔", 50000, "技术部");
// 内推:缺薪资
Employee emp2 = new Employee("白歌", "技术部", "内推");
// 校招:仅姓名和期望薪资
Employee emp3 = new Employee("小崔", 8000);
// 校招(第二个)
Employee emp4 = new Employee("黄俪", 12000);
// 社招:直接到部门
Employee emp5 = new Employee("孔蓝", 15000, "测试部");
emp1.showInfo();
emp2.showInfo();
emp3.showInfo();
emp4.showInfo();
emp5.showInfo();
}
}
运行输出:
===== 飞翔科技 2025 年春季入职 =====
工号:1001 | 姓名:大翔 | 薪资:50000 | 部门:技术部 | 来源:社招
工号:1002 | 姓名:白歌 | 薪资:0 | 部门:技术部 | 来源:内推
工号:1003 | 姓名:小崔 | 薪资:8000 | 部门:待分配 | 来源:校招
工号:1004 | 姓名:黄俪 | 薪资:12000 | 部门:待分配 | 来源:校招
工号:1005 | 姓名:孔蓝 | 薪资:15000 | 部门:测试部 | 来源:社招
对象创建全流程
易错场景
反例一:误把构造方法写成普通方法
// ❌ 错误:void 让"构造方法"退化成了普通方法
public class Student {
private String name;
// 这不是构造方法!这只是一个恰好叫 Student 的普通方法
public void Student(String name) {
this.name = name;
}
}
public class Test {
public static void main(String[] args) {
Student s = new Student(); // ✅ 调用了默认构造方法(编译器生成的)
s.Student("小崔"); // ✅ 这是普通方法调用(合法但迷惑)
// 想象一下你要 new Student("小崔") 时:
// Student s = new Student("小崔"); // ❌ 编译错误!因为真正意义上的带参构造方法不存在
}
}
纠正:去掉 void:
// ✅ 正确
public Student(String name) { // 这才是构造方法
this.name = name;
}
反例二:this() 不在第一行
// ❌ 错误:this() 必须在构造方法体的第一行
public class Employee {
private long id;
private String name;
public Employee() {
System.out.println("初始化开始"); // ❌ 这行代码导致 this() 不是第一条语句
this(0, "待入职"); // ❌ 编译错误:对 this 的调用必须是构造器中的第一个语句
}
public Employee(long id, String name) {
this.id = id;
this.name = name;
}
}
纠正:
// ✅ 正确:this() 必须是第一条语句
public Employee() {
this(0, "待入职"); // ✅ 第一行
System.out.println("初始化开始"); // ✅ 在 this() 之后可以写其他代码
}
反例三:构造方法循环调用
// ❌ 错误:构造方法之间形成循环调用
public class Employee {
public Employee() {
this(0); // 调用 ②
}
public Employee(long id) { // ②
this(); // 调用 ① —— 与 ① 形成循环!
}
}
// ❌ 编译错误:递归构造器调用
面试考点
Q1:构造方法和普通方法有什么区别?
维度 构造方法 普通方法 名称 必须与类名相同 任意合法标识符 返回类型 没有(不能写 void) 必须有 调用方式 new关键字自动调用通过对象引用显式调用 调用时机 对象创建时执行且仅执行一次 随时随地,可多次调用 继承 不被子类继承 被子类继承(除非 private)
Q2:如果父类没有无参构造方法,子类会怎样?
class Parent {
public Parent(String name) { } // 有参构造方法,默认无参构造方法消失
}
class Child extends Parent {
// ❌ 编译错误:父类没有无参构造方法
// 子类构造方法默认调用 super() 会失败
}
// ✅ 正确:显式调用父类有参构造方法
class Child extends Parent {
public Child(String name) {
super(name); // ✅ 必须显式调用
}
}
Q3:this() 和 super() 为什么都必须在第一行?谁先执行?
原因:这是 Java 语言设计规范,保证对象在构造方法体中执行任何代码之前,父类部分已经初始化完毕。 谁先执行:
this()和super()不能同时出现在同一个构造方法中。如果都没有写,编译器自动插入super()。this()会转而调用另一个构造方法,最终一定会有某个构造方法调用super()。所以执行顺序是:this()链 →super()→ 构造方法体。
Q4:构造方法可以被继承吗?可以被重写吗?
都不能。构造方法不属于类的成员方法,子类不能继承父类的构造方法。子类只能通过
super()来调用父类的构造方法。因此也没有"重写构造方法"这个概念。
小崔的笔记:"构造方法就是对象的'出生证明'——名字和类完全一样,不给写返回类型。
this()必须在第一行,就像出生证明上必须先写名字才能写其他信息。多个构造方法可以用this()串成一条链,把初始化逻辑集中在'终点站'构造方法里,既省代码又不容易出错。"