利用泛型元資料

工程 | 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 社群所有即將舉行的活動。

檢視所有