Java面向对象(下)

​ 本文继续讲述Java面向对象的相关知识,主要介绍继承和多态,还涉及到final关键字、抽象类、接口、包和控制访问

1. 类的继承

​ 描述类的所属关系,主要用于已有类属性和行为的沿用和扩展。被沿用的类称为父类(基类)、新的类称为子类(派生类)

​ 值得注意的是,Java只支持单继承,就像你只有一个亲生父亲一样,一个子类只能继承一个父类。但是你却还有一个爷爷、一个太爷爷,我们称之为多层继承。换句话说,Java类可以有无限多个间接父类。

1.1 重写父类方法

​ 我们已经知道子类能够继承父类的属性和方法,但是父类的方法往往不能令我们十分满意,就像是你时常也会不太理解你的父亲那样。在生活中我们往往会参考父亲的建议来使用我们自己的方法,Java中也是同样,这个过程称为方法的重写或覆盖。方法重写要求我们重写的方法名、返回值类型和参数列表需要相同。

​ 在这里需要注意访问权限的问题,子类重写的方法不能有比父类更严格的访问权限。打个比方,这就像是你想要改变父亲的想法,但前提是你不能瞒着他,必须要让他知道你改在哪儿了。

1.2 super关键字

当子类重写父类的方法之后,子类的对象便无法再直接访问父类中被重写的方法了,想要重新访问重写之前的父类方法就需要super关键字。这有点类似于我们常说的长幼有序,在未经许可的情况下,你敢直接翻阅长辈的隐私吗?

​ 当我们想要实例化一个子类对象时,会首先调用父类无参数的构造方法,然后再调用子类的构造方法。子类在继承父类时不会继承父类的构造方法,但是子类可以调用父类的构造方法。

​ 结合我们之前所学过的this关键字,有如下总结:

  • 同一个类下,在一个构造方法中调用另一个构造方法使用this()关键字;
  • 在子类中想要调用父类的构造方法应该使用super()关键字;

  • this和super使用的相同之处:该语句必须位于构造方法的第一行;

​ 除此之外还需要注意,当子类的在构造方法中想要调用父类的构造方法时,没有显式的调用则会默认调用父类中无参数的构造方法,而如果恰巧父类的构造方法都已经定义成有参数的构造方法,那么此时系统就不会自动生成无参数的构造方法,这样就会导致子类调用父类的构造方法出现错误.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Father {
Father(int a) {
System.out.println("父类构造方法");
}
}
class Son extends Father {
Son(){
//super(5);
System.out.print("调用了子类构造方法");
}
}
public class test1_9 {
public static void main(String[] args) {
Son s = new Son();
}
}

运行则显示错误

image-20210109201943317

image-20210109202026400

  • 如果父类没有无参数的构造方法,想要调用父类的构造方法就需要在子类构造方法的首句使用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
2
3
4
class 类名 implements 接口列表 {
属性
方法
}

2.2.2 接口的继承

​ 除了一个接口能够继承多个父接口,其他的继承规则和类的继承相同。

1
2
3
4
interface 接口名 extends 接口列表Interface1,Interface2…… {
全局常量声明
抽象方法声明
}

​ 和抽象类相似,除非被声明abstract,否则该类需要实现所有继承的接口。

2.3 抽象类和接口的关系

  • 抽象类可以提供成员方法的实现细节,但是接口只能方法的声明
  • 抽象类中的变量可以是各种类型,但是接口中只能是public static final类型的变量
  • 一个类只能继承一个抽象类,但是一个类却能够继承多个接口

抽象类是对整个事物的抽象,即对类抽象,而接口确实对行为抽象,即对类的局部进行抽象。

3. 多态

3.1 多态的概念

​ 多态就是同一个行为具有多个不同表现形式或形态的能力,也就是同一个接口在不同的实例中可以执行不同的操作。我们平时在所使用的电脑快捷键能帮助我们更好的理解多态,当在不同程序中的快捷键的按键相同时,快捷键的具体的功能就要看你究竟打开的是哪一个程序了。

