继承
前言
继承
是面向对象的四大特性之一,用来表示类之间的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()
方法。此时该怎么扩展呢?
- 放在父类里不合适,因为工人不用上学,学生还不用工作;
- 如果扩展在子类,
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
继承通用方法,这些都遵循了继承结构稳定,继承层次比较浅的特点,否则可以使用组合来替代继承。
正文中有提到继承的方法重写和引用类型转型,其实是多态特性的一种体现,下一篇详细解析多态。