第五章 继承
5.1 类、超类和子类
-
extends
表示 继承,java 中的 所有继承都是公有继承
(没有 c++ 中的 私有 和 保护继承) 👿 👿 👿 -
子类 调用 父类方法时 用
super.超类方法名()
-
子类
没有 显式 调用超类
的 构造器,自动调用超类
的 无参构造器,如果超类
没有,子类
也没调用其他构造器,就会报错。👿👿👿 -
关键字
this
- 指示隐式参数的 引用
- 调用该类的其他构造器
-
关键词
super
(super 不是一个对象引用, 他只是一个 指示 编译器 调用 超类 方法的特殊关键字)- 调用超类的方法
- 调用超类的构造器
-
Java 不支持
多重继承
(即一个类 有多个超类) -
对象变量
是多态
的。(理解运行时动态绑定的概念)👿👿👿- 如果 父类 引用了 子类对象,编译器 只是将 它看作为 父类对象。调用子类方法就可能会报错(如果子类定义,父类没有定义)。
-
重载方法 在运行时 动态绑定,虚拟机会调用 所引用对象的实际类型对应的那个方法。(说的天花乱坠,其实就是 “运行时的多态”)
多态
总结 :-
重载解析
(词如其名,重载过程解析…)- 编译器确定方法调用中提供的参数类型,并去相同名字中去找与所提供的参数类型完全匹配的方法,如果存在,就选择这个方法
方法的签名
: 方法名称 + 参数 (不包括方法返回值)
-
动态绑定
与静态绑定
静态绑定
: 编译器 准确知道调用哪个方法, private 方法、 static方法、 final方法、构造器动态绑定
: 要调用方法 依赖于 隐式参数 的 实际类型,必须在运行时使用动态绑定 ,采用该方法时,虚拟机必须调用与 该对象变量所引用 对象的实际类型对应的那个方法。(有调用,没有向上找,由于时间开销大,虚拟机是预先会为每个类创建方法表
方便搜索)- java中
动态绑定
是 默认的 , c++ 则需要将成员函数声明为virtual
, 如果不希望这样,在 java 中使用final
修饰该函数)👿👿👿
-
多态其实就说了两个问题:👿👿👿
- 子类对象可以传给父类引用,但是只能调用父类的方法(如果调用实际类型的方法父类中没有定义会报错)
- 如果子类把父类定义的方法覆盖了,虚拟机就会动态绑定,调用子类对应同名方法
-
-
在覆盖一个方法的时候,子类方法 不能低于 超类方法的可见性。
-
final类
会阻止继承 ,其中的所有方法都会 自动的 成为 final 方法,而不包括 字段。 eg: String类 就是 final类 -
内联
: 如果一个方法没有被覆盖并且很短,编译器就能够对他进行优化处理,这个过程称作:内联
。 eg:内联调用 e.getName() 将被替换为访问字段 e.name -
进行
强制类型转换
的唯一原因: 要在暂时忽视对象的 实际类型之后,使用对象的全部功能。- 只能在 继承层次(一个 公共超类 派生出来的所有类的 集合) 内进行
强制类型转换
- 在将
超类
强制转换成子类
之前,应该使用instanceof
进行检查
- 在将
- 只能在 继承层次(一个 公共超类 派生出来的所有类的 集合) 内进行
-
包含一个或多个方法 必须声明为抽象类
,抽象类可以包含 字段 和 具体方法。扩展的抽象类还可以定义为抽象类
,抽象类不能实例化。(对我来说,这个知识点其实不需要写,我早就记住了,略略略) -
访问权限 👿 👿 👿
public | (default) | protected | private |
---|---|---|---|
可以由任意类使用 | 仅对本包可见 | 对本包 和 其他包的所有子类可见 | 只能由定义他们的类使用 |
5.2 Object:所有类的超类
-
在 Java 中,只有 基本类型 不是对象,所有数组类型(不管是 对象数组 还是 基本类型数组 )都扩展了Object 类
Employee[] staff = new Employee[10]; obj = staff; // ok obj = new int[10]; // ok
-
equal方法
: 确定两个对象引用是否相等(还有基于状态检测是否相等), 为了防止调用方法的对象为 null,可以采用Objects.equals()
方法。-
子类可以有自己 相等性 概念,则 对称性需求 将强制使用 getClass 检测
-
如果由超类决定 相等性 概念, 可以使用
instanceof
检测,这样可以在不同子类之间进行相等性比较
完美
equals
方法建议: 👿 👿 👿-
显示参数命名为
otherObject
-
检测
this
和otherObject
是否相等if(this == otherObect) return true;
-
检测
otherObject
是否为 null,如果为 null,返回 false。if(otherObject == null) return false;
-
比较
this
与otherObject
类// 如果 equals 的语义可以在子类中改变,就使用 getClass检测: if(getClass() != otherObject.getClass()) return false // 如果所有子类都有相同的 相等性 语义,可以在父类中 使用 instanceof 检测 if(!(otherObject instanceof ClassName) return false // ClassName子类变量名
-
将
otherObject
强制转换为相应类类型变量ClassName other = (ClassName)otherObject
-
根据相等的概念来比较字段,使用
==
来比较 基本类型字段,使用Objects.equals
比较对象字段。return field1 == other.field1 && Objects.equals(field2, other.field2) && ...;
-
在子类重新定义了
equals
,就要在其中包含一个super.equals(other)
调用
-
-
-
getClass方法
: 返回这个对象运行时的类( 返回Class类型的对象 ) -
如果要定义一个 覆盖超类方法的 子类方法,可以使用
Override标记
。如果这个方法并没有覆盖超类的任何方法,就会看到一个错误报告。 -
散列码
(hash code): 由对象导出的一个 整型 值 。 👿 👿 👿-
Object类
的 hashcode 由 对象的存储地址 导出 -
String 的散列码 hashcode 的由 内容 导出(这就是为什么一样内容的 String字符串 导出的 hashcode值相同) 👿 👿 👿
// Sting类型 中的 hashcode 定义方式 int hash = 0 for(int i = 0; i.length(); i++) hash = 31 * hash + charAt(i);
-
重新定义了
equals方法
,就要重新定义hashcode方法
, 两者必须 相容(equal方法
返回 true,hashcode方法
必须返回相同的值 ) eg:如果比较 Employee.equals 比较员工 ID, 则hashcode方法
就需要散列 ID,而不是 姓名 和 存储地址 -
合理组合实例字段散列码,以便能让不同对象产生的散列码分布更均匀
- 可用
objects.hash(args1,...,argsn)
:对各个参数调用Objects.hashCode()
,并组合这些散列码 - 数组可用
Arrays.hashCode(xxx[] a)
, 散列码由数组元素的散列码组成
- 可用
-
-
toString方法
:返回表示对象值的一个字符串。-
遵循格式一般为:类名 (
getClass().getName
)+ 一对方括号括起来的字段值。 -
只要对象与
+
号连接起来。Java编译器就会自动调用toString 方法
来 获取这个对象的字符串描述 -
Object类
定义的toString方法
。可以打印对象的 类名 和 散列码 👿 👿 👿 -
数组继承了
Object类
的toString方法
,想要格式打印使用:Arrays.toString()
-
5.3 泛型数组列表 - ArrayList
-
在老版本中,使用
vector类
实现动态数组,但目前ArrayList类
更加高效,因舍弃以前的。 -
如果估计出数组可能存储的 元素数量, 可以调用
ensureCapacity
方法,这样前 n 次add调用不会带来开销很大的重新分配空间。 -
trimToSize方法
: 将存储块大小调整为保存当前元素数量所需要的 存储空间,垃圾回收器将回收多余空间 -
自动扩容的便利,增加了访问元素语法的复杂程度。必须使用
get
和set
方法访问和设置元素。 -
ArrayList
需要掌握的方法:E set(int index, E obj)
E get(int index)
void add(int index, E obj)
E remove(int index)
小技巧 : 既可以灵活扩展数组,又可以方便访问数组元素
var list = new ArrayList<X>(); while(...){ x = ...; list.add(x); } var a = new X[list.size()] list.toArray(a)
5.4 对象包装器 与 自动装箱
包装器类
是 不可变 的, 一旦构造,不允许更改包装在其中的值。包装器是final
,不能派生他的子类- 自动装箱 和拆箱 是
编译器
的工作,不是虚拟机
的。编译器
在生成类的字节码时就会插入必要的方法调用,虚拟机
只是执行 这些字节码
5.5 参数数量可变的方法
-
方法参数列表用到
...
, 相当于一个对应数据类型的数组,举个例子,// 与 double[] 完全一样 public static double max(double... values)
5.6 枚举类
-
自定义
枚举类型
,枚举类型
包括有限个命名的值。enum Size {SMALL, MEDIUM, LARGE, EXTRA_LARGE} // 声明 这种类型的变量 Size s = Size.MEDIUM // Size类型 只能存储 这个类型声明中给定的某个枚举值, 或者 null
-
实际上声明定义的类型 是 一个类, 他刚好有 4 个对象(枚举常量) ,不能再构造新的对象。因此比较两个枚举类型值 时 用
==
而非equals
👿 👿 👿 -
所有的枚举类型 都是
Enum类
的子类,继承了这个类很多方法-
toString()
, 返回枚举常量名 eg:Size.SMALL.toString()
返回字符串 “SMALL” -
valueOf()
,toString()
逆方法, 返回给定类中有指定名字的枚举常量 (枚举对象)// 将 是设置为 Size.SMALL Size s = Enum.valueOf(Size.class(), "SMALL")
-
static values()
, 返回一个包含全部枚举值的数组 eg:Size[] values = Size.values();
-
ordinal()
: 返回 enum 声明中 枚举常量的位置,从 0 开始计数 eg:Suze.MEDIUM.ordinal()
返回 1
-
5.7 反射
-
反射
: 能够分析 类能力 的程序 -
程序运行期间,
Java运行时系统
会始终为 所有对象 维护一个运行时类型标识
, 虚拟机利用这个信息选择要执行的正确的方法。他会跟踪每个对象所属类。可以使用一个特殊类访问这些信息,即Class类
。-
就像之前 Employee对象 描述一个特定员工的属性一样,
Class对象
会描述一个特定类的属性。 -
获取
Class对象
的方法:-
使用
Object类
中的getClass方法
Employee e; ... Class cl = e.getClass();
-
使用
Class类
的静态方法forName
获得类名所对应的对象(字符串会变化,使用这个类)String className = "java.util.Random"; Class cl = Class.forName(className);
-
如果
T
是 java 任意类型,使用T.class
将代表匹配的 类对象。Class cl1 = Random.class; Class cl2 = int.class; Class cl3 = DOuble[].class;
-
-
一个
Class对象
实际上 **表示的是一个类型,**这可能是 一个类,也可能不是 ,eg:int.class
-
Class类
实际上是一个 泛型类。 eg:Employee.class
的类型是Class<Employee>
-
虚拟机为 每个类型 管理 唯一 一个
Class对象
。所以上一节中使用==
实现两个类的比较(言外之意是:判断是否属于同一个类生成的对象):if(e.getClass() == Employee.class)
👿 👿 👿
-
-
使用
Class类
对象,构造类的实例。调用getConstructor
方法将得到一个Constructor
类型对象,然后使用newInstance
方法来构造实例var className = "java.util.Random"; Class cl = Class.forName(className); Object = cl.getConstructor().newInstance();
-
异常
的种类:非检查型异常
和检查型异常
非检查型异常
:编译器不希望你对这些异常提供处理器。检查型异常
:编译器将会检查你(程序员)是否知道这个异常并做好准备来处理后果
-
利用反射分析类的能力:反射机制的重要内容 -
检查类结构
java.lang.reflect
包中有三个类:Field
、Method
、Constructor
分别用于描述类的字段、方法、和构造器。带 Declared 修饰的方法包括从超类继承过来的,不带的只是这个类的。Field[] getFields()
Field[] getDeclaredFields()
Method[] getMethods()
Method[] getDeclaredMethods()
Constructor[] getConstructors()
Constructor[] getDeclaredConstructors()
- 打印对应方法、字段、构造器的修饰符,先获取 修饰符 对应的整数值:
cl.getModifiers()
, 然后利用该类的静态方法打印:Modifier.toStirng(cl.getModifiers())
-
使用 反射 在运行时分析对象: 可以利用反射机制, 查看在编译时还不知道的对象字段
-
Field类
的get方法
可以获取对象字段的值,也可以通过set(obj,value)
设置对象字段的值-
下面例子有一处不恰当之处,
name
是私有属性,会抛出IllegalAccessException异常
。Java安全机制
允许查看有哪些字段,如果想读写哪些字段,除非拥有访问权限 。👿 👿 👿var harry = new Emptyee("Harry Hacker", 50000, 10, 1, 1989); Class cl = harry.getClass(); Field f = cl.getDeclaredField("name"); Object v = f.get(harry)
-
-
反射机制
的 默认行为,受限制于Java的访问控制
。可以用Field
、Method
、Constructor
的setAccessible方法
修改权限,但是也可能被拒绝。eg:f.setAccessible(true);
-
-
使用
反射
编写 通用泛型数组
代码-
使用反射的 核心原因:可以 将得到的
Object泛型数组
转换回对应类型 。 -
java.lang.reflect包
中的Array类
,允许动态地创建数组-
举一个 下面例子, 编写一个 通用方法
var a = new Employee[100]; ... // 数组已满,需要扩充数组 a = Arrays.copyof(a, 2 * a.length);
-
坏的尝试,由于java数组会记住每个元素的类型,即创建数组时候 new表达式 使用的类型。
public static Object[] badCopyOf(Object[] a, int newLength) { var = newArray = new Object[newLength]; System.arraycopy(a, 0, newArray, 0, Math.min(a.length, newLength)); return newArray; }
-
上面的不好的原因是:返回的
Object[]数组
无法转换成Employee[] 数组
。因此,为了 编写通用代码,需要能够创建与原数组类型相同的数组。Public static Object goodCopyof(Object a, int newLength) { // 获得 a 数组的类对象 Class cl = a.getClass(); // 确认他是一个数组 if(!cl.isArray()) return null; // 确定数组的正确类型 Class componentType = cl.getComponentType(); int length = Array.getLength(a); Object newArray = Array.newInstance(componentType, newLength); System.arraycopy(a, 0, newArray, 0, Math.min(a.length, newLength)); return newArray; }
-
这里 用
Object
而不用Object[]
的原因是:为了使该方法可以扩展 任意类型数组 而不单单是对象数组。int[] a ={1, 2, 3, 4, 5}; a = (int[]) goodCopyof(a.10);
-
-
-
-
可以利用
反射机制
调用任意的方法
和构造器
-
d调用
任意方法
: 1.获得Method 对象
2. 调用invoke方法
-
例如 : 可以利用
Method类
中的invoke方法
,调用包装在当前 Method对象 中的方法// Object invoke(Object obj, Object...args) // 第一个 参数 是隐式参数(static 为 null), 其余对象提供显式参数 String n = (String) m1.invoke(harry)
-
如何得到
Method对象
方法, 有下面两种。不过有若干种 同名方法,所以还必须提供方法的参数类型-
调用
getDeclaredMethods方法
-
调用
Class类
的getMethod
`// Method getMethod(String name, Class... parameterTypes) // 说明了如何获得方法指针 Method m1 = Employee.class.getMethod("getname"); Method m2 = Employee.class.getMethod("raiseSalary", double.class);
-
-
调用
构造器
: 1. 获取Constructor对象
2. 调用方法newInstance方法
-
将 参数类型 提供给
Class.getConstructor方法
-
把 参数值 提供给
Constructor.newInstance方法
Class cl = Random.class; Constructor cons = cl.getConstructor(long.class); Object obj = cons.newInstance(42L);
-
-
使用反射获得的方法指针比直接调用慢很多(因为来回强制类型转换需要花费大量时间)
-
-
反射总结:
- 什么是 Class类
- 分析 类的结构: 字段、方法、构造器
- 在运行时 分析对象:主要是对象的字段
- 写泛型数组代码
- 调用 任意方法 和 构造器 :方法指针
5.8 继承的设计技巧
- 将公共操作和字段放在超类中
- 不要使用受保护字段
- 使用 继承实现“is- a” 关系时,避免滥用
- 除非所有继承方法都有意义,否则不要使用继承
- 在覆盖方法时,不要改变预期行为
- 使用多态,而不要使用类型信息
- 不要滥用反射