泛型

作者: Drew_MyINTYRE | 来源:发表于2020-10-14 09:27 被阅读0次

    Java 泛型(generics)

    Java 泛型(generics)是 JDK 5 中引入的一个新特性, 就是为了解决类型转换的问题,泛型提供了编译时类型安全检测机制,并且所有的强制转换都是自动和隐式的,在运行时丢弃了一些类型实参的信息,对于内存占用也会减少很多。

    例如:
    List<Float>、List<String>、List<Student>在JVM运行时Float、String、Student都被替换成Object类型,如果是泛型定义是List<T extends Student>那么运行时T被替换成Student类型,具体可以通过反射Erasure类可看出。

    泛型中通配符

    1,常用的 T,E,K,V,?

    • ?表示不确定的 java 类型
    对于不确定或者不关心实际要操作的类型,可以使用无限制通配符(尖括号里一个问号,即 ),表示可以持有任何类型。
    
    int countLegs (List<? extends Animal > animals ) {}
    
    • T (type) 表示具体的一个java类型
    • K V (key value) 分别代表java键值中的Key Value
    • E (element) 代表Element
    类型擦除

    定义:使用泛型的时候加上类型参数,在编译器编译的时候会去掉,这个过程成为类型擦除。

    如在代码中定义List<Object>和List<String>等类型,在编译后都会变成List,JVM看到的只是List,而由泛型附加的类型信息对JVM是看不到
    
    public class Test {
        public static void main(String[] args) {
            ArrayList<String> list1 = new ArrayList<String>();
            list1.add("abc");
            ArrayList<Integer> list2 = new ArrayList<Integer>();
            list2.add(123);
            System.out.println(list1.getClass() == list2.getClass());
        }
    }
    
    打印结果为true,说明泛型类型String和Integer都被擦除掉了,只剩下原始类型。
    
    //通过反射添加其它类型元素
    public class Test {
        public static void main(String[] args) throws Exception {
            ArrayList<Integer> list = new ArrayList<Integer>();
            list.add(1);  //这样调用 add 方法只能存储整形,因为泛型类型的实例为 Integer
            list.getClass().getMethod("add", Object.class).invoke(list, "David");
            for (int i = 0; i < list.size(); i++) {
                System.out.println(list.get(i));
            }
        }
    }
    
    我们利用反射调用add()方法的时候,却可以存储字符串,这说明了Integer泛型实例在编译之后被擦除掉了,只保留了原始类型。
    
    

    原始类型 就是擦除去了泛型信息,最后在字节码中的类型变量的真正类型。

    我们也可以明白ArrayList<Integer>被擦除类型后,原始类型也变为Object,所以通过反射我们就可以存储字符串了。

    比如ArrayList中,如果不指定泛型,那么这个ArrayList可以存储任意的对象。
    
    public static void main(String[] args) {  
        ArrayList list = new ArrayList();  
        list.add(1);  
        list.add("David");  
        list.add(new Date());  
    }  
    
    

    看下面这个例子:

    public class Test {  
        public static void main(String[] args) {  
    
            //list1引用能完成泛型类型的安全检查
           ArrayList<String> list1 = new ArrayList();  
            list1.add("1"); //编译通过  
            list1.add(1); //编译错误  
            String str1 = list1.get(0); //返回类型就是String  
    
            //list2没有使用泛型,没有做类型安全检查
            ArrayList list2 = new ArrayList<String>();  
            list2.add("1"); //编译通过  
            list2.add(1); //编译通过  
            Object object = list2.get(0); //返回类型就是Object  
    
            new ArrayList<String>().add("11"); //编译通过  
            new ArrayList<String>().add(22); //编译错误  
    
            String str2 = new ArrayList<String>().get(0); //返回类型就是String  
        }  
    
    }  
    

    泛型类中的泛型参数的实例化是在定义对象的时候指定的,而静态变量和静态方法不需要使用对象来调用。对象都没有创建,如何确定这个泛型参数是何种类型,所以当然是错误的。

    public class Test2<T> {    
        public static T one;   //编译错误    
        public static  T show (T one) { //编译错误    
            return null;    
        }    
    }
    
    //但是要注意区分下面的一种情况:
    //这是一个泛型方法,在泛型方法中使用的T是自己在方法中定义的 T,而不是泛型类中的T。
    public class Test2<T> {    
        public static <T >T show (T one) { //这是正确的    
            return null;    
        }    
    }
    

    泛型类型变量不能是基本数据类型

    没有ArrayList<double>,只有ArrayList<Double>。因为当类型擦除后,ArrayList的原始类型变为Object,但是Object类型不能存储double值,只能引用Double的值。

    Java 协变和逆变

    解决的就是这类问题:
    Integer 是 Number 的子类型,问你 List<Integer> 是不是 List<Number> 的子类型?

    数组支持协变

    // objects 这个句柄只能存储 Integer
     Object objects[] = new Integer[20];
     objects[0] = "David";
    
    Exception in thread "main" java.lang.ArrayStoreException: java.lang.String
            at Main.main(Main.java:13)
    

    集合不支持协变,编译不通过

    // smell code
     ArrayList<Object> list = new ArrayList<String>();
    
    // 协变
     ArrayList< ? extends  Number> list= new ArrayList<Integer>();
    
    // 逆变
    ArrayList< ? super Integer> list2 = new ArrayList<Number>();
    list.add(1); //error 
    

    变型的种类具体分为三种:协变型 & 逆变型 & 不变型

    • 协变型(covariant): 子类型关系被保留

    • 逆变型(contravariant): 子类型关系被翻转

    • 不变型(invariant): 子类型关系被消除

    <? extends T> 上界通配符

    代表的是 T 及其子类

    要想类型参数支持协变,需要使用上界通配,但是这会引入一个编译时限制:就是只能访问不能修改(非严格)

    List<? extends Number> l1;
    List<Integer> l2 = new ArrayList<>();
    l1 = l2; // OK
    

    <? super T> 下界通配符

    表示只能为 T 及其父类

    要想类型参数支持逆变,需要使用下界通配符,同样,这也会引入一个编译时限制,只能修改不能访问(非严格)

    // ArrayList.java
    public E get(int index) {
        ...
    }
    
    Integer i = l1.get(0); // compiler error
    

    <?> 无界通配符

    List<?> l1;
    List<Integer> l2 = new ArrayList<>();
    l1 = l2; // OK
    

    泛型代码的设计,应遵循PECS原则(Producer extends Consumer super):
    如果只需要获取元素,使用 <? extends T>
    如果只需要修改,使用<? super T>

    举例:

    // Collections.java
    public static  void copy(List<? extends T> src, List<? super T> dest) {
    }
    

    Kotlin 协变和逆变

    • in 关键字对应 ? super

    • out 关键字对应 ? extends

    reified

    使用类型实参的确切类型代替类型实参,必须搭配 inline 使用,注意无法从 Java 代码里调用带实化类型参数的内联函数。

    举个例子:

    inline fun <reified T> Context.startActivity() {
        Intent(this, T::class.java).apply {
            startActivity(this)
        }
    }
    
    // how to use
    context.startActivity<MainActivity>()
    

    感谢:

    相关文章

      网友评论

          本文标题:泛型

          本文链接:https://www.haomeiwen.com/subject/jagmpktx.html