zoukankan      html  css  js  c++  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 得到很多的 T
    Number 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 T
    list.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];      //1
    Object o = lsa;
    Object[] oa = (Object[]) o;
    List<Integer> li = new ArrayList<Integer>();
    li.add(new Integer(3));
    // Unsound, but passes run time store check
    oa[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];                //1
    Object 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);     //1
    Object 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

  • 相关阅读:
    在linux系统上源码安装nginx前的准备
    linux上源码安装ftp
    CentOS-7.2网络配置
    linux安装nginx过程中出现的问题及解决办法
    ubuntu:安装httpd和nginx步骤和常见问题及解决办法
    APP性能(Android手机):帧率FPS
    APP性能(Android手机):APP启动时间
    APP性能(Android手机):流量
    常用网址
    navicat mysql与sqlserver数据互转
  • 原文地址:https://www.cnblogs.com/YJK923/p/9584020.html
Copyright © 2011-2022 走看看