(Generic programming)
什么是泛型
泛型,即“参数化类型”,也是类型参数,就是将类型由原来的具体的类型参数化,类似于方法中的变量参数,此时类型也定义成参数形式(可以称之为类型形参),然后在使用/调用时传入具体的类型(类型实参)。
为什么要使用泛型程序设计
泛型程序设计意味着编写的代码可以被很多不同类型的对象所重用。
(ArrayList 类就是一个泛型程序设计的实例)
- 泛型提供了一个 类型参数, 类型参数可以使程序具有更好的可读性和安全性
- 所有的强制转换都是自动和隐式的,提高代码的重用率
//获取值时必须强制类型转换ArrayList files = new ArrayList();String filename = (String)files.get(0);//这里没有错误检查,可以向数组列表添加任何类的对象file.add(new File("..."));//解决方案,类型参数//类型参数指定为String类型ArrayList<String> files = new ArrayList<String>();//无须强制类型转换,编译器知道返回String类型而不是ObjectString filename = file.get(0);//编译器可以检查,避免插入错误类型的对象,更安全//can only add String objects to an ArrayList<String>file.add(new File("..."));
泛型的使用😁
泛型类型参数只能被类或接口类型赋值,不能被原生数据类型赋值,原生数据类型需要使用对应的包装类。
泛型类
一个泛型类就是具有一个或多个类型变量的类
一个简单的栗子
/***简单的泛型类,在类名后面引入类型变量T,用<>括起来*/public class Pair<T>{//类定义中的类型变量指定方法的返回类型以及域和局部变量的类型private T first;private T second;public Pair(){first = null;second = null;}public Pair(T first,T second){this.first=first;this.second=second;}public T getFirst(){return first;}public T getSecond(){return second;}public void setFirst(T newValue){first=newValue;}public void setSecond(T newValue){second=newValue;}}
用具体的类型替换类型变量就可以实例化泛型类型,如:
Pair<String>//可以将结果想象成带有构造器的普通类Pair<String>()Pair<String>(String,String)//和方法String getFirst();String getSecond();void setFirst(String);void setSecond(String);
换句话说,泛型类可以看作普通类的工厂
泛型类也可以接受多个类型参数
public class test <K,V>{K key;V value;...}
泛型方法
一个带有类型参数的方法
//一个在普通类中定义的泛型方法class ArrayAlg{public static <T> T getMiddle(T... a){return a[a.length/2];}}
⚠ 类型变量放在修饰符(public static)后面,返回类型前面
当调用一个泛型方法时,在方法名前的<>中放入具体类型
//调用泛型方法String middle = ArrayAlg.<String>getMiddle("John","Q.","Public");
在大多数情况下,方法调用中可以省略类型参数。编译器有足够的信息能推断出所调用的方法。它用names的类型与泛型类型T[ ]进行匹配并推断出T一定是String,也就是说
//这样调用也是可以的,编译器可以推断出String类型String middle = ArrayAlg.getMiddle("John","Q.","Public");
当然也有出现错误的情况
当泛型类和泛型方法共存时,泛型类中的类型参数与泛型方法中的类型参数是没有相应的联系的,泛型方法始终以自己定义的类型参数为准
类型变量的限定
泛型可以限定类型变量必须实现某几个接口或者继承某个类,多个限定类型用&分隔,类必须放在限定列表中所有接口的前面。
绑定类型可以是类也可以是接口
又来一个举烂的栗子,计算数组中的最小元素:
class ArrayAlg{public static <T>T min(T[] a){if(a==null || a.length==0)return null;T smallest = a[0];for(int i=1;i<a.length;i++){if(smallest.compareTo(a[i]) > 0)smallest = a[i];return smallest;}}}
方法内部有compareTo方法,但是smallest类型为T,它可以是任何一个类的对象。为保证它所属的类有compareTo方法,需要给T设置限定:
//将T限制为实现了Comparable接口的类,保证有上述方法public static <T extends Comparable> T min(T[] a)...
泛型代码和虚拟机
类型擦除
无论何时定义一个泛型类型,都自动提供一个相应的原始类型。原始类型的名字就是删去类型参数后的泛型类型名。
泛型信息只存在于代码编译阶段,在进入 JVM 之前,与泛型相关的信息会被擦除掉,专业术语叫做类型擦除。
擦除类型变量,并替换为限定类型(无限定的变量用Object)
List<String> l1 = new ArrayList<String>();List<Integer> l2 = new ArrayList<Integer>();//打印结果是true 因为 List<String>和 List<Integer>在 jvm 中的 Class 都是 List.classSystem.out.println(l1.getClass() == l2.getClass);
//可以通过反射调用方法add,存储字符串类型@Testpublic void test2() throws NoSuchMethodException, InvocationTargetException,IllegalAccessException {ArrayList<Integer> list = new ArrayList<Integer>();list.add(123);//list.add("ABC"); 报错//利用反射向list里添加了字符串list.getClass().getMethod("add",Object.class).invoke(list,"ABC");System.out.println(list.get(1));}
泛型参数会擦除到它的第一个边界,比如说上面的 Pair类,参数类型是一个单独的 T,那么就擦除到 Object,相当于所有出现 T 的地方都用 Object 替换。所以在 JVM 看来,保存的变量还是 Object 类型。之所以取出来自动就是我们传入的参数类型,这是因为编译器在编译生成的字节码文件中插入了类型转换的代码,不需要我们手动转型了。
原始类型
原始类型(raw type)就是擦除去了泛型信息,最后在字节码中的类型变量的真正类型。
翻译泛型表达式
当程序调用泛型方法时,如果擦除返回类型,编译器插入强制类型转换。
//擦除getFirst的返回类型后将返回Object类型。编译器自动插入Employee的强制类型转换Pair<Employee> buddies = ...;Employee buddy = buddyes.getFirst();
翻译泛型方法
核心卷一P318
Java泛型转换的事实
- 虚拟机中没有泛型,只有普通的类和方法
- 所有的类型参数都用它们的限定类型替换
- 桥方法被合成来保持多态
- 为保持类型安全性,必要时插入强制类型转换
约束和局限性
- 不能用基本类型实例化类型参数
例如擦除之后类含有Object类型的域,而Object不能存储double值。
- 运行时类型查询只适用于原始类型
虚拟机中的对象总有一个特定的非泛型类型,因此所有的类型查询只产生原始类型。
//测试a是否是任意类型的一个Pairif(a instanceof Pair<String>); //Errorif(a instanceof Pair<T>); //ErrorPair<String> p = (Pair<String>) a; //Warning:can only test that a is a pair
getClass方法总是返回原始类型
- 不能创建参数化类型的数组
Pair<String>[] table = new Pair<String>[10]; //Error//擦除之后,table的类型是Pair[],可以转换为ObjectObject[] objarray = table;//数组会记住它的元素类型,如果试图存储其他类型,会抛出ArrayStoreExceptionobjarray[0] = "Hello"; //Error-component type is Pair
不过对于泛型类型,擦除会使这种机制无效
objarray[0] = new Pair<String>();//能够通过数组存储检查,不过仍会导致一个类型错误
因此,不允许创建参数化类型的数组。
- Varargs警告
当向参数个数可变的方法传递一个泛型类型的实例时,会发出Varargs警告,可以用@SuppressWarnings(“unchecked”)注解或者@SafeVarargs标注方法来抑制警告
public static <T> void addAll(Collection<T> coll, T ... ts){}Collection<Pair<String>> table;Pair<String> pair1;Pair<String> pair2;addAll(table,pair1,pair2);//为了调用这个方法,jvm必须建立一个Pair<String>数组,违反了前面的规则(不能创建参数化类型的数组)//抛出警告
- 不能实例化类型变量
不能使用像new T(…),new T[…]或T.class这样的表达式中的类型变量
public Pair(){first = new T();second = new T();}//例如这个Pair<T>构造器就是非法的,类型擦除后T变成Object
- 不能构造泛型数组
就像不能实例化一个泛型实例一样,也不能实例化数组。
- 泛型类的静态上下文中类型变量无效
- 不能抛出或捕获泛型类的实例
- 可以消除对受查异常的检查
- 注意擦除后的冲突
泛型类型的继承规则
- 无论S与T有什么联系,通常,Pair
与Pair没有什么联系
class Father{}class Son extends Father{}Father father = new Son();//在普通类中,父类可以用来指向子类//但是在泛型类中不行,泛型擦除后,Pair<T>之间没有继承关系Pair<Son> bb = new Pair<>();Pair<Father> cc = bb; //Error
- 永远可以将参数化类型转换为一个原始类型
List<String> a2 = new ArrayList<>();List a3 = a2;//可以通过编译,但是后面使用方法时可能会产生类型错误//这时候a3对象是原始类型,所以add(Object object);a3.add(123);//是对a3进行操作,但是最终结果保存到了a2中,将一个Integer装入String中,错误!
- 泛型类可以扩展或实现其他的泛型类
//泛型接口interface List1<E>{}//实现了泛型接口的泛型类class List2<T,E> implements List1<E>{}//泛型类class List3<T>{}//继承了其他泛型类的泛型类class List4<T,E> extends List3<E>{}//因为List2实现了List1List1<Father> b1 = new List2<Son,Father>();//List4继承了List3,所以List3是父类,可以指向子类对象。List3<Father> b2 = new List4<Son,Father>();
虽然这样也完成了泛型类的继承,实现了和普通类一样的多态,但是使用起来并不是特别好,所以Java引入了通配符概念。
通配符类型
通配符概念
为了解决类型被限制死了不能动态根据实例来确定的缺点,引入了“通配符泛型”。
Pair<? extends Employee>;//表示任何泛型Pair类型,它的类型参数是Employee的子类,如Pair<Manager>,但不是String
固定边界通配符
- 固定上边界的通配符的泛型,能够接受指定类及其子类类型的数据,采用<? extends E>的形式声明,E就是该泛型的上边界。(也可代指实现了该接口E的类)。
禁止向List<? extends E>中添加任何对象,可以添加null
- 固定下边界的通配符的泛型,能够接受指定类及其父类类型的数据,采用<? super E>的形式声明,E就是该泛型的下边界。
//水果是苹果的父类,实现了向上转型Plate<? extends Fruit> plate = new Plate<Apple>();
extends通配符的缺陷
虽然通过这种方式,Java 支持了 Java 泛型的向上转型,但是这种方式是有缺陷的,那就是:其无法向 Plate 中添加任何对象,只能从中读取对象。
Plate<? extends Fruit> plate = new Plate<Apple>();plate.add(new Apple()); //Compile Errorplate.get(); // Compile Success
因为在我们还未具体运行时,JVM并不知道我们要往里放的到底是什么,所以干脆什么都不给放,避免出错。
super通配符的缺陷
Plate<? super Apple> plate = new Plate<Fruit>();
当然了,下面的声明肯定也是对的,因为 Object 是任何一个类的父级。
Plate<? super Apple> plate = new Plate<Object>();
既然这样,也就是说 plate 指向的具体类型可以是任何 Apple 的父级,JVM 在编译的时候肯定无法判断具体是哪个类型。但 JVM 能确定的是,任何 Apple 的子类都可以转为 Apple 类型,但任何 Apple 的父类都无法转为 Apple 类型。
所以对于使用了 super 通配符的情况,我们只能存入 T 类型及 T 类型的子类对象。
Plate<? super Apple> plate = new Plate<Fruit>();plate.add(new Apple());plate.add(new Fruit()); //Error
当我们向 plate 存入 Apple 对象时,编译正常。但是存入 Fruit 对象,就会报编译错误。
而当我们取出数据的时候,也是类似的道理。JVM 在编译的时候知道,我们具体的运行时类型可以是任何 Apple 的父级,那么为了安全起见,我们就用一个最顶层的父级来指向取出的数据,这样就可以避免发生强制类型转换异常了。
Object object = plate.get();Apple apple = plate.get(); //ErrorFruit fruit = plate.get(); //Error
从上面的代码可以知道,当使用 Apple 类型或 Fruit 类型的变量指向 plate 取出的对象,会出现编译错误。而使用 Object 类型的额变量指向 plate 取出的对象,则可以正常通过。
总结:
- 对于 extends 通配符,我们无法向其中加入任何对象,但是我们可以进行正常的取出。
- 对于 super 通配符,我们可以存入 T 类型对象或 T 类型的子类对象,但是我们取出的时候只能用 Object 类变量指向取出的对象。
无限定通配符
1、当方法是使用原始的Object类型作为参数时,如下:
public static void printList(List<Object> list) {for (Object elem : list)System.out.println(elem + "");System.out.println();}
可以选择改为如下实现:
public static void printList(List<?> list) {for (Object elem: list)System.out.print(elem + "");System.out.println();}
这样就可以兼容更多的输出,而不单纯是List
List<Integer> li = Arrays.asList(1, 2, 3);List<String> ls = Arrays.asList("one", "two", "three");printList(li);printList(ls);
2、在定义的方法体的业务逻辑与泛型类型无关,如List.size,List.cleat。实际上,最常用的就是Class<?>,因为Class并没有依赖于T。
最后提醒一下的就是,List
通配符捕获
//交换成对元素方法public static void swap(Pair<?> p);//通配符不是类型变量,不能使用?作为一种类型? t = p.getFirst(); //Error//解决方法,写一个辅助方法public static <T> void swapHelper(Pair<T> p){T t = p.getFirst();p.setFirst(p.getSecond());p.setSecond(t);}//现在swapHelper是一个泛型方法,而swap不是public static void swap(Pair<?> p){swapHelper(p);}//这种情况下,swapHelper方法的参数T捕获通配符。//它不知道是哪种类型的通配符,但是这是一个明确的类型,并且<T>swapHelper的定义只有在T指出类型时才有明确的含义
通配符捕获只有在有许多限制的情况下才是合法的。编译器必须能够确信通配符表达的是单个、确定的类型。
