Java泛型

Java泛型

泛型机制是在Java SE5.0新增的。

泛型的类型由专门的类型变量来限定。

按照泛型的应用场景,分为泛型类和泛型方法。

泛型类

泛型类是指定义类时使用了一个或多个泛型变量。

语法:

class Test<T>{...}

泛型方法

泛型方法是指在定义方法时使用了一个或多个泛型变量。

语法:

public <T,U> void f(){...}

泛型方法调用时,如果可以根据上下文自动推导出泛型变量的指,可以不用指定泛型变量。

泛型限定符

有时候需要限定类型变量的范围。例如需要设计一个泛型方法,对传入的两个参数比较大小(compareTo),这个时候如果不加以限定,传入的参数可能是不可比较的(没有实现Comparable接口),就会引起预料之外的错误。此时需要将类型限定在“实现了Comparable接口的类型”中。

如果有必要,可以同时限定多个范围的交集(例如:实现了Comparable接口,且必须是Number的子类)。

语法:

public <T extends Comparable & Number> int f(){...}

泛型与虚拟机

在虚拟机中,并没有定义泛型机制,泛型机制是在编译器层面定义的。

从编码到运行泛型代码通常需要经过一些编译器层面的处理。

类型擦除

对于虚拟机来说,它只认常规类型,不认泛型,因此在编译时,编译器会做类型擦除,将泛型类用其原始类型来表示。原始类型中的泛型变量用限定类型来替代。

  1. 如果没有限定符。限定类型是Object
  2. 如果有单个限定范围。限定类型是限定符之后的类型。
  3. 如果有多个限定范围。限定类型是限定符后第一个类型。

翻译泛型表达式

在运行泛型方法过程中,如果有表达式带有泛型定义的变量,编译器会自动对其插入强制类型转换语句。一般包括返回时、存取泛型变量时。

翻译泛型方法(桥方法)

编译器在编译泛型方法的时候,会对泛型方法进行类型擦除,而类型擦除之后,可能会和Java的 多态机制产生冲突。例如:

class T1<T> {
    public void f(T t) {
        System.out.println("T1:" + t);
    }
}

class T2 extends T1<Number> {
    public void f(Number t) {
        System.out.println("T2:" + t);
    }
}

对于T1,类型擦除之后剩下public void f(Object t);

对于T2,有public void f(Number t);(严格意义上是重载,而非重写)

​ 以及从T1继承来的public void f(Object t);

当通过T1类型的指针调用T2对象的f方法时,多态要求我们调用public void f(Object t);而泛型的实现要求我们调用public void f(Number t);,这时,多态机制和泛型机制发生了冲突。

在Java中,引入了桥方法机制,来解决这个冲突,在T2中,有两个方法public void f(Object t);public void f(Number t);。前一个是通过继承得来(并非T2重写)后一个泛型定义的,但是严格意义上来说他们只是重载而已,当触发多态机制时,根据多态机制,Java将调用public void f(Object t);方法,Java将这个通过继承得来的方法定义为桥方法,然后在这个桥方法中调用public void f(Number t);即:

@Override
public void f(Object t){
    f((Number)t);
}

这段代码是编译器生成的,并非用户自定义。

若是用户自己重写了该方法,那就不存在多态机制和泛型机制冲突了,也就不需要桥方法了。

因此,桥方法的合成仅仅是为了保持多态性。

泛型的局限性(几乎都是由类型擦除带来)

不支持基本类型

泛型的类型变量不能用int、double等,只能用Integer、Double等包装器对象,因为int匹配类型擦除后的原始类型。

运行时类型查询只适用于原始类型

//if(a instanceOf Pair<String>){...}//错误

if(a instanceOf Pair){...}//正确

因为类型擦除之后,泛型类的类型只有原始类型。

同理,getClass方法总是会返回原始类型。

不能创建参数化类型数组

T1<String> t1;//可以声明
//t1 = new T1<String>[];//不可以创建

可以声明这样的数组变量,但是不能创建。

原因是Java中数组会记住自己的元素类型,当存储元素时,会进行类型检查(编译期间)。对于参数化类型,由于类型擦擦,将会使得这一步无法有效进行,尽管编译时正确,但是运行时可能出错。例如:

T1 t1=new T1<String>[10];
t1[0]=new T1<Integer>();

这段代码无法编译,假如可以编译,那么当类型擦擦之后,t1会允许T1类型的变量存储,然而这是不符合数组规范的,后续如果将T1当作T1使用,可能就会引发一些运行时错误。

出于这个原因就,Java禁止创建参数化类型数组。

Varargs警告

上面说过,Java不能创建参数化类型数组,然而却可以声明。

当使用变参函数时,由于可变参数会自动转变成数组,倘若可变参数是参数化类型,那就会创建参数化类型数组。这是由编译器内部创建的,因此Java并没有禁止这个特性,而是会给出一个Varargs警告。

这个警告可以通过SafeVarargs注解消除。如下:

    //Varargs警告
    @SafeVarargs
    public static <T> void f7(T... values) {
        System.out.println("hello world");
    }

可以编译运行。

不能实例化类型变量

当用泛型的类型变量直接创建一个对象时,由于类型擦除,会直接进行原始类型创建,这显然不是我们想要的。因此,Java禁止实例化类型变量。如下:

T t;
//t = new T();//这句非法

