目录
- 五、面向对象(进阶)
五、面向对象(进阶)
5.1 关键字:this
5.1.1 this
的使用场景
- 目前出现的问题?解决方案?
-
问题:我们在声明setXxx方法时,通过形参给对应的属性赋值。如果形参名和属性名同名,那么该如何在方法内区分这两个变量呢?
-
解决方案:使用
this
。使用this
修饰的变量,表示的是属性(成员变量);没有用this
修饰的,表示的是形参(局部变量)。
-
this
可以调用的结构:成员变量、方法、构造器。 -
this
的理解:当前对象(在方法中调用) 或 当前正在创建的对象(在构造器中调用时)
5.1.2 this调用构造器
- 格式:
this(形参列表)
- 我们可以在类的构造器中,调用当前类中指定的其它构造器
- 要求:“
this(形参列表)
”必须声明在当前构造器的首行 - 结论:“
this(形参列表)
”在构造器中最多声明一个 - 如果一个类中声明了 n 个构造器,则最多有 n-1 个构造器可以声明“
this(形参列表)
”的结构
5.2 面向对象特征二:继承
5.2.1 继承性的理解
代码层面:
- 自上而下:定义了类A,在定义另一个类B时,若类B的功能与类A相似,则考虑类B继承于类A
- 自下而上:定义了类B,C,D等,发现这些类有类似的属性和方法,则可以考虑将相同的属性和方法进行抽取,封装到类A中,让类B,C,D继承于类A,同时类B,C,D中相似的属性和方法就可以删除了。
5.2.2 继承的优点
- 继承减少了代码冗余,提高了代码复用性。
- 继承的出现,更有利于功能的扩展。
- 继承的出现让类与类之间产生了
is-a
的关系,为多态的使用提供了前提。(父类更通用、更一般,子类更具体。)
5.2.3 继承的格式
class A{
//属性、方法
}
class B extends A{
}
类A:父类、superClass、超类、基类
类B:子类、subClass、派生类
-
通过继承性,子类获取到父类的属性和方法
-
但由于封装性的影响,子类可能不能直接调用父类中声明的属性或方法
-
子类在继承父类之后,还可以扩展自己特有的功能
(体现:增加特有的属性或方法)
-
子类和父类的理解,要区别于集合和子集。
5.2.4 默认的父类
Java中声明的类,如果没有显示声明其父类,
则默认继承于 java.lang.Object
5.2.5 补充说明
- Java支持多层继承(直接父类、间接父类)
- Java中一个类可以声明多个子类。反之,一个子类只能有一个父类。(Java的单继承性)
5.3 封装性中4种权限修饰
-
同一个包 不同类:可以访问
public、protected、缺省
不能访问
private
-
不同包下 子类: 可以访问
public、protected
不能访问
private、缺省
-
不同包下 无关类:可以访问
public
不能访问
private、缺省、protected
5.4 方法的重写(overwrite / override)
5.4.1 方法重写概述
-
为何需要重写?
子类在继承父类后,就获取了父类中声明的所有方法。但父类中的方法可能不太适用于子类,即子类需要对父类中继承过来的方法进行覆盖、覆写的操作。
-
何为方法的重写?
子类对父类的方法进行覆盖、覆写的操作。
5.4.2 方法重写规则
方法声明格式: 权限修饰符 返回值类型 方法名(形参列表){//方法体}
具体规则:
-
父类被重写的方法 与 子类重写的方法 的 方法名 和 形参列表 必须相同。
-
子类重写的方法的权限修饰符 不小于 父类被重写的方法 的权限修饰符。
- 权限修饰符按访问权限从高到低:
public、protected、缺省、private
。 - 子类不能重写父类中声明为
private
权限修饰的方法。
- 权限修饰符按访问权限从高到低:
-
关于返回值类型:
①父类被重写的方法 的 返回值类型是
void
, 则子类重写的方法 的 返回值必须是
void
。②父类被重写的方法 的 返回值类型是 基本数据类型 ,
则子类重写的方法 的 返回值必须 与被重写的方法的返回值类型相同。
③父类被重写的方法 的 返回值类型是 引用数据类型 ,
则子类重写的方法 的 返回值类型 可以
与被重写的方法的返回值类型相同 或为
被重写的方法的返回值类型的子类。
④子类重写的方法 抛出的异常类型 可以
与父类被重写的方法抛出的异常类型相同,或是
父类被重写的方法抛出的异常类型的子类。
补充说明:
方法体 没有要求。但是子类重写的方法的方法体必然与父类被重写的方法的方法体不同。
5.4.3 面试题:区分方法的重载与重写
-
重载:“两同一不同”
-
重写:继承以后,子类覆盖父类中同名同参数的方法
5.5 关键字:super
5.5.1 super
的理解
-
super
可用于访问父类中定义的属性 -
super
可用于调用父类中定义的成员方法 -
super
可用于在子类构造器中调用父类的构造器
5.5.2 super
的使用场景
1. super
调用属性、方法
子类继承父类后,可以在子类的方法或构造器中使用 super.
调用 父类中声明的属性或方法。(满足封装性前提)
一般省略super.
结构,但可以用super.
显示声明 调用父类被重写的方法 或 父类中声明的同名的属性。
2. super
调用构造器
① 子类继承父类时,不会继承父类的构造器;
只能通过 super(形参列表)
的方式调用父类指定的构造器。
② 规定:“super(形参列表)
”,必须声明在构造器的首行。
③ 已知``this关键字,在构造器首行可以使用
this(形参列表)`,调用本类 中重载的构造器,结合②:
在构造器首行,“this(形参列表)
”和“super(形参列表)
”只能二选一。
④ 如果在子类构造器首行既没有显示调用“this(形参列表)
”,也没有显 示调用"super(形参列表)
", 则子类此构造器默认调用“super(形参列表)
”,即调用 父类中空参的构造器 。
⑤ 由③和④得到结论:子类的任何一个构造器中,要么会调用 本类中重载的构造器 ,
要么会调用 父类的构造器 。 只能是这两种情况之一。
⑥ 由⑤得到:一个类中声明有 n 个构造器,最多有 n-1 个构造器中使用 了 “this(形参列表)
”,则剩 下的那个一定使用 “super(形参列表)
”。
- 我们在通过子类的构造器创建对象时,一定在调用子类构造器的过程中,直接或间接地调用到父类的构造器。也正因为调用过父类的构造器,我们才会将父类中声明的属性或方法加载到内存中,供子类对象使用。
5.5.3 子类继承的补充
1. 子类的内存
在实例化子类前,会调用父类构造器,可以把子类的内存看做两部分:一部分是 父类 ,一部分是 子类其本身 。
super
和this
就分别指向这两部分,this
的权限相比于super
要大一些。
-
super
只能访问属于父类的那块区域, -
this
除了能访问子类本身的那部分区域外,还能访问super
中那些没有被private
修饰的东西。
2. 成员变量的隐藏
如果 子类声明了与父类相同名字的成员变量(此时,子类会隐藏所继承的成员变量),但没有重写父类的 get()
set()
方法,则:
- 当子类调用
set()
方法时,修改的是 父类的属性, - 当子类调用
get()
方法时,返回的还是 父类的属性。
即:
- 子类对象 以及 子类自己定义的方法 操作 与父类同名的成员变量 是指 子类重新声明的这个成员变量 。
- 子类继承的方法 所操作的成员变量 一定是 被子类继承或隐藏的成员变量。
5.6 子类对象实例化全过程(熟悉)
5.6.1 代码举例
class Creature{ //生物类
//声明属性、方法、构造器
}
class Dog extends Animal{ //狗类
}
class DogTest{
public static void main(String[] args){
Dog dog = new Dog();
}
}
-
从结果的角度来看:体现为类的继承性
当我们创建子类对象后,子类对象就回去了其父类中声明的所有的属性和方法,在权限允许的情况下,可以直接调用。
-
从过程的角度来看:
-
当我们通过子类的构造器创建对象时,子类的构造器一定会直接或间接地调用到其父类的构造器,而其父类的构造器同样会直接或间接地其父类的构造器,... ,直到调用了Object类中的构造器为止。
-
正因我们调用过子类所有的父类的构造器,所以我们就会将父类中声明的属性、方法加载到内存中,供子类的对象使用。
-
先加载父类,再加载子类。
-
5.6.2 问题
-
在创建子类对象的过程中,一定会调用父类中的构造器吗?
Yes!
-
创建子类的对象时,内存中到底有几个对象?
只有一个对象!即为当前new后面构造器对应的类的对象。
5.7 面向对象的特征之三:多态性(polymorphism)
-
如何理解多态性?
一个事物的多种形态。
5.7.1 Java中多态性体现
1. 对象的多态性
父类的引用 指向 子类的对象 。(或 子类的对象 赋给 父类的引用 )
- 格式 : 父类类型 变量名 = 子类对象;(父类类型:指子类继承的父类类型,或者实现的接口类型)
比如 :Person p2 = new Man();
-
对象的多态
在 Java 中,子类的对象 可以 替代 父类的对象 使用。
所以,一个 引用类型变量 可能指向(引用) 多种不同类型的对象 。
2. 多态的理解
-
Java 引用变量有两个类型:编译时类型 和 运行时类型 。编译时类型由声明 该变量时使用的类型决定,运行时类型由 实际赋给该变量的对象 决定。
- 若 编译时类型 和 运行时类型 不一致,就出现了 对象的多态性(Polymorphism)
- 在多态的场景下,调用方法时:
- 编译时,认为方法是 左边声明的父类的类型 的方法(即被重写的方法)
- 执行时,实际执行的是 子类重写父类 的方法。
- 简称:编译时,看左边;运行时,看右边。
-
多态的使用前提:① 类的继承关系
② 方法的重写
-
多态性的应用:虚拟方法调用 (Virtual Method Invocation)
-
虚方法:指在 编译阶段 不能确定方法的调用入口地址,在 运行阶段才能确定的方法,即 (父类中)可能被重写 的方法。
-
多态的适用性:仅适用于 方法 ,不适用于 属性 。
- 成员变量 没有多态性。
- 属性 编译 和 运行 都看左边。
5.7.2 多态的好处与弊端
-
好处:
使用场景:使用父类做 方法形参 。
变量引用的子类对象不同,执行的方法就不同,实现动态绑定。
极大地减少了代码冗余,不需要定义多个重载的方法。
-
弊端:
一个引用类型变量 如果声明为 父类的类型 ,但实际引用的是 子类对象 ,则该变量 加载了 子类特有的属性和方法 ( 内存中存在 ) ,但是 不能直接访问 子类特有的属性和方法 。
5.7.3 向上转型与向下转型
1. 概念
首先,一个对象在 new 的时候创建是哪个类型的对象,它从头至尾都不会变。 即这个对象的运行时类型 ,本质的类型永远不会变。但是,把这个对象赋值给不同类型的变量 时,这些变量的 编译时类型 却 不同。
-
向上转型:左边的变量的类型(父类) > 右边对象/变量的类型(子类)
- 此时,编译时 按照 左边变量的类型 处理,就只能调用父类中有的变量和方法,不能调用子类特有的变量和方法
- 但是,运行时,仍然是 对象本身的类型,所以执行的方法是 子类重写的方法体 。
- 此时,一定是安全的,而且也是自动完成的
-
向下转型:左边的变量的类型(子类)<右边对象/变量的编译时类型(父类)
- 此时,编译时 按照 左边变量的类型 处理,就可以调用子类特有的变量和方 法了
- 但是,运行时,仍然是 对象本身的类型
- 不是所有通过编译的向下转型都是正确的,可能会发生 ClassCastException,为了安全,可以通过 Instanceof 关键字进行判断
2. 转型方法
- 向上转型:自动完成
- 向下转型:(子类类型)父类变量
3. instanceof
关键字
- 在向下转型时,可能会出现:类型转换异常(ClassCastException)
- 在向下转型之前,使用
instanceof
关键字对 引用变量 做类型校验,避免出现 类型转换异常(ClassCastException)。 - 格式:
对象a instanceof 数据类型A
:判断 对象 a 是否是 数据类型 A 的实例,返回值boolean型 - 如果:
a instanceof A
返回 true,则:
a instanceof superA
返回也是 true(A 是superA的子类)
5.7.4 面试题
1.多态是编译时行为还是运行时行为?
运行时行为。
代码证明如下:
①先定义一个随机数,打印一下(方便比较);
②调用一下getInstance方法,getInstance方法根据随机数的不同,new不同的对象,然后返回赋给Animal;
③此时,调用eat方法;
④因为我们根据代码无法看出结果,只有真正运行时才知道new的是谁!
5.8 Object类的使用
5.8.1 Object类说明
说明:类java.lang.Object
是类层次结构的根类,即所有其它类的父类。
每个类都使用Object
作为父类。
- 任何一个Java类(除Object类)都直接或间接地继承于Object类
- Object类称为java类的根父类
- Object类中声明的结构(属性、方法等)具有通用性
- Object类中没有声明属性
- Object类提供了一个空参的构造器
- 重点关注:Object类中声明的方法
5.8.2 常用方法
重点方法: equals() \ toString()
了解方法:clone() \ finalize()
目前不需要关注:
getClass() \ hashCode() \ notify() \ notifyAll() \ wait() \ wait(long timeoutMillis) \ wait(long timeoutMillis, int nanos)
1. clone()
方法
- 定义:
protected Object clone()
- 作用:创建并返回此对象的副本。
- 可用于创建对象
- 创建:指在堆空间新划分一片内存。
2. finalize()
方法(deprecated)
-
当对象 被回收 时,系统 自动调用 该对象的 finalize() 方法。
- 不是 垃圾回收器 调用的, 是 本类对象 调用的
-
什么时候被回收:当某个对象 没有任何引用 时,JVM 就认为这个对象是垃圾对象,就会在之后 不确定的时间 使用 垃圾回收机制 来销毁该对象,在销毁该对象前,会先调用 finalize()方法。
-
子类可以 重写 该方法,目的是 在对象被清理之前执行必要的清理操作 。
-
finalize()可能导致内部循环调用,导致此对象无法被回收。
3. equals()
方法
3.1 适用性
任何 引用数据类型(包括数组) 都可以使用。
3.2 equals()
方法的使用
- 只能比较引用类型,自定义的类中
equals()
的作用与 “==
” 相同:比较是否指向同一个对象。 - 对于像
String
、File
、Date
和包装类
等,都重写了Object类
中的equals()
方法:用于比较 两个对象的实体内容是否相等。
3.3 开发中使用说明
-
实际开发中,针对自定义的类,常常会判断两个对象是否相等,而此时主要是判断两个对象的属性值是否相等。所以,我们需要重写
Object类
中的equals()
方法。 -
重写方法:
- 手动自己实现
- 调用IDEA自动实现
3.4 高频面试题:区分 ==
和 equals()
-
==
:运算符①使用范围:基本数据类型,引用数据类型
②基本数据类型:判断 数据值 是否相等
③引用数据类型:比较两个引用变量的 地址值 是否相等(或比较两个引用是否指向同一个对象实体)
char c1 = 'A'; int i1 = 65; sout(c1==i1);//true float f1 = 12.0f; int i2 = 12; sout(f1==i2);//true
-
equals()
:java.lang.Object
类里面的方法①使用范围:引用数据类型(包括数组)
②具体使用:不重写
equals()
:默认也是==
;重写equals()
:一般比较类中各属性是否都相等。
4. toString()
方法
4.1 Object类
中toString()
的定义
public String toString() {
return getClass().getName() + "@" + Integer.toHexString(hashCode());
}
4.2 开发中的适用场景
平时我们在调用System.out.println()
打印对象引用变量时,其实就调用了对象的toString()
方法
4.3 子类使用说明
- 自定义的类,在没有重写
Object类
的toString()
的情况下,默认返回的是当前对象的地址值 - 像
String
、File
、Date
或包装类
等Object的子类,都重写了Object
类的toString()
方法,在调用toString()
时,返回当前对象的实体内容。
4.4 开发中使用说明
习惯上,开发中对于自定义的类在调用toString()
时,也希望显示其对象的实体内容,而非地址值。这时候就需要重写Object类
中的toString()
。