文章目录
8. 泛型程序设计
8.1泛型基础
以ArrayList为例,最早它的内部是一个Object类型,这导致2个问题:(1)每次使用get(i)都要强制类型转换;(2)可以往ArrayList里面添加任何类型
解决方法:泛型提供了类型参数,此后变为:ArrayList< String> list = new ArrayList< String>();,提供可读性和安全性
新的问题:当Son是Father的子类时,希望可以实现:addAll能将 ArrayList< Son> 全部添加到 ArrayList< Father>里面去,反之不行
//即实现以下规则(实际上也是现在Java具备的功能)
fathers.addAll(sons);//成功
sons.addAll(fathers);//报错
解决方法:为了实现这一目标,提出了通配符类型
//泛型类
public class Pair<T, U> {
. . . }
//泛型方法
public static <T> T getMiddle(T... a);//注意:<T>放在修饰符后面,返回类型前面
在调用泛型方法时,多数情况不用指明类似的类型,因为会进行自动推导
-
示例:Pair
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 void setFirst(T first){ this.first=first;}; }
8.4 类型变量的限定
场景:定义 T smallest,之后需要用到 smallest.compareTo 函数,如何确定它所属的类有这个函数呢?
限定类型变量
public static <T extends Comparable> T min(T[] a);
(1)尽管Comparable是接口,但用的仍然是extends,而非implements
(2)不能对模板参数的类型加以限制
(3)一个类型变量或通配符可以有多个限定,如
T extends Comparable & Serializable
类型擦除(表达式):
虚拟机没有泛型类型对象——所有对象都属于普通类
泛型类型都有相应的原始类型,对于有限定的类型变量,它的原始类型是第一个限定类型;无限定则是Object类型,例如
public class Interval <T extends Comparable & Serializable〉implements Serializable{
private T lower;
}
它的原始类型是:
public class Interval implements Serializable{
private Comparable lower;
}
//对于被忽略掉的限定类型,编译器会在必要时插入强制类型转换
原始类型和强制类型转换:
Pair<Employee> buddies = ...
Employee buddy = buddies.getFirst();
/**
* 编译针对泛型类型,将进行以下两步
* 1. 对原始方法Pair.getFirst的调用
* 2. 将返回的Object类型强制转换为Employee类型
*/
类型擦除(方法):
public static <T extends Comparable〉T min(T[] a)
//擦除后变成:
public static Comparable min(Comparable[] a)
存在的问题:多态和类型擦除的冲突
class DateInterval extends Pair<LocalDate>{
public void setSecond(LocalDate second){
...
}
}
//擦除后变成
class DateInterval extends Pair{
public void setSecond(LocalDate second){
...
}
}
此时,DateInterval中还有从Pair继承而来的另一个setSecond方法,即
public void setSecond(Object second);//继承而来
public void setSecond(LocalDate second);//自身的
/**
* 本意是在子类中覆盖掉超类的setSecond方法,但因为类型擦除的存在,没有覆盖住
*/
当执行以下语句时
DateInterval interval = new DateInterval(. . .);
Pair<LocalDate> pair = interval; // OK assignment to superclass
pair.setSecond(aDate);
pair是超类变量,但引用了子类对象,执行setSecond时,它将调用子类对象,即DateInterval.setSecond()
问题在于:DataInterval中有2个setSecond,怎样才能调用到合适的哪个呢?
-
桥方法:桥方法被合成来保持多态
当面临多态和类型擦除带来的矛盾时:编译器会在子类中生成一个桥方法,如下:
public void setSecond(Object second) { setSecond((Date) second);
当执行pair.setSecond(aDate)时,编译器工作步骤:
(1)调用这个桥方法
(2)桥方法再调用子类的方法 -
进一步:子类覆盖了超类的方法
class DateInterval extends Pair<LocalDate>{ public LocalDate getSecond() { return (Date) super.getSecond().clone(); } }
此时子类中有2个方法:
Object getSecond();//继承而来 LocalDate getSecond();//自身所有
程序员不允许编写仅返回类型不同的函数,但JVM会用参数类型和返回类型来确定一个方法
@SuppressWarnings(“unchecked”):这个注解会关闭对方法中所有代码的检査
8.6 约束和局限性
(1)不能用基本类型实例化类型参数
原因是类型擦除后Object不能存储基本类型的值
(2)运行时类型查询只适用于原始类型
Pair<String> stringPair = ...
Pair<Employee> employeePair = ...
if (stringPair.getClass() == employeePair.getClass()) //getClass会获取到原始类型,将得到相等的结果
Pair<String> pairString = new Pair<>();
if(pairString instanceof Pair<String>)//报错
if(pairString instanceof Pair)//成功
(3)不能创建参数化类型的数组
Pair<String>[] table = new Pair<String>[10];//Error,不能这样使用
原因:参考博客:https://blog.csdn.net/qq_41286138/article/details/105250938
主要是为了避免数组里出现类型不一致的元素
Pair[] table = new Pair[10];
table[0] = new Object(); //编译错误
编译器会记住创建时的类型,如果赋值不一致类型则报错。那么将数组向上转换一下,再存储呢?
Pair[] table = new Pair[10];
Object[] o = table; //自动转换
o[0] = new Object();
此时编译器是不报错,但是运行时会抛出ArrayStoreException异常
但是类型擦除会破坏这种机制
Pair<String>[] table = new Pair<String>[10] //假设可以
Object[] o = table; //泛型擦除变为Pair[],向上自动转换为Object[]
o[0] = new Pair<Double>();//成功
o[1] = new Object(); // 编译不报错,运行抛出异常
为了安全性,java禁止了创建参数化类型的数组
可以使用通配符完成这种数组创建,但仍然是不安全的做法
(4)Varargs 警告
public static <T> void addAll(Collections<T> coll, T...ts){
for (t : ts) coll.add(t);
}
调用:
Collection<Pair<String>> table = . . .;
Pair<String> pairl = . . .;
Pair<String> pair2 = . .
addAll (table, pairl, pair2);
此时addAll中的ts实际上是一个数组,违反了不能创建了泛型数组的规定。这里的规则放松一点,只会警告,不会报错
可以使用@SuppressWarnings(“unchecked”)或者@SafeVarargs标注这个方法
@SafeVarargs也可以用来解决泛型数组的问题,它能够顺利编译且运行,但在使用时会在别处得到一个异常
(5)不能实例化类型变置
不能使用new T(…),newT[…] 或T.class 这样的表达式,例如
//这段代码将报错
class Pair<T>{
private T first;
private T second;
public Pair() {
first = new T(); second = new T(); }//报错
}
//类型擦除会使得new T(),变成new Object()
最好的解决方法是:
- Supplier函数式接口,表示一个无参数而且返回类型为 T 的函数
class Pair<T>{
private T first;
private T second;
public Pair(T t1, T t2) {
this.first=t1; this.second=t2;}
//public Pair() { first = new T(); second = new T(); }//报错
public static<T> Pair<T> makePair(Supplier<T> constr){
return new Pair<>(constr.get(), constr.get());
}
}
public class Test{
public static void main(String[] args){
//1. 推荐方法:构造器方式
Pair<String> p = Pair.makePair(String::new);//推荐方式
//2. 相同效果:lambda方式
Pair<String> p = Pair.makePair(()->new String());
//3. 更好理解的:匿名内部类方式
Pair<String> p = Pair.makePair(new Supplier<String>() {
@Override
public String get() {
return new String();
}
});
}
}
传统方法是使用newInstance
//makePair
public static <T> Pair<T> makePair(Class<T> cl){
try{
return new Pair<>(cl.newInstance(), cl.newInstance());
}catch(Exception ex) {
return null; }
}
//调用
Pair<String> p = Pair.makePair(String.class);//能进行自动类型推导
Java中反射的效率是比较低的,newInstance是通过反射的方式;Supplier方式是直接new,效率更高
(6)不能构造泛型数组
可以采用如下形式
//方法
public static <T extends Comparable> T[] minmax(IntFunction<T[]> constr, T...a){
Arrays.sort(a);
T[] mm = constr.apply(2);
mm[0] = a[0];
mm[1] = a[a.length-1];
return mm;
}
//调用和测试
String[] ss = Pair.minmax(String[]::new, "Tom", "Dick", "Harry");
Integer[] ii = Pair.minmax(Integer[]::new, 3, 1, 4);
System.out.println(Arrays.toString(ss));
System.out.println(Arrays.toString(ii));
或者老式方法(通过反射):
//方法
public static <T extends Comparable> T[] minmax(T... a){
T[] mm = (T[]) Array.newlnstance(a.getClass().getComponentType() , 2);
}
//调用
String[] ss = Pair.minmax( "Tom", "Dick", "Harry");
ArrayList的toString所有所不同,有机会看看源码学习
(7)不能在静态域或方法中引用类型变量
public class Singleton<T>{
private static T singleInstance; // Error
public static T getSingleInstance(){
}; //Error
}
原理:以静态域singlelnstance为例,当同时声明一个Singleton< Random> 共享随机数生成器和一个Singleton< JFileCh00Ser> 共享文件选择器对话框时,经过类型擦除,就只剩下一个singlelnstance域,存在问题
(8)不能抛出或捕获泛型类的实例
不能抛出或捕获泛型类,泛型类不能扩展Throwable !
public class Problem<T> extends Exception{
}//报错
catch语句中,不能有泛型。但在异常规范中使用类型变量是允许的。如
//报错!!
public static <T extends Throwable> void doWork(Class<T> t){
try{
}
catch (T e){
}
}
//通过!!
public static <T extends Throwable> void doWork(T t) throws T{
try{
}
catch (Throwable e){
t.initCause(e);
throw t;
}
}
(9)可以消除对受查异常的检查
场景:假设存在一个方法body(),要求其必须抛出异常(即受查),如下
abstract class Block{
public abstract void body() throws Exception;//声明一个方法,并规定该方法必须抛出Exception
public void test() throws Exception {
//这里如果是不 throws 或者 throws RuntimeException 都将报错!!
body();
}
}
用于消除受查异常检测的重要代码
@SuppressWarnings("unchecked")
public static <T extends Throwable> void throwAs(Throwable e) throws T{
throw (T)e;
}
//假设这个方法包含在类Block中,如果调用下属语句,就相当于告诉编译器,这是一个非受查异常
//RuntimeExceptions是非受查异常
Block.< RuntimeException>throwAs(t);
修改之后
abstract class Block{
public abstract void body() throws Exception;//声明一个方法,并规定该方法必须抛出Exception
public void test() {
try {
body();
}catch (Throwable e){
//throw new RuntimeException();//这条语句也能执行
Block.< RuntimeException>throwAs (e);
}
}
@SuppressWarnings("unchecked")
public static <T extends Throwable> void throwAs(Throwable e) throws T{
throw (T)e;
}
}
用法是这样用,但意义暂且模糊:把一个受查异常包装为非受查异常,但要保留其实质是受查异常的
throw new RuntimeException()的实质也是非受查的
(10)注意擦除后的冲突
下列代码将报错
class Pair<T>{
public boolean equals(T value){
return true;}//报错,clashes with 'equals(Object)'
public boolean equals(String value){
return true;}//这样就可以,因为T会被擦除为Object,而String不会
}
这是因为Pair默认继承Object类,这样它里面就会产生两个equals,例如考虑Pair< String>
Pair<String>
//产生的两个equals分别是
public boolean equals(Object value);//自己定义的equals经过类型擦除之后的结果
public boolean equals(Object value);//来自Object
//注意,自己定义的方法编译后不是equals(String value),而是泛型T在类型擦除的影响下也变成equals(Object value)
//所以前面的代码会报错:clashes with 'equals(Object)'
可以通过重命名的方式解决这一问题
还有一个重要原则:一个类不能同时 implements 同一接口的不同参数化
class Employee implements Comparable<Employee>{
... }
class Manager extends Employee implements Comparable<Manager>{
... }//报错
//这里将报错:'java.lang.Comparable' cannot be inherited with different type arguments: 'pack.Employee' and 'pack.Manager'
这里 Manager 是 Employee 的子类,所以它implements Comparable< Employee>
Manager 本身又 implements Comparable< Manager>
这种情况是不被允许的,不被允许的原因可能是和桥方法冲突有关
(11)泛型类型将破坏原有的继承关系
Pair<Manager> managerBuddies = new Pair<>(ceo, cfo);
Pair<Employee> employeeBuddies = managerBuddies;//尽管Employee是Manager的超类,但泛型后继承关系被破坏了,这里将报错
这样规定的原因:
//假设上面不报错,执行下面语句
employeeBuddies.setFirst(lowlyEmployee);
//这将普通员工lowlyEmployee赋值给了manager,实际中不应当这样做
泛型和数组的一个重要区别:数组中保留了继承关系,如下
Manager[] managers = new Manager[5];
Employee[] employees = managers;
employees[0] = new Employee();//这句能通过编译,但运行时报错ArrayStoreException
//注意这里的employees只能存储manager对象,否则报错ArrayStoreException
//这是因为数组的类型会被记住,修改数组变量类型除了逃避编译器外反而没有太大作用
泛型:可以将参数化类型转换给原始类型,从而与遗留代码兼容(但不太安全,有可能出现类型错误)
Manager ceo = new Manager();
Manager cfo = new Manager();
Pair<Manager> managerBuddies = new Pair<>(ceo, cfo);
Pair rawBuddies = managerBuddies;
rawBuddies.setFirst(new File("F://test.txt")); //only a compile-time warning
//这里不会报错
8.8 通配符类型
(子类型限定)通配符
无论类S和类T有着怎样的关系,Pair< S>和Pair< T>都是无关的,不能赋值Pair< S> … = Pair< T>…
假设S和T有继承关系,且希望 泛型能够具有同样的继承关系,那么可以使用 通配符 来解决这个问题
Pair<? extends Employee> //表示任何泛型Pair类型,它的类型参数是Employee的子类
这里可以做个比较:
Pair< ? extends Employee> 是为了保留继承关系,它可以接收 Pair< Employee的子类>
Pair< T extends Comparable> 是为了保证T实现了某个接口,它可以接收 Pair<Comparable的实现类>
-
失败场景:使用普通泛型
public static void printBuddies(Pair<Employee> p){ System.out.println(p.getFirst().getName()); } public static void main(String[] args){ Employee employee = new Employee("Tony"); Pair<Employee> employeePair = new Pair<>(employee, new Employee()); printBuddies(employeePair);//成功,输出 Tony Manager manager = new Manager("Admin"); Pair<Manager> managerPair = new Pair<>(manager, new Manager()); printBuddies(managerPair);//报错!! }
原因:Pair< Employee> 不能接收 Pair< Manager>
-
成功场景:使用通配符
//将方法改为 public static void printBuddies(Pair<? extends Employee> p){ System.out.println(p.getFirst().getName()); } //此时成功输出 Tony和Admin
类型Pair 是Pair<? extends Employee> 的子类型
-
?也不能使父类对象被子类引用
Pair<? extends Employee> wildBuddies = managerPair;//ok,managerPair是Pair<Manager>类型 wildBuddies.setFirst(new Employee("Buddy"));//error,这里将直接报错,因为这里将一个父类Employee赋给子类空间managerPair
使用getFirst不存在这个问题,因为任意子类都可以赋值给超类Employee
超类型限定通配符
? super Manager
带有 ? super 的通配符和上一节相反,可以使用setFirst(),但不能使用getFirst
//Pair<? super Manager>
? super Manager getFirst();//error; 这里返回的是Manager的任意一个超类,不能将超类返回给子类,所以
void setFirst(? super Manager);//ok; 相反,? super Manager表示任意的超类,可以用超类接子类
? super超类型限定 set 写入,? extends子类型限定 get 读出
无限定通配符
Pair<?>
它的get和set方法变为
? getFirst();//它的返回值只能赋值给Object对象变量,类似于<? extends Object>时的情况
void setFirst(?);//set方法直接不能被调用,哪怕是用Object去调用
Pair<?>和Pair本质的不同在于:可以用任意Object对象调用原始Pair类的setObject方法
无限定通配符的作用:对于许多简单操作有用,如
public static boolean hasNulls(Pair<?> p){
return p.getFirst()==null || p.getSecond()==null;
}
//它的等价泛型方法是
public static <T> boolean hasNUlls(Pair<T> p);
通配符捕获
场景:要写一个方法,交换 Pair 的 first 和 second
-
失败写法
public static void swap(Pair<?> p);//失败 //因为 ? 不能作为一种类型,但在交换的时候必须临时保存第一个元素 ? t = p.getFirst();
-
成功写法
public static <T> void swapHelper(Pair<T> p); //T可以作为类型,执行临时保存 T t = p.getFirst();
-
当用 ? 调用 T 方法,即 swap 调用 swapHelper
public static void swap(Pair<?> p) { swapHelper(p); }
8.9 反射和泛型
-
泛型Class类
String.class实际上是一个Class类的对象
Class 中的方法使用的类型参数T newInstance() //返回一个实例,这个实例所属的类由默认的构造器获得,它的返回类型与Class<T>描述的类相同,避免类型转换 T cast(Object obj) //强制转换类型 T[] getEnumConstants() //如果这个类不是enum类或类型T的枚举值的数组,getEnumConstants方法将返回null Class<? super T> getSuperclass() //返回超类 Constructor<T> getConstructor(C1ass... parameterTypes) Constructor<T> getDeclaredConstructor(Class... parameterTypes)
-
使用Class 参数进行类型匹配
//标准示例 public static <T> Pair<T> makePair(Class<T> c) throws InstantiationException, IllegalAccessException{ return new Pair<>(c.newInstance(), c.newInstance()); } //调用 makePair(Employee.class) /** * Employee.class是类型Class<Employee>的一个对象,T同Employee匹配 * 编译器可以推断出这个方法将返回一个Pair<Employee> */
-
虚拟机中的泛型类型信息
擦除的类仍然保留一些泛型祖先的微弱记忆
//泛型方法 public static <T extends Comparable<? super T>> T min(T[] a) //擦除后 public static Comparable min(Comparable[] a)
个人理解:
Comparable<? super T>,表示Compareble< S>,S是T的父类
T extends Comparable<? super T>,T继承自Compareble< S>
java.lang.reflect 包中的接口Type包含以下子类型:
- Class 类,描述具体类型
- TypeVariable 接口,描述类型变量(如T extends Comparable<? super T>)
- WildcardType 接口, 描述通配符(如?super T)
- ParameterizedType 接口, 描述泛型类或接口类型(如Comparable<? super T>)
- GenericArrayType 接口, 描述泛型数组(如T[])
-
API
-
Class
//java.lang.Class<T> 1.0 T newInstance();//返回无参数构造器构造的一个新实例 T cast(Object obj);//如果obj为null或有可能转换成类型T,则返回obj;否则拋出BadCastException异常 T[] getEnumConstants();// 5.0,如果T是枚举类型,则返回所有值组成的数组,否则返回null Class<? super T> getSuperclass();//返回这个类的超类。如果T不是一个类或Object类,则返回null。 Constructor<T> getConstructor(Class.. .parameterTypes);// 1.1 Constructor<T> getDeclaredConstructor(Class... parameterTypes); //1.1 获得公有的构造器,或带有给定参数类型的构造器 //5.0 如果被声明为泛型类型,则获得泛型类型变量,否则获得一个长度为0的数组 TypeVariable[] getTypeParameters(); //5.0 获得被声明为这一类型的超类的泛型类型;如果这个类型是Object或不是一个类类型,则返回null Type getGenericSuperclass(); //5.0 获得被声明为这个类型的接口的泛型类型(以声明的次序),否则,如果这个类型没有实现接口,返回长度为0的数组。 Type[] getGenericInterfaces();
-
Constructor
//java.lang.reflect.Constructor<T> 1.1 T newInstance(0bject... parameters );//返回用指定参数构造的新实例
-
Method
//java.lang.reflect.Method 1.1 TypeVariable[] getTypeParameters();//5.0,如果这个方法被声明为泛型方法,则获得泛型类型变量,否则返回长度为0的数组 Type getGenericReturnType();//5.0,获得这个方法被声明的泛型返回类型 Type[] getGenericParameterTypes();//5.0,获得这个方法被声明的泛型参数类型。如果这个方法没有参数,返回长度为0的数组
-
TypeVariable
//java.lang.reflect.TypeVariable 5.0 String getName();//获得类型变量的名字 Type[] getBounds();//获得类型变量的子类限定,否则,如果该变量无限定,则返回长度为0 的数组
-
WildcardType
//java.lang.reflect.WildcardType 5.0 Type[] getUpperBounds();//获得这个类型变量的子类(extends)限定,否则,如果没有子类限定,则返回长度为0的数组 Type[] getLowerBounds();//获得这个类型变量的超类(super)限定,否则 如果没有超类限定,则返回长度为0的数组
-
ParameterizedType
//java.lang.reflect.ParameterizedType 5.0 Type getRawType();//获得这个参数化类型的原始类型 Type[] getActualTypeArguments();//获得这个参数化类型声明时所使用的类型参数 Type getOwnerType();//如果是内部类型,则返回其外部类型,如果是一个顶级类型,则返回null
-
GenericArrayType
//java.lang.reflect.GenericArrayType 5.0 Type getGenericComponentType();//获得声明该数组类型的泛型组件类型
-