Java 泛型(generics)
Java 泛型(generics)是 JDK 5 中引入的一个新特性, 就是为了解决类型转换的问题,泛型提供了编译时类型安全检测机制,并且所有的强制转换都是自动和隐式的,在运行时丢弃了一些类型实参的信息,对于内存占用也会减少很多。
例如:
List<Float>、List<String>、List<Student>在JVM运行时Float、String、Student都被替换成Object类型,如果是泛型定义是List<T extends Student>那么运行时T被替换成Student类型,具体可以通过反射Erasure类可看出。
泛型中通配符
1,常用的 T,E,K,V,?
- ?表示不确定的 java 类型
对于不确定或者不关心实际要操作的类型,可以使用无限制通配符(尖括号里一个问号,即 ),表示可以持有任何类型。
int countLegs (List<? extends Animal > animals ) {}
- T (type) 表示具体的一个java类型
- K V (key value) 分别代表java键值中的Key Value
- E (element) 代表Element
类型擦除
定义:使用泛型的时候加上类型参数,在编译器编译的时候会去掉,这个过程成为类型擦除。
如在代码中定义List<Object>和List<String>等类型,在编译后都会变成List,JVM看到的只是List,而由泛型附加的类型信息对JVM是看不到
public class Test {
public static void main(String[] args) {
ArrayList<String> list1 = new ArrayList<String>();
list1.add("abc");
ArrayList<Integer> list2 = new ArrayList<Integer>();
list2.add(123);
System.out.println(list1.getClass() == list2.getClass());
}
}
打印结果为true,说明泛型类型String和Integer都被擦除掉了,只剩下原始类型。
//通过反射添加其它类型元素
public class Test {
public static void main(String[] args) throws Exception {
ArrayList<Integer> list = new ArrayList<Integer>();
list.add(1); //这样调用 add 方法只能存储整形,因为泛型类型的实例为 Integer
list.getClass().getMethod("add", Object.class).invoke(list, "David");
for (int i = 0; i < list.size(); i++) {
System.out.println(list.get(i));
}
}
}
我们利用反射调用add()方法的时候,却可以存储字符串,这说明了Integer泛型实例在编译之后被擦除掉了,只保留了原始类型。
原始类型 就是擦除去了泛型信息,最后在字节码中的类型变量的真正类型。
我们也可以明白ArrayList<Integer>被擦除类型后,原始类型也变为Object,所以通过反射我们就可以存储字符串了。
比如ArrayList中,如果不指定泛型,那么这个ArrayList可以存储任意的对象。
public static void main(String[] args) {
ArrayList list = new ArrayList();
list.add(1);
list.add("David");
list.add(new Date());
}
看下面这个例子:
public class Test {
public static void main(String[] args) {
//list1引用能完成泛型类型的安全检查
ArrayList<String> list1 = new ArrayList();
list1.add("1"); //编译通过
list1.add(1); //编译错误
String str1 = list1.get(0); //返回类型就是String
//list2没有使用泛型,没有做类型安全检查
ArrayList list2 = new ArrayList<String>();
list2.add("1"); //编译通过
list2.add(1); //编译通过
Object object = list2.get(0); //返回类型就是Object
new ArrayList<String>().add("11"); //编译通过
new ArrayList<String>().add(22); //编译错误
String str2 = new ArrayList<String>().get(0); //返回类型就是String
}
}
泛型类中的泛型参数的实例化是在定义对象的时候指定的,而静态变量和静态方法不需要使用对象来调用。对象都没有创建,如何确定这个泛型参数是何种类型,所以当然是错误的。
public class Test2<T> {
public static T one; //编译错误
public static T show (T one) { //编译错误
return null;
}
}
//但是要注意区分下面的一种情况:
//这是一个泛型方法,在泛型方法中使用的T是自己在方法中定义的 T,而不是泛型类中的T。
public class Test2<T> {
public static <T >T show (T one) { //这是正确的
return null;
}
}
泛型类型变量不能是基本数据类型
没有ArrayList<double>,只有ArrayList<Double>。因为当类型擦除后,ArrayList的原始类型变为Object,但是Object类型不能存储double值,只能引用Double的值。
Java 协变和逆变
解决的就是这类问题:
Integer 是 Number 的子类型,问你 List<Integer> 是不是 List<Number> 的子类型?
数组支持协变
// objects 这个句柄只能存储 Integer
Object objects[] = new Integer[20];
objects[0] = "David";
Exception in thread "main" java.lang.ArrayStoreException: java.lang.String
at Main.main(Main.java:13)
集合不支持协变,编译不通过
// smell code
ArrayList<Object> list = new ArrayList<String>();
// 协变
ArrayList< ? extends Number> list= new ArrayList<Integer>();
// 逆变
ArrayList< ? super Integer> list2 = new ArrayList<Number>();
list.add(1); //error
变型的种类具体分为三种:协变型 & 逆变型 & 不变型
-
协变型(covariant): 子类型关系被保留
-
逆变型(contravariant): 子类型关系被翻转
-
不变型(invariant): 子类型关系被消除
<? extends T> 上界通配符
代表的是 T 及其子类
要想类型参数支持协变,需要使用上界通配,但是这会引入一个编译时限制:就是只能访问不能修改(非严格)
List<? extends Number> l1;
List<Integer> l2 = new ArrayList<>();
l1 = l2; // OK
<? super T> 下界通配符
表示只能为 T 及其父类
要想类型参数支持逆变,需要使用下界通配符,同样,这也会引入一个编译时限制,只能修改不能访问(非严格)
// ArrayList.java
public E get(int index) {
...
}
Integer i = l1.get(0); // compiler error
<?> 无界通配符
List<?> l1;
List<Integer> l2 = new ArrayList<>();
l1 = l2; // OK
泛型代码的设计,应遵循PECS原则(Producer extends Consumer super):
如果只需要获取元素,使用 <? extends T>
如果只需要修改,使用<? super T>
举例:
// Collections.java
public static void copy(List<? extends T> src, List<? super T> dest) {
}
Kotlin 协变和逆变
-
in
关键字对应? super
-
out
关键字对应? extends
reified
使用类型实参的确切类型代替类型实参,必须搭配
inline
使用,注意无法从 Java 代码里调用带实化类型参数的内联函数。
举个例子:
inline fun <reified T> Context.startActivity() {
Intent(this, T::class.java).apply {
startActivity(this)
}
}
// how to use
context.startActivity<MainActivity>()
网友评论