构造器与 this | JavaSE

构造器与 this

一、学习目标

学完本章,你应该能够:

  • 能够解释什么是构造器(Constructor),以及构造器与普通成员方法的区别。
  • 能够手写无参数构造器、有参数构造器和多个重载构造器。
  • 能够解释创建对象时构造器为什么会被自动调用。
  • 能够判断一个类是否拥有编译器隐式提供的默认构造器。
  • 能够解释 this 表示什么,并正确使用 this.成员变量 解决名称冲突。
  • 能够根据对象初始化需求选择合适的构造器。

二、核心知识

2.1 为什么需要构造器

上一章已经学习过:

Student student = new Student();
student.name = "张三";
student.age = 18;
student.score = 95.5;

这种方式当然可以创建并初始化对象。

但是存在一个问题:

创建对象和给对象初始化数据被拆成了多个步骤。

如果一个 Student 对象创建出来以后,本来就应该立即拥有姓名、年龄、成绩,那么更自然的方式是:

Student student = new Student("张三", 18, 95.5);

这里:

new Student("张三", 18, 95.5)

调用的就是 Student 类的构造器。

构造器的主要作用之一,就是:

在创建对象时完成对象的初始化。


2.2 什么是构造器

构造器(Constructor)是类中的一种特殊成员。

基本语法:

修饰符 类名(参数列表) {
    初始化代码;
}

例如:

public class Student {

    String name;
    int age;

    public Student() {
        System.out.println("Student对象正在创建");
    }
}

其中:

public Student()

就是构造器。


2.3 构造器的基本特征

构造器有几个必须掌握的规则。

规则一:构造器名称必须与类名完全一致

类名:

Student

那么构造器必须写成:

public Student() {
}

不能写成:

public student() {
}

Java 区分大小写。


规则二:构造器不能声明返回值类型

正确:

public Student() {
}

错误:

public void Student() {
}

很多初学者看到:

public void Student()

以为它是构造器。

实际上不是。

因为一旦写了:

void

它就已经变成了一个普通成员方法,只不过这个方法恰好叫做 Student

所以:

public Student() {
}

和:

public void Student() {
}

是完全不同的两个东西。


2.4 构造器什么时候执行

观察:

public class Student {

    public Student() {
        System.out.println("构造器执行了");
    }
}

创建对象:

Student s1 = new Student();
Student s2 = new Student();

每执行一次:

new Student()

就会创建一个新的 Student 对象,并调用对应的构造器。

可以把过程暂时理解成:

执行 new Student()
        ↓
创建 Student 对象
        ↓
调用 Student()
        ↓
执行构造器中的初始化代码
        ↓
得到对象引用
        ↓
赋值给变量

因此:

Student s = new Student();

不能只理解成“new 一个对象”。

它实际上还包含:

寻找并调用与参数匹配的构造器。


2.5 无参数构造器

没有参数的构造器称为无参数构造器。

public class Student {

    public Student() {
        System.out.println("无参数构造器");
    }
}

调用:

Student student = new Student();

2.6 有参数构造器

构造器也可以接收参数:

public class Student {

    String name;
    int age;

    public Student(String name, int age) {
        this.name = name;
        this.age = age;
    }
}

创建对象:

Student student = new Student("张三", 18);

这样对象在创建完成时,就已经拥有了初始化数据。


2.7 默认构造器

这是构造器最容易出错的规则之一。

假设:

public class Student {
}

我们虽然没有手写:

public Student() {
}

但下面代码仍然可以使用:

Student student = new Student();

原因是:

当一个普通类没有显式声明任何构造器时,Java 编译器会隐式声明一个默认构造器(Default Constructor)。

初学阶段可以近似理解为编译器帮我们补了:

public Student() {
}

但需要注意,默认构造器的访问权限并不永远都是 public,它会与类本身的可访问性相关,因此不要把“默认构造器一定是 public”当作规则记忆。


2.8 手写构造器以后会发生什么

看下面的类:

public class Student {

    String name;

    public Student(String name) {
        this.name = name;
    }
}

此时:

Student s1 = new Student("张三");

合法。

但是:

Student s2 = new Student();

会产生编译错误。

