07-17条泛型和反射高质量准则

我们最大的弱点在于放弃。成功的必然之路就是不断的重来一次。 —— 达尔文

泛型可以减少将至类型转换,可以规范集合的元素类型,还可以提高代码的安全性和可读性,优先使用泛型。

反射可以“看透”程序的运行情况,可以让我们在运行期知晓一个类或实例的运行情况,可以动态的加载和调用,虽然有一定的性能忧患,但它带给我们的便利大于其性能缺陷。

93:Java的泛型是可以擦除的

1、Java泛型的引入加强了参数类型的安全性,减少了类型的转换,Java的泛型在编译器有效,在运行期被删除,也就是说所有的泛型参数类型在编译后会被清除掉,我们来看一个例子,代码如下:

image-20210218164019797

两个一样的方法冲突了?

这就是Java泛型擦除引起的问题:在编译后所有的泛型类型都会做相应的转化。转换规则如下:

  • List<String>、List<Integer>、List<T>擦除后的类型为List
  • List<String>[] 擦除后的类型为List[].
  • List<? extends E> 、List<? super E> 擦除后的类型为List<E>.
  • List<T extends Serializable & Cloneable >擦除后的类型为List<Serializable>.

2、明白了这些规则,再看如下代码:

1
2
3
4
5
public static void main(String[] args) {
List<String> list = new ArrayList<String>();
list.add("abc");
String str = list.get(0);
}

进过编译后的擦除处理,上面的代码和下面的程序时一致的:

1
2
3
4
5
public static void main(String[] args) {
List list = new ArrayList();
list.add("abc");
String str = (String) list.get(0);
}

3、Java之所以如此处理,有两个原因:

① 避免JVM的运行负担。

如果JVM把泛型类型延续到运行期,那么JVM就需要进行大量的重构工作了。

② 版本兼容

在编译期擦除可以更好的支持原生类型(Raw Type),在Java1.5或1.6…平台上,即使声明一个List这样的原生类型也是可以正常编译通过的,只是会产生警告信息而已。

4、明白了Java泛型是类型擦除的,我们就可以解释类似如下的问题了:

① 泛型的class对象是相同的:每个类都有一个class属性,泛型化不会改变class属性的返回值,例如:

1
2
3
4
5
6
public static void main(String[] args) {
List<String> list = new ArrayList<String>();
List<Integer> list2 = new ArrayList<Integer>();
System.out.println(list.getClass());
System.out.println(list.getClass()==list2.getClass());
}

以上代码返回true,原因很简单,List<String>和List<Integer>擦除后的类型都是List,没有任何区别。

image-20210218164108420

② 泛型数组初始化时不能声明泛型,如下代码编译时通不过: 

1
List<String>[] listArray = new List<String>[];

原因很简单,可以声明一个带有泛型参数的数组,但不能初始化该数组,因为执行了类型擦除操作,List<Object>[]与List<String>[] 就是同一回事了,编译器拒绝如此声明。

③ instanceof不允许存在泛型参数

以下代码不能通过编译,原因一样,泛型类型被擦除了:

image-20210218164229477

94:不能初始化泛型参数和数组

泛型类型在编译期被擦除,我们在类初始化时将无法获得泛型的具体参数,比如这样的代码:

image-20210218164238997

这段代码是编译不过的,因为编译时需要获得T类型,但泛型在编译期类型已经被擦除了。在某些情况下,我们需要泛型数组,那该如何处理呢?代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Student<T> {
// 不再初始化,由构造函数初始化
private T t;
private T[] tArray;
private List<T> list = new ArrayList<T>();

// 构造函数初始化
public Student() {
try {
Class<?> tType = Class.forName("");
t = (T) tType.newInstance();
tArray = (T[]) Array.newInstance(tType, 5);
} catch (Exception e) {
e.printStackTrace();
}
}
}

此时,运行就没有什么问题了,剩下的问题就是怎么在运行期获得T的类型,也就是tType参数,一般情况下泛型类型是无法获取的,不过,在客户端调用时多传输一个T类型的class就会解决问题。

类的成员变量是在类初始化前初始化的,所以要求在初始化前它必须具有明确的类型,否则就只能声明,不能初始化。

95:强制声明泛型的实际类型

