推荐阅读
Java 泛型,你了解类型擦除吗?
Java泛型指南
java 泛型详解
定义
泛型的英文是 generics,generic 的意思是通用,而翻译成中文,泛应该意为广泛,型是类型。所以泛型就是能广泛适用的类型。
但泛型还有一种较为准确的说法就是为了参数化类型(在不创建新的类型的情况下,通过泛型指定的不同类型来控制形参具体限制的类型)。也就是说在泛型使用过程中,操作的数据类型被指定为一个参数,这种参数类型可以用在类、接口和方法中,分别被称为 泛型类、泛型接口、泛型方法。
常见的使用
泛型类
泛型类型用于类的定义中,被称为泛型类。通过泛型可以完成对一组类的操作对外开放相同的接口。最典型的就是各种容器类,如:List、Set、Map。
public class Generic<T>{
private T key;
public Generic(T key) {this.key = key;}
public T getKey(){return key;}
}
定义的泛型类,就一定要传入泛型类型实参么?不一定
在使用泛型的时候如果传入泛型实参,则会根据传入的泛型实参做相应的限制,此时泛型才会起到本应起到的限制作用。如果不传入泛型类型实参的话,在泛型类中使用泛型的方法或成员变量定义的类型可以为任何的类型。如下面的例子,编译运行都是没问题的。
泛型接口
public interface Generator<T> {
public T next();
}
实现泛型接口的类 未传入泛型实参:
泛型方法
泛型类,是在实例化类的时候指明泛型的具体类型;
泛型方法,是在调用方法的时候指明泛型的具体类型 。
泛型擦除
泛型是 Java 1.5 版本才引进的概念,在这之前是没有泛型的概念的,但显然,泛型代码能够很好地和之前版本的代码很好地兼容。
这是因为,泛型信息只存在于代码编译阶段,在进入 JVM 之前,与泛型相关的信息会被擦除掉,专业术语叫做类型擦除。
我们通过 javap 命令反编译字节码:
可以看到编译后 ArrayList 的泛型已经不存在了,List 的 add 方法参数类型是 Object。
这意味着在运行时 list 和 list1 的类型都是 ArrayList,并且我们可以 通过反射向 list 对象中 add 其它类型的数据**,因为运行时 add 方法接收的参数类型是 Object。
如上图,通过反射向 list 中添加 Integer 类型的元素。
通配符和泛型边界
看一下这个例子
由于多态,第一行肯定是可以编译通过的,但同样的思路放在第二行便不使用了。 我们可以将 ArrayList
理解为「装水果的篮子」,ArrayList 理解为「装苹果的篮子」 苹果和水果存在继承关系,但「装苹果的篮子」和「装水果的篮子」并没有直接的继承关系。
但现实的编码中,我们希望泛型能够处理某一范围内的数据类型,对此 Java 引入了 通配符 的概念
通配符的作用:指定泛型中类型的范围。
通配符有三种形式:
<?>
无限定通配符<? extends T>
有上界的通配符<? super T>
有下界的通配符
无限定通配符 <?>
无限定通配符经常与容器类配合使用,它其中的 ? 其实代表的是未知类型,所以涉及到 ? 时的操作,一定与具体类型无关。
上面的代码中,方法内的参数是被 无限定通配符 修饰的 Collection 对象,它隐略地表达了一个限定, test() 这个方法内部无需关注 Collection 中的真实类型,因为它是未知的。所以,你只能调用 Collection 中与类型无关的方法,比如 size()。
简而言之,上面的代码中,只提供了 对 collection 读的能力,而限制了对其写的能力,**null 是可以写入的**。
<? extends T>
<?>
代表着类型未知,但有些场景需要对类型的描述更加精确,比如需要的类型为:类型 T 及其 子类。
此时我们可以使用 <? extends T>
例如解决本节开头的问题,便可以使用 <? extends T>
虽然 fruits 的声明编译通过了,但它仍没有写操作的能力(不过可以add null)
<? super T>
<? superT>
代表着需要的类型为 类型 T 及其 超类。
与 <? extends T>
不同,<? superT>
拥有一定程度的 写操作 的能力
注意:
- 此场景的 ArrayList<? super Fruit> 其实等价于 ArrayList
- 从 fruits 中取元素时要注意类型转换
通配符连接多个类型
使用通配符时可以使用 & 连接多个类型。
注意:
- Class 必须放在第一位,即 Apple 必须放在最前面
- 由于 Java 仅支持单继承,意味着 & 后面连接的均是接口
泛型转译
泛型擦除一节我们提到,编译后的字节码文件中已经没有泛型相关的信息了,那么泛型是如何处理的呢?直接用 Object 替代吗?
这里我们做一个实验:
public static void main(String[] args) {
Test<String> test = new Test<>();
try {
Field clazz = test.getClass().getDeclaredField("value");
clazz.setAccessible(true);
clazz.set(test, "abc");
System.out.println(clazz.getType());
} catch (NoSuchFieldException | IllegalAccessException e) {
e.printStackTrace();
}
System.out.println(test.value);
}
Test 接受一个泛型 T,其内部 value 属性的类型正是 T
public class Test<T> {
T value;
}
此时的打印结果为:
class java.lang.Object
abc
由此可见泛型 T 被转译成 Object
但如果为 泛型设定了上限
public class Test<T extends String> {
T value;
}
打印的结果为:
class java.lang.String
abc
在泛型类被类型擦除的时候,之前泛型类中的类型参数部分如果没有指定上限,如 **<T>
则会被转译成普通的 Object 类型,如果指定了上限如 <T extends String>
**则类型参数就被替换成类型上限。
**
常见问题
向上转型
List<String> list = new ArrayList<>();
List list2 = list;
list2.add("1");
list2.add(1);
String res1 = list.get(0);
String res2 = list.get(1);
这段代码是编译期报错还是运行期报错?如果报错是哪一行?
第四行可以通过,因为支持向上转型。第六行会在运行时报错,java.lang.Integer cannot be cast to java.lang.String
如果使用 list2 获取,则会在编译期报错
泛型类和泛型方法的限制域
m1 方法参数 1 和 2 的类型均受到 泛型类 的限制 Generic
generic = new Generic<>(); 限制了 T 为 String,E 为 Boolean
m2 方法参数 1 类型 受到 泛型方法 的限制,参数 2 受 泛型类 限制 generic.m2(“”, Boolean.TRUE); 参数 1 受左侧尖括号中的 String 限制,参数 2 受泛型类的限制 泛型方法调用时也可不使用尖括号,由编译器自动推断类型,以下声明都可以编译通过 generic.m2(“”, Boolean.TRUE);
generic.m2(Boolean.TRUE, Boolean.TRUE);
静态方法
普通静态方法访问不到类的泛型,当然静态泛型方法也是不行的,但可以访问调用者传入的泛型,如上图箭头
泛型不可传入基本数据类型
为什么基本数据类型不可用,需要使用引用数据类型?因为泛型擦除时泛型被转译为 Object,只有引用数据类型才是 Object 的子类
泛型数组
在java中是 不能创建一个确切的泛型类型的数组 的。
List<String>[] lsa = new List<String>[10]; // Not really allowed.
Object o = lsa;
Object[] oa = (Object[]) o;
List<Integer> li = new ArrayList<Integer>();
li.add(new Integer(3));
oa[1] = li; // Unsound, but passes run time store check
String s = lsa[1].get(0); // Run-time error: ClassCastException.
这种情况下,由于JVM泛型的擦除机制,在运行时JVM是不知道泛型信息的,所以可以给oa[1]赋上一个ArrayList而不会出现异常,但是在取出数据的时候却要做一次类型转换,所以就会出现ClassCastException,如果可以进行泛型数组的声明,上面说的这种情况在编译期将不会出现任何的警告和错误,只有在运行时才会出错。
而对泛型数组的声明进行限制,对于这样的情况,可以在编译期提示代码有类型安全问题,比没有任何提示要强很多。
下面采用通配符的方式是被允许的:数组的类型不可以是类型变量,除非是采用通配符的方式,因为对于通配符的方式,最后取出数据是要做显式的类型转换的。
List<?>[] lsa = new List<?>[10]; // OK, array of unbounded wildcard type.
Object o = lsa;
Object[] oa = (Object[]) o;
List<Integer> li = new ArrayList<Integer>();
li.add(new Integer(3));
oa[1] = li; // Correct.
Integer i = (Integer) lsa[1].get(0); // OK