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

1.Java泛型是类型擦除的

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

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

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

  • List、List、List擦除后的类型是List
  • List[]擦除后的类型为List[]
  • List、List擦除后的类型为List
  • List擦除后为List

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

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

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

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

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

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

class Foo{
    //不在初始化,由构造函数初始化
    private T t;
    private T[] tArray;
    private List list = new ArrayList();
    //构造函数初始化
    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  void read(List list){
    for(E e : list){
        //业务逻辑操作
    }
}
  • 泛型结构只支持写操作则限定下界(super关键字)
public static  void write(List list){
    list.add(1);
    list.add(1.2);
}

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

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)