泛型是 java 1.5中添加的。在没有泛型之前,从集合中读取每一个对象都必须进行转换,如果有人不小型插入了错误的类型,那么就会在运行的时候出现类型转换异常。
泛型的作用就是告诉每个集合可以接受哪些类型,在编译阶段就可以告知是否插入了错误的对象
泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数。
泛型类的优点:
- 在使用时,不用强转转换
- 能够在编译阶段识别到 插入了错误的对象类型
泛型类
1 | class Point<T>{// 此处可以随便写标识符号 |
泛型类在构造时,需要在类名后添加上<String>
,即一对尖括号,中间写上要传入的类型。之后的使用方法就和普通类的使用差才不多了。
泛型接口
1 | interface Info<T>{ // 在接口上定义泛型 |
泛型方法
泛型方法与以往方法的唯一不同点就是
在返回值前加上
<T>
来表示泛型变量。
1 | public class StaticFans { |
1 | class Test{ |
方法一,可以像普通方法一样,直接传值,任何值都可以(但必须是派生自Object类的类型,比如String,Integer等),函数会在内部根据传进去的参数来识别当前T的类别。但尽量不要使用这种隐式的传递方式,代码不利于阅读和维护。因为从外观根本看不出来你调用的是一个泛型函数。
方法二,与方法一不同的地方在于,在调用方法前加了一个<String>
来指定传给<T>
的值,如果加了这个<String>
来指定参数的值的话,那StaticMethod()函数里所有用到的T类型也就是强制指定了是String类型。这是我们建议使用的方式。
返回值为泛型:
1 | public static <T> List<T> parseArray(String response,Class<T> object){ |
上面的代码出现了 Class<T>
。Class
通配符 ?
除了用 <T>
表示泛型外,还有 <?>
这种形式。`` 被称为通配符。
<T>
的局限性
1 | class Base{ |
上面代码显示,Base 是 Sub 的父类,它们之间是继承关系,所以 Sub 的实例可以给一个 Base 引用赋值,那么
1 | List<Sub> lsub = new ArrayList<>(); |
最后一行代码成立吗?编译会通过吗?
答案是否定的。
编译器不会让它通过的。Sub 是 Base 的子类,不代表 List<Sub>
和 List<Base>
有继承关系。
但是,在现实编码中,确实有这样的需求,希望泛型能够处理某一范围内的数据类型,比如某个类和它的子类,对此 Java 引入了通配符这个概念。
所以,通配符的出现是为了指定泛型中的类型范围。
通配符有 3 种形式。
<?>
被称作无限定的通配符。<? extends T>
被称作有上限的通配符。<? super T>
被称作有下限的通配符。
无限定通配符
1 | public void testWildCards(Collection<?> collection){ |
上面的代码中,方法内的参数是被无限定通配符修饰的 Collection 对象,它隐略地表达了一个意图或者可以说是限定,那就是 testWildCards() 这个方法内部无需关注 Collection 中的真实类型,因为它是未知的。
所以,你只能调用 Collection 中与类型无关的方法。
1 | class test{ |
我们可以看到,当 <?>
存在时,Collection 对象丧失了 add() 方法的功能,编译器不通过。
我们再看代码。
1 | List<?> wildlist = new ArrayList<String>(); |
有人说,<?>
提供了只读的功能,也就是它删减了增加具体类型元素的能力,只保留与具体类型无关的功能。它不管装载在这个容器内的元素是什么类型,它只关心元素的数量、容器是否为空?我想这种需求还是很常见的吧。
有同学可能会想,<?>
既然作用这么渺小,那么为什么还要引用它呢?
个人认为,提高了代码的可读性,程序员看到这段代码时,就能够迅速对此建立极简洁的印象,能够快速推断源码作者的意图。
<? extends T>
<?>
代表着类型未知,但是我们的确需要对于类型的描述再精确一点,我们希望在一个范围内确定类别,比如类型 A 及 类型 A 的子类都可以。
1 |
|
上面代码中,para 这个 Collection 接受 Base 及 Base 的子类的类型。
但是,它仍然丧失了写操作的能力。也就是说
1 | para.add(new Sub()); // 编译不通过 |
仍然编译不通过。
没有关系,我们不知道具体类型,但是我们至少清楚了类型的范围。
<? super T>
这个和 <? extends T>
相对应,代表 T 及 T 的超类。
1 | public void testSuper(Collection<? super Sub> para){ |
<? super T>
神奇的地方在于,它拥有一定程度的写操作的能力。
1 | public void testSuper(Collection<? super Sub> para){ |
通配符与类型参数的区别
一般而言,通配符能干的事情都可以用类型参数替换。
比如
public void testWildCards(Collection<?> collection){}
可以被
public
void test(Collection collection){}
取代。
值得注意的是,如果用泛型方法来取代通配符,那么上面代码中 collection 是能够进行写操作的。只不过要进行强制转换。
1 |
|
类型擦除
泛型是 Java 1.5 版本才引进的概念,在这之前是没有泛型的概念的,但显然,泛型代码能够很好地和之前版本的代码很好地兼容。
这是因为,泛型信息只存在于代码编译阶段,在进入 JVM 之前,与泛型相关的信息会被擦除掉,专业术语叫做类型擦除。
通俗地讲,泛型类和普通类在 java 虚拟机内是没有什么特别的地方。回顾文章开始时的那段代码
1 | List<String> l1 = new ArrayList<String>(); |
打印的结果为 true 是因为 List<String>
和 List<Integer>
在 jvm 中的 Class 都是 List.class。
泛型信息被擦除了。
泛型类运行状态的情况
1 | public class Erasure <T>{ |
Erasure 是一个泛型类,我们查看它在运行时的状态信息可以通过反射。
1 | Erasure<String> erasure = new Erasure<String>("hello"); |
打印的结果是
erasure class is:com.frank.test.Erasure
Class 的类型仍然是 Erasure 并不是 Erasure
1 | Field[] fs = eclz.getDeclaredFields(); |
打印结果是
Field name object type:java.lang.Object
那我们可不可以说,泛型类被类型擦除后,相应的类型就被替换成 Object 类型呢?
这种说法,不完全正确。
我们更改一下代码。
1 |
|
现在再看测试结果:
Field name object type:java.lang.String
我们现在可以下结论了,在泛型类被类型擦除的时候,之前泛型类中的类型参数部分如果没有指定上限,如 <T>
则会被转译成普通的 Object 类型,如果指定了上限如 <T extends String>
则类型参数就被替换成类型上限。
泛型中值得注意的地方
泛型类或者泛型方法中,不接受 8 种基本数据类型。
所以,你没有办法进行这样的编码。
1 | List<int> li = new ArrayList<>(); |
需要使用它们对应的包装类。
1 | List<Integer> li = new ArrayList<>(); |