Java泛型入门篇: 泛型类、泛型接口以及泛型方法
Java泛型进阶篇: 无界通配符、上界通配符以及下界通配符
Java泛型原理篇: 类型擦除以及桥接方法
我们在平时的开发当中基本上无时无刻都在使用泛型,尤其是涉及到集合、多态或自定义类的场景,可以说是泛型是一个十分重要的特性。但包括我在内的大多数人,对泛型的掌握不够深刻,大多数情况下只在使用List、Map等集合的时候才会使用到,其他情况下基本不用或者说不会用,在编码以及设计的时候也就无法做到得心应手,所以是时候来梳理一下有关泛型的知识了。
以经典的集合为例,在引进泛型之前,我们定义的集合对象中可以存放任意类型的数据,即Object
。而在获取元素的时候,大多数时间都需要我们强制转换该对象至某一数据类型,后续才可以调用其相关方法或得到某些属性值,也就是需要明确的知道每一个元素的类型,此时非常容易引发ClassCastException
。可怕的是,在编译期间并没有任何迹象表现出代码有问题,只有当运行时程序执行至此并且触发某种逻辑时才会引起异常,如:
List list = new ArrayList();
list.add("1");
list.add(2);
list.add(Collections.emptyList());
可以看到,list中能存放任意类型,即Object类型或其子类类型。而当我们取元素时,一般需要进行强制类型转换:
String str;
for (Object obj : list) {
str = (String) obj;
System.out.println(str.length);
}
在我们写完此段程序时,IDE并没有提示有任何问题,但是当程序执行时就会抛出ClassCastException
,因为list中的第2、3个元素并不是String
类型。
既然是转型错误,那么我们使用instance of
关键字进行所属类型判断应该就不会有问题了,改进后的代码如下:
for (Object obj : list) {
if (obj instanceof String) {
System.out.println((String) obj);
} else if (obj instanceof Integer) {
System.out.println((Integer) obj);
} else if (obj instanceof List) {
System.out.println((List) obj);
}
}
改进后的代码执行后确实没有再触发异常了,但问题是如果我们添加新的数据类型(如添加一个浮点数),那么程序就要加一种对应的类型判断。在日常开发中,程序都是有可能对外提供的,我们也不知道调用者到底会传递什么类型,那么要检查多少种类型才能让程序健壮呢,10种,100种还是1000种?增加这么多判断的同时,代码也就失去了可读性和维护性。甚至调用者会传递自定义的类型,这个时候就更加不能采取这种手段解决问题了。
从Java5以后,我们可以使用新特性泛型(Generic)来解决这一问题,它提供了编译期的类型安全检查机制,即在编译期间就可以判断类型是否匹配,无需等到运行时,并且在获取元素时也无需手动进行强制转换。
可以把泛型理解成一个标签或标记。假如有一个箱子,我们能够向箱子中放入任意东西,比如球鞋、手机、衣服等,没有任何限制。在从箱子里取出东西时需要辨别具体是什么东西,才能够使用这件物品,如果提前预判是什么物品就有可能会出差错。泛型就类似一个便利贴,贴在箱子上,上边写着"球鞋",那么我们就知道这个箱子是专门放球鞋的,无法放入其他物品,从箱子里取出的时候,也不用去辨别,因为里边的东西就是球鞋,拿出来直接穿就行。(有一种情况除外,在后续的文章中会介绍)
泛型的本质就是参数化类型,即所有的操作类型被指定为一个参数,可在整个类或方法中进行传递。
见名知意,泛型类就是将泛型标识定义在类上,他与普通类的创建类似,只不过多了<>
来存放泛型标识。
public class 类名 <泛型标识,泛型标识2,...> {
private 泛型标识 变量名;
}
泛型标识可以为任意字符串,一般只用一个大写的字母。平时常见的有E
, T
, K
, V
,当然也可以定义为A
,B
,C
,D
等,泛型标识的数量也可以是任意个。
在泛型类中定义的泛型可以理解为类型的形式参数,可以用来做成员变量,也可以作为方法的入参或方法的返回值类型,如:
public class TestGeneric<T> {
// 做成员变量
private T t;
// 做方法入参
public TestGeneric(T t) {
this.t = t;
}
// 做方法返回值类型
public T getT() {
return t;
}
}
创建泛型类对象与创建普通对象相似,但多了<>
来声明实际的泛型类型,可以理解为实参,即从当前创建的对象中的成员变量、方法或返回值类型由T
转换为了所传递的类型。
类名<数据类型> 对象名 = new 类名<数据类型>();
比如经常使用的List
与ArrayList
:
List<String> list = new ArrayList<String>();
上述写法比较啰嗦,前后共定义了两次泛型的实际数据类型。在Java7之后,后边的泛型类型可以不用写,程序会自动识别,后边就变成了一个空的<>
。
List<String> list = new ArrayList<>();
如果不指定泛型类型时,会默认为Object
。
在泛型介绍中提到,泛型的两个好处,一是可以在编译期校验数据类型,二是获取时可以不用强制类型转换,我们平时在使用的时候也确实如此。
List<String> list = new ArrayList<>();
list.add("1");
// 编译错误,只能存储String类型
list.add(2);
// 编译错误,只能存储String类型
list.add(Collections.emptyList());
// 无需强制转型,直接就可以获取String类型
String str = list.get(0);
public class TestList<T> extends ArrayList<T> {
}
class Test {
public static void main(String[] args) {
List<String> list = new TestList<>();
list.add("1");
}
}
如果泛型类型不一致则会编译错误
// Cannot resolve symbol 'E'
public class TestList<T> extends ArrayList<E> {
}
分析:上文中提出泛型实质上为参数化类型,泛型T
可以在实例化的时候指定,如TestList
,也就是泛型类型T
的实际类型为String
,父类中的泛型类型同样为T
,也为String
类型。但如果不一致的话,如上例中,父类中的泛型类型E
无法通过参数化进行传递,也就不能确定实际的类型,编译也就不通过了。
public class TestList2 extends ArrayList<Integer> {
}
class Test2 {
public static void main(String[] args) {
TestList2 list = new TestList2();
list.add(1234);
}
}
分析:由于子类不是泛型类,自然也就无法将泛型类型传递给父类,所以需要在定义类时指定具体的泛型类型,否则就会编译错误。如上例所示,将ArrayList
的泛型类型指定为Integer
,那么使用TestList2
时,就可以调用ArrayList
的相关方法了。
泛型接口的定义与泛型类完全一致
public interface 接口名 <泛型标识1, 泛型标识2...> {
}
最典型的例子就是熟知的框架Mybatis-Plus
中Mapper
以及IBaseService
接口,下图截取自官网Gitee
public class TestList3<E> implements List<E> {
@Override
public int size() {
return 0;
}
...
如果泛型类型不一致则会编译错误
// Cannot resolve symbol 'E'
public class TestList3<T> implements List<E> {
}
同泛型类继承,如果实现类不是泛型类,又想指定父接口的泛型类形时,那么需要明确的指出具体的类型。
public class TestList4 implements List<Integer> {
@Override
public int size() {
return 0;
}
...
}
class Test4 {
public static void main(String[] args) {
TestList4 list = new TestList4();
list.add(1234);
}
}
泛型方法就是在定义方法时额外定义泛型标识,在使用方法时传递该泛型类型。需要注意的是,并不是带有泛型标识的方法就是泛型方法。如:
public class Box<T> {
private List<T> list = new ArrayList<>();
public void put(T t) {
list.add(t);
}
public T get(int index) {
if (index >= list.size()) {
throw new IllegalArgumentException("Illegal index");
}
return list.get(index);
}
}
其中,虽然put()
以及get()
方法的参数或返回值类型都包含了泛型标识T
,但是这两个方法并不是泛型方法,而是使用了泛型类Box
中定义的泛型类型的普通方法,T
是通过泛型类定义的泛型标识T
传递而来的,而不是在方法中定义的。
泛型方法是不依托于泛型类或泛型接口的,即普通类中也可以定义泛型方法。
在返回值类型前使用<>
来定义该方法的泛型标识。
public <泛型标识1, 泛型标识2...> 返回值类型 方法名(参数1...) {
...
}
public class TestUtil {
public <T> T getFirst(List<T> list) {
if (CollectionUtils.isEmpty(list)) {
return null;
}
return list.get(0);
}
public static void main(String[] args) {
TestUtil testUtil = new TestUtil();
List<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);
list.add(3);
Integer num = testUtil.getFirst(list);
// 结果为1
System.out.println(num);
List<String> list2 = new ArrayList<>();
list2.add("a");
list2.add("b");
list2.add("c");
String letter = testUtil.getFirst(list2);
// 结果为a
System.out.println(letter);
}
}
上例中,TestUtil
并不是泛型类,但是getFirst()
却是泛型方法,它使用了
作为泛型标识,在参数中以及返回值进行传递。
如果是想定义静态泛型方法,需在泛型标识前加static,上述方法可以改为
public static <T> T getFirst(List<T> list) {
if (CollectionUtils.isEmpty(list)) {
return null;
}
return list.get(0);
}
当出现这种情况时,可以同时使用两者的泛型标识。
public class Test<T> {
public <E> void test(T t, E e) {
// Do Something...
}
}
当泛型方法存在于一个泛型类或泛型接口,并且两者的泛型标识一致时,此时该方法中的泛型标识为方法中定义的泛型标识,而不是泛型类/泛型接口中定义的。
public class TestGeneric<T> {
public <T> T test(T t) {
return t;
}
public static void Main(String[] args) {
TestGeneric<String> testGeneric = new TestGeneric<>();
// 实参与返回值类型都不是创建泛型类对象时定义的String,而是Integer
Integer number = testGeneric.get(1);
}
}