细说Java 泛型

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

泛型看起来就是将实际的类型参数化,这样就能够在使用的时候传人实际的类型,或者推断其表明的类型(如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泛型是一种伪泛型,这么作也是为了兼容旧版本。jvm

2.泛型类,泛型接口

咱们能够在接口,类上声明类型行参而将其泛型话:ide

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后的第一个类或接口类型。spa

  • 基类劫持接口设计

    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(字节码)文件

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

相关文章
相关标签/搜索