本篇文章的主要目的是复习或者说重新认识Java中的泛型知识,从而也为老四浅析《手册》的集合规约中的第六点规约做一个基本的解释。具体可以参考文章《阿里巴巴Java开发规约第一章-集合处理篇》指点批评一下。
一、泛型基础
泛型的来源是为了让集合能记住其元素的数据类型。JDK 1.5之前,把一个对象丢进集合之后,集合就不记得对象的类型了,比如说你往list里面放了一个String对象,再往里面放入一个Integer对象,放没啥问题,但是你取出来用可能就满天星、遍地坑了。集合的设计者可能是为了考虑通用性,但是这给Java的使用方面带来了很多困扰,所以java 5之后泛型应运而生,叫做参数化类型,也被称为泛型(Generic)。增加了泛型支持以后的集合,完全可以记住集合中元素的类型,并且在编译阶段就可以检查集合中元素的类型。而且,代码更加简洁,程序更加健壮。
Java 7之后支持简略的菱形泛型语法,见如下代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
package com.glorze.generic; import java.util.ArrayList; import java.util.List; /** * 泛型测试类 * @ClassName OutOfTime * @author: 高老四 * @since: 2018年9月28日 下午7:16:53 */ public class GenericTest { public static void main(String[] args) { // 泛型的菱形语法 List<String> strList = new ArrayList<>(); // Java 7之前要求后面的构造也必须带泛型 List<String> stringList = new ArrayList<String>(); } } |
二、泛型接口、泛型类
我们都知道,所谓泛型就是允许在定义类或者接口以及下面要讲的方法的时候使用类型形参,然后在调用的时候传入具体的类型。泛型接口、泛型类的定义我们不用举例,直接看一下List的源码或者Map的源码就能了解到:
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 40 41 42 43 44 45 46 47 48 |
public interface List<E> extends Collection<E> { int size(); boolean isEmpty(); boolean contains(Object o); Iterator<E> iterator(); Object[] toArray(); <T> T[] toArray(T[] a); boolean add(E e); boolean remove(Object o); boolean containsAll(Collection<?> c); boolean addAll(Collection<? extends E> c); boolean addAll(int index, Collection<? extends E> c); boolean removeAll(Collection<?> c); boolean retainAll(Collection<?> c); default void replaceAll(UnaryOperator<E> operator) { Objects.requireNonNull(operator); final ListIterator<E> li = this.listIterator(); while (li.hasNext()) { li.set(operator.apply(li.next())); } } @SuppressWarnings({"unchecked", "rawtypes"}) default void sort(Comparator<? super E> c) { Object[] a = this.toArray(); Arrays.sort(a, (Comparator) c); ListIterator<E> i = this.listIterator(); for (Object e : a) { i.next(); i.set((E) e); } } void clear(); boolean equals(Object o); int hashCode(); E get(int index); E set(int index, E element); void add(int index, E element); E remove(int index); int indexOf(Object o); int lastIndexOf(Object o); ListIterator<E> listIterator(); ListIterator<E> listIterator(int index); List<E> subList(int fromIndex, int toIndex); @Override default Spliterator<E> spliterator() { return Spliterators.spliterator(this, Spliterator.ORDERED); } } |
我们从list的源码中就可以看到泛型接口的声明,类也是一样。其实这也就是泛型的实质: 允许在定义接口、类型时声明类型形参,类型形参在整个接口、类体内可当成类型使用,几乎所有可使用普通类型的地方都可以使用这种类型形参。如List的源码所示,实际我们使用的过程中可以产生无数多个List接口,只要为E传入不同的类型实参,系统就会多出一个新的List子接口。
注意: 包含泛型声明的类型可以在定义变量、创建对象的时候传入一个类型实参,但是系统没有进行源码复制,二进制中也没有对应的实参源码,磁盘中没有,内存中也没有,说白了就是这种实参类型的所谓”子类”物理上是不存在的。即是不存在具体泛型类.class这种文件的,不论我们为泛型传入哪一种实参,对JVM来说都是被当做同一个类来处理,内存也占用一块空间,所以static修饰的静态方法、静态初始化块或者静态变量的声明和初始化中都是不允许使用类型形参的。关于static的基本知识可以 参考一下老四的这篇《Java面向对象之类成员浅析》文章。
除此之外,泛型不仅仅是集合中才能使用和声明,我们可以为任何类、接口增加泛型声明,基本的泛型类示例如下:
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 40 41 42 43 44 45 |
package com.glorze.generic; /** * 泛型示例类 * @ClassName GenericExample * @author: 高老四 * @since: 2018年9月29日 上午10:57:11 */ public class GenericExample<E> { private E e; public GenericExample() { super(); } public GenericExample(E e) { super(); this.e = e; } public E getE() { return e; } public void setE(E e) { this.e = e; } @Override public String toString() { return "GenericExample [e=" + e + "]"; } public static void main(String[] args) { // 菱形里面根据Java 7之后支持菱形缩略语法,可以不加String的声明 GenericExample<String> genericExample = new GenericExample<>("高老四博客"); String e = genericExample.getE(); System.out.println(e); GenericExample<Integer> ge = new GenericExample<>(999); Integer eInfo = ge.getE(); System.out.println(eInfo); } } |
既然我们可以定一泛型接口或者泛型类,我们就可以通过这些类派生子类,但是切记,派生子类的时候不要包含类型形参,比如子类SubExample要继承GenericExample,应该写public class SubExample extends GenericExample<String>,当然重写父类的方法的时候也需要注意重写方法的返回值要与实参一样。
三、类型通配符
说通配符之前我们需要了解一个知识,就是如果Foo是Bar的一个子类,E是一个带有泛型声明的类或者接口,但是E<Foo>绝对不是E<Bar>的子类。也正是因为如此,很多时候都会遇到以下这种情况:
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 |
package com.glorze.generic; import java.util.ArrayList; import java.util.List; /** * 泛型类型通配符冲突场景示例类 * @ClassName GenericExample * @author: 高老四 * @since: 2018年9月29日 上午11:23:03 */ public class GenericExample { public void test(List<Object> objList) { for(Object object : objList) { System.out.println(object); } } public static void main(String[] args) { List<String> strList = new ArrayList<>(); strList.add("高老四博客"); strList.add("http://www.glorze.com"); new GenericExample().test(strList); } } |
如上代码所示,其实我们的意愿或者说我们的愿景是希望strList可以传入到test方法中并且正确执行,但是这样的写你会看到如下的编译错误提示:
这个错误就说明List<String>并不是List<Object>的子类,但是我们还要实现我们的愿景,同时Java的泛型设计之初的原则也是要求只要代码编译时没有出现警告,就不会遇到运行时ClassCastException(类转换异常)异常,所以通配符应运而生,泛型的类型通配符,通过设置一个问号”?”作为类型实参出给List集合,代表一个未知的List,它可以匹配任何类型,从而也代表了List<?>是各种泛型List的父类了,这样我们就解决了上面的问题,当我们调用test的时候传入了String实参,objList里面的问号”?”也就代表适配了String类型,那么同理Set<?>、Collection<?>、Map<?, ?>等都是一样的道理。但是类型通配符有一个需要我们注意的地方,就是往往因为问号”?”代表了各种类型的泛型,所以List<?> qmList方式的声明是不能向qmList中添加任何元素的,因为我们不知道qmList中到底代表什么类型。上面的程序改写如下:
1 2 3 4 5 6 7 8 9 10 11 12 |
public void test(List<?> qmList) { for(int i = 0; i < qmList.size(); i++) { System.out.println(qmList.get(i)); } } public static void main(String[] args) { List<String> strList = new ArrayList<>(); strList.add("高老四博客"); strList.add("http://www.glorze.com"); new GenericExample().test(strList); } |
接下来还有一种特殊的情况,也就是我们要说的泛型类型的通配符上限,上面的程序我们也看到了,那就是问号”?”代表了各种泛型类型,但是我们往往需要的是指定某一种特定的类型,只希望问号”?”代表某一类泛型的集合等,然后针对这个特定的类型进行业务操作。所以Java也提供了被限制的泛型通配符,形如List<? extends E>,其中E代表具体的泛型实参,问号”?”依然代表任意未知类型,但是我们限定了这个未知的类型一定是E本身或者E的子类型,E也就被称作这个通配符的上限。基本的示例如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
package com.glorze.generic; /** * 设定泛型类型形参的上限 * @ClassName GenericExample * @author: 高老四 * @since: 2018年9月29日 上午11:23:03 */ public class GenericExample<E extends Number> { public static void main(String[] args) { GenericExample<Double> genericExample = new GenericExample<>(); GenericExample<Integer> ge = new GenericExample<>(); // String不是Number的子类型,所以传入String类型直接就会编译错误 GenericExample<String> generic = new GenericExample<>(); } } |
四、泛型方法
前面介绍的都是在定义类、接口的时候我们可以使用类型形参,同样,我们也可以在类的方法中自定义类型形参,这就是泛型方法: 在声明方法时定义一个或多个类型形参,形如: “修饰符<E, T> 返回值类型 方法名(形参列表)”,比普通的方法签名多了一个形参的声明在返回值类型的前面。我们可以翻看前面老四贴出的List源码,其中的集合转数组的方法声明就是泛型方法: “<T> T[] toArray(T[] a);”。对于泛型方法,我们可以看到,形参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 30 31 |
package com.glorze.generic; import java.util.ArrayList; import java.util.Collection; /** * 泛型方法示例类 * @ClassName GenericMethodExample * @author: 高老四 * @since: 2018年9月29日 下午12:26:02 */ public class GenericMethodExample { static <T> void fromArrayToCollection(T[] arr, Collection<T> col) { for(T t : arr) { col.add(t); } } public static void main(String[] args) { Integer[] intArr = {1,2,3}; Collection<Integer> col = new ArrayList<>(); fromArrayToCollection(intArr, col); String[] strArr = {"高老四博客","glorze.com","倚楼听风雨"}; Collection<String> collection = new ArrayList<>(); fromArrayToCollection(strArr, collection); System.out.println(col); System.out.println(collection); } } |
当我们调用fromArrayToCollection(数组元素放入集合当中)这个方法的时候,是没必要在调入之前传入String或者Integer等实参类型的,JVM编译器会根据世纪过来的参数自己推断,但是我们也不要去迷惑系统的推断,否则编译期间就会报错,比如上面代码中strArr用的是String,但是下面的collection你改成了其他的就会变异不同过。
了解了泛型方法之后,融会贯通,举一反三,我们可以发现泛型方法其实和之前降到泛型通配符时刻一互相转换的,例如”<T> bool add(T t);”是可以用泛型方法”add(E<? extends T> e)”的形式的,那么二者之间有什么区别和各自的使用场景呢?
其实通配符的设计如之前所述,他是被设计用来支持灵活的子类型的,但是泛型方法其实更多的用来表示方法中的参数之间的依赖关系,或者说方法返回值与参数之间的依赖关系,如果不涉及这样的关系,其实不应该使用泛型方法。另外泛型方法只能必须在对应的方法中显示声明,然而类型通配符就可以在方法签名中声明,定义形参类型,也可以用于定义变量类型,所以二者各有其设计的作用方向,在各自的试用场景下发挥其能就好。
之前我们提到了泛型类型的通配符的上限,那么也就有对应的泛型类型通配符下限,其实通配符的上限解决的是我们对内容读取的问题,因为不能确定泛型类型他不能试用增加(add)方法,那么通配符下限其实就是为了互补,解决的是内容增加的问题,所以通配符下限不能使用获得(get)方法。通配符下限的表示形如: “<? super Type>”,通配符问号”?”表示它必须是Type的本身或者是Type的父类型。举个例子来说明一下: 我们要把源集合中的元素复制到目标集合中,并且返回复制元素最后一个元素的值,首先我们使用通配符上限来实现这个写法:
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 |
package com.glorze.generic; import java.util.List; /** * 泛型方法示例类 * @ClassName GenericMethodExample * @author: 高老四 * @since: 2018年9月29日 下午12:26:02 */ public class GenericRegexExample { /** * 集合复制 * @Title: copySrcToDest * @param srcList * @param destList * @return T * @author: glorze.com * @since: 2018年9月29日 下午1:52:34 */ public static <T> T copySrcToDest(List<? extends T> srcList, List<T> destList) { T last = null; for(T t : srcList) { destList.add(t); last = t; } return last; } } |
表面看起来我们的实现也没什么问题,但实际上,对于srcList里面放入的是什么样的类型元素我们是不确定的,我们只知道他是T本身或者T的子类型,我们的程序也只好在foreach的时候用T笼统的来表示,当我们在main方法中定义一个”List<Integer> srcList = new ArrayList<>();”以及”List<Number> destList = new ArrayList<>();”并将两个集合传入copySrcToDest方法中其实会报错的,编译不同过。因为这样传入方法中之后,我们会知道T代表的实际类型是Number,这样就导致了最后元素返回的时候也变成了Number类型,但实际上我们的愿景是得到Integer类型,即元素在复制的过程中丢掉了源集合中的自己本身的类型,所以为了满足这种愿景,我们需要置换思想,要以Interger类型为最大的类型准则,即不管源集合中的元素是什么类型,我们要只管保证目标集合的中元素类型要与前者相同或者是他的父类才可以,Java中就采用了设定通配符下限的方式来表示。
改写上面的程序如下:
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 40 41 42 |
package com.glorze.generic; import java.util.ArrayList; import java.util.List; /** * 泛型方法示例类 * @ClassName GenericMethodExample * @author: 高老四 * @since: 2018年9月29日 下午12:26:02 */ public class GenericRegexExample { /** * 集合复制 * @Title: copySrcToDest * @param srcList * @param destList * @return T * @author: glorze.com * @since: 2018年9月29日 下午1:52:34 */ public static <T> T copySrcToDest(List<T> srcList, List<? super T> destList) { T last = null; for(T t : srcList) { destList.add(t); last = t; } return last; } public static void main(String[] args) { List<Integer> srcList = new ArrayList<>(); srcList.add(1); srcList.add(2); srcList.add(3); List<Number> destList = new ArrayList<>(); Integer result = copySrcToDest(srcList, destList); System.out.println(result); } } |
使用通配符下限以后,我们看到T实际得到类型是Integer类型,符合我们的预期。更多的内容也会在下面的PECS(Producer Extends,Consumer Super)原则中提到。
接下来简单提及一下Java中关于对泛型类型推断的改进,Java8改进了泛型方法的类型推断能力:
- 可通过调用方法的上下文来推断类型参数的目标类型
- 可以在方法调用链中,蒋推断得到的类型参数传递到最后一个方法。
可能是看不太懂,看看就好,举一个书上的例子直观的感受一下:
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 |
package com.glorze.generic; /** * Java8中改进的泛型类型推断 * @ClassName GenericJdkEight * @author: 高老四 * @since: 2018年9月29日 下午2:44:39 */ public class GenericJdkEight<E> { public static <T> GenericJdkEight<T> methodOne() { return null; } public static <T> GenericJdkEight<T> methodTwo(T one, GenericJdkEight<T> two){ return null; } public static void main(String[] args) { // 可以通过方法赋值的目标参数来推断类型参数为String,即T取到了E GenericJdkEight<String> one = GenericJdkEight.methodOne(); // 无须使用下面这样的语句在调用methodOne的时候指定String类型 GenericJdkEight<String> two = GenericJdkEight.<String>methodOne(); // 通过传入的88参数为整型来推断类型参数就是Integer GenericJdkEight.methodTwo(88, GenericJdkEight.methodOne()); // 无须再次给methodOne指定Integer类型 GenericJdkEight.methodTwo(88, GenericJdkEight.<Integer>methodOne()); } } |
五、擦除和转换
擦除与转换的概念其实不知道不觉中我们也有所体会到,其实就是把一个带有泛型信息的对象如果复制给一个不带有泛型信息的对象,泛型信息就会被擦除,形如List list = new ArrayList<String>();当把一个具有泛型信息的对象赋值给另一个没有泛型信息的变量list,那个String类型信息就会被擦除。而形如List<String> list = new ArrayList<Integer>这种又在Java中是允许的,只不过是编译时期或告诉你未经检查的转换警告,但是如果你把集合中的元素当作String类型取出的时候会发生运行时异常,所以书写的时候要注意这两点。
六、PECS(Producer Extends,Consumer Super)原则
其实PECS原则就是老四之前描述关于泛型类型通配符上下限的使用经过前辈们总结出来的一条原则,顾名思义或者换句话说,如果泛型表示一个生产者,咱们就是用<? extends T>;如果泛型表示一个消费者,就使用<? super T>。这里面的消费者其实代表的就是插入增加这类的操作的对象,而生产者代表就是读取内容的对象。在前面代码的集合复制示例中,我们讲述了集合复制方式的由通配符上限转为通配符下限的过程,在那里面当使用通配符上限的时候,srcList就是生产者,而当改为通配符下限的时候的,destList就是消费者,所以如果是集合复制,我们需要使用消费者即通配符下限的模式来完美实现功能,而当我们是从源集合读取元素的时候,我们就需要使用生茶这即通配符上限的模式来实现功能。总结下来也就是:
- 如果你想遍历集合,需要对集合(生产者)里面的元素进行业务操作,那么就是用通配符上限(? extends T)
- 如果你想将外部的元素添加到某个集合(消费者)当中,那么就使用通配符下限(? super T)
更博不易,如果觉得文章对你有帮助并且有能力的老铁烦请赞助盒烟钱,点我去赞助。或者扫描文章下面的微信/支付宝二维码打赏任意金额,老四这里抱拳了。赞助时请备注姓名或者昵称,因为您的署名会出现在赞赏列表页面,您的赞赏钱财也会被用于小站的服务器运维上面,再次抱拳。