Arrays工具类有一个方法asList可以把一个边长参数或数组转变为列表,但它有一个缺点:它所生成的list长度是不可变的,而在我们的项目开发中有时会很不方便。如果期望可变,那就需要写一个数组的工具类了,代码如下:

1
2
3
4
5
6
7
8
class ArrayUtils {
// 把一个变长参数转化为列表,并且长度可变
public static <T> List<T> asList(T... t) {
List<T> list = new ArrayList<T>();
Collections.addAll(list, t);
return list;
}
}

这很简单,与Arrays.asList的调用方式相同,我们传入一个泛型对象,然后返回相应的List,代码如下:

1
2
3
4
5
6
7
8
public static void main(String[] args) {
// 正常用法
List<String> list1 = ArrayUtils.asList("A", "B");
// 参数为空
List list2 = ArrayUtils.asList();
// 参数为整型和浮点型的混合
List list3 = ArrayUtils.asList(1, 2, 3.1);
}

这里有三个变量需要说明:

1、变量list1:变量list1是一个常规用法,没有任何问题,泛型实际参数类型是String,返回结果就是一个容纳String元素的List对象。

2、变量list2:变量list2它容纳的是什么元素呢?我们无法从代码中推断出list2列表到底容纳的是什么元素(因为它传递的参数是空,编译器也不知道泛型的实际参数类型是什么),不过,编译器会很聪明地推断出最顶层类Object就是其泛型类型,也就是说list2的完整定义如下:

1
List<Object> list2 = ArrayUtils.asList();

如此一来,编译器就不会给出” unchecked “警告了。现在新的问题又出现了:如果期望list2是一个Integer类型的列表,而不是Object列表,因为后续的逻辑会把Integer类型加入到list2中,那该如何处理呢?

强制类型转换(把asList强制转换成List<Integer>)?行不通,虽然Java泛型是编译期擦出的,但是List<Object>和List<Integer>没有继承关系,不能强制转换。  

重新声明一个List<Integer>,然后读取List<Object>元素,一个一个地向下转型过去?麻烦,而且效率又低。

最好的解决办法是强制声明泛型类型,代码如下:

1
List<Integer> intList = ArrayUtils.<Integer>asList();

就这么简单,asList方法要求的是一个泛型参数,那我们就在输入前定义这是一个Integer类型的参数,当然,输出也是Integer类型的集合了。

3、变量list3:变量list3有两种类型的元素:整数类型和浮点类型,那它生成的List泛型化参数应该是什么呢?是Integer和Float的父类Number?你太高看编译器了,它不会如此推断的,当它发现多个元素的实际类型不一致时就会直接确认泛型类型是Object,而不会去追索元素的公共父类是什么,但是对于list3,我们更期望它的泛型参数是Number,都是数字嘛,参照list2变量,代码修改如下:

1
List<Number> list3 = ArrayUtils.<Number>asList(1, 2, 3.1);

Number是Integer和Float的父类,先把三个输入参数、输出参数同类型,问题是我们要在什么时候明确泛型类型呢?一句话:无法从代码中推断出泛型的情况下,即可强制声明泛型类型。

96:不同的场景使用不同的泛型通配符

Java泛型支持通配符(Wildcard),可以单独使用一个“?”表示任意类,也可以使用extends关键字表示某一个类(接口)的子类型,还可以使用super关键字表示某一个类(接口)的父类型,但问题是什么时候该用extends,什么该用super呢?

1、泛型结构只参与 “读” 操作则限定上界(extends关键字),也就是要界定泛型的上界

image-20210218164310968

编译失败,失败的原因是list中的元素类型不确定,也就是编译器无法推断出泛型类型到底是什么,是Integer类型?是Double?还是Byte?这些都符合extends关键字的定义,由于无法确定实际的泛型类型,所以编译器拒绝了此类操作。

2、泛型结构只参与“写” 操作则限定下界(使用super关键字),也就是要界定泛型的下界

image-20210218164318543

甭管它是Integer的123,还是浮点数3.14,都可以加入到list列表中,因为它们都是Number的类型,这就保证了泛型类的可靠性。

97:警惕泛型是不能协变和逆变的

协变:窄类型替换宽类型

逆变:宽类型替换窄类型

1、泛型不支持协变,编译不通过,,,窄类型变成宽类型(Integer>>Number)

image-20210218164326279