为什么?

因为:

只要类中显式声明了任意构造器,编译器就不会再隐式声明默认构造器。

所以如果既需要:

new Student()

又需要:

new Student("张三")

应该自己同时定义两个构造器:

public class Student {

    String name;

    public Student() {
    }

    public Student(String name) {
        this.name = name;
    }
}

2.9 构造器重载

同一个类可以定义多个构造器。

例如:

public class Student {

    String name;
    int age;
    double score;

    public Student() {
    }

    public Student(String name) {
        this.name = name;
    }

    public Student(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public Student(String name, int age, double score) {
        this.name = name;
        this.age = age;
        this.score = score;
    }
}

它们名称全部都是:

Student

但是参数列表不同。

这就是构造器重载(Constructor Overloading)。

因此可以:

Student s1 = new Student();

Student s2 = new Student("张三");

Student s3 = new Student("李四", 20);

Student s4 = new Student("王五", 21, 96.5);

不同的创建需求,可以选择不同的构造器。


2.10 什么是 this

this 是 Java 中非常重要的关键字。

在实例对象上下文中,可以先这样理解:

this 表示当前对象的引用。

例如:

public class Student {

    public void printThis() {
        System.out.println(this);
    }
}

测试:

Student s1 = new Student();
Student s2 = new Student();

s1.printThis();
s2.printThis();

当:

s1.printThis();

执行时,方法内部的:

this

指向 s1 对象。

当:

s2.printThis();

执行时,方法内部的:

this

指向 s2 对象。

因此可以建立一个非常重要的理解:

谁调用实例方法
     ↓
this 就代表谁

构造器中的 this 则表示:

当前正在被构造的那个对象。


2.11 this 解决成员变量和局部变量同名问题

假设:

public class Student {

    String name;

    public Student(String name) {
        name = name;
    }
}

这里出现了两个 name

构造器参数:

String name

属于局部变量。

成员变量:

String name;

属于对象。

当二者同名时:

name

默认优先找到距离当前位置更近的局部变量。

因此:

name = name;

实际上可以理解成:

局部变量 name = 局部变量 name

成员变量根本没有得到赋值。

正确写法:

public Student(String name) {
    this.name = name;
}

其中:

this.name

表示:

当前对象的成员变量 name

右边:

name

表示:

构造器参数 name

所以:

this.name = name;

可以读成:

把参数 name 的值赋给当前对象的成员变量 name


2.12 this 不只可以访问成员变量

this 本质上表示当前对象,因此也可以调用当前对象的实例方法:

public class Student {

    String name;

    public void study() {
        System.out.println(name + "正在学习");
    }

    public void startLearning() {
        this.study();
    }
}

很多情况下:

this.study();

也可以简写为:

study();

因为当前实例上下文已经明确。

因此实际开发中最常看到的仍然是:

this.name = name;

这种用于消除成员变量与局部变量歧义的写法。


三、使用方法

3.1 完整 Student 示例

public class Student {

    String name;
    int age;
    double score;

    public Student() {
    }

    public Student(String name) {
        this.name = name;
    }

    public Student(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public Student(String name, int age, double score) {
        this.name = name;
        this.age = age;
        this.score = score;
    }

    public void printInfo() {
        System.out.println(
                "姓名:" + name +
                ",年龄:" + age +
                ",成绩:" + score
        );
    }
}

测试:

public class Test {

    public static void main(String[] args) {

        Student s1 = new Student();

        Student s2 = new Student("张三");

        Student s3 = new Student(
                "李四",
                20,
                95.5
        );

        s1.printInfo();
        s2.printInfo();
        s3.printInfo();
    }
}

这里体现了一个非常重要的设计思想:

让调用者根据自己拥有的数据选择合适的对象初始化方式。


3.2 如何判断应该设计几个构造器

没有固定答案。

例如一个用户对象:

User

如果业务允许:

new User()

那么可以提供无参构造器。

如果创建用户时姓名必须存在:

new User("LingXi")

那么可以提供:

public User(String username)

如果还需要一次性初始化更多信息:

new User("LingXi", 20, "lingxi@example.com")

还可以继续增加重载构造器。

核心原则不是:

构造器越多越好。

而是:

构造器应该表达对象合理的初始化方式。


四、原理与进阶

4.1 构造器不是普通方法

构造器与方法在语法上非常相似,但不能把构造器简单理解为“一个特殊名字的方法”。

例如构造器:

public Student() {
}

没有返回值类型。

普通方法:

public void printInfo() {
}

具有返回值类型声明:

void

在 Java 语言层面,二者属于不同的声明结构。


4.2 默认构造器不是永远存在

一定要区分两个概念:

情况一:没有声明任何构造器

public class Student {
}

编译器隐式声明默认构造器。


情况二:已经声明了一个构造器

public class Student {

