领先一步
VMware 提供培训和认证,助您加速进步。
了解更多与客户交流时,我经常听到一种普遍的误解,即认为泛型类型的所有信息都会在 Java 类文件中被擦除。这是完全不正确的。所有静态泛型信息都会被保留,只有关于单个实例的泛型信息才会被擦除。所以,如果我有一个类 Foo,它实现了 List<String>,那么在运行时,我可以确定 Foo 实现的是由 String 参数化的 List 接口。然而,如果我在运行时实例化一个 ArrayList<String> 实例,我无法通过该实例来确定其具体的类型参数(我可以确定 ArrayList 需要类型参数)。在这篇文章中,我将向您展示一些可用的泛型元数据的一个实际用途,它简化了策略接口及其实现的设计,这些接口和实现因它们处理的对象类型而异。
我在许多应用程序中看到的一种模式是使用某种策略接口,其具体实现分别处理特定的输入类型。例如,考虑投资银行业务中的一个简单场景。任何上市公司都可以发布公司行为,从而导致其股票发生实际变化。一个主要例子是股息支付,它按每股向所有股东支付一定数量的现金、股票或财产。在投资银行内部,接收这些事件的通知并计算由此产生的权益非常重要,以便使交易账簿与正确的股票和现金价值保持同步。
作为一个具体示例,考虑 BigBank 持有 1,200,000 股 IBM 股票。IBM 决定派发每股 0.02 美元的股息。因此,BigBank 需要接收股息行动的通知,并在适当的时间更新其交易账簿,以反映额外的 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>,导致 E 在 DividendEntitlementCalculator 中被相应地替换为 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 来确保类型安全。为什么会这样,以及为什么这个例子实际上可以编译,我将在接下来的文章中继续讨论……