面向过程把问题分解成一个一个步骤,每个步骤用函数实现,依次调用即可。
我们在进行面向过程编程的时候,不需要考虑那么多,上来先定义一个函数,然后使用各种诸如if-else、for-each等方式进行代码执行。最典型的用法就是实现一个简单的算法,比如实现冒泡排序。
面向对象将问题分解成一个一个步骤,对每个步骤进行相应的抽象,形成对象,通过不同对象之间的调用,组合解决问题。
就是说,在进行面向对象进行编程的时候,要把属性、行为等封装成对象,然后基于这些对象及对象的能力进行业务逻辑的实现。比如想要造一辆车,上来要先把车的各种属性定义出来,然后抽象成一个Car类。
面向对象有封装、继承、多态三大基本特征,和单一职责原则、开放封闭原则、Liskov替换原则、依赖倒置原则和 接口隔离原则等五大基本原则。
三大基本特征:封装、继承、多态。
封装就是把现实世界中的客观事物抽象成一个Java类,然后在类中存放属性和方法。如封装一个汽车
类,其中包含了发动机
、轮胎
、底盘
等属性,并且有启动
、前进
等方法。
像现实世界中儿子可以继承父亲的财产、样貌、行为等一样,编程世界中也有继承,继承的主要目的就是为了复用。子类可以继承父类,这样就可以把父类的属性和方法继承过来。
如Dog类可以继承Animal类,继承过来嘴巴
、颜色
等属性, 吃东西
、奔跑
等行为。
多态是指在父类中定义的方法被子类继承之后,可以通过重写,使得父类和子类具有不同的实现,这使得同一个方法在父类及其各个子类中具有不同含义。
在Java中,接口可以继承接口,抽象类可以实现接口,抽象类也可以继承具体类。普通类可以实现接口,普通类也可以继承抽象类和普通类。
Java支持多实现,但是只支持单继承。即一个类可以实现多个接口,但是不能继承多个类。
以下是一些示例,通过代码的方式给大家介绍一下这几个原则具体的应用和实践。
假如有一个类用于日志消息的处理,但是这个类不仅仅负责创建日志消息,还负责将其写入文件。根据单一职责原则,我们应该将这两个职责分开,让一个类专注于创建日志消息,而另一个类专注于日志消息的存储。
// 负责日志消息的创建
class LogMessageCreator {
public String createLogMessage(String message, LogLevel level) {
// 创建和格式化日志消息
LocalDateTime now = LocalDateTime.now();
return now.toString() + " [" + level.toString() + "] " + message;
}
}
// 日志级别枚举
enum LogLevel {
INFO, WARNING, ERROR;
}
// 负责日志消息的存储
class LogFileWriter {
public void writeToFile(String message, String filename) {
// 将日志消息写入指定的文件
try {
Files.write(Paths.get(filename), message.getBytes(), StandardOpenOption.APPEND);
} catch (IOException e) {
// 处理文件写入异常
}
}
}
// 使用例子
public class Logger {
private LogMessageCreator messageCreator;
private LogFileWriter fileWriter;
public Logger() {
messageCreator = new LogMessageCreator();
fileWriter = new LogFileWriter();
}
public void log(String message, LogLevel level, String filename) {
String logMessage = messageCreator.createLogMessage(message, level);
fileWriter.writeToFile(logMessage, filename);
}
}
LogMessageCreator类只负责创建和格式化日志消息,而LogFileWriter类只负责将日志消息写入文件。这种分离确保了每个类只有一个改变的原因,遵循了单一职责原则。
假设有一个图形绘制应用程序,其中有一个Shape类。
在遵守开闭原则的情况下,如果要添加新的形状类型,应该能够扩展Shape类而无需修改现有代码。这可以通过创建继承自Shape的新类来实现,如Circle和Rectangle。
// 形状接口
interface Shape {
void draw();
}
// 圆形类
class Circle implements Shape {
public void draw() {
// 绘制圆形
}
}
// 矩形类
class Rectangle implements Shape {
public void draw() {
// 绘制矩形
}
}
// 图形绘制类
class GraphicEditor {
public void drawShape(Shape shape) {
shape.draw();
}
}
这样,当我们想要修改Circle的时候不会对Rectangle有任何影响。
假设有一个函数接受Bird对象作为参数。根据里氏替换原则,这个函数应该能够接受一个Bird的子类对象(如Sparrow或Penguin)而不影响程序运行。
// 鸟类
class Bird {
public void fly() {
// 实现飞行
}
}
// 麻雀类
class Sparrow extends Bird {
// 重写飞行行为
}
// 企鹅类
class Penguin extends Bird {
@Override
public void fly() {
throw new UnsupportedOperationException("Penguin can't fly");
}
}
public static void main(String[] args){
Penguin penguin = new Penguin();
makeItFly(penguin);
Sparrow sparrow = new Sparrow();
makeItFly(sparrow);
}
// 使用鸟类的函数
public static void makeItFly(Bird bird) {
bird.fly();
}
我们可以把任意一个Bird的实现传入到makeItFly方法中,实现了用子类替换父类
在构建一个电商应用程序时,一个高层的“订单处理”模块不应该直接依赖于一个低层的“数据访问”模块。相反,它们应该依赖于抽象,例如一个接口。这样,数据访问的具体实现可以随时改变,而不会影响订单处理模块。
// 数据访问接口
interface DataAccess {
void saveOrder(Order order);
}
// 高层模块:订单处理
class OrderProcessingService {
private DataAccess dataAccess;
public OrderProcessingService(DataAccess dataAccess) {
this.dataAccess = dataAccess;
}
public void processOrder(Order order) {
// 订单处理逻辑
dataAccess.saveOrder(order);
}
}
// 低层模块:数据访问实现
class DatabaseDataAccess implements DataAccess {
public void saveOrder(Order order) {
// 数据库保存逻辑
}
}
这样底层的数据存储我们就可以任意更换,可以用MySQL,可以用Redis,可以用达梦,也可以用OceanBase,因为我们做到了依赖接口,而不是具体实现。
如果有一个多功能打印机接口包含打印、扫描和复制功能,那么只需要打印功能的客户端应该不必实现扫描和复制的接口。这可以通过将大接口分解为更小且更具体的接口来实现。
// 打印接口
interface Printer {
void print();
}
// 扫描接口
interface Scanner {
void scan();
}
// 多功能打印机类
class MultiFunctionPrinter implements Printer, Scanner {
public void print() {
// 打印实现
}
public void scan() {
// 扫描实现
}
}
// 仅打印类
class SimplePrinter implements Printer {
public void print() {
// 打印实现
}
}