继承是面向对象编程的三大特征之一,继承将面向对象的编程思想体现的更加淋漓尽致,允许类和类之间产生关联,对于类和对象的基本知识可进传送门: Java中的基本操作单元 - 类和对象。
一、思想解读
1. 什么是继承
从类的概念出发,我们可以通过定义class去描述一类事物,具有相同的属性和行为。但在很多时候我们希望对类的定义能够进一步细化,这就相当于是一个大的分类下面有很多的子分类,如文具下面可以分为:写字笔、便签、文件管理等等。
如果品类更加的复杂,可以先分为:学生文具、办公文具、财会用品,然后在每个品类下面再根据具体的作用去划分。这些被划分出来的子类别都一定具有父类别的某些共同特征或用途,并且有可能存在多级的分类关系,那么如果我们使用面向对象的语言去描述出这样一种关系就可以使用继承。
下面我们将例子与面向对象中的概念进行对应:
- 上述关系可以用子类别继承自父类别来描述
- 父类别被称作父类或超类
- 子类别被称作子类
- 继承可以使子类具有父类的各种属性和方法,不需要再次编写相同的代码
2. 继承有什么用
如果我们将学生类进一步细化为:初中生、高中生、大学生。显然,细化之后的类与类之间一定是存在某些差异的,但是也一定会存在共同点。如果我们使用代码进行表示,三个类中会有很多相同的属性或方法,也会存在一些差异:
// 定义类:初中生 public class JuniorStudent{ // 相同的属性 public String name; public int age; public String school; public String grade; // 其他方法 }
// 定义类:高中生 public class SeniorStudent{ // 相同的属性 public String name; public int age; public String school; public String grade; // 不同的属性 public String subject;// 科目:文理科 // 其他方法 }
// 定义类:大学生 public class UniversityStudent{ // 相同的属性 public String name; public int age; public String school; public String grade; // 不同的属性 public String college;// 学院 public String major;// 专业 // 其他方法 }
上面只是列举了部分的属性,可以发现有很多属性是完全重合的,方法也有可能存在相同的现象。这个时候我们就们就可以将其中相同的属性和方法抽取出来,定义一个Student学生类,从而对每个类进行简化。
// 定义类:学生 public class Student{ // 提取公共的属性 public String name; public int age; public String school; public String grade; // 提取公共的方法 }
// 简化后的初中生 public class JuniorStudent extends Student{ // 其他方法 }
// 简化后的高中生 public class SeniorStudent extends Student{ // 不同的属性 public String subject;// 科目:文理科 // 其他方法 }
// 简化后的大学生 public class UniversityStudent extends Student{ // 不同的属性 public String college;// 学院 public String major;// 专业 // 其他方法 }
// 定义测试类 public class Test{ public static void main(String[] args){ JuniorStudent juniorStudent = new JuniorStudent(); juniorStudent.name = "小明";// 正常使用,来自父类 SeniorStudent seniorStudent = new SeniorStudent(); seniorStudent.name = "小李";// 正常使用,来自父类 seniorStudent.subject = "文科";// 自有属性,来自子类 UniversityStudent universityStudent = new UniversityStudent(); universityStudent.name = "小陈";// 正常使用,来自父类 universityStudent.college = "XX大学";// 自有属性,来自子类 universityStudent.major = "XX专业";// 自有属性,来自子类 } }
从上面的例子可以看出,子父类之间可以通过extends关键字建立继承关系。子类可以直接使用父类中定义的属性和方法,也可以覆盖父类中的方法,表现出子类自己的特点。使用继承有以下几个好处:
- 减少代码量,子类可以继承父类的属性和方法
- 提高复用性,便于维护
- 子类可以通过覆盖父类的方法表达自身的特点
- 可以使类和类之间产生关联,是多态的前提
3. 继承的限制与规则
在Java中,继承的使用存在一些限制,我们需要先明确使用规则才能更好的去设计子父类。一言以蔽之:Java不支持多继承,但支持多重继承(多级继承),从一个子类出发,只存在一条路找到最终的父类。
- 单继承
class A{ ... } class B extends A{ ... }
- 多重继承
class A{ ... } class B extends A{ ... } class C extends B{ ... }
- 多子类继承同一父类
class A{ ... } class B extends A{ ... } class C extends A{ ... }
4. 如何设计子父类
当我们需要通过程序去描述某一个场景或实现某一个应用系统时,就需要构建很多相关的类,合理的使用继承可以使代码更加高效也更加利于维护。那么子父类的构建就可以从类本身所代表的意义出发,如果含义相似或相近,并且类与类之间没有较大的冲突,那么我们就可以把他们归为一类。另外一种情况就是原有构建的类不能满足新功能的需要,需要据此改进,那么我们就可以将原有类作为父类,扩充出他的子类,使整体的功能更加强大,同时又不会对已有的代码产生较大的影响。
- 从多个相关联的类中提取出父类
可以从管理员(AdminUser)、普通用户(NormalUser)、VIP用户(VIPUser)中提取相同的特征,得到父类:用户(User),同样具有用户名,密码,昵称等信息,同样存在登录方法,只不过各自的实现会有所不同。我们也可以混合使用多种继承方式,得到如下的类关系:
- 从一个已有的类中扩充出子类
对于一个简单的电商场景,产品类的设计会比较简单,只需要标识基本信息和价格即可。如果此时需要举行一个秒杀活动,要在购买页面中标识出原价、特价、活动时间、活动介绍等等信息,这就使得我们要对产品类做出升级,如果直接去修改产品类,会导致出现一些可能不会经常使用的属性和方法,因为这些属性和方法纯粹是为特价商品而设计的。比较好的做法是将原有的商品类(Product)作为父类,然后扩充出它的子类:特价商品类(SpecialProduct),在特价商品类中存放新出现的信息。
- 子类是父类的一个扩充和扩展,使用extends关键字来表示真的是很恰当
二、子父类的使用
理解了相关的概念后,我们回到Java的语法中来,子父类间通过extends来建立继承关系,结构如下:
// 定义父类 public class Father{ ... }
// 定义子类,继承父类 public class Son extends Father{ ... }
1. 权限修饰符
在之前的文章中,已经介绍了权限修饰符的用法,不清楚的同学可以进传送门:Java面向对象编程三大特征 - 封装。当两个类建立了继承关系时,虽然父类当中的所有内容均会被子类继承,但是由于存在权限修饰符,无访问权限的属性或方法会被隐藏,无法被调用和访问(实例化子类对象时,父类对象也会一同被实例化,详细过程会在后面的文章中单独说明)。
在子类中可以直接调用父类中被public和protected声明的属性和方法,如果是在测试类中,在进行属性调用时依然会受到权限修饰符的限制,看下面一个例子:
src └──edu └──sandtower └──bean │ Father.java │ Son.java └──test │ Test.java
以上为实体类与测试类所在的目录结构
- Father实体类所在包:edu.sandtower.bean
- Son实体类为Father的子类,与Father在同一包下
- Test测试类所在包:edu.sandtower.test
package edu.sandtower.bean; public class Father{ // 父类中的私有属性 private double ownMoney = 2000;// 私房钱 // 父类中的受保护属性 protected double money = 5000; // 父类中的公开属性 public String name = "老李"; }
package edu.sandtower.bean; public class Son extends Father{ // 子类中的独有属性 ... // 测试方法 public void test(){ Son son = new Son(); System.out.println(son.ownMoney);// 编译失败,无法访问私有属性,查看私房钱 System.out.println(son.money);// 编译通过,在子类中可以访问protected属性 System.out.println(son.name);// 编译通过,可以访问public属性 } }
package edu.sandtower.test; import edu.sandtower.bean.Son; public class Test{ public static void main(String[] args){ // 在test包中的Test类中创建Son实例 Son son = new Son(); son.name = "小李";// 编译通过,可以访问自父类继承的公开属性 // 编译失败,在测试类中无法访问protected属性,因为Test类与Father类并无子父类关系 son.money -= 500.0; // 对于Son的其他属性和Father的使用可以自行进行测试,不再赘述 } }
从上面的例子可以看到,权限修饰符所起的作用还是很大的。测试类对于子父类来说是一个处在不同包中的完全无关的类,在调用时会被权限修饰符所限制,所以这里也再度明确一下:权限修饰符是根据类的所在路径与类之间的结构关系进行限定的,不是说在任意一个地方使用子类实例都能调用出父类中的属性和方法。
2. this与super
明确了权限修饰符的作用规则后就带来了一个问题,既然在其他类中不能够访问某些属性,那应该如何赋值和使用呢?这时就可以使用封装的办法,同时结合this和super的使用来解决。
- this:指代当前对象,可以调用当前类中的属性和方法
- super:指代父类对象,可以调用父类中可访问的属性和方法,包括被子类覆盖重写的方法
在使用子类实例时,如果我们想要使用某些父类的属性或方法,可以借助构造器和封装方法。将代码修改后,得到如下结果:
package edu.sandtower.bean; public class Father{ // 父类中的私有属性 private double ownMoney = 2000;// 私房钱 // 父类中的受保护属性 protected double money = 5000; // 父类中的公开属性 public String name = "老李"; }
package edu.sandtower.bean; public class Son extends Father{ // 子类中的独有属性 ... // 使用构造器为属性赋值 public Son(String name,double money){ super.name = name; super.money = money; } // 使用封装方法操作父类中的属性 public void setMoney(double money){ super.money = money; } public double getMoney(){ return super.money; } }
package edu.sandtower.test; import edu.sandtower.bean.Son; public class Test{ public static void main(String[] args){ // 在test包中的Test类中创建Son实例 Son son = new Son("小李",3000);// 成功为父类继承而来的属性赋值 // 以下代码编译通过 double money = son.getMoney(); System.out.println(money); son.setMoney(money - 500); } }
3. final修饰符
final修饰符可以用来修饰属性和方法,也可以用来修饰类。
当修饰属性时,如果是基本数据类型,则可看做是定义了一个常量,值一旦被指定则不可变。如果是引用类型,则引用无法发生变化,即:可以修改数组或实例中的属性值,但是引用的指向不能再发生变化,无法再指向其他的实例和数组。
由final修饰的方法被子类继承后不能被重写,有关于继承中子父类方法的关系将在论述多态的文章中详细讨论。
由final修饰的class不能被继承,如果我们把继承关系想象成一棵大树,父类为根,子类为枝的话,那么final class就一定只能做树叶了,因为确认不会有它的子类存在了。
4. 终极父类:Object
在刚接触面向对象时,我们可能就听说过一个类,那就是:Object,好像它无所不能装,大饼夹一切的存在。我们所有定义的class都会隐式的继承Object,即:如果我们的类没有使用extends关键字显示的指定父类,那么会自动认为Object是父类,这一过程是在JVM运行时完成的,所以我们不能通过反编译来进行验证。
Object中提供了特别通用的方法,如:toString,hashCode,equals等等。那么为什么使用Object能装下一切呢?首先就是因为Object类一定是最终父类的存在,换句话说就是树根本根!因为如果一个类显示的指定了另外一个类作为父类,那么他的父类或者父类的父类,一定会在某一级隐式的继承Object。
如果想进一步了解为什么任意类型的对象实例都能使用Object类型的引用接收可以查看另外一篇文章:Java面向对象编程三大特征 - 多态。