領先一步
VMware 提供培訓和認證,助您加速進步。
瞭解更多與客戶交流時,我經常聽到一種普遍的誤解,即認為泛型型別的所有資訊都會在 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>,導致 E 在 DividendEntitlementCalculator 中被相應地替換為 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() 將返回一個包含 Class 和 ParameterizedType 例項的陣列。
接下來,我們迭代 Type[] 並找到“原始型別”為 EntitlementCalculator 的 ParameterizedType 例項。由此,我們可以使用 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 來確保型別安全。為什麼會這樣,以及為什麼這個例子實際上可以編譯,我將在接下來的文章中繼續討論……