Java面向对象编程:三大特性与五大原则解析

177 阅读8分钟

面向对象是一种编程范式,依赖于类和对象的概念。

Java是一个典型的'面向对象'的编程语言。

面向对象有三大特性和五大原则(---划重点---)。

三大特征

1、封装

为什么引入封装:

程序设计追求“高内聚,低耦合”的设计理念。

  • 高内聚:类的内部数据操作细节自己完成,不允许外部干涉;
  • 低耦合:仅对外暴露少量的方法用于使用;

核心概念:封装是指将数据和操作数据的方法捆绑在一起,对外提供公共的访问接口,隐藏内部实现细节。这样能增强数据的安全性,避免外部随意访问和修改。

示例

在 Java 中,可以通过访问修饰符(如privatepublic)来实现封装。

class Person {
    private String name; // 私有属性,外部无法直接访问
    private int age; // 私有属性,外部无法直接访问
​
    // 公共的访问方法
    public String getName() {
        return name;
    }
​
    public void setName(String name) {
        this.name = name;
    }
​
    public int getAge() {
        return age;
    }
​
    public void setAge(int age) {
        if (age >= 0) {
            this.age = age;
        }
    }
}

在使用这个Person的时候,通常是先Person person = new Person();,而name和age是private修饰,无法直接通过person.name;的这种方式调用。

但是我们提供了public修饰的操作这些属性的方法,比如get和set方法,我们可以通过这些方法来对这些属性进行操作。

有人要问了这不是脱裤子放屁么?我直接person.name;去操作这个属性不行么,其实也可以。但是如果我这个类是只读的呢(比如一个人生下来血型就固定了),只读写法如下:

class Person {
    // 血型,一个人的血型在生下来的时候就已经决定了,只能查看不能修改了
    private String bloodType;
​
    // 公共的访问方法
    public String getBloodType() {
        return bloodType;
    }
​
    // 构造方法(提供父母的血型)
    public Person(String faterBloodType,String motherBloodType){
        // ... 试想一下这里有超级复杂的血型计算方式,这里我们使用拼接来简化
        this.bloodType = faterBloodType + motherBloodType;
    }
}

使用的时候就是通过new Person(String faterBloodType,String motherBloodType)方法拿到这个Person对象,而bloodType的在new这个对象的时候就已经给了,但是我们没有对外提供修改血型的方法,只提供了get方法。换句话说我管你是咋算出的血型,我只管拿知道这个人的血型方便日后输血就得了。

这样下来封装是不是有意义的呢?

2、继承

为什么引入继承:

  • 减少了代码的冗余,提高了代码的复用性;
  • 便于功能的扩展;
  • 为之后多态性的使用,提供了前提;

使用场景:

核心概念:继承允许一个类(子类)继承另一个类(父类)的属性和方法,从而实现代码的复用。子类能够扩展父类的功能,也可以重写父类的方法。

示例

在 Java 中,使用 extends 关键字来实现继承。以下是一个示例:

class Person {
    protected String name;
    protected int age;
​
    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }
​
    public void introduce() {
        System.out.println("My name is " + name + " and I'm " + age + " years old.");
    }
}
​
// 虽然这个类没有写name、age、introduce(),但是这些已经被继承了过来
class Student extends Person {
    private String studentId;
​
    public Student(String name, int age, String studentId) {
        super(name, age);
        this.studentId = studentId;
    }
​
    public String getStudentId() {
        return studentId;
    }
​
    public void setStudentId(String studentId) {
        this.studentId = studentId;
    }
}

如果没有继承的话,我们在写Student类的时候就需要再把name和age等内容再写一次,多麻烦。

通过这种方式可以节约很多的代码。

接口间的继承:接口也是类,接口间的继承属于特殊情况,接口之间可以使用extends进行继承,并且可以一次继承多个。

// 定义基础接口
interface Playable {
    void play();
}
​
interface Recordable {
    void record();
}
​
// 接口的继承(用组合接口来表述更贴切)
interface MediaDevice extends Playable, Recordable {
    void stop(); // 新增方法
}
​
// 实现类必须实现所有继承的方法
class SmartPhone implements MediaDevice {
    @Override public void play() { System.out.println("播放媒体"); }
    @Override public void record() { System.out.println("录制媒体"); }
    @Override public void stop() { System.out.println("停止操作"); }
}

规则总结

场景是否允许原因
继承抽象类 + 实现多个接口✅ 允许抽象类提供部分实现,接口补充额外行为,两者不冲突
继承具体类 + 实现多个接口✅ 允许类的单继承限制不影响实现多个接口
继承具体类 + 继承其他类❌ 禁止Java 不支持类的多重继承(单继承规则)
继承具体类+继承接口❌ 禁止多继承仅支持接口之间
继承抽象类+继承接口❌ 禁止多继承仅支持接口之间
接口间的多继承✅ 允许因为接口不涉及实现,所以可以看作是规范的整合
实现多个接口✅ 允许接口支持多实现,可以看作是规范的整合与实现