    public Student(String name) {
    }
}

这时编译器不会再提供默认构造器。

所以:

new Student();

不能使用。

这是大量 Java 初学者第一次写实体类时最容易出现的问题之一。


4.3 this 为什么不能直接出现在 static 方法中

考虑:

public static void test() {
    System.out.println(this);
}

这是错误的。

原因在于:

this

表示当前对象。

但静态方法属于类,不依赖某个具体实例对象才能调用:

Student.test();

调用时甚至可能根本不存在 Student 对象。

既然没有“当前对象”,自然也没有:

this

后面的 static 专章会系统讨论这一规则。


4.4 本章暂不展开 this(...)

Java 还允许在构造器之间通过:

this(...)

进行调用。

但这涉及构造器链以及后续的:

super(...)

为了保持章节边界,本章只掌握:

this

作为“当前对象引用”的含义。

this(...)super(...) 将在后续 02-09 子类构造器与 this(...) / super(...) 中系统学习。


五、实践应用

5.1 用户对象初始化

public class User {

    String username;
    String email;

    public User(String username, String email) {
        this.username = username;
        this.email = email;
    }
}

使用:

User user = new User(
        "LingXi",
        "lingxi@example.com"
);

相比:

User user = new User();
user.username = "LingXi";
user.email = "lingxi@example.com";

第一种方式能够更加明确地表达:

创建用户对象时,需要同时提供这些初始化信息。


5.2 构造器不是数据校验的唯一位置

构造器可以对输入数据进行检查:

public Student(String name, int age) {
    if (age >= 0 && age <= 150) {
        this.age = age;
    }

    this.name = name;
}

不过,当对象的数据以后还可能被修改时,仅仅在构造器中校验并不够。

例如:

student.age = -100;

仍然可能破坏数据。

如何阻止外部代码随意修改成员变量,就是下一章:

封装(Encapsulation)

要解决的问题。


六、常见问题

6.1 构造器必须写吗?

不一定。

如果一个普通类没有声明任何构造器,编译器会隐式声明默认构造器。

但是实际开发中,如果对对象初始化方式有要求,通常会显式设计构造器。


6.2 构造器可以写 void 吗?

不能。

例如:

public void Student() {
}

这是普通方法,不是构造器。


6.3 一个类可以有多个构造器吗?

可以。

只要参数列表不同,就可以形成构造器重载。


6.4 写了有参构造器以后为什么 new Student() 报错?

因为一旦显式声明任意构造器,编译器就不再隐式提供默认构造器。

如果仍然希望使用:

new Student()

就需要自己声明:

public Student() {
}

6.5 this 是对象吗?

更准确地说:

this 是一个引用当前对象的表达式。

因此:

this.name

表示当前对象中的 name 成员变量。


6.6 this.name = name 两个 name 是什么?

this.name = name;

左边:

this.name

是当前对象的成员变量。

右边:

name

是当前作用域中的局部变量或参数。


6.7 static 方法中为什么不能直接使用 this?

因为 this 必须对应某个具体对象,而静态方法属于类,可以在没有对象的情况下调用。


七、练习与验收

7.1 知识问答

