利用泛型元數據

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

在與客戶交談時,我經常聽到一個常見的誤解,認為所有關於泛型類型的資訊都會從 Java 類別檔案中刪除。 這完全是不正確的。 所有靜態泛型資訊都會被保留,只有關於個別實例的泛型資訊才會被刪除。 因此,如果我有一個類別 Foo 實作 List<String>,那麼我可以確定 Foo 實作了由 String 參數化的 List 介面(在執行期間)。 但是,如果我在執行期間實例化 ArrayList<String> 的實例,我無法取得該實例並確定其具體類型參數(我可以確定 ArrayList 需要類型參數)。 在本文中,我將向您展示一些可用的泛型元數據的實際用途,這些元數據簡化了策略介面和實作的建立,它們因處理的物件類型而異。

我在許多應用程式中看到的一種模式是使用某種類型的策略介面,其具體實作各自處理特定的輸入類型。 例如,考慮一個來自投資銀行界的簡單場景。 任何公開交易的公司都可以發布公司行為,這些行為會對其股票帶來實際變化。 其中一個關鍵範例是股息支付,它會向所有股東支付每股一定數量的現金、股票或財產。 在投資銀行內部,接收這些事件的通知並計算由此產生的權利非常重要,以便使交易帳簿保持最新的正確股票和現金價值。

作為一個具體的例子,考慮一下持有 1,200,000 股 IBM 股票的 BigBank。 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.getInterfaces() 大不相同,後者傳回 Class[]。 呼叫 DividendEntitlementCalculator.class.getInterfaces() 傳回一個表示 EntitlementCalculator 類型的 Class 實例。 呼叫 DividendEntitlementCalculator.class.getGenericInterfaces() 傳回一個表示 EntitlementCalculator 類型的 ParameterizedType 實例,該類型具有 DividendCorporateActionEvent 的類型參數。 在具有泛型和非泛型介面的類別上呼叫 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 社群中所有即將舉行的活動。

查看全部