细说Java 泛型

3,293 阅读10分钟

为了让集合容器记住元素类型是jdk1.5引入泛型(Generic Types)的一个主要原因。

泛型看起来就是将实际的类型参数化,这样就可以在使用的时候传人实际的类型,或者推断其代表的类型(如ArrayList)。但从本质上讲jvm并不认识ArrayList这种类型,它只是java的语法糖,即只在源码层面的表现,在编译后jvm加载时就只是ArrayList而已。

1.为什么引入泛型

先看一个例子:

List list = new ArrayList();
list.add(100);
list.add("100");

// 第一个元素就是int类型,OK
System.out.println((int)list.get(0) + 1);
// 第二个元素实际为String,因此会引发ClassCastException
System.out.println((int)list.get(1) + 1);

在引入泛型之前,list的元素类型固定为Object,所以可以添加任意类型的元素进去,编译不会有问题,但取出来时需要从Object转成实际的类型才有意义,这样就容易引发运行时类型转换异常,尤其在循环或作为方法参数多次传递后更难以分清起真实类型。

从实际使用角度来看,我们更希望一个容器存储相同类型或同一类(包括子类)的元素。通过泛型的编译时检查则可以帮助我们避免不小心把其他类型的元素加进来。java的泛型是一种语法糖,其采用的方式是类型擦除,所以java泛型是一种伪泛型,这么做也是为了兼容旧版本。

2.泛型类,泛型接口

我们可以在接口,类上声明类型行参而将其泛型话:

public interface Collection<E> extends Iterable<E> {
  boolean add(E e);
  ...
}

public class HashMap<K,V> extends AbstractMap<K,V>
    implements Map<K,V>, Cloneable, Serializable {
    public V put(K key, V value) {
    	//...
    }
}    

带泛型的类在派生子类的时候需要传入实际的类型,或者不带泛型:

class Base<T> {}

// 错误
class Sub extends Base<T> {}

// Ok
class Sub extends Base<String> {}
// Ok
class Sub extends Base {}
// Ok
class Sub<T> extends Base<T> {}

通过extends为泛型指定边界:

class Base<T extends Comparable & Serializable & Cloneable> {}
class Base<T extends ArrayList & Comparable & Serializable & Cloneable> {}

T被限定为实现指定的类或接口。可以指定多个接口,但只能指定一个类且类必须为第一个。在编译时T的类型会被替换为extends后的第一个类或接口类型。

  • 基类劫持接口

    abstract class Animal implements Comparable<Animal> {}
    
    class Dog extends Animal implements Comparable<Dog> {
    		/** 无论CompareTo参数是Dog还是Animal,都不行 */
        @Override
        public int compareTo(Dog o) {
            return 0;
        }
    }
    

    Dog实现了Comparable,泛型参数是Dog,但不巧其基类Animal也实现了Comparable接口并且传人了一个不同的泛型参数Animal,导致compareTo参数类型冲突,这种现象被称为基类劫持了接口。

3.泛型方法

使用泛型的另一种场景是泛型方法,如果在接口或类上没有定义泛型参数,但想在方法中使用泛型,则可以像下面这样定义一个泛型方法:

public static <T> Set<T> synchronizedSet(Set<T> s) {
    return new SynchronizedSet<>(s);
}

// 明确传人泛型参数类型
Collections.<String>synchronizedSet(new HashSet<>());
// 隐式使用,由编译器推导实际类型
Collections.synchronizedSet(new HashSet<String>());

4.类型通配符

假设有个统计列表中数字(<100)出现频率的方法:

public static Map<Number, Long> count(List<Number> list) {
    return list.stream()
            .filter(n -> n.intValue() < 100)
            .collect(Collectors.groupingBy(l -> l, Collectors.counting()));
}

期望可以像接受任何数字的列表:

List<Integer> numsA = Arrays.asList(1, 2, 3, 100, 200, 300);
// 错误
Map<Number, Long> countA = count(numsA);
       
List<Double> numsB = Arrays.asList(1D, 2D, 3.55D, 100D, 200D, 330D);
// 错误
Map<Number, Long> countB = count(numsB);

上面代码会报错,List<Integer>,List<Double>不是List<Number>的子类型。把方法参数改成count(List<Object> list)也不行,它们也不是List<Object>的子类型,就算运行时传进去的都是Object的List。因为如果这样的话,传人一个子类的List,但是试图把它的元素转成另一个子类时就会有问题。

这种编译时检查虽然增加的程序的安全性,但降低了编码的灵活性,如果有多种类型需要统计,我们不得不为每一种类型编写一份count方法,还有就是count方法不能重载,在一个类中可能写出countInt,countDouble...这样的代码。

