泛型,是在Java SE5加入的新特性,也是Java中最重要的特性之一。
泛型的主要目的就是为了创造各式各样的容器。泛型实现了参数化类型的概念,使代码可以应用于多种类型;在使用泛型时,编译器会自动为你负责转型操作(使代码更简洁),保证类型的正确性,并将类型检查提前到编译期(使错误提前暴露)。
为什么使用泛型?
在没有泛型之前,使用Java内置容器所写出的代码一般都是这种形式。
1 | List list = new ArrayList(); |
容器存储的是Object类型元素,因此,它可以存储任何类型的元素。所以在取出元素时,我们想要将元素由Object转换为原本的类型,那就得强制类型转换。这样看来,在使用非泛型容器时,我们就必须非常小心了。因为我们可以将任何类型的类型都添加到容器中,并且,从容器中取出某个元素时,将其强制转换为任何你想要的类型才能正常使用。这些操作,编译器都不会强制对容器操作进行类型安全检查,所以,从容器获取元素后,我们对元素有意或无意地胡乱强制类型转换,在编译期是没有任何错误提示的,但运行时是否会出现错误我们就无法保证了。
就拿上面的代码来说,将第5行的改成String i = (String) list.get(1)
,我们都知道应该要强转为Integer
或int
才对,但有时候,由于种种外部原因,我们还是写出了这种代码。编译器并没有报错,但在我们运行这段代码时,就会产生一个ClassCastException
。所以,光靠我们自身是无法保证涉及非泛型容器的代码的安全性。
在Java加入泛型特性之后,使用Java内置容器所写出的代码又是另一番模样。
1 | List<String> strList = new ArrayList<String>(); |
这里我们可以很清楚看到,我们可以给容器指定存储什么类型的元素。指定元素类型后,之后的所有操作都和指定的类型绑定起来。比如这里指定为String
,获取元素直接返回的就是String
元素,能够往里面添加的元素只能是String
类型,如果添加其他类型的元素进去,编译器就会报错。
在这对比之下,我们很容易就可以体验到使用泛型可以让代码更加安全更加漂亮。
泛型基础知识
接下来要讲的都是泛型最基础的语法知识。掌握这些,在学习工作中应用泛型是基本足够了。
泛型术语、语法
以下这些概念术语我们都应该搞清楚。
类型变量
- 在定义与泛型相关的类型时所在尖括号中指定的占位符就是:
类型变量(type variable)
类型参数(type parameter)
类型形参(formal type parameter)
这些术语是对占位符同一描述的3种不同的表达方式。
类型实参
- 对已声明好的泛型进行参数化时,尖括号中的是类型实参(actual type argument)。
- 类型实参只能是引用类型(reference type)或者通配符(wildcard)。
引用类型包括:类类型、接口类型、数组类型、类型变量。
泛型类型
- 声明类或接口,并且它携带类型变量,它们就是泛型(generic)类或接口,这些类型就被统称为泛型(generic type)。
参数化类型
- 使用其它类型作为类型实参进行参数化(parameterize)后的泛型类型就是参数化类型(parameterized type)。
- 参数化泛型类型可以理解为执行泛型类型调用,这个过程就像执行函数调用,但有所区别的是执行该调用是传递类型作为实参。
举个例子:所定义好的Collection<E>
是泛型类型,当我们调用它并指定类型实参String
进行参数化后便有了Collection<String>
参数化类型。
读法
对于这种些类型List<E>
、List<Integer>
、Map<String, Integer>
,我们应该怎么称呼它们呢?
List<E>
-E
的List
,持有E
的List
,元素类型为E
的List
。List<Integer>
-Integer
的List
,持有Integer
的List
,元素类型为Integer
的List
。Map<String, Integer>
-String
和Integer
的Map
,持有String
和Integer
的Map
,键为String
值为Integer
的Map
。
容器、数据结构
使用泛型后,我们就可以随心所欲地构建通用的容器和数据结构了。
1 | public class LinkedStack<T> { |
泛型接口
既然可以将泛型应用在类类型上,那同样是可以应用在接口类型上。
1 | interface Generator<T> { |
泛型接口看起来并没有什么特别之处,使用起来和普通接口差不多。
泛型方法
泛型也可以作用于单一的方法上面,语法稍有变化。
1 | public class GenericMethod { |
打印结果:
String
Integer
Long
Float
Double
Object
GenericMethod
其中方法f
就是泛型方法,我们给它传递什么类型的参数值,它都会打印出参数值的类型名称,证明泛型是能够作用于某个方法上面独立工作的。
定义泛型方法的语法也很简单,只要将<T>
这样的泛型描述置于方法的返回值之前就可以了。
定义泛型方法时所指定的泛型形参的可见性只限定在该泛型方法,就如方法中的局部变量一样,所以,就算泛型类有同名的类型形参,也是与泛型方法中的类型形参没有任何关系。
如果使用泛型方法可以取代将整个类泛型化,那么就应该只使用泛型方法,因为这样可以使得代码更清晰简单。这是使用泛型方法的原因之一。而另一个原因,以下代码揭示。
1 | public class GenericClass<T> { |
如果把注释去掉,编译器就会报错。我们可以清楚地看到:静态方法无法访问泛型类的类型形参,同理,静态块也是如此。但是如果需要将泛型特性应用到静态方法上,则必须使用泛型方法了。
1 | public class GenericClass { |
这里,泛型特性就能应用到静态方法上了。同时引申出一个问题:如果在泛型方法内部的有泛型方法,并且类型形参同名,那会发生什么?
1 | public class GenericClass<T> { |
静态泛型方法与泛型类都有同名的类型形参T
,但编译器没有发出任何错误或警告。而非静态泛型方法,编译器却发出警告The type parameter T is hiding the type T
,表示非静态泛型方法的类型形参T把泛型类的类型形参隐藏起来。静态泛型方法本身无法访问泛型类类型形参,所以不会触发隐藏现象。所以我们现在只关注可以访问泛型类类型形参的非静态泛型方法即可。
我们都知道:在Java中并不能像C/C++里那样使用一个拥有较小作用域的变量将一个拥有较大作用域的同名变量隐藏起来。
但在Java中对于类型的处理却不是如此,请看以下代码。
1 | class A { } |
方法f
中的局部内部类A
把外部定义的类A
给隐藏了。所以,如果想要在f
内部访问外部定义的A
,只能是用类全名。
类型的隐藏虽然给我们带来的只是编译器的警告,我们可以使用@SuppressWarnings("hiding")
注解压制该警告,但是它会使得代码逻辑更加混乱,所以这种隐晦的代码我们要尽量避免,使用泛型方法时更是要如此。
- 显式的类型说明
有时候,我们希望在调用泛型方法时显式指定它所使用的具体类型。语法比较怪异,但是这种特性平时使用得并不多,所以稍微了解即可。
静态泛型方法:ClassName.<typeArgumentList>methodName(...)
非静态泛型方法:objectReference.<typeArgumentList>methodName(...)
总体来说,就是在点操作符与方法名之间插入一对尖括号,然后在里面写上具体类型实参。
1 | public class ExplicitTypeSpecification { |
类型推断
在调用泛型方法时,我们没有显式指明类型实参时,编译器会根据泛型方法所接收的参数或即将返回赋值给的目标对象类型来决定泛型方法类型形参的具体类型到底是什么。
泛型方法那一小节的第一个例子正是如此,我们传递什么参数进去泛型方法,泛型方法都能推断这个参数是什么类型。
1 | static <T> T f(T itemA, T itemB) { |
当泛型方法接受2个同一个类型形参约束的参数,那编译器就会推断出这两个参数共同的类型。1是Integer
,1.5是Double
,所以编译器推断出的共同类型就是Number
。
还有一点:类型推断只对赋值操作有效,其他时候并不起作用。
1 | public class LimitsOfInference { |
在Java SE8之前,就如前面那描述一样,类型推断对于这样的上下文并不起作用,上面这段代码会报错,但是在Java SE8环境下确实是可以正常工作的。