Java 泛型详解
# 前言
Java 泛型(generics)是 JDK 5 中引入的一个新特性, 泛型提供了编译时类型安全检测机制,该机制允许开发者在编译时检测到非法的类型。
泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数。
使用泛型机制编写的程序代码要比那些杂乱地使用Object
变量,然后再进行强制类型转换的代码具有更好的安全性和可读性。泛型对于集合类尤其有用,例如,ArrayList
就是一个无处不在的集合类。
泛型带来的好处
在没有泛型的情况的下,通过对类型 Object 的引用来实现参数的“任意化”,“任意化”带来的缺点是要做显式的强制类型转换,而这种转换是要求开发者对实际参数类型可以预知的情况下进行的。对于强制类型转换错误的情况,编译器可能不提示错误,在运行的时候才出现异常,这是本身就是一个安全隐患。
那么泛型的好处就是在编译的时候能够检查类型安全,并且所有的强制转换都是自动和隐式的。
# 泛型的使用
泛型有三种常用的使用方式:泛型类,泛型接口和泛型方法。下面讲一一简单介绍这三种使用方法:
# 泛型类
一个泛型类(generic class
)就是具有一个或多个类型变量的类。作用在类上的泛型:当数据类型不确定的时候,可以⼀个泛型,通过创建对象传递过来⼀个类型,来确定T的类型 。下面通过一个简单的Computer
类作为例子。对于这个类来说,我们只关注泛型,而不会为数据存储的细节烦恼。
修饰符 class 类名<代表泛型的变量> { }
泛型类,是在实例化类的时候指明泛型的具体类型
举个例子
/*
* 泛型类
* Java库中 E表示集合的元素类型,K 和 V分别表示表的关键字与值的类型
* T(需要时还可以用临近的字母 U 和 S)表示“任意类型”
*/
public class Computer<T> {
private T name;
private T price;
public Computer() {
}
public Computer(T name, T price) {
this.name = name;
this.price = price;
}
// 省略setter、getter方法
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Computer
类引入了一个类型变量T,用尖括号(<>)括起来,并放在类名的后面。泛型类可以有多个类型变量。例如,可以定义Computer类,其中第一个域和第二个域使用不同的类型:
public class Computer<T,U> { ... }
类方法中的类型变量指定方法的返回类型以及域和局部变量的类型。例如:
private T first; //uses the type variable
用具体的类型替换类型变量就可以实例化泛型类型,例如:
Computer<String> computer = new Computer<>();
computer.setName("xiaomi");
computer.setPrice("2999");
2
3
注意:泛型的类型参数参数只能是类类型,不能是简单类型。
# 泛型方法
修饰符 <代表泛型的变量> 返回值类型 ⽅法名(参数){ }
什么时候确定泛型?调用方法时,确定泛型的类型
public class Test {
// 静态方法无法访问类上定义的泛型;如果静态方法操作的引用数据类型不确定的时候,必须要将泛型定义在方法上。
// 即:如果静态方法要使用泛型的话,必须将静态方法也定义成泛型方法 。
public static <M> void test(M m){
//静态⽅法上要单独声明,它不能使⽤类上⾯的声明的 泛型,调⽤静态⽅法的时候,确定类型
System.out.println("检测到了静态⽅法上的泛型:"+m.getClass());
}
/**
* 泛型方法的基本介绍
* @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;
}
public static void main(String[] args){
Test.test(1);
Test.test("2");
}
}
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
泛型方法使用比较复杂,在举个例子
public class GenericFruit {
class Fruit{
@Override
public String toString() {
return "fruit";
}
}
class Apple extends Fruit{
@Override
public String toString() {
return "apple";
}
}
class Person{
@Override
public String toString() {
return "Person";
}
}
class GenerateTest<T>{
public void show_1(T t){
System.out.println(t.toString());
}
//在泛型类中声明了一个泛型方法,使用泛型E,这种泛型E可以为任意类型。可以类型与T相同,也可以不同。
//由于泛型方法在声明的时候会声明泛型<E>,因此即使在泛型类中并未声明泛型,编译器也能够正确识别泛型方法中识别的泛型。
public <E> void show_3(E t){
System.out.println(t.toString());
}
//在泛型类中声明了一个泛型方法,使用泛型T,注意这个T是一种全新的类型,可以与泛型类中声明的T不是同一种类型。
public <T> void show_2(T t){
System.out.println(t.toString());
}
}
public static void main(String[] args) {
Apple apple = new Apple();
Person person = new Person();
GenerateTest<Fruit> generateTest = new GenerateTest<Fruit>();
//apple是Fruit的子类,所以这里可以
generateTest.show_1(apple);
//编译器会报错,因为泛型类型实参指定的是Fruit,而传入的实参类是Person
//generateTest.show_1(person);
//使用这两个方法都可以成功
generateTest.show_2(apple);
generateTest.show_2(person);
//使用这两个方法也都可以成功
generateTest.show_3(apple);
generateTest.show_3(person);
}
}
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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
# 泛型接口
定义格式:
修饰符 interface 接⼝名<代表泛型的变量> { }
public interface Demo<T1,T2> {
void test1(T1 t1);
void test2(T2 t2);
}
// 未传入泛型实参时,与泛型类的定义相同,在声明类的时候,需将泛型的声明也一起加到类中
class Demo2<T1,T2> implements Demo<T1,T2>{
@Override
public void test1(T1 t1) {
}
@Override
public void test2(T2 t2) {
}
}
//当实现泛型接口的类,传入泛型实参时:
class DemoImpl implements Demo<String,Integer> {
@Override
public void test1(String s) {
}
@Override
public void test2(Integer integer) {
}
}
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
# 泛型通配符
当使用泛型类或者接口时,传递的数据中,泛型类型不确定,可以通过通配符<?>表示。但是⼀旦使用泛型的通配符后,只能使用Object类中的共性方法,集合中元素自身方法无法使用。
# 常用的 T,E,K,V,?
本质上这些个都是通配符,没啥区别,只不过是编码时的一种约定俗成的东西。比如上述代码中的 T ,我们可以换成 A-Z 之间的任何一个 字母都可以,并不会影响程序的正常运行,但是如果换成其他的字母代替 T ,在可读性上可能会弱一些。通常情况下,T,E,K,V,?是这样约定的:
- ?表示不确定的类型,代表集合中可以存储任意类型的数据。
- T (type) 表示具体的一个类型
- K V (key value) 分别代表键值中的Key Value
- E (element) 代表Element
List<Object>
:该容器有数据类型:Object
List<?>:该容器的数据类型可以是任意的
通过 T 来 确保 泛型参数的一致性
/ 通过 T 来 确保 泛型参数的一致性
public <T extends Number> void test(List<T> dest, List<T> src)
//通配符是 不确定的,所以这个方法不能保证两个 List 具有相同的元素类型
public void test(List<? extends Number> dest, List<? extends Number> src)
2
3
4
5
# 泛型中extends和super的区别
# 上界通配符 < ? extends E>
上届:用 extends 关键字声明,表示参数化的类型可能是所指定的类型,或者是此类型的子类。
在类型参数中使用 extends 表示这个泛型中的参数必须是 E 或者 E 的子类,这样有两个好处:
- 如果传入的类型不是 E 或者 E 的子类,编译不成功
- 泛型中可以使用 E 的方法,要不然还得强转成 E 才能使用
# 下界通配符 < ? super E>
下界: 用 super 进行声明,表示参数化的类型可能是所指定的类型,或者是此类型的父类型,直至 Object
在类型参数中使用 super 表示这个泛型中的参数必须是 E 或者 E 的父类。
public class Demo1 {
class Person {
}
class Student extends Person {
}
//?代表可以接收任意类型
public static void test1(List<?> list) {
}
public static void test2(List<Object> list) {
}
//? extends Person限定所有类型是Person以及Person的⼦类,
public static void test3(List<? extends Person> list) {
}
//? super Student限定所有类型是Student以及Student的⽗类
public static void test4(List<? super Student> list) {
}
public static void main(String[] args) {
List<String> list1 = new ArrayList<String>();
List<Integer> list2 = new ArrayList<>();
test1(list2);
List<Object> list3 = new ArrayList<>();
list3.add(1);
list3.add("wowo");
test2(list3);
List<Person> list4 = new ArrayList<>();
List<Student> list5 = new ArrayList<>();
test3(list4);
test4(list4);
}
}
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
40
# Class<T>
和 Class<?>
区别
前面介绍了 ?和 T 的区别,那么对于,Class<T>
和 <Class<?>
又有什么区别呢?Class<T>
和 Class<?>
最常见的是在反射场景下的使用,这里以用一段发射的代码来说明下。
// 通过反射的方式生成 multiLimit
// 对象,这里比较明显的是,我们需要使用强制类型转换
MultiLimit multiLimit = (MultiLimit) Class.forName("com.test.generic.MultiLimit").newInstance();
2
3
对于上述代码,在运行期,如果反射的类型不是 MultiLimit 类,那么一定会报 java.lang.ClassCastException 错误。
Class<T>
在实例化的时候,T 要替换成具体类。Class<?>
它是个通配泛型,? 可以代表任何类型,所以主要用于声明时的限制情况。比如,我们可以这样做申明:
// 可以
public Class<?> clazz;
// 不可以,因为 T 需要指定类型
public Class<T> clazzT;
2
3
4
那如果也想 public Class<T> clazzT;
这样的话,就必须让当前的类也指定 T ,
public class Test3<T> {
public Class<?> clazz;
// 不会报错
public Class<T> clazzT;
}
2
3
4
5