备注:关于为什么接口之间可以多继承,而其他类不可以的原因,可以去看看菱形继承。

3、多态

为什么引入多态:

  • 实现代码可扩展性
  • 解耦接口与实现
  • 实现代码复用和抽象建模

核心概念:多态是同一个行为具有多个不同表现形式或形态的能力。

多态可以细分为

  1. 运行时多态

    1. 由继承+方法重写导致的多态
    2. 由不同方式实现接口导致的多态
  2. 编译时多态

    1. 由函数重载(参数以及返回值不同)导致的多态

示例

由继承+方法重写导致的多态

class Animal {
    void speak() { System.out.println("动物叫"); }
}
​
class Dog extends Animal {
    @Override
    void speak() { System.out.println("汪汪汪"); }
}
​
class Cat extends Animal {
    @Override
    void speak() { System.out.println("喵喵喵"); }
}
​
// 多态调用
Animal animal = new Dog();
animal.speak(); // 输出:汪汪汪
Animal animal2 = new Cat();
animal2.speak(); // 输出:喵喵喵

由不同方式实现接口导致的多态

interface Animal {
    void speak();
}
​
class Dog implements Animal {
    @Override
    public void speak() { System.out.println("汪汪汪"); }
}
​
class Cat implements Animal {
    @Override
    public void speak() { System.out.println("喵喵喵"); }
}
​
// 多态调用
Animal animal1 = new Dog();
animal1.speak(); // 输出:汪汪汪
Animal animal2 = new Cat();
animal2.speak(); // 输出:喵喵喵

由函数重载导致的多态

class Calculator {
    int add(int a, int b) { return a + b; }          // 版本1:两个整数
    double add(double a, double b) { return a + b; } // 版本2:两个浮点数
    int add(int a, int b, int c) { return a + b + c; } // 版本3:三个整数
}
​
// 编译时多态调用
Calculator calc = new Calculator();
int sum1 = calc.add(1, 2);         // 调用版本1
double sum2 = calc.add(1.5, 2.5);  // 调用版本2
int sum3 = calc.add(1, 2, 3);      // 调用版本3

不论是什么多态,表现在上面代码中就是调用同一个方法可以说是调用同名称方法所表现出的不同形式

五大基本原则

1、单一职责原则(SRP)

  • 定义:一个类应该只有一个引起它变化的原因。

  • 核心思想:一个类只负责一项职责,避免将多个不相关的功能耦合在一起。

  • 示例

    • 错误设计:UserService 类同时包含用户管理、权限验证和日志记录功能。
    • 正确设计:拆分为 UserManager(用户管理)、AuthValidator(权限验证)和 Logger(日志记录)三个类。
  • 优点:降低类的复杂度,提高可维护性和可测试性。

2、开闭原则(OCP)

  • 定义:软件实体(类、模块、函数等)应该对扩展开放,对修改关闭。

  • 核心思想:通过抽象(接口、抽象类)定义稳定的行为,通过实现扩展新功能,而非修改原有代码。

  • 示例

    • 定义接口 Shape 包含 draw() 方法,新增图形(如圆形、矩形)时实现该接口,无需修改接口。
  • 优点:系统更灵活,新增功能时不影响现有代码,降低风险。

3、里氏替换原则(LSP)

  • 定义:子类可以替换父类且不影响程序的正确性。

  • 核心思想:子类必须遵守父类的契约(方法签名、行为约定),确保父类出现的地方子类可以无缝替换。

  • 示例

    • Rectangle长方形 是父类,Square 作为子类应保证 setWidth()setHeight() 方法行为一致(如正方形的宽高必须相等)。
  • 优点:保证继承体系的正确性,避免子类破坏父类的预期行为。

4、接口隔离原则(Interface Segregation Principle, ISP)

  • 定义:客户端不应该依赖它不需要的接口,一个类对另一个类的依赖应该建立在最小的接口上。

  • 核心思想:将胖接口拆分为多个小而具体的接口,避免客户端依赖不需要的方法。

  • 示例

    • 错误设计:Worker 接口包含 work()eat() 方法,导致 RobotWorker 必须实现 eat()
    • 正确设计:拆分为 Workable(含 work())和 Eatable(含 eat())接口,RobotWorker 只需实现 Workable
  • 优点:减少接口的冗余,提高代码的内聚性。

5、依赖倒置原则(DIP)

  • 定义

    1. 高层模块不应该依赖低层模块,两者都应该依赖抽象。
    2. 抽象不应该依赖细节,细节应该依赖抽象。
  • 核心思想:通过接口或抽象类解耦高层模块和低层模块,使两者依赖于抽象而非具体实现。

  • 示例

    • 高层模块 OrderService 不直接依赖低层 MySQLOrderDao,而是依赖 OrderDao 接口,具体实现通过依赖注入(如 Spring 框架)。
  • 优点:降低模块间的耦合度,提高系统的可扩展性和灵活性。