本文是系列第三篇,深入剖析类与对象、封装继承多态等关键概念,助力构建面向对象编程思维体系
一、引言
面向对象编程(OOP)是 Java 语言的核心编程范式,通过创建 ** 类(Class)和对象(Object)** 来模拟现实世界中的实体及其交互关系。熟练掌握 OOP 的三大特性 —— 封装、继承和多态,是迈向 Java 进阶开发的关键一步。在本文中,我们将借助大量实际案例,深入解析 OOP 的核心概念,为后续学习 Java 框架和进行企业级开发筑牢根基。
二、类与对象:OOP 的基石
2.1 类的定义与实例化
类的结构
类是对现实世界中一类具有相同属性和行为的事物的抽象描述。以Student类为例:
public class Student {
// 成员变量(属性)
private String name;
private int age;
private boolean isGraduated;
// 成员方法(行为)
public void study() {
System.out.println(name + "正在学习Java");
}
public void setName(String n) {
name = n;
}
}
上述代码定义了一个Student类,其中包含了姓名、年龄和毕业状态等属性,以及学习和设置姓名的方法。
对象创建
对象是类的具体实例。通过new关键字创建对象,并调用其方法:
// 实例化对象
Student stu = new Student();
// 调用方法
stu.setName("张三");
stu.study(); // 输出:张三正在学习Java
内存模型
在 Java 中,对象存储在堆内存中,而对象的引用变量(如stu)则存储在栈内存中。当执行new Student()时,会在堆内存中开辟一块空间来存储Student对象的属性值,栈内存中的stu变量则指向堆内存中的这个对象实例。
2.2 构造方法与初始化块
构造方法(Constructor)
构造方法用于初始化对象的成员变量。它具有以下特点:
- 方法名与类名完全相同,且没有返回值类型(包括void也不能有)。
- 可以进行重载,即通过不同的参数列表来定义多个构造方法。
- 如果类中没有显式定义构造方法,编译器会自动生成一个无参构造方法。
public class Student { // 有参构造 public Student(String name, int age) { this.name = name; // this指向当前正在创建的对象 this.age = age; } // 无参构造(显式定义) public Student() {} }
初始化块
初始化块分为静态初始化块和实例初始化块:
- 静态初始化块:使用static关键字修饰,在类加载时执行,仅执行一次,通常用于初始化静态变量。
static { System.out.println("Student类已加载"); }
- 实例初始化块:没有修饰符,在创建对象时执行,先于构造方法执行,用于初始化对象的实例变量。
{ isGraduated = false; // 初始化默认值 }
三、封装:数据的保护层
3.1 访问修饰符
访问修饰符用于控制类、成员变量和方法的访问权限:
修饰符 |
类内 |
同包 |
子类(不同包) |
其他包 |
使用场景 |
private |
✔️ |
❌ |
❌ |
❌ |
类内私有数据(如成员变量) |
default(无) |
✔️ |
✔️ |
❌ |
❌ |
同包内可见(默认修饰符) |
protected |
✔️ |
✔️ |
✔️ |
❌ |
子类跨包继承时可见 |
public |
✔️ |
✔️ |
✔️ |
✔️ |
对外公开的类 / 方法 / 变量 |
例如,在BankAccount类中,通过将balance变量设置为private,并提供getBalance和setBalance方法来控制对余额的访问,实现数据校验:
public class BankAccount {
private double balance;
// 对外提供访问接口
public double getBalance() {
return balance;
}
public void setBalance(double balance) {
if (balance >= 0) { // 数据校验
this.balance = balance;
}
}
}
3.2 包(Package)机制
包机制用于组织代码结构,避免命名冲突:
- 作用:将相关的类和接口组织在一起,方便管理和维护代码。
- 规范:
package com.ds.cn.entity; import com.ds.cn.util.Logger; // 跨包需导入
-
- 包名通常采用反向域名的形式,如com.ds.cn.oop。
-
- 同一包内的类无需导入即可直接使用,不同包的类需要使用import语句导入。
四、继承:代码复用的桥梁
4.1 继承语法
Java 中通过extends关键字实现类的继承,一个子类只能继承一个父类(单继承):
// 子类继承父类(单继承)
public class Dog extends Animal {
private String breed;
// 重写父类方法
@Override
public void speak() {
System.out.println("汪汪汪");
}
}
// 父类
class Animal {
protected String color;
public void eat() {
System.out.println("正在进食");
}
public void speak() { // 父类默认实现
System.out.println("动物发出声音");
}
}
4.2 关键特性
- 向上转型:子类对象可以赋值给父类引用,在运行时根据对象的实际类型调用方法,这就是动态绑定。
Animal animal = new Dog(); // 运行时类型为Dog animal.speak(); // 调用子类重写的方法(动态绑定)
- super 关键字:用于访问父类的成员,包括属性和方法。
super.eat(); // 调用父类的eat方法 super.color = "白色"; // 访问父类属性
- final 关键字:
-
- final类:不能被继承,例如String类就是final类。
-
- final方法:不能被重写。
-
- final变量:常量,其值一旦初始化就不能改变。
五、多态:同一接口,不同实现
5.1 多态实现方式
(1)方法重写(Override)
子类重新实现父类的非静态、非final方法,前提是满足 “is-a” 关系,比如Dog是一种Animal。
(2)接口实现(Interface)
类通过implements关键字实现接口,接口中的方法默认是public abstract,变量默认是public static final。
(3)抽象类继承(Abstract Class)
抽象类包含抽象方法,需要子类去实现这些抽象方法。
5.2 动态绑定机制
Java 虚拟机在运行时会根据对象的实际类型来调用方法,而不是根据引用变量的类型。例如:
Animal[] animals = {new Dog(), new Cat()};
for (Animal a : animals) {
a.speak(); // 输出:汪汪汪 / 喵喵喵(动态分派)
}
这里animals数组中存储了不同子类的对象,在遍历调用speak方法时,会根据每个对象的实际类型调用对应的方法实现。
六、抽象类与接口:设计模式的基石
6.1 抽象类(Abstract Class)
抽象类是一种不能被实例化的类,用abstract关键字修饰:
- 特点:
abstract class Shape { abstract double getArea(); // 抽象方法 public void draw() { // 具体方法 System.out.println("绘制图形"); } } class Circle extends Shape { private double radius; @Override public double getArea() { return Math.PI * radius * radius; } }
-
- 可以包含抽象方法,即只有方法声明,没有方法体,需要子类去实现。
-
- 也可以包含具体方法和构造方法。
6.2 接口(Interface)
接口是一种特殊的抽象类型,完全由抽象方法和常量组成,用interface关键字声明:
- 特点:
interface Drawable { void draw(); // 隐式抽象方法 int COLOR_RED = 1; // 隐式常量 } class Rectangle implements Drawable { @Override public void draw() { System.out.println("绘制矩形"); } }
-
- 接口中的方法默认是public abstract,可以省略不写。
-
- 接口中的变量默认是public static final常量。
-
- 一个类可以实现多个接口,弥补了 Java 单继承的局限性。
6.3 核心区别
特性 |
抽象类 |
接口 |
实例化 |
不能(需子类实现) |
不能(需实现类实例化) |
方法类型 |
可包含抽象 + 具体方法 |
只能是抽象方法(JDK8 + 支持默认方法) |
继承方式 |
单继承(extends) |
多实现(implements) |
使用场景 |
定义领域内的公共属性和行为 |
定义跨领域的通用规范 |
七、常见问题与最佳实践
7.1 封装陷阱
- 避免直接暴露成员变量:始终通过setter和getter方法来访问和修改私有成员变量,这样可以更好地控制数据的访问和修改逻辑。
- 在构造方法中初始化必要属性:确保对象创建时,关键属性都有合理的初始值,避免出现空指针异常。
7.2 继承原则
- 遵循 “is-a” 关系:子类必须在逻辑上是父类的一种,例如 “狗是动物” 是合理的继承关系,而 “狗是颜色” 则不合理。
- 优先使用组合(Composition)而非继承:当一个类 “拥有” 另一个类的功能时,应优先考虑使用组合关系,比如Car类包含Engine类,而不是让Car继承Engine。
7.3 多态设计
- 面向接口编程:将方法参数定义为接口类型,这样可以提高代码的扩展性和灵活性,使方法能够接受所有实现该接口的对象。
public void process(Drawable d) { d.draw(); // 接受所有实现Drawable的对象 }
八、总结与实战建议
核心收获
- 深刻理解类与对象的关系,熟练掌握构造方法与初始化逻辑。
- 能够熟练运用访问修饰符实现数据封装,严格遵循包管理规范。
- 掌握继承语法,理解向上转型与动态绑定机制。
- 能够准确区分抽象类与接口,并根据实际场景选择合适的设计方案。
实战练习
- 学生管理系统:定义Student类,封装姓名、成绩等信息,Teacher类继承Person类,并实现多态方法evaluate(),用于对学生进行评价。
- 图形计算程序:利用抽象类Shape和接口Printable,实现圆形、矩形等图形的面积计算与打印功能。
系列预告
下一篇将深入探讨异常处理与常用类库(如String、集合框架),结合实际案例提升代码的健壮性和开发效率。关注浩南,获取完整学习路径!
互动思考
在实际开发中,你认为 “封装” 和 “继承” 最容易被滥用的场景有哪些?如何依据设计原则来规避这些问题?欢迎在评论区分享你的见解!