@posper
@version 1.0 2021/6/12
@version 1.2 2022/4/12
ch5 继承
类、超类和子类
- 继承:基于已有的类创建新的类。继承一个类就是复用(继承)这些类的方法,而且可以增加一些新的方法和字段
- 继承:is-a 关系
定义子类
- 父类,又叫基类、超类
- 子类,又叫派生类、孩子类
- 子类比父类拥有的功能更多
- 通过扩展父类定义子类时,只用指出子类与父类的不同之处
- 通用的方法放在父类中,更特殊的方法放在子类中
覆盖方法 ⭐️
- 超类中的有些方法对子类并不一定适用,此时需要在子类中覆盖(重写)父类方法
- 使用
super
关键字可以调用父类方法,避免覆盖父类方法时调用父类方法造成不必要的递归 super
不是一个对象的引用, 不能将supe
赋给另一个对象变量
方法的重写(覆盖)需要遵循 ”两同两小一大“ (《疯狂Java讲义》)
- “两同”:即方法名、方法参数列表相同;
- “两小”:指的是子类方法返回值类型应比父类方法返回值类型更小或者相等、子类方法声明抛出的异常类型应比父类方法声明抛出的异常类型更小或者相等;
- “一大”:指的是子类方法的访问权限应该比父类方法的访问权限更大或者相等。
注意 ⚠️
- 子类覆盖父类方法时,两个方法的方法名、参数列表和返回类型必须完全一致;
- 否则,则无法覆盖父类方法,而是在子类中建立了一个子类的特有方法!!
- 此时,使用多态时,上转型变量调用的是父类方法(因为子类方法此时是特有的,只有子类变量能调用)
ps:ch 8 泛型方法例外,因为它经过类型擦除后,会在子类中自动合成一个桥方法,用来实现多态。
子类构造器
- 通过
super(...)
调用父类构造器- 必须放在子类构造器的第一条
- 若子类构造器没有显式调用父类构造器,将自动调用父类的无参构造器;
- 若父类没有无参构造器,必须要在子类构造器中明确指明调用父类哪个构造器;否则,Java 编译器就会报错
一个对象变量可以指示多种实际类型的现象称为多态;
在运行时能够自动选择适当的方法,称为动态绑定。
this 与 super 关键字
this
关键字的作用
- 隐式参数的引用
- 调用该类的其他构造器
super
关键字的作用
- 调用父类方法
- 调用父类构造器
注意:
this
可以作为当前对象的引用,但是super
却不可以作为父类对象的引用。
继承层次
- 由一个公共超类派生出来的所有类的集合称为继承层次;
- 在继承层次中,从某个特定的类到其祖先的路径成为该类的继承链(子到父)
Java 中不支持多继承,但是可以通过接口来实现
多态
-
Java 中,对象变量是多态的
- 父类类型的变量既可以引用自身类型的变量,还可以引用子类类型的变量;
- 但是,子类类型的变量不可以引用父类类型的变量
-
子类可以调用父类
public
、protected
、包
权限的方法,但是父类不可以调用子类的 “特有方法”(子类特有方法指的是,只在子类中存在,但是父类中没有的方法)- 纯父类变量(左右都是父类)不可以调用子类的任何方法;
- 纯子类对象(左右都是子类)可以调用父类和子类的任何方法(除了父类的
private
方法); - 上转型变量(左边是父类,右边是子类)可以调用子类重写父类的方法(多态 & 动态绑定),但是仍然不能调用子类特有的方法(就是仅仅在子类中增加的,但是父类中没有的)
class Employee { String name; int age; } class Manager extends Employee { private double bonus; public void setBonus(double bonus) { // Manger 中独有的方法,Employee 类中没有 this.bonus = bonus; } } public static void main(String[] args) { Manager boss = new Manager(); // 纯子类变量(引用、对象) Employee staff = boss; // 上转型对象 boss.setBonus(100); // OK。子类调用子类方法,没毛病 // staff.setBonus(100); // ERROR // setBonus 方法是子类独有的,不能通过上转型对象和父类对象调用,只能通过子类对象调用 }
向上转型 vs. 向下转型
- 向上转型(
upcasting
)- 子类型 -> 父类型
- 又被称为:自动类型转换
- 向下转型(
downcasting
)- 父类型 -> 子类型
- 又被称为:强制类型转换
注意:无论是向上转型还是向下转型,这两种类型之间必须要有继承关系;否则,编译器报错。
何时需要向下转型?
-
进行强制类型转换的**唯一原因**是:在暂时忽视对象的实际类型之后, 使用对象的全部功能。
- 即,当调用的方法是子类型中特有的,在父类型当中不存在,则必须向下转型。
-
注意:向下转型可能发生
ClassCastException
异常,最好在强制类型转换时候使用instanceof
运算符。例如,上面🌰中
staff.setBonus(100);
出错,可通过如下方式来解决:public static void main(String[] args) { Manager boss = new Manager(); Employee staff = boss; // 向下转型(强制类型转换) if (staff instanceof Manager) { Manager manager = (Manager)staff; manager.setBonus(100); } }
在一般情况下,应该尽量少用强制类型转换和
instanceof
运算符。应考虑超类设计是否合理,是否可以利用多态的动态绑定。(5.8节 继承的设计技巧 第6条)
大多情况下,因为多态性的动态绑定机制,我们不用将 Employee
转换为 Manager
也能正确调用方法;
只有在使用 Manager
特有方法才需要进行强制类型转换。例如,setBonus
方法。
但是,此时应该自问超类设计是否合理。是否需要重新设计超类,并添加 setBonus
方法,这才是更合适的选择。
静态绑定 vs. 动态绑定
-
Java 程序分为编译阶段和运行阶段
- 先分析编译阶段,再分析运行阶段;
- 如果编译无法通过,则肯定无法运行。
-
静态绑定
-
又被称为:编译阶段绑定;
-
检查的是字节码
.class
文件是否有对应的方法; -
private
、static
、final
方法和构造器,编译器可以直接知道属于哪个类型,这便是静态绑定。public class Animal { public void move() { System.out.println("动物在移动...."); } } public class Cat extends Animal { @Override public void move() { System.out.println("猫在溜达..."); } public void catchMouse() { System.out.println("猫抓老鼠"); } } Animal a = new Cat(); // 编译器 ok a.move(); // ok // Animal.class 中有 move() 方法,所以编译 ok a.catchMouse(); // error // Animal.class 中没有 catchMouse() 方法,所以编译 gg
-
只有静态绑定成功,即编译阶段通过了,才能进入运行阶段
-
-
动态绑定
-
又被称为:运行阶段绑定
-
检查的是
JVM
的堆内存中真实创建的对象是什么 -
动态绑定,虚拟机必须调用对象变量引用的 实际类型对应的方法。
Animal a = new Cat(); a.move(); // 这里 a 指向的是 JVM 堆中的 Cat 类型对象,所以调用的是 cat 的 move 方法 // 而 Cat 类中对 move 方法进行了重写。如果 Cat 没重写 move 方法,仍然是调用 Cat 类中的 move 方法 // 只不过是直接从父类 Animal 中继承来的。
-
-
多态
- 父类型引用指向子类型对象这种机制导致程序存在编译阶段和运行阶段绑定两种不同的形态/状态,这种机制可以称为一种多态语法机制。
-
多态的作用是什么?
-
降低程序的耦合度,提高程序的扩展力;
-
能使用多态尽量使用多态;
-
父类型引用指向子类型对象。
核心:面向抽象编程,尽量不要面向具体类型编程。
即,方法参数尽量设置为抽象类型(父类),而具体调用和实现则用子类类型(即,实现时用向上转型)
-
-
小结
-
当程序采用多态机制时,首先进行静态绑定,检查对象变量对应的类型中是否存在被调用的方法:
- 如果有,则进入运行阶段;否则,则出现编译错误,无法进行入运行阶段。
-
当程序编译正确时,多态程序进入动态绑定阶段。此时,调用方法会在具体构造的对象上进行调用。
-
即,程序编译时看的是对象变量类型(= 左边)中是否有相应的方法;
-
但是,在运行时调用的是 new 出来的具体对象(=右边)上的方法。
注意:运行阶段时,由于动态绑定,都是在具体构造的对象上调用方法(上转型对象其实是调用子类的方法,如果方法未在子类重写,则调用的是子类中从父类继承而来的方法;而不是调用父类的方法)
-
-
注意:调用子类中从父类继承来的方法,和直接调用父类方法不是不同的概念!!!
理解方法调用
以调用 x.f(arg) 为例,隐式参数 x 为类 C 的一个对象:
-
编译器查看对象的声明类型和方法名。
- 编译器查找 C 类中所有名为 f 的方法和父类中名为 f 且可访问的方法(父类 private 方法不可访问)
- 此时,编译器知道所有可能被调用的候选方法
-
编译器确定方法调用中提供的参数类型。
- 重载解析:在所有名为 f 的方法中,找到一个与所提供参数类型完全匹配的方法
- 此时,编译器已经知道需要调用的方法名字和参数类型
-
静态绑定。
-
如果是 private 方法、static 方法、final 方法或者构造器, 那么编译器将可以准确地知道应该调用哪个方法。
-
此时调用方法只用考虑 x 的类型(若在类 C 中找不到 f 方法,则向其父类中找),不需要考虑类 C 的子类(因为这几种修饰的方法都不能被继承)
static 方法也可以被继承…
-
-
动态绑定。
- 如果调用的方法依赖于隐式参数的实际类型,那么必须在运行时使用动态绑定。
- 虚拟机必须调用与 x 所引用对象的实际类型对应的那个方法
- 例如,x 的实际类型是D,它是C 类的子类。如果D 类定义了方法 f(String),就直接调用它;否则, 将在D 类的超类中寻找f(String),以此类推。
- 如果调用的方法依赖于隐式参数的实际类型,那么必须在运行时使用动态绑定。
阻止继承:final
类和方法
将方法或类声明为
final
主要原因是: 确保它们不会在子类中改变语义。
- final 修饰字段
- 基本类型:不可更改
- 引用类型:不可指向新的引用,但是对象状态(字段值)可能会改变
- final 修饰类:该类不可以被继承
- final 修饰方法:子类不能覆盖这个方法
抽象类
-
abstract
关键字- abstract 修饰方法
- 抽象方法,自己不用实现,而是在具体的子类中实现;
- 若子类未实现抽象方法,则子类也要定义为抽象类
- abstract 修饰类
- 抽象类,可以包含一个或多个抽象方法(也可以一个也不包含);
- 抽象类也可以包含字段和具体方法
- abstract 修饰方法
-
抽象类的作用:提高程序清晰度
-
父类中定义抽象方法,子类中具体实现
-
变量定义为父类(抽象类)类型,具体实现(
new
)子类类型(上转型) -
方法调用,通过父类变量(多态,动态绑定)
即,使用抽象类能更好地利用多态机制!
-
受保护访问 protectd
protected
关键字- 本包和子类可见
- 要谨慎使用
protected
修饰字段- 当前类的所有孩子类(孙子类,重孙类,当然没有这个叫法,瞎说的)都可以使用该字段
- 如果修改了当前类的实现,则很有可能会影响其他继承该类的子类。这违背了 OOP 提倡封装的精神。
- Java 用于控制可见性的 4 个访问修饰符:
private
:仅对本类可见;
2.public
:对所有类可见;
3.protected
:对本包和所有子类可见;
4.包
(默认):对本包可见,不需要修饰符
Object
: 所有类的超类
- Java 中每个类都是扩展了
Object
,但是并不需要extends Object
- 如果没有明确地指出超类,
Object
就被认为是这个类的超类
Object 类型的变量
- 可以使用
Object
类型的变量引用任何类型的对象 - Java中,只有基本类型(8种)不是对象
- 所有数组类型,不管是对象数组还是基本类型的数组都扩展了 Object 类
equals
方法
-
Object 类中的 equals 方法用于检测两个对象的引用是否相等
// jdk 中 Object 类的 equals 方法 public boolean equals(Object obj) { return (this == obj); }
-
如果想要比较两个对象的内容是否相等,则需要覆盖
Object
类中的equals
方法 -
自定义编写(覆盖 Object 类) equals 方法的建议 💡
显式参数命名为
otherObject
, 稍后需要将它转换成另一个叫做 other 的变量。- 检测
this
与otherObject
是否引用同一个对象: - 检测
otherObject
是否为null
, 如果为 null , 返回 false - 比较
this
与otherObject
是否属于同一个类 - 将
otherObject
转换为相应的类类型变量 (强制类型转换) - 对所有需要比较的域进行比较
- 使用
==
比较基本类型域,使用equals
比较对象域 - 如果所有的域都匹配, 就返回
true
;否则返回false
- 使用
public class Employee public boolean equals(Object othe「Object) { // 1、a quick test to see if the objects are identical if (this == otherObject) return true; // 2、must return false if the explicit parameter is null if (otherObject == null ) return false; // 3、if the classes don' t match, they can' t be equal if (getClass() != otherObject.getClass()) // 比较是否属于同一个类 return false; // 4、now we know otherObject is a non-null Employee Employee other = (Employee) otherObject; // 强制类型转换 // 5、test whether the fields have identical values return name.equals(other.name) && salary = other , salary && hireDay.equals(other, hireDay); // 比较各个域是否相等 } }
-
在子类中定义 equals 方法时,首先调用超类的
equals
。- 如果检测失败,对象就不可能相等;
- 如果超类中字段都相等,还需要比较子类中的实例字段
public class Manager extends Employee { public boolean equals(Object otherObject) { if (!super.equals(otherObject)) return false; // super.equals checked that this and otherObject belong to the same class Manager other = (Manager) otherObject; return bonus == other.bonus; } }
- 检测
-
Objects.equals 方法:
- 使用场景:如果存在参数为 null 时,则需要使用 Objects.equals 方法,代替 a.equals(b) 方法
- 调用过程:
- 如果两个参数都为 null,Objects.equals(a,b) 调用将返回 true ;
- 如果其中一个参数为 null , 则返回 false;
- 否则(两个都不为 null),则返回 a.equals(b)。
-
对于数组类型, 可以使用静态的 Arrays.equals 方法 判断两个数组元素(内容)是否相等
-
使用 @Override 对覆盖超类的方法进行标记,避免出现定义了一个无关的方法,而不是覆盖 Object 类中的 equals 方法
hashCode
方法
- 散列码( hash code ) 是由对象导出的一个整型值
- 每个对象都有一个默认的散列码,其值为对象的存储地址
- 如果重新定义
equals
方法, 就必须重新定义hashCode
方法, 以便用户可以将对象插入到散列表中 equals
与hashCode
的定义必须相容- 如果
x.equals(y)
返回true
, 那么x.hashCode()
就必须与y.hashCode()
具有相同的值
- 如果
toString
方法
toString
方法, 它用于返回表示对象值的字符串- Object 类中的
toString
方法,返回 类的名字随后是一对方括号括起来的域值 - 可以自定义实现(覆盖 Object)
toString
方法,返回对当前类实例字段的字符串描述 - 使用 toString 方法的主要原因:
- 只要对象与一个字符串通过操作符 “ +” 连接起来,Java 编译器就会自动地调用 toString 方法,以便获得这个对象的字符串描述。
- 子类中实现 toString 方法时,可以先调用父类的 toString 方法 【
super.toString()
】,再添加子类中实例字段的描述
equals()
、hashCode()
和toString()
这三个方法可以由 eclipse 或 IDEA 生成,且equals()
、hashCode()
必须同时重写!
泛型数组列表
-
ArrayList<ElemType>
能自动调整数组容量 -
声明数组列表
-
使用
var
关键字(Java10)声明,<> 中需要带上具体泛型类型;var list = new ArrayList<Integer>();
-
没有使用 var 关键字,可以省略 <> 里面的类型(钻石表达式)
Integer list = new int ArrayList<>();
-
-
如果 ArrayList 内部数组已经满了,会自动创建更大的数组,并将原数组元素拷贝过去
-
可通过
ensureCapacity
方法初始化数组列表的原始大小list.ensureCapacity (100); // 分配一个包含 100 个对象的内部数组
-
还可以通过
ArrayList
构造器来初始初始容量Integer list = new int ArrayList<>(100);
-
-
ArrayList 通过移动元素来进行增删元素,增删操作较多时效率较低,此时可采用链表
对象包装器
-
8 大基本类型每一种都有对应的包装类型
Integer
、Long
、``Float、
Double、
Short、
Byte【这六个
extends Number`】;Character
、Boolean
-
使用包装器的原因
- 可以将某些基本方法放在包装器中,这样会十分方便。
- 比如,
int x = Integer.parselnt(s) ;
-
包装器类中的值不可变
-
包装器类都是
final
的,不能被继承
装箱 vs. 拆箱 📦
-
装箱
-
基本类型 to 包装类型
-
对应
valueOf()
方法 -
自动装箱:Integer n = 3;
// 添加int 类型的元素到 ArrayList<lntege> 中 list.add(3); // 自动装箱 // 编译器将自动地变换成 list.add(Integer.value0f(3));
-
-
拆箱
-
包装类型 to 基本类型
-
对应
xxxValue
方法,比如intValue()
-
自动拆箱
// 当将一个Integer 对象赋给一个int 值时 int n = list.get(i); // 自动拆箱 // 编译器将以上语句转换为 int n = list.get(i).intValue();
-
自动装箱/拆箱的三点注意 ⚠️
-
首先, 由于包装器类引用可以为
null
, 所以自动拆箱有可能会抛出一个NullPointerException
异常Integer n = null; System.out.println(2 * n); // Throws NullPointerException
-
如果在一个条件表达式中混合使用
Integer
和Double
类型,Integer
值就会拆箱,提升为double
, 再装箱为Double
:Integer n = 1; Double x = 2.0; System.out.println(true ? n : x); // Prints 1.0
-
装箱和拆箱是编译器认可的, 而不是虚拟机
- 编译器在生成类的字节码时, 插入必要的方法调用。虚拟机只是执行这些字节码。
不能通过包装器类来实现方法修改实参,因为包装器类是不可变的。
如果想编译一个能修改实参的方法,可以使用 IntHolder、BooleanHolder等。
每个持有者类型都包含你一个公共字段 value,通过它访问存储在其中的值
public static void triple(int x) { // won't work
x = 3 * x; // modifies local variable
}
public static void triple(IntHolder x) { // works。这里可以修改 x 的值
x.value = 3 * x.value;
}
变参方法
- Object … 在一定程度上等价于 Object[]
...
表示可以接受任意数量的参数
枚举类
枚举类用法
- 枚举类的本质:枚举类型(
enum
)实际上是一个类,它具有有限个实例,且不能构造新的对象- 所有枚举类型都是
Enum
类的子类
- 所有枚举类型都是
- 由于枚举类型不能构造新的对象,所以比较两个枚举类型的值时,直接使用
==
就行,不需要使用equals
方法 - 也可以为枚举类型增加构造器、方法和字段
- 构造器只是在构造枚举常量的时候被调用
- 枚举的构造器只能是
private
的- 可以省略
private
,其默认是private
修饰 - 但是,一旦将枚举类型构造器声明为
public
或者protected
,则会报错
- 可以省略
栗子 🌰
-
不包含构造器
// 不包含构造器(默认无参构造器) enum Size { SMALL, MEDIUM, LARGE, EXTRA_LARGE; } public class Test { public static void main(String[] args) { System.out.println(Size.SMALL + ": " + Size.SMALL.ordinal()); System.out.println(Size.MEDIUM + ": " + Size.MEDIUM.ordinal()); System.out.println(Size.LARGE + ": " + Size.LARGE.ordinal()); System.out.println(Size.EXTRA_LARGE + ": " + Size.EXTRA_LARGE.ordinal()); } }
运行结果:
-
包含构造器和字段
// 包含构造器和字段 enum Size { // 这里如果仍然 SMALL,就会报错。因为此时没有无参构造器 SMALL("S") , MEDIUM("M") , LARGE("L"), EXTRA_LARGE("XL") ; // 构造器只能在构造枚举常量的时候使用 private Size(String abbreviation) { // 枚举类型可以有构造器,但是只能是 private 修饰. // 所以枚举类型不能构造新的对象 this.abbreviation = abbreviation; } public String getAbbreviation() { return abbreviation; } private String abbreviation; // 枚举类型可以包含字段 } public class Test { public static void main(String[] args) { // Size.SMALL其实调用的是其 toString() 方法,也是返回 name 字段 System.out.println(Size.SMALL + ": " + Size.SMALL.ordinal()); System.out.println(Size.MEDIUM + ": " + Size.MEDIUM.ordinal()); System.out.println(Size.LARGE + ": " + Size.LARGE.ordinal()); System.out.println(Size.EXTRA_LARGE + ": " + Size.EXTRA_LARGE.ordinal()); System.out.println("-----------------------"); System.out.println(Size.SMALL.name() + " : " + Size.SMALL.getAbbreviation()); System.out.println(Size.MEDIUM.name() + " : " + Size.MEDIUM.getAbbreviation()); System.out.println(Size.LARGE.name() + " : " + Size.LARGE.getAbbreviation()); System.out.println(Size.EXTRA_LARGE.name() + " : " + Size.EXTRA_LARGE.getAbbreviation()); } }
运行结果:
Enum
源码解读
-
所有枚举类型都是
Enum
类的子类 -
Enum 类常见方法
String toString()
:返回枚举常量名;public final String name()
:返回枚举常量名;int ordinal ()
:返回枚举常量在 enum 声明中的位置,位置从0
开始计数;static Enum valueOf (Class enumClass , String name)
:返回指定名字、给定类的枚举常量
-
Enum 类部分源码解读
// jdk 1.8 Enum 部分源码解读... public abstract class Enum<E extends Enum<E>> implements Comparable<E>, Serializable { private final String name; private final int ordinal; /** * Enum唯一的构造器,它只能在“生命枚举类型”时由编译器调用,而不能由程序员调用。 * * @param name 枚举常量的名字 * @param ordinal 枚举常量的序号(它在枚举声明中的位置,序号从0开始)。 */ protected Enum(String name, int ordinal) { this.name = name; this.ordinal = ordinal; } public final int ordinal() { return ordinal; } public final String name() { return name; } public String toString() { return name; } public final boolean equals(Object other) { return this == other; } public final int hashCode() { return super.hashCode(); } // 省略其他... }
其中,
name()
和toString()
方法实现相同, 都是返回name
字段值。
继承的设计技巧
-
将公共操作和字段放在超类
-
不要使用
protected
字段- 子类集合是无限制的,可能会破坏封装性
- Java 中,同一个包也是可以访问
protected
字段的
-
使用继承实现“
is-a
” 关系- 只有两个类之间是 “
is-a
” 关系时,才使用继承。不要滥用继承
- 只有两个类之间是 “
-
除非所有继承的方法都有意义, 否则不要使用继承
-
在覆盖方法时, 不要改变预期的行为
-
使用多态, 而不要使用具体类型信息
// 看到以下形式代码,要考虑使用多态性。 if (x is of type1) action1(x) else if (x is of type 2) action2(x) /* 考虑 action1, 与 action2 表示的是相同的概念吗? 如果是相同的概念, 就应该为这个概念定义一个方法, * 并将其放置在两个类的超类或接口中,然后, 就可以调用 x.action(); 以便利用多态的动态绑定分派相应的动作 */
-
不要滥用反射