领先一步
VMware 提供培训和认证,助您快速进步。
了解更多当我与客户交谈时,经常听到一个常见的误解,那就是关于泛型类型的所有信息都从你的 Java 类文件中被擦除了。这是完全不正确的。所有的静态泛型信息都保留了下来,只有关于个体实例的泛型信息被擦除。因此,如果我有一个实现了 List<String> 的类 Foo,那么我可以在运行时确定 Foo 实现了使用 String 作为类型参数的 List 接口。然而,如果在运行时实例化一个 ArrayList<String> 的实例,我无法通过该实例确定其具体的类型参数(我可以确定 ArrayList 需要类型参数)。在本文中,我将向您展示泛型元数据的一些实际用途,它简化了根据处理对象类型而不同的策略接口和实现的过程。
我在许多应用程序中看到的一种模式是使用某种策略接口,其具体实现分别处理特定的输入类型。例如,考虑投资银行业中的一个简单场景。任何上市公司都可以发布 公司行为(Corporate Actions),这会对其股票带来实际改变。一个关键的例子是股息支付,它按每股向所有股东支付一定数量的现金、股票或财产。在投资银行内部,接收这些事件通知并计算由此产生的权益非常重要,以便使交易账簿保持最新的正确股票和现金价值。
作为一个具体示例,考虑持有 1,200,000 股 IBM 股票的大银行(BigBank)。IBM 决定派发每股 0.02 美元的股息。因此,大银行需要接收股息行动通知,并在适当的时间点更新其交易账簿,以反映额外的 24,000 美元现金。
权益的计算将根据执行的何种公司行为而有很大不同。例如,合并很可能导致一家公司的股票损失和另一家公司的股票收益。
如果我们思考这在 Java 应用中是什么样子,可以假设看到类似如下所示的(大大简化的)例子:
public class CorporateActionEventProcessor {
public void onCorporateActionEvent(CorporateActionEvent event) {
// do we have any stock for this security?
// if so calculate our entitlements
}
}
关于事件的通知可能通过多种机制从外部方接收,然后发送到这个 CorporateActionEventProcessor 类。这个 CorporateActionEvent 接口可能通过多个具体类实现:
public class DividendCorporateActionEvent implements CorporateActionEvent {
private PayoutType payoutType;
private BigDecimal ratioPerShare;
// ...
}
public class MergerCorporateActionEvent implements CorporateActionEvent {
private String currentIsin; // security we currently hold
private String newIsin; // security we get
private BigDecimal conversionRatio;
}
计算权益的过程可以通过这样一个接口封装:
public interface EntitlementCalculator {
void calculateEntitlement(CorporateActionEvent event);
}
伴随这个接口,很可能看到一些像这样的实现:
public class DividendEntitlementCalculator implements EntitlementCalculator {
public void calculateEntitlement(CorporateActionEvent event) {
if(event instanceof DividendCorporateActionEvent) {
DividendCorporateActionEvent dividendEvent = (DividendCorporateActionEvent)event;
// do some processing now
}
}
}
那么我们的 CorporateActionEventProcessor 可能看起来像这样:
public class CorporateActionEventProcessor {
private Map<Class, EntitlementCalculator> entitlementCalculators = new HashMap<Class, EntitlementCalculator>();
public CorporateActionEventProcessor() {
this.entitlementCalculators.put(DividendCorporateActionEvent.class, new DividendEntitlementCalculator());
}
public void onCorporateActionEvent(CorporateActionEvent event) {
// do we have any stock for this security?
// if so calculate our entitlements
EntitlementCalculator entitlementCalculator = this.entitlementCalculators.get(event.getClass());
}
}
这里可以看到我们维护了一个从 CorporateActionEvent 类型到 EntitlementCalculator 实现的 Map,我们使用它来定位每个 CorporateActionEvent 对应的正确 EntitlementCalculator。
回顾这个例子,第一个显著的问题是 EntitlementCalculator.calculateEntitlement 被类型化为只接收 CorporateActionEvent,导致在每个实现内部需要进行类型检查和类型转换。使用泛型可以轻松解决这个问题:
public interface EntitlementCalculator<E extends CorporateActionEvent> {
void calculateEntitlement(E event);
}
public class DividendEntitlementCalculator implements EntitlementCalculator<DividendCorporateActionEvent> {
public void calculateEntitlement(DividendCorporateActionEvent event) {
}
}
如您所见,我们引入了一个类型参数 E,它被绑定为继承自 CorporateActionEvent。然后我们定义 DividendEntitlementCalculator 实现了 EntitlementCalculator<DividendCorporateActionEvent>,这导致在 DividendEntitlementCalculator 中 E 被适当地替换为 DividendCorporateActionEvent,从而消除了类型检查和类型转换的需要。
这个 CorporateActionEventProcessor 类继续按原样工作,然而现在存在一些重复,也存在出错的可能性。在注册特定的 EntitlementCalculator 时,我们仍然需要指定它处理的类型,尽管这已经在类定义中指定了。鉴于此,可能会注册一个 EntitlementCalculator 来处理它不可能处理的类型:
public CorporateActionEventProcessor() {
this.entitlementCalculators.put(MergerCorporateActionEvent.class, new DividendEntitlementCalculator());
}
幸运的是,通过从泛型接口声明中提取参数类型并将其用作键类型,可以很容易地解决这个问题:
public void registerEntitlementCalculator(EntitlementCalculator calculator) {
this.entitlementCalculators.put(extractTypeParameter(calculator.getClass()), calculator);
}
我们首先添加一个 registerEntitlementCalculator 方法,它委托给 extractTypeParameter 来查找 EntitlementCalculator 类的类型参数。
private Class extractTypeParameter(Class<? extends EntitlementCalculator> calculatorType) {
Type[] genericInterfaces = calculatorType.getGenericInterfaces();
// find the generic interface declaration for EntitlementCalculator<E>
ParameterizedType genericInterface = null;
for (Type t : genericInterfaces) {
if (t instanceof ParameterizedType) {
ParameterizedType pt = (ParameterizedType)t;
if (EntitlementCalculator.class.equals(pt.getRawType())) {
genericInterface = pt;
break;
}
}
}
if(genericInterface == null) {
throw new IllegalArgumentException("Type '" + calculatorType
+ "' does not implement EntitlementCalculator<E>.");
}
return (Class)genericInterface.getActualTypeArguments()[0];
}
这里我们首先通过调用 Class.getGenericInterfaces() 来获取表示 EntitlementCalculator 类型的泛型接口的 Type[]。这个方法与返回 Class[] 的 Class.getInterfaces() 有很大不同。调用 DividendEntitlementCalculator.class.getInterfaces() 返回一个表示 EntitlementCalculator 类型的 Class 实例。调用 DividendEntitlementCalculator.class.getGenericInterfaces() 返回一个表示使用 DividendCorporateActionEvent 作为类型参数的 EntitlementCalculator 类型的 ParameterizedType 实例。在同时具有泛型接口和非泛型接口的类上调用 getGenericInterfaces() 将返回一个包含 Class 和 ParameterizedType 实例的数组。
接下来,我们遍历 Type[] 并找到其“原始类型”是 EntitlementCalculator 的 ParameterizedType 实例。从这里,我们可以使用 getTypeArguments() 提取 E 的类型参数,并返回数组的第一个实例——我们知道在这种情况它总是存在。
调用代码只需按需传入 EntitlementCalculator 实现即可:
CorporateActionEventProcessor processor = createCorporateActionEventProcessor();
processor.registerEntitlementCalculator(new DividendEntitlementCalculator());
现在这是一个非常不错的 API,并且可以使用 Spring 这样的东西进一步扩展,您可以使用 ListableBeanFactory.getBeansOfType() 来定位所有配置的 EntitlementCalculator 实现,并自动将它们注册到 CorporateActionEventProcessor。
你们中有些人可能已经注意到的一个有趣情况是,完全可能写出这样的代码:
EntitlementCalculator calculator = new DividendEntitlementCalculator();
calculator.calculateEntitlement(new MergerCorporateActionEvent());
这段代码编译时没有问题,但我们知道 DividendEntitlementCalculator.calculateEntitlement 方法只接受一个 DividendCorporateActionEvent 对象。那么为什么它能编译呢?而且,既然它能编译,运行时会发生什么?好吧,先回答第二个问题——Java 仍然通过在运行时抛出 ClassCastException 来确保类型安全。关于为什么它可以工作,以及为什么这个例子确实能编译的问题,我很快会写另一篇文章...