【JAVA进化论】LV2-4:类的特性、关系
一、继承和多态
1.1:一个例子
参考之前的Cat类,如果我现在让你设计一个Dog类,它仍然具备眼睛、性别等属性,也会跑、吃等动作,这个时候你一定会发现,单独再为Dog搞一套跟Cat差不多的类定义吗?就没有更聪明的办法吗?
此时java里允许的类继承就可以起到作用了,想想我们的目标,我们的目标是让这些重复的属性不再重复定义一遍,那么像眼睛、毛色这种属性,或者跑、吃饭这类的操作就可以再拆出一个类:动物类
现在我们来定义下动物类:
1 | public class Animal { |
然后再定义一下Cat和Dog类:
1 | public class Cat extends Animal { //让Cat类继承Animal类 |
1 | public class Dog extends Animal { //让Dog类继承Animal类 |
Cat和Dog都是一个”空“类,但是分别利用extends关键词继承了Animal类,现在让我们进行如下测试:
1 | public static void main(String[] args) { |
上述程序输出结果如下:
1 | 叫声为:未知 |
可以看到,Cat和Dog虽然啥都没定义,但仍然可以使用Animal类里的方法,我们此时管Cat和Dog叫做Animal的子类,相反的Animal叫做Cat和Dog的父类。
可以通过这个例子发现,子类通过继承,可以得到父类的一些功能,需要确认的一个点是:在初始化完成一个子类初始化的同时其父类信息也会自动加载进子类,然后子类可以共享父类里的方法和属性。
这里说一下this
关键词,它表示的是本类所产生的具体对象对其内部内容做访问时用的一个标识,你甚至可以省略不写。
1.2:父类方法覆盖
通过上面的输出,我们发现,猫狗的cry方法输出的是Animal默认的内容,这时我们需要猫和狗能有自己的叫声,那么这时就可以选择重写父类的方法:
Cat类重写父类的cry
方法:
1 | public class Cat extends Animal { |
Dog类重写父类cry方法:
1 | public class Dog extends Animal { |
现在再运行下测试代码输出如下:
1 | 喵喵喵~ |
通过这个例子,我们可以知道,如果子类不想要父类里的实现,那么可以通过重写的方式重新设计对应的方法,例子中通过重写cry
方法,让Cat和Dog都拥有了自己的cry
方法。
1.3:子类方法扩展
经过上面一步,我们知道猫和狗都是动物,所以它们离不开动物类里定义的属性和方法,但是猫和狗仍存在一些不太一样的地方,比如猫具有捕鼠的能力,但不具备看家的能力,而狗具有看家能力,却没有捕鼠的能力,这种相对比较独立的能力就可以用来扩展:
Cat类新增捕鼠方法
1 | public class Cat extends Animal { |
Dog类新增看家方法:
1 | public class Dog extends Animal { |
通过本例我们知道,子类是可以自由扩展的,通过子类的引用变量依然可以完成调用:
1 | public static void main(String[] args) { |
输出:
1 | 我会捉老鼠 |
1.4:多态
现在来介绍下多态,多态是一种类和类之间的一种引用关系,因为有了继承,才有了多态这种特性,我们现在来改造下代码块4
:
1 | public static void main(String[] args) { |
运行如下:
1 | 喵喵喵~ |
仍然可以正常运行,并且被重写的方法cry
仍然执行的是子类里的那个。
这就是多态,如果不太好理解,我们可以通过下图来加深一下记忆:
这种使用父类修饰的引用变量,可以指向其任意子类对象的行为,我们称为类的多态,可以发现父类接收的子类对象无法访问扩展内容,但仍然可以访问父类所具备的内容,哪怕被重写的方法cry,但这时由于父类指向的还是子类对象,因此所触发的内容均属于子类,这里先不探讨多态的好处,你只需要加深对代码块13
和图1
的理解类的这种特性即可。
❓ 疑问点1:为什么通过Animal的引用变量指向的Cat对象,无法调用其catchMice方法?为什么cry方法可以调用触发的却是Cat里的cry方法?
1.5:如何直接访问父类的内容?
1.5.1:直接new
父类也是一个普通的类嘛,要想不受子类的任何影响,直接new
不就完事儿了。但一般父类都不太可能直接new,在多子类的情况下,又该怎样直接访问自己的资源呢?来接着往下看1.5.2
吧。
1.5.2:super关键词
通过上面的介绍,我们了解了类的继承和多态,子类在继承了父类的功能和属性之后,可以自定义方法实现对父类的扩展,针对实现不合子类要求的方法,子类也可以通过方法重写来覆盖原父类方法,然后就是多态,多态简单来说就是通过一个父类引用变量直接指向一个子类对象,还是结合图1理解。
那么我们现在再来做一个实验,现在我想让Cat和Dog在吃饭的时候自带叫声,现在我们来继续改造下这些类:
首先把Animal里的eat方法改造下,让它的逻辑执行之前,先调用下cry方法:
1 | public class Animal { |
别的代码不动,单独测试下eat方法:
1 | public static void main(String[] args) { |
最终输出:
1 | 喵喵喵~ |
这里说一下,即便代码块16直接用Cat和Dog接收对应的类对象,结果都是一致的。
🦩 结论:new出来具体的子类之后,父类里即便使用this调用它内部的被子类重写的方法,实际触发的仍然是子类的方法。
❓ 疑问点2:为什么呢?我明明是在Animal对象里调用的this.cry,为什么执行的却是子类的?
ok,那我们怎么直接访问父类里的方法呢?借助super关键词
即可,同this关键词一样用来描述类对象内部引用的,与this不同的是,子类利用super可以调用父类资源,注意,这里说了,想用super,就得在某子类内,我们现在如果想调用Animal本身的cry方法,那么就需要在Cat或Dog里,通过super触发:
首先把Animal里面的eat方法还原:
1 | public class Animal { |
然后改造下Cat类,让它重写eat方法,我们目标是让这个eat方法先调用一遍父类原始的cry方法,再调用一次父类原始的eat方法:
1 | public class Cat extends Animal { |
现在再次调用eat方法,输出结果如下:
1 | 叫声为:未知 |
你会发现,现在的调用就是父类里面原生的方法。
现在,让我们把代码块19
里的cry的super关键词去掉(之前说过,去掉相当于使用this
关键词调用):
1 | public class Cat extends Animal { |
然后调用eat的结果如下:
1 | 喵喵喵~ |
可见,Cat里使用this调用cry方法,当然是调用的它自己的咯。
❓ 疑问点3:为什么父类的被子类重写过的方法可怜到只能通过子类的super关键词才能触发?为什么父类里也有的方法要优先执行子类里重写后的?不管在父类还是子类,用this关键词调用的资源都是优先以子类为准吗?找不到的资源才去考虑找父类吗?
1.6:继承链
首先请记住现在的Animal类:
1 | public class Animal { |
Cat类清理一下之前的测试代码,记住现在的Cat类:
1 | public class Cat extends Animal { |
java语法里,只允许一个类继承一个类,也就是说,extends关键词只能在一个类定义里出现一次,根据这个规则,父类也是一个类,类允许继承一个别的类(套娃警告),描述起来是越来越乱,不如再改造下前面的例子来说明问题;现在我们发现Cat也只是定义了猫的基本概念,包括被它重写后的cry,确实,猫都是喵喵喵的叫,猫都会捕鼠,但是不同品种的猫,也存在差异,例如相比其它猫,橘猫的食量惊人,它还可以扮猪,如果这时定义一个橘猫类,首先它也是猫,那就让它继承Cat类,那么eat这个方法就可以单独拎出来再被重写一次,以显示橘猫特有的食量,其次应该单独加一个橘猫特有技能:扮猪,看代码:
1 | public class OrangeCat extends Cat { |
ok,至少通过橘猫类,我们知道了,java虽然不允许一个类继承多个类,但可以间接继承多个类,现在的继承链为:
那么一样的,我们前面了解了java的继承和多态,这种继承链也是符合上述所有的继承相关的特性,比如我们来利用OrangeCat做个试验:
1 | public static void main(String[] args) { |
结果如下:
1 | 喵喵喵~ |
可以看到,不管用谁去接收OrangeCat对象,最终输出结果都是正确的,首先OrangeCat没有重写cry方法,因此触发的是父类Cat里的cry方法,又因为自己重写了eat对象,所以eat调用时就直接触发了自己的eat方法。
同样的,playAPig方法属于OrangeCat自定义的扩展方法,它只能由OrangeCat类型的引用变量触发,因此代码块26
里,只有orangeCat3可以调用这个方法,由于OrangeCat继承了Cat,所以它也会捕鼠(catchMice)了。
我们再来看看OrangeCat具备了哪些能力:
1 | public static void main(String[] args) { |
看到了吗,经过一层层的继承,OrangeCat拥有的技能要比俩父类加起来还多,这是因为继承的特性,就是父类所有的东西都是可以给子类的(当然能不能访问就是另一回事了,下面会说访问权限相关的内容),而子类又可以重写父类的方法,如果不重写,则按照就近原则触发对应的方法,比如例子里虽然OrangeCat没有重写cry方法,但它调用cry的输出结果却是Cat类里的那个,因此,在重写方法调用上,一般遵循就近原则,此外子类也可以扩展自己的内容,比如Cat相比Animal,可以捕鼠,再比如OrangeCat相比Cat和Animal,可以扮猪。
1.7:巩固继承&多态
前面讲了那么多,现在我们通过画图的方式,系统的解释一遍继承
和多态
。
1.7.1:继承关系下,重复方法的就近调用规则
先看下我们在前面留下的3个问题:
❓ 疑问点1:为什么通过Animal的引用变量指向的Cat对象,无法调用其catchMice方法?为什么cry方法可以调用触发的却是Cat里的cry方法?
❓ 疑问点2:为什么呢?我明明是在Animal对象里调用的this.cry,为什么执行的却是子类的?
❓ 疑问点3:为什么父类的被子类重写过的方法可怜到只能通过子类的super关键词才能触发?为什么父类里也有的方法要优先执行子类里重写后的?不管在父类还是子类,用this关键词调用的资源都是优先以子类为准吗?找不到的资源才去考虑找父类吗?
先不用急着看问题,我们先来分析下一个类方法或属性在调用时,都经历了些什么。
在LV2-1
的时候,我们就知道了,对象实际上就是一个整体,被保存在了内存里,然后通过一个引用变量来指向它,然后就可以利用这个引用变量来操纵它了,现在我们来看下,正常没有继承任何类的类,是如何访问方法的:
🦜 这里说明一下,下面的流程图只是为了便于让大家理解并记住java继承关系下的访问规则,真实的java对象在内存里并不长这样(虽然确实包含属性值),方法的调用在java底层也很复杂,这些都要对jvm有深入了解才行。
如果一个类没有任何父类,那么它的方法调用就很纯粹,没有任何悬念,现在让我们的看看Cat类:
让我们再来看看更复杂的橘猫:
我们仍然可以找到一些规则,来说明在继承里方法的调用和最终触发哪个方法的问题。
好的,截止目前,集合图4
和图5
,我们解决了疑问点2
和疑问点3
,通过图4
,我们知道了this关键词取决于当前的对象究竟属于哪个类,仍然符合就近调用规则,例如Animal里面使用this.cry调用cry,如果当前对象是Animal自己,那么毫无疑问,最终触发的会是它自己的cry方法,但如果当前对象是Cat,那么this调用cry时就遵循就近规则,优先触发自己所属类(也就是Cat类)里的cry方法。
图4
告诉我们,super
关键字在子类里可用来直接调用其父类里的方法:
这个可以解答疑问点3
.
现在让我们解答下疑问点1
,为什么父类引用变量可以接收子类对象?
还记得讲基本类型时,存在大类型和小类型吗?大类型可以自动接收小类型,而小类型想要变成大类型就得强转,类同样拥有类似的规则,比如你可以把父类理解成比较大的类,子类是比较小的类,那么父类当然可以自动接收子类咯,只不过类有不一样的地方,一个子类被父类接收以后,访问域就受父类限制了:
既然规则跟基本类型差不多,当然也有类型强转啦:
1 | Animal cat = new Cat(); |
看吧,连写法都跟基本类型的强转一样,括号里加上要转的目标类型即可。
但是需要注意的是,别瞎转,比如你知道这个Animal的确是一个Cat,然后像代码块29
里那样转成Cat类型即可,但如果对象并不是Cat,那样强转语法里是允许的,但是运行时会报类型转换错误,比如下面这样:
1 | Animal dog = new Dog(); |
上面的代码在语法里不会有问题,但实际上运行时便会报错,即类型转换错误。
到目前为止,有关问题1、2、3都说完了,继承和多态这一块确实很复杂,最好能结合本节流程图记下来这个规则,以后做分析时就不会乱了。
1.7.2:属性在继承关系下的访问规则
上面只是说了方法的调用规则,却没有说属性,我们来做个试验:
现在让Cat类里也有一个叫age的属性:
1 | public class Cat extends Animal { |
然后让父类里的age访问权限变成public(详细看下方1.8
,这里改成public的目的是为了让Cat可以直接访问这个属性),然后让其初始值为15,并且在其eat方法里使用this关键词访问age:
1 | public class Animal { |
现在让我们来测试下:
1 | public static void main(String[] args){ |
输出如下:
1 | 12 |
🎑 结论:由运行结果我们可以知道,与方法的重写不同,类的属性本身该属于哪个类还是属于哪个类,记住这个规则。
好了,上面的这个试验有画蛇添足的嫌疑,因为age父类里就有,为什么Cat里还要加上一个同名的age呢?是的,真实设计类的时候没人会这样设计的,我们需要明确一个点,方法是行为,方法在继承关系里允许各种重写,但是属性作为一种没有任何行为方式的东西,你重复定义它的意义大吗?显然是不大的,而且极易跟父类混淆,所以我们说子类可以重写父类的方法,继承关系里,方法调用符合以对象所属类为参照就近调用的规则,子类还可以自己拓展父类里没有的方法,子类的对象还可以被父类引用变量接收,只是这样会影响子类对象的访问域(图7
),子类还可以拓展父类里没有的属性,但不建议子类覆盖父类的属性,这种覆盖是没有意义的,通过上面的例子我们也发现了,同名属性在类继承链里是相互隔离的。
1.8:访问权限
通过1.7
,你应该知道了某种类与父类访问规则,但是我们在LV2-2的访问权限符的介绍里,介绍了访问权限修饰符以及被它们修饰了的资源的访问权限,也就是说理想状态下1.7的流程图都是成立的,但是你有没有发现,这些方法的权限修饰符都是public
的?是的,public表示资源公开,比如Animal类里的age和name两个属性都是private
类型的,子类可以通过super
直接访问这俩属性吗?显然是不行的,必须得通过public的setName和setAge俩方法完成对name和age的赋值,这就是访问权限符的意义了,至于怎么区分能不能访问呢?我们再把那张表里有关修饰符访问等级的表贴来:
只需要记住,如果俩类为继承关系,则protected
关键词无视包目录,均可访问,其余规则全部跟普通类一样,由于之前讲过权限修饰符,因此这里不再做详细介绍。
1.9:继承和多态的好处
通过前面对继承和多态的了解,我们大体上对这俩特性有了一个认识,首先继承是为了解决什么问题的?
回归到我们的例子上,Animal里定义了符合动物特性一些行为和属性,Cat是一种动物,符合Animal定义的行为和属性,它们俩建立起父子关系后,Cat就可以直接使用父类里定义的属性和方法了,这样像eat这种方法,Cat和Dog都不需要自己定义了,这样会让我们的代码看起来很简洁,而且同样的代码逻辑块,不需要重复定义多次,有需要可以重写,子类还可以自定义自己的行为方法。
多态呢?它有什么好处?
多态就是指使用一个父类引用变量,可以接收子类对象并允许触发它内部的方法,参见图1
,那么这样的一种特性的好处究竟是什么呢?我们本节先不会涉及,之后讲JDBC
时可以展开讲,现在你要做的是,牢记java的这种机制。
1.10:抽象类
了解完了继承
和多态
,我们再来了解一种特殊的类:抽象类
什么是抽象类?让我们来改造下Animal方法,不知道你有没有发现,由Animal派生
出来的子类Cat和Dog都重写了cry方法,说明大部分时候,Animal里的cry方法都不会主动生效,因为意义不大,这时我们认为cry方法在绝大多数情况下都是需要被重写的,像这种方法,我们就可以把它定义成一个抽象方法,包含抽象方法的类被称作抽象类,普通的类是不能有抽象方法的,因此想要cry变成抽象方法,首先要把Animal变成抽象类:
1 | //利用abstract修饰的class,被称为抽象类 |
我们可以看到,抽象方法没有任何实现,所以称它为一个标准,所有直系子类都必须实现这个标准,这时Cat类继承了Animal类,如果不实现cry方法,语法上就会报错:
现在我们只能让Cat类实现这个抽象方法:
这就相当于强制让子类去重写cry方法,事实上确实需要每个动物都实现自己的cry方法,但是像eat这种方法就没有必要这样做,因为大部分动物都需要进食且都是用嘴进食的,大同小异,如果真的存在不一样的吃东西的方法,那么就跟之前一样重写就好了,但大部分动物都不需要重写eat方法,父类Animal里的已经够用了,这是在告诉你什么时候需要搞抽象方法,什么时候不需要。
现在你知道了,有的方法需要被抽象,那就定义抽象方法,因为存在抽象方法,所以类必须被声明为抽象类(abstract
关键词)。
抽象类的特点:
- 抽象类不能被实例化(也就是说不可以利用
new
关键词实例化一个抽象类) - 由于1的特性,我们认为,抽象类是为继承而生的,如果你定义了一个抽象类,而它没有任何子类,那么这个抽象类就没有任何意义
- 抽象类不一定有抽象方法,但有抽象方法的类一定是抽象类
- 抽象类可以继承抽象类,当继承了抽象类时,子类抽象类也可以不实现其父类的抽象方法,全部交给非抽象的子类去做
- 抽象类也可以继承普通类,也可以选择重写普通类的方法,更神奇的是,它还可以让通过重写,让普通类的方法变成一个抽象方法
- 抽象类虽然不可以被实例化,但是它仍然存在构造器(这是句废话,抽象类是天生的父类,父类构造器在子类被实例化的时候会自动调用一次,所以作为天生的父类,它必有构造器)
🌵 思考:
现在Animal定义如下:
Cat类是它的子类,定义如下:
结合前面对继承和多态以及本部分对抽象类的理解,请问如下测试代码输出结果是什么?
1.11:万类の父-Object
不知道你有没有发现,任何对象都自动有这些方法:
这是因为类被定义出来,就隐藏继承了一个父类,名叫Object
,这是java自带的类,不需要显式的extends出来。
二、封装
封装充满我们代码,还是以前面的例子为准,我们认为对一类操作使用一个方法定义来圈起来,就是封装,比如eat、cry等方法,例子里面很简单,只打印了一句话,但实际开发中,一个方法可能会完成很复杂的操作,这里拿之前一个例子来说明问题:
1 | public class Cat { |
通过本例,我们知道了封装的目的是减少代码的重复度,提高复用率,这点是不是跟继承也很像?继承的意义之一也是为了解决代码重复度的。
封装仅仅只是为了解决重复度吗?并不是,通过之前的Animal的例子,我们发现像age、name这种属性的赋值操作也被封装成了一个个方法(setName和setAge),而Animal里的name和age全是private的,这是为什么呢?为什么不让age和name变成protected或public这种权限呢?让别的类直接赋值不是更好?为啥还要多此一举搞俩专门赋值的方法?
这就体现了封装的另外一层意义:隔离
现在我们试着将Animal里的age变成public的,来看看下面:
1 | Animal animal = new Animal(); |
但是Animal这时不干了,你见过能活10000岁的生物吗??因此Animal把自己的age封了起来,首先把访问权限变成private,然后提供一个setAge方法用来给age赋值,setAge就是Animal对外封装的一个专门给age赋值的方法,这个时候Animal就由之前的被动状态变成主动状态了,如果你不想让别人设置的age太过分,就可以在setAge里做限制,反正是自己的方法嘛,还不是想怎么设计怎么设计,让我们来改一下Animal里setAge的逻辑:
1 | public void setAge(int age) { |
看看,这样age的赋值就完全被隔离进本类内了,外界进行赋值时就不敢那么放肆了~
封装的意义:
- 封装的意义在于保护或者防止代码(数据)被我们无意中破坏。
- 保护成员属性,不让类以外的程序直接访问和修改;
- 隐藏方法细节
这也是为啥我们在定义一个类的时候,属性经常被设置成private
访问级别的,然后通过定义赋值方法来让外界给自己的属性赋值。
关于对象封装的原则:
- 内聚:内聚是指一个模块内部各个部分之间的关联程度
- 耦合:耦合指各个模块之前的关联程度
- 封装原则:隐藏对象的属性和实现细节,仅对外公开访问方法,并且控制访问级别
- 在面向对象方法中,用类来实现上面的要求。用类实现封装,用封装来实现高内聚,低耦合。
↑上面的话先读一下,做多了东西自然就懂了,我们做开发时所作的封装、多态等,都是为了这一个目标:高内聚、低耦合来进行的。