泛型不支持协变,但可以使用通配符模拟协变,代码如下:

image-20210218164351019

“ ? extends Number “ 表示的意思是,允许Number的所有子类(包括自身) 作为泛型参数类型,但在运行期只能是一个具体类型,或者是Integer类型,或者是Double类型,或者是Number类型,也就是说通配符只在编码期有效,运行期则必须是一个确定的类型。

2、泛型不支持逆变

image-20210218164359244

image-20210218164406956

“ ? super Integer “ 的意思是可以把所有的Integer父类型(自身、父类或接口) 作为泛型参数,这里看着就像是把一个Number类型的ArrayList赋值给了Integer类型的List,其外观类似于使用一个宽类型覆盖一个窄类型,它模拟了逆变的实现。

98:list中泛型顺序为T、?、Object

List<T>、List<?>、List<Object>这三者都可以容纳所有的对象,但使用的顺序应该是首选List<T>,次之List<?>,最后选择List<Object>,原因如下:

1、List<T>是确定的某一类型

List<T>表示的是list集合中的元素是T类型,具体类型在运行期决定;

List<?>表示的是任意类型,与List<T>类型,而List<Object>表示list集合中的所有元素为Object类型,从字面意义上来分析,List<T>更符合习惯,编译者知道它是某一个类型,只是在运行期确定而已。

2、List<T>可以进行读写操作,不能进行增加修改操作,因为编译器不知道list中容纳的是什么类型的元素,也就无法校验类型是否安全。

而List<?>读取出的元素都是Object类型的,需要主动转型,所以它经常用于泛型方法的返回值。注意List<?>虽然无法增加,修改元素,但是却可以删除元素,比如执行remove、clear等方法,那是因为它的删除动作与泛型类型无关。

List<Object> 也可以读写操作,但是它执行写入操作时需要向上转型(Up cast),在读取数据的时候需要向下转型,而此时已经失去了泛型存在的意义了。

99:严格限定泛型类型采用多重界限

在Java的泛型中,可以使用&符号关联多个上界(extends)并实现多个边界限定,下界(super)没有多重限定的情况。

100:数组的真实类型必须是泛型类型的子类型

List接口的toArray方法可以把一个集合转化为数组,但是使用不方便,toArray()方法返回的是一个Object数组,所以需要自行转变。toArray(T[] a)虽然返回的是T类型的数组,但是还需要传入一个T类型的数组,这也挺麻烦的,我们期望输入的是一个泛型化的List,这样就能转化为泛型数组了,来看看能不能实现,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import java.util.Arrays;
import java.util.List;

public class GenericFruit {

public static <T> T[] toArray(List<T> list) {
T[] t = (T[]) new Object[list.size()];
for (int i = 0, n = list.size(); i < n; i++) {
t[i] = list.get(i);
}
return t;
}

public static void main(String[] args) {
List<String> list = Arrays.asList("A","B");
for(String str :toArray(list)){
System.out.println(str);
}
}
}

编译没有任何问题,运行后出现如下异常:

image-20210218164426740

数组是一个容器,只有确保容器内的所有元素类型与期望的类型有父子关系时才能转换,Object数组只能保证数组内的元素时Object类型,却不能确保它们都是String的父类型或子类,所以类型转换失败。

总而言之,就是数组使用具体类型使用就完了。

101:注意Class类的特殊性

Java语言是先把Java源文件编译成后缀为class的字节码文件,然后通过classLoader机制把这些类加载到内存中。最后生成实例执行。

class类是Java的反射入口,只有在获得一个类的描述对象后才能动态的加载、调用,一般获得class对象的三种方法:

1、类属性加载:如String.class

2、对象的getClass方法:如new String.getClass()

3、forName方法加载:如Class.forName(“java.lang.String”)

获得class对象之后,就可以通过getAnnotation()获得注解,通过getMethods()获得方法,通过getConstructors()获得构造函数等。

102:适时选择getDeclaredXXX和getXXX

getMethod方法获得的是所有public访问级别的方法,包括从父类继承的方法。

getDeclaredMethod获得的是自身类的方法,包括公用的(public)方法、私有(private)方法,而且不受限于访问权限。

103:反射访问属性或方法时Accessible设置为true

Java中通过反射执行一个方法:获取一个方法对象,然后根据isAccessible返回值确定是否能够执行,如果返回false,则调用setAccessible(true),然后再调用invoke执行方法:

