Java 中的泛型

Java基础

浏览数:357

2019-3-25

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