前言

泛型这个词了解编程的人应该都和熟悉,一般强类型的语言都会有,参数类型比较明确。类似Java,C#,C++等语言都有泛型,而且他们都是编译类型或者半编译半解释型的语言。这从另外一个层面说明泛型只能生效在编译期,等到运行时久被擦除。我想熟悉Java的同学都知道我在说什么,估计之前都了解一个词叫类型擦除,而且在面向对象会经常使用,这个还会涉及多态的概念。

什么是泛型

泛型,即“参数化类型”。一提到参数,最熟悉的就是定义方法时有形参,然后调用此方法时传递实参。

泛型的本质是为了参数化类型(在不创建新的类型的情况下,通过泛型指定的不同类型来控制形参具体限制的类型)。也就是说在泛型使用过程中,操作的数据类型被指定为一个参数,这种参数类型可以用在类、接口和方法中,分别被称为泛型类、泛型接口、泛型方法。

作用:一些强类型程序语言支持泛型,其主要目的是加强类型安全及减少类转换的次数,但一些支持泛型的程序语言只能达到部分目的。

类型擦除

Java 泛型的参数只可以代表类,不能代表个别对象。由于Java泛型的类型参数之实际类型在编译时会被消除,所以无法在运行时得知其类型参数的类型,而且无法直接使用基本值类型作为泛型类型参数。Java编译程序在编译泛型时会自动加入类型转换的编码,故运行速度不会因为使用泛型而加快。

下面我们写一个小的demo来看看类型擦除。

1
2
3
4
5
6
7
8
9
10
11
public class TestClient {
public static void main(String[] args) {
List<String> list1 = new ArrayList<String>();
list1.add("1");
List<Integer> list2 = new ArrayList<Integer>();
list2.add(1);
System.out.println(list1.getClass() == list2.getClass());
System.out.println(list1.getClass());
System.out.println(list2.getClass());
}
}

结果很多人估计都知道,这很明显list1和list2结果是class java.util.ArrayList,第一打印是ture。这说明Integer和String对List的类型并没有影响。换一句话说String和Integer只是一个传入到List的参数,并不会对List的类有什么影响,在运行时被擦除。

举一个栗子

我们来通过反射来证明String和Integer是没有什么用的在运行时。它只能在编译器控制(恶心一下你,编译不通过),保证数据的准确性,类似与门口保安。

1
2
3
4
5
6
7
8
9
10
11
12
13
public static void main(String[] args) {
List<Integer> ls = new ArrayList<Integer>();
ls.add(23);
try {
Method method = ls.getClass().getDeclaredMethod("add",Object.class);
method.invoke(ls,"hhhash");
method.invoke(ls,66);
method.invoke(ls,88.09);
} catch (Exception e) {
e.printStackTrace();
}
System.out.println(ls);
}

上面的栗子会不会报错,会打印出什么值。很明显不会报错,都说了类型会擦除,也就是ls会打印。结果[23, hhhash, 66, 88.09]。所以List中的泛型在运行时被擦除,List中存的是Object对象,而不是你传入的泛型对象。

举一个错误的栗子

所以泛型就是在编译时刻保证数据的住准确性。我在写一个关于mybatis泛型擦除的一个错误。

声明的xml对象

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
37
38
39
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<!-- namespace属性是名称空间,必须唯一 -->
<mapper namespace="cn.itcast.javaee.mybatis.app04.Student">

<!-- resultMap标签:映射实体与表
type属性:表示实体全路径名
id属性:为实体与表的映射取一个任意的唯一的名字
-->
<resultMap type="student" id="studentMap">
<!-- id标签:映射主键属性
result标签:映射非主键属性
property属性:实体的属性名
column属性:表的字段名
-->
<id property="id" column="id"/>
<result property="name" column="name"/>
<result property="sal" column="sal"/>
</resultMap>

<!--
查询根据id
resultMap这个属性代表是返回值类型,返回值的类型是Student,就是上面实体类型
-->
<select id="findById" parameterType="int" resultMap="studentMap">
SELECT * FROM STUDENTS WHERE id = #{id};
</select>

<!--
查询所有数据
返回值类型讲道理是List<Student>的,但我们只要写集合中的类型就行了
-->
<select id="findAll" resultMap="studentMap">
SELECT id FROM STUDENTS;
</select>

