泛型和反射。

    泛型可以减少强制类型的转换,可以规范集合的元素类型,还可以提高代码的安全性和可读性,正式因为有这些优点,自从Java引入泛型后,项目的编码规则上便多了一个一条:优先使用泛型。

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

  • Java的泛型是类型擦除的

        Java的泛型在编译器有效,在运行期被删除,也就是说所有的泛型参数类型在编译后都会被清除掉。

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

        1、List<String>、List<Integer>、List<T>擦出后的类型为List。

        2、List<String>[]擦除后的类型为List[]。

        3、List<? extends E>、List<? super E>擦出后的类型为List<E>。

        4、List<T extends Serializable & Cloneable>擦出后为List<Serializable>。

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

        1、避免JVM的大换血。C++的泛型生命期延续到了运行期,而Java是在编译器擦除掉的,我们想想,如果JVM也罢泛型类型延续到运行期,那么JVM就需要进行大量的重构工作了。

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

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

        1、泛型的class对象是相同的

        2、泛型数组初始化时不能声明泛型类型

        3、instanceof不允许存在泛型参数

  • 不能初始化泛型参数和数组

        一般情况下泛型类型是无法获取的,不过,在客户端调用时多传输一个T类型的class就会解决问题。

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

  • 强制声明泛型的实际类型
        无法从代码中推断出泛型类型的情况下,即可强制生命泛型类型。
  • 不同的场景使用不同的泛型通配符

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

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

        2、泛型结构只参与“写”操作则限定下届(使用super关键字)

        对于是要限定下界还是限定上界,JDK的Collections.copy方法是一个非常好的例子,他实现了把源列表中的所有元素拷贝到目标列表中对应的索引位置上。

        如果一个泛型结构即用作“读”操作又用作“写”操作,那该如何进行限定呢?不限定,使用确定的泛型类型即可,如List<E>。

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

        在编程语言的类型框架中,协变和逆变是指宽类型和窄类型在某种情况下(如参数、泛型、返回值)替换或交换的特性,简单的说,协变(返回值类型)是用一个窄类型替换宽类型,而逆变(参数)则是用宽类型覆盖窄类型。

        1、泛型不支持协变

        Java为了保证运行期的安全性,必须保证泛型参数类型是固定的,所以他不允许一个泛型参数可以同时包含两种类型,即使是父子类也不行。

        泛型不支持协变,但可以使用通配符(Wildcard)模拟协变,通配符只是在编译期有效,运行期则必须是一个确定类型。

        2、泛型不支持逆变

        注意:Java的泛型是不支持协变和逆变的,只是能够实现协变和逆变。

  • 建议采用的顺序是List<T>、List<?>、List<Object>

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

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

        2、List<T>可以进行读写操作

  • 严格限定泛型类型采用多重界限
        在java的泛型中,可以使用“&”符号关联多个上界并实现多个边界限定,而且只有上界才有此限定,下界没有多重限定的情况。
  • 数组的真实类型必须是泛型类型的子类型
        当一个泛型类(特别是泛型集合)转变为泛型数组时,泛型数组的真是类型不能是泛型类型的父类型(比如顶层类Object),只能是泛型类型的子类型(当然包括自身类型),否则就会出现类型转换异常。
  • 注意Class类的特殊性

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

        Java使用一个元类(MetaClass)来描述加载到内存中的类数据,这就是Class类,他是一个描述类的类对象,比如Dog.class文件加载到内存中后就会一个Class的实例对象描述之。因为Class类是“类中类”,也就有预示着他有很多特殊的地方:

        1、无构造函数。

        2、可以描述基本类型。

        3、其对象都是单历模式。

        Class类是java的反射入口,只有在获得了一个类的描述对象后才能动态的加载、调用,一般获得一个Class对象有三种途径:

        1、类属性方式,如String.class。

        2、对象的Class方法,如new String().getClass()。

        3、forName的方法加载,如Class.forName("java.lang.String")。

  • 适时选择getDeclaredXXX和getXXX

        getMethod方法获得的是所有public访问级别的方法,包括从父类继承的方法,而getDeclaredMethod获得是自身类的所有方法,包括公用(public)方法、私有(private)方法等,而且不受限于访问权限。

        其他的getDeclaredConstructors和getConstructors、getDeclaredFields和getFields等与此类似。Java之所以如此处理,是因为反射本意只是正常代码逻辑的一种补充,而不是让正常代码逻辑产生翻天覆地的变动,所以public的属性和方法最容易获取,私有属性和方法也可以获取,但要限定本类。

        那现在问题来了:如果需要列出所有继承自父类的方法,该如何实现呢?简单,先获得父类,然后使用getDecilaredMethods,之后持续递归即可。

  • 反射访问属性或方法时将Accessible设置为true

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

        Accessible的属性并不是我们语法层级理解的访问权限,而是指是否更容易获得,是否进行安全检查。

        动态修改一个类或方法或执行方法时都会受Java安全体系的制约,而安全的处理是非常消耗资源的(性能非常低),因此对于运行期要执行的方法或要修改的属性就提供了Accessible可选项:由开发者决定是否要逃避安全体系的检查。

        Accessible属性只是用来判断是否需要进行安全检查的,如果不需要则直接执行,这就可以大幅度的提升系统性能(当然了,由于取消安全检查,也可以运行Private方法、访问private私有属性了)。经过测试,在大量的反射情况下,设置Accessible为true可以提升性能20倍以上。

  • 使用forName动态加载类文件

        动态加载(Dynamic Loading)是指在程序运行时加载需要的类库文件,对Java程序来说,一般情况下,一个类文件在启动时或首次初始化时会被加载到内存中,而反射则可以在运行时决定是否要加载一个类,比如从Web上接收一个String参数作为类名,然后在JVM中加载并初始化,这就是动态加载,此动态加载通常是通过Class.forName(String)实现的。

        一个对象的生成必然会经过以下两个步骤:

        1、加载到内存中生成Class的实例对象。

        2、通过new关键字生成实例对象。

        import关键字产生的依赖包,JVM在启动时会自动加载所有依赖包下的类文件。

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

        对于动态加载,最经典的应用就是数据库驱动程序的加载片段。

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

        注意:forName只是加载类,并不执行任何代码。

  • 动态加载不适合数组

        如果forName要加载一个类,那他首先必须是一个类——8个基本类型排除在外,他们不是一个具体的类:其次他必须具有可追索的类路径,否则就会报ClassNotFoundException。

        注意:通过反射操作数组使用Array类,不要采用通用的反射处理API。

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

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

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

        动态代理很容易实现通用的代理类,只要在InvocationHandler的invoke方法中读取持久化数据即可实现,而且还能实现动态切入的效果,这也是AOP(Aspect Oriented Programming)编程概念。

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

        模板方法模式(Template Method Pattern)的定义是:定义一个操作中的算法骨架,将一些步骤延迟到子类中,使子类不改变一个算法的结构即可重定义该算法的某些特定步骤。简单的说,就是父类定义抽象模板作为骨架,其中包括基本方法(是由子类实现的方法,并且在模板方法被调用)和模板方法(实现对基本方法的调度,完成固定的逻辑),他使用了简单地继承和覆写机制。

        junit。

  • 不需要太多关注反射效率

        反射的效率相对于正常的代码执行确实低很多(经过测试,相差15倍左右),但一般情况下反射并不是性能的终极杀手,而代码结构混乱、可读性差则很可能会埋下性能隐患。

        对于反射效率问题,不要做任何的提前优化和预期,这基本上是杞人忧天,很少有项目是因为反射问题引起系统效率故障的(除非是拷贝工的垃圾代码),而且根据二八原则,80%的性能消耗在20%的代码上,这20%的代码才是我们关注的重点,不要单单把反射作为重点关注对象。

猜你喜欢

转载自blog.csdn.net/en_joker/article/details/80523595