天天看点

泛型的常见问题及概念

为什么会引入泛型?

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

场景一:数字求和

常用的数据类型会有很多种,例如int,double等等,那使用泛型的逻辑写一个公共的求和方法怎么实现呢?

package com.springtest.generic;

/**
 * 测试泛型使用
 *
 * @author huzhibo
 * @version [1.0, 10:20 2021/08/03]
 */
public class GenericNumberUtils {
    /**
     * 计算两数之和
     * @param t1 数字1
     * @param t2 数字2
     * @param <T> 两个数的类型
     * @return 和值
     */
    public static <T extends Number> double add(T t1,T t2){
        return t1.doubleValue() + t2.doubleValue();
    }
}

           

写一个测试类,测试该方法

package com.springtest.generic;

import junit.framework.TestCase;
import org.junit.Test;

/**
 * 泛型测试
 *
 * @author huzhibo
 * @version [1.0, 10:24 2021/08/03]
 */
public class GenericNumberUtilsTest {

    @Test
    public void addTest(){
        Integer i1 = 10;
        Integer i2 = 20;
        System.out.println(GenericNumberUtils.add(i1, i2));
    }
    @Test
    public void addTest2(){
        double i1 = 10;
        double i2 = 20;
        System.out.println(GenericNumberUtils.add(i1, i2));
    }
    @Test
    public void addTest3(){
        Integer i1 = 10;
        double i2 = 20;
        System.out.println(GenericNumberUtils.add(i1, i2));
    }


}
           

打印的结果为:

30.0
30.0
30.0
           

泛型类及泛型接口

单元泛型

package com.springtest.generic;

import lombok.Data;

/**
 * 泛型类
 *
 * @author huzhibo
 * @version [1.0, 10:48 2021/08/03]
 */
@Data
public class Point<T> {
    private T t;
}

           

多元泛型

package com.springtest.generic;

import lombok.Data;

/**
 * 多元泛型类
 * @author huzhibo
 * @version [1.0, 10:52 2021/08/03]
 */
@Data
public class MultipleGeneric<T,K> {

    private T t;
    private K k;
}

           

泛型接口

package com.springtest.generic;

/**
 * 泛型接口
 *
 * @author huzhibo
 * @version [1.0, 10:57 2021/08/03]
 */
public interface GenericInterface<T> {
    T getT();
}

package com.springtest.generic;


/**
 * 泛型接口实现类
 *
 * @author huzhibo
 * @version [1.0, 10:57 2021/08/03]
 */
public class GenericInterfaceImpl<T> implements GenericInterface<T>{
    private T t;

    @Override
    public T getT() {
        return this.t;
    }
    public void setT(T t){
        this.t = t;
    }
    public GenericInterfaceImpl(T t){
        this.setT(t);
    }
}

           

测试类如下:

@Test
    public void classTest(){
        Point<Integer> integerPoint = new Point<>();
        integerPoint.setT(10);
        Integer t = integerPoint.getT();
        System.out.println(t);
    }
    @Test
    public void multipleGenericTest(){
        MultipleGeneric<String, Integer> multipleGeneric = new MultipleGeneric<>();
        multipleGeneric.setT("测试");
        multipleGeneric.setK(20);
        System.out.println(multipleGeneric);
    }
    @Test
    public void genericInterfaceTest(){
        GenericInterface<String> genericInterface = new GenericInterfaceImpl<String>("测试");
        System.out.println(genericInterface.getT());
    }
           

泛型上下限

<?> 无限制通配符
<? extends E> extends 关键字声明了类型的上界,表示参数化的类型可能是所指定的类型,或者是此类型的子类
<? super E> super 关键字声明了类型的下界,表示参数化的类型可能是指定的类型,或者是此类型的父类

// 使用原则《Effictive Java》
// 为了获得最大限度的灵活性,要在表示 生产者或者消费者 的输入参数上使用通配符,使用的规则就是:生产者有上限、消费者有下限
1. 如果参数化类型表示一个 T 的生产者,使用 < ? extends T>;
2. 如果它表示一个 T 的消费者,就使用 < ? super T>;
3. 如果既是生产又是消费,那使用通配符就没什么意义了,因为你需要的是精确的参数类型。

           

示例一就是上限的一种使用。

下限的使用如下

public static void fun(Point<? super String> temp){    // 只能接收String或Object类型的泛型,String类的父类只有Object类
        System.out.print(temp + ", ") ;
    }
    
    
      @Test
    public void superTest(){
        Point<String> stringPoint = new Point<>();
        stringPoint.setT("测试");
        GenericNumberUtils.fun(stringPoint);

        Point<Object> objectPoint = new Point<>();
        objectPoint.setT(new Object());
        GenericNumberUtils.fun(objectPoint);
    }
           

多个泛型条件也可以使用&符号 如:<T extends Staff & Passenger>

泛型擦除

消除类型参数声明,即删除

<>

及其包围的部分。

根据类型参数的上下界推断

并替换所有的类型参数为原生态类型:如果类型参数是无限制通配符或没有上下界限定则替换为Object,如果存在上下界限定则根据子类替换原则取类型参数的最左边限定类型(即父类)。

为了保证类型安全,必要时插入强制类型转换代码。

自动产生“桥接方法”以保证擦除类型后的代码仍然具有泛型的“多态性”。

  • 擦除类定义中的类型参数 - 无限制类型擦除

当类定义中的类型参数没有任何限制时,在类型擦除中直接被替换为Object,即形如

<T>

<?>

的类型参数都被替换为Object。

  • 擦除类定义中的类型参数 - 有限制类型擦除

当类定义中的类型参数存在限制(上下界)时,在类型擦除中替换为类型参数的上界或者下界,比如形如

<T extends Number>

<? extends Number>

的类型参数被替换为

Number

<? super Number>

被替换为Object。

  • 擦除方法定义中的类型参数

擦除方法定义中的类型参数原则和擦除类定义中的类型参数是一样的,这里仅以擦除方法定义中的有限制类型参数为例。

常见问题场景

Oracle官网提供的一个例子:

List<String>[] lsa = new List<String>[10]; // Not really allowed.
Object o = lsa;
Object[] oa = (Object[]) o;
List<Integer> li = new ArrayList<Integer>();
li.add(new Integer(3));
oa[1] = li; // Unsound, but passes run time store check
String s = lsa[1].get(0); // Run-time error ClassCastException.

           

由于 JVM 泛型的擦除机制,所以上面代码可以给

oa[1]

赋值为 ArrayList 也不会出现异常,但是在取出数据的时候却要做一次类型转换,所以就会出现

ClassCastException

,如果可以进行泛型数组的声明则上面说的这种情况在编译期不会出现任何警告和错误,只有在运行时才会出错,但是泛型的出现就是为了消灭

ClassCastException