版权声明:本文为博主ExcelMann的原创文章,未经博主允许不得转载。
作者:ExcelMann,转载需注明。
第8章内容目录:
本章内容将介绍实现自己的泛型代码所需了解的全部知识,这些知识大多数情况下是用来帮助排除自己代码的问题。
泛型程序设计意味着编写的代码可以对多种不同类型的对象重用。
在Java中增加泛型类之前,泛型程序设计是通过继承实现的。例如,ArrayList只维护一个Object引用的数组。
后来,泛型提供了一个更好的解决方案:类型参数。例如,ArrayList类有一个类型参数用来指示元素的类型。
好处一:使得代码具有更好的可读性;
好处二:编译器可以充分利用这个类型信息。调用ArrayList对象的get方法时,编译器知道返回的类型是指定的类型,而不是Object,所以不需要强制类型转换;
好处三:编译器可以检查,防止插入错误类型的对象;
例子:
public class Pair<T> {
private T first;
private T second;
public Pair(){}
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;
}
public T getSecond() {
return second;
}
public void setSecond(T second) {
this.second = second;
}
}
class ArrayAlg
{
public static <T> T getMiddle(T... a)
{
return a[a.length/2];
}
}
String middle = ArrayAlg.<String>getMiddle("abs","dba");
String middle = ArrayAlg.getMiddle("abs","dba");
有的时候,类或方法需要对类型变量进行限定(加以约束)。
例子:我们要计算一个数组的最小和最大元素
public static <T> Pair<T> minmax(T[] a){
if(a == null || a.length==0) return null;
T min = a[0];
T max = a[0];
for(T temp:a){
if(min.compareTo(temp)>0) min = temp;
if(max.compareTo(temp)<0) max = temp;
}
return new Pair<>(min,max);
}
问题存在:对于这里的类型T,我们怎么知道该类型的对象一定有compareTo方法呢?
解决方法:限制T只能是实现了Comparable接口的类。可以通过对类型变量T设置一个限定来实现这一点,如下所示
public static
注意:
请注意,虚拟机没有泛型类型对象——所有对象都属于普通类。下面的内容中介绍编译器如何“擦除”类型参数,以及该过程对Java程序员有什么影响。
无论何时定义一个泛型类型的类或者方法,都会自动地提供一个原始类型的类和方法。
其中原始类型指的是类型变量被擦除后的类型,一般为Object(无限定的情况下)或者限定类型(第一个限定类型)。
(注意:应该将标签接口(即没有方法的接口)放在限定列表的末尾!)
比如,对于Pair类,其原始类型的类如下:
public class Pair<Object> {
private Object first;
private Object second;
public Pair(){}
public Pair(Object first,Object second){
this.first = first;
this.second = second;
}
public Object getFirst() {
return first;
}
public void setFirst(Object first) {
this.first = first;
}
public Object getSecond() {
return second;
}
public void setSecond(Object second) {
this.second = second;
}
}
当编写一个泛型方法调用时,如果擦除了返回类型,编译器会插入强制类型转换。
例子:
Pair<Employee> buddies = ...;
Employee buddy = buddies.getFirst();
在这里getFirst返回值的类型在擦除后是Object。
所以编译器会自动插入强制类型转换,把这个方法调用转换为两条虚拟机指令:
1)对原始方法Pair.getFirst的调用;
2)将返回的Object类型强制转换为Employee类型;
注意:当访问一个泛型字段时,也会插入强制类型转换;
问题描述:
class DateInteval extends Pair<Employee>
{
public void setSecond(LocalDate second)
{
if(second.compareTo(getFirst())>=0)
super.setSecond(second);
}
}
当该类擦除后,会变成:
class DateInteval extends Pair //after erasure
{
public void setSecond(LocalDate second) {...}
public void setSecond(Object second){...} //从Pair继承的setSecond方法
}
其中存在一个从Pair继承的setSecond方法,显然两个方法不是同一个方法(所以不存在动态绑定)。
故考虑下面的语句序列:
var interval = new DateInterval();
Pair<LocalDate> pair = interval; //ok。赋值给父类
pair.setSecond(aDate);
当调用pair.setSecond(aDate)方法时,因为pair对象声明的是Pair
解决方法:
为了解决这个问题,编译器引入了桥方法。通过在DateInteval类中生成一个桥方法:
//从父类覆盖的方法
public void setSecond(Object second){
setSecond((LocalDate) second);
}
使得调用DateInterval对象的setSecond(Object)方法时,顺利调用setSecond(LocalDate)方法。
桥方法的另一用处:
当一个方法覆盖另一个方法时,可以指定不同的返回类型,这叫做有协变的返回类型。
第5章中的clone方法中,对于下面的例子,实际上Employee类有两个克隆方法:
Employee clone()
Object clone() //合成的桥方法,重写Object.clone
合成的桥方法会调用新定义的clone方法。
public class Employee implements Cloneable
{
public Employee clone() throws CloneNotSupportedException {...}
}
关于Java泛型转换的总结:
对于遗留代码,一般会存在两个问题:
1)将泛型类对象赋值给遗留类变量的情况;
2)将遗留类的方法得到的原始类型对象赋值给泛型类变量的情况;
对于这两种情况,可以判断警告的严重性,如果是可控的,那么可以使用注解使警告消失。(因为这种警告不会比没有泛型之前的情况更加糟糕。最差的情况也就是抛出一个异常。)
在这一章节中,将讨论使用Java泛型时需要考虑的一些限制,大多数都是由于类型擦除导致的限制。
因此,没有Pair
虚拟机中的对象总有一个特定的非泛型类型。因此,所有的类型查询只产生原始类型。
例子1:
if(a instanceof Pair
实际上仅仅测试a是否是任意类型的一个Pair,会得到一个编译器错误。
例子2:
Pair
会得到一个警告。
例子3:
Pair
stringPair.getClass(); //return Pair.class
对于泛型类对象,其调用getClass方法,都会返回擦除类型变量的类名.class。
不能实例化参数化类型的数组,例如:
var table = new Pair
如果需要收集参数化类型对象,可以简单地使用ArrayList:ArrayList
上一节中提出不能创建参数化类型的数组。不过,对于参数个数可变的一些方法却需要传递一个参数化类型的数组。
对于这种情况,规则有所放松,你只会得到一个警告,而不是一个错误。
可以采用两种方法抑制该警告:第一种是添加注解@SuppressWarnings(“unchecked”);第二种是在Java7中,可以用@SafeVarargs注解方法;
注意:
@SafeVarargs static <E> E[] array(E... array){
return array;
}
//故现在可以调用:
Pair<String> table = array(pair1,pair2);
//不过该代码隐藏着危险,由于类型擦除
问题描述:
不能在类型new T(…)的表达式中使用类型变量。例如,下面的Pair
public Pair(){ first = new T(); second = new T(); } //ERROR
因为类型擦除后,变成了new Object();
解决方法一:
在Java8之后,最好的解决方法就是让调用者提供一个构造器表达式。例如:
Pair<String> p = Pair.makePair(String::new); //方法引用:指定了Supplier.get方法执行的代码
makePair方法接收一个Supplier
public static <T> Pair<T> makePair(Supplier<T> constr){
return new Pair<>(constr.get(), constr.get());
}
解决方法二:
比较传统的解决方法是通过反射调用Constructor.newInstance方法来构造泛型对象。
public static <T> Pair<T> makePair(Class<T> cl){
try{
return new Pair<>(cl.getConstructor().newInstance(),
cl.getConstructor().newInstance());
}
catch(Exception e)
{
return null;
}
}
//如下调用:
Pair<String> p = Pair.makePair(String.class); //因为String.class是Class的对象
问题描述:
考虑下面的例子:
public static <T extends Comparable> T[] minmax(T... a)
{
T[] mm = new T[2]; //ERROR
}
类型擦除会让这个方法总是构造Comparable[2]数组。
解决方法一:
在这种情况下,最好让用户提供一个数组构造器表达式:
String[] names = ArrayAlg.minmax(String[]::new, "Tom", "Dick");
mixmax方法使用这个参数生成一个有正确类型的数组:
public static <T extends Comparable> T[] minmax(IntFunction<T[]> constr, T... a)
{
T[] result = constr.apply(2);
...
}
构造器表达式String::new指示一个函数式接口,该接口的apply方法,通过给定所需的长度,会构造一个指定长度的String数组。
解决方法二:
比较老式的方法是利用反射,并调用Array.newInstance:
public static <T extends Comparable> T[] minmax(T... a){
var result = (T[]) Array.newInstance(a.getClass().getComponentType(), 2);
...
}
注意:不能在静态字段或方法中引用类型变量。例如:
private static T temp;
技术介绍:
@SuppressWarnings("unchecked")
static <T extends Throwable> void throwAs(Throwable t) throws T
{
throw (T) t;
}
//★假如该方法存在于接口Task中,如果有一个检查型异常e,并调用
Task.<RuntimeException>throwAs(e);
//那么编译器就会认为e是一个非检查型异常
Java异常处理的规则要求对每一个检查型异常提供一个处理器。不过可以利用泛型取消这个机制,技术代码如上所示。
应用:
解决一个棘手的问题。要在一个线程中运行代码,需要把代码放在一个实现了Runnable接口的类的run方法中。不过该方法不允许抛出检查型异常。
所以我们提供一个从Task到Runnable的适配器,它的run方法可以抛出任何异常。
interface Task
{
//run()是唯一的未实现接口,故该接口是一个函数式接口
void run() throws Exception;
@SuppressWarnings("unchecked")
static <T extends Throwable> void throwAs(Throwable t) throws T
{
throw (T) t;
}
static Runnable asRunnable(Task task)
{
//Runnable也是一个函数式接口,lambda表达式代表了接口的run()
return ()->
{
try
{
task.run();
}
catch(Exception e){
Task.<RuntimeException>.throwAs(e);
}
};
}
}
例如,以下程序运行了一个线程,它会抛出一个异常:
(对于该段代码的解释:创建一个Thread对象,参数是Runnable,该Runnable通过接口Task的静态方法返回得到,而该静态方法的参数是一个Task对象,所以采用lambda表达式代替Task函数式接口)
public class Test
{
public void static main(String[] args)
{
var thread = new Thread(Task.AsRunnable(
()->{
Thread.sleep(1000);
System.out.println("hello");
throw new Exception("check this exception");
}
)
);
}
}
class Employee implements Comparable<Employee>{...}
class Manager extends Employee implements Comparable<Maneger>{...} //ERROR
//桥方法,若是上述情况,会出现两个同签名的方法
public int compareTo(Object object){
return compareTo((X) object);
}
Manager[] managerBuddies = {ceo,cfo};
Employee[] employeeBuddies = managerBuddies; //OK
不过数组有特别的保护。如果试图将一个低级别的员工存储到employeeBuddies,虚拟机将会抛出ArrayStoreException异常。
介绍:
在通配符类型中,允许类型参数发生变化,例如:
Pair<? extends Employee>
表示任何泛型Pair类型,它的类型参数是Employee的子类。
用法:
假设要编写一个打印员工对的方法,如下所示:
public static void printBuddies(Pair<Employee> p)
{
Employee first = p.getFirst();
Employee second = p.getSecond();
}
对于该代码,不能将Pair
public static void printBuddies(Pair<? extends Employee> p)
//extends的情形下,p存的是Employee类型
对于setXxx的问题(不安全的更改器和安全的访问器):
例如:
var managerBuddies = new Pair<Manager>(ceo,cfo);
Pair<? extends Employee> wildcardBuddies = managerBuddies;
wildcardBuddies.setFirst(lowlyEmployee);
此时调用setFirst会出现类型错误。
因为对于Prir extends Employee>,它的方法如下:
? extends Employee getFirst()
void setFirst(? extends Employee)
而这样子将不可能调用setFirst方法。因为编译器只知道需要是Employee的某个子类型,单不知道是具体的哪个类型。它拒绝传递任何特定的类型,毕竟?不能匹配。
而对于getFirst方法则不会出现问题。因为将?类型赋值给一个Employee类型变量没问题。
介绍:
通配符的限定可以指定一个超类型限定,如下所示:
? super Manager
这个通配符限制为Manager的所有超类型。
作用:
超类型限定实现了安全的更改器和不安全的访问器。
例子:对于Pair super Manager>有以下方法
? super Manager getFirst()
void setFirst(? super Manager)
对于get方法,返回值的类型不能保证,所以只能赋值给一个Object对象。
对于set方法,编译器无法知道参数的具体类型,因此不能接受参数类型为Employee或Object。但是可以接受Manager或其子类型。
另一个例子,有助于对超类型限定的理解:
假如有一个经理数组,并且想把奖金最高和最低的经理放在一个Pair对象中。对于这个Pair对象,其类型可以是Manager的超类型!
public static void minmaxBonus(Manager[] a, Pair<? super Manager> result){
//super下的情形,result存的是Manager类型
if(a.length == 0) return;
Manager min = a[0];
Manager max = a[0];
for(int i=1;i<a.length;i++){
if(min.getBonus() > a[i].getBonus()) min = a[i];
if(max.getBonus() < a[i].getBonus()) max = a[i];
}
result.setFirst(min);
result.setSecond(max);
}
注意区分:对于result的泛型类型,可以是Manager的超类型。而对于result的set方法,其方法参数的类型只能是Manager或其子类型!
public static <T extends Comparable<? super T>> Pair<T> minmax(T[] a)
//现在的compareTo方法写成
int compareTo(? super T)
它可以使用任何T的超类型对象作为参数。
//Collection接口有一个方法
default boolean removeIf(Predicate<? super E> filter)...
//该方法会删除所有满足给定谓词条件的元素。
//假如你不喜欢有奇数散列码的员工,就可以将他们删除
ArrayList<Employee> staff =...;
Predicate<Object> oddHashCode = obj->obj.hashCode()%2==0;
staff.removeIf(oddHashCode);
//这样你就能够传入一个Predicate
介绍:
类型Pair>采用了无限定的通配符。它有以下方法:
? getFirst()
void setFirst(?)
其getXxx返回值只能赋值给一个Object对象。而setXxx不能调用(Object对象也不行)。
用法之一:下面这个方法可优惠用来测试一个对组是否只包含一个null引用,它不需要实际的类型(或者任何类型参数都行)。
public static boolean hasNulls(Pair<?> pair)
{
return pair.getFirst()==null || pair.getSecond()==null;
}
例子说明:
public static <T> void swapHelper(Pair<T> p)
{
T t = p.getFirst();
p.setFirst(p.getSecond());
p.setSecond(t);
}
public static void maxminBonus(Manager[] a,Pair<? super Manager> result)
{
minmaxBonus(a,result);
PairAlg.swapHelper(result); //这里的swapHelper的类型变量T捕获了?通配符
}
在该例子中,首先有一个泛型方法,在maxminBonux方法中,通过调用该泛型方法,它的类型变量T会捕获通配符?。
通配符捕获只在非常限定的情况下才是合法的。因为编译器需要保证通配符表示单个确定的类型。
这部分内容暂时没浏览书籍,不过在另一篇博客的第十五章节记录过一些笔记。
【原创】深入理解Java——注解和反射