在了解多态之前我们必须要明白的知识:

  • 访问一个对象的唯一方法就是通过引用型变量,而引用型变量可以声明为类类型或者接口类型
  • 引用类型的变量只能有一种类型,一旦被申明,就不能再改变
  • 引用型变量能够被重置成其他对象(当这些对象没有被final修饰时),还可以引用和他类型相同或者相兼容的对象。

多态得以实现的基本原理:

  • 编译阶段:编译器会在父类的class文件中查找调用的方法进行绑定,能够成功找到就能编译成功。(编译阶段属于静态绑定)
  • 运行阶段:运行阶段实际上是在动态的执行子类的方法。(运行阶段的动态绑定)
  • 多态表示多种形态:在编译阶段只绑定了一种形态,而在运行的时候动态绑定了多种形态。程序要能够运行必须同时满足静态绑定和动态绑定。

3.2 对象的类型转换

​ 可以将同一个继承结构中的对象的类型转化成另一种类型,分为一下两种转化:

  • 向上转型,从父类(超类)转型成子类,隐式转型。通常情况下,向上转移都是由编译器隐式执行的

    向上转换主要用于在需要使用子类对象的时候,通过父类型的引用指向子类型的变量

  • 向下转型,从子类转型成父类,显式转型

    向下转换主要为了能够调用子类特有的方法

3.2.1 向上转型的方法

向上转型比较简单,就是将父类的引用指向子类的变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Animal {
public void eat() {
//...
}
}

public class Cat extends Animal {
public void meow() {
//...
}
public void eat() {
//...
}
}

​ 当我们运行:Animal a1 = new Cat();时,就进行了隐式的向上转换。

3.2.2 向下转型的方法

​ 向下转换则稍微复杂一些,父类型转化成子类型必须通过显式强制类型转换,并且必须确保父类引用变量是子类的一个实例(子类一定是父类的一个实例,但是父类却不一定是子类的一个实例),例如上述代码中再加入:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class Animal {
public void eat() {
//...
}
}

public class Cat extends Animal {
public void meow() {
//...
}
public void eat() {
//...
}
}

public class Dog extends Animal {
public void woof() {
//...
}
public void eat() {
//...
}
}

​ 如果在主函数中运行:

1
2
Animal c1 = new Cat();
Animal d1 = new Dog();

​ 在执行了上述两行程序之后,这时任意给一个Animal变量就不能直接确定究竟是Cat子类还是Dog子类了,运行结果就会出现错误,这就是类型转换异常。

​ 为了解决该问题,好的习惯是运用instanceof运算来判断父类变量是否是该子类的一个实例:

1
2
3
4
Cat x = nUll;
if(c1 instanceof Cat) {
x = (Cat) c1
}

3.2.3 为什么要使用向下转型

向下转型的主要作用是为了使父类能能够调用子类独有的方法,示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
import java.awt.*;

class Ball {
public void play(){
System.out.println("play ball");
}
}
class Basketball extends Ball{
public void play(){
System.out.println("打篮球");
}
}
class Football extends Ball{
public void play(){
System.out.println("踢足球");
}
public void famous(){
System.out.println("Messi");
}
}
public class duotai2021_1_9 {
public static void main(String[] args) {
//向上转型
Ball b = new Basketball();
b.play();
//向下转型
Football a = null;
Ball c = new Football();
//c.famous(); 报错
if(c instanceof Football){
a = (Football)c;
}
a.play();
//能够调用子类中独有的方法
a.famous();
}
}

需要注意的专业问题:

向上转型、向下转型和自动类型转换、强制类型转换的区别,前者用于说明引用类型的转换,而后者只用于说明基本数据类型之间的转换。

3.3多态在实际开发中的作用

​ 当我们想要给软件扩展新的需求的时候,我们可以选择修改原先版本中的类或者方法,但是根据软件开发原则中的OCP(开闭原则),对扩展开放、对修改关闭。因为修改会使得系统的稳定性变差,未知的风险变多。

​ 这时就需要我们通过多态来扩展系统的功能,在我们编程的时候,我们要面向抽象对象编程,而不是面向具体的对象来编程,面向具体的对象来编程会使得程序的扩展性变差。

