✅如何理解面向对象和面向过程?

典型回答

面向过程把问题分解成一个一个步骤,每个步骤用函数实现,依次调用即可。

我们在进行面向过程编程的时候,不需要考虑那么多,上来先定义一个函数,然后使用各种诸如if-else、for-each等方式进行代码执行。最典型的用法就是实现一个简单的算法,比如实现冒泡排序。

面向对象将问题分解成一个一个步骤,对每个步骤进行相应的抽象,形成对象,通过不同对象之间的调用,组合解决问题。

就是说,在进行面向对象进行编程的时候,要把属性、行为等封装成对象,然后基于这些对象及对象的能力进行业务逻辑的实现。比如想要造一辆车,上来要先把车的各种属性定义出来,然后抽象成一个Car类。

面向对象有封装、继承、多态三大基本特征,和单一职责原则、开放封闭原则、Liskov替换原则、依赖倒置原则和 接口隔离原则等五大基本原则。

知识扩展

面向对象的三大基本特征?

三大基本特征:封装、继承、多态。

封装

封装就是把现实世界中的客观事物抽象成一个Java类,然后在类中存放属性和方法。如封装一个汽车类,其中包含了发动机轮胎底盘等属性,并且有启动前进等方法。

继承

像现实世界中儿子可以继承父亲的财产、样貌、行为等一样,编程世界中也有继承,继承的主要目的就是为了复用。子类可以继承父类,这样就可以把父类的属性和方法继承过来。

如Dog类可以继承Animal类,继承过来嘴巴颜色等属性, 吃东西奔跑等行为。

多态

多态是指在父类中定义的方法被子类继承之后,可以通过重写,使得父类和子类具有不同的实现,这使得同一个方法在父类及其各个子类中具有不同含义。

✅如何理解Java中的多态?

继承和实现

在Java中,接口可以继承接口,抽象类可以实现接口,抽象类也可以继承具体类。普通类可以实现接口,普通类也可以继承抽象类和普通类。

Java支持多实现,但是只支持单继承。即一个类可以实现多个接口,但是不能继承多个类。

为什么Java不支持多继承?

✅为什么Java不支持多继承?

面向对象的五大基本原则?

  • 单一职责原则(Single-Responsibility Principle
    • 内容:一个类最好只做一件事
    • 提高可维护性:当一个类只负责一个功能时,其实现通常更简单、更直接,这使得理解和维护变得更容易。
    • 减少代码修改的影响:更改影响较小的部分,因此减少了对系统其他部分的潜在破坏。
  • 开放封闭原则(Open-Closed principle)
    • 内容:对扩展开放、对修改封闭
    • 促进可扩展性:可以在不修改现有代码的情况下扩展功能,这意味着新的功能可以添加,而不会影响旧的功能。
    • 降低风险:由于不需要修改现有代码,因此引入新错误的风险较低。
  • Liskov替换原则(Liskov-Substituion Principle)
    • 内容:子类必须能够替换其基类
    • 提高代码的可互换性:能够用派生类的实例替换基类的实例,使得代码更加模块化,提高了其灵活性。
    • 增加代码的可重用性:遵循LSP的类和组件更容易被重用于不同的上下文。
  • 依赖倒置原则(Dependency-Inversion Principle)
    • 内容:程序要依赖于抽象接口,而不是具体的实现
    • 提高代码的可测试性:通过依赖于抽象而不是具体实现,可以轻松地对代码进行单元测试。
    • 减少系统耦合:系统的高层模块不依赖于低层模块的具体实现,从而使得系统更加灵活和可维护。
  • 接口隔离原则(Interface-Segregation Principle)。
    • 内容:使用多个小的专门的接口,而不要使用一个大的总接口
    • 减少系统耦合:通过使用专门的接口而不是一个大而全的接口,系统中的不同部分之间的依赖性减少了。
    • 提升灵活性和稳定性:更改一个小接口比更改一个大接口风险更低,更容易管理。

以下是一些示例,通过代码的方式给大家介绍一下这几个原则具体的应用和实践。

单一职责原则:一个类最好只做一件事

假如有一个类用于日志消息的处理,但是这个类不仅仅负责创建日志消息,还负责将其写入文件。根据单一职责原则,我们应该将这两个职责分开,让一个类专注于创建日志消息,而另一个类专注于日志消息的存储。

// 负责日志消息的创建
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() {
        // 打印实现
    }
}

原文: https://www.yuque.com/hollis666/xkm7k3/sy3eyr