Java从环境到类与对象 · SSH 实操

第四单元 · 类、对象、封装、综合 · 第 11 / 12 节

构造方法、this 与封装

写无参/带参构造方法,用 this 区分成员变量与参数,用 private 隐藏字段并提供 getter/setter,做一个标准 JavaBean 风格的类。

约 40 分钟 · 8 个步骤 · 进度只存本机

上一节的 Student 已经能创建对象、保存姓名年龄、调用方法。但它的字段是 public 的:s1.age = -5; 也能编译通过,对象没法保护自己的数据。这一节做三件事:

  1. 用 private 把字段藏起来,只留下 get/set 方法作为出入口;
  2. 写构造方法,让 new Student("张三", 20) 一步完成创建和初始化;
  3. 用 this 区分“同名的字段”和“参数”,并在 setter 里做最基本的校验。

先把三个词用白话记住:

  • 封装:字段自己管,外部不能直接改,只能通过你提供的方法改;方法里可以先检查再决定收不收。
  • 构造方法:new 对象时自动执行的特殊方法,名字必须和类名完全一样,没有返回值类型(连 void 都不写)。
  • **this**:在成员方法或构造方法里,指“当前这个对象”。当参数名和字段名撞车时,this.name 明确指字段,name 指参数。

本节的主线仍然只写 Student.java 和 StudentDemo.java 两个文件;Student 是设计图,StudentDemo 是入口。main 仍然只是入口方法名,不是 Student 的字段。后面的 ConstructorCheck.java 和 ctor-check/ 只是检查和错误演示用的临时文件,不替代这两个主线文件。

SSH 服务器终端Java 源码程序交互输入输出示例(只读)复制 Java 源码不带文件名;复制命令不带 $ 提示符;输出和输入示例没有复制按钮。
这是教程网页,不是在线运行环境。Java 代码和 SSH 命令都要在你的服务器上执行;网页不会执行代码、不会连接服务器、也不会接收密码。

已完成 0 / 8 步

第 1 步:把字段改成 private,补上 getter/setter

进入本节目录,先备份再编辑。注意这一步先把字段藏起来,但还不写构造方法,所以编译器仍会自动提供一个无参构造方法,下一步还能用 new Student()。

SSH 服务器终端bash
$ source ~/java-practice/env.sh
$ mkdir -p ~/java-practice/lesson11
$ cd ~/java-practice/lesson11
$ if [ -e Student.java ]; then mv Student.java "Student.java.bak.$(date +%s)"; fi
$ nano Student.java

粘贴下面整段 Student.java。在 nano 里按 Ctrl+O,回车确认文件名,再按 Ctrl+X 退出;不保存就退出,后面编译会找不到文件。

Java 源码Student.java
public class Student {
    private String name;
    private int age;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public void introduce() {
        System.out.println("我叫" + name + ",今年" + age + "岁。");
    }
}

逐行看:

  • private String name;:private 表示只有 Student 类内部能直接访问这个字段。StudentDemo 里再写 s1.name 就编译不过。
  • public String getName():getter,把字段的值返回出去。命名习惯是 get + 字段名(首字母大写)。
  • public void setName(String name):setter,外部通过它改字段。void 表示不返回结果。
  • this.name = name;:左边 this.name 是当前对象的字段,右边 name 是方法参数。参数名和字段名相同,所以必须用 this 区分。
  • public int getAge() / public void setAge(int age):同样的套路,只不过类型是 int。

现在 setAge 还只是直接赋值,没有校验;第 7 步再给它加“年龄必须在 0 到 120 之间”的检查。先把封装的结构搭起来,不要一次塞太多。

说明 private 不是“删掉”

private 不是让字段消失,而是把“直接访问”的入口关掉。类内部的方法仍然能读写它;外部只能通过 public 的 getter/setter。这样以后想加校验,只需要改类内部的方法,不用追着每个调用点改。

说明 本课说的“JavaBean 风格”

在本课范围内,标准 JavaBean 风格指:字段用 private 藏起来,提供一个 public 无参构造方法,再提供 public 的 getter/setter。目的是让对象能被统一地创建和读写;不需要继承、框架或额外注解。第 6 步会补齐无参构造方法。

第 2 步:用 setter 给对象赋值,先跑通封装后的版本

写一个只用无参构造方法 + setter 的入口,确认 private 字段仍然能通过方法写入。

SSH 服务器终端bash
$ if [ -e StudentDemo.java ]; then mv StudentDemo.java "StudentDemo.java.bak.$(date +%s)"; fi
$ nano StudentDemo.java
Java 源码StudentDemo.java
public class StudentDemo {
    public static void main(String[] args) {
        Student s1 = new Student();
        s1.setName("张三");
        s1.setAge(20);
        s1.introduce();

        Student s2 = new Student();
        s2.setName("李四");
        s2.setAge(21);
        s2.introduce();
    }
}

编译两个文件并运行:

SSH 服务器终端bash
$ javac -encoding UTF-8 Student.java StudentDemo.java
$ java StudentDemo
输出示例(只读)output不可复制
我叫张三,今年20岁。
我叫李四,今年21岁。

这段能成立,是因为此时 Student 里一个构造方法都没写,编译器自动补了一个看不见的 public Student() {}。下一步自己写带参构造方法后,这个“自动补”的待遇就没有了。

第 3 步:加带参构造方法,用 this 区分字段和参数

现在给 Student 加一个带参构造方法。它接收姓名和年龄,在 new 的时候一次性把字段填好。

SSH 服务器终端bash
$ cp Student.java "Student.java.bak.$(date +%s)"
$ nano Student.java

把 Student.java 改成下面这样(新增的是 public Student(String name, int age) 这一段):

Java 源码Student.java
public class Student {
    private String name;
    private int age;

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

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public void introduce() {
        System.out.println("我叫" + name + ",今年" + age + "岁。");
    }
}

构造方法的四个硬规则,逐条对照:

  1. 名字必须和类名完全一样:类叫 Student,构造方法就必须叫 Student,大小写一致。
  2. 没有返回值类型:写 public Student(...)。如果写成 public void Student(...) 或 public Student Student(...),编译器会把它当成普通方法(方法名和返回类型的组合本身可能合法),new 不会把它当构造方法使用。第 8 步会用 void 的例子验证这一点。
  3. **由 new 调用**:写 new Student("张三", 20),不是 s1.Student("张三", 20)。构造方法不是给已有对象调用的。
  4. 可以重载:参数列表不同,就可以有多个构造方法。现在先只写带参的,第 6 步再补无参的。

this.name = name; 这一行要慢慢看:

  • 等号左边 this.name:当前对象的 name 字段。
  • 等号右边 name:构造方法的参数。
  • 如果只写 name = name;,两边都是参数,字段没有被赋值,这是初学者最常见的空转。

this 不是“另一个对象”,它就是“当前正在创建/正在使用的这个对象”。在构造方法里,它指 new 正在做出来的那个对象;在 setName 里,它指调用 setName 的那个对象。

注意 这一步先别编译

StudentDemo.java 还在用 new Student(),而 Student 现在只有带参构造方法。直接编译会失败,这是下一步要专门观察的现象;先把 StudentDemo 改成带参创建,再一起编译。

第 4 步:改 StudentDemo,用带参构造方法创建对象

把 StudentDemo 里两次 new Student() 改成带参调用:

SSH 服务器终端bash
$ cp StudentDemo.java "StudentDemo.java.bak.$(date +%s)"
$ nano StudentDemo.java
Java 源码StudentDemo.java
public class StudentDemo {
    public static void main(String[] args) {
        Student s1 = new Student("张三", 20);
        Student s2 = new Student("李四", 21);

        s1.introduce();
        s2.introduce();
    }
}

两个文件一起编译、运行:

SSH 服务器终端bash
$ javac -encoding UTF-8 Student.java StudentDemo.java
$ java StudentDemo
输出示例(只读)output不可复制
我叫张三,今年20岁。
我叫李四,今年21岁。

new Student("张三", 20) 做了三件事:创建对象、把 "张三" 传给 name 参数、把 20 传给 age 参数;进入构造方法后,this.name = name; 和 this.age = age; 把值写进对象。构造方法执行完,new 把做好的对象交给 s1。

