什么是泛型
Java 泛型(generics)是 JDK5 中引入的一个新特性, 泛型提供了编译时类型安全检测机制,该机制允许程序员在编译时检测到非法的类型。泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数。(PHP就不需要泛型,哈哈)
下面我们看一道关于Java泛型的经典测试题:
1 | List<String> list1 = new ArrayList<String>(); |
上面代码最终结果输出的是什么?不了解泛型的和很熟悉泛型的同学应该能够答出来,而对泛型有所了解,但是了解不深入的同学可能会答错。
正确答案是 true。
上面提到了泛型的本质是类型参数化,如何解释类型参数化呢?
1 | public class Cache { |
这样的 Cache 是能够存取任何类型的值,但是我们在使用的时候会有点麻烦。我们要获取真正的类型,就要进行类型的强制转换了。
1 | Cache cache = new Cache(); |
这样的编程真的是太麻烦了,万一忘记了存放变量的类型,程序在运行的时候就会报类型转换错误。于是在JDK5 中引用的泛型给我们带来了另一番编程体验。
1 | public class Cache<T> { |
这就是泛型,它将 value 这个属性的类型也参数化了,这就是所谓的参数化类型。再看它的使用方法。
1 | Cache<String> cache1 = new Cache<String>(); |
最显而易见的好处就是它不再需要对取出来的结果进行强制转换了。但,还有另外一点不同。泛型除了可以将类型参数化外,而参数一旦确定好,如果类似不匹配,编译器就不通过。
综合上面信息,我们可以得到下面的结论:
- 与普通的 Object 代替一切类型这样简单粗暴而言,泛型使得数据的类别可以像参数一样由外部传递进来。它提供了一种扩展能力。它更符合面向抽象开发的软件编程宗旨。
- 当具体的类型确定后,泛型又提供了一种类型检测的机制,只有相匹配的数据才能正常的赋值,否则编译器就不通过。所以说,它是一种类型安全检测机制,一定程度上提高了软件的安全性防止出现低级的失误。
- 泛型提高了程序代码的可读性,不必要等到运行的时候才去强制转换,在定义或者实例化阶段,因为 Cache
这个类型显化的效果,程序员能够一目了然猜测出代码要操作的数据类型。
泛型的定义和使用
泛型按照使用情况可以分为 3 种。
- 泛型类
- 泛型方法
- 泛型接口
泛型类
那如何定义泛型类呢?
1 | public class Test<T> { |
尖括号 <> 中的 T 被称作是类型参数,用于指代任何类型。事实上,T 只是一种习惯性写法,如果你愿意。你可以这样写。
1 | public class Test<TMD> { |
但出于规范的目的,Java 还是建议我们用单个大写字母来代表类型参数。常见的如:
- T 代表一般的任何类。
- E 代表Element 的意思,或者是Exception 异常的意思
- K 代表 Key 的意思
- V 代表 Value 的意思,通常与 K 一起配合使用
- S 代表 Subtype 的意思
如果一个类被
1 | Test<String> test1 = new Test<>(); |
只要在对泛型类创建实例的时候,在尖括号中赋值相应的类型便是。T 就会被替换成对应的类型,如 String 或者是 Integer。你可以相像一下,当一个泛型类被创建时,内部自动扩展成下面的代码。
1 | public class Test<String> { |
当然,泛型类不是只能接受一个类型参数,它还可以这样接受多个类型参数。
1 | public class MultiType <E,T>{ |
泛型方法
1 | public class Test1 { |
泛型方法与泛型类稍有不同的地方是,类型参数也就是尖括号那一部分是写在返回值前面的。
当然,声明的类型参数,其实也是可以当作返回值的类型的。
1 | public <T> T testMethod1(T t){ |
泛型类与泛型方法的共存现象:
1 | public class Test<T>{ |
上面代码中,Test1
所以,针对上面的代码,我们可以这样编写测试代码。
1 | Test1<String> t = new Test1(); |
泛型类的实际类型参数是 String,而传递给泛型方法的类型参数是 Integer,两者不想干。
但是,为了避免混淆,如果在一个泛型类中存在泛型方法,那么两者的类型参数最好不要同名。比如,Test
1 | public class Test1<T>{ |
泛型接口
泛型接口定义:
1 | public interface Iterable<T> { |
通配符 ?
除了用
1 | class Base{ |
上面代码显示,Base 是 Sub 的父类,它们之间是继承关系,所以 Sub 的实例可以给一个 Base 引用赋值,那么
1 | List<Sub> lsub = new ArrayList<Sub>(); |
最后一行代码成立吗?编译会通过吗?
答案是通过不了编译的。
编译器不会让它通过的。Sub 是 Base 的子类,不代表 List 和 List
但是,在现实编码中,确实有这样的需求,希望泛型能够处理某一范围内的数据类型,比如某个类和它的子类,对此 Java 引入了通配符这个概念。
所以,通配符的出现是为了指定泛型中的类型范围。
通配符有 3 种形式:
- <?> 被称作无限定的通配符
- <? extends T> 被称作有上限的通配符
- <? super T> 被称作有下限的通配符
无限定通配符
1 | public void testWildCards(Collection<?> collection){ |
上面的代码中,方法内的参数是被无限定通配符修饰的 Collection 对象,它隐略地表达了一个意图或者可以说是限定,那就是 testWidlCards() 这个方法内部无需关注 Collection 中的真实类型,因为它是未知的。所以,你只能调用 Collection 中与类型无关的方法。
1 | public class TestWildCards{ |
我们可以看到,当 <?> 存在时,Collection 对象丧失了 add() 方法的功能,编译器不通过。
我们再看下面的代码:
1 | List<?> wildlist = new ArrayList<String>(); |
有人说,<?> 提供了只读的功能,也就是它删减了增加具体类型元素的能力,只保留与具体类型无关的功能。它不管装载在这个容器内的元素是什么类型,它只关心元素的数量、容器是否为空?我想这种需求还是很常见的吧。
有同学可能会想,<?> 既然作用这么渺小,那么为什么还要引用它呢?
个人认为,提高了代码的可读性,程序员看到这段代码时,就能够迅速对此建立极简洁的印象,能够快速推断源码作者的意图。
<? extends T>
<?> 代表着类型未知,但是我们的确需要对于类型的描述再精确一点,我们希望在一个范围内确定类别,比如类型 A 及 类型 A 的子类都可以。
1 | public void testSub(Collection<? extends Base> para){ |
上面代码中,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){ |
通配符与类型参数的区别
一般而言,通配符能干的事情都可以用类型参数替换。比如:
1 | public void testWildCards(Collection<?> collection){ |
可以被
1 | public <T> void test(Collection<T> collection){ |
取代。值得注意的是,如果用泛型方法来取代通配符,那么上面代码中 collection 是能够进行写操作的。只不过要进行强制转换。
1 | public <T> void test(Collection<T> collection){ |
需要特别注意的是,类型参数适用于参数之间的类别依赖关系,举例说明:
1 | public class Test2 <T,E extends T>{ |
E 类型是 T 类型的子类,显然这种情况类型参数更适合。
有一种情况是,通配符和类型参数一起使用。
1 | public <T> void test(T t,Collection<? extends T> collection){ |
如果一个方法的返回类型依赖于参数的类型,那么通配符也无能为力。
1 | public T test1(T t){ |
类型擦除
泛型是 Java 1.5 版本才引进的概念,在这之前是没有泛型的概念的,但显然,泛型代码能够很好地和之前版本的代码很好地兼容。
这是因为,泛型信息只存在于代码编译阶段,在进入 JVM 之前,与泛型相关的信息会被擦除掉,专业术语叫做类型擦除。
通俗地讲,泛型类和普通类在 java 虚拟机内是没有什么特别的地方。回顾文章开始时的那段代码:
1 | List<String> list1 = new ArrayList<String>(); |
打印的结果为 true 是因为 List
泛型信息被擦除了。
可能同学会问,那么类型 String 和 Integer 怎么办?
答案是泛型转译。
1 | public class Erasure <T>{ |
Erasure 是一个泛型类,我们查看它在运行时的状态信息可以通过反射。
1 | Erasure<String> erasure = new Erasure<String>("hello"); |
Class 的类型仍然是 Erasure 并不是 Erasure
1 | Field[] fs = eclz.getDeclaredFields(); |
那我们可不可以说,泛型类被类型擦除后,相应的类型就被替换成 Object 类型呢?
这种说法,不完全正确。 我们更改一下代码。
1 | public class Erasure <T extends String>{ |
我们现在可以下结论了,在泛型类被类型擦除的时候,之前泛型类中的类型参数部分如果没有指定上限,如
所以,在反射中:
1 | public class Erasure <T>{ |
add() 这个方法对应的 Method 的签名应该是 Object.class。
1 | Erasure<String> erasure = new Erasure<String>("hello"); |
也就是说,如果你要在反射中找到 add 对应的 Method,你应该调用 getDeclaredMethod(“add”,Object.class) 否则程序会报错,提示没有这么一个方法,原因就是类型擦除的时候,T 被替换成 Object 类型了。
泛型中值得注意的地方
泛型类或者泛型方法中,不接受 8 种基本数据类型。
1 | // 这种是错误的使用方式 |
对泛型方法的困惑
1 | public <T> T test(T t){ |
有的同学可能对于连续的两个 T 感到困惑,其实
你可以相像一下,如果 test() 这样被调用:
1 | test("123"); |
Java 不能创建具体类型的泛型数组
这句话可能难以理解,用代码来说明:
1 | List<Integer>[] li2 = new ArrayList<Integer>[]; |
这两行代码是无法在编译器中编译通过的。原因还是类型擦除带来的影响。
List
1 | List<?>[] li3 = new ArrayList<?>[10]; |
借助于无限定通配符却可以,前面讲过 ? 代表未知类型,所以它涉及的操作都基本上与类型无关,因此 jvm 不需要针对它对类型作判断,因此它能编译通过,但是,只提供了数组中的元素因为通配符原因,它只能读,不能写。比如,上面的 v 这个局部变量,它只能进行 get() 操作,不能进行 add() 操作,这个在前面通配符的内容小节中已经讲过。
泛型,并不神奇
我们可以看到,泛型其实并没有什么神奇的地方,泛型代码能做的非泛型代码也能做。
而类型擦除,是泛型能够与之前的 java 版本代码兼容共存的原因。
可量也正因为类型擦除导致了一些隐患与局限。
但,我还是要建议大家使用泛型,如官方文档所说的,如果可以使用泛型的地方,尽量使用泛型。
毕竟它抽离了数据类型与代码逻辑,本意是提高程序代码的简洁性和可读性,并提供可能的编译时类型转换安全检测功能。
类型擦除不是泛型的全部,但是它却能很好地检测我们对于泛型这个概念的理解程度。
来源:blog.csdn.net/briblue/article/details/76736356