Java 技术小栈Java 技术小栈
  • 面向对象
  • 封装
  • 继承
  • 活动场次设计
  • mybatis-crud
关于
  • 面向对象
  • 封装
  • 继承
  • 活动场次设计
  • mybatis-crud
关于
  • Java基础

    • 面向对象
    • 封装
    • 继承

继承

  • 前言
  • 正文
    • is-a关系
    • 继承的基本使用
    • 方法的重写
    • 向上转型与向下转型
    • 继承层次过深优化
    • 如何判断该用组合还是继承?
  • 小结

前言

继承是面向对象的四大特性之一,用来表示类之间的is-a关系,可以解决代码复用的问题。但继承结构不稳定、层次过深,也会影响到代码的可维护性。 所以对于是否应该在项目中使用继承,继承结构不稳定、层次过深怎么办,本文从继承的基本使用带你知其所以然。

正文

is-a关系

类的is-a关系指的是继承或实现接口,即一个类是集成了一个父类或实现了某个接口。它表示一种特殊化的关系,其中一个实体是另一个实体的更具体版本。

例如,定义一个人类Person,那么我们可以有子类员工类Employee和学生类Student,然后继承自 Perosn,此时可以说 Employee is a Person或Student is a Person。意味着所员工/学生都是人类,但不能说所有人都是员工。

继承的基本使用

继承最大的一个好处就是代码复用,假如两个类有一些相同的属性或方法,我们就可以将这些相同的部分,抽取到父类中,让两个子类继承父类。这样,两个子类就可以重用父类中的代码,避免代码重复。

java中使用extends关键字来实现继承。 我们以Person和Employee类举例,代码如下:

public class Person {
    private String name;
    private String sex;
    private int age;
    
    public Person(String name, String sex, int age) {//构造方法
        this.name = name;
        this.sex = sex;
        this.age = age;
    }

    public void selfInfo(){//自我介绍
        System.out.println(String.format("我是%s,今年%s岁", this.name, this.age));
    }
}

public class Employee extends Person{
    private String workAge;//工龄
    
    public Employee(String name, String sex, int age, String workAge) {//构造方法
        super(name, sex, age);
        this.workAge = workAge;
    }
}

public class Test {
    public static void main(String[] args) {
        Employee employee = new Employee("李四","男生",19,"1年");
        employee.selfInfo();//通过继承调用父类方法
        //打印:我是李四,今年19岁
    }
}

在上面代码中我们有看到super和this关键字,是用来区分继承后的成员变量/方法的调用。

  • super 用在子类中调用父类的public、protected修饰的成员变量/方法
  • this 用来访问本类的成员变量/方法

方法的重写

在继承特性中,方法重写是一种很常见的场景。当子类需要父类的功能,但子类有自己特有内容时,可以重写父类中的方法。

这样,当子类调用该方法时,父类中的方法如被重写,则会调用具体之类的实现。 以Employee类举例,代码如下:

public class Employee extends Person{
    private String workAge;//工龄
    
    @Override
    public void selfInfo() {//重写
        super.selfInfo();
        System.out.println("工作了"+this.workAge);
    }
}

public class Test {
    public static void main(String[] args) {
        Employee employee = new Employee("李四","男生",19,"1年");
        employee.selfInfo();
        //打印:我是李四,今年19岁 工作了1年
    }
}

上述代码基于继承,Employee子类重写了父类的selfInfo方法,来实现和父类中不同的业务处理逻辑。

向上转型与向下转型

在继承里还有一个重要点就是引用类型转型,分为向上转型和向下转型。

public class Test {
    public static void main(String[] args) {
        //向上转型 父类的引用指向子类实例化对象
        Person person = new Employee("李四","男生",19,"1年");
        person.selfInfo();
        //向下转型 强制类型转换
        Employee employee = (Employee) person;
        employee.selfInfo();
        employee.work();//子类特有的方法
    }
}
  • 向上转型:子类向父类向上转换的过程,这个过程是默认的,当父类引用指向一个子类对象时,便是向上转型。
  • 向下转型:父类向子类向下转换的过程,这个过程是强制的。如果想要调用子类特有的方法,则必须做向下转型(使用强制类型转换)。

继承层次过深优化

Java中类只支持单继承,不支持多继承,但可以多层继承。

