01-面向对象(继承-概述)
接下来讲面向对象的另一个特征:继承。
我们发现,学生和工人都有姓名和年龄属性。
思考之后,将学生和工人的共性描述提取出来,单独进行描述,只要让学生和工人与单独描述的这个类有关系,就可以了。
引入一个新的关键词:extends(继承)。
Person是基类,学生和工人是Person的子类。
继承的作用:
1,提高了代码的复用性。
2,让类与类之间产生了关系。有了这个关系,才有了多态的特性。
注意:
千万不要为了获取其他类的功能,简化代码而继承。
必须是类与类之间有所属关系才可以继承。所属关系:* is a *
有个例子,哈哈哈哈,比如别人有个iphoneXS,你想玩,但不能为了玩它管别人叫爸爸,哈哈哈。这个时候就不需要继承。
02-面向对象(继承-概述2)
怎么判断是否有所属关系呢?
如果父类中的内容,子类都有,则有所属关系。如果父类中有的内容子类没有,则没有所属关系。
比如:
在这个例子中,B继承A之后,B拿到demo1的同时,也拿到demo2,而它不需要demo2,所以它们没有所属关系。
虽然A和B之间没有直接继承关系,但是它们之间有共性。
将共性抽取出来,封装进类C中:
A和B继承C即可。
在Java语言中:java只支持单继承,不支持多继承。
因为多继承容易带来隐患。
假设,C继承了A和B:
这时c.show()该打印a还是b?
多继承带来的安全隐患:
当多个父类中定义了相同功能,当功能内容不同时,子类对象不确定要运行哪一个。
而C++支持多继承,所以Java语言在这一方面相对比C++做了优化。
但是java保留了这种机制。并用另一种体现形式来完成表示,叫做多实现,也就是对多继承的一个改良,这个知识点后面会说。
java支持多层继承。
C继承B,B继承A这样。
那么,如果创建c对象,能不能使用a中内容呢?
可以。
A,B,C就相当于爷爷、父亲、孙子的关系,父亲继承了爷爷的东西,孙子当然可以用爷爷的东西。
java支持多层继承。也就是一个继承体系。
如何使用一个继承体系中的功能呢?
想要使用体系,先查阅体系父类的描述,因为父类中定义的是该体系中的共性功能。
通过了解共性功能,就可以知道该体系的基本功能。
那么这个体系已经可以基本使用了。
那么在具体调用时,要创建最子类的对象,为什么呢?一是因为有可能父类不能创建对象,二是创建子类对象可以使用更多的功能,包括基本的也包括特有的。
简单一句话:查阅父类功能,创建子类对象使用功能。
实际开发的时候就按这个规律来找,否则一个一个查是查不完的。
03-面向对象(聚集关系)
Java中类与类之间的关系不只继承这一种,还有其他关系。
聚集关系(has a),分成两种,组合和聚合,它们俩的紧密联系程度会稍有不同。
组合:手是人身体的一部分。
聚合:球员是球队中的一部分。
手是人身体不可或缺的一部分,人没有手是不行的,但球队离开某一个球员就没有这么严重的后果。
04-面向对象(子父类中变量的特点)
类中成员:
1,变量。
2,函数。
3,构造函数。
子父类出现后,类成员的特点:
1.变量
当父类与子类中出现同名变量时,打印的是子类:
但是又想在子类中访问父类的num,为了区分这里引入一个新的关键词:super。
如果子类中出现非私有的同名成员变量时,子类要访问本类中的变量,用this;子类要访问父类中的同名变量,用super。
super的使用和this的使用几乎一致。
this代表的是本类对象的引用,super代表的是父类对象的引用。
一个例子:
这种情况下,super.num和this.num打印出来的都是4。
可以有两种理解方式:
第一种,子类继承了父类,那么子类中相当于有自己的num,那么this就是用自己的,没有问题。
第二种,super是引用没有错,this是引用没有错,这个时候this和super指向的都是同一个对象:new zi();
而super代表的是父类引用,现在没有父类对象,只有子类对象,那么super和this指向的是同一个对象。
其实挺不好理解的,this指向子类的对象可以理解,因为是本类的,super为什么也指向子类的对象呢?
这个就是面向对象的第三个特征:多态,父类引用指向子类对象。
比如过来一只猫,我们说,诶~这动物好可爱,这种说法也没错~
再比如我想要一只动物,你给我一只猫,也没毛病~
这个后面还会仔细讲。
05-面向对象(子父类中函数的特点-覆盖)
刚刚所说的子父类中出现同名变量的例子其实很少见,因为父类已经有这个变量了,子类可以直接获取这个变量,就没有自己再定义的必要了。
接下来说另一部分,函数。
2.函数
当子类出现和父类一模一样的函数时,子类对象调用该函数,会运行子类函数的内容,如同父类的函数被覆盖一样。
这种情况是函数的另一个特性:重写(覆盖)。
父类函数其实还在内存当中,只是没有运行而已,不要真的以为它被覆盖掉了~
一个例子,框出的两个函数功能是相同的:
当子类沿袭了父类的功能到子类中,但是子类已经具备该功能,只是功能的内容和父类稍有区别。这时,没有必要定义新功能,而是使用覆盖特性,保留父类的功能定义,并重写功能内容。
注意:修改源码绝对是灾难。
因此,不用重新定义speak2函数,只需要重写父类speak函数即可:
手机的例子,出了新款手机,加了新的功能,我们不需要在源码中修改,只需要继承、复写它里面的功能,就可以啦:
复写还可以用于扩展。
这个升级我们发现如上那种方式写也可以,但还会有点麻烦,比如来电号码,父类已经完成这个了,子类只需要在原有功能上加个姓名和照片就完事了。
注意一定不要写成this.show();,否则会变成递归,自己调用自己,陷入死循环。
覆盖的注意事项:
1,子类覆盖父类,必须保证子类权限大于等于父类权限,才可以覆盖,否则编译失败。
2,静态只能覆盖静态。(如果覆盖非静态,想想先加载后加载的问题,就知道不可以)
我们之前用的权限修饰符有private,public,但是也有时候没有用修饰符,这个时候是什么权限呢?默认权限。默认权限介于私有和公有之间。
像这种就不能叫覆盖,子类都不知道父类有这个方法。
这样也不行哦,子类权限并不比父类权限大:
记住:
重载:只看同名函数的参数列表。
重写:子父类方法要一模一样,包括返回值类型。(当然,在多态中,父子类的返回值类型是可以有不同的)
比如,像下面这种情况,调show()谁运行呢?
这样会报错的,因为虚拟机也不知道该运行哪个。
06-面向对象(子父类中构造函数的特点-子类实例化过程)
3.子父类中的构造函数。
我们new一个Zi,运行结果是这样滴:
这是为什么捏?
这个原因是因为,子类的构造函数前面其实有一条隐式的语句,这句话你不写,系统会自动默默加在前面:
在对子类对象进行初始化时,父类的构造函数也会运行,那是因为子类的构造函数默认第一行有一条隐式的语句 super();
super():会访问父类中空参数的构造函数。而且子类中所有的构造函数默认第一行都是super();
再写一个带参数的构造函数试试:
这个构造函数也会运行父类构造函数哦:
即使我们再写一个父类的构造函数,也不会影响最后的结果,因为和新写的构造函数并没有关系,只和父类空参数的构造函数有关哦。
试着改一下父类的空参数的构造函数,改成有参的,这个时候程序挂掉了:
这个时候我们的解决方案是,在子类构造函数中手动指定父类的构造函数,像酱紫:
程序就没有问题啦。
那么, 为什么子类一定要访问父类中的构造函数?
因为父类中的数据子类可以直接获取。所以子类对象在建立时,需要先查看父类是如何对这些数据进行初始化的。
所以子类在对象初始化时,要先访问一下父类中的构造函数。
如果要访问父类中指定的构造函数,可以通过手动定义super语句的方式来指定。
注意:
super语句一定定义在子类构造函数的第一行。
这里有个例子:
第二个构造函数第一行是this(),调用了本类的空参数构造函数,这个时候就不调用父类的空参数构造函数super()啦。
可是不是说子类在对象初始化时,要先访问一下父类中的构造函数吗?
没关系,第二个构造函数不是调用了本类的空参数的构造函数嘛,本类的空参数的构造函数的第一行就是super()呀,逃不过哒。
结论:
子类的所有的构造函数,默认都会访问父类中空参数的构造函数。
因为子类每一个构造函数内的第一行都有一句隐式super();
当父类中没有空参数的构造函数时,子类必须手动通过super();语句形式来指定要访问的父类中的构造函数。
当然,子类的构造函数第一行也可以手动指定this();语句来访问本类中的构造函数。子类中至少会有一个构造函数会访问父类中的构造函数。
其实,这个过程就叫做子类的实例化过程。
那父类中有木有super();语句呢?
有的。
它找的是谁呢?
它找的是父类的父类。
而Java当中有一个类特别牛,叫做所有类的父类。就是Object。
还有个问题,为什么this();和super();不能在同一行?
因为它们都只能写第一行。
为什么必须要写第一行?
因为初始化动作要先做。
07-面向对象(final关键字)
final关键字:
final关键字是什么意思呢?
最终。
final关键字有什么特点呢?
1,final可以修饰类,方法,变量。
2,final修饰的类不可以被继承。
继承的出现很有好处呀,复用性提高了,而且也有多态了。
但是继承有一个弊端,就是它打破了封装性。
什么意思呢?
本来写一个类写的好好的,定义了一些功能,子类不好好用这些功能,还要把它复写,输出一个哈哈!哼!好过分!
所以集成有利有弊,它的出现对封装性是一个挑战,里面一些东西反倒不是隐藏了,还能复写。
比如说,搞了一个类,里面有几个功能,有些功能调用的直接是虚拟机底层的内容。换句话说,调用了系统内容了。而你把它一复写,把功能改掉了,这个功能还能遇到底层吗?
为了进一步保证封装性,还能有继承的特性,叫做:有些类我不让你继承,这里面的功能你不允许复写,它是固定的,要么用要么不用。
所以,为了避免被继承,被子类复写,定义类的时候加上修饰符final,这个类就叫最终类,不能被子类所继承。
如下例,再继承就会出错:
3,final修饰的方法不可以被覆盖。
比如现在有个类,里面有十个功能,里面4个是调用系统功能不能被复写,另外6个是可以被复写的,这个时候就不能把这个类都定义为final,否则都不能复写了。
这个时候,只对不允许被复写的方法修饰final就好了。
如下例,覆盖了final修饰的方法会报错:
4,final修饰的变量是一个常量,只能被赋值一次。既可以修饰成员变量,又可以修饰局部变量。
加了final修饰符后,x就终身为3,y就终身为4了:
把y重新赋值会怎样呢?
程序会挂掉的:
当在描述事物时,一些数据的出现值是固定的,那么这时为了增强阅读性,都给这些值起个名字,方便于阅读。
而这个值不需要改变,所以加上final修饰。作为常量:常量的书写规范是所有字母都大写,如果由多个单词组成,单词通过_连接。
比如圆周率π:
像这样的数据以后还会碰到很多,都是把一些特定的数据用一些名称来标识。
注意:
以后写程序的过程中,但凡出现了一些固定数据,即使只出现一次,也建议把这个数据起个名字,人们阅读这个名字,绝对要比阅读数字来得快来得方便。当然前提是这个数据不能变。
其实final的作用已经体现出来了,就相当于一个锁变量罐,对于一个变量,把final一加,这个变量空间就锁住了,值就固定在里面了,再往里面进值就进不去了。
而成员变量在不变化的情况下,往往还会跟上一个修饰符:static。如果再加上public,就变成全局常量了,类名就可以直接访问。
5,内部类定义在类中的局部位置上时,只能访问该局部被final修饰的局部变量。
08-面向对象(抽象类)
当多个类中出现相同功能,但是功能主体不同,这时可以进行向上抽取。
这时,只抽取功能定义,而不抽取功能主体。
引入修饰符:abstract(抽象),被它修饰的方法意思就是看不懂的方法。
抽象方法必须定义在抽象类中。
这样有个好处,别人都知道它是抽象类,就不会创建它的对象。否则创建了它的对象,调用其中的抽象方法,并没有意义。
例:
抽象类的特点:
1,抽象方法一定在抽象类中。
2,抽象方法和抽象类都必须被abstract关键字修饰。
3,抽象类不可以用new创建对象,因为调用抽象方法没意义。
我们试着new一下,会报错:
(就算我们把类和方法前面的abstract修饰符撤掉,再建立该类的对象并调用那个空方法,而且运行也不会报错,可是它又有什么意义呢?并没有。)
4,抽象类中的方法要被使用,必须由子类复写其所有的抽象方法后,建立子类对象调用。
如果子类只覆盖了部分抽象方法,那么该子类还是一个抽象类。
09-面向对象(抽象类2)
抽象方法有一个好处,就是必须被复写,强迫子类去完成复写这件事情。
如果没有抽象,那子类可以偷懒直接拿去用。但抽象了,子类就必须复写,完成自己的特定功能。也算是功能分工的明确。
抽象类中可以有非抽象方法。
抽象类和一般类没有太大的不同。
该如何描述事物,就如何描述事物,只不过,该事物出现了一些看不懂的东西。
这些不确定的部分,也是该事物的功能,需要明确出现,但是无法定义主体。这些通过抽象方法来表示。凡是功能不确定的,就沿袭到子类去做,但是它也属于父类的一部分。
抽象类和一般类的一丢丢小小不同:
1,抽象类比一般类多了抽象函数。就是在类中可以定义抽象方法。
抽象类中可以不定义抽象方法吗?
这是可以的。
那它岂不是没用嘛?
有用的。
有一个特殊作用,但不多见:抽象类中可以不定义抽象方法,这样做仅仅是不让该类建立对象。
2,抽象类不可以实例化。
PS:abstract可以修饰类、方法,但不可以修饰变量哦,没有抽象变量这一说。
10-面向对象(抽象类练习)
接下来做一个练习:
分析:
定义员工类:
工作内容是这个方法是抽象的,因为每个人干的活都不一样。
经理类继承员工类:
经理干经理的活,复写工作内容的抽象方法。
再来一个普通员工,普通员工也复写工作内容这个抽象方法,普通员工干普通员工的活:
11-面向对象(模版方法模式)
一个例子:
需求:获取一段程序运行的时间。
原理:获取程序开始和结束的时间并相减即可。
获取时间用到的方法:System.currentTimeMillis();
定义一个类,在其中写一个获取时间方法,建立对象并运行获取时间方法:
成功获取到时间啦。
接下来有一个小问题,如果想把中间那段代码换成其他代码该怎么办呢?
一次一次在代码中间手动改很麻烦。
可不可以把这部分可以修改的代码单独封装一下呢?
这样可以吗:
想用到这个方法的时候,只需继承这个类,复写runcode()方法就好啦。
虽然调用的依然是父类的getTime()方法,但由于子类已经将runcode()覆盖掉了,所以父类的getTime()方法运行的是子类的runcode()方法。
这样就很方便程序扩展。
那么再思考一下,我们定义了一个获取时间的功能,功能中获取的是哪段代码的时间,这段代码不明确,不确定则必然是抽象的。
此时这个类也变成了抽象类:
因为我们这个类就是提供了获取时间的功能,getTime()方法提供的功能就是获取时间,所以不允许复写,用final修饰:
到目前为止,这个类就是能够获得某段代码运行时间的一个类了:
当代码完成优化后,就可以解决这类问题。
这种方式,我们取名为模板方法设计模式。
什么是模板方法呢?
在定义功能时,功能的一部分是确定的,但是有一部分是不确定的,而确定的部分在使用不确定的部分,那么这时就将不确定的部分暴露出去,由该类的子类去完成。
啥叫模板呢?
就跟做月饼那个模子一样,做出来的形状是确定的,但是馅儿是不确定的,我们可以豆沙、放蛋黄、放芝士~流口水ing~~~
但是注意一点,不确定的那段代码不一定是抽象的哦,有可能这个方法里面有默认的内容,你不爽你可以复写~确定的那段代码也不一定是固定的要用final修饰,这里我们不愿意让它被复写所以用final修饰了,如果不是很介意的话也可以不用final修饰。
12-面向对象(接口)
我们发现,抽象类中可以定义抽象方法,也可以定义非抽象方法,那么如果抽象类中的方法全都是抽象的,这个时候我们可以把它转换成另外一种体现形式,叫做接口。
当然接口产生的原因并不是上段所说的,哈哈,只是正好说到这里很形象的带到这里了。
接口:初期理解,可以认为是一个特殊的抽象类。
当抽象类中的方法都是抽象的,那么该类可以通过接口的形式来表示。
class用于定义类。
interface用于定义接口。
接口定义时,格式特点:
1,接口中常见定义:常量,抽象方法。
2,接口中的成员都有固定修饰符。
常量:public static final
方法:public abstract
记住:接口中的成员都是public的。
比如:
当然,在写的过程中我们也可以将常量和方法前面的固定修饰符省略掉,也不影响,因为只要前面使用interface修饰的,里面的成员都是默认有固定修饰符的。
但是这样省略写法的话阅读性就会很差,我们就不知道它是公有的,也不知道它是可以被名称访问的。所以写的时候还是写全最好。
而且直接写void show();会引起误解。
比如正常写void() show();编译的时候会出现错误提示:
这个时候要么补一个方法主体,要么声明抽象。
接口能创建对象吗?
肯定不能。
这个时候就搞一个类去实现它。
类与类之间是继承关系,而类与接口之间就变成了实现关系:implements。
类为什么不是实现类呢?这个不一定哦。不过叫继承是因为类里面一般会有一些非抽象方法可以直接拿来用,这个子类不用太清楚。而接口里面全都是抽象方法,子类拿过来全部要复写。所以这个时候就需要更确切的表达方式,子类要将接口中的方法全都实现后才能实例。否则,子类就是一个抽象类。
还用刚才那个例子:
其中NUM的调用方式有三种:
分别是:对象调用、类名调用、接口调用。
13-面向对象(接口2)
Java不支持多继承,但它有另一个方式:多实现。
接口可以被类多实现。
比如这就是多实现:
不支持多继承的原因是担心方法会有重复,担心子类调用的问题。
那么为什么接口就没这个问题呢?
其实关键就是一点:多继承里的方法有方法体,多实现里的方法没有方法体,你爱怎么弄怎么弄~
假如真的有两个接口的方法都叫show():
接口中的show()方法都没有主体,可以由子类来任意定义。
搞定~
一个类在继承一个类的同时,还可以实现多个接口。
先继承,再实现,可以扩展这个类的功能。
接口与接口之间其实也有关系:继承。
例:
还有更厉害的呢。接口之间可以多继承,像酱紫:
注意:别这么写哦,这么设计是有问题的:
编译器会报错的。
14-面向对象(接口的特点)
定义接口可以有什么好处捏?
举个例子。
以前想换CPU了直接换一个主板,但是我们在主板上弄一个槽来插CPU,下次想换CPU直接把CPU拔出来,换一个新的就好了。CPU更新换代快,这样做可以在更新CPU的前提下提高主板的复用性、功能的扩展性。
这个槽就相当于接口。
接口的特点:
1,接口是对外暴露的规则。
2,接口是程序的功能扩展。
3,接口可以用来多实现。
卡槽的出现降低了CPU和主板的耦合性,接口的出现也降低了这两个设备的耦合性。
Intel的CPU既可以插在华硕的板子上,也可以插在别的板子上。只要它们定义的是同一个插槽就都能用。
什么开发最难受?
就是依赖性太强。你没做,我也不能做。你做不出来,我也动不了。
降低了耦合性,你做你的,我做我的,就很好了,模块化的开发。
再举个例子。
笔记本的USB接口。想要升级硬盘,这个时候只要能对得上这个口,不用拆电脑也可以实现升级。买个移动硬盘咔一插,就扩展了。
4,类与接口之间是实现关系,而且类可以继承一个类的同时实现多个接口。
5,接口与接口之间可以有继承关系。
15-面向对象(接口举例体现)
接下来通过一个实例来说明接口这个事情。
这个实例可能稍微有点不恰当,但能说清楚问题。
定义了一个学生类,其中有学习和睡觉的行为。
张三继承了学生类,具有所有学生具有的行为。张三还是一个烟民,有抽烟的行为。这时定义了一个抽烟的接口,张三继承学生类的同时实现了抽烟接口。
而李四不抽烟,所以他继承学生类就好了,不用实现抽烟的接口。
因此,接口可以用于类功能的扩展。
再来一个运动员的例子。
运动员都有运动的行为,有的运动员还学Java。基本功能定义在(父)类中,扩展功能定义在接口中。
大概酱紫: