为了账号安全,请及时绑定邮箱和手机立即绑定

Java 中的泛型

标签:
Java

先来看一下以下 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

原文出处:https://www.cnblogs.com/YJK923/p/9584020.html

点击查看更多内容
TA 点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
  • 推荐
  • 评论
  • 收藏
  • 共同学习,写下你的评论
感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消