Java面向对象(下)
本文继续讲述Java面向对象的相关知识,主要介绍继承和多态,还涉及到final关键字、抽象类、接口、包和控制访问
1. 类的继承
描述类的所属关系,主要用于已有类属性和行为的沿用和扩展。被沿用的类称为父类(基类)
、新的类称为子类(派生类)
。
值得注意的是,Java只支持单继承,就像你只有一个亲生父亲一样,一个子类只能继承一个父类。但是你却还有一个爷爷、一个太爷爷,我们称之为多层继承
。换句话说,Java类可以有无限多个间接父类。
1.1 重写父类方法
我们已经知道子类能够继承父类的属性和方法,但是父类的方法往往不能令我们十分满意,就像是你时常也会不太理解你的父亲那样。在生活中我们往往会参考父亲的建议来使用我们自己的方法,Java中也是同样,这个过程称为方法的重写或覆盖。方法重写要求我们重写的方法名、返回值类型和参数列表需要相同。
在这里需要注意访问权限的问题,子类重写的方法不能有比父类更严格的访问权限。打个比方,这就像是你想要改变父亲的想法,但前提是你不能瞒着他,必须要让他知道你改在哪儿了。
1.2 super关键字
当子类重写父类的方法之后,子类的对象便无法再直接访问父类中被重写的方法了,想要重新访问重写之前的父类方法就需要super关键字。这有点类似于我们常说的长幼有序,在未经许可的情况下,你敢直接翻阅长辈的隐私吗?
当我们想要实例化一个子类对象时,会首先调用父类无参数的构造方法,然后再调用子类的构造方法。子类在继承父类时不会继承父类的构造方法,但是子类可以调用父类的构造方法。
结合我们之前所学过的this关键字,有如下总结:
- 同一个类下,在一个构造方法中调用另一个构造方法使用this()关键字;
在子类中想要调用父类的构造方法应该使用super()关键字;
this和super使用的相同之处:该语句必须位于构造方法的第一行;
除此之外还需要注意,当子类的在构造方法中想要调用父类的构造方法时,没有显式的调用则会默认调用父类中无参数的构造方法,而如果恰巧父类的构造方法都已经定义成有参数的构造方法,那么此时系统就不会自动生成无参数的构造方法,这样就会导致子类调用父类的构造方法出现错误.
1 | class Father { |
运行则显示错误
- 如果父类没有无参数的构造方法,想要调用父类的构造方法就需要在子类构造方法的首句使用
super(参数)
来手动调用。
1.3 final关键字
final的产生是出于安全考虑,被final修饰的值不能再被继承或修改.
final关键词能够修饰的类型
- final修饰类:该类不能被继承
- final修饰方法:该方法不能被重写
- final修饰变量:该变量初始化后不能再被修改,需要注意的是,一旦变量被final修饰,系统就不会默认初始化,这就需要需要我们显式的对final修饰的变量初始化(声明时初始化/在构造方法中初始化)。
- final修饰的引用:该引用不能再指向其他的对象。
2. 抽象类和接口
抽象类和接口都用于为对象定义共同的行为,两者在很大程度上是可以相互替换的,下面我们通过他们的区别来认识抽象类和接口。
2.1 抽象类
面向对象的概念强调,所有的对象都是通过类来描述的。但是并不是所有的类都是用来描述对象的,比如抽象类。
抽象类中的方法称为抽象方法,它没有具体的方法体,只含有方法的声明,使用关键字abstract 修饰。需要注意的是:抽象类不能够被实例化,即不能通过new创建对象。想要实现抽象方法必须通过子类继承之后重写方法,并且必须重写抽象类中的全部抽象方法,否则子类也需要声明称抽象类。
关于抽象方法:
- 不能使用static修饰,原因:抽象类不能实例化,而被static修饰的变量要求可以通过类直接访问。
- 不能使用private修饰,原因:限制了子类的访问权限。
- 不能通过final修饰,原因:抽象方法在子类中需要被重写
2.2 接口
接口是全局变量和公共抽象方法的合集,通过interface
关键字来修饰接口,比较特殊的是:接口中的变量和方法都包含默认的修饰符public static final
,即全局常量
2.2.1 接口的实现
接口中同样包含抽象方法,因此也不能实例化。想要实现接口,Java提供了implements关键字,用于实现多个接口。
1 | class 类名 implements 接口列表 { |
2.2.2 接口的继承
除了一个接口能够继承多个父接口,其他的继承规则和类的继承相同。
1 | interface 接口名 extends 接口列表Interface1,Interface2…… { |
和抽象类相似,除非被声明abstract,否则该类需要实现所有继承的接口。
2.3 抽象类和接口的关系
- 抽象类可以提供成员方法的实现细节,但是接口只能方法的声明
- 抽象类中的变量可以是各种类型,但是接口中只能是public static final类型的变量
- 一个类只能继承一个抽象类,但是一个类却能够继承多个接口
抽象类是对整个事物的抽象,即对类抽象,而接口确实对行为抽象,即对类的局部进行抽象。
3. 多态
3.1 多态的概念
多态就是同一个行为具有多个不同表现形式或形态的能力,也就是同一个接口在不同的实例中可以执行不同的操作。我们平时在所使用的电脑快捷键能帮助我们更好的理解多态,当在不同程序中的快捷键的按键相同时,快捷键的具体的功能就要看你究竟打开的是哪一个程序了。
在了解多态之前我们必须要明白的知识:
- 访问一个对象的唯一方法就是通过引用型变量,而引用型变量可以声明为类类型或者接口类型
- 引用类型的变量只能有一种类型,一旦被申明,就不能再改变
- 引用型变量能够被重置成其他对象(当这些对象没有被final修饰时),还可以引用和他类型相同或者相兼容的对象。
多态得以实现的基本原理:
- 编译阶段:编译器会在父类的class文件中查找调用的方法进行绑定,能够成功找到就能编译成功。(编译阶段属于静态绑定)
- 运行阶段:运行阶段实际上是在动态的执行子类的方法。(运行阶段的动态绑定)
- 多态表示多种形态:在编译阶段只绑定了一种形态,而在运行的时候动态绑定了多种形态。程序要能够运行必须同时满足静态绑定和动态绑定。
3.2 对象的类型转换
可以将同一个继承结构中的对象的类型转化成另一种类型,分为一下两种转化:
向上转型,从父类(超类)转型成子类,隐式转型。通常情况下,向上转移都是由编译器隐式执行的
向上转换主要用于在需要使用子类对象的时候,通过父类型的引用指向子类型的变量
向下转型,从子类转型成父类,显式转型
向下转换主要为了能够调用子类特有的方法
3.2.1 向上转型的方法
向上转型比较简单,就是将父类的引用指向子类的变量
1 | public class Animal { |
当我们运行:Animal a1 = new Cat();
时,就进行了隐式的向上转换。
3.2.2 向下转型的方法
向下转换则稍微复杂一些,父类型转化成子类型必须通过显式强制类型转换,并且必须确保父类引用变量是子类的一个实例(子类一定是父类的一个实例,但是父类却不一定是子类的一个实例),例如上述代码中再加入:
1 | public class Animal { |
如果在主函数中运行:
1 | Animal c1 = new Cat(); |
在执行了上述两行程序之后,这时任意给一个Animal变量就不能直接确定究竟是Cat子类还是Dog子类了,运行结果就会出现错误,这就是类型转换异常。
为了解决该问题,好的习惯是运用instanceof运算
来判断父类变量是否是该子类的一个实例:
1 | Cat x = nUll; |
3.2.3 为什么要使用向下转型
向下转型的主要作用是为了使父类能能够调用子类独有的方法,示例如下:
1 | import java.awt.*; |
需要注意的专业问题:
向上转型、向下转型和自动类型转换、强制类型转换的区别,前者用于说明引用类型的转换,而后者只用于说明基本数据类型之间的转换。
3.3多态在实际开发中的作用
当我们想要给软件扩展新的需求的时候,我们可以选择修改原先版本中的类或者方法,但是根据软件开发原则中的OCP(开闭原则),对扩展开放、对修改关闭。因为修改会使得系统的稳定性变差,未知的风险变多。
这时就需要我们通过多态来扩展系统的功能,在我们编程的时候,我们要面向抽象对象编程,而不是面向具体的对象来编程,面向具体的对象来编程会使得程序的扩展性变差。
3.4 Object类
Object类是所有类的父类,一个类如果没有显式的继承父类,那么他的父类默认是Object。在Object类中我们主要学习两种方法,它们分别是toString()方法和equals()方法。
3.4.1 toString()方法
- toString ()方法的作用:可以将一个Java对象转换成字符串的表示形式
- toString()方法返回的形式:类名+@+对象内存地址的十六进制的形式
toString()的输出结果往往不是我们想要的结果, toString()方法希望我们能够重写自己需要的输出,这样在我们直接输出引用类型的时候,会自动调用toString()方法得到我们想要的结果。
1 | class Message { |
3.4.2 equals()方法
- equals()方法的作用:判断两个对象是否相等,源代码如下:
1 | public boolean equals(Object obj) { |
程序中的基本数据类型可以通过“==”进行相等的判断,但是因为“==”号是判断两个数据的内存地址是否相等。
由equals()方法的源代码可知,原始的equals()方法只能判断两个对象的内存地址是否相等,要想判断引用对象的数据是否相等,需要重写equals()方法。在上述代码中的Message类中重写如下:
1 | //重写equals()方法 |
但是上述代码在运行过程中的效率并不高,我们对其进行第一次优化:
- 优化体现在先进行特殊条件的判断,再进行一般条件下的相等判定
1 | public boolean equals(Object obj) { |
上述代码的语句较为繁琐,可以进行第二次优化
- 优化体现在合并了条件的判断,减少了变量的声明。
1 | public boolean equals(Object obj) { |
毕竟一开始要求我们重写系统方法就要尽可能地实现简洁易懂,所以这两步的优化也是理所应该的。优化到这一步得到的equals()方法就是最终的编写框架,这种编写框架是固定的!
意料之外的是
IDEA这个强大的编译器竟然能够自动生成许多重写方法以及构造方法。 按下快捷键
alt+Ins
就能够选择自动生成的方法了。不知道IDEA还隐藏了多少如此强大的惊喜!尽管我们知道源代码希望我们根据自己的需求重写toString()方法和equals()方法,但是有一个例外,就是string类型,我们知道string不属于基本数据类型,他本质上也是一个类,并且在源代码中已经重写的toString()和equals(),故在我们比较两个字符串是否相等的时候不能使用“==”,而应该直接调用equals()方法。输出String类型的引用时也可以使用toString()
总结:
1.toString()方法需要重写,重写的越简单越好,当我们使用
System.out.println(引用)
的时候,系统会自动调通toString()方法。2.String类的toString()方法默认被重写过,可以直接调用来输出String类。
3.equals()方法所有类都需要进行重写,Object中默认给出的equals()方法是在比较两个对象的内存地址是否相等。
4.基本数据类型的比较使用“==”,引用数据类型的比较全部使用重写过后的equals()方法。
5.String类的equals()方法默认被重写过,可以直接调用来比较String类。
6.equals()方法需要重写彻底,在重写一个equals()方法时调用另一个对象的equals()方法时需要检查另一个是已经重写
7.无论是重写toString()方法还是equals()方法,都有固定的最简格式。
3.4.3 finalize()方法 (额外了解一下)
finalize()方法是当一个对象将要回收时,JVM的垃圾回收器自动调用的方法。在Object类中的源代码是
1 | protected void finalize() throws Throwable() |
在具体使用时,finalize()方法可以看成是一个时机,就是在马上要被回收的对象的弥留之际,如果还有需要执行的代码,就写在finalize()方法中,我愿意把它称之为 Java对象的遗书
,例如:我们想要记录该对象是什么时候被GC(Garbage Collection)回收释放的。
他与之前提到的两个方法不同的是,我们只需要重写finalize()方法,方法的具体执行时间是GC决定,在我们看来这是自动的。
Java中的垃圾回收器的具体执行时间不能够确定,当垃圾池中垃圾的量累计到一定程度的时候GC才会进行垃圾回收。
但是有代码可以 建议 垃圾回收器启动(建议:可能会启动)
1 system.gc();finalize()方法自从JDK9之后就已经被淘汰!!!!!!
恭喜你,白学啦!
4.包
4.1 package概叙
package是Java为了方便程序的管理而建立的机制,不同功能的类放置在不同的包下。package是一个关键字,声明了一个子目录的位置,package.包名
该语句只允许出现在源代码的第一行。
在今后的开发中,包的命名机制一般都会采用公司域名的倒叙方式(具有唯一性),命名规范:
公司域名倒叙 + 项目名 + 模块名 + 功能名
4.2 import概叙
A类需要使用B类的时候:
当他们两个在同一个包下的时候不需要使用import就能够直接使用。除此之外,使用java.lang中的直接类也不需要使用import调用
当他们两个不在同一个包下的时候就需要在A中使用import来调用B类所在的包
import 的使用方法:
1
2 >import 包名.类名 //表示导入包中的单个类
>import 报名.* //表示导入包层次下的全部类import使用的位置:只能出现在package语句之下,class语句之上
5.访问控制权限
访问控制权限 | 表示 | 控制的范围 |
---|---|---|
private | 私有的 | 只能在本类中访问 |
public | 公共的 | 在任何位置都可以访问 |
protected | 受保护的 | 只能在本类、同包、子类中访问 |
默认的 | 默认只能在本类或者通报下访问 |
访问的权限严格程度排名(从严到松):
private→默认→protected→public
访问控制权限可以修饰:属性、方法、类、接口,但是类和接口只能使用public
和默认
两种权限来修饰。
总结
总结一下面向对象的编程,它具有三大特性:封装、继承、多态。三个环节是密切相关的,封装产生了对象的整体概念,继承就是用来描述对象与对象之间的关系,有了继承之后,才有方法的覆盖和多态。