泛型提供了一种将集合类型传达给编译器的方法,一旦编译器知道了集合元素的类型,编译器就能够对其类型进行检查,作类型约束。java
在没有泛型以前:spring
/** * 迭代 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
异常,这对调用方来讲并不友好。json
经过泛型,能够将方法的 doc 注释转移到了方法签名上:forEachCollection(Collection<String> collection)
,方法调用者一看方法签名便知道此处须要一个Collection<String>
,编译器也能够在编译时检查是否违反类型约束。须要说明的是,编译器的检查也是很是容易绕过的,如何绕过呢?请看下文哦~安全
画外音:代码就是最好的注释。微信
思考,如下代码是否合法:app
List<String> strList = new ArrayList<>(); List<Object> objList = new ArrayList<>(); objList.add("公众号:Coder小黑"); // 代码1 objList = strList; // 代码2
废话很少说,直接上答案。框架
代码1
很明显是合法的。Object
类型是String
类型的父类。debug
那么代码2
为何不合法呢?code
在 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: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 时发现类型不一致马上报错,而不是继续运行可能存在问题的程序
咱们知道,编译器会将泛型擦除,那怎么理解泛型擦除呢?是统一改为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
。
了解完泛型擦除规则以后,咱们来看一下当泛型遇到方法重载,会遇到什么样的问题呢?
阅读以下代码:
// 第一组 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
的子类,构成重载。
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()); } }
最近看到这样一个代码,使用 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小黑