关于继承可能引起的问题,我们举例说明:

继承结构不稳定

初始设计一个父类Person,具有eat()方法,我们设计子类Employee、Student继承Person,代码如下:

public class Person {
    public void eat(){
        //do something ...
    }
}

public class Employee extends Person{
    
}

public class Student extends Person{
    
}

假设现在需要增加一个子类CollegeStudent,且增加方法study()和work()方法。此时该怎么扩展呢?

  1. 放在父类里不合适,因为工人不用上学,学生还不用工作;
  2. 如果扩展在子类,Employee里增加work()方法,Student里增加study()方法,这时CollegeStudent又需要重复实现work()方法;
public class Person {

    public void eat(){
        //do something ...
    }
    public void study(){
        //do something ...
    }
    public void work(){
        //do something ...
    }
}

public class CollegeStudent extends Student{
    //我又上课 又工作
}

public class Employee extends Person{
    @Override
    public void study(){
        //我是工人不用上课
    }
}

public class Student extends Person{
    @Override
    public void work() {
        //我还在上学不会工作
    }
}

这就是典型的继承结构不稳定会引发的问题,不易扩展维护;

继承层次过深

另一个问题继承层次过深是因为java中可以多层继承,而继承可以使子类具有父类具有相同的行为方法,子类还可以重写方法。

这种情况下类的继承层次如果很深,很复杂的继承关系,一方面,会导致代码的可读性变差。因为我们要搞清楚某个类具有哪些方法、属性,必须阅读父类的代码、父类的父类的代码 ...... 一直追溯到最顶层父类的代码。

另一方面,这也破坏了类的封装特性,将父类的实现细节暴露给了子类。子类的实现依赖父类的实现,两者高度耦合,一旦父类代码修改,就会影响所有子类的逻辑。

总之,继承最大的问题就在于:继承结构不稳定、继承层次过深会影响到代码的可维护性和可读性。

那刚刚例子中继承存在的问题,我们又该如何来解决呢?

我们可以利用接口、组合、委托三个技术手段,一块儿来解决刚刚继承存在的问题。

  • 接口:java支持的接口类
  • 组合:一个类的定义中使用其他对象。
  • 委托:一个对象请求另一个对象的功能,捕获一个操作并将其发送到另一个对象。
public interface Study {//接口
    void study();
}

public class StudyImpl implements Study{
    @Override
    public void study() {
        //...
    }
}

public interface Work {//接口
    void work();
}

public class WorkImpl implements Work{
    @Override
    public void work() {
        //...
    }
}

public class Employee implements Work{
    private Work work = new WorkImpl();//组合
    @Override
    public void work() {
        work.work();//委托
    }
}

public class Student implements Study{
    private Study study = new StudyImpl();//组合

    @Override
    public void study() {
        study.study();//委托
    }
}

public class CollegeStudent implements Study,Work{
    private Study study = new StudyImpl();//组合
    private Work work = new WorkImpl();//组合

    @Override
    public void study() {
        study.study();//委托
    }
    @Override
    public void work() {
        work.work();//委托
    }
}

如何判断该用组合还是继承?

鼓励多用组合少用继承,但组合也并不是完美的,继承也并非一无是处。

从上面的例子来看,继承改写成组合意味着要做更细粒度的类的拆分。这也就意味着,我们要定义更多的类和接口。类和接口的增多也就或多或少地增加代码的复杂程度和维护成本。

所以,在实际的项目开发中,我们还是要根据具体的情况,来具体选择该用继承还是组合。
如果类之间的继承结构稳定(不会轻易改变),继承层次比较浅(比如,最多有两三层继承关系),继承关系不复杂,我们就可以大胆地使用继承。反之,系统越不稳定,继承层次很深,继承关系复杂,我们就尽量使用组合来替代继承。

小结

继承虽强但要慎用,项目中一般经常简单使用Model的基类CrudMo继承通用属性、Controller的BaseController继承通用方法,这些都遵循了继承结构稳定,继承层次比较浅的特点,否则可以使用组合来替代继承。

正文中有提到继承的方法重写和引用类型转型,其实是多态特性的一种体现,下一篇详细解析多态。

最近更新:: 2025/3/10 22:16
Contributors: cool
Prev
封装