LESSON 03 · 类与对象深入

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 都是参数,字段根本没被赋值
✅ 写 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() 的两个铁律:
  1. 必须出现在构造器的第一行(不能放在第二行)
  2. 只能调用一次(不能递归链式,否则会死循环)

用起来是这样的:

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)
// 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
// 点上面按钮看不同比较方式的结果
⭐ 面试必背的契约:
  1. equals 相等 → hashCode 必须相等(这条铁律)
  2. hashCode 相等 → equals 不一定相等(可能"哈希冲突")
  3. 放进 HashMap / HashSet 的对象必须重写这两个方法

你的作业(分步走)

  1. static 计数器:写个 Counter 类,用 static 字段记录创建了多少个对象,每 new 一次 +1,提供静态方法 getCount() 查看
  2. final 常量:写个 AppConfig 类,定义 3 个 public static final 常量(比如 APP_NAMEVERSIONMAX_USERS
  3. 构造器重载 + this():给 Student 写两个构造器(Student(String name)Student(String name, int age)),让简化的那个通过 this() 调用完整的那个
  4. 重写 equals + hashCode:给 User 类(有 usernameemail 字段)重写两个方法,要求 username 和 email 都相同的两个 User 才算相等
  5. 把代码贴给我,我帮你 review