4.1 通配符

为了解决上述问题,我们可以使用通配符:

// list的元素可以是任意类型
public static Map<Number, Long> count(List<?> list) {
    return list.stream()
        .map(n -> (Number)n)
        .filter(n -> n.intValue() < 100)
        .collect(Collectors.groupingBy(l -> l, Collectors.counting()));
}

?就是通配符,代表任意类型。这样就可以接收任何类型的List了,大大提高了灵活性,代码也很简洁,但安全性缺又降低了,试想有人传了一个List<String> s = Arrays.asList("1", "2", "3", "4", "5");进去会发生什么?

4.2 通配符上界

继续上面的问题,我们真实的需求并不是传人任意类型,而是任意Number的子类。这时可以对通配符做进一步的限制:

public static Map<Number, Long> count(List<? extends Number> list) {
    return list.stream()
        .filter(n -> n.intValue() < 100)
        .collect(Collectors.groupingBy(l -> l, Collectors.counting()));
}

<? extends Number>指定了传人的列表元素必须是Number及其子类,即?所代表类型的上界是Number,通配符上界同样可以用在类或接口泛型定义上。

在count方法中依然不能通过list.add(1);添加一个Number或其子类元素进去,WHY?

4.3 通配符下界
List<? super Number> list = new ArrayList<>();
list.add(Integer.valueOf(1));//ok
list.add(Long.valueOf(2L));//ok
// 因为只指定下届,所以元素类型为Object
Object object = list.get(0);

<? super Number>表明List的元素类型是Number及其基类,即?的下限是Number,通配符下界同样可以用在类或接口泛型定义上。 为什么通配符上界可以添加Number的子类进去呢?

其实不难理解,由List<? super Number>可知,List中的元素必是Number或其基类,Integer,Long等是Number的子类,必然也是Number的父类的子类。A是B的子类,B是C的子类,A必然是C的子类。所以根据LSP这是可行的。

4.4 逆变与协变

逆变: 当某个类型A可以由其子类B替换,则A是支持协变的。

协变: 当某个类型A可以由其基类B替换,则A是支持逆变的。

由前面我们知道既不能List<Number> list = new ArrayList<Integer>();,也不能List<Integer> list = new ArrayList<Number>();,因为Java泛型设计为不可变的(数组除外)。

但我们可以通过通配符实现逆变与协变:

// 协变
List<? extends Number> list = new ArrayList<Integer>();
// 逆变
List<? super Integer> list = new ArrayList<Number>();

另一个例子:

class Animal {}
class Pet extends Animal {}
class Cat extends Pet {}

static class Person<T extends Animal> {
    T pet;
}

// 协变
Person<? extends Pet> lily = new Person<Cat>();
// error
lily.pet = new Cat();
// 逆变
Person<? super Pet> alien = new Person<Animal>();
// ok
alien.pet = new Cat();
  • 泛型参数相同的时候,在泛型类上是支持协变的,如ArrayList<String> -> List<String> -> Collection<String>
  • 泛型参数使用通配符的时候,即在泛型类自身上支持协变,又可在泛型参数类型上支持协变,如Collection<? extends Number>,子类型可以是List<? extends Number>,Set<? extends Number>,又可以是Collection<Integer>Collection<Long>,通过传递可以知道HashSet<Long>Collection<? extends Number>的子类型。
  • 包含多个泛型类型参数,对每个类型参数分别适用上面的规则,HashMap<String, Long>Map<? extends CharSequence, ? extends Number>的子类型。
4.5 PECS

应该在什么时候用通配符上界,什么时候用通配符下界呢?《Effective Java》提出了PECS(producer-extends, consumer-super),即一个对象产生泛型数据时用extends,一个对象接收(消费)泛型数据时,用super。

/** 
 * Collections #copy方法
 * src产生了copy需要的泛型数据,用extens
 * dest消费了copy产生的泛型数据,用super
 */
public static <T> void copy(List<? super T> dest, List<? extends T> src)
4.6 通配符与泛型方法

用泛型方法实现之前的count方法:

/** 与之前通配符实现相同功能,同时在方法中可以添加新元素 */
public static <T extends Number> Map<T, Long> count(List<T> list) {
    return list.stream()
        .filter(n -> n.intValue() < 100)
        .collect(Collectors.groupingBy(l -> l, Collectors.counting()));
}

再来一个🌰,假设有个工具类方法,实现将一个非空的数字添加到传人的列表中:

public static void safeAdd(List<? extends Number> list, Number num) {
    if (num == null) {
        return;
    }

  	//error,虽然使用通配符限定了泛型的范围,但具体类型仍是不确定的
    list.add(num);
}

