你对Java泛型的理解够深入吗?
泛型
泛型提供了一种将集合类型传达给编译器的方法,一旦编译器知道了集合元素的类型,编译器就可以对其类型进行检查,做类型约束。
在没有泛型之前:
/**
* 迭代 Collection ,注意 Collection 里面只能是 String 类型
*/
public static void forEachStringCollection(Collection collection) {
Iterator iterator = collection.iterator();
while (iterator.hasNext()) {
String next = (String) iterator.next();
System.out.println("next string : " + next);
}
}
这是使用泛型之后的程序:
public static void forEachCollection(Collection<String> collection) {
Iterator<String> iterator = collection.iterator();
while (iterator.hasNext()) {
String next = iterator.next();
System.out.println("next string : " + next);
}
}
在没有泛型之前,我们只能通过更直观的方法命名和 doc 注释来告知方法的调用者,forEachStringCollection
方法只能接收元素类型为String
的集合。然而这只是一种“约定”,如果使用方传入了一个元素不为String
类型的集合,在编译期间代码并不会报错,只有在运行时,会抛出ClassCastException
异常,这对调用方来说并不友好。
通过泛型,可以将方法的 doc 注释转移到了方法签名上:forEachCollection(Collection<String> collection)
,方法调用者一看方法签名便知道此处需要一个Collection<String>
,编译器也可以在编译时检查是否违反类型约束。需要说明的是,编译器的检查也是非常容易绕过的,如何绕过呢?请看下文哦~
画外音:代码就是最好的注释。
泛型和类型转化
思考,以下代码是否合法:
List<String> strList = new ArrayList<>();
List<Object> objList = new ArrayList<>();
objList.add("公众号:Coder小黑"); // 代码1
objList = strList; // 代码2
废话不多说,直接上答案。
代码1
很明显是合法的。Object
类型是String
类型的父类。
那么代码2
为什么不合法呢?
在 Java 中,对象类型的赋值其实是引用地址的赋值,也就是说,假设代码2
赋值成功,objList
和strList
变量引用的是同一个地址。那会有什么问题呢?
如果此时,往objList
中添加了一个非String
类型的元素,也就相当于往strList
中添加了一个非String
类型的元素。很明显,此处就破坏了List<String> strList
。所以,Java 编译器会认为代码2
是非法的,这是一种安全的做法。
画外音:可能和大多数人的直觉不太一样,那是我们考虑问题还不够全面,此处的原因比结果更重要哦
泛型通配符
我们已经知道,上文的代码2
是不合法的。那么,接下来思考这样两个方法:
public static void printCollection1(Collection c) {}
public static void printCollection2(Collection<Object> c) {}
这两个方法有什么区别呢?
printCollection1
方法支持任意元素类型的Collection
,而printCollection2
方法只能接收Object
类型的Collection
。虽然String
是Object
的子类,但是Collection<String>
并不是Collection<Object>
的子类,和代码2
有异曲同工之妙。
再看一下下面这个方法:
public static void printCollection3(Collection<?> c) {}
printCollection3
和上面的两个方法又有什么区别呢?怎么理解printCollection3
方法上的?
呢?
?
表示任意类型,表明printCollection3
方法接收任意类型的集合。
好,那么问题又来了,请看如下代码:
List<?> c = Lists.newArrayList(new Object());
Object o = c.get(0);
c.add("12"); // 编译错误
为什么会编译报错呢?
我们可以将任意类型的集合赋值给List<?> c
变量。但是,add
方法的参数类型是?
,它表示未知类型,所以调用add
方法时会编程错误,这是一种安全的做法。
而get
方法返回集合中的元素,虽然集合中的元素类型未知,但是无论是什么类型,其均为Object
类型,所以使用Object
类型来接收是安全的。
有界通配符
public static class Person extends Object {}
public static class Teacher extends Person {}
// 只知道这个泛型的类型是Person的子类,具体是哪一个不知道
public static void method1(List<? extends Person> c) {}
// 只知道这个泛型的类型是Teacher的父类,具体是哪一个不知道
public static void method2(List<? super Teacher> c) {}
思考如下代码运行结果:
public static void test3() {
List<Teacher> teachers = Lists.newArrayList(new Teacher(), new Teacher());
// method1 处理的是 Person 的 子类,Teacher 是 Person 的子类
method1(teachers);
}
// 只知道这个泛型的类型是Person的子类,具体是哪一个不知道
public static void method1(List<? extends Person> c) {
// Person 的子类,转Person, 安全
Person person = c.get(0);
c.add(new Person()); //代码3,编译错误
}
代码3
为什么会编译错误呢?
method1
只知道这个泛型的类型是Person
的子类,具体是哪一个不知道。如果代码3
编译成功,那么上述的代码中,就是往List<Teacher> teachers
中添加了一个Person
元素。此时,后续在操作List<Teacher> teachers
时,大概率会抛出ClassCastException
异常。
再来看如下代码:
public static void test4() {
List<Person> teachers = Lists.newArrayList(new Teacher(), new Person());
// method1 处理的是 Person 的 子类,Teacher 是 Person 的子类
method2(teachers);
}
// 只知道这个泛型的类型是Teacher的父类,具体是哪一个不知道
public static void method2(List<? super Teacher> c) {
// 具体是哪一个不知道, 只能用Object接收
Object object = c.get(0); // 代码4
c.add(new Teacher()); // 代码5,不报错
}
method2
泛型类型是Teacher
的父类,而Teacher
的父类有很多,所以代码4
只能使用Object
来接收。子类继承父类,所以往集合中添加一个Teacher
对象是安全的操作。
最佳实践:PECS 原则
PECS:producer extends, consumer super
。
- 生产者,生产数据的, 使用
<? extends T>
- 消费者,消费数据的,使用
<? super T>
怎么理解呢?我们直接上代码:
/**
* producer - extends, consumer- super
*/
public static void addAll(Collection<? extends Object> producer,
Collection<? super Object> consumer) {
consumer.addAll(producer);
}
有同学可能会说,这个原则记不住怎么办?
没关系,笔者有时候也记不清。不过幸运的是,在 JDK 中有这个一个方法:java.util.Collections#copy
,该方法很好的阐述了 PECS 原则。每次想用又记不清的时候,看一眼该方法就明白了~
// java.util.Collections#copy
public static <T> void copy(List<? super T> dest, List<? extends T> src){}
画外音:知识很多、很杂,我们应该在大脑中建立索引,遇到问题,通过索引来快速查找解决方法
更安全的泛型检查
上述的一些检查都是编译时的检查,而想要骗过编译器的检查也很简单:
public static void test5() {
List<Integer> list = new ArrayList<>(Arrays.asList(1, 2, 3, 4, 5));
List copy = list;
copy.add("a");
List<Integer> list2 = copy;
}
test5
方法就骗过了编译器,而且能成功运行。
那什么时候会报错呢?当程序去读取list2
中的元素时,才会抛出ClassCastException
异常。
Java 给我们提供了java.util.Collections#checkedList
方法,在调用add
时就会检查类型是否匹配。
public static void test6() {
List<Integer> list = Collections.checkedList(Arrays.asList(1, 2, 3, 4, 5), Integer.class);
List copy = list;
// Exception in thread "main" java.lang.ClassCastException: Attempt to insert class java.lang.String element into collection with element type class java.lang.Integer
copy.add("a");
}
画外音:这是一种 fail-fast 的思想,在 add 时发现类型不一致立刻报错,而不是继续运行可能存在问题的程序
类型擦除(Type Erasure)
我们知道,编译器会将泛型擦除,那怎么理解泛型擦除呢?是统一改成Object
吗?
泛型擦除遵循以下规则:
- 如果泛型参数无界,则编译器会将其替换为
Object
。 - 如果泛型参数有界,则编译器会将其替换为边界类型。
public class TypeErasureDemo {
public <T> void forEach(Collection<T> collection) {}
public <E extends String> void iter(Collection<E> collection) {}
}
使用javap
命令查看 Class 文件信息:
通过 Class 文件信息可以看到:编译器将forEach
方法的泛型替换为了Object
,将iter
方法的泛型替换为了String
。
泛型和方法重载(overload)
了解完泛型擦除规则之后,我们来看一下当泛型遇到方法重载,会遇到什么样的问题呢?
阅读如下代码:
// 第一组
public static void printArray(Object[] objs) {}
public static <T> void printArray(T[] objs) {}
// 第二组
public static void printArray(Object[] objs) {}
public static <T extends Person> void printArray(T[] objs) {}
上面两组方法是否都构成了重载呢?
第一组:泛型会被擦除,也就是说,在运行时期,
T[]
其实就是Object[]
,因此第一组不构成重载。第二组:
<T extends Person>
表明接收的方法是Person
的子类,构成重载。
使用 ResolvableType 解析泛型
Spring 框架中提供了org.springframework.core.ResolvableType
来优雅解析泛型。
一个简单的使用示例如下:
public class ResolveTypeDemo {
private static final List<String> strList = Lists.newArrayList("a");
public <T extends CharSequence> void exchange(T obj) {}
public static void resolveFieldType() throws Exception {
Field field = ReflectionUtils.findField(ResolveTypeDemo.class, "strList");
ResolvableType resolvableType = ResolvableType.forField(field);
// class java.lang.String
System.out.println(resolvableType.getGeneric(0).resolve());
}
public static void resolveMethodParameterType() throws Exception {
Parameter[] parameters = ReflectionUtils.findMethod(ResolveTypeDemo.class, "exchange", CharSequence.class).getParameters();
ResolvableType resolvableType = ResolvableType.forMethodParameter(MethodParameter.forParameter(parameters[0]));
// interface java.lang.CharSequence
System.out.println(resolvableType.resolve());
}
public static void resolveInstanceType() throws Exception {
PayloadApplicationEvent<String> instance = new PayloadApplicationEvent<>(new Object(), "hi");
ResolvableType resolvableTypeForInstance = ResolvableType.forInstance(instance);
// class java.lang.String
System.out.println(resolvableTypeForInstance.as(PayloadApplicationEvent.class).getGeneric().resolve());
}
}
泛型和 JSON 反序列化
最近看到这样一个代码,使用 Jackson 将 JSON 转化为 Map。
public class JsonToMapDemo {
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
public static <K, V> Map<K, V> toMap(String json) throws JsonProcessingException {
return (Map) OBJECT_MAPPER.readValue(json, new TypeReference<Map<K, V>>() {
});
}
public static void main(String[] args) throws JsonProcessingException {
// {"1":{"id":1}}
String json = "{\"1\":{\"id\":1}}";
Map<Integer, User> userIdMap = OBJECT_MAPPER.readValue(json, new TypeReference<Map<Integer, User>>() {
});
}
@Data
public static class User implements Serializable {
private static final long serialVersionUID = 8817514749356118922L;
private int id;
}
}
运行 main 方法,代码虽然正常结束。但是这个代码其实是有问题的,有什么问题呢?一起来看如下代码:
public static void main(String[] args) {
// {"1":{"id":1}}
String json = "{\"1\":{\"id\":1}}";
Map<Integer, User> userIdMap = toMap(json);
userIdMap.forEach((integer, user) -> {
// 出处代码会报错
// Exception in thread "main" java.lang.ClassCastException: java.lang.String cannot be cast to java.lang.Integer
System.out.println(user.getId());
});
}
为什么会报ClassCastException
呢?让我们来 Debug 一探究竟。
通过 Debug 可以发现:Map<Integer, User> userIdMap
对象的 key 其实是String
类型,而 value 是一个LinkedHashMap
。这很好理解,上述代码这个写法,根本不知道 K,V 是什么。正确写法如下:
public static void main(String[] args) throws JsonProcessingException {
// {"1":{"id":1}}
String json = "{\"1\":{\"id\":1}}";
Map<Integer, User> userIdMap = OBJECT_MAPPER.readValue(json, new TypeReference<Map<Integer, User>>() {
});
userIdMap.forEach((integer, user) -> {
System.out.println(user.getId());
});
}
欢迎关注微信公众号:Coder小黑
你对Java泛型的理解够深入吗?的更多相关文章
- 对java泛型的理解
正确的应用java泛型的特性可以更好的实现编程的开闭原则(对扩展开放,对修改关闭),这得益于java泛型提供的在程序运行时获取对象声明类型的特性. 静态语言的特性是在程序编译前进行声明,这样程序在编译 ...
- java泛型的理解
总体介绍泛型: 1.泛型是计算机程序中一种重要的思维方式,它将数据结构和算法与数据类型相分离,使得同一套数据结构和算法,能够应用于各种数据类型,而且还可以保证类型安全,提高可读性.在Java中,泛型广 ...
- Java泛型深入理解(转载)
原文地址 http://blog.csdn.net/sunxianghuang/article/details/51982979 泛型之前 在面向对象编程语言中,多态算是一种泛化机制.例如,你可以将 ...
- Java:泛型的理解
本文源自参考<Think in Java>,多篇博文以及阅读源码的总结 前言 Java中的泛型每各人都在使用,但是它底层的实现方法是什么呢,为何要这样实现,这样实现的优缺点有哪些,怎么解决 ...
- 对于Java泛型的理解
源起:查看COLLECIOTNS类 Q1:为什么java需要泛型? 因为java对于对象类型的确认在编译期,那么强制类型转换就可以通过编译,但是运行时的错误却无法避免,那么泛型的存在可以避免强制类型转 ...
- Java泛型深入理解
泛型的优点: 泛型的主要优点就是让编译器保留參数的类型信息,执行类型检查,执行类型转换(casting)操作,编译器保证了这些类型转换(casting)的绝对无误. /******* 不使用泛型类型 ...
- Java泛型简单理解
优点1: 没有使用泛型,向list集合中添加非字符串,运行时会报错:类型不匹配 ObjectList.java: package cn.nxl2018; import java.util.ArrayL ...
- 关于Java泛型深入理解小总结
1.何为泛型 首先泛型的本质便是类型参数化,通俗的说就是用一个变量来表示类型,这个类型可以是String,Integer等等不确定,表明可接受的类型,原理类似如下代码 int pattern; //声 ...
- java 泛型的理解与应用
为什么使用泛型? 举个例子: public class GenericTest { public static void main(String[] args) { List list = new A ...
随机推荐
- php统计近一周和近30天的用户数据
https://blog.csdn.net/shenpengchao/article/details/59073589 先上一张效果图 这边用的是echarts插件http://echarts.bai ...
- 解决电脑性能一般,打开webstorm后,电脑比较卡的问题
刚到一公司实习,要求使用webstrom开发前端,但安装后发现自己的电脑很卡,特别是在运行项目时,卡的不要不要的. 后来,发现一奇淫技巧,用sublime代替webstrom,但是没法启动项目啊 找到 ...
- 洛谷P2590 [ZJOI2008]树的统计 题解 树链剖分+线段树
题目链接:https://www.luogu.org/problem/P2590 树链剖分模板题. 剖分过程要用到如下7个值: fa[u]:u的父节点编号: dep[u]:u的深度: size[u]: ...
- Centos下添加用户到用户组
将一个用户添加到用户组中,千万不能直接用: usermod -G groupA 这样做会使你离开其他用户组,仅仅做为 这个用户组 groupA 的成员. 应该用 加上 -a 选项: usermod - ...
- 版本号/缓存刷新 laravel mix函数
很多开发者会给编译的前端资源添加时间戳或者唯一令牌后缀以强制浏览器加载最新版本而不是代码的缓存副本.Mix 可以使用 version 方法为你处理这种场景. version 方法会自动附加唯一哈希到已 ...
- js用for循环实现乘法口诀表
for循环可以打印一个乘法口诀表.需要使用for循环的嵌套 <script> for(var i = 0; i <= 9; i++){ // 外层循环控制行数,外层循环执行一次,内层 ...
- [转]在Windows中安装Memcached
Memcached是一个高并发的内存键值对缓存系统,它的主要作用是将数据库查询结果,内容,以及其它一些耗时的计算结果缓存到系统内存中,从而加速Web应用程序的响应速度. Memcached最开始是作为 ...
- 【9101】求n!的值
Time Limit: 10 second Memory Limit: 2 MB 问题描述 用高精度的方法,求n!的精确值(n的值以一般整数输入). Input 文件输入仅一行,输入n. Output ...
- 分布式全局唯一ID
方案一.UUID UUID的方式能生成一串唯一随机32位长度数据,它是无序的一串数据,按照开放软件基金会(OSF)制定的标准计算,UUID的生成用到了以太网卡地址.纳秒级时间.芯片ID码和许多可能的数 ...
- H3C 域名