Java笔记_7
继承
基本思想: 基于已有的类创建新的类, 复用已有类的方法, 同时可以增加一些新的方法和字段
- 反射是程序在运行期间更多地了解类以及属性的能力
CPP
中使用:
表示继承, 除了公共继承以外, 还存在私有继承和保护继承Java
中extends
关键字表示继承, 所有继承都是公共继承
extends
表示正在构造的类(子类, 派生类)派生于一个已经存在的类(超类, 基类, 父类)- 子类比超类拥有更多的功能
- “声明为私有的类成员不会被这个类的子类继承”
- 这里其实是子类不能直接访问这些私有成员
- 但是子类的每个实例对象中依然会包含超类中的私有字段
- 记录不能被扩展, 记录也不能继承别的类
- 如果希望使用超类中的方法, 就使用
super
关键字,super
只是用于指示编译器调用超类方法的特殊关键字- 同时可以使用
super()
来调用超类中对应的构造器 - 不管是
this
还是super
, 在调用其他构造器的时候, 都必须放在第一行, 否则会报错
- 同时可以使用
- 一个对象可以指示多种实际类型
- 比如一个类的超类以及他的子类都在一个数组中, 使用
foreach
循环的时候, 循环变量可以同时指示多个不同的类, 那么这就是多态
- 比如一个类的超类以及他的子类都在一个数组中, 使用
- 运行的时候可以自动的选择适合的方法, 就是动态绑定
CPP
中, 如果希望实现动态绑定, 可以将成员函数设置为virtual
Java
中默认会执行动态绑定, 如果不希望方法是虚拟的, 可以使用final
关键字CPP
中, 一个类可以有多个超类Java
中不支持多重继承, 但是可以使用接口实现多重继承的功能
对象方法调用过程
假设需要
x.f(args)
,x
是类C
的一个对象, 具体调用过程如下:
- 编译器查看对象的声明类型和方法名, 可能存在多个名为
f
的方法, 他们具有不同的参数类型, 编译器会一一列举出C
中的所有名为f
的方法, 以及C
的超类中所有名为f
的非私有方法 - 重载解析: 编译器确定方法调用中提供的参数类型, 如果所有名为
f
的方法中存在一个与所提供的参数类型完全匹配的方法, 就直接使用这个方法- 如果找不到匹配的方法, 或者找到了多个匹配的方法, 编译器就会抛出异常
- 方法的名字与参数类型会组成签名, 如果子类的签名和超类的重复了, 子类的方法会覆盖超类的方法
- 尽管返回类型不是签名的一部分, 但是在覆盖的时候, 需要保证子类的返回类型是超类返回类型的子类型
- 如果是
private
,static
,final
方法 或者 构造器方法, 编译器就可以准确知道调用哪个方法, 这称为静态绑定. 如果调用方法依赖于隐式参数的实际类型, 就是动态绑定. 所以只要不是上述四种方法, 就是动态绑定 - 程序使用动态绑定时, 虚拟机必须调用与
x
引用对象实际类型对应的方法, 比如x
的实际类型是D
,D
是C
的子类, 如果D
定义了方法f(String)
, 那么就会调用这个方法, 否则就会在D
的超类中寻找这个方法- 如果每一次调用方法都需要执行一次搜索的话, 时间消耗非常大, 所以虚拟机预先为每个类计算了一个方法表, 列出了所有方法的签名和需要调用的方法
- 虚拟机加载一个类以后就可以构建这个方法表
- 覆盖一个方法的时候, 子类方法的可见性不能低于超类方法的可见性
- 如果超类方法是
public
, 子类也必须要是public
方法, 如果漏了, 就会报错
final
方法
- 使用
final
修饰一个类, 就可以阻止定义这个类的子类1
public final class E extends M {}
final
类的方法自动变为final
方法final
类的字段不会自动变为final
字段
- 如果将类中的某个方法设定为
final
, 则该类的所有子类都不能覆盖这个方法1
2
3public class E {
public final String getName() {}
} final
字段表示以后都不会改变的字段- 枚举和记录总是
final
的, 因为他们不允许被扩展
对象引用的强制类型转换
- 现在有
M
类是E
的子类1
2
3
4
5
6var s = new E[3];
s[0] = new M();
s[1] = new E();
s[2] = new E();
M b = (M) s[0]; // 强制类型转换
M c = (M) s[1]; // 抛出异常 ClassCastException - 为了避免抛出异常, 可以使用
instanceof
1
2
3if (s[i] instanceof M) {
b = (M) s[i];
}JDK 16
中有更加简单的写法, 可以直接在instanceof
语句中声明子类变量1
2
3if (s[i] instanceof M b) {
b.setB();
}
- 只能在继承层次内使用强制类型转换
- 将超类转换为子类之前, 需要使用
instanceof
检查类型 - 如果
x
是null
,x instanceof C
不会抛出异常, 返回false
- 一般情况下最好少用强制类型转换和
instanceof
protected
- 将超类中的某个字段声明为
protected
, 子类就可以进行访问 - 受保护的字段只能由同一个包中的类进行访问, 如果子类在不同的包中, 就不能访问了
- 相比之下,
protected
方法更有意义, 表示可以相信子类能够正确的使用这个方法 - 所以
Java
中的protected
允许所有子类, 以及同一个包中的所有其他类访问, 不如CPP
中的安全
Object
Object
类是Java
中所有类的超类
写equals
方法
- 显式参数命名为
otherObject
- 检测
this
与otherObject
是否相同
1 | if (this == otherObject) { |
- 检测
otherObject
是否为null
, 如果为null
, 则返回false
1 | if (otherObject == null) { |
- 比较
this
与otherObject
的类
如果equals
的语义可以在子类中改变, 就使用getClass
检测
1 | if (getClass() != otherObject.getClass()) { |
如果所有的子类都有相同的相等性语义, 就可以使用instanceof
检测
1 | if (!(otherObject instanceof ClassName other)) { |
- 使用相等性概念来比较字段, 使用
==
比较基本类型字段, 使用Objects.equals
比较对象字段, 如果所有的字段都匹配, 返回true
1 | return field1 == other.field1 && Objects.equals(field2, other.field2) && ... ; |
- 对于数组类型, 使用
Arrays.equals()
方法检查相应的数组元素, 如果是多维数组, 可以使用Arrays.deepEquals()
1 | public class E { |
hashCode
hashCode
方法定义在Object
类中, 所以每个对象都有一个默认的散列码, 由对象的存储地址得出
- 自定义
hashCode
方法
1 | public class E { |
- 如果有数组类型, 可以使用静态
Arrays.hashCode()
计算一个散列码, 这个散列码由数组元素的散列码组成 - 记录类型会自动提供一个
hashCode()
, 由字段值的散列码得到一个散列码
toString()
- 使用
getClass().getName()
获得类名的字符串 - 每一个子类都应该实现自己的
toString()
, 如果超类中实现了, 可以直接使用super.toString()
- 只要一个对象与一个字符串通过
+
连接, 编译器就会自动调用toString()
方法获得这个字符串的描述 - 如果
x
是任意一个对象, 使用System.out.println(x)
也会自动调用x.toString()
Objects
定义了toString()
, 会打印对象的类名和散列码
ArrayList
ArrayList
是一个有类型参数的泛型类
- 使用
var
可以避免重复写类型var staff = new ArrayList<E>();
- 如果使用了
var
, 就需要声明类型; 如果不使用var
, 可以使用菱形语法ArrayList<E> staff = new Arraylist<>();
- 如果使用了
- 使用
add
添加元素, 如果满了会自动扩容 - 如果可以估计大小, 可以在添加元素之前使用
staff.ensureCapacity(nums)
来设置分配的空间- 也可以使用
var staff = new ArrayList<E>(nums)
将初始容量传递给构造器
- 也可以使用
- 如果数组的大小保持恒定不会发生变化了, 可以使用
staff.trimToSize()
将内存块的大小调整为当前所需空间,GC
回收多余的空间
对象包装器
- 有时候需要将
int
转换为对象, 所有基本类型都有一个与之对应的类 Integer, Long, Float, Double, Short, Byte, Character, Boolean
前六个派生于公共超类Number
- 包装类不可变, 一旦构造了包装器, 就不能更改其中的值
- 包装器类还是
final
, 所以不能派生子类 - 尖括号中的类型参数不能是基本数据类型, 必须要是包装器类型
- 因为每一个值都包装在对象中, 所以
ArrayList<integer>
效率低于int[]
- 当使用
list.add(3)
的时候, 会自动转换为list.add(Integer.valueOf(3))
, 这就是自动装箱 - 同样的, 当我们使用
int n = list.get(i)
的时候, 实际上是将Integer
对象赋值给int
类型, 这是自动拆箱, 等价于list.get(i).intValue()
Integer n = 3; n ++;
这里面实际上先自动拆箱, 然后+1
, 再自动装箱- 不要使用包装器类构造器, 可以使用
Integer.valueOf(i)
, 也可以依赖自动装箱:Integer a = i
, 不要使用new Integer(i)
, 这个将会被删除 - 包装器类引用可以为
null
, 所以会触发NPE
- 如果表达式中混用了
Integer, Double
, 则Integer
会自动拆箱, 提升为double
, 再自动装箱为Double
- 自动装箱和拆箱是编译器做的工作, 不是虚拟机
可变参数个数方法
1 | public class PrintStream { |
- 这里的
...
表示接受任意数量的对象, 是一个Object
数组, 保存着除了fmt
之外的其他参数 - 如果调用者给了其他类型或者基本类型的值, 就会自动装箱为对象, 现在就只需要
fmt
中扫描到第i
个格式说明, 与args[i]
值匹配
抽象类
- 如果一个类中存在抽象方法, 类本身必须声明为抽象的; 抽象类可以没有抽象方法; 抽象类可以有具体字段和方法
1
2
3public abstract class P {
public abstract String getD();
} - 抽象类的子类可以保留抽象类中的部分或所有抽象方法, 那么子类依然是抽象的; 也可以全部实现, 则子类不是抽象的
- 抽象类不能被实例化, 但是可以存在抽象类的变量, 只是只能引用其非抽象子类
1
P p = new S();
- 接口是抽象类的泛化
密封类
- 比如有一个抽象类
JSONValue
, 还有两个final
子类, 分别是JSONNumber, JSONArray
- 两个子类是
final
的, 所以无法被派生了, 但是不能阻止别人派生JSONValue
- 可以将
JSONValue
声明为密封类1
2
3
4
5public abstract sealed class JSONValue
permits JSONArray, JSONNumber, JSONString, JSONBoolean, JSONObject, JSONNull {
...
}
// 这样使用sealed声明为密封类, 可以保证JSONValue只有六个定义好的子类, 无法派生别的子类了 - 一个密封类的子类必须是可以访问的, 不能是嵌套在别的类中的私有类, 也不能位于另一个包中
- 密封类允许的公共子类, 必须要在同一个包中, 如果使用了模块, 还必须要在同一个模块中
- 声明密封类可以不加
permits
, 但是这样的话所有子类都必须要在同一个文件中声明, 这样的话, 子类就不是公共的了 - 密封类的子类必须声明为
sealed, final, non-sealed
中的一种,最后一种允许继续派生
反射
Class
类
Java
始终为所有对象维护一个运行时类型标识, 跟踪每个对象所属的类- 可以用
Class
类访问这些信息,Object.getClass()
返回一个Class
对象的实例- 最常用的方法就是
getName()
, 返回一个对象类型的名称, 包名也作为类名的一部分
- 最常用的方法就是
- 也可以直接使用
类名.class
的方法访问这个类 - 虚拟机为每个类型管理一个唯一的
Class
对象, 所以可以使用==
比较1
2
3if (e.getClass() == E.class) {}
// 如果e是一个E的实例, 则为true, 如果e是M的实例, 其中M是E的子类, 则为false
// 如果是 e instanceof E的话, 那么当e是M的实例时, 依然会返回true - 如果有一个
Class
类型的对象, 可以用他构造实例1
2
3
4var className = "java.uril.Random";
Class cl = Class.forName(className);
Object obj = cl.getConstructor().newInstance();
// 如果这个类没有无参构造器, 则会抛出异常InvocationTargetException Class.forName()
会抛出一个检查型异常, 没有办法保证指定名字的类一定存在, 所以需要在函数后面加上throws ReflectOperationException
异常
- 分两种: 非检查型异常和检查型异常
- 检查型异常: 编译器会检查你是否知道这个异常, 并做好准备处理
- 非检查型异常: 比如数组越界,
null
引用访问, 编译器不期望你为这些异常提供处理方法
应用
-
资源文件加载
- 获得拥有资源的类的
Class
对象, 比如ResourcesTest.class
- 调用部分可以接受描述资源位置
URL
的方法, 比如URL url = cl.getResource("about.txt");
- 否则, 使用
getResourceStream()
得到输入流读取文件
- 获得拥有资源的类的
-
国际化
- 与语言相关的字符串都放在资源文件中, 每个语言对应一个文件
利用反射分析类
java.util.reflect
包中有三个类:Field, Method, Constructor
, 分别用于描述类的字段, 方法和构造器- 三个类都有一个方法, 名为
getName()
Field.getType()
可以返回字段类型的一个对象, 对象的类型同样是Class
Method, Constructor
有报告类型参数的方法,Method
有报告返回类型的方法, 三者都有getModifiers()
返回一个整数, 用不同的0/1
位描述修饰符, 比如public, static
- 可以使用
Modifier
类的静态方法分析getModifiers()
返回的整数, 需要做的就是在返回的整数基础上, 调用Modifier
类中适当的方法 - 可以用
Modifier.toString()
打印修饰符
- 三个类都有一个方法, 名为
Class
中的getFields(), getMethods(), getConstructors()
分别返回这个类支持的公共字段, 方法和构造器Class
中的getDeclaredFields(), getDeclaredMethods(), getDeclaredConstructors()
返回这个类声明的全部字段, 方法和构造器, 包括私有成员, 包成员,protected
成员, 有包访问权限的成员, 但是不会包括超类的成员
利用反射分析对象
- 利用反射可以查看在编译时还不知道的对象字段
- 利用
Field
中的get
, 比如f
是一个Field
类型的对象,obj
是包含f
字段的类的对象, 则f.get(obj)
将会返回一个对象, 值为obj
的当前字段值1
2
3
4var h = new E();
Class cl = h.getClass();
Field f = cl.getDeclaredField("name");
Object v = f.get(h); - 同样可使用
f.set(obj, val)
设置值, 但是如果name
是一个私有字段, 则不能使用get, set
, 会抛出IllageAccessException
- 只能对可以访问的字段使用
get, set
,Java
允许查看一个对象中的字段, 但是无法访问 - 不过可以调用
f.setAccessible(true)
覆盖Java
的访问控制 setAccessible()
是Field, Method, Constructor
的公共超类AccessibleObject
中的方法
通用toString()
- 使用
getDeclaredFields
获得实例字段, 使用setAccessible()
将字段设置为可以访问的, 再对每个字段调用toString()
- 不过如果引用循环会导致无限递归,
ObjectAnalyzer
会跟踪已经访问过的对象
使用反射编写泛型数组
java.util.reflect
中的Array
类,Arrays.copyOf()
就使用了这个类, 这个方法可以用来扩展一个数组1
2
3
4
5
6
7
8
9
10public static Object goodCopyOf(Object a, int newLength) {
Class cl = a.getClass();
if (!cl.isArray()) return null;
Class componentType = cl.getComponentType();
// 如果对象是一个数组类型, 返回对应元素的Class, 否则返回null
int len = Array.getLength(a);
Object newArray = Array.newInstance(componentType, newLength);
System.arraycopy(a, 0, newArray, 0, Math.min(length, newLength));
return newArray;
}- 这个
goodCopyOf
可以扩展任意数组, 参数声明为Object
类型, 而不是Object[]
, 因为int[]
可以转换为一个Object
, 而不是转换成对象数组 - 如果是
Object[]
的话, 在强制类型转换回去的时候会抛出异常ClassCastException
使用反射调用任意的方法
- 可以使用
Field.get()
查看一个方法的字段, 使用Method.invoke()
调用包装在当前Method
中的方法1
2
3
4
5
6
7
8
9
10
11
12
13
14
15Object invoke(Object obj, Object... args);
// 第一个参数是隐式参数, 其他参数是显式参数
// 如果是静态方法, 则第一个参数会被忽略为null
// 比如m1表示E类中的getName()
String n = (String) m1.invoke(h);
// 如果返回的是基本数据类型, invoke会返回其包装类型, 需要强制类型转换后使用自动拆箱
// 比如m2表示E类中的getSalary()
double d = (Double) m2.invoke(h);
// 使用getMethod()可以得到一个类中的方法
Method m1 = E.class.getMethod("getName");
Method m2 = E.class.getMethod("getSalary", double.class);
// 可以获得构造器方法
Class cl = Random.class;
Constructor cons = cl.getConstructor(long.class);
Object obj = cons.newInstance(42L); Method, Construtor
类扩展了Executable
类, 并且Executable
是sealed
的, 只允许Method, Constructor
作为子类- 比如调用
Math.sqrt()
1
2
3
4
5
6
7
8double dx = (to - from) / (n - 1);
Math.class.getMethod("sqrt", double.class);
for (double x = from; x <= to; x += dx) {
double y = (Double) f.invoke(null, x);
// 因为Math.sqrt是一个静态方法, 所以invoke的第一个参数为null
System.out.printf("%10.4f | %10.4f%n", x, y);
} - 反射能完成所有操作, 但是很不方便,
invoke
参数错误还会抛出异常. 同时invoke
返回类型一定是Object
的, 所以必须来回强制类型转换 - 编译器就丧失了检查代码的机会, 反射获得方法指针的代码比直接调用慢的多
- 所以一般只有绝对必要的时候才会引入
Method
对象, 更好的方法是使用lambda
表达式 - 不要使用回调函数的
Method
对象, 要是用回调的接口, 这样执行速度更快, 也更好维护
继承设计技巧
- 公共字段和方法放在超类中
- 不要使用
protected
- 使用继承实现
is-a
关系 - 除非所有继承的方法都有意义, 否则不要使用继承
- 覆盖方法不要改变预期的行为
- 不要滥用反射
- 使用多态, 不要使用类型信息
1
2
3
4
5
6
7if (x is of type 1)
action1(x)
else
action2(x)
// 这种形式的代码都可以用多态实现
// 如果action1() action2()表示通用的概念, 可以定义为这两个类型的公共超类或接口中的方法, 然后可以调用
// x.action(), 利用多态的动态分配机制执行正确的动作
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 Sangs Blog!