Java 类的"内功心法"
this / static / final / 构造器
上一节我们学了"类是什么、对象怎么用",这节我们深入 Java 类的修饰和内功:this 怎么用、static 怎么玩、final 三种位置、构造器怎么重载,还有面试必考的 equals() / hashCode()。
学完这节:你写的类才能算专业——不是"能跑"就行,而是"知道为什么这么写"。
1
this 关键字:指的是"我自己"
💡 动机:当参数名和字段名撞了,Java 怎么知道你说的是哪个?
先看一个最常见的场景:
public class Student {
private String name;
public Student(String name) { // 这里的 name 是参数
this.name = name; // 左边 this.name 是字段,右边 name 是参数
}
}
一句话记住:
this 就是"我自己"——指的是当前这个对象。所以 this.name 就是"我这个对象的 name 字段"。
对比两种写法:
❌ 不写 this
两个 name 都是参数,字段根本没被赋值!
name = name;两个 name 都是参数,字段根本没被赋值!
→
✅ 写 this
左边 = 字段,右边 = 参数,赋值成功!
this.name = name;左边 = 字段,右边 = 参数,赋值成功!
2
构造器深入:重载 + this() 链式调用
💡 动机:用户想用不同方式创建对象,怎么办?
有时候用户只想给名字,有时候想给名字+年龄,有时候啥都不给。Java 用重载解决——同名方法,不同参数。
public class Student {
private String name;
private int age;
// 主构造器:名字 + 年龄
public Student(String name, int age) {
this.name = name;
this.age = age;
}
// 重载 1:只给名字,年龄默认 18
public Student(String name) {
this(name, 18); // 调用上面的构造器!
}
// 重载 2:啥都不给
public Student() {
this("未知", 0); // 也能链式调用
}
}
this() 的两个铁律:
- 必须出现在构造器的第一行(不能放在第二行)
- 只能调用一次(不能递归链式,否则会死循环)
用起来是这样的:
Student s1 = new Student(); // → "未知", 0
Student s2 = new Student("张三"); // → "张三", 18
Student s3 = new Student("李四", 22); // → "李四", 22
3
static 修饰符:属于"类",不属于"对象" ⭐
💡 动机:有些数据是"全班共享"的,不是"每个学生独有的"
你可能见过 Math.random()、System.out.println(),这些"不用 new 就能用"的东西,背后就是 static。
A
静态字段
static field
全类共享一份,所有对象共用
🏫 全班共用一个黑板
B
静态方法
static method
直接通过类名调用,不用 new
🔧 工具箱里的扳手
C
静态代码块
static block
类加载时执行一次(仅一次)
⚙️ 工厂开机时初始化
public class Student {
// 静态字段:所有学生共享一个计数器
private static int totalCount = 0;
// 普通字段:每个学生独有的属性
private String name;
// 静态方法:通过类名直接调用
public static int getTotalCount() {
return totalCount;
}
// 静态代码块:类加载时执行一次
static {
System.out.println("Student 类被加载了!");
}
public Student(String name) {
this.name = name;
totalCount++; // 每次 new 都让计数器 +1
}
}
点下面的按钮,体会"static 共享 / instance 独立":
总学生数 totalCount(static 共享)
0
还没有学生。点上面按钮创建一个~
关键观察:每次点按钮,
totalCount 都会 +1(因为它被所有对象共享),但每个学生有自己独立的 name。
4
final 关键字:不能改!
💡 动机:有些东西明确不允许被修改(常量、不允许重写、不允许继承)
final 出现在三个地方,每种"不能改"的含义不一样:
final 字段
值不能改
(常量)
(常量)
final 方法
子类不能
重写
重写
final 类
不能被继承
(如 String)
(如 String)
// 1. final 字段:常量
public class Config {
public static final int MAX_USERS = 100; // 公开常量
public static final String APP_NAME = "MyApp"; // 字符串常量
}
// 2. final 方法:不允许子类重写
public class Parent {
public final void showConfig() { // 子类不能改这个
System.out.println("固定配置");
}
}
// 3. final 类:不能被继承(String 就是 final 的)
public final class ImmutablePoint { // 不能再 extends
private final int x;
private final int y;
public ImmutablePoint(int x, int y) {
this.x = x;
this.y = y;
}
}
小白记法:看到
final 就想"盖章封死了"——字段 → 值定死;方法 → 实现定死;类 → 不能再扩展。
5
内部类:嵌套的"小类"(简单了解)
💡 动机:有些类只为某个外部类服务,没必要单开一个文件
Java 允许你把一个类定义在另一个类里面。有 4 种类型:
| 类型 | 定义位置 | 使用频率 |
|---|---|---|
| 成员内部类 | 类的内部 | 少用 |
| 静态内部类 | 类的内部 + static | 较常用 |
| 局部内部类 | 方法里面 | 很少见 |
| 匿名内部类 | new 接口() { ... } | ⭐ 历史常用(被 Lambda 取代) |
最经典的例子:以前写"按钮点击事件"必须用匿名内部类:
// Java 8 之前:匿名内部类
button.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
System.out.println("按钮被点了!");
}
});
// Java 8+:用 Lambda 替代(更简洁,效果一样)
button.addActionListener(e -> System.out.println("按钮被点了!"));
现阶段只要知道:内部类是"类中类",匿名内部类曾经是回调的主流写法,现在基本被 Lambda 替代了(Lesson 08 详讲)。
6
equals() 和 hashCode() ⭐ 面试必问
💡 动机:== 比的是地址,对象内容比较要用 equals
先看一个"反直觉"的例子:
Student s1 = new Student("张三", 20);
Student s2 = new Student("张三", 20);
System.out.println(s1 == s2); // ❌ false(两个不同对象,地址不同)
System.out.println(s1.equals(s2)); // ❌ 默认也是 false(Object.equals 就是 ==)
地址: 0x001
s1 → {name: "张三", age: 20}
地址: 0x002
s2 → {name: "张三", age: 20}
明明"长得一样",怎么就不相等?因为 Object.equals 默认就是 ==。我们要重写它:
import java.util.Objects;
public class Student {
private String name;
private int age;
public Student(String name, int age) {
this.name = name;
this.age = age;
}
// 重写 equals:内容相等就算相等
@Override
public boolean equals(Object o) {
if (this == o) return true; // 是同一个对象?
if (!(o instanceof Student)) return false; // 类型不对?
Student s = (Student) o; // 转型
return age == s.age && Objects.equals(name, s.name); // 字段比较
}
// 重写 hashCode:equals 相等 → hashCode 必须相等
@Override
public int hashCode() {
return Objects.hash(name, age);
}
}
点下面的按钮,体验 == vs equals:
s1
new Student("张三", 20)
hashCode: —
s2
new Student("张三", 20)
hashCode: —
// s1 和 s2 是两个不同对象,但"内容"都是 张三/20
// 点上面按钮看不同比较方式的结果
⭐ 面试必背的契约:
- equals 相等 → hashCode 必须相等(这条铁律)
- hashCode 相等 → equals 不一定相等(可能"哈希冲突")
- 放进 HashMap / HashSet 的对象必须重写这两个方法
你的作业(分步走)
- static 计数器:写个
Counter类,用 static 字段记录创建了多少个对象,每 new 一次 +1,提供静态方法getCount()查看 - final 常量:写个
AppConfig类,定义 3 个public static final常量(比如APP_NAME、VERSION、MAX_USERS) - 构造器重载 + this():给
Student写两个构造器(Student(String name)和Student(String name, int age)),让简化的那个通过this()调用完整的那个 - 重写 equals + hashCode:给
User类(有username和email字段)重写两个方法,要求 username 和 email 都相同的两个 User 才算相等 - 把代码贴给我,我帮你 review