第四单元 · 类、对象、封装、综合 · 第 11 / 12 节
构造方法、this 与封装
写无参/带参构造方法,用 this 区分成员变量与参数,用 private 隐藏字段并提供 getter/setter,做一个标准 JavaBean 风格的类。
上一节的 Student 已经能创建对象、保存姓名年龄、调用方法。但它的字段是 public 的:s1.age = -5; 也能编译通过,对象没法保护自己的数据。这一节做三件事:
- 用
private把字段藏起来,只留下get/set方法作为出入口; - 写构造方法,让
new Student("张三", 20)一步完成创建和初始化; - 用
this区分“同名的字段”和“参数”,并在 setter 里做最基本的校验。
先把三个词用白话记住:
- 封装:字段自己管,外部不能直接改,只能通过你提供的方法改;方法里可以先检查再决定收不收。
- 构造方法:
new对象时自动执行的特殊方法,名字必须和类名完全一样,没有返回值类型(连void都不写)。 - **
this**:在成员方法或构造方法里,指“当前这个对象”。当参数名和字段名撞车时,this.name明确指字段,name指参数。
本节的主线仍然只写 Student.java 和 StudentDemo.java 两个文件;Student 是设计图,StudentDemo 是入口。main 仍然只是入口方法名,不是 Student 的字段。后面的 ConstructorCheck.java 和 ctor-check/ 只是检查和错误演示用的临时文件,不替代这两个主线文件。
$ 提示符;输出和输入示例没有复制按钮。已完成 0 / 8 步
第 1 步:把字段改成 private,补上 getter/setter
进入本节目录,先备份再编辑。注意这一步先把字段藏起来,但还不写构造方法,所以编译器仍会自动提供一个无参构造方法,下一步还能用 new Student()。
$ 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 退出;不保存就退出,后面编译会找不到文件。
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 字段仍然能通过方法写入。
$ if [ -e StudentDemo.java ]; then mv StudentDemo.java "StudentDemo.java.bak.$(date +%s)"; fi
$ nano 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();
}
}
编译两个文件并运行:
$ javac -encoding UTF-8 Student.java StudentDemo.java
$ java StudentDemo
我叫张三,今年20岁。
我叫李四,今年21岁。
这段能成立,是因为此时 Student 里一个构造方法都没写,编译器自动补了一个看不见的 public Student() {}。下一步自己写带参构造方法后,这个“自动补”的待遇就没有了。
第 3 步:加带参构造方法,用 this 区分字段和参数
现在给 Student 加一个带参构造方法。它接收姓名和年龄,在 new 的时候一次性把字段填好。
$ cp Student.java "Student.java.bak.$(date +%s)"
$ nano Student.java
把 Student.java 改成下面这样(新增的是 public Student(String name, int age) 这一段):
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 + "岁。");
}
}
构造方法的四个硬规则,逐条对照:
- 名字必须和类名完全一样:类叫
Student,构造方法就必须叫Student,大小写一致。 - 没有返回值类型:写
public Student(...)。如果写成public void Student(...)或public Student Student(...),编译器会把它当成普通方法(方法名和返回类型的组合本身可能合法),new不会把它当构造方法使用。第 8 步会用void的例子验证这一点。 - **由
new调用**:写new Student("张三", 20),不是s1.Student("张三", 20)。构造方法不是给已有对象调用的。 - 可以重载:参数列表不同,就可以有多个构造方法。现在先只写带参的,第 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() 改成带参调用:
$ cp StudentDemo.java "StudentDemo.java.bak.$(date +%s)"
$ nano 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();
}
}
两个文件一起编译、运行:
$ javac -encoding UTF-8 Student.java StudentDemo.java
$ java StudentDemo
我叫张三,今年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()。
$ if [ -e ConstructorCheck.java ]; then mv ConstructorCheck.java "ConstructorCheck.java.bak.$(date +%s)"; fi
$ nano ConstructorCheck.java
public class ConstructorCheck {
public static void main(String[] args) {
Student s = new Student();
System.out.println("无参构造方法可用。");
}
}
编译它:
$ javac -encoding UTF-8 Student.java ConstructorCheck.java
这次会失败,而且是故意的。错误类别是“构造方法参数不匹配”,你会看到类似:
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 赋值。
$ cp Student.java "Student.java.bak.$(date +%s)"
$ nano 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 有两个构造方法:无参的和带参的。它们参数列表不同,这叫构造方法重载,可以同时存在。
重新编译并运行检查文件:
$ javac -encoding UTF-8 Student.java ConstructorCheck.java StudentDemo.java
$ java ConstructorCheck
无参构造方法可用。
ConstructorCheck 通过了,说明无参构造方法已经补上。这里 new Student() 创建出来的对象,name 是 null、age 是 0;null 表示“还没有指向任何字符串对象”,不是空字符串 ""。本节不打印它,先知道有这回事即可;lesson12 用对象数组时会再遇到 null。
第 7 步:给 setAge 加校验,非法年龄不写入
封装的真正好处是:字段只能通过你写的方法改,所以你可以在方法里加规则。现在规定年龄必须在 0 到 120 之间;不合法就拒绝修改并说明原因,不静默改成 0,也不偷偷改成 120。
$ cp Student.java "Student.java.bak.$(date +%s)"
$ nano Student.java
把 setAge 改成带 if 的版本,并让带参构造方法也调用 setAge,这样创建对象时同样受校验保护:
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:
$ cp StudentDemo.java "StudentDemo.java.bak.$(date +%s)"
$ nano 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());
}
}
编译运行:
$ javac -encoding UTF-8 Student.java StudentDemo.java
$ java StudentDemo
我叫张三,今年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:
$ mkdir -p ctor-check
$ cd ctor-check
$ if [ -e BadCtor.java ]; then mv BadCtor.java "BadCtor.java.bak.$(date +%s)"; fi
$ nano BadCtor.java
public class BadCtor {
private String name;
public void BadCtor(String name) {
this.name = name;
}
public String getName() {
return name;
}
}
再写一个使用它的 BadCtorDemo.java,按“带参构造方法”的方式去 new:
$ if [ -e BadCtorDemo.java ]; then mv BadCtorDemo.java "BadCtorDemo.java.bak.$(date +%s)"; fi
$ nano BadCtorDemo.java
public class BadCtorDemo {
public static void main(String[] args) {
BadCtor b = new BadCtor("张三");
System.out.println(b.getName());
}
}
两个文件一起编译:
$ javac -encoding UTF-8 BadCtor.java BadCtorDemo.java
会失败,错误类别同样是“构造方法参数不匹配”:
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("张三") 这样的方式调用。
现在回到主目录,把本节的主文件重新编译一遍,确认错误演示没有影响它们:
$ 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。
练习
- 用最终的
Student类(带无参/带参构造、private 字段、带校验的setAge)写AnswerLesson11.java:创建三个学生张三20、李四21、王五19,分别调用introduce();然后对s1调用setAge(200),打印s1.getAge()和s1.getName(),解释年龄为什么没有变成 200。 - 把
AnswerLesson11.java里的s1.setAge(200)改成s1.setAge(121),再改成s1.setAge(18),分别预测年龄并运行验证。
先自己写,再展开对答案。答案文件叫 AnswerLesson11.java,不会覆盖你的 Student.java 或 StudentDemo.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());
}
}
编译运行:
$ javac -encoding UTF-8 Student.java AnswerLesson11.java
$ java AnswerLesson11
我叫张三,今年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。
说明:本站是个人学习整理,不是官方教材;不包含原课程的书本、视频或受版权保护的整本内容。原课程与商标归其权利人。