利用泛型元数据

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

与客户交流时,我经常听到一种普遍的误解,即认为泛型类型的所有信息都会在 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>,导致 EDividendEntitlementCalculator 中被相应地替换为 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 社区所有即将举行的活动。

查看所有