泛型

Posted 有心有梦

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了泛型相关的知识,希望对你有一定的参考价值。

泛型的概述

​ 泛型程序设计意味着编写的代码可以被很多不同类型的对象所重用。比如,我们并不想为了聚集String和Integer对象分别设计不同的类,一个ArrayList<T>类就可以聚集任何类型的对象。使用泛型机制编写的程序代码要比那些杂乱地使用Object变量,然后再进行强制类型转换的代码具有更好的安全性和可读性。

泛型的本质是为了参数化类型(在不创建新的类型的情况下,通过泛型指定的不同类型来控制形参具体限制的类型)。也就是说在泛型使用过程中,操作的数据类型被指定为一个参数,这种参数类型可以用在类、接口和方法中,分别被称为泛型类、泛型接口、泛型方法。

泛型的使用

1.泛型类

一个泛型类就是具有一个或者多个类型变量的类。泛型类的基本写法为:

class 类名<类型变量:T,U...>{
	private T var1;
	public T get(){}
}

类型变量使用大写形式,且比较短。类定义中的类型变量指定方法的返回类型以及域和局部变量的类型。再Java中一般使用变量E标识集合的元素类型;K和V分别表示表的关键字与值得类型。T表示”任意类型“。

泛型类示例:

public class Pair<T> {

    private T first;
    private T second;

    public Pair(){
        first = null;
        second = null;
    }

    public Pair(T first,T second){
        this.first = first;
        this.second = second;
    }
	// 虽然在方法中使用了泛型,但是这并不是一个泛型方法。
    public T getFirst(){
        return first;
    }

    public T getSecond(){
        return second;
    }

    public void setFirst(T newValue){
        this.first = newValue;
    }

    public void setSecond(T newValue){
        this.second = newValue;
    }
}

注意:对于泛型类,在创建泛型类实例的时候,并不一定要传入泛型类型实参。在使用泛型的时候如果传入泛型实参,则会根据传入的泛型实参做相应的限制,此时泛型才会起到本应起到的限制作用。如果不传入泛型类型实参的话,在泛型类中使用泛型的方法或成员变量定义的类型可以为任何的类型。

public static void main(String[] args) {
    Pair pair1 = new Pair(11,22);
    Pair pair2 = new Pair("11","22");
    Pair pair3 = new Pair(true,true);
    System.out.println(pair1.getFirst());
    System.out.println(pair2.getFirst());
    System.out.println(pair3.getFirst());
    
    /*
    11
    11
    true
     */
}

2.泛型方法

声明泛型方法时,类型变量的声明放在方法修饰符(public static)和方法返回类型的中间,用"<>"括起来。泛型方法可以定义在普通类中,也可以定义在泛型类中。泛型方法,是在调用方法的时候指明泛型的具体类型

基本用法

public  <T> T getMiddle(T... a){
    return a[a.length / 2];
}

public static void main(String[] args) {
    ArrayAlg arrayAlg = new ArrayAlg();
    arrayAlg.<Integer>getMiddle(1,2);
    Integer middle = arrayAlg.getMiddle(1, 3, 0);
    Number middle1 = arrayAlg.getMiddle(1, 2.2);
}

调用一个泛型方法的时候,在方法名前的尖括号放入具体的类型。如上面的main方法中的第二行代码,此时就只能向泛型方法中传入指定类型变量的参数。

当然编译器也能够推断出传入的参数类型,比如第三行代码中,并没有指定类型变量的类型,但是依然可以根据传入的参数推断出类型变量的实参类型为Integer。

不过,看最后一行代码中对泛型方法的调用,此时传入的是一个整型数据和一个浮点型数据,此时程序也能够正常运行。简单地说,编译器将会自动打包参数为1个Integer对象、1个Double对象,然后寻找这些类的共同超类型。此时二者的超类型就是Number接口。

泛型类中的泛型方法

public class GenericTest01<T> {

    /**
     * 在泛型类中声明了一个泛型方法,
     * 使用泛型T,注意这个T是一种全新的类型,
     * 可以与泛型类中声明的T不是同一种类型。
     * @param t
     * @param <T>
     */
    public <T> void show(T t){
        System.out.println(t.toString());
    }

    /**
     * 在泛型类中声明了一个泛型方法,
     * 使用泛型E,这种泛型E可以为任意类型。
     * 可以类型与T相同,也可以不同。
     * @param e
     * @param <E>
     */
    public <E> void show2(E e){
        System.out.println(e.toString());
    }

    /**
     * 泛型类中的普通方法,T的类型与泛型类中的T的类型保持一致
     * @param t
     */
    public void show3(T t){
        System.out.println(t.toString());
    }
}

