类与对象
本章定位:理解"类"是蓝图、"对象"是产品的核心隐喻。掌握
class定义语法、new的对象创建流程、JVM 堆栈内存模型,以及匿名对象的适用场景。这是面向对象编程的物理基础——不理解内存,就无法写出正确的 Java。
从现实世界到代码世界
周五下午,实习僧小崔在飞翔科技工位上对着屏幕发呆。HR 部门要求他录入 300 名员工信息,他正打算写 300 个变量:
String name1 = "张三";
int age1 = 28;
double salary1 = 15000;
String name2 = "李四";
// ... 还有 298 个
坐在旁边的白歌一把按住他的键盘:"小崔,你要用这种方式写到明天吗?我们用一个 class 把所有属性打包在一起,然后用 new 来创建每个员工不就完了?"
类(Class) 是对一类事物的抽象描述,是创建对象的模板;对象(Object) 是类的具体实例,是真实存在于内存中的实体。两者的关系就像"汽车设计图纸"与"你家车库里的那辆宝马"。
类的定义
语法格式
[修饰符] class 类名 {
// 1. 成员变量(属性/字段):描述对象的状态
[修饰符] 数据类型 变量名;
// 2. 构造方法:创建对象时初始化
[修饰符] 类名(参数列表) { ... }
// 3. 成员方法:描述对象的行为
[修饰符] 返回类型 方法名(参数列表) { ... }
}
定义语法表
| 组成部分 | 说明 | 是否必须 | 示例 |
|---|---|---|---|
class 关键字 | 声明这是一个类 | 是 | class Employee |
| 类名 | 标识符,大驼峰命名 | 是 | Employee、Student |
| 成员变量 | 描述对象的属性/状态 | 否(可以有零个) | private long id; |
| 构造方法 | 创建对象时初始化成员变量 | 否(编译器自动生成默认构造器) | public Employee() {} |
| 成员方法 | 描述对象的行为 | 否(可以有零个) | public void work() {} |
对象的创建
new 关键字的工作流程
new 关键字在 JVM 中做了三件事:
- 堆上分配内存:在堆内存中为新对象开辟空间
- 初始化成员变量:将成员变量赋默认值(
int → 0,引用类型→ null) - 执行构造方法:调用对应的构造方法完成自定义初始化
- 返回引用值:将对象在堆中的地址返回给栈上的引用变量
操作前 vs 操作后
操作前(面向过程思维):
// 小崔最初的做法:用零散的变量管理员工数据
public class EmployeeManager {
public static void main(String[] args) {
// 每个员工的三个属性在不同数组中的索引必须对应 —— 极易出错!
String[] names = {"大翔", "白歌", "小崔"};
long[] ids = {1001, 1002, 1003};
double[] salaries = {50000, 35000, 8000};
// 打印第二个员工的信息——需要跨三个数组取数据
System.out.println("姓名:" + names[1]
+ " 工号:" + ids[1]
+ " 薪资:" + salaries[1]);
}
}
操作后(面向对象):
// ✅ 将数据和行为封装在一个类中
public class Employee {
private long id;
private String name;
private double salary;
// 构造方法
public Employee(long id, String name, double salary) {
this.id = id;
this.name = name;
this.salary = salary;
}
// 打印自身信息的行为也封装在类内部
public void showInfo() {
System.out.println("姓名:" + name
+ " 工号:" + id
+ " 薪资:" + salary);
}
}
// 使用方代码简洁清晰
public class EmployeeManager {
public static void main(String[] args) {
Employee emp1 = new Employee(1001, "大翔", 50000);
Employee emp2 = new Employee(1002, "白歌", 35000);
Employee emp3 = new Employee(1003, "小崔", 8000);
emp2.showInfo(); // 一个方法调用,内部自动组织好所有属性
}
}
运行输出:
姓名:白歌 工号:1002 薪资:35000.0
成员变量与局部变量
这两种变量是面试中的"送分题",但很多同学在代码审查时被孔蓝抓住过低级错误。
| 对比维度 | 成员变量 | 局部变量 |
|---|---|---|
| 定义位置 | 类中、方法外 | 方法内、代码块内、方法参数列表中 |
| 存储位置 | 堆内存(在对象内部) | 栈内存(在栈帧的局部变量表中) |
| 生命周期 | 随对象创建而创建,随对象被 GC 回收而消亡 | 随方法调用而创建,随方法结束而销毁 |
| 默认值 | 有默认值(int→0,引用→null) | 无默认值,必须显式初始化后才能使用 |
| 修饰符 | 可使用 public/private/protected/static/final 等 | 只能用 final |
| 内存分配时机 | new 对象时 | 方法被调用时 |
对象引用与内存模型
核心概念:引用 = 遥控器
在 Java 中,通过 new 创建的变量实际上是引用(reference),不是对象本身。引用就像电视遥控器——遥控器可以换(重新赋值),也可以按按钮(调用方法),频道变了(修改对象状态)电视机(堆上的对象)也跟着变。
// 以下代码揭示了引用的本质
Employee empA = new Employee(1001, "大翔", 50000); // empA 指向堆上的对象 A
Employee empB = empA; // empB 复制了 empA 的引用值
// 现在 empA 和 empB 指向同一个对象!
empB.setSalary(99999); // 通过 empB 修改
System.out.println(empA.getSalary()); // empA 看到的也是 99999
面试常考:以下代码输出什么?
Employee a = new Employee(1, "A", 100); Employee b = a; a = new Employee(2, "B", 200); System.out.println(b.getName()); // 输出 "A",因为 b 仍然指向原来的对象
匿名对象
匿名对象是没有引用变量指向的对象,创建后只能使用一次。
// 有名对象:可通过 emp 反复访问
Employee emp = new Employee(1001, "大翔", 50000);
emp.showInfo();
emp.showInfo(); // 可以多次调用
// 匿名对象:一次性使用
new Employee(1002, "白歌", 35000).showInfo();
// 这行执行完后,这个 Employee 对象就没有引用指向它了
// GC 会在适当时机回收这块内存
匿名对象的适用场景
| 场景 | 代码示例 | 说明 |
|---|---|---|
| 方法参数传参 | printEmployee(new Employee(1003, "小崔", 8000)); | 不需要复用该对象 |
| 方法返回值 | return new Employee(1004, "黄俪", 12000); | 调用方会接收返回值 |
| 链式调用 | new EmployeeService().process(new Employee(...)); | 不常使用,注意 NullPointerException |
| 仅调用一次方法 | new Employee(1005, "孔蓝", 15000).showInfo(); | 之后不再需要此对象 |
完整示例:飞翔科技员工管理系统
场景
大翔要求小崔实现一个 Employee 类,支持工号、姓名、薪资三个属性,并能将信息打印输出。孔蓝负责编写测试代码验证功能。
/**
* 飞翔科技 —— 员工类
* 演示:类的定义、对象的创建、成员变量与局部变量、匿名对象
*/
public class Employee {
// ========== 成员变量 ==========
private long id; // 工号
private String name; // 姓名
private double salary; // 薪资
// ========== 构造方法 ==========
public Employee(long id, String name, double salary) {
this.id = id;
this.name = name;
this.salary = salary;
}
// ========== 成员方法 ==========
public void showInfo() {
// bonus 是局部变量 —— 存储在栈中,方法结束即销毁
double bonus = salary * 0.2; // 20% 年终奖
System.out.println("===== 飞翔科技员工信息 =====");
System.out.println("工号: " + id);
System.out.println("姓名: " + name);
System.out.println("月薪: " + salary);
System.out.println("年终奖: " + bonus);
}
// 每年加薪的方法
public void raiseSalary(double percent) {
// percent 是局部变量(方法参数也是局部变量)
this.salary = this.salary * (1 + percent);
}
// --- getter / setter ---
public long getId() { return id; }
public void setId(long id) { this.id = id; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public double getSalary() { return salary; }
public void setSalary(double salary) { this.salary = salary; }
}
/**
* 测试类 —— 孔蓝编写
*/
public class EmployeeTest {
public static void main(String[] args) {
// ---------- 有名对象 ----------
Employee emp1 = new Employee(1001, "大翔", 50000);
Employee emp2 = new Employee(1002, "白歌", 35000);
Employee emp3 = new Employee(1003, "小崔", 8000);
emp1.showInfo();
emp2.showInfo();
emp3.showInfo();
System.out.println("\n--- 给大翔加薪 10% ---");
emp1.raiseSalary(0.10);
emp1.showInfo();
// ---------- 匿名对象 ----------
System.out.println("\n--- 临时打印一个访客信息 ---");
new Employee(9999, "访客", 0).showInfo();
// ---------- 引用赋值演示 ----------
System.out.println("\n--- 引用赋值:黄俪和孔蓝是同一个对象? ---");
Employee huangli = new Employee(1004, "黄俪", 12000);
Employee konglan = huangli; // konglan 指向 huangli 的对象
konglan.setName("黄俪(已被孔蓝改名)"); // 通过 konglan 修改
System.out.println("huangli.getName() = " + huangli.getName());
// 输出:黄俪(已被孔蓝改名) —— 因为 huangli 和 konglan 指向同一个对象
}
}
运行输出:
===== 飞翔科技员工信息 =====
工号: 1001
姓名: 大翔
月薪: 50000.0
年终奖: 10000.0
===== 飞翔科技员工信息 =====
工号: 1002
姓名: 白歌
月薪: 35000.0
年终奖: 7000.0
===== 飞翔科技员工信息 =====
工号: 1003
姓名: 小崔
月薪: 8000.0
年终奖: 1600.0
--- 给大翔加薪 10% ---
===== 飞翔科技员工信息 =====
工号: 1001
姓名: 大翔
月薪: 55000.00000000001
年终奖: 11000.000000000002
--- 临时打印一个访客信息 ---
===== 飞翔科技员工信息 =====
工号: 9999
姓名: 访客
月薪: 0.0
年终奖: 0.0
--- 引用赋值:黄俪和孔蓝是同一个对象? ---
huangli.getName() = 黄俪(已被孔蓝改名)
易错场景
反例一:局部变量未初始化
// ❌ 错误:成员变量有默认值,但局部变量没有
public class LocalVarError {
// 成员变量 —— 默认值 0
int memberVar;
public void test() {
int localVar; // 局部变量 —— 未初始化
System.out.println(memberVar); // ✅ 输出 0(默认值)
System.out.println(localVar); // ❌ 编译错误:可能尚未初始化变量 localVar
}
}
纠正:
public void test() {
int localVar = 0; // ✅ 必须显式赋值
System.out.println(localVar);
}
反例二:空引用调用方法(NullPointerException)
// ❌ 错误:小崔忘记初始化引用变量
public class NullPointerDemo {
public static void main(String[] args) {
Employee emp = null; // emp 不指向任何对象
emp.showInfo(); // ❌ NullPointerException
}
}
运行输出:
Exception in thread "main" java.lang.NullPointerException
at NullPointerDemo.main(NullPointerDemo.java:4)
纠正:
// ✅ 正确:先 new 再使用
Employee emp = new Employee(1003, "小崔", 8000);
emp.showInfo(); // ✅ 正常输出
反例三:混淆成员变量与局部变量
// ❌ 错误:在同一个方法内,局部变量"遮蔽"了成员变量
public class ShadowDemo {
int x = 10; // 成员变量
public void print() {
int x = 20; // 局部变量 —— 与成员变量同名
System.out.println(x); // 输出 20 —— 局部变量优先!
}
}
// ✅ 正确:使用 this 明确引用成员变量
public class ShadowDemo {
int x = 10;
public void print() {
int x = 20;
System.out.println(x); // 20 —— 局部变量
System.out.println(this.x); // 10 —— 成员变量(通过 this)
}
}
面试考点
Q1:成员变量和局部变量有什么区别?
① 存储位置:成员变量在堆内存中(对象内部),局部变量在栈内存中(栈帧的局部变量表内); ② 默认值:成员变量有默认值(
int→0,double→0.0,引用→null),局部变量没有默认值,必须显式初始化; ③ 生命周期:成员变量随对象创建而诞生、随对象被 GC 回收而消亡,局部变量随方法进入而创建、随方法退出而销毁; ④ 修饰符:成员变量可加public/private/protected/static/final等,局部变量只能加final。
Q2:Employee emp = new Employee() 在内存中做了什么?
① JVM 在堆中为 Employee 对象分配内存;② 成员变量被赋予默认值(
id=0, name=null, salary=0.0);③ 执行构造方法中的初始化代码;④ 将堆中对象的内存地址返回并赋值给栈上的引用变量emp。
Q3:匿名对象的优缺点是什么?
优点:代码简洁,适用于"用后即弃"的场景(如方法传参、匿名对象调用单次方法);缺点:无法复用,创建后没有引用指向,使用过多可能增加 GC 压力(短期大量创建匿名对象会增加 Young GC 频率)。
Q4:以下代码创建了几个对象?
Employee e1 = new Employee(1, "A", 100);
Employee e2 = new Employee(2, "B", 200);
e2 = e1;
2 个对象。第 1 行创建了
Employee(1, "A", 100),第 2 行创建了Employee(2, "B", 200)。第 3 行只是把e2的引用指向了第一个对象,第二个对象失去引用后将在 GC 时被回收。
小崔的笔记:"引用不是对象本身,而是对象的'门牌号'。把门牌号复制给别人,不等于复制了一栋楼。new 了才能住人,null 的房子敲门会崩。"