利用泛型元数据

工程 | Rob Harrop | 2006年9月29日 | ...

当我与客户交谈时,经常听到一个常见的误解,那就是关于泛型类型的所有信息都从你的 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>,这导致在 DividendEntitlementCalculatorE 被适当地替换为 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() 将返回一个包含 ClassParameterizedType 实例的数组。

接下来,我们遍历 Type[] 并找到其“原始类型”是 EntitlementCalculatorParameterizedType 实例。从这里,我们可以使用 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 来确保类型安全。关于为什么它可以工作,以及为什么这个例子确实能编译的问题,我很快会写另一篇文章...

延伸阅读

证券运营

公司行为

Java 编程语言中的泛型

订阅 Spring 新闻简报

通过 Spring 新闻简报保持联系

订阅

领先一步

VMware 提供培训和认证,助您快速进步。

了解更多

获取支持

Tanzu Spring 通过一项简单的订阅,为 OpenJDK™、Spring 和 Apache Tomcat® 提供支持和二进制文件。

了解更多

即将到来的活动

查看 Spring 社区所有即将到来的活动。

查看全部