注意这里没有调用 setName、setAge,对象一创建就已经有数据。这就是构造方法比“先 new 再一个个 set”更直接的地方。

第 5 步:验证“写了带参构造后,无参构造不再自动给”

这是本节最重要的规则之一。写一个 ConstructorCheck.java,它只做一件事:用无参方式 new Student()。

SSH 服务器终端bash
$ if [ -e ConstructorCheck.java ]; then mv ConstructorCheck.java "ConstructorCheck.java.bak.$(date +%s)"; fi
$ nano ConstructorCheck.java
Java 源码ConstructorCheck.java
public class ConstructorCheck {
    public static void main(String[] args) {
        Student s = new Student();
        System.out.println("无参构造方法可用。");
    }
}

编译它:

SSH 服务器终端bash
$ javac -encoding UTF-8 Student.java ConstructorCheck.java

这次会失败,而且是故意的。错误类别是“构造方法参数不匹配”,你会看到类似:

输出示例(只读)text不可复制
ConstructorCheck.java:3: error: constructor Student in class Student cannot be applied to given types;
        Student s = new Student();
                    ^
  required: String,int
  found:    no arguments
  reason: actual and formal argument lists differ in length
1 error

原因:编译器只在“你一个构造方法都没写”的时候,才自动补一个无参构造方法。 你在第 3 步已经写了一个带参构造方法,编译器就不再补无参的了。现在 Student 只有 Student(String, int) 这一个构造方法,所以 new Student() 找不到匹配的构造方法。

这不是“Java 坏了”,而是构造方法的重载规则:new 后面括号里的参数,必须能匹配某一个构造方法的参数列表。没有匹配就编译失败。

第 6 步:补回无参构造方法,让两种创建方式都能用

既然带参构造方法把自动无参构造顶掉了,就自己补一个。它不需要做任何事,只负责让 new Student() 能成立;字段会保持默认值,后面再用 setter 赋值。

SSH 服务器终端bash
$ cp Student.java "Student.java.bak.$(date +%s)"
$ nano Student.java

在带参构造方法前面加一个空的无参构造方法:

Java 源码Student.java
public class Student {
    private String name;
    private int age;

    public Student() {
    }

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

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public void introduce() {
        System.out.println("我叫" + name + ",今年" + age + "岁。");
    }
}

现在 Student 有两个构造方法:无参的和带参的。它们参数列表不同,这叫构造方法重载,可以同时存在。

重新编译并运行检查文件:

SSH 服务器终端bash
$ javac -encoding UTF-8 Student.java ConstructorCheck.java StudentDemo.java
$ java ConstructorCheck
输出示例(只读)output不可复制
无参构造方法可用。

ConstructorCheck 通过了,说明无参构造方法已经补上。这里 new Student() 创建出来的对象,name 是 null、age 是 0;null 表示“还没有指向任何字符串对象”,不是空字符串 ""。本节不打印它,先知道有这回事即可;lesson12 用对象数组时会再遇到 null。

第 7 步:给 setAge 加校验,非法年龄不写入

封装的真正好处是:字段只能通过你写的方法改,所以你可以在方法里加规则。现在规定年龄必须在 0 到 120 之间;不合法就拒绝修改并说明原因,不静默改成 0,也不偷偷改成 120。

SSH 服务器终端bash
$ cp Student.java "Student.java.bak.$(date +%s)"
$ nano Student.java

把 setAge 改成带 if 的版本,并让带参构造方法也调用 setAge,这样创建对象时同样受校验保护:

Java 源码Student.java
public class Student {
    private String name;
    private int age;

    public Student() {
    }

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

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        if (age >= 0 && age <= 120) {
            this.age = age;
        } else {
            System.out.println("年龄不合法,未修改:" + age);
        }
    }

    public void introduce() {
        System.out.println("我叫" + name + ",今年" + age + "岁。");
    }
}

再改 StudentDemo,先正常改成 22,再故意传一个 -5:

SSH 服务器终端bash
$ cp StudentDemo.java "StudentDemo.java.bak.$(date +%s)"
$ nano StudentDemo.java
Java 源码StudentDemo.java
public class StudentDemo {
    public static void main(String[] args) {
        Student s1 = new Student("张三", 20);
        Student s2 = new Student("李四", 21);

        s1.introduce();
        s2.introduce();

        s1.setAge(22);
        System.out.println("修改后 s1 的年龄:" + s1.getAge());

        s1.setAge(-5);
        System.out.println("非法修改后 s1 的年龄:" + s1.getAge());
    }
}

编译运行:

SSH 服务器终端bash
$ javac -encoding UTF-8 Student.java StudentDemo.java
$ java StudentDemo
输出示例(只读)output不可复制
我叫张三,今年20岁。
我叫李四,今年21岁。
修改后 s1 的年龄:22
年龄不合法,未修改:-5
非法修改后 s1 的年龄:22

看最后三行:s1.setAge(22) 合法,年龄变成 22;s1.setAge(-5) 不合法,setter 打印提示后直接返回,age 仍然是 22。它没有把 -5 改成 0,也没有改成 120,而是拒绝写入。 这比“悄悄纠正”更安全,因为调用者能立刻看到自己的输入有问题。

带参构造方法里写 setAge(age) 而不是 this.age = age,也是为了让创建时走同一套校验。如果构造时传 new Student("张三", 200),会打印“年龄不合法,未修改:200”,对象的 age 保持默认值 0。这个行为要在正文说清楚,不能假装它被自动改成了 120。

提示 为什么不直接写 this.age = age

如果构造方法和 setter 各写一套赋值逻辑,以后改规则要改两处,容易漏。让构造方法调用 setter,规则只有一份。现在只要求会用,不展开更复杂的复用。

第 8 步:故意给构造方法加 void,看它怎样变成普通方法

最后做一个错误演示,专门记“构造方法没有返回值类型”这条规则。我们在 ctor-check/ 子目录里做,不碰主目录的 Student.java。

先建目录,写一个 BadCtor.java。注意它的“构造方法”前面多了 void:

SSH 服务器终端bash
$ mkdir -p ctor-check
$ cd ctor-check
$ if [ -e BadCtor.java ]; then mv BadCtor.java "BadCtor.java.bak.$(date +%s)"; fi
$ nano BadCtor.java
Java 源码BadCtor.java
public class BadCtor {
    private String name;

    public void BadCtor(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }
}

再写一个使用它的 BadCtorDemo.java,按“带参构造方法”的方式去 new:

SSH 服务器终端bash
$ if [ -e BadCtorDemo.java ]; then mv BadCtorDemo.java "BadCtorDemo.java.bak.$(date +%s)"; fi
$ nano BadCtorDemo.java
Java 源码BadCtorDemo.java
public class BadCtorDemo {
    public static void main(String[] args) {
        BadCtor b = new BadCtor("张三");
        System.out.println(b.getName());
    }
}

两个文件一起编译:

SSH 服务器终端bash
$ javac -encoding UTF-8 BadCtor.java BadCtorDemo.java

会失败,错误类别同样是“构造方法参数不匹配”:

输出示例(只读)text不可复制
BadCtorDemo.java:3: error: constructor BadCtor in class BadCtor cannot be applied to given types;
        BadCtor b = new BadCtor("张三");
                    ^
  required: no arguments
  found:    String
  reason: actual and formal argument lists differ in length
1 error

为什么?因为 public void BadCtor(String name) 多了一个 void,它不再被当作构造方法,而是一个名字碰巧叫 BadCtor 的普通方法。BadCtor 类因此只剩编译器自动补的无参构造方法,new BadCtor("张三") 找不到带 String 的构造方法,就失败了。

这也说明:new 后面的括号只认构造方法,不认普通方法。普通方法要等对象创建出来以后,用 b.BadCtor("张三") 这样的方式调用。

现在回到主目录,把本节的主文件重新编译一遍,确认错误演示没有影响它们:

SSH 服务器终端bash
$ cd ~/java-practice/lesson11
$ javac -encoding UTF-8 Student.java StudentDemo.java ConstructorCheck.java

javac 没有输出就是成功。ctor-check/ 里的错误文件可以留着当反例,不影响主目录。