解决方案:在外部,通过将T的构造方法(方法引用)传入,利用函数式接口进行创建。如下:

    //不能实例化类型变量
    public static <T> void f8(Supplier<T> supplier) {
        T t;
        //t = new T()非法
        t = supplier.get();//合法
    }

通过f8(String::new);这样调用。

不能创建泛型数组

由于类型擦擦,创建泛型数组其实是创建原始类型数组,如果数组仅仅是作为一个类的私有实例,外部无法直接访问,那么声明为原始类型不会有任何问题,只需要在使用的时候进行强制类型转换即可。(ArrayList正是采用这样的做法)。

然而,如果该数组需要对外提供访问权限,再采用这样的作法就会出问题。如下:

    //不能创建泛型数组
    public static <T> T[] f9() {
        //转换时编译器不会报错。
        return (T[]) new Object[10];
        /*
        * 类型擦除之后,new T[10]等价于new Object[10];
        */
    }

    public static void main(String[] args) {
        String[] strings = f9();
        strings[0]="hello";
        /*
        * 这里试图向Object类型数组存放String实例,运行时出错。
        */
    }

由于上述原因,Java禁止创建泛型数组。

解决方案:由调用者在外部将泛型数组的构造器传入,通过函数式接口创建。

    //不能创建泛型数组
    public static <T> T[] f9(Function<Integer, T[]> function) {
        //return (T[]) new Object[10];
        return function.apply(10);
    }

    public static void main(String[] args) {
        String[] strings = f9(String[]::new);
        strings[0] = "hello";
    }

成功运行。

不能在静态域、静态方法中使用泛型

静态域和静态方法无法使用泛型。

因为类型擦除之后,静态域中的所有类型变量都是同一个值(原始类型)。

其他

除上述问题之外,还有一些由于泛型的类型擦除机制引发的局限性,都是可以根据类型擦除进行简单分析的,既不常用,也不在此赘述。

泛型与继承

image-20200805091459995

  1. Class<T>Class<S>没有任何关系,就算T和S有继承关系也一样

  2. 继承时,会将类型变量一并继承。

    例如ClassA extends ClassB<String>,那么在ClassA中 类型变量也是String,不能是别的。

通配符

使用固定的类型参数,有诸多不便。

因此Java设计者发明了泛型的通配符机制。

对于泛型通配符,和泛型T有很重要的区别:在泛型T中,所有出现T的地方都代表了同一个类型,T f()、T g()会返回同样的类型;而在通配符中? extends Fruit f()、? extends Fruit g()可能会一个返回Apple,一个返回Banana,两者唯一的联系是有共同的上界

为了便于后面的理解,需要了解几条规则:

  1. 如上。T和?的区别。
  2. null是所有类的子类、Object类是所有类的超类。
  3. 向上类型转换是安全的,向下类型转换是不安全的。
  4. ?是一个具体的类型,但是不确定到底是哪个类型,而不是一个类型族。

上边界通配符

  1. 使用extends限定上边界。

  2. 无法使用多重限定

  3. 上边界通配符限定的变量不能写(可以写null)、只能读(读到上边界类型)

    这是因为,上面所说,在T item在通配符下,变为? extends Fruit,这是一个无法确定的具体类型,编译器知道这个类型可能是Fruit、也可能是Apple,因此无法赋值。

    假如编译器认为它是Fruit,于是给其赋值Fruit类型,但实际上它的类型是Apple,这样就会引发错误。

例如:

package com.ame;

class Plane<T> {
    public T getItem() {
        return item;
    }

    public void setItem(T item) {
        this.item = item;
    }

    private T item;
}

class Fruit {
    public String name = "fruit";
}

class Apple extends Fruit {
    public String name = "apple";
}

public class Test {
    //extends
    public static void f1() {
        Plane<Fruit> plane = new Plane<>();
        Plane<? extends Fruit> plane1 = plane;

        plane.setItem(new Apple());
        // plane1.setItem(new Apple());

        System.out.println(plane1.getItem().name);
    }

    public static void main(String[] args) {
        f1();
    }
}

下边界通配符

  1. 下边界通配符使用super进行限定。

  2. 下边界通配符无法多重限定。

  3. 下边界通配符可以写(下边界)、但是不可以读(读到的是Object类型)

    这是因为,通配符? super Apple虽然无法知道具体是哪个Apple的超类型,但是无论哪个都可以容纳Apple类型的值,所以可以赋值。

    读的时候,因为无法确定具体类型,所以读到的只能是Object类型。

package com.ame;

import com.sun.xml.internal.ws.addressing.WsaActionUtil;

class Plane<T> {
    public T getItem() {
        return item;
    }

    public void setItem(T item) {
        this.item = item;
    }

    private T item;
}

class Fruit {
    public String name = "fruit";
}

class Apple extends Fruit {
    public String name = "apple";
}

public class Test {
	//super
    public static void f2() {
        Plane<Fruit> plane = new Plane<>();
        Plane<? super Apple> plane1 = plane;

        plane1.setItem(new Apple());
        //这里会报错
        //plane1.setItem(new Fruit());
        
        //这里会报错
        //System.out.println(plane1.getItem().name);
        System.out.println(plane.getItem().name);
    }

    public static void main(String[] args) {
        f2();
    }
}

无边界通配符

没有边界限定的通配符,既不能写(可以写null),也不能读(可以读Object)

Reference

https://mp.weixin.qq.com/s/0_nbg-axlpngrunTa7mWzQ

posted @ 2020-08-06 08:21  燈心  阅读(147)  评论(0编辑  收藏  举报