  1. 什么是构造器?它主要解决什么问题?
  2. 构造器与普通成员方法在语法上有什么区别?
  3. 为什么构造器名称必须与类名一致?
  4. 构造器是否需要声明返回值类型?
  5. 创建对象时,构造器在什么时候执行?
  6. 一个类完全没有声明构造器时会发生什么?
  7. 什么是默认构造器?
  8. 显式声明一个有参构造器以后,默认构造器还存在吗?
  9. 为什么很多实体类会主动提供无参数构造器?
  10. 什么是构造器重载?
  11. this 在实例方法中表示什么?
  12. this 在构造器中表示什么?
  13. this.name = name; 两个 name 分别表示什么?
  14. this 为什么可以解决成员变量与局部变量同名问题?
  15. 静态方法中为什么不能直接使用 this

7.2 代码阅读

阅读代码:

class Student {

    String name;

    public Student() {
        System.out.println("A");
    }

    public Student(String name) {
        this.name = name;
        System.out.println("B");
    }
}

public class Test {

    public static void main(String[] args) {

        Student s1 = new Student();

        Student s2 = new Student("张三");

        System.out.println(s2.name);
    }
}

不要运行代码,回答:

  1. 一共创建了几个 Student 对象?
  2. 创建 s1 时调用哪个构造器?
  3. 创建 s2 时调用哪个构造器?
  4. 三条输出语句最终按照什么顺序出现?
  5. 如果删除无参数构造器,new Student() 是否还能使用?为什么?

阅读代码:

class Student {

    String name;

    public void printThis() {
        System.out.println(this);
    }
}

public class ThisRead {

    public static void main(String[] args) {

        Student s1 = new Student();
        Student s2 = new Student();

        s1.printThis();
        s2.printThis();

        System.out.println(s1);
        System.out.println(s2);
    }
}

回答:

  1. 哪两次输出表示同一个对象引用?
  2. s1.printThis() 中的 this 表示谁?
  3. s2.printThis() 中的 this 表示谁?
  4. 为什么同一个方法中的 this 会因为调用对象不同而不同?

7.3 手写代码

任务一:Student 构造器

从零设计:

Student

包含:

name
age
score

要求:

  • 提供无参数构造器。
  • 提供只接收 name 的构造器。
  • 提供 name + age 构造器。
  • 提供 name + age + score 构造器。
  • 使用 this 解决成员变量和参数同名问题。
  • 创建至少 4 个对象验证不同构造器。

任务二:User 类

设计:

User

包含:

username
password
email

要求至少设计两种合理的对象初始化方式,并说明为什么需要这两个构造器。


7.4 Debug

下面代码存在问题:

public class Student {

    String name;
    int age;

    public void Student(String name, int age) {
        name = name;
        age = age;
    }
}

要求:

  1. 找出构造器声明中的错误。
  2. 判断 name = name 的问题。
  3. 判断 age = age 的问题。
  4. 修复代码。
  5. 使用下面代码验证:
Student s = new Student("张三", 18);

再分析:

public class User {

    String username;

    public User(String username) {
        this.username = username;
    }
}

测试:

User u1 = new User();

回答:

  1. 为什么编译失败?
  2. Java 是否仍然自动生成了无参构造器?
  3. 应该如何修改 User 类才能同时支持两种创建方式?

7.5 综合训练

设计一个 Book 类。

包含:

title
author
price

要求:

  1. 设计至少三个构造器。
  2. 每个构造器代表一种合理初始化场景。
  3. 参数名称必须与成员变量名称一致。
  4. 使用 this 完成赋值。
  5. 编写 printInfo() 输出图书信息。
  6. 创建至少三个图书对象验证。

完成后说明:

为什么你选择这三个构造器,而不是随意增加大量构造器?


7.6 本章验收

不查看资料,检查自己能否完成:

  • [ ] 30 秒内口述什么是构造器。
  • [ ] 从零手写无参构造器。
  • [ ] 从零手写有参构造器。
  • [ ] 写出三个构造器形成重载。
  • [ ] 解释默认构造器什么时候存在。
  • [ ] 解释为什么写了有参构造器以后 new Student() 可能报错。
  • [ ] 解释 this 表示什么。
  • [ ] 解释 this.name = name
  • [ ] 判断为什么 static 方法不能直接使用 this
  • [ ] 不依赖 IDE 自动生成,从零完成一个带多个构造器的实体对象。

如果以上内容仍有任何一项需要依赖提示才能完成,本章就还没有达到真正掌握的标准。