</mapper>

声明mapper对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import java.util.List;
import java.lang.Long;
import cn.itcast.javaee.mybatis.app04.Student;

public interface StudentDAO {

/**
* 根据Id查询学生信息
* @param id
* @return
*/
Long findById(Long id);

/**
* 查询所有学生
* @return
*/
List<Long> findAll();

}

执行这个两个方法,会又什么结果,各位看官可以猜一下;结合上面说的类型擦除的问题。

我们来说结果,findById(Long id)执行结果肯定是报错的,因为在mapper上声明的是resultMap是Student类型,当你返回时,mybatis会做类型转换,Long类型和Student类型不配合会报类型转换的异常。

第二个执行结果时不报错的,因为返回时List对象,在查询返回List<Object>对象,而Long这个在编译期会生效,但是xml返回时候被擦除也是List<Object>。所以findAll()是不报错的,而且返回时List<Student>的对象而不是List<Long>

泛型的应用

泛型一般使用又三种方式。泛型类,泛型接口和泛型方法。

泛型类
1
2
3
public class Test<T> {
T field1;
}

尖括号 <>中的 T 被称作是类型参数,用于指代任何类型。事实上,T 只是一种习惯性写法,如果你愿意。你可以这样写。

1
2
3
public class Test<Hello> {
Hello field1;
}

但出于规范的目的,Java 还是建议我们用单个大写字母来代表类型参数。常见的如:

T 代表一般的任何类。
E 代表 Element 的意思,或者 Exception 异常的意思。
K 代表 Key 的意思。
V 代表 Value 的意思,通常与 K 一起配合使用。
S 代表 Subtype 的意思。子类型

当然,泛型类不至接受一个类型参数,它还可以这样接受多个类型参数。

1
2
3
4
5
6
7
8
9
10
11
12
public class MultiType <E,T>{
E value1;
T value2;

public E getValue1(){
return value1;
}

public T getValue2(){
return value2;
}
}

只要在对泛型类创建实例的时候,在尖括号中赋值相应的类型便是。E 和 T就会被替换成对应的类型,如 String 或者是 Integer。当一个泛型类被创建时,内部自动扩展成下面的代码。

1
2
3
4
5
6
7
8
9
10
11
12
public class MultiType <String,Integer>{
String value1;
Interger value2;

public String getValue1(){
return value1;
}

public Interger getValue2(){
return value2;
}
}
泛型方法
1
2
3
4
5
6
public class Test1 {

public <T> void testMethod(T t){

}
}

泛型方法是传入的参数是一个指定类型。

1
2
3
4
5
6
public class Test1 {

public <String> void testMethod(String t){

}
}

指定类型,也就是指定了传入参数的类型。<T>中的 T 被称为类型参数,而方法中的 T 被称为参数化类型,它不是运行时真正的参数。同样也可以指定出参的类型。

1
2
3
4
5
6
public class Test1 {

public <T> T testMethod(T t){
return null;
}
}

泛型类、泛型方法和多个泛型同时使用

1
2
3
4
5
6
7
8
9
public class Test1<T,R> {

public <R,T> R testMethod(T t){
return null;
}

public void testMethod1(T t){
}
}

上面代码中,Test1<T,R>是泛型类,testMethod 是泛型类中的普通方法,而 testMethod1 是一个泛型方法。而泛型类中的类型参数与泛型方法中的类型参数是没有相应的联系的,泛型方法始终以自己定义的类型参数为准.例如下面这个栗子

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
public class Test1<R,T> {
//声明入参和返回值不一样类型的方法,使用自己的泛型,尽管名字一样
public <R,T> R testMethod(T t){
System.out.println(t.getClass().getName());
R a = (R) t.getClass().getName();
System.out.println(a.getClass().getName());
return a;
}
//使用类的泛型
public void testMethod1(T t){
System.out.println(t.getClass().getName());
}
//使用类的泛型
public R testMethod2(String a){
R b = (R)a;
System.out.println(b);
return b;
}


public static void main(String[] args) {
Test1<Integer,String> test1 = new Test1<>();
test1.testMethod1("3");
test1.testMethod2("4");
//java会进行强转但是报错了
// Byte name = test1.testMethod(100L);
String name = test1.testMethod(100L);
System.out.println(name);
}
}

所以如果非必要情况下类的泛型和方法的泛型不需要使用同一个字母。这样会好看的多。

1
2
3
4
5
6
7
8
9
public class Test1<T,R> {

public <K,V> K testMethod(V t){
return null;
}

public void testMethod1(T t){
}
}
泛型接口
1
2
3
4
5
6
public interface Test1<T> {

public T testMethod(T t);

public void testMethod1(T t)
}

实现类

1
2
3
4
5
6
7
8
9
10
11
12
public class Test2Impl implements Test2<String>{

@Override
public String testMethod(String s) {
return null;
}

@Override
public void testMethod1(String s) {

}
}
泛型与可变参数
1
2
3
4
5
6
7
8
9
10
11
12
public class Test1 {

public <T> void printMsg( T... args){
for(T t : args){
System.out.println("泛型测试 t is " + t+"class "+t.getClass().getName());
}
}

public static void main(String[] args) {
test1.printMsg("111",222,"aaaa","2323.4",55.55);
}
}

泛型上线边界

泛型也不是无限使用,像上面Byte类型的会报错,所以在泛型上也使用一些上下边界来处理。

1
2
3
4
5
6
7
8
class Base{}

class Sub extends Base{}

Sub sub = new Sub();
Base base = sub;
List<Sub> lsub = new ArrayList<>();
List<Base> lbase = lsub;

以上面的代码为例,肯定是编译不能通过的。但是我们可以通配符来处理保证一定范围内的可用。

<?>无限通配符

无限通配符,就是可以指定任何类型。这样久违背了java是强类型语言的特性。所以java在做无限通配符还是做了限制。比如如果集合中使用了无限通配符那么这个集合久只允许执行赋值操作和读取操作,而不能使用add方法。

1
2
3
4
5
List<?> list = new ArrayList<>();
list.add(1); //编译不通过
list.add(2);
List<String> list2 = new ArrayList<>();
list2 = (List<String>) list; //这个是可以编译通过的

所以使用<?>无限通配符的限度很小,主要用来提高代码的可读性。

<? extends T>`被称作有上限的通配符

在一个范围内确定类别,比如类型 T及 类型 T的子类都可以。

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
public class Test3 <T extends Test3.Base>{

static class Base{

}

static class Sub extends Base{

}
static class NoSub {

}

public void testMethod( T t){
System.out.println(t.getClass().getName());
}

public static void main(String[] args) {
Test3<Base> data = new Test3<>();
Sub sub = new Sub();
SubSon subSon = new SubSon();
Base base = new Base();
data.testMethod(base);//编译通过
data.testMethod(sub); //编译通过
data.testMethod(subSon);//编译通过
Test3<NoSub> data1 = new Test3<>();//编译不通过
}

}

同样如果sub的子类也可以在这里使用,因为都是这个地方使用的是上线也就是,只要是base的子类都可以使用。

想一下如果Test3<Base> data = new Test3<>();声明的是Sub,那么代码编译会是怎么样的。留个你去思考

<? super T>`被称作有下限的通配符。
1
2
3
4
5
6
public class Collections {
public static <T> void copy(List<? super T> dest, List<? extends T> src) {
for (int i = 0; i < src.size(); i++)
dest.set(i, src.get(i));
}
}

保证两个类可以互相copy保证类的可靠性。src是源类,他是T的子类,src是子类的集合,而dest的T事故父类。保证dest是父类的集合。

泛型使用注意

1、泛型只能使用于包装类型而不能使用int、long等基本类型。

2、静态方法和变量上不可以使用泛型。因为静态方法和类都是属于类本身的属性,没有运行时状态,也不存在类型擦除。

3、Java中不能创建指定类型的泛型数组。

1
2
3
4
5
6
7
8
//不能创建一个确切的泛型类型的数组,编译器报错
List<String>[] ls = new ArrayList<String>[10];
//使用通配符创建泛型数组是可以的
List<?>[] ls1 = new ArrayList<?>[10];
//编译器也时通过的
List<String>[] ls2 = new ArrayList[10];
//不过一把都使用google guava 去创建List
List<String> fileNames = Lists.newArrayList();

4、使用泛型主要是在集合和一些通用的包装类中使用,比如dubbo接口的统一返回,用于做分页的返回和反射结合会用的比较多之外。切记乱用,增加代码复杂性,降低代码可读性。