(Generic programming)

什么是泛型

泛型,即“参数化类型”,也是类型参数,就是将类型由原来的具体的类型参数化,类似于方法中的变量参数,此时类型也定义成参数形式(可以称之为类型形参),然后在使用/调用时传入具体的类型(类型实参)。

为什么要使用泛型程序设计

泛型程序设计意味着编写的代码可以被很多不同类型的对象所重用。

(ArrayList 类就是一个泛型程序设计的实例)

  • 泛型提供了一个 类型参数, 类型参数可以使程序具有更好的可读性和安全性
  • 所有的强制转换都是自动和隐式的,提高代码的重用率
  1. //获取值时必须强制类型转换
  2. ArrayList files = new ArrayList();
  3. String filename = (String)files.get(0);
  4. //这里没有错误检查,可以向数组列表添加任何类的对象
  5. file.add(new File("..."));
  6. //解决方案,类型参数
  7. //类型参数指定为String类型
  8. ArrayList<String> files = new ArrayList<String>();
  9. //无须强制类型转换,编译器知道返回String类型而不是Object
  10. String filename = file.get(0);
  11. //编译器可以检查,避免插入错误类型的对象,更安全
  12. //can only add String objects to an ArrayList<String>
  13. file.add(new File("..."));

泛型的使用😁

泛型类型参数只能被类或接口类型赋值,不能被原生数据类型赋值,原生数据类型需要使用对应的包装类。

泛型类

一个泛型类就是具有一个或多个类型变量的类

一个简单的栗子

  1. /**
  2. *简单的泛型类,在类名后面引入类型变量T,用<>括起来
  3. */
  4. public class Pair<T>{
  5. //类定义中的类型变量指定方法的返回类型以及域和局部变量的类型
  6. private T first;
  7. private T second;
  8. public Pair(){
  9. first = null;
  10. second = null;
  11. }
  12. public Pair(T first,T second){
  13. this.first=first;
  14. this.second=second;
  15. }
  16. public T getFirst(){return first;}
  17. public T getSecond(){return second;}
  18. public void setFirst(T newValue){first=newValue;}
  19. public void setSecond(T newValue){second=newValue;}
  20. }

用具体的类型替换类型变量就可以实例化泛型类型,如:

  1. Pair<String>
  2. //可以将结果想象成带有构造器的普通类
  3. Pair<String>()
  4. Pair<String>(String,String)
  5. //和方法
  6. String getFirst();
  7. String getSecond();
  8. void setFirst(String);
  9. void setSecond(String);

换句话说,泛型类可以看作普通类的工厂

泛型类也可以接受多个类型参数

  1. public class test <K,V>{
  2. K key;
  3. V value;
  4. ...
  5. }

泛型方法

一个带有类型参数的方法

  1. //一个在普通类中定义的泛型方法
  2. class ArrayAlg{
  3. public static <T> T getMiddle(T... a){
  4. return a[a.length/2];
  5. }
  6. }

类型变量放在修饰符(public static)后面,返回类型前面

当调用一个泛型方法时,在方法名前的<>中放入具体类型

  1. //调用泛型方法
  2. String middle = ArrayAlg.<String>getMiddle("John","Q.","Public");

在大多数情况下,方法调用中可以省略类型参数。编译器有足够的信息能推断出所调用的方法。它用names的类型与泛型类型T[ ]进行匹配并推断出T一定是String,也就是说

  1. //这样调用也是可以的,编译器可以推断出String类型
  2. String middle = ArrayAlg.getMiddle("John","Q.","Public");

当然也有出现错误的情况

当泛型类和泛型方法共存时,泛型类中的类型参数与泛型方法中的类型参数是没有相应的联系的,泛型方法始终以自己定义的类型参数为准

类型变量的限定

泛型可以限定类型变量必须实现某几个接口或者继承某个类,多个限定类型用&分隔,类必须放在限定列表中所有接口的前面

绑定类型可以是类也可以是接口

又来一个举烂的栗子,计算数组中的最小元素:

  1. class ArrayAlg{
  2. public static <T>T min(T[] a){
  3. if(a==null || a.length==0)return null;
  4. T smallest = a[0];
  5. for(int i=1;i<a.length;i++){
  6. if(smallest.compareTo(a[i]) > 0)smallest = a[i];
  7. return smallest;
  8. }
  9. }
  10. }

方法内部有compareTo方法,但是smallest类型为T,它可以是任何一个类的对象。为保证它所属的类有compareTo方法,需要给T设置限定:

  1. //将T限制为实现了Comparable接口的类,保证有上述方法
  2. public static <T extends Comparable> T min(T[] a)...

泛型代码和虚拟机

类型擦除

无论何时定义一个泛型类型,都自动提供一个相应的原始类型。原始类型的名字就是删去类型参数后的泛型类型名。

泛型信息只存在于代码编译阶段,在进入 JVM 之前,与泛型相关的信息会被擦除掉,专业术语叫做类型擦除

擦除类型变量,并替换为限定类型(无限定的变量用Object)

  1. List<String> l1 = new ArrayList<String>();
  2. List<Integer> l2 = new ArrayList<Integer>();
  3. //打印结果是true 因为 List<String>和 List<Integer>在 jvm 中的 Class 都是 List.class
  4. System.out.println(l1.getClass() == l2.getClass);
  1. //可以通过反射调用方法add,存储字符串类型
  2. @Test
  3. public void test2() throws NoSuchMethodException, InvocationTargetException,IllegalAccessException {
  4. ArrayList<Integer> list = new ArrayList<Integer>();
  5. list.add(123);
  6. //list.add("ABC"); 报错
  7. //利用反射向list里添加了字符串
  8. list.getClass().getMethod("add",Object.class).invoke(list,"ABC");
  9. System.out.println(list.get(1));
  10. }

泛型参数会擦除到它的第一个边界,比如说上面的 Pair类,参数类型是一个单独的 T,那么就擦除到 Object,相当于所有出现 T 的地方都用 Object 替换。所以在 JVM 看来,保存的变量还是 Object 类型。之所以取出来自动就是我们传入的参数类型,这是因为编译器在编译生成的字节码文件中插入了类型转换的代码,不需要我们手动转型了。

原始类型

原始类型(raw type)就是擦除去了泛型信息,最后在字节码中的类型变量的真正类型。

翻译泛型表达式

当程序调用泛型方法时,如果擦除返回类型,编译器插入强制类型转换。

  1. //擦除getFirst的返回类型后将返回Object类型。编译器自动插入Employee的强制类型转换
  2. Pair<Employee> buddies = ...;
  3. Employee buddy = buddyes.getFirst();

翻译泛型方法

核心卷一P318

Java泛型转换的事实

  • 虚拟机中没有泛型,只有普通的类和方法
  • 所有的类型参数都用它们的限定类型替换
  • 桥方法被合成来保持多态
  • 为保持类型安全性,必要时插入强制类型转换

约束和局限性

  • 不能用基本类型实例化类型参数

例如擦除之后类含有Object类型的域,而Object不能存储double值。

  • 运行时类型查询只适用于原始类型

虚拟机中的对象总有一个特定的非泛型类型,因此所有的类型查询只产生原始类型。

  1. //测试a是否是任意类型的一个Pair
  2. if(a instanceof Pair<String>); //Error
  3. if(a instanceof Pair<T>); //Error
  4. Pair<String> p = (Pair<String>) a; //Warning:can only test that a is a pair

getClass方法总是返回原始类型

  • 不能创建参数化类型的数组
  1. Pair<String>[] table = new Pair<String>[10]; //Error
  2. //擦除之后,table的类型是Pair[],可以转换为Object
  3. Object[] objarray = table;
  4. //数组会记住它的元素类型,如果试图存储其他类型,会抛出ArrayStoreException
  5. objarray[0] = "Hello"; //Error-component type is Pair

不过对于泛型类型,擦除会使这种机制无效

  1. objarray[0] = new Pair<String>();
  2. //能够通过数组存储检查,不过仍会导致一个类型错误

因此,不允许创建参数化类型的数组。

  • Varargs警告

当向参数个数可变的方法传递一个泛型类型的实例时,会发出Varargs警告,可以用@SuppressWarnings(“unchecked”)注解或者@SafeVarargs标注方法来抑制警告

  1. public static <T> void addAll(Collection<T> coll, T ... ts){
  2. }
  3. Collection<Pair<String>> table;
  4. Pair<String> pair1;
  5. Pair<String> pair2;
  6. addAll(table,pair1,pair2);
  7. //为了调用这个方法,jvm必须建立一个Pair<String>数组,违反了前面的规则(不能创建参数化类型的数组)
  8. //抛出警告
  • 不能实例化类型变量

不能使用像new T(…),new T[…]或T.class这样的表达式中的类型变量

  1. public Pair(){
  2. first = new T();
  3. second = new T();
  4. }
  5. //例如这个Pair<T>构造器就是非法的,类型擦除后T变成Object
  • 不能构造泛型数组

就像不能实例化一个泛型实例一样,也不能实例化数组。

  • 泛型类的静态上下文中类型变量无效
  • 不能抛出或捕获泛型类的实例
  • 可以消除对受查异常的检查
  • 注意擦除后的冲突

泛型类型的继承规则

  • 无论S与T有什么联系,通常,Pair与Pair没有什么联系
  1. class Father{}
  2. class Son extends Father{}
  3. Father father = new Son();
  4. //在普通类中,父类可以用来指向子类
  5. //但是在泛型类中不行,泛型擦除后,Pair<T>之间没有继承关系
  6. Pair<Son> bb = new Pair<>();
  7. Pair<Father> cc = bb; //Error
  • 永远可以将参数化类型转换为一个原始类型
  1. List<String> a2 = new ArrayList<>();
  2. List a3 = a2;
  3. //可以通过编译,但是后面使用方法时可能会产生类型错误
  4. //这时候a3对象是原始类型,所以add(Object object);
  5. a3.add(123);
  6. //是对a3进行操作,但是最终结果保存到了a2中,将一个Integer装入String中,错误!
  • 泛型类可以扩展或实现其他的泛型类
  1. //泛型接口
  2. interface List1<E>{}
  3. //实现了泛型接口的泛型类
  4. class List2<T,E> implements List1<E>{}
  5. //泛型类
  6. class List3<T>{}
  7. //继承了其他泛型类的泛型类
  8. class List4<T,E> extends List3<E>{}
  9. //因为List2实现了List1
  10. List1<Father> b1 = new List2<Son,Father>();
  11. //List4继承了List3,所以List3是父类,可以指向子类对象。
  12. List3<Father> b2 = new List4<Son,Father>();

虽然这样也完成了泛型类的继承,实现了和普通类一样的多态,但是使用起来并不是特别好,所以Java引入了通配符概念。

通配符类型

通配符概念

为了解决类型被限制死了不能动态根据实例来确定的缺点,引入了“通配符泛型”。

  1. Pair<? extends Employee>;
  2. //表示任何泛型Pair类型,它的类型参数是Employee的子类,如Pair<Manager>,但不是String

固定边界通配符

  • 固定上边界的通配符的泛型,能够接受指定类及其子类类型的数据,采用<? extends E>的形式声明,E就是该泛型的上边界。(也可代指实现了该接口E的类)。

禁止向List<? extends E>中添加任何对象,可以添加null

  • 固定下边界的通配符的泛型,能够接受指定类及其父类类型的数据,采用<? super E>的形式声明,E就是该泛型的下边界。
  1. //水果是苹果的父类,实现了向上转型
  2. Plate<? extends Fruit> plate = new Plate<Apple>();

extends通配符的缺陷

虽然通过这种方式,Java 支持了 Java 泛型的向上转型,但是这种方式是有缺陷的,那就是:其无法向 Plate 中添加任何对象,只能从中读取对象。

  1. Plate<? extends Fruit> plate = new Plate<Apple>();
  2. plate.add(new Apple()); //Compile Error
  3. plate.get(); // Compile Success

因为在我们还未具体运行时,JVM并不知道我们要往里放的到底是什么,所以干脆什么都不给放,避免出错。

super通配符的缺陷

  1. Plate<? super Apple> plate = new Plate<Fruit>();

当然了,下面的声明肯定也是对的,因为 Object 是任何一个类的父级。

  1. Plate<? super Apple> plate = new Plate<Object>();

既然这样,也就是说 plate 指向的具体类型可以是任何 Apple 的父级,JVM 在编译的时候肯定无法判断具体是哪个类型。但 JVM 能确定的是,任何 Apple 的子类都可以转为 Apple 类型,但任何 Apple 的父类都无法转为 Apple 类型。

所以对于使用了 super 通配符的情况,我们只能存入 T 类型及 T 类型的子类对象。

  1. Plate<? super Apple> plate = new Plate<Fruit>();
  2. plate.add(new Apple());
  3. plate.add(new Fruit()); //Error

当我们向 plate 存入 Apple 对象时,编译正常。但是存入 Fruit 对象,就会报编译错误。

而当我们取出数据的时候,也是类似的道理。JVM 在编译的时候知道,我们具体的运行时类型可以是任何 Apple 的父级,那么为了安全起见,我们就用一个最顶层的父级来指向取出的数据,这样就可以避免发生强制类型转换异常了。

  1. Object object = plate.get();
  2. Apple apple = plate.get(); //Error
  3. Fruit fruit = plate.get(); //Error

从上面的代码可以知道,当使用 Apple 类型或 Fruit 类型的变量指向 plate 取出的对象,会出现编译错误。而使用 Object 类型的额变量指向 plate 取出的对象,则可以正常通过。

总结:

  • 对于 extends 通配符,我们无法向其中加入任何对象,但是我们可以进行正常的取出。
  • 对于 super 通配符,我们可以存入 T 类型对象或 T 类型的子类对象,但是我们取出的时候只能用 Object 类变量指向取出的对象。

无限定通配符

1、当方法是使用原始的Object类型作为参数时,如下:

  1. public static void printList(List<Object> list) {
  2. for (Object elem : list)
  3. System.out.println(elem + "");
  4. System.out.println();
  5. }

可以选择改为如下实现:

  1. public static void printList(List<?> list) {
  2. for (Object elem: list)
  3. System.out.print(elem + "");
  4. System.out.println();
  5. }

这样就可以兼容更多的输出,而不单纯是List

  1. List<Integer> li = Arrays.asList(1, 2, 3);
  2. List<String> ls = Arrays.asList("one", "two", "three");
  3. printList(li);
  4. printList(ls);

2、在定义的方法体的业务逻辑与泛型类型无关,如List.size,List.cleat。实际上,最常用的就是Class<?>,因为Class并没有依赖于T。

最后提醒一下的就是,List

通配符捕获

  1. //交换成对元素方法
  2. public static void swap(Pair<?> p);
  3. //通配符不是类型变量,不能使用?作为一种类型
  4. ? t = p.getFirst(); //Error
  5. //解决方法,写一个辅助方法
  6. public static <T> void swapHelper(Pair<T> p){
  7. T t = p.getFirst();
  8. p.setFirst(p.getSecond());
  9. p.setSecond(t);
  10. }
  11. //现在swapHelper是一个泛型方法,而swap不是
  12. public static void swap(Pair<?> p){swapHelper(p);}
  13. //这种情况下,swapHelper方法的参数T捕获通配符。
  14. //它不知道是哪种类型的通配符,但是这是一个明确的类型,并且<T>swapHelper的定义只有在T指出类型时才有明确的含义

通配符捕获只有在有许多限制的情况下才是合法的。编译器必须能够确信通配符表达的是单个、确定的类型。

反射和泛型