//将其替换为:
public static <T extends Number> void safeAdd(List<T> list, T num) {
    if (num == null) {
        return;
    }

  	//ok,不过num是什么类型,它都和list元素是同一类型
    list.add(num);
}

总结:

  • 当方法中不需要改变容器时,用通配符,否则用泛型方法
  • 当方法其他参数,返回值与泛型参数具有依赖关系,使用泛型方法

5.类型擦除(type erasure)

上面所说泛型参数都是java在语法层面的规范定义,是面向编译器的,在jvm中运行时并不存在泛型,类型被擦除了,所有泛型类型都被替换成Object或者通配符上界类型,如果是容器类型如List则变成List。

ArrayList<Integer> listA = new ArrayList<>();
ArrayList<String> listB = new ArrayList<>();

// listA和listB运行时的类型都是java.util.ArrayList.class, 返回true
System.out.println(listA.getClass() == listB.getClass());

由于类型擦除的原因,不能在静态变量,静态方法,静态初始化块中使用泛型,也不能使用obj instanceof java.util.ArrayList<String>判断泛型类,接口中定义的泛型。

6.通过反射获取泛型信息

存在泛型擦除的原因,运行时是无法获取类上的泛型信息的。但对于类的field,类的method上的泛型信息,在编译器编译时,将它们存储到了class文件常量池中(确切是Signature Attrbute),所以可以通过反射获取field,method的泛型信息。

在java.lang.reflect中提供Type(Type是java中所有类型的父接口,class就实现了Type)及其几个子接口用来获取相关泛型信息,以List为例:

TypeVariable: 代表类型变量,E

ParameterizedType: 代表类型参数,如List,参数为String

WildcardType: 通配符类型,如List<?>,List<? extends Number>中的?, ? extends Number

GenericArrayType: 泛型数组,如List[],它的基本类型又是一个ParameterizedType List<java.lang.Integer>

具体API可以看javadoc,一个简单演示:

public class GenericCls<T> {

    private T data;

    private List<String> list;

    private List<Integer>[] array;

    public <T> List<String> strings(List<T> data) {
        return Arrays.asList(data.toString());
    }

    public static void main(String[] args) throws NoSuchFieldException, NoSuchMethodException {
        Class<GenericCls> cls = GenericCls.class;

        System.out.println("============== class - GenericCls ==============\n");
        TypeVariable<Class<GenericCls>> classTypeVariable = cls.getTypeParameters()[0];
        System.out.println(classTypeVariable.getName());

        Field field = cls.getDeclaredField("list");
        Type genericType = field.getGenericType();
        ParameterizedType pType = (ParameterizedType) genericType;
        System.out.println("============== filed - list ==============\n");
        System.out.println("type: " + genericType.getTypeName());
        System.out.println("rawType: " + pType.getRawType());
        System.out.println("actualType: " + pType.getActualTypeArguments()[0]);

        Method method = cls.getDeclaredMethod("strings", List.class);
        Type genericParameterType = method.getGenericParameterTypes()[0];
        ParameterizedType pMethodType = (ParameterizedType) genericParameterType;
        System.out.println("============== method - strings parameter ==============\n");
        System.out.println("type: " + genericParameterType.getTypeName());
        System.out.println("rawType: " + pMethodType.getRawType());
        System.out.println("actualType: " + pMethodType.getActualTypeArguments()[0]);

        Field array = cls.getDeclaredField("array");
        GenericArrayType arrayType = (GenericArrayType) array.getGenericType();
        System.out.println("============== filed - array ==============\n");
        System.out.println("array type: " + arrayType.getTypeName());
        ParameterizedType arrayParamType = (ParameterizedType) arrayType.getGenericComponentType();
        System.out.println("type: " + arrayParamType.getTypeName());
        System.out.println("rawType: " + arrayParamType.getRawType());
        System.out.println("actualType: " + arrayParamType.getActualTypeArguments()[0]);

    }
}

关于反射与泛型我会在另外的文章中再详细介绍

7.泛型与数组

java数组是协变的:Pet[] pets = new Cat[10];,但却无法创建泛型的数组,可以创建不带泛型的数组然后强转,也可以声明泛型数组的引用。

Person<Pet>[] people = new Person<Pet>[10];//error
Person<Pet>[] people = new Person[10];//ok
Person<Pet>[] people = (Person<Pet>[])new Person[10];//ok
public static void consume(Person<? extends Pet>[] people){}//ok

问题:为什么异常类不能使用泛型?


下期预告:详解class(字节码)文件

欢迎关注我的个人微信博客