java泛型

java泛型

原文链接

泛型的基本概念

泛型是 Java SE 1.5 的新特性,泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数。这种参数类型可以用在类、接口和方法的创建中,分别称为 泛型类 、泛型接口、泛型方法。 Java 语言 引入泛型的好处是安全简单。在 Java SE 1.5 之前,没有泛型的 情况 的下,通过对类型 Object 的引用来实现参数的 “ 任意化 ” , “ 任意化 ” 带来的缺点是要做显式的 强制类型转换 ,而这种转换是要求开发者对 实际参数 类型可以预知的情况下进行的。对于强制类型转换错误的情况, 编译器 可能不提示错误,在运行的时候才出现异常,这是一个安全隐患。泛型的好处是在编译的时候检查 类型安全 ,并且所有的 强制转换 都是自动和 隐式 的,以提高代码的重用率。

泛型要解决的问题

  1. 集合类型元素在运行期出现类型装换异常,增加编译时类型的检查
    @Test
    public void test01() {
        List<Object> list = Lists.newArrayList();
        list.add("1");
        list.add("abc");
        list.add("12L");

        //Integer a =(Integer) list.get(0);
        String b =(String) list.get(1);
        Long c =(Long) list.get(2);
    }
java.lang.ClassCastException: java.lang.String cannot be cast to java.lang.Integer

	at com.example.test.controller.GenericsTest.test01(GenericsTest.java:36)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)

  1. 解决重复代码的编写,能够复用算法

如 ArrayList 的 Add 方法可以使用在任何类型上或限定的类型上。

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

    注:(Java的泛型是伪泛型,这是因为Java在编译期间,所有的泛型信息都会被擦掉,正确理解泛型概念的首要前提是理解类型擦除。Java的泛型基本上都是在编译器这个层次上实现的,在生成的字节码中是不包含泛型中的类型信息的,使用泛型的时候加上类型参数,在编译器编译的时候会去掉,这个过程成为类型擦除。

    如在代码中定义List<Object>List<String>等类型,在编译后都会变成List,JVM看到的只是List,而由泛型附加的类型信息对JVM是看不到的。Java编译器会在编译时尽可能的发现可能出错的地方,但是仍然无法在运行时刻出现的类型转换异常的情况,类型擦除也是Java的泛型与C++模板机制实现方式之间的重要区别)

        /**
         * 类型擦除举例
         * 例1.原始类型相等
         */
        @Test
        public void test02() {
    
            ArrayList<String> list1 = new ArrayList<String>();
            list1.add("abc");
    
            ArrayList<Integer> list2 = new ArrayList<Integer>();
            list2.add(123);
    
            System.out.println(list1.getClass() == list2.getClass());
        }
    
        /**
         * 例2.通过反射添加其它类型元素
         *
         * @throws NoSuchMethodException
         * @throws InvocationTargetException
         * @throws IllegalAccessException
         */
        @Test
        public void test03() throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
    
            ArrayList<Integer> list = new ArrayList<Integer>();
    
            list.add(1);  //这样调用 add 方法只能存储整形,因为泛型类型的实例为 Integer
    
            list.getClass().getMethod("add", Object.class).invoke(list, "asd");
    
            for (int i = 0; i < list.size(); i++) {
                System.out.println(list.get(i));
            }
    
        }
    
    

泛型类、泛型接口

类和接口上

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为泛型指定边界:

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

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

泛型方法

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

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

类型通配符

   @Test
    public  void  test4(){
        List<Integer> list = new ArrayList<>();
        list.add(1);
        list.add(2);
        list.add(3);
//        list.add("1");
//        list.add("2");
//        list.add("3");
        System.out.println(count(list));
    }
    // list的元素可以是任意类型  string 的时候会类型转换异常
    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()));
    }

通配符上界

//通过通配符限制传入的参数
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()));
}

通配符下界

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

逆变与协变

逆变: 当某个类型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>的子类型。

PECS

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

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

通配符与泛型方法

用泛型方法实现之前的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);
}

总结:

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

类型擦除

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>判断泛型类,接口中定义的泛型。

通过反射获取泛型信息

存在泛型擦除的原因,运行时是无法获取类上的泛型信息的。但对于类的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]);

    }
}

posted @ 2021-04-20 11:08  ThomasChang0109  阅读(81)  评论(0)    收藏  举报