该系列博文会告诉你如何从入门到进阶,一步步地学习Java基础知识,并上手进行实战,接着了解每个Java知识点背后的实现原理,更完整地了解整个Java技术体系,形成自己的知识框架。
一、泛型概述
1、定义:
所谓泛型,就是允许在定义类、接口、方法时使用类型形参,这个类型形参(或叫泛型)将在声明变量、创建对象、调用方法时动态地指定(即传入实际的类型参数,也可称为类型实参)。Java5改写了集合框架中的全部接口和类,为这些接口、类增加了泛型支持,从而可以在声明集合变量、创建集合对象时传入类型实参。
2、泛型初体验:一个被举了无数次的栗子
1
2
3
4
5
6
7
8
|
List arrayList = new ArrayList();
arrayList.add( "aaaa" );
arrayList.add( 100 );
for ( int i = 0 ; i< arrayList.size();i++){
String item = (String)arrayList.get(i);
Log.d( "泛型测试" , "item = " + item);
}
|
运行上述代码,我们可以在控制台看到这样的错误信息:
1
|
java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String
|
ArrayList可以存放任意类型,例子中添加了一个String类型,添加了一个Integer类型,再使用时都以String的方式使用,因此程序崩溃了。为了解决类似这样的问题(在编译阶段就可以解决),泛型应运而生。
3、泛型的特性:
先思考如下一段代码:
1
2
3
|
List<String> sList= new ArrayList<String>();
List<Integer> iList= new ArrayList<Integer>();
System.out.println(sList.getClass()==iList.getClass());
|
先不要看结果,自己思考一下。
结果:
1
|
true
|
我们看到输出的结果为true,通过上面的例子可以证明,在编译之后程序会采取去泛型化的措施。也就是说Java中的泛型,只在编译阶段有效。在编译过程中,正确检验泛型结果后,会将泛型的相关信息擦出,并且在对象进入和离开方法的边界处添加类型检查和类型转换的方法。也就是说,泛型信息不会进入到运行时阶段。(泛型的这一特性在下述文字中会有详解介绍)
小结:泛型类型在逻辑上看以看成是多个不同的类型,实际上都是相同的基本类型。
二、泛型的使用
泛型有三种使用方式,分别为:泛型类、泛型接口、泛型方法
1、泛型类:
泛型类型用于类的定义中,被称为泛型类。通过泛型可以完成对一组类的操作对外开放相同的接口。最典型的就是各种容器类,如:List、Set、Map。
泛型的基本写法:
1
2
3
4
5
6
|
class 类名称 <泛型标识:可以随便写任意标识号,标识指定的泛型的类型>{
private 泛型标识 /*(成员变量类型)*/ var;
.....
}
}
|
看不懂?看不懂就对了,下面我们来看一个栗子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
|
public class Apple<T> {
//使用T类型定义变量
private T info;
public Apple() {}
//下面方法使用T类型定义构造器
public Apple(T info){
this .info=info;
}
public T getInfo() {
return info;
}
public void setInfo(T info) {
this .info = info;
}
public static void main(String[] args) {
//由于传给T形参的是String,所以构造器参数只能是String
Apple<String> apple= new Apple<String>( "苹果" );
System.out.println(apple.getInfo());
//由于传给T形参的是Double,所以构造器参数只能是Double
Apple<Double> apple2= new Apple<Double>( 5.56 );
System.out.println(apple2.getInfo());
}
}
|
这里的T可以写成任意符合,常用的有如下几个符合:
- ?:表示不确定的 java 类型
- T (type): 表示具体的一个java类型
- K V (key value): 分别代表java键值中的Key Value
- E (element) :代表Element
2、泛型接口:
泛型接口与泛型类的定义及使用基本相同。下面是Java5改写后的List接口,Map接口的代码片段
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
public interface List<E>{
//在该接口中,E可以作为类型使用
//下面方法可以使用E作为参数类型
void add(E x);
Iterator<E> iterator();
}
//定义该接口时指定了两个泛型形参,其参数名为K,V
public interface Map<K,V>{
//在该接口中K,V完全可以作为类型使用
Set<K> keySet()
V put(K key,V value);
}
|
下面我们来看一个泛型案例:
定义一个泛型接口:
1
2
3
4
|
//定义一个泛型接口
public interface Generator<T> {
public T next();
}
|
现在有一个类实现了这个泛型接口,类的代码如下:
1
2
3
4
5
6
|
class FruitGenerator<T> implements Generator<T>{
@Override
public T next() {
return null ;
}
}
|
我们看到了这个类中也使用了泛型,并未传入实际的参数
未传入泛型实参时,与泛型类的定义相同,在声明类的时候,需将泛型的声明也一起加到类中,即:class FruitGenerator
如果不声明泛型,如:class FruitGenerator implements Generator
1
2
3
4
5
6
7
8
9
10
|
public class FruitGenerator implements Generator<String> {
private String[] fruits = new String[]{ "Apple" , "Banana" , "Pear" };
@Override
public String next() {
Random rand = new Random();
return fruits[rand.nextInt( 3 )];
}
}
|
这段代码也是实现了Generator接口,不同的是传入了实际的类型String
传入泛型实参时:定义一个生产器实现这个接口,虽然我们只创建了一个泛型接口Generator
3、泛型通配符:
为什么要使用泛型通配符:
正如前面讲的,当使用一个泛型类时(包括声明变量和创建对象两种情况),都应该为这个泛型类传入一个类型实参。如果没有传入类型实际参数,编译器就会提出泛型警告。假设现在需要定义一个方法,该方法里有一个集合形参,集合形参的元素类型是不确定的,那应该怎样定义呢?
考虑如下代码:
1
2
3
4
5
|
public void test(List c) {
for ( int i= 0 ;i<c.size;i++) {
System.out.println(c.get(i));
}
}
|
上面程序当然没有问题:这是一段最普通的遍历List集合的代码。问题是上面程序中List是一个有泛型声明的接口,此处使用List 接口时没有传入实际类型参数,这将引起泛型警告。为此,考虑为List 接口传入实际的类型参数——因为List集合里的元素类型是不确定的
泛型通配符的使用:
为了表示各种泛型List的父类,可以使用类型通配符,类型通配符是一个问号(?),将一个问号作为类型实参传给List集合,写作:List>(意思是元素类型未知的List)。这个问号(?)被称为通配符,它的元素类型可以匹配任何类型。可以将上面方法改写为如下形式:
1
2
3
4
5
|
public void test(List<?> c) {
for ( int i= 0 ;i<c.size;i++) {
System.out.println(c.get(i));
}
}
|
这样就不会出现警告了,但这种带通配符的List仅表示它是各种泛型List的父类,并不能将其他元素加入到其中,例如将String放入其中
List> c=new ArrayList
因为程序无法确定c集合中元素的类型,所以不能向其中添加对象。根据前面的List
设置类型通配符的上限:
现在想实现一个简单的绘图程序,代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
public abstract class Shape{
public abstract void draw(Canvas c);
}
//定义Shape的子类Circle
public class Circle extends Shape{
//实现画图方法,以打印字符串来模拟画图方法实现
public void draw(Canvas c)
{
System.out.println( "在画布" +c+ "上画一个圆" );
}
}
//定义Shape的子类Rectangle
public class Rectangle extends Shape{
//实现画图方法,以打印字符串来模拟画图方法实现
public void draw(Canvas c)
{
System.out.print1n( "把一个矩形画在画布" +c+ "上" );
}
}
|
上面定义了三个形状类,其中Shape是一个抽象父类,该抽象父类有两个子类:Circle和Rectangle。接下来定义一个Canvas类,该画布类可以画数量不等的形状(Shape子类的对象),那应该如何定义这个Canvas类呢?考虑如下的Canvas实现类。
1
2
3
4
5
|
//同时在画布上绘制多个形状
public void drawAll(List<Shape>shapes) {
for (Shape s:shapes)
s.draw( this );
}
|
注意上面的drawAll()方法的形参类型是List
1
2
3
4
|
List<Circle> circleList= new ArrayList<Circle>();
Canvas c= new Canvas();
//不能把List<Circle>当成List<Shape>使用,所以下面代码引起编译错误
c.drawAll(circleList);
|
这时我们可以通过通配符的上限来解决这个问题:
1
|
List<? extends Shape>
|
List<? extends Shape>是受限制通配符的例子,此处的问号(?)代表一个未知的类型,就像前面看到的通配符一样。但是此处的这个未知类型一定是Shape的子类型(也可以是Shape本身),因此可以把Shape称为这个通配符的上限(upper bound)。
设置类型通配符的下限:
除可以指定通配符的上限之外,Java也允许指定通配符的下限,通配符的下限用<?super类型>的方式来指定,通配符下限的作用与通配符上限的作用恰好相反。
指定通配符的下限就是为了支持类型型变。比如Foo是Bar的子类,当程序需要一个A<? super Bar>变量时,程序可以将A
对于逆变的泛型集合来说,编译器只知道集合元素是下限的父类型,但具体是哪种父类型则不确定。因此,这种逆变的泛型集合能向其中添加元素(因为实际赋值的集合元素总是逆变声明的父类),从集合中取元素时只能被当成Object类型处理(编译器无法确定取出的到底是哪个父类的对象)。
4、泛型方法:
前面介绍了在定义类、接口时可以使用泛型形参,在该类的方法定义和成员变量定义、接口的方法定义中,这些泛型形参可被当成普通类型来用。在另外一些情况下,定义类、接口时没有使用泛型形参,但定义方法时想自己定义泛型形参,这也是可以的,Java5还提供了对泛型方法的支持。
(1)、泛型方法的基本用法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
/**
* 泛型方法的基本介绍
* @param tClass 传入的泛型实参
* @return T 返回值为T类型
* 说明:
* 1)public 与 返回值中间<T>非常重要,可以理解为声明此方法为泛型方法。
* 2)只有声明了<T>的方法才是泛型方法,泛型类中的使用了泛型的成员方法并不是泛型方法。
* 3)<T>表明该方法将使用泛型类型T,此时才可以在方法中使用泛型类型T。
* 4)与泛型类的定义一样,此处T可以随便写为任意标识,常见的如T、E、K、V等形式的参数常用于表示泛型。
*/
public <T> T genericMethod(Class<T> tClass) throws InstantiationException ,
IllegalAccessException{
T instance = tClass.newInstance();
return instance;
}
Object obj = genericMethod(Class.forName( "com.test.test" ));
|
(2)、类中的泛型方法:
泛型方法可以出现杂任何地方和任何场景中使用。但是有一种情况是非常特殊的,当泛型方法出现在泛型类中时,我们再通过一个例子看一下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
|
//注意泛型类先写类名再写泛型,泛型方法先写泛型再写方法名
//类中声明的泛型在成员和方法中可用
class A <T, E>{
{
T t1 ;
}
A (T t){
this .t = t;
}
T t;
public void test1() {
System.out.println( this .t);
}
public void test2(T t,E e) {
System.out.println(t);
System.out.println(e);
}
}
@Test
public void run () {
A <Integer,String > a = new A<>( 1 );
a.test1();
a.test2( 2 , "ds" );
// 1
// 2
// ds
}
static class B <T>{
T t;
public void go () {
System.out.println(t);
}
}
|
(3)、泛型方法和可变参数:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
public class 泛型和可变参数 {
@Test
public void test () {
printMsg( "dasd" , 1 , "dasd" , 2.0 , false );
print( "dasdas" , "dasdas" , "aa" );
}
//普通可变参数只能适配一种类型
public void print(String ... args) {
for (String t : args){
System.out.println(t);
}
}
//泛型的可变参数可以匹配所有类型的参数
public <T> void printMsg( T... args){
for (T t : args){
System.out.println(t);
}
}
//打印结果:
//dasd
//1
//dasd
//2.0
//false
}
|
(4)、静态方法与泛型
静态方法有一种情况需要注意一下,那就是在类中的静态方法使用泛型:静态方法无法访问类上定义的泛型;如果静态方法操作的引用数据类型不确定的时候,必须要将泛型定义在方法上。
即:如果静态方法要使用泛型的话,必须将静态方法也定义成泛型方法 。
1
2
3
4
5
6
7
8
9
10
11
12
13
|
public class StaticGenerator<T> {
....
....
/**
* 如果在类中定义使用泛型的静态方法,需要添加额外的泛型声明(将这个方法定义成泛型方法)
* 即使静态方法要使用泛型类中已经声明过的泛型也不可以。
* 如:public static void show(T t){..},此时编译器会提示错误信息:
"StaticGenerator cannot be refrenced from static context"
*/
public static <T> void show(T t){
}
}
|
总结:
泛型方法能使方法独立于类而产生变化,以下是一个基本的指导原则:
无论何时,如果你能做到,你就该尽量使用泛型方法。也就是说,如果使用泛型方法将整个类泛型化,那么就应该使用泛型方法。另外对于一个static的方法而已,无法访问泛型类型的参数。所以如果static方法要使用泛型能力,就必须使其成为泛型方法。
三、泛型的类型擦除:
1、什么是类型擦除:
还记得我们在文章开始介绍的代码吗?我们现在再来看一下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
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());
}
}
|
在这个例子中,我们定义了两个ArrayList
数组,不过一个是ArrayList<String>
泛型类型的,只能存储字符串;一个是ArrayList<Integer>
泛型类型的,只能存储整数,最后,我们通过list1
对象和list2
对象的getClass()
方法获取他们的类的信息,最后发现结果为true
。这就是java的泛型擦除。
下面我们再来看一个例子加深一下理解:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
public class Test001 {
public static void main(String[] args) throws Exception {
ArrayList<Integer> list= new ArrayList<Integer>();
list.add( 1 );
list.getClass().getMethod( "add" ,Object. class ).invoke(list, "asd" );
for ( int i= 0 ;i<list.size();i++) {
System.out.println(list.get(i));
}
}
}
|
上面这段代码首先创建了一个ArrayList,泛型类型实例化为Integer
对象,如果我们直接调用add()方法,那么只能添加Integer类型的值,但是现在我们利用反射,发现可以往里面加入String类型的值,这也说明了java的泛型擦除。
定义:
Java在编译期间,所有的泛型信息都会被擦掉,这就是泛型擦除。
正确理解泛型概念的首要前提是理解类型擦除。Java的泛型基本上都是在编译器这个层次上实现的,在生成的字节码中是不包含泛型中的类型信息的,使用泛型的时候加上类型参数,在编译器编译的时候会去掉,这个过程成为类型擦除。
2、类型擦除后保留的原始类型
原始类型 就是擦除去了泛型信息,最后在字节码中的类型变量的真正类型,无论何时定义一个泛型,相应的原始类型都会被自动提供,类型变量擦除,并使用其限定类型(无限定的变量用Object)替换。
例1、
1
2
3
4
5
6
7
8
9
|
class Pair<T> {
private T value;
public T getValue() {
return value;
}
public void setValue(T value) {
this .value = value;
}
}
|
Pair的原始类型为:Object
1
2
3
4
5
6
7
8
9
|
class Pair {
private Object value;
public Object getValue() {
return value;
}
public void setValue(Object value) {
this .value = value;
}
}
|
因为在Pair<T>
中,T 是一个无限定的类型变量,所以用Object
替换,其结果就是一个普通的类,如同泛型加入Java语言之前的已经实现的样子。在程序中可以包含不同类型的Pair
,如Pair<String>
或Pair<Integer>
,但是擦除类型后他们的就成为原始的Pair
类型了,原始类型都是Object
。
如果类型变量有限定,那么原始类型就用第一个边界的类型变量类替换。
例如: