1. 继承:代码复用的艺术
继承是面向对象编程中最具标志性的特性之一,它允许我们基于现有类创建新类。想象一下生物界的遗传机制——孩子会继承父母的特征,同时又有自己的独特性。编程中的继承也是如此。
1.1 继承的基本语法
在Java中,我们使用extends关键字实现继承:
java复制class Animal {
void eat() {
System.out.println("动物在进食");
}
}
class Dog extends Animal {
void bark() {
System.out.println("汪汪叫");
}
}
这里Dog类继承了Animal类的所有非私有成员。这意味着Dog对象可以直接调用eat()方法,就像这个方法是在Dog类中定义的一样。
注意:Java不支持多继承,一个类只能直接继承一个父类。这是为了避免"菱形继承"问题,即当两个父类有同名方法时产生的歧义。
1.2 方法重写与super关键字
当子类需要修改父类方法的行为时,可以使用方法重写(Override):
java复制class Dog extends Animal {
@Override
void eat() {
System.out.println("狗在啃骨头");
}
void bark() {
System.out.println("汪汪叫");
}
}
如果想在子类中访问父类的被重写方法,可以使用super关键字:
java复制@Override
void eat() {
super.eat(); // 先调用父类的eat方法
System.out.println("然后开始啃骨头");
}
1.3 继承的构造方法
构造方法不会被继承,但子类构造方法必须调用父类构造方法。如果没有显式调用,编译器会自动插入对父类无参构造方法的调用:
java复制class Animal {
Animal() {
System.out.println("Animal构造方法");
}
}
class Dog extends Animal {
Dog() {
// 这里编译器会自动插入super();
System.out.println("Dog构造方法");
}
}
如果父类没有无参构造方法,或者你想调用特定的父类构造方法,必须显式使用super():
java复制class Animal {
Animal(String name) {
System.out.println("Animal叫" + name);
}
}
class Dog extends Animal {
Dog() {
super("旺财"); // 必须显式调用
System.out.println("Dog构造方法");
}
}
1.4 继承的深入理解
继承不仅仅是代码复用,更重要的是它建立了"是一个(is-a)"的关系。当我们说"Dog extends Animal"时,我们在声明"狗是一种动物"。这种关系是继承是否合适的判断标准。
在实际开发中,过度使用继承会导致代码僵化。一个常见原则是:优先使用组合而非继承。只有在确实存在"是一个"关系,并且需要利用多态特性时,才使用继承。
2. 封装:保护你的数据
封装是面向对象编程的另一个核心概念,它隐藏对象的内部细节,只暴露必要的接口。这就像我们使用手机——我们不需要知道内部电路如何工作,只需要知道如何使用触摸屏和按钮。
2.1 访问修饰符
Java提供了四种访问级别:
private:仅当前类可见default(不加修饰符):同一包内可见protected:同一包内及子类可见public:所有类可见
良好的封装实践通常将字段设为private,通过public方法提供访问:
java复制class Person {
private String name;
private int age;
public String getName() {
return name;
}
public void setName(String name) {
if(name == null || name.trim().isEmpty()) {
throw new IllegalArgumentException("名字不能为空");
}
this.name = name;
}
// 其他getter/setter
}
2.2 封装的好处
- 数据保护:防止外部代码随意修改内部数据
- 灵活性:可以修改内部实现而不影响外部代码
- 易于维护:验证逻辑集中在一处
- 隐藏复杂性:用户只需要知道"做什么",不需要知道"怎么做"
2.3 JavaBean规范
标准的Java类通常遵循JavaBean规范:
- 私有字段
- 公共的getter/setter方法
- 无参构造方法
- 可序列化(实现Serializable接口)
虽然现代Java开发中不一定严格遵循这些规范,但getter/setter的模式仍然被广泛使用。
2.4 封装的进阶技巧
- 不可变对象:将类设为final,所有字段设为private final,不提供setter方法:
java复制public final class ImmutablePoint {
private final int x;
private final int y;
public ImmutablePoint(int x, int y) {
this.x = x;
this.y = y;
}
// 只有getter,没有setter
public int getX() { return x; }
public int getY() { return y; }
}
- 建造者模式:当构造复杂对象时,使用建造者模式可以保持对象的不可变性同时提供灵活的构造方式。
3. 多态:同一接口,不同实现
多态是面向对象编程中最强大的特性之一,它允许我们使用统一的接口操作不同类型的对象。想象动物园的管理员给不同动物喂食——他不需要知道具体是什么动物,只需要知道它们都能"吃"。
3.1 多态的实现方式
在Java中,多态主要通过以下方式实现:
- 方法重写(Override)
- 接口实现
- 方法重载(Overload)
3.1.1 方法重写示例
java复制class Animal {
void makeSound() {
System.out.println("动物发出声音");
}
}
class Dog extends Animal {
@Override
void makeSound() {
System.out.println("汪汪汪");
}
}
class Cat extends Animal {
@Override
void makeSound() {
System.out.println("喵喵喵");
}
}
public class Test {
public static void main(String[] args) {
Animal myAnimal = new Animal(); // Animal对象
Animal myDog = new Dog(); // Dog对象
Animal myCat = new Cat(); // Cat对象
myAnimal.makeSound(); // 输出: 动物发出声音
myDog.makeSound(); // 输出: 汪汪汪
myCat.makeSound(); // 输出: 喵喵喵
}
}
3.1.2 接口多态示例
java复制interface Shape {
double area();
}
class Circle implements Shape {
private double radius;
Circle(double r) { radius = r; }
@Override
public double area() {
return Math.PI * radius * radius;
}
}
class Square implements Shape {
private double side;
Square(double s) { side = s; }
@Override
public double area() {
return side * side;
}
}
public class Test {
public static void printArea(Shape shape) {
System.out.println("面积: " + shape.area());
}
public static void main(String[] args) {
printArea(new Circle(5)); // 输出圆的面积
printArea(new Square(4)); // 输出正方形的面积
}
}
3.2 多态的类型
- 编译时多态:通过方法重载实现,在编译时确定调用哪个方法
- 运行时多态:通过方法重写实现,在运行时根据实际对象类型确定调用哪个方法
3.3 多态的应用场景
- 框架设计:框架定义接口,用户实现具体逻辑
- 插件系统:通过接口统一插件行为
- 算法抽象:同一算法可以处理多种数据类型
- GUI事件处理:不同控件对同一事件有不同的响应
3.4 多态的实现原理
Java通过动态绑定(Dynamic Binding)实现运行时多态。当调用一个方法时,JVM会检查对象的实际类型(而不是引用类型),然后调用该类型对应的方法。这个过程是通过虚方法表(Virtual Method Table)实现的。
4. 面向对象设计原则
理解了三大特性后,我们需要了解如何合理运用它们。以下是五个重要的面向对象设计原则(SOLID原则):
4.1 单一职责原则(SRP)
一个类应该只有一个引起它变化的原因。换句话说,一个类应该只负责一项职责。
违反SRP的例子:
java复制class Employee {
void calculatePay() { /* 计算薪资 */ }
void saveToDatabase() { /* 保存到数据库 */ }
void generateReport() { /* 生成报告 */ }
}
遵循SRP的改进:
java复制class Employee {
void calculatePay() { /* 计算薪资 */ }
}
class EmployeeRepository {
void save(Employee emp) { /* 保存到数据库 */ }
}
class ReportGenerator {
void generate(Employee emp) { /* 生成报告 */ }
}
4.2 开闭原则(OCP)
软件实体应该对扩展开放,对修改关闭。这意味着我们应该能够添加新功能而不修改现有代码。
实现方式:
- 使用抽象(抽象类/接口)
- 使用多态
- 依赖倒置
4.3 里氏替换原则(LSP)
子类必须能够替换它们的父类而不影响程序的正确性。这意味着子类不应该破坏父类的行为约定。
违反LSP的例子:
java复制class Rectangle {
protected int width, height;
void setWidth(int w) { width = w; }
void setHeight(int h) { height = h; }
int area() { return width * height; }
}
class Square extends Rectangle {
@Override
void setWidth(int w) {
super.setWidth(w);
super.setHeight(w);
}
@Override
void setHeight(int h) {
super.setWidth(h);
super.setHeight(h);
}
}
这里Square改变了Rectangle的行为约定(长宽可以独立设置),违反了LSP。
4.4 接口隔离原则(ISP)
客户端不应该被迫依赖它们不使用的接口。应该将庞大的接口拆分为更小、更具体的接口。
违反ISP的例子:
java复制interface Worker {
void work();
void eat();
void sleep();
}
class Robot implements Worker {
void work() { /* 机器人工作 */ }
void eat() { throw new UnsupportedOperationException(); }
void sleep() { throw new UnsupportedOperationException(); }
}
遵循ISP的改进:
java复制interface Workable {
void work();
}
interface Eatable {
void eat();
}
interface Sleepable {
void sleep();
}
class Robot implements Workable {
void work() { /* 机器人工作 */ }
}
class Human implements Workable, Eatable, Sleepable {
// 实现所有方法
}
4.5 依赖倒置原则(DIP)
高层模块不应该依赖低层模块,两者都应该依赖抽象。抽象不应该依赖细节,细节应该依赖抽象。
违反DIP的例子:
java复制class LightBulb {
void turnOn() { /* 开灯 */ }
void turnOff() { /* 关灯 */ }
}
class Switch {
private LightBulb bulb;
Switch(LightBulb bulb) {
this.bulb = bulb;
}
void operate() {
// 操作灯泡
}
}
遵循DIP的改进:
java复制interface Switchable {
void turnOn();
void turnOff();
}
class LightBulb implements Switchable {
void turnOn() { /* 开灯 */ }
void turnOff() { /* 关灯 */ }
}
class Fan implements Switchable {
void turnOn() { /* 开风扇 */ }
void turnOff() { /* 关风扇 */ }
}
class Switch {
private Switchable device;
Switch(Switchable device) {
this.device = device;
}
void operate() {
// 操作设备
}
}
5. 实战:设计一个简单的图形系统
让我们综合运用继承、封装和多态,设计一个简单的图形系统。
5.1 定义基础接口和抽象类
java复制// 所有图形的基接口
interface Shape {
double area();
double perimeter();
void draw();
}
// 可填充图形的接口
interface Fillable {
void setFillColor(String color);
String getFillColor();
}
// 抽象基类,提供公共实现
abstract class AbstractShape implements Shape {
protected String name;
AbstractShape(String name) {
this.name = name;
}
@Override
public void draw() {
System.out.println("绘制 " + name);
}
}
5.2 实现具体图形类
java复制// 圆形
class Circle extends AbstractShape implements Fillable {
private double radius;
private String fillColor;
Circle(String name, double radius) {
super(name);
this.radius = radius;
}
@Override
public double area() {
return Math.PI * radius * radius;
}
@Override
public double perimeter() {
return 2 * Math.PI * radius;
}
@Override
public void setFillColor(String color) {
this.fillColor = color;
}
@Override
public String getFillColor() {
return fillColor;
}
@Override
public void draw() {
super.draw();
System.out.println("这是一个圆形,半径: " + radius);
if(fillColor != null) {
System.out.println("填充颜色: " + fillColor);
}
}
}
// 矩形
class Rectangle extends AbstractShape implements Fillable {
private double width, height;
private String fillColor;
Rectangle(String name, double w, double h) {
super(name);
this.width = w;
this.height = h;
}
@Override
public double area() {
return width * height;
}
@Override
public double perimeter() {
return 2 * (width + height);
}
@Override
public void setFillColor(String color) {
this.fillColor = color;
}
@Override
public String getFillColor() {
return fillColor;
}
@Override
public void draw() {
super.draw();
System.out.println("这是一个矩形,宽: " + width + ", 高: " + height);
if(fillColor != null) {
System.out.println("填充颜色: " + fillColor);
}
}
}
5.3 使用图形系统
java复制public class GraphicsSystem {
public static void main(String[] args) {
Shape[] shapes = new Shape[3];
shapes[0] = new Circle("大圆", 10);
shapes[1] = new Rectangle("方框", 5, 8);
// 设置填充色
if(shapes[0] instanceof Fillable) {
((Fillable)shapes[0]).setFillColor("红色");
}
// 多态调用
for(Shape shape : shapes) {
if(shape != null) {
shape.draw();
System.out.println("面积: " + shape.area());
System.out.println("周长: " + shape.perimeter());
System.out.println("------------");
}
}
}
}
这个简单的图形系统展示了面向对象编程的三大特性:
- 继承:Circle和Rectangle继承AbstractShape
- 封装:将图形属性和行为封装在类中
- 多态:通过Shape接口统一处理不同类型的图形
6. 常见问题与最佳实践
6.1 继承 vs 组合
什么时候用继承?
- 存在明确的"是一个(is-a)"关系
- 需要利用多态特性
- 子类确实是父类的特殊化
什么时候用组合?
- 存在"有一个(has-a)"关系
- 只需要复用代码而不需要多态
- 可能会改变的关系
组合示例:
java复制class Engine {
void start() { /* 启动引擎 */ }
}
// 使用组合而不是继承
class Car {
private Engine engine;
Car() {
engine = new Engine();
}
void start() {
engine.start();
}
}
6.2 多态的性能考虑
多态方法调用比非多态调用稍慢,因为需要在运行时解析方法。但在现代JVM中,这种差异通常可以忽略不计。JVM使用内联缓存(Inline Cache)等技术优化多态调用。
6.3 防止继承滥用
- final类:如果一个类不应该被继承,声明为final
- private构造方法:防止子类化(用于工具类)
- 组合优先:在不确定是否应该使用继承时,优先考虑组合
6.4 接口与抽象类的选择
使用接口当:
- 需要定义行为契约
- 需要多重继承
- 需要定义API
使用抽象类当:
- 需要提供部分实现
- 需要共享公共代码
- 需要定义模板方法
6.5 封装的最佳实践
- 最小化访问权限:从private开始,只在必要时放宽
- 不可变性:尽可能使对象不可变
- 防御性拷贝:对于可变对象,返回拷贝而不是引用
- 验证:在setter方法中进行参数验证
7. Java特有的面向对象特性
7.1 内部类
Java支持四种内部类:
- 成员内部类
- 静态内部类
- 局部内部类
- 匿名内部类
匿名内部类示例(用于事件监听):
java复制button.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
System.out.println("按钮被点击");
}
});
7.2 枚举类
枚举是特殊的类,适合表示固定数量的常量:
java复制enum Day {
MONDAY, TUESDAY, WEDNESDAY,
THURSDAY, FRIDAY, SATURDAY, SUNDAY;
public boolean isWeekend() {
return this == SATURDAY || this == SUNDAY;
}
}
7.3 注解
注解是Java的元数据机制,可以用于标记代码:
java复制@Override
public String toString() {
return "重写的toString方法";
}
7.4 泛型
泛型提供了编译时类型安全:
java复制class Box<T> {
private T content;
void set(T content) { this.content = content; }
T get() { return content; }
}
8. 现代Java中的面向对象编程
8.1 记录类(Record)
Java 14引入了记录类,简化了不可变数据载体的定义:
java复制record Point(int x, int y) {
// 自动获得:
// - final字段x和y
// - 构造方法
// - equals(), hashCode(), toString()
// - getter方法x()和y() (不是getX())
}
8.2 模式匹配(instanceof增强)
Java 16改进了instanceof操作符:
java复制if(shape instanceof Circle c) {
// 可以直接使用c,不需要再强制转换
System.out.println("半径: " + c.radius());
}
8.3 密封类(Sealed Class)
Java 17引入了密封类,限制哪些类可以继承它:
java复制public sealed class Shape
permits Circle, Rectangle, Triangle {
// 基类定义
}
// 只有这三个类可以继承Shape
final class Circle extends Shape { /*...*/ }
final class Rectangle extends Shape { /*...*/ }
final class Triangle extends Shape { /*...*/ }
9. 设计模式中的三大特性应用
9.1 工厂方法模式(多态应用)
java复制interface Product {
void use();
}
class ConcreteProductA implements Product {
public void use() { System.out.println("使用A产品"); }
}
class ConcreteProductB implements Product {
public void use() { System.out.println("使用B产品"); }
}
abstract class Creator {
abstract Product createProduct();
void someOperation() {
Product product = createProduct();
product.use();
}
}
class ConcreteCreatorA extends Creator {
Product createProduct() { return new ConcreteProductA(); }
}
class ConcreteCreatorB extends Creator {
Product createProduct() { return new ConcreteProductB(); }
}
9.2 策略模式(封装应用)
java复制interface SortingStrategy {
void sort(int[] data);
}
class BubbleSort implements SortingStrategy {
public void sort(int[] data) { /* 冒泡排序实现 */ }
}
class QuickSort implements SortingStrategy {
public void sort(int[] data) { /* 快速排序实现 */ }
}
class Sorter {
private SortingStrategy strategy;
Sorter(SortingStrategy strategy) {
this.strategy = strategy;
}
void setStrategy(SortingStrategy strategy) {
this.strategy = strategy;
}
void sort(int[] data) {
strategy.sort(data);
}
}
9.3 模板方法模式(继承应用)
java复制abstract class Game {
abstract void initialize();
abstract void startPlay();
abstract void endPlay();
// 模板方法
public final void play() {
initialize();
startPlay();
endPlay();
}
}
class Cricket extends Game {
void initialize() { System.out.println("板球游戏初始化"); }
void startPlay() { System.out.println("开始玩板球"); }
void endPlay() { System.out.println("板球游戏结束"); }
}
class Football extends Game {
void initialize() { System.out.println("足球游戏初始化"); }
void startPlay() { System.out.println("开始玩足球"); }
void endPlay() { System.out.println("足球游戏结束"); }
}
10. 从面向对象到函数式编程
现代Java也支持函数式编程,可以与面向对象编程结合使用:
10.1 Lambda表达式
java复制List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
// 传统面向对象方式
Collections.sort(names, new Comparator<String>() {
@Override
public int compare(String a, String b) {
return a.compareTo(b);
}
});
// 函数式方式
Collections.sort(names, (a, b) -> a.compareTo(b));
10.2 方法引用
java复制// 等同于上面的lambda表达式
Collections.sort(names, String::compareTo);
10.3 Stream API
Stream API提供了声明式的数据处理方式:
java复制List<String> filtered = names.stream()
.filter(name -> name.startsWith("A"))
.map(String::toUpperCase)
.collect(Collectors.toList());
面向对象和函数式不是对立的,而是可以互补的。良好的Java代码通常会结合两者的优点。
