包 (package) 是组织类的一种方式,简单来说就是一个文件夹。在一个包中可以创建于其他包中类名相同的类。
创建方式(IDEA)
但本章重点不是包,所以这里不做过多介绍,在本章只需要记住本章提及的包是个文件夹就OK了。
基本语法
class 子类 extends 父类 {
}
规则
代码中创建的类, 主要是为了抽象现实中的一些事物(包含属性和方法),有的时候客观事物之间就存在一些关联关系, 那么在表示成类和对象的时候也会存在一定的关联,例如以下代码
在src包下创建一个Main类,用来测试代码。
这个代码我们发现其中存在了大量的冗余代码,仔细分析, 我们发现 Animal 和 Cat 以及 Bird 这几个类中存在一定的关联关系:
此时我们就可以让 Cat 和 Bird 分别继承 Animal 类, 来达到代码重用的效果,代码如下
但图二相比图一来讲,通过使用继承特性达到代码的复用性,少写了许多冗余代码,使整个程序看上去更加简洁。
思考
要是我们将父类Animal中的属性name的public改成private会怎样?
我们可以看到,改成private后不仅在Main文件中会报错,在子类Bird文件当中也会报错
因为private这个访问修饰符的特性是私有的,被private修饰的成员只能在本类中访问,也就是说,如果将父类Animal中的属性name改成private,此时name属性只能在父类中访问,而子类Brid就访问不到,所以会报错,由此得知
重点
子类在构造时, 一定要先帮助父类进行构造。在子类的构造方法中,我们要先调用父类的构造方法来帮助父类进行构造,不知道构造方法的同学可以先去我的这篇博客了解一下,链接: Java类和对象的学习.
如果父类当中是默认构造方法,那么子类当中的构造方法如果没主动调用父类构造方法的话,此时会显式调用父类默认的构造方法
如果父类实现了构造方法,那么此时子类在构造时就必须得使用super关键字调用父类的构造方法,不管子类当中有多少个构造方法,都必须在子类构造之前调用父类构造方法先帮助父类进行构造。
否则代码就会报错
super关键字后面有讲。
通过上面问题我们发现,使用public来修饰成员字段达不到"封装"的目的,而使用private子类又无法继承,那有没有两全其美的方法呢?
当然有,Java提供了一个protected访问权限,它的作用
此时可以发现,在Main文件当中执行代码依然会报错,而在子类Brid当中却没报错了。
protected其实是一个访问权限修饰符,而在Java当中一共有4种访问权限修饰符,分别有着不同的权限范围,以下一幅图可做参考
所以,我们希望类要尽量做到 “封装”, 即隐藏内部实现细节, 只暴露出 必要 的信息给类的调用者.因此我们在使用的时候应该尽可能的使用 比较严格 的访问权限. 例如如果一个方法能用 private, 就尽量不要用public.
final是Java中的一个关键字,被final修饰的变量表示常量 (不能被修改),而final其实还可以修饰类,当一个类被final修饰时,那么代表这个类不能被继承。
在刚才的继承学习当中我们知道,动物是一个类,而动物有很多种类,猫是动物的一种,但实际生活当中不管是猫还是鸟,其种类也是有很多种种类
此时子类可以进一步的再派生出新的子类,以此类推,将此叫做多层继承,但是即使如此, 我们并不希望类之间的继承层次太复杂. 一般我们不希望出现超过三层的继承关系. 如果继承层次太多, 就需要考虑对代码进行重构了,防止继承层次,那么此时我们可以使用final关键字来进行限制。
示例
我们将ChineseGardenCat类使用final修饰,此时发现如果OrangeCat类去继承ChineseGardenCat则代码会报错。
子类可继承父类中的方法,而不需要重新编写相同的方法。但有时子类并不想原封不动地继承父类的方法,而是想作一定的修改,这就需要采用方法的重写。子类实现父类的同名方法, 并且参数的类型和个数完全相同, 这种情况称为 覆写/重写/覆盖(Override),
构成重写的规则
我们发现,在main方法中执行的b.func调用的是父类继承的方法
那么我们将子类B重写父类的func方法,会发生什么呢?
此时执行的是子类重写的方法。
针对重写的方法, 可以使用 @Override 注解来显式指定,有了这个注解能帮我们进行一些合法性校验.,例如不小心将方法名字写错了 (比如写成 fun), 那么此时编译器就会发现父类中没有 fun方法, 就会编译报错, 提示无法构成重写.
重写的注意事项
super 表示获取到父类实例的引用
super关键字的三种用法,类似于this关键字
前面的代码中由于使用了重写机制, 调用到的是子类的方法. 如果需要在子类内部调用父类方法怎么办? 可以使用super 关键字。
此时调用子类的func方法的同时调用了父类的func方法
需要注意,如果子类不加super调用func方法,默认调用的是子类重写的func方法。
this关键字与super关键字的区别
尽管两个关键字的用法都差不多,但还是有本质区别的,可参考下图
多态是同一个行为具有多个不同表现形式或形态的能力,就是同一个接口,使用不同的实例而执行不同操作,多态是一种思想。
在了解多态前,我们先来了解一下两个重要知识点,向上转型与动态绑定
在面向对象程序设计中, 针对一些复杂的场景(很多类, 很复杂的继承关系), 程序猿会画一种 UML 图的方式来表示类之间的关系. 此时父类通常画在子类的上方. 所以我们就称为 “向上转型” , 表示往父类的方向转.
在Java中使用父类引用引用子类对象,此时就会发生向上转型,发生向上转型的时机有三种
直接赋值
在代码中,如果使用父类定义的变量引用子类创建的对象,此时就会发生向上转型,例如:
方法传参
在传给方法的参数中也可以发生向上转型的,例如:
方法返回值
不仅方法的参数可以发生向上转型,方法的返回者也可以发生向上转型,例如:
注意
发生向上转型后,通过父类的引用,只能访问自己(父类)的方法或属性。
如果通过父类去引用子类的方法,此时代码会发生报错
那么向上转型有什么作用呢?这个在后续的多态思想中很重要。
动态绑定也叫运行时绑定,发生在向上转型调用子类重写的方法的时候。我们学习了前面的向上转型,知道了通过父类的引用只能调用到父类的属性及方法,但如果子类重写了父类的方法,此时编译时调用的是父类的方法,但实际运行中调用的是子类重写的方法。
发生动态绑定的前提:
举个例子
上图代码,在子类当中,重写了父类方法,此时在编译时调用的是父类的eat方法,但在运行时调用的是子类重写的eat方法
坑
我们来看以下一段有坑的代码。
从代码中,我们看到,通过new关键字,调用了D的构造方法创建了一个对象,而前面我们说过在子类构造之前要先帮助父类先进行构造,而此时父类B当中的构造方法调用了func方法,那么此时这个调用的func方法是子类D重写的func方法呢,还是父类B当中的func方法呢?
运行截图
通过运行截图我们可以发现,调用的是子类D重写的方法,我们仔细分析一下,在父类B当中的构造方法调用被重写func时,此时也会发生动态绑定,所以调用的是子类D重写的方法,但此时父类B还没构造完成,也就是说,此时子类D自身还没有被构造,那么num也是默认值的状态0,所以在调用重写的func时num并没有被赋值成1,所以最后打印在屏幕上的num是0。
结论:在父类构造方法中,如果调用了重写的方法,也是会发生动态绑定的,所以在编程当中用尽量简单的方式使对象进入可工作状态", 尽量不要在构造器中调用方法(如果这个方法被子类重写, 就会触发动态绑定, 但是此时子类对象还没构造完成), 可能会出现一些隐藏的但是又极难发现的问题.
有了前面向上转型与动态绑定的只是铺垫,那么现在我们可以使用多态思想来进行编程了,所谓多态,无非就是通过一个引用来呈现出不同的形态。代码中的多态也是如此. 一个引用到底是指向父类对象, 还是某个子类对象(可能有多个), 也是要根据上下文的代码来确定.
来看以下代码
按照以往的方法,假如要输出所有的图形,那我们就要创建三个对象,分别调用打印图形的方法,如下图
但我们知道,这三个子类当中都重写了父类的draw方法,并且这三个子类都是同一个父类,那么我们此时就能将代码改成以下:
可以看到,达到的效果都是一样的,当类的调用者在编写 DisplayPattern 这个方法的时候, 参数类型为 Shape (父类), 此时在该方法内部并不知道, 也不关注当前的 shape 引用指向的是哪个类型(哪个子类)的实例. 此时 shape 这个引用调用 draw 方法可能会有多种不同的表现(和 shape 对应的实例相关), 这种行为就称为 多态。
类调用者对类的使用成本进一步降低
能够降低代码的 “圈复杂度”, 避免使用大量的 if - else
- 圈复杂度是一种描述一段代码复杂程度的方式. 一段代码如果平铺直叙, 那么就比较简单容易理解. 而如果有很多的条件分支或者循环语句, 就认为理解起来更复杂.
- 因此我们可以简单粗暴的计算一段代码中条件语句和循环语句出现的个数, 这个个数就称为 “圈复杂度”. 如果一个方法的圈复杂度太高, 就需要考虑重构.
比如我们现在打算打印多个图案,如果不基于多态,则需要使用大量的if else语句来逐步判断,实现代码如下:
但如果使用多态的话,则可以不用这么多if else分支语句,只需要一个循环即可实现,代码如下。
可扩展能力更强
如果要新增一种新的形状, 使用多态的方式代码改动成本也比较低
对于类的调用者来说(DisplayPattern方法), 只要创建一个新类的实例就可以了, 改动成本很低。
而对于不用多态的情况, 就要把 DisplayPattern中的 if - else 进行一定的修改, 改动成本更高.
多态总结
多态是面向对象程序设计中比较难理解的部分, 重点是多态带来的编码上的好处,另一方面, 如果抛开 Java, 多态其实是一个更广泛的概念, 和 “继承” 这样的语法并没有必然的联系.
无论是哪种编程语言, 多态的核心都是让调用者不必关注对象的具体类型. 这是降低用户使用成本的一种重要方式
以上就是本章介绍的内容,希望能给读者带来帮助!!!感兴趣的读者可以看看博主其它博客哦。
Java类和对象的学习
Java数据结构 : 顺序表
Java数据结构 :单链表