1
2
3
4
5
6
7
Method method= ...;
//检查是否可以访问
if(!method.isAccessible()){
method.setAccessible(true);
}
//执行方法
method.invoke(obj, args);

通过反射方法执行方法时,必须在invoke之前检查Accessible属性。

104:使用forName动态加载类文件

动态加载是指程序运行时加载需要的类库文件,对Java程序来说,雷哥类文件在启动时或首次初始化时会被加载到内存中,而反射则可以在运行时再决定是否需要加载。

动态加载的意义在于:加载一个类表示要初始化该类的static变量,特别是static代码块,在这里可以做大量的工作,比如注册,初始化环境等等。

对于动态加载最经典的应用就是数据库驱动程序的加载片段,代码如下:

1
2
3
4
5
//加载驱动
Class.forName("com.mysql..jdbc.Driver");
String url="jdbc:mysql://localhost:3306/db?user=&password=";
Connection conn =DriverManager.getConnection(url);
Statement stmt =conn.createStatement();

当程序动态加载该驱动时,也就是执行到Class.forName(“com.mysql..jdbc.Driver”)时,Driver类会被加载到内存中,于是static代码块开始执行,也就是把自己注册到DriverManager中。

forName只是把一个类加载到内存中,并不保证由此产生一个实例对象,也不会执行任何方法,之所以会初始化static代码,那是由类加载机制所决定的,而不是forName方法决定的。也就是说,如果没有static属性或static代码块,forName就是加载类,没有任何的执行行为。

总而言之,forName只是把一个类加载到内存中,然后初始化static代码。

105:动态加载不适合数组

106:动态代理可以使代理模式更加灵活

Java的反射框架提供了动态代理(Dynamic Proxy)机制,允许在运行期对目标类生成代理,避免重复开发。我们知道一个静态代理是通过主题角色(Proxy)和具体主题角色(Real Subject)共同实现主题角色(Subkect)的逻辑的,只是代理角色把相关的执行逻辑委托给了具体角色而已,一个简单的静态代理如下所示:

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
38
39
40
41
42
43
44
45
46
interface Subject {
// 定义一个方法
public void request();
}

// 具体主题角色
class RealSubject implements Subject {
// 实现方法
@Override
public void request() {
// 实现具体业务逻辑
}

}

class Proxy implements Subject {
// 要代理那个实现类
private Subject subject = null;

// 默认被代理者
public Proxy() {
subject = new RealSubject();
}

// 通过构造函数传递被代理者
public Proxy(Subject _subject) {
subject = _subject;
}

@Override
public void request() {
before();
subject.request();
after();
}

// 预处理
private void after() {
// doSomething
}

// 善后处理
private void before() {
// doSomething
}
}

这是一个简单的静态代理。Java还提供了java.lang.reflect.Proxy用于实现动态代理:只要提供一个抽象主题角色和具体主题角色,就可以动态实现其逻辑的,其实例代码如下:

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
interface Subject {
// 定义一个方法
public void request();
}

// 具体主题角色
class RealSubject implements Subject {
// 实现方法
@Override
public void request() {
// 实现具体业务逻辑
}

}

class SubjectHandler implements InvocationHandler {
// 被代理的对象
private Subject subject;

public SubjectHandler(Subject _subject) {
subject = _subject;
}

@Override
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable {
// 预处理
System.out.println("预处理...");
//直接调用被代理的方法
Object obj = method.invoke(subject, args);
// 后处理
System.out.println("后处理...");
return obj;
}
}

注意这里没有代理主题角色,取而代之的是SubjectHandler作为主要的逻辑委托处理,其中invoke方法是接口InvocationHandler定义必须实现的,它完成了对真实方法的调用。

通过InvocationHandler接口的实现类来实现,所有的方法都是有该Handler进行处理的,即所有被代理的方法都是由InvocationHandler接管实际的处理任务。

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
public static void main(String[] args) {
//具体主题角色,也就是被代理类
Subject subject = new RealSubject();
//代理实例的处理Handler
InvocationHandler handler =new SubjectHandler(subject);
//当前加载器
ClassLoader cl = subject.getClass().getClassLoader();
//动态代理
Subject proxy = (Subject) Proxy.newProxyInstance(cl,subject.getClass().getInterfaces(),handler);
//执行具体主题角色方法
proxy.request();
}

