利用泛型元資料

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

檢視全部