Java 中的泛型 2019-04-28

先来看一下以下 2 段代码,然后再进一步引出我们的泛型。

 

public static void main(String[] args) { List list = new ArrayList(); list.add("123"); list.add(456); Iterator it = list.iterator(); while(it.hasNext()){ // Error : Integer cannot be cast to String String next = (String)it.next(); } }

 

上面这段代码,会出现转化异常的情况,但是编译是没问题的,在输出转化的时候却出现了异常,有没有一种冲动想要把集合中的类型归一?下面就是很正常的一个求和的方法,然而我们只能求类型为 Integer 的参数的和。

 

public Integer add(Integer a,Integer b){ return a + b;}

 

对于集合来说,我们若是能在编译时期指定该集合中存放数据的类型,这样在类型转化的时候就不会再出现错误了,同样的,在下面的求和方法中,这个方法我们只能求得类型为 Integer 的参数的和,我们能不能做到可以通用的求和呢?使用泛型,就可以做到。

 

泛型的概念也就是 “ 数据类型参数化 ” 正是由于我们不确定集合中可以指定的类型有哪些,是 Integer 还是 String ?求和方法中参数的数据类型可以有哪些,是 Float 还是 Double ?那我们就可以使用泛型来把这个数据类型给参数化。

 

泛型的应用有泛型接口,泛型类和泛型方法。下面定义一个泛型类,并演示使用方式。

 

public class Box <T> { // T 是 Type 的简写,代表任意类型,注意是类,而不是基本数据类型。 // 也可以换成其它单词,这只是一个表示而已。 T t; public T getT() { return t; } public void setT(T t) { this.t = t; } // 在下面的应用中,我们可以将 T 换成任意我们想要的类型 public static void main(String[] args) { Box<Integer> iBox = new Box<Integer>(); Box<Double> dBox = new Box<Double>(); // 在 JDK1.7 及其以上版本可以利用 “类型推断” 这样写。 Box<String> stringBox = new Box<>(); }}

 

泛型方法的定义只需要在方法的声明中添加 < T > 即可,或是添加一组泛型 <K ,V> 。 

 

public class Util { public static <K, V> boolean compare(Pair<K, V> p1, Pair<K, V> p2) { return p1.getKey().equals(p2.getKey()) && p1.getValue().equals(p2.getValue()); }}public class Pair<K, V> { private K key; private V value; public Pair(K key, V value) { this.key = key; this.value = value; } public void setKey(K key) { this.key = key; } public void setValue(V value) { this.value = value; } public K getKey() { return key; } public V getValue() { return value; }}

 

我们可以这样来调用泛型方法。

 

Pair<Integer, String> p1 = new Pair<>(1, "apple");Pair<Integer, String> p2 = new Pair<>(2, "pear");boolean same = Util.<Integer, String>compare(p1, p2);

 

以上也就是简单的应用,那我们还会遇到什么情况呢,下面看一看关于通配符的问题,为了演示效果,我写了一个实际用处不大的方法。

     

public static void printSize(List<Object> list){ System.out.println(list.size()); } public static void main(String[] args) { List<Integer> list1 = new ArrayList<>(); List<String> list2 = new ArrayList<>(); printSize(list1); // 报错 }

 

上面我们已经知道,在集合中使用泛型可以使程序更加安全,易读,所以 Java 规范推荐我们使用泛型,那我们看一下上面这种情况,我该如何表示接收所有带有泛型的 List 集合呢。上面使用 LIst<Object> 是不行的。为什么不行?我们该使用何种方式接收参数呢?

 

首先来解释一下为什么不行,先看一下简单的区别。

 

Object obj = new Integer(1);ArrayList<Object> list = new ArrayList<Integer>(); // 报错

 

obj 可以赋值成功,是因为多态(往深了说是里氏替换原则),父类的引用指向了子类的实体,而下面的报错了,直观的理由说明,ArrayList<Object> 不是 ArrayList<Integer> 的父类。嗯,确实不是,为什么不是,一句话带过,是因为 Java 中正常的泛型是不变的,当然我们也可以使其改变。(不变,协变和逆变的概念可以自行百度)

传送门:https://www.cnblogs.com/keyi/p/6068921.html

那我就是想让方法接收所有的带泛型的集合该怎么办呢?这时候通配符就出现了,我们可以使用 List<?> 代表所有的带泛型的 List ,这样也就是说可以使用 List<?> 来指向所有的带泛型的 LIst 。

 

public static void printSize(List<?> list){ System.out.println(list.size()); } public static void main(String[] args) { List<Integer> list1 = new ArrayList<>(); List<String> list2 = new ArrayList<>(); printSize(list1); printSize(list2); }

 

好的,再次梳理一下逻辑,在 Java 中我们可以使用父类的引用指向子类的对象,而在泛型中,List<Object> 和 List<Integer> 不构成继承关系,原因是因为泛型是不可变的,然而我们又希望表示所有带有泛型的集合,这时就出现了 ?通配符。我们可以使用 List<?> 来引用其它带泛型的 List 。

 

实际的效果就是这样

 

List<Object> list1 = new ArrayList<Integer>(); // 报错List<?> list2 = new ArrayList<Integer>();

 

那好,现在要求升级了,我希望我的 List 集合不要什么都可以指向,下面就看一下一些有限制条件的修饰符该如何表示。

 

// 通用修饰符List<?> list1 = new ArrayList<Integer>();// <? extends T> 可用于表示 T 以及 T 的子类 List<? extends Number> list2 = new ArrayList<Number>();List<? extends Number> list3 = new ArrayList<Integer>();List<? extends Number> list4 = new ArrayList<String>(); // 报错// <? super T> 可用于表示 T 以及 T 的父类 List<? super Number> list5 = new ArrayList<Number>();List<? super Number> list6 = new ArrayList<Object>();List<? super Number> list7 = new ArrayList<Integer>(); //报错

 

对于上面的 <? extends Number> 和 <? super Number> 该如何选择呢 ?先说结论:” Producer Extends,Consumer Super ” 简称 PECS 原则。

“Producer Extends” – 如果你需要一个只读 List,用它来 produce T,那么使用< ? extends T > 。

“Consumer Super” – 如果你需要一个只写 List,用它来 consume T,那么使用< ? super T > 。

如果需要同时读取以及写入,那么我们就不能使用通配符了。

 

List<? extends Number> list = new ArrayList<Number>();List<? extends Number> list = new ArrayList<Integer>();List<? extends Number> list = new ArrayList<Double>();// 不论具体的实例化是什么,我们 get 元素之后都是父类 Number// 所以是 produce ,可以 get 得到很多的 TNumber number = list.get(0);list.add(new Integer(1)); // 报错

 

由于以上的三种实例化方式都是允许的,那么假如我现在想从 list 中 get 一个实例,因为 list 指向的实例可能是 Animal ,Dog,或 Cat 实例的集合。所以返回的值会统一为其父类。而在 add 值的时候就会存在问题,我不能确定添加的元素具体是哪一个,除了 null ,所以会报错。

 

同样的思路再来看< ? super T > 操作。

 

List<? super Integer> list = new ArrayList<Integer>();List<? super Integer> list = new ArrayList<Number>();List<? super Integer> list = new ArrayList<Object>();// 不同的实例化,我们 get 元素之后返回的值不确定// 或是 Integer, Number, Object ……list.get(0); // 添加数据的时候可以确定的添加是什么// 所以 super 对应只写入的情况,即 consume Tlist.add(new Integer(1));

 

关于泛型还要说明的是泛型是应用在编译时期的一项技术,而在运行期间是不存在泛型的。原因在于泛型类型擦除。为什么这么说,我们可以来看个示例

 

public static void main(String[] args) { List<Integer> list = new ArrayList<Integer>(); list.add(456); list.add("123");// 编译报错 } -------------------------------------------------- public static void main(String[] args) { List<String> l1 = new ArrayList<String>(); List<Integer> l2 = new ArrayList<Integer>(); System.out.println(l1.getClass()); System.out.println(l1.getClass().equals(l2.getClass())); } // class java.util.ArrayList // true

 

究其原因,在于 Java 中的泛型这一概念提出的目的,导致其只是作用于代码编译阶段,在编译过程中,正确检验泛型结果后,会将泛型的相关信息擦出,也就是说,成功编译过后的 class 文件中是不包含任何泛型信息的。泛型信息不会进入到运行时阶段。

 

在类型擦除之后,若是在代码中有相应的类型变量,遵循 " 保留上界 " 规则,会将相应的 T 替换成具体的类。

< ? > ---- > Object

< ? extends T > ---- > T

< ? super T > ----- > Object

补充说明一点,Java 中不允许直接创建泛型数组。

 

List<Integer>[] lists = new ArrayList<Integer>(); // 报错看以下演示代码// Not really allowed.List<String>[] lsa = new List<String>[10]; //1Object o = lsa;Object[] oa = (Object[]) o;List<Integer> li = new ArrayList<Integer>();li.add(new Integer(3));// Unsound, but passes run time store checkoa[1] = li;// Run-time error: ClassCastException.String s = lsa[1].get(0);

 

如果允许泛型数组的存在(第 1 处代码编译通过),那么在第 2 处代码就会报出 ClassCastException,因为 lsa[1] 是 List<Integer> 。Java 设计者本着首要保证类型安全(type-safety)的原则,不允许泛型数组的存在,使得编译期就可以检查到这类错误。

 

解决方案

 

List<?>[] lsa = new List<?>[10]; //1Object o = lsa;Object[] oa = (Object[]) o;List<Integer> li = new ArrayList<Integer>();li.add(new Integer(3));// Correct.oa[1] = li;// Run time error, but cast is explicit.String s = (String) lsa[1].get(0); //2

 

在第 1 处,用 ? 取代了确定的参数类型。根据通配符的定义以及 Java 类型擦除的保留上界原则,在 2 处 lsa[1].get(0) 取出的将会是 Object,所以需要程序员做一次显式的类型转换。

 

还有一种通过反射的方式来实现,使用 java.util.reflect.Array,可以不使用通配符,而达到泛型数组的效果。

 

List<String>[] lsa = (List<String>[])Array.newInstance(ArrayList.class, 4); //1Object o = lsa;Object[] oa = (Object[]) o;List<Integer> li = new ArrayList<Integer>();li.add(new Integer(3));// Correct.oa[1] = li;// Run time error, but cast is explicit.String s = lsa[1].get(0); //2

 

可以看到,利用 Array.newInstance() 生成了泛型数组,这里没有使用任何通配符,在第 2 处也没有做显式的类型转换,但是在第 1 处,仍然存在显式类型转换。

 

所以要想使用泛型数组,要求程序员必须执行一次显示的类型转换,也就是将类型检查的问题从编译器交给了程序员。但是呢,泛型的设计初衷就是编译器会帮助我们检查数据类型。你说矛盾不矛盾!

 

参考资料:

http://www.importnew.com/24029.html

https://blog.csdn.net/sunxianghuang/article/details/51982979

https://www.cnblogs.com/wxw7blog/p/7517343.html

https://www.cnblogs.com/keyi/p/6068921.html

https://blog.csdn.net/yi_Afly/article/details/52058708

Copyright © 2019 m.126bet.com All Rights Reserved
陈笑荣
地址:南湖东路139号中国人寿高层办公住宅楼1栋1单元18层1809号房
全国统一热线:15806586650