此时实现了不用显示创建代理类即实现代理的功能,例如可以在被代理的角色执行前进行权限判断,或者执行后进行数据校验。

动态代理很容易实现通用的代理类,只要在InvocationHandler的invoke方法中读取持久化的数据即可实现,而且还能实现动态切入的效果。

107:使用反射增加修饰模式的普适性

装饰模式(Decorator Pattern)的定义是“动态的给一个对象添加一些额外的职责。就增加功能来说,装饰模式相比于生成子类更加灵活“,不过,使用Java的动态代理也可以实现修饰模式的效果,而且其灵活性、适应性会更强。

我们以卡通片《猫和老鼠》(Tom and Jerry)为例,看看如何包装小Jerry让它更强大。首先定义Jerry的类:老鼠(Rat类),代码如下: 

1
2
3
4
5
6
7
8
9
10
interface Animal{
public void doStuff();
}

class Rat implements Animal{
@Override
public void doStuff() {
System.out.println("Jerry will play with Tom ......");
}
}

接下来,我们要给Jerry增加一些能力,比如飞行,钻地等能力,当然使用继承也很容易实现,但我们这里只是临时的为Rat类增加这些能力,使用装饰模式更符合此处的场景,首先定义装饰类,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//定义某种能力
interface Feature{
//加载特性
public void load();
}
//飞行能力
class FlyFeature implements Feature{

@Override
public void load() {
System.out.println("增加一对翅膀...");
}
}
//钻地能力
class DigFeature implements Feature{
@Override
public void load() {
System.out.println("增加钻地能力...");
}
}

此处定义了两种能力:一种是飞行,另一种是钻地,我们如果把这两种属性赋予到Jerry身上,那就需要一个包装动作类了,代码如下: 

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
class DecorateAnimal implements Animal {
// 被包装的动物
private Animal animal;
// 使用哪一个包装器
private Class<? extends Feature> clz;

public DecorateAnimal(Animal _animal, Class<? extends Feature> _clz) {
animal = _animal;
clz = _clz;
}

@Override
public void doStuff() {
InvocationHandler handler = new InvocationHandler() {
// 具体包装行为
@Override
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable {
Object obj = null;
if (Modifier.isPublic(method.getModifiers())) {
obj = method.invoke(clz.newInstance(), args);
}
animal.doStuff();
return obj;
}
};
//当前加载器
ClassLoader cl = getClass().getClassLoader();
//动态代理,又handler决定如何包装
Feature proxy = (Feature) Proxy.newProxyInstance(cl, clz.getInterfaces(), handler);
proxy.load();
}
}

注意看doStuff方法,一个修饰类型必然是抽象构建(Component)的子类型,它必须实现doStuff方法,此处的doStuff方法委托了动态代理执行,并且在动态代理的控制器Handler中还设置了决定修饰方式和行为的条件(即代码中InvocationHandler匿名类中的if判断语句),当然,此处也可以通过读取持久化数据的方式进行判断,这样就更加灵活了。

编写客户端进行调用了,代码如下: 

1
2
3
4
5
6
7
8
9
10
public static void main(String[] args) {
//定义Jerry这只老鼠
Animal jerry = new Rat();
//为Jerry增加飞行能力
jerry = new DecorateAnimal(jerry, FlyFeature.class);
//jerry增加挖掘能力
jerry = new DecorateAnimal(jerry, DigFeature.class);
//Jerry开始戏弄毛了
jerry.doStuff();
}

此类代码只是一个比较通用的装饰模式,只需要定义被装饰的类及装饰类即可,装饰行为由动态代理实现,实现了对装饰类和被装饰类的完全解耦,提供了系统的扩展性。

109:不需要太多关注反射效率

反射的效率相对于正常的代码执行确实低很多,但它是一个非常有效的运行期工具类,只要代码结构清晰、可读性好那就先开发出来,等到进行性能测试时有问题再优化。

最基本的编码规则:”Don’t Repeat Yourself”


07-17条泛型和反射高质量准则
https://janycode.github.io/2016/12/28/02_编程语言/01_Java/05_高质量代码/07-17条泛型和反射高质量准则/
作者
Jerry(姜源)
发布于
2016年12月28日
许可协议