3.4 Object类

​ Object类是所有类的父类,一个类如果没有显式的继承父类,那么他的父类默认是Object。在Object类中我们主要学习两种方法,它们分别是toString()方法和equals()方法。

3.4.1 toString()方法

  • toString ()方法的作用:可以将一个Java对象转换成字符串的表示形式
  • toString()方法返回的形式:类名+@+对象内存地址的十六进制的形式

​ toString()的输出结果往往不是我们想要的结果, toString()方法希望我们能够重写自己需要的输出,这样在我们直接输出引用类型的时候,会自动调用toString()方法得到我们想要的结果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Message {
private int year;
private int month;
private int day;
public Message(int year, int month, int day) {
this.year = year;
this.month = month;
this.day = day;
}
//重写tosString()方法
public String toString() {
return this.year+"年"+this.month+"月"+this.day+"日";
}
}

public class Reset_Tostring {
public static void main(String [] args) {
Message m1 = new Message(2020,2,5);
//由于重写的toString()方法,直接输出引用变量就可以得到我们想要的结果
System.out.println(m1);
}
}

3.4.2 equals()方法

  • equals()方法的作用:判断两个对象是否相等,源代码如下:
1
2
3
public boolean equals(Object obj) {
return (this == obj);
}

​ 程序中的基本数据类型可以通过“==”进行相等的判断,但是因为“==”号是判断两个数据的内存地址是否相等。

​ 由equals()方法的源代码可知,原始的equals()方法只能判断两个对象的内存地址是否相等,要想判断引用对象的数据是否相等,需要重写equals()方法。在上述代码中的Message类中重写如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//重写equals()方法
//使equals()方法能够直接比较两个对象的数据
public boolean equals(Object obj) {
//获取第一个对象的数据
int year1 = this.year;
int month1 = this.month;
int day1 = this.day;
//获取第二个对象的数据
//由于Object是Message的父类,需要进行强制类型转化
if(obj instanceof Message){
Message n = (Message) obj;
int year2 = n.year;
int month2 = n.month;
int day2 = n.day;
if(year1 == year2 && month1 == month2 && day1 == day2)
return true;
}
return false;
}

​ 但是上述代码在运行过程中的效率并不高,我们对其进行第一次优化:

  • 优化体现在先进行特殊条件的判断,再进行一般条件下的相等判定
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public boolean equals(Object obj) {
//当obj是null时,两个对象肯定不相等,可直接判断
if(obj == null) {
return false;
}
//当obj不是Message的类型的时候,直接可以判断不相等
if(!(obj instanceof Message)) {
return false;
}
//如果两个对象的内存地址相同,那么这两个对象的数据一定相等
if(obj == this) {
return true;
}
//当以上判断完成之后,进行最一般的判断
//获取第一个对象的数据
int year1 = this.year;
int month1 = this.month;
int day1 = this.day;
//已经判断过后的obj可以直接向下转型
Message n2 = (Message) obj;
int year2 = n2.year;
int month2 = n2.month;
int day2 = n2.day;
if(year1 == year2 && month1 == month2 && day1 == day2) {
return true;
}
return false;
}

​ 上述代码的语句较为繁琐,可以进行第二次优化

  • 优化体现在合并了条件的判断,减少了变量的声明。
1
2
3
4
5
6
7
8
9
10
public boolean equals(Object obj) {
if(obj==null || !(obj instanceof Message)) {
return false;
}
if(this==obj) {
return true;
}
Message n2 = (Message) obj;
return this.year==n2.year && this.month==n2.month && this.day==n2.day;
}

​ 毕竟一开始要求我们重写系统方法就要尽可能地实现简洁易懂,所以这两步的优化也是理所应该的。优化到这一步得到的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默认两种权限来修饰。

总结

​ 总结一下面向对象的编程,它具有三大特性:封装、继承、多态。三个环节是密切相关的,封装产生了对象的整体概念,继承就是用来描述对象与对象之间的关系,有了继承之后,才有方法的覆盖和多态。