利用泛型元数据

工程 | Rob Harrop | 2006 年 9 月 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());
    }
}

在这里您可以看到我们维护了一个Map,它将CorporateActionEvent类型映射到EntitlementCalculator实现,我们用它来为每个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中用DividendCorporateActionEvent替换E,从而无需进行类型检查和强制转换。

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.getInterfaces()有很大不同,后者返回Class[]。调用DividendEntitlementCalculator.class.getInterfaces()返回一个表示EntitlementCalculator类型的单个Class实例。调用DividendEntitlementCalculator.class.getGenericInterfaces()返回一个表示EntitlementCalculator类型并带有一个DividendCorporateActionEvent类型参数的单个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 社区中所有即将举行的活动。

查看全部