美文网首页Java语言
改善 Java 程序的151个建议之泛型和反射

改善 Java 程序的151个建议之泛型和反射

作者: AaronSimon | 来源:发表于2019-05-13 11:22 被阅读125次

    1.Java泛型是类型擦除的

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

    public class Foo{
    //listMethod接收数组参数并进行重载
        public void arrayMethod(String[] strArray){
        }
        public void arrayMethod(Integer[] strArray){
        }
        //listMethod接收泛型list参数并进行重载
        public void listMethod(List<String> strArray){
        }
        public void listMethod(List<Integer> strArray){
        }
    }
    

    这个程序是无法编译的,List<String>和List<Integer>在编译时查出类型后的都是List<E>,造成方法签名重复。这就是Java泛型擦除引起的问题。在编译后所有的泛型类型都会做相应的转化,转换规则如下:

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

    Java编译后的字节码中没有泛型的任何信息。比如Foo<T>类只有一份Foo.class,不管是Foo<String>还是Foo<Integer>引用的都是同一字节码。

    2.不能初始化泛型参数和数组

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

    class Foo<T>{
        private T t = new T();//1.编译不通过
        private T[] tArr = new T[5];//2.编译不通过
        private List<T> list = new ArrayList<T>();//3.编译通过
    }
    

    1,2编译不通过是因为编译期在编译时需要获得T类型,但泛型在编译期类型已经被擦除了,所以new T(),new T[5]都会报错,那为什么3编译通过呢?其实ArrayList表面是泛型的,其实已经在编译期转型为Object了,详细可以查看ArrayList的源代码。

    在某些情况下,我们确实需要泛型数组,那该怎么处理呢?代码如下:

    class Foo<T>{
        //不在初始化,由构造函数初始化
        private T t;
        private T[] tArray;
        private List<T> list = new ArrayList<T>();
        //构造函数初始化
        public Foo(){
            try{
                Class<?> tType = Class.forName("");
                t = (T)tType.newInstance();
                tArray = (T[])Array.newInstance(tType,5);
            }catch (Exception e){
                e.printStackTrace();
            }
        }
    }
    

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

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

    3.不同场景使用不同的泛型通配符

    java泛型支持通配符,可以单独使用一个?,也可以使用extends关键字表示一个类(接口)的子类型,也可以使用super关键字表示一个类(接口)的父类型,使用规则如下:

    • 泛型结构只支持读操作则限定上界(extends关键字)
    public static <E> void read(List<? extends E> list){
        for(E e : list){
            //业务逻辑操作
        }
    }
    
    • 泛型结构只支持写操作则限定下界(super关键字)
    public static <E> void write(List<? super E> list){
        list.add(1);
        list.add(1.2);
    }
    

    如果一个泛型结构既用作“读操作”,又用作“写操作”,直接使用确定的泛型类即可,如List<E>

    4.适时选择getDeclaredxxx和getxxx

    Java的Class类提供了很多的getDeclaredxxx方法和getxxx方法,例如getDeclaredMethod和getMethod成对出现,getDeclaredConstructors也是成对出现,两者的区别如下:

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

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

    Accessible的属性并不是我们语法层次理解的访问权限,而是指是否更容易获得,是否进行安全检查。我们知道,修改一个类或方法或执行方法时受Java安全体系的制约,而安全的处理是非常消耗资源的(性能非常低),因此对于运行期要执行的方法或要修改的属性就提供了Accessible可选项;由开发者决定是否要逃避安全体系的检查

    • 设置Accessible为true可以提升性能20倍以上,但可以运行private方法,访问private私有属性等

    6.动态加载不适合数组

    如果forName要加载一个类,那它首先必须是一个类(8个基本类型排除在外),它们不是一个具体的类,其次,它必须具有可追索的类路径。

    在Java中,数组是一个非常特殊的类,虽然它是一个类,但没有定义路径,例如这样的代码:

    public static void main(String[] args)throw Exception{
        String[] strs = new String[10];
        Class.forName("java.lang.String[]");
    }
    

    运行结果报错!虽然数组是一个类,但编译器编译后会为不同的数组类型产生不同的类:

    元素类型 编译后的类型
    byte[] [B
    char[] [C
    Double[] [D
    Float[] [F
    Int[] [I
    Long[] [J
    Short[] [S
    Boolean[] [Z
    引用类型(如String[]) [L 引用类型(如:[Ljava.lang.String)

    反射不能定义一个数组,可以使用Array数组反射类来动态加载,代码如下:

    String[] strs = (String[])Array.newInstance(String.class,8)
    

    相关文章

      网友评论

        本文标题:改善 Java 程序的151个建议之泛型和反射

        本文链接:https://www.haomeiwen.com/subject/njklaqtx.html