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)