测试:

public static void main(String[] args) {
    
    // 指定泛型类的类型变量的实参为String
    GenericTest01<String> stringGenericTest01 = new GenericTest01<>();
    
    // 分别调用三个方法,全部运行成功
    stringGenericTest01.show(123);
    stringGenericTest01.show2(\'c\');
    stringGenericTest01.show3("fym");
}

可以看出,泛型类中的泛型方法不受泛型类的类型变量的影响,可以独立于泛型类而产生变化。但是泛型类中的普通方法,如果使用了泛型类的类型变量作为方法的返回值类型或者参数类型,则必须和泛型类的类型变量保持一致,否则无法编译。

静态方法与泛型

静态方法有一种情况需要注意一下,那就是在类中的静态方法使用泛型:静态方法无法访问类上定义的泛型;如果静态方法操作的引用数据类型不确定的时候,必须要将泛型定义在方法上。

即:如果静态方法要使用泛型的话,必须将静态方法也定义成泛型方法

public class StaticGenerator<T> {
    ....
    ....
    /**
     * 如果在类中定义使用泛型的静态方法,需要添加额外的泛型声明(将这个方法定义成泛型方法)
     * 即使静态方法要使用泛型类中已经声明过的泛型也不可以。
     * 如:public static void show(T t){..},此时编译器会提示错误信息:
          "StaticGenerator cannot be refrenced from static context"
     */
    public static <T> void show(T t){

    }
}

3.泛型接口

泛型接口与泛型类的定义及使用基本相同。泛型接口常被用在各种类的生产器中,可以看一个例子:

//定义一个泛型接口
public interface Generator<T> {
    public T next();
}

当实现泛型接口的类,未传入泛型实参时,与泛型类的定义相同,在声明类的时候,需将泛型的声明也一起加到类中:

class FruitGenerator<T> implements Generator<T>{
    ……
}

如果不声明泛型,如:class FruitGenerator implements Generator,编译器会报错:"Unknown class"

当实现泛型接口的类,传入泛型实参时,则所有使用泛型的地方都要替换成传入的实参类型:

public class FruitGenerator implements Generator<String> {
    @Override
    public String next() {
        Random rand = new Random();
        return fruits[rand.nextInt(3)];
    }
}

定义一个生产器实现这个接口,虽然我们只创建了一个泛型接口Generator<T>, 但是我们可以为T传入无数个实参,形成无数种类型的Generator接口。

泛型通配符

泛型类型的继承规则

在了解通配符之前,先来了解一下泛型类型的继承规则。

对于两个普通的类S和T,无论S与T之间存在什么联系,通常情况下,ArrayList<S>与ArrayList<T>之间是没有联系的。如下所示:

 public void showValue(GenericTest<Number> obj){
        System.out.println(obj.toString());
    }

 public static void main(String[] args) {
        GenericTest02 genericTest02 = new GenericTest02();

        GenericTest<Number> numberGenericTest = new GenericTest<>();
        GenericTest<Integer> integerGenericTest = new GenericTest<>();
        
        genericTest02.showValue(numberGenericTest);
//        genericTest02.showValue(integerGenericTest);

 }

Integer虽然是Number的子类,但是GenericTest<Number>并非是GenericTest<Integer>的父类,由此可见同一种泛型可以对应多个版本(因为参数类型是不确定的),不同版本的泛型类实例是不兼容的

但是,泛型类可以扩展或者实现其他的泛型类,就这点而言,与普通的泛型类无异。比如ArrayList<S>类实现了List<S>接口,这就意味着,一个ArrayList<Number>可以被转换为一个List<Number>。但是,一个ArrayList<Integer>并不可以与List<Number>兼容。

通配符的子类型限定

为了解决上面的问题,通配符被提了出来。比如:

GenericTest<? extends Number>

表示任何泛型GenericTest类型,它的类型参数是Number的子类,比如GenericTest<Integer>,但不能是GenericTest<String>。对于<? extends 类型>这种拥有限定的通配符泛型,可以称之为泛型类的”协变“。

将上面的showValue方法的类型参数修改为通配符类型,再次进行测试:

public void showValue(GenericTest<? extends Number> obj){
        System.out.println(obj.toString());
}

public static void main(String[] args) {
        GenericTest02 genericTest02 = new GenericTest02();
        GenericTest<Number> numberGenericTest = new GenericTest<>();
        GenericTest<Integer> integerGenericTest = new GenericTest<>();
        GenericTest<String> SintegerGenericTest = new GenericTest<>();

        genericTest02.showValue(numberGenericTest);
        genericTest02.showValue(integerGenericTest);
//        genericTest02.showValue(SintegerGenericTest);

 }

GenericTest<Integer>与GenericTest<Number>对象全都可以都可以正常传入到showValue方法中,并执行。GenericTest<Number>此时是GenericTest<Integer>的父类。

这就是通配符的子类型限定:

? extends 类型

使用子类型限定通配符定义的泛型变量进行引用时,被引用的泛型对象的泛型类型,只能是指定类型本身或其子类。

例如

  • 类型的变量,只能引用泛型为Number及其Number子类的泛型对象。
  • 类型的变量,只允许引用泛型为实现Comparable接口的实现类的泛型对象。

注意

  • 通配符只允许出现在引用变量声明中,比如普通变量引用、形参等的声明。一般是用作<? extends 具体类型>或者<? super 具体类型>
  • 通配符不允许出现在泛型定义中(泛型类、泛型接口、泛型方法的< >里),class one<? extends Integer> {}这样是不允许的。
  • 类定义时,继承泛型类时的< >里也不可以出现。
  • 在泛型类或泛型方法的代码体{ }里还有泛型方法的形参上,配合占位符,甚至可以使用? extends T或者? super T这种形式来进行引用变量的声明。
  • 在new泛型类的时候也不可以使用通配符,比如new ArrayList<?>()
  • 泛型方法的显式类型说明也不可以使用通配符。
  • 对于使用通配符引用的泛型对象来说,只能对其进行”读“操作,而不能对其进行写操作,否则代码无法通过编译。如下代码示例:

image-20210522214141223

其中Pair类的代码为:

public class Pair<T> {
    private T first;
    private T second;
    public Pair(){
        first = null;
        second = null;
    }
    public Pair(T first,T second){
        this.first = first;
        this.second = second;
    }
    public T getFirst(){
        return first;
    }
    public T getSecond(){
        return second;
    }
    public void setFirst(T newValue){
        this.first = newValue;
    }
    public void setSecond(T newValue){
        this.second = newValue;
    }
}

对于pair3,它内部的setFirst方法大致是这样的:

public void setFirst(? extends Father newValue)
public ? extends Father getFirst()

这样将不能调用setFirst方法,因为编译器只知道需要某个Father子类型,但是不知道具体是什么类型,所以它拒绝传递任何特定的类型,比较"?"不能用来匹配。

而对于getFirst方法就不存在这个问题,将getFirst的返回值赋值给一个Father的引用完全合法。

通配符的超限定类型限定

通配符限定还可以指定一个超类型限定:

? super 类型

这个通配符限制为指定类型的所有超类型。对于使用超限定类型的泛型类型引用变量,其只能引用其指定类型及其指定类型的所有超类的泛型对象,而不能引用指定类型的子类泛型对象。例如<? super Number>,只允许引用泛型为Number及Number父类的泛型对象。

带有超类型限定的通配符的行为与普通的通配符限定的行为正好相反,可以为方法提供参数,但是不能使用返回值。

直观地讲就是,带有超类型限定的通配符可以向泛型对象写入。但是只能向其中写入指定类型的对象或者指定类的子类对象。另外,如果对这个泛型对象进行读取的话,那不能保证返回对象的类型,只能把它赋予给一个Object类型的引用。下面看一个案例:

image-20210522221454879

另外,需要注意的是,对于使用超限定类型的泛型类型引用变量,其只能引用其指定类型及其指定类型的所有超类的泛型对象,而不能引用指定类型的子类泛型对象。如下图所示,Father为Son的父类,Son为SonSon的子类。

image-20210522222336756

无限定通配符

<?>

允许引用所有类型的泛型对象。比如List<?>、Map<?,?>,是List<String>,List<Object>等各种泛型List的父类。

读取List<?>的对象list中的元素时,永远是安全的,因为不管list的真实类型是什么,它包含的都是Object。

写入list中的元素时,不行。因为我们不知道添加元素的元素类型,我们不能向其中添加对象。唯一的例外是,可以向泛型对象中添加null值,因为它是所有类型的成员。

案例实操

1.子类型限定通配符,代码注释部分都是编译不成功的。

public static void main(String[] args) {

        ArrayList<? extends Father> af1 = new ArrayList<Father>();
        ArrayList<? extends Father> af2 = new ArrayList<Son>();
//        ArrayList<? extends Father> af3 = new ArrayList<Object>();

//        af1.add(new Father());
//        af1.add(new Son());
        Father father = af1.get(0);
        Father father1 = af2.get(0);
}

这组代码中,三个af体现的是ArrayList<? extends Father>的协变作用,这种引用可以接受的ArrayList的类型参数的具体类型只能是Father本身及其子类。

在代码中,由于af1的引用类型为ArrayList<? extends Father>,这种情况下在读写操作中,唯一能安全进行的就只有读操作了(读操作指返回值类型是类型参数E),因为你把? extends Father类型的返回值赋值给引用something super Father肯定是合法的。所以在代码里,可以且仅可以把get函数返回值赋值给Father或者Father父类的引用。
在代码中,af1是不可以进行写操作的(af1.add(new Son());),因为读取到的数据的类型是? extends Apple,类型没有下限,编译器根本不知道该给形参传什么类型的实参才对,所以禁止了这种行为。
2.超限定类型

    public static void main(String[] args) {
        ArrayList<? super Son> as1 = new ArrayList<Son>();
        ArrayList<? super Son> as2 = new ArrayList<Father>();
//        ArrayList<? super Son> as3 = new ArrayList<SonSon>();

        as1.add(new SonSon());
        as1.add(new Son());
//        as1.add(new Father());

//        as2.add(new Father());
        Object object = as1.get(0);
//        Son s = as1.get(0);   获取数据时,只能赋值给一个Object类型的变量,而不能返回给一个特定类型的变量
 }

在这组代码中,三个as变量体现的是ArrayList<? super Son>的逆变作用,这种引用可以接受的ArrayList的类型参数的具体类型只能是Son本身及其所有超类。

在代码中,as1的引用类型为ArrayList<? super Son>,这种情况下,在读写操作中,唯一安全的只有写操作了,这里的写操作指的是泛型类中那些形参类型为类型参数的设置值的方法,因为你把something extends Son类型的对象赋值给形参? super Son肯定是合法的。所以在代码里,可以且仅可以add类型为Son或者Son子类的对象。

在这组代码中,本来as1是不可以进行读操作的,因为读取到的数据的类型是? super Son,类型没有上限你根本不知道该赋值为什么类型,但由于java所有对象都是Object的子类,导致? super Son也拥有了上限。所以Object o1 = as1.get(0)可以获得到一个Object引用,但这样的操作也没有什么意义。

3.无限定类型

public static void main(String[] args) {
        ArrayList<?> a1 = new ArrayList<Father>();
        ArrayList<?> a2 = new ArrayList<Son>();
        ArrayList<?> a3 = new ArrayList<SonSon>();
        ArrayList<?> a4 = new ArrayList<String>();

//        a4.add("a");
        a4.add(null);  // 无限定通配符可以写入null
        a1.add(null);

        Object o = a4.get(0); // 获取数据时,只能赋值给一个Object类型的变量,而不能返回给一个特定类型的变量

}

ArrayList<?>相当于List<? extends Object>,分析过程同上。

分析读写操作哪个是合法的,实际上应该关注方法的形参或返回值的类型是否为泛型类型参数,我们可以从引用的类型反向推测出来:

  • 引用类型为List<? super Apple>,那么合法行为只能是something extends Apple类型赋值给? super Apple,要做到这样的事,只能是方法的形参类型是类型参数E。因为这样使用者就可以把实参控制为something extends Apple,传递给形参类型为? super Apple的方法以实参(此时E被? super Apple代替)。而形参类型为类型参数的方法,一般都是“写操作”。引用类型为List<? extends Apple>,那么合法行为只能是? extends Apple类型赋值给something super Apple,要做到这样的事,只能是方法的返回值类型是类型参数E。因为这样使用者就可以把函数返回赋值的引用控制为something super Apple,然后用这个引用来接返回值类型为? extends Apple的返回值(此时E被? extends Apple代替)。而返回值类型为类型参数的方法,一般都是“读操作”。

  • 引用类型为List<? extends Apple>,那么合法行为只能是? extends Apple类型赋值给something super Apple,要做到这样的事,只能是方法的返回值类型是类型参数E。因为这样使用者就可以把函数返回赋值的引用控制为something super Apple,然后用这个引用来接返回值类型为? extends Apple的返回值(此时E被? extends Apple代替)。而返回值类型为类型参数的方法,一般都是“读操作”。

image-20210522233056854

上图就完美表达了最后说的这两点,箭头代表了赋值关系,左边是返回值赋值给引用,右边是实参赋值给形参。而something则是客户端程序员来控制的。这两个箭头即表达了两种情况的合法操作,左边箭头代表了读操作,右边箭头代表了写操作。

泛型擦除

Java虚拟机中没有泛型类型对象,其中的所有对象都属于普通类。无论何时定义一个泛型类型,都自动提供了一个相应的原始类型。原始类型的名字就是删去类型参数后的泛型类型名字。泛型擦除就是程序在编译期间会去掉类型变量,并替换为限定类型,如果没有限定类型则替换为Object。如果一个泛型类中有多个限定类型,那么就用从左到右顺序的第一个限定的类型来替换代码体中的类型变量。

如下面的这个泛型类,

public class Pair<T> {

    private T first;
    private T second;

    public Pair(){
        first = null;
        second = null;
    }

    public Pair(T first,T second){
        this.first = first;
        this.second = second;
    }

    public T getFirst(){
        return first;
    }

    public T getSecond(){
        return second;
    }

    public void setFirst(T newValue){
        this.first = newValue;
    }

    public void setSecond(T newValue){
        this.second = newValue;
    }

}

经过泛型擦除以后,变为:

public class Pair {

    private Object first;
    private Object second;

    public Pair(){
        first = null;
        second = null;
    }

    public Pair(Object first,Object second){
        this.first = first;
        this.second = second;
    }

    public Object getFirst(){
        return first;
    }

    public Object getSecond(){
        return second;
    }

    public void setFirst(Object newValue){
        this.first = newValue;
    }

    public void setSecond(Object newValue){
        this.second = newValue;
    }

}

翻译泛型表达式

当我们在程序中调用泛型类中方法时,如果该方法的返回值类型被擦除掉了或者是方法的形参类型被擦除掉了,编译器会向字节码中插入强制类型转换。比如:

Pair<Integer> p1 = new Pair<>();
Integer i = p1.getFirst();

调用这个方法时,编译器将这行代码翻译为两条虚拟机指令:

​ 1、对原始方法getFirst的调用;

​ 2、将返回的Object类型强制转换为Integer类型。

当存取一个泛型域时也要插入强制类型转换。

翻译泛型方法

类型擦除也会出现在泛型方法中。比如下面这个泛型方法:

public static <T extends Comparable> T min(T[] a)

经过类型擦除以后,变为:

public static Comparable min(Comparable[] a)

不过方法的擦除可能会造成多态与类型擦除之间的冲突。比如这里有一个继承了Pair泛型类的类:

public class IntegerPair extends Pair<Integer>{
    
    @Override
    public void setSecond(Integer newValue) {
        super.setSecond(newValue);
    }
}

它指定了泛型参数的具体类型为Integer,然后重写了含有泛型参数的setSecond方法。在编译后,泛型参数被擦除了,此时该类变为:

public class IntegerPair extends Pair{
    
    @Override
    public void setSecond(Integer newValue) {
        super.setSecond(newValue);
    }
}

而在Pair类中还有一个setSecond方法:

public void setSecond(Object newValue)

此时如果按照重写的原则来判断的话,那IntegerPair类实现的setSecond方法不符合重写的原则,因为Java中方法重写的原则是:方法的方法名、参数类型、返回值类型必须与父类的那个被重写的方法保持一致。此时类型擦除以后,二者没有保持一致。所以为了解决这个问题,编译器就会在IntegerPair类中生成一个”桥“方法:

public void setSecond(Object newValue){
	setSecond((Integer)newValue)
}

不过此时要强调的一点是,在Java中,判断两个方法是否是同一个方法,是根据方法的签名来决定的。而在我们编写程序时,方法签名=方法名 + 参数;在JVM中,方法签名=方法名+参数+返回值。

所以如果IntegerPair类还重写了getSecond方法,然后编译器又生成了一个桥方法,此时有:

public Integer getFirst()
public Object getFirst()

如果按照我们编写代码的原则,这是不行的,因为只有返回值类型的差别是无法任务两个方法是不一样的;而这是在虚拟机中,JVM的方法签名中含有方法的返回值类型这一项,所以此时认为这两个方法不一样。

总之,关于Java泛型转换,需要记住以下几点:

  • 虚拟机中没有泛型,只有普通的类和方法;
  • 所有的类型参数都用它们的限定类型替换
  • 桥方法被合成来保持多态度;
  • 为了保持类型安全性,必要时插入强制类型转换。

参考:

Java泛型 通配符详解_anlian523的博客-CSDN博客_泛型通配符

以上是关于泛型的主要内容,如果未能解决你的问题,请参考以下文章

作业09-集合与泛型

Java泛型:类型擦除

201621044079 韩烨作业09-集合与泛型

201621123062《java程序设计》第九周作业总结

什么意思 在HashMap之前 ? Java中的泛型[重复]

201621123037 《Java程序设计》第9周学习总结