文章目录
- 编写高质量代码: 改善Java程序的151个建议
- 第七章 泛型与反射
- 前言
- 建议93:Java的泛型是类型擦除的
- 建议94:不能初始化泛型参数和数组
- 建议95:强制声明泛型的实际类型
- 建议96:不同的场景使用不同的泛型通配符
- 建议97:警惕泛型是不能协变和逆变的
- 建议98:建议采用的顺序是List,List,List
- 建议99:严格限定泛型类型采用多重界限
- 建议100:数组的真实类型必须是泛型类型的子类型
- 建议101:注意Class类的特殊性
- 建议102:适时选择getDeclaredXXX和getXXX
- 建议103:反射访问属性或方法是将Accessible设置为true
- 建议104:使用forName动态加载类文件
- 建议105:动态加载不适合数组
- 建议106:动态代理可以使代理模式更加灵活
- 建议107:使用反射增加装饰模式的普适性
- 建议108:反射让模板方法模式更强大
- 建议109:不需要太多关注反射效率
编写高质量代码: 改善Java程序的151个建议
第七章 泛型与反射
前言
前言 圈主 [Rocky编程日记] 学习 编写高质量代码: 改善Java程序的151个建议 笔记记录。希望我写得笔记你能够喜欢, 希望我写的笔记能够给你提供帮助。同时若笔记中存在不对的地方,那一定是圈主当时的理解还不够, 希望你能够及时指出嗷~
建议93:Java的泛型是类型擦除的
-
加入泛型优点:加强了参数类型的安全性,减少了类型的转换。Java的泛型在编译期有效,在运行期被删除,也就是说所有的泛型参数类型在编译后都会被清除掉。
- 泛型的class对象时是相同的;
- 泛型数组初始化时不能声明泛型类型;
- instanceof不允许存在泛型参数
public class Suggest93 { public void arrayMethod(String[] strArray) {} public void arrayMethod(Integer[] strArray) {} // 'listMethod(List<String>)' clashes with // 'listMethod(List<Integer>)'; // both methods have same erasure // public void listMethod(List<String> strArray) {} public void listMethod(List<Integer> strArray) {} }
建议94:不能初始化泛型参数和数组
- 泛型类型在编译期被擦除,在类初始化时将无法获得泛型的具体参数,所以泛型参数和数组无法初始化,但是ArrayList却可以,因为ArrayList初始化是向上转型变成了Object类型;需要泛型数组解决办法:只声明,不再初始化,由构造函数完成初始化操作
建议95:强制声明泛型的实际类型
- 无法从代码中推断出泛型类型的情况下,即可强制声明泛型类型;方法:List list2 = ArrayUtils.****asList();在输入前定义这是一个Integer类型的参数
建议96:不同的场景使用不同的泛型通配符
- Java泛型支持通配符(Wildcard),可以单独使用一个“?”表示任意类,也可以使用extends关键字表示某一个类(接口)的子类型,还可以使用super关键字表示某一个类(接口)的父类型。
- 泛型结构只参与“读”操作则限定上界(extends关键字);
- 泛型结构只参与“写”操作则限定下界(使用super关键字);
- 如果一个泛型结构既用作“读”操作也用作“写”操作则使用确定的泛型类型即可,如List
建议97:警惕泛型是不能协变和逆变的
-
Java的泛型是不支持协变和逆变的,只是能够实现协变和逆变)(协变和逆变是指宽类型和窄类型在某种情况下(如参数、泛型、返回值)替换或交换的特性。
-
简单地说,协变是用一个窄类型替换宽类型,而逆变则是用宽类型覆盖窄类型。
-
子类覆写父类返回值类型比父类型变窄,则是协变;子类覆写父类型的参数类型变宽,则是逆变。数组支持协变,泛型不支持协变
public class Suggest97 { public static void main(String[] args) { // 数组支持协变 Number[] n = new Integer[10]; // 编译不通过, 泛型不支持协变 // List<Number> ln = new ArrayList<Integer>(); List<? super Integer> li = new ArrayList<Number>(); } }
建议98:建议采用的顺序是List,List,List
- List是确定的某一个类型,编码者知道它是一个类型,只是在运行期才确定而已;
- List可以进行读写操作,List<?>是只读类型,因为编译器不知道List中容纳的是什么类型的元素,无法增加、修改,但是能删除,List也可以读写操作,只是此时已经失去了泛型存在的意义了
建议99:严格限定泛型类型采用多重界限
-
使用“&”符号连接多个泛型界限,如:<T extends Staff & Passenger>
public interface Staff { int getSalary(); }
public interface Passenger { public boolean isStanding(); }
public class Me implements Staff, Passenger { @Override public boolean isStanding() { return true; } @Override public int getSalary() { return 2000; } public static <T extends Staff & Passenger> void discount(T t) { if (t.getSalary() < 2500 && t.isStanding()) { System.out.println("恭喜你! "); } } public static void main(String[] args) { discount(new Me()); } }
建议100:数组的真实类型必须是泛型类型的子类型
-
有可能会抛出ClassCastException异常,toArray方法返回后会进行一次类型转换,Object数组转换成了String数组。由于我们无法在运行期获得泛型类型的参数,因此就需要调用者主动传入T参数类型
public class Suggest100 { public static <T> T[] toArray(List<T> list) { T[] t = (T[]) new Object[list.size()]; for (int i = 0; i < list.size(); 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); } } }
Exception in thread "main" java.lang.ClassCastException: [Ljava.lang.Object; cannot be cast to [Ljava.lang.String; at code.rocky.chapter07.Suggest100.main(Suggest100.java:18)
-
为什么是 main 方法 抛出异常, 而不是toArray 方法?
其实,是在toArray方法中进行的类型向下转换,而不是main方法中。那为什么异常会在main方法中抛出,应该在toArray方法的“T[t =(T[])new Object[list.size()]”这段代码才对呀?那是因为泛型是类型擦除的,toArray方法经过编译后与如下代码相同:
public static Object toArray(List list)
建议101:注意Class类的特殊性
-
Java处理的基本机制:先把Java源文件编译成后缀为class的字节码文件,然后再通过ClassLoader机制把这些类文件加载到内存中,最后生成实例执行。
-
Java使用一个元类(MetaClass)来描述加载到内存中的类数据,这就是Class类,它是一个描述类的类对象。
-
Class类是“类中类”,具有特殊性:
-
无构造函数,不能实例化,Class对象是在加载类时由Java虚拟机通过调用类加载器中的defineClass方法自动构建的;
-
可以描述基本类型,8个基本类型在JVM中并不是一个对象,一般存在于栈内存中,但是Class类仍然可以描述它们,例如可以使用int.class表示int类型的类对象;
-
其对象都是单例模式,一个Class的实例对象描述一个类,并且只描述一个类,反过来也成立,一个类只有一个Class实例对象。Class类是Java的反射入口,只有在获得了一个类的描述对象后才能动态地加载、调用,一般获得一个Class对象有三种途径:1、类属性方式,如String.class;2、对象的getClass方法,如new String().getClass();
3、forName方法重载,如Class.forName(“java.lang.String”)。获得了Class对象后,就可以通过getAnnotation()获得注解,通过个体Methods()获得方法,通过getConstructors()获得构造函数等
-
建议102:适时选择getDeclaredXXX和getXXX
- getMethod方法获得的是所有public访问级别的方法,包括从父类继承的方法,而getDeclaredMethod获得的是自身类的所有方法,包括公用方法、私有方法等,而且不受限于访问权限。
- Java之所以这样处理,是因为反射本意只是正常代码逻辑的一种补充,而不是让正常代码逻辑产生翻天覆地的改动,所以public的属性和方法最容易获取,私有属性和方法也可以获取,但要限定本类。
- 如果需要列出所有继承自父类的方法,需要先获得父类,然后调用getDeclaredMethods方法,之后持续递归。
建议103:反射访问属性或方法是将Accessible设置为true
- 通过反射方式执行方法时,必须在invoke之前检查Accessible属性。而Accessible属性并不是我们语法层级理解的访问权限,而是指是否更容易获得,是否进行安全检查。Accessible属性只是用来判断是否需要进行安全检查的,如果不需要则直接执行,这就可以大幅度地提升系统性能。经过测试,在大量的反射情况下,设置Accessible为true可以提升性能20倍以上
建议104:使用forName动态加载类文件
-
forName只是加载类,并不执行任何代码)(动态加载(Dynamic Loading)是指在程序运行时加载需要的类库文件,一般情况下,一个类文件在启动时或首次初始化时会被加载到内存中,而反射则可以在运行时再决定是否要加载一个类,然后在JVM中加载并初始化。
-
动态加载通常是通过Class.forName(String)实现。一个对象的生成必然会经过一下两个步骤:
- 加载到内存中生成Class的实例对象;
- 通过new关键字生成实例对象;
-
**动态加载的意义:**加载一个类即表示要初始化该类的static变量,特别是static代码块,在这里我们可以做大量的工作,比如注册自己,初始化环境等,这才是我们重点关注的逻辑
public class Test01 { static { System.out.println("Do something"); } }
public class Suggest104 { public static void main(String[] args) throws ClassNotFoundException { // 动态加载 Class.forName("code.rocky.chapter07.Test01"); } }
建议105:动态加载不适合数组
-
通过反射操作数组使用Array类,不要采用通用的反射处理API
-
如果forName要加载一个类,那它首先必须是一个类–8个基本类型排除在外,不是具体的类;
public class Suggest105 { public static void main(String[] args) throws ClassNotFoundException { String[] strs = new String[10]; Class.forName("java.lang.String[]"); } }
Exception in thread "main" java.lang.ClassNotFoundException: java/lang/String[] at java.lang.Class.forName0(Native Method) at java.lang.Class.forName(Class.java:264) at code.rocky.chapter07.Suggest105.main(Suggest105.java:6)
元素类型 编译后的类型 byte[] [B char[] [C Double[] [D Float[] [F Int[] [I Long[] [J Short[] [S Boolean [Z 引用类型(如String[]) [L 引用类型(如: [java.lang.String) -
其次,它必须具有可追索的类路径,否则会报ClassNotFoundException异常。在Java中,数组是一个非常特殊的类,虽然是一个类,但没有定义类路径。作为forName参数时会抛出ClassNotFoundException异常,原因是:数组虽然是一个类,在声明时可以定义为String[],但编译器编译后会为不同的数组类型生成不同的类,所以要想动态创建和访问数组,基本的反射是无法实现的。
// 动态创建数组 String[] str1 = (String[]) Array.newInstance(String.class, 8); // 创建一个多维数组 int[][] ints = (int[][]) Array.newInstance(int.class,2,3);
建议106:动态代理可以使代理模式更加灵活
- Java的反射框架提供了动态代理(Dynamic Proxy)机制,允许在运行期对目标类生成代理,避免重复开发。静态代理是通过代理主题角色(Proxy)和具体主题角色(Real Subject)共同实现抽象主题角色(Subject)的逻辑的,只是代理主题角色把相关的执行逻辑委托给了具体主题角色而已。动态代理需要实现InvocationHandler接口,必须要实现invoke方法,该方法完成了对真实方法的调用。
建议107:使用反射增加装饰模式的普适性
-
装饰模式(Decorator Pattern)的定义是“动态地给一个对象添加一些额外的职责。就增加功能来说,装饰模式相比于生成子类更为灵活”。比较通用的装饰模式,只需要定义被装饰的类及装饰类即可,装饰行为由动态代理实现,实现了对装饰类和被装饰类的完全解耦,提供了系统的扩展性。
public interface Animal { public void doStuff(); }
public class Rat implements Animal { @Override public void doStuff() { System.out.println("Jerry will play with Tom."); } }
public interface Feature { public void load(); }
public class FlyFeature implements Feature { @Override public void load() { System.out.println("增加一只翅膀..."); } }
public class DigFeature implements Feature { @Override public void load() { System.out.println("增加钻地能力"); } }
public 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 c1 = getClass().getClassLoader(); // 动态代理, 由 Handler 决定如何包装 Feature proxy = (Feature) Proxy.newProxyInstance(c1, clz.getInterfaces(), handler); proxy.load(); } public static void main(String[] args) { Animal Jerry = new Rat(); // 增加一只翅膀 Jerry = new DecorateAnimal(Jerry, FlyFeature.class); // 增加钻地能力 Jerry = new DecorateAnimal(Jerry, DigFeature.class); Jerry.doStuff(); } }
建议108:反射让模板方法模式更强大
- 决定使用模板方法模式时,请尝试使用反射方式实现,它会让你的程序更灵活、更强大)(模板方法模式(Template Method Pattern)的定义是:定义一个操作中的算法骨架,将一些步骤延迟到子类中,使子类不改变一个算法的结构即可重定义该算法的某些特定步骤。简单说,就是父类定义抽象模板作为骨架,其中包括基本方法(是由子类实现的方法,并且在模板方法被调用)和模板方法(实现对基本方法的调度,完成固定的逻辑),它使用了简单的继承和覆写机制。使用反射后,不需要定义任何抽象方法,只需定义一个基本方法鉴别器即可加载复合规则的基本方法
建议109:不需要太多关注反射效率
- 反射效率低是个真命题,但因为这一点而不使用它就是个假命题)(反射效率相对于正常的代码执行确实低很多(经测试,相差15倍左右),但是它是一个非常有效的运行期工具类。