总结java泛型相关知识点

泛型介绍

Java的泛型特性使得类型可以参数化,增强代码复用性,且可以实现编译前类型检查,增强类型安全。

但是Java的泛型是伪泛型,但是在编译之后的java字节码中具体的类型信息都会被擦除掉,JVM看到的java字节码中的实际类型其实都是泛型参数对应的最小超类或者边界类型(参考: Erasure of Generic TypesErasure of Generic Methods)。

List<String> list1=new ArrayList<String>();
list1.add("abc");
List<Integer> list2=new ArrayList<Integer>();
list2.add(123);
//返回结果true,编译之后String,Integer类型被擦除,getClass()获取到的类型都是List.class
System.out.println(list1.getClass()==list2.getClass());

List<Integer> list3=new ArrayList<Integer>();
list3.add(1);
//通过反射可以存储非Integer类型的数据,getClass()之后获取到的是List.class,所以可以存储Object数据
list3.getClass().getMethod("add", Object.class).invoke(list3, "abc");
for (int i=0;i<list3.size();i++) {
    System.out.println(list3.get(i));
}

泛型使用的各种限制:

Restrictions on Generics

类型擦除

  1. 类型擦除介绍
  1. 类型擦除的副作用及桥方法

泛型种类

泛型参数可以用在类、接口、方法上,分别成为泛型类、泛型接口、泛型方法

1. 泛型类

  • 类型参数放在类名称后面
  • 类型参数不可以实例化
  • 不能声明参数化类型的数组
Pair<String>[] pairList=new Pair<String>[10]; //报错:创建泛型数组

2. 泛型接口

  • 类型参数放在接口名称后面

3. 泛型方法

  • 类型参数放在方法签名中返回类型前面
public static void main(String[] args) {
    /**不指定泛型的时候,泛型变量的类型为该方法中的几种类型的同一个父类的最小级,直到Object*/  
    int i=Test2.add(1, 2); //这两个参数都是Integer,所以T为Integer类型  
    Number f=Test2.add(1, 1.2);//这两个参数一个是Integer,以风格是Float,所以取同一父类的最小级,为Number  
    Object o=Test2.add(1, "asd");//这两个参数一个是Integer,以风格是Float,所以取同一父类的最小级,为Object  

    /**指定泛型的时候,该方法中的几种类型必须是该泛型实例类型或者其子类*/  
    int a=Test2.<Integer>add(1, 2);//指定了Integer,所以只能为Integer类型或者其子类  
    int b=Test2.<Integer>add(1, 2.2);//编译错误,指定了Integer,不能为Float  
    Number c=Test2.<Number>add(1, 2.2); //指定为Number,所以可以为Integer和Float  
}
 
public static <T> T add(T x,T y){  
    return y;  
}
  • 泛型类中的静态方法和静态变量不可以使用泛型类所声明的泛型类型参数
public class Test2<T> {
    public static T one;   //编译错误
    public static  T show(T one){ //编译错误
        return null;    
    }  
}


public class Test2<T> {
    //这是正确的
    //因为这是一个泛型方法,在泛型方法中使用的T是自己在方法中定义的T,而不是泛型类中的T
    public static <T> T show(T one){
        return null;    
    }
}

泛型约束

对类型参数实现接口或者继承某个基类的约束方式:

  • 多个约束用&连接
  • 继承的基类必须放在第一个位置,接口放在后面
public static <T extends Object&Comparable&Serializable> T get(T t1,T t2) {
	if(t1.compareTo(t2)>=0){
        return t1;
    }
	return t2;  
}

通配符<?>

  1. 无边界的通配符(Unbounded Wildcards), 就是<?>, 比如List<?>
    无边界的通配符的主要作用就是让泛型能够接受未知类型的数据
  2. 固定上边界的通配符(Upper Bounded Wildcards),<? extends E>
    注意: 这里虽然用的是extends关键字, 却不仅限于继承了父类E的子类, 也可以代指显现了接口E的类
  3. 固定下边界的通配符(Lower Bounded Wildcards),<? super E>
    注意: 你可以为一个泛型指定上边界或下边界, 但是不能同时指定上下边界

<?> 等同于 <? extends Object>,例如List<?>List<? extends Object>一样,都无法添加元素(除去null)

public static void addTest(List<?> list) {
    Object o = new Object();
    // list.add(o); // 编译报错
    // list.add(1); // 编译报错
    // list.add("ABC"); // 编译报错
    list.add(null);

    Object firstElement = list.get(0);
    // String secondElement = list.get(1); //编译报错,List中存储的数据不确定是某种具体类型,所以只能用Object接收
}

PECS原则

生产者(Producer)使用extends,消费者(Consumer)使用super。

  • “Producer Extends” - If you need a List to produce values (you want to read s from the list), you need to declare it with <? extends T>, e.g. List<? extends Integer>. But you cannot add to this list.
  • “Consumer Super” - If you need a List to consume values (you want to write s into the list), you need to declare it with <? super T>, e.g. List<? super Integer>. But there are no guarantees what type of object you may read from this list.
  • If you need to both read from and write to a list, you need to declare it exactly with no wildcards, e.g. List.

经典示例,Collections.copy源码:

// Collections.java
public static <T> void copy(List<? super T> dest, List<? extends T> src) {
    int srcSize = src.size();
    if (srcSize > dest.size())
        throw new IndexOutOfBoundsException("Source does not fit in dest");

    if (srcSize < COPY_THRESHOLD ||
        (src instanceof RandomAccess && dest instanceof RandomAccess)) {
        for (int i=0; i<srcSize; i++)
            dest.set(i, src.get(i));
    } else {
        ListIterator<? super T> di=dest.listIterator();
        ListIterator<? extends T> si=src.listIterator();
        for (int i=0; i<srcSize; i++) {
            di.next();
            di.set(si.next());
        }
    }
}
//copy方法限制了拷贝源src必须是T或者是它的子类,而拷贝目的地dest必须是T或者是它的父类,这样就保证了类型的合法性。

示例:<? extends T>

public static void main(String[] args) {
    List<Integer> list1 = Arrays.asList(1, 2, 3, 4);
    System.out.println(sumOfList(list1));
    List<Double> list2 = Arrays.asList(1.1, 2.2, 3.3, 4.4);
    System.out.println(sumOfList(list2));
    List<Number> list3=Arrays.asList(1.1, 2.2, 3.3, 4.4);
    System.out.println(sumOfList(list3));
    List<Object> list4=Arrays.asList(1.1, 2.2, 3.3, 4.4);
    // System.out.println(sumOfList(list4)); //编译报错
}

public static double sumOfList(List<? extends Number> list) {
    double s = 0.0;
    for (Number n : list) {
        // 注意这里得到的n是其上边界类型的, 也就是Number, 需要将其转换为double.
        s += n.doubleValue();
    }
    return s;
}

示例:<? super T>

public static void main(String[] args) {
    List<Object> list1 = new ArrayList<>();
    addNumbers(list1);
    System.out.println(list1);
    List<Number> list2 = new ArrayList<>();
    addNumbers(list2);
    System.out.println(list2);
    List<Double> list3 = new ArrayList<>();
    // addNumbers(list3); // 编译报错
}

public static void addNumbers(List<? super Integer> list) {
    for (int i = 1; i <= 10; i++) {
        list.add(i);
    }
}

参考

The Java™ Tutorials – Generics

java 泛型详解

java泛型(二)、泛型的内部原理:类型擦除以及类型擦除带来的问题

Java泛型学习笔记 - (七)浅析泛型中通配符的使用