泛型

2020-10-14  本文已影响0人  Drew_MyINTYRE

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,?

对于不确定或者不关心实际要操作的类型,可以使用无限制通配符(尖括号里一个问号,即 ),表示可以持有任何类型。

int countLegs (List<? extends Animal > animals ) {}
类型擦除

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

如在代码中定义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 

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

<? 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 协变和逆变

reified

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

举个例子:

inline fun <reified T> Context.startActivity() {
    Intent(this, T::class.java).apply {
        startActivity(this)
    }
}

// how to use
context.startActivity<MainActivity>()

感谢:

上一篇 下一篇

猜你喜欢

热点阅读