常见错误

  • **constructor Student in class Student cannot be applied to given types**:new Student(...) 的参数和现有构造方法对不上。先看类里到底有几个构造方法、每个要几个参数;写了带参构造后,如果还需要 new Student(),必须自己补无参构造方法。
  • **class Student is public, should be declared in a file named Student.java**:public class 的名字和文件名不一致,或大小写不同。先 ls -l 看文件名,再改成完全一致。
  • **name has private access in Student**:在 StudentDemo 里直接写了 s1.name。字段已经是 private,改用 s1.getName() / s1.setName(...)。
  • **non-static variable name cannot be referenced from a static context**:在 main 里直接写 name。name 是对象的字段,必须先有 Student 对象,再通过对象调用 getter/setter。
  • **void BadCtor(...) 能单独编译,但 new BadCtor("张三") 失败**:给构造方法多写了 void 或返回类型,它就成了普通方法;类里只剩编译器自动补的无参构造方法。构造方法只能写 public Student(...),前面不能有 void、int 等返回类型。
  • **cannot find symbol ... ConstructorCheck**:ConstructorCheck.java 没有参与编译,或当前目录不对。先 pwd,再把要用的 .java 文件一起交给 javac。

练习

  1. 用最终的 Student 类(带无参/带参构造、private 字段、带校验的 setAge)写 AnswerLesson11.java:创建三个学生 张三 20、李四 21、王五 19,分别调用 introduce();然后对 s1 调用 setAge(200),打印 s1.getAge() 和 s1.getName(),解释年龄为什么没有变成 200。
  2. 把 AnswerLesson11.java 里的 s1.setAge(200) 改成 s1.setAge(121),再改成 s1.setAge(18),分别预测年龄并运行验证。

先自己写,再展开对答案。答案文件叫 AnswerLesson11.java,不会覆盖你的 Student.java 或 StudentDemo.java。

参考答案 对答案
Java 源码AnswerLesson11.java
public class AnswerLesson11 {
    public static void main(String[] args) {
        Student s1 = new Student("张三", 20);
        Student s2 = new Student("李四", 21);
        Student s3 = new Student("王五", 19);

        s1.introduce();
        s2.introduce();
        s3.introduce();

        s1.setAge(200);
        System.out.println("s1 的年龄:" + s1.getAge());
        System.out.println("s1 的姓名:" + s1.getName());
    }
}

编译运行:

SSH 服务器终端bash
$ javac -encoding UTF-8 Student.java AnswerLesson11.java
$ java AnswerLesson11
输出示例(只读)output不可复制
我叫张三,今年20岁。
我叫李四,今年21岁。
我叫王五,今年19岁。
年龄不合法,未修改:200
s1 的年龄:20
s1 的姓名:张三

setAge(200) 不满足 age >= 0 && age <= 120,所以 setter 只打印提示,没有执行 this.age = age;,对象的年龄仍然是 20。第二题中,121 超过 120,被拒绝,年龄仍是 20;18 在 0–120 内,年龄变成 18。

做到这里

做到这里今天可以停。你已经会:

  • 用 private 隐藏字段,用 getter/setter 提供受控访问;
  • 写无参构造方法和带参构造方法,知道构造方法没有返回值类型、名字必须等于类名;
  • 用 this 区分同名的字段和参数;
  • 知道“写了任何构造方法后,编译器不再自动补无参构造方法”;
  • 在 setter 里做简单校验,非法值拒绝写入而不是静默纠错。

下一节把 Student 放进 Student[],用 count 管理真实人数,做一个能增、查、改、删的控制台小模型。

来源 来源与改编说明(非官方)

主题参考:黑马程序员 Java 入门到起飞(BV17F411T7Ao,2022-04-27)P083–P086、P089 的封装、就近原则和 this、构造方法、标准 JavaBean 主题。本站为独立原创讲解,非官方站点/教材,未获授权,不逐字复刻视频或讲义;示例、错误清单与练习为原创补充。详见 source/SOURCE.md。

说明:本站是个人学习整理,不是官方教材;不包含原课程的书本、视频或受版权保护的整本内容。原课程与商标归其权利人。