第7章:面向对象(上)——类与对象
本章开始深入 Java 最核心的概念。学完它,你就能真正用 Java "造东西"了。
📌 代码说明:标注"可复制运行"的代码块包含完整的
main方法,存为对应文件名后可直接编译运行。没标注的是概念示意片段,帮助你理解,但不能直接复制运行。
一个故事开头
你过去 6 章的程序是这样的:数据散落在变量里,方法到处穿插。
String name = "小明";
int age = 20;
double score = 85.5;
void printInfo(String name, int age, double score) {
System.out.println(name + ", " + age + "岁, 分数" + score);
}现在来了第二个学生小红,你复制粘贴一遍。第三个小张,再复制一遍。
如果每个学生还有地址、课程、成绩列表呢?方法的参数会变成一场灾难。
面向对象(OOP)的解决方式:把"一个学生"的所有属性和行为打包成一个整体——叫做对象。
public class Student {
String name; // 数据(字段)
int age;
void printInfo() { // 行为(方法)
System.out.println(name + ", " + age + "岁");
}
}
// 创建对象
Student s1 = new Student();
s1.name = "小明";
s1.age = 20;
s1.printInfo(); // 小明, 20岁不再是传一大堆松散的数据——你跟一个对象说话,对象自己知道怎么做。
1. 从数组到对象的演进
💥 拆了它:想想没有 OOP 的麻烦
概念示意,不可直接运行
// 三个学生,每科成绩
String[] names = {"小明", "小红", "小张"};
int[] ages = {20, 21, 19};
int[][] scores = {{85, 90}, {92, 88}, {78, 95}};
// 要打印小明的信息
System.out.println(names[0] + " " + ages[0] + "岁");
// 有什么问题?——索引对应关系完全靠人脑记,加一个人要改三个数组如果有对象:
class Student {
String name;
int age;
int[] scores;
void printInfo() {
System.out.println(name + ", " + age + "岁");
}
}数据和方法在一起——这是 OOP 的核心思想。
2. 类和对象——设计图和实物
// 可复制运行,保存为 MilkTea.java
public class MilkTea {
String flavor; // 字段(属性)
double price;
void display() { // 方法(行为)
System.out.println(flavor + " — " + price + "元");
}
public static void main(String[] args) {
MilkTea tea1 = new MilkTea(); // new 关键字造对象
tea1.flavor = "波波奶茶";
tea1.price = 15.0;
MilkTea tea2 = new MilkTea(); // 同一张设计图,造不同的实物
tea2.flavor = "杨枝甘露";
tea2.price = 18.0;
tea1.display(); // 波波奶茶 — 15.0元
tea2.display(); // 杨枝甘露 — 18.0元
}
}一个类可以创建无数个对象,每个对象有自己的字段值。
3. 构造方法:一造出来就设好值
上面那种创建方式有点麻烦——先 new MilkTea(),再一个个设字段。
构造方法(Constructor)让你在 new 的同时直接传值:
public class MilkTea {
String flavor;
double price;
// 构造方法:名字和类名一样,没有返回值
MilkTea(String flavor, double price) {
this.flavor = flavor; // this.flavor = 对象的 flavor 字段
this.price = price; // = 右边的 flavor 是参数
}
void display() {
System.out.println(flavor + " — " + price + "元");
}
}
// 使用:创建的时候直接传参数
MilkTea t1 = new MilkTea("波波奶茶", 15.0);
MilkTea t2 = new MilkTea("杨枝甘露", 18.0);this 指"当前对象"。 当参数名和字段名冲突时,this.字段 表示对象的字段,不加 this 的是参数。
💥 拆了它:构造方法最容易踩的三个坑
坑1:写了带参构造器,无参构造器就消失了
public class Student {
String name;
Student(String name) { this.name = name; }
}
Student s = new Student(); // ❌ 编译错误!Student 没有无参构造器了Java 在你没有写任何构造器时,会自动提供一个无参构造器。但一旦你写了一个带参的,无参的那个就不存在了。要么显式加一个无参构造器:
public class Student {
String name;
Student() { } // 显式的无参构造器
Student(String name) { // 带参构造器
this.name = name;
}
}坑2:构造器不是方法——它没有返回类型,连 void 都不能写
public class Student {
String name;
void Student(String name) { // ❌ 加了 void——这是普通方法,不是构造器
this.name = name;
}
}加了 void 之后 Java 把它当普通方法处理,new Student("小明") 仍然会调无参构造器,name 没被赋值。
坑3:构造器里不要调可能会被重写的方法
这个坑要到第8章学完重写才能真正理解,先打预防针。如果构造器里调了一个被子类重写的方法,会执行子类的版本——而此时子类可能还没初始化完,结果会很奇怪。
4. static——属于类的,不属于对象的
前面所有例子里的字段和方法都属于对象——每个对象有自己的副本。
但有些东西不属于某个具体的对象,而属于类本身——那就是 static。
public class Student {
String name; // 实例字段——每个学生有自己的名字
static int totalCount; // 静态字段——所有学生共享一个计数器
Student(String name) {
this.name = name;
totalCount++; // 每创建一个学生,总人数+1
}
void sayHi() {
System.out.println("我是" + name);
}
static void printTotal() { // 静态方法—不需要对象就能调用
System.out.println("总人数:" + totalCount);
// System.out.println(name); // ❌ 静态方法不能访问实例字段!
}
}
// 使用
Student s1 = new Student("小明");
Student s2 = new Student("小红");
Student s3 = new Student("小张");
Student.printTotal(); // 总人数:3(通过类名调用)
s1.sayHi(); // 我是小明(通过对象调用)static 在 main 里的意义
main 方法必须是 static——因为 JVM 启动时还没有任何对象,必须能直接调用 main。这带来了一个重要规则:在静态方法(比如 main)里,不能直接调用实例方法(非 static 的方法)——因为没有对象让你调。
public class Demo {
void sayHello() { // 实例方法——需要对象才能调
System.out.println("你好");
}
public static void main(String[] args) {
// sayHello(); // ❌ 编译错误!静态方法里不能直接调实例方法
Demo d = new Demo();
d.sayHello(); // ✅ 先创建对象,通过对象调
}
}5. 封装——把东西藏起来
你开奶茶店,总不可能把配方单贴在大街上让所有人都看到吧?
封装(Encapsulation)就是 OOP 里"保护数据"的机制——把字段藏起来(private),只通过公开的方法(public)来访问。
// ❌ 不封装——谁都能改,甚至改成不合理的值
public class Person {
public int age;
}
Person p = new Person();
p.age = -100; // 合法——但年龄怎么能是负数?
// ✅ 封装——用 private 隐藏字段,用方法控制访问
public class Person {
private int age; // private:只有本类的方法能访问
public int getAge() {
return age;
}
public void setAge(int age) {
if (age < 0 || age > 150) {
System.out.println("非法年龄:" + age);
return;
}
this.age = age;
}
}
Person p = new Person();
// p.age = -100; // ❌ 编译错误!private 不让外部直接访问
p.setAge(20); // ✅ 通过方法设置——可以加校验
p.setAge(-100); // 非法年龄:-100(被拦截了)关于 getter/setter:字段通常设为 private。但不是说每个字段都要配一对 getter/setter——需要外部读取的才加 getter,需要外部修改的才加 setter。比如学生 ID 只需要 getter(不允许修改),年龄需要 getter + setter(带校验)。IDE 里的"Generate Getters and Setters"功能很方便,但想清楚哪些字段真的需要暴露再生成。
6. 引用与垃圾回收
引用 vs 对象
Student s1 = new Student("小明");
Student s2 = s1; // s2 获得了 s1 的引用副本——指向同一个对象
s2.name = "小张";
System.out.println(s1.name); // "小张"——s1 和 s2 是同一个对象💥 拆了它:看看引用的效果
public class RefDemo {
public static void main(String[] args) {
Student s = new Student("小明");
changeName(s);
System.out.println(s.name); // 猜猜是"小明"还是"小张"?
}
static void changeName(Student stu) {
stu.name = "小张"; // 改的是 stu 指向的对象,和 s 指向的是同一个
}
}结果是"小张"。因为 Java 里引用传递的是引用值的副本——两份副本指向同一个对象。
null 和 GC
Student s = new Student("小明");
s = new Student("小红");
// "小明"那个对象没人引用了——变成垃圾
// Java 的垃圾回收器会在某个时候自动回收它Student s = null; // 引用指向"空"
// s.sayHi(); // ❌ NullPointerException7. 完整例子:奶茶店员工管理系统
public class Employee {
private String name;
private String position;
private double salary;
private static int totalEmployees = 0;
// 构造方法
public Employee(String name, String position, double salary) {
this.name = name;
this.position = position;
setSalary(salary);
totalEmployees++;
}
// Getter
public String getName() { return name; }
public String getPosition() { return position; }
public double getSalary() { return salary; }
// Setter(带校验)
public void setSalary(double salary) {
if (salary < 0) {
System.out.println("工资不能为负数");
return;
}
this.salary = salary;
}
// 静态方法
public static void printTotal() {
System.out.println("总员工数: " + totalEmployees);
}
// 普通方法
public void printInfo() {
System.out.println(name + " | " + position + " | " + salary + "元/月");
}
public static void main(String[] args) {
Employee e1 = new Employee("小明", "店长", 8000);
Employee e2 = new Employee("小红", "调茶师", 5000);
Employee e3 = new Employee("小张", "收银员", 4500);
e1.printInfo();
e2.printInfo();
e3.printInfo();
Employee.printTotal(); // 总员工数: 3
}
}本章小结
- 类 = 设计图,对象 = 实物——用
new创建对象 - 字段存数据、方法定义行为——数据和操作在一起
- 构造方法——创建对象的同时初始化,名字跟类名一样
this——指当前对象,用来区分参数和字段static——属于类的,不属于对象,通过类名调用- 封装——
private字段 +publicgetter/setter,保护数据 - 引用——多个引用可以指向同一个对象,
null是"啥也不指" - GC——没被引用的对象会被自动回收
✅ 验收标准
完成本章后,你应该能:
- [ ] 定义一个类,包含字段、构造器、方法
- [ ] 用
new创建对象并调用方法 - [ ] 区分实例成员和
static成员 - [ ] 用
private+ getter/setter 实现封装 - [ ] 理解对象引用赋值和 GC
📌 常见卡点
- 构造器的名字必须和类名完全一致——大小写都要一样
- 定义了有参构造器后默认无参构造器消失——需要手动写
- this 用于区分参数和字段
- 对象赋值是引用复制——`a = b` 后改 a 也会影响 b
🔜 现在不需要理解
- 构造器重载的 this() 调用
- 内部类和匿名类
- finalize——已被弃用
- 单例模式等设计模式
🧪 练习
1. 判断题:类和对象谁是设计图?谁是实物?
2. 改一改:给本章的 MilkTea 类增加一个 sweetness 字段(0-10 的整数),在 setter 里做范围校验。
3. 造一个类:定义一个 Book 类,包含 title、author、isBorrowed(boolean)三个字段。实现 borrow() 方法(如果已借出则提示"已被借出",否则设为已借出)和 returnBook() 方法。在 main 里测试。