簡體   English   中英

如何封裝未經檢查的強制類型轉換以僅允許未經檢查的泛型類型轉換?

[英]How to encapsulate an unchecked cast to only allow unchecked generic type conversion?

我正在尋找一種“適當的”方法來減少在編譯時檢索/修改泛型類型參數所涉及的Java樣板。 通常,此樣板涉及:

  • 使用@SuppressWarnings("unchecked")
  • 明確說明目標通用類型參數。
  • 通常,創建一個其他無用的局部變量只是為了使壓縮只能應用於該台站。

作為一個理論示例,假設我想保留一個ClassSupplier的映射,以便對於每個keyClass ,其關聯的valueSupplier生成擴展了keyClass對象。

編輯2 :我將示例從Class映射到Class ,更改為Class映射到Supplier ,因為Class對象(值)對於強制類型轉換是特殊的,並且原始示例具有另一種不涉及未經檢查的強制類型轉換的解決方案(感謝@Holger)。 同樣,我僅添加一個示例來說明問題,而無需解決任何特定示例。

編輯1 :更准確地說,是從配置文件填充單個SupplierMap對象,並保存諸如“實現接口I1的對象由供應商S1提供的對象”,“ I2由S2提供”之類的信息。 在運行時,我們得到諸如I1 i1 = supplierMap.get(I1.class).get()調用,這些調用應產生一個i1.getClass() == C1.class的對象。 我對修復/快捷方式不感興趣,例如將演員表移到它不屬於的地方,例如讓get()返回Supplier<Object> 強制轉換在概念上屬於SupplierMap內部。 另外,我不太在乎這個特定的示例,而是在一般的語言問題上。

使用SupplierMap ,我不相信有一種方法可以捕獲Java中的鍵值通用參數關系,從而使get()不會涉及未經檢查的編譯時強制轉換。 具體來說,我可以擁有:

class SupplierMap {

    // no way to say in Java that keys and values are related
    Map<Class<?>, Supplier<?>> map;

    // can check the relation at compile time for put()
    <T> void put(Class<T> keyClass, Supplier<? extends T> valueSupplier) {
        map.put(keyClass, valueSupplier);
    }

    // but not for get()
    <T> Supplier<? extends T> get(Class<T> keyClass) {
        @SuppressWarnings("unchecked")
        final Supplier<? extends T> castValueSupplier = (Supplier<? extends T>) map.get(keyClass);
        return castValueSupplier;
    }
}

作為替代,可以有:

@SupressWarnings("unchecked")
<T> T uncheckedCast(Object o) {
    return (T) o;
}

<T> Supplier<? extends T> get(Class<T> keyClass) {
    return uncheckedCast(map.get(keyClass));
}

看起來好多了,但是問題是uncheckedCast可以說太強大了:它可以在編譯時將任何內容投射到其他內容中(隱藏警告)。 在運行時,我們仍然會獲得CCE,但這不是重點。 (論點是……)如果將uncheckedCast放入庫中,則可能會濫用該函數來隱藏在編譯時可以檢測到的問題。

有沒有一種方法可以定義這樣一個未經檢查的強制轉換函數,以便編譯器可以強制其僅用於更改通用類型參數?

我試過了:

// error at T<U>: T does not have type parameters
<T, U> T<U> uncheckedCast(T t) {
    return (T<U>) t;
}

<T, U extends T> U uncheckedCast(T t) {
    return (U) t;
}

void test() {
    Class<?> aClass = String.class;
    // dumb thing to do, but illustrates cast error:
    // type U has incompatible bounds: Class<capture of ?> and Class<Integer>
    Class<Integer> iClass = uncheckedCast(aClass);
}

編輯 :您是否在公共庫中看到過這種未經檢查的演員表(甚至是上面功能最強大的演員表)? 我查看了Commons Lang和Guava,但我唯一能找到的是Chronicle Core的ObjectUtils.convertTo() :在那里,傳遞eClass == null等同於功能強大的uncheckedCast ,除了它還會產生不希望的@Nullable (由其他分支機構使用)。

你說

另外,我不太在乎這個特定的示例,而是在一般的語言問題上。

但實際上,應該始終針對您要解決的實際問題來處理此類問題。 我要說的是,這種情況很少發生,因此,不管實際用例如何,都無法進行通用的通用方法來進行未經檢查的強制類型轉換。

考慮到SupplierMap ,您說

我不相信有一種方法可以捕獲Java中的鍵值通用參數關系,從而使get()不會涉及未經檢查的編譯時強制轉換。

這就是解決問題的根源,並指向了干凈的解決方案。 您必須在鍵和值之間創建一個關系,例如

class SupplierMap {
    static final class SupplierHolder<T> {
        final Class<T> keyClass;
        final Supplier<? extends T> valueSupplier;

        SupplierHolder(Class<T> keyClass, Supplier<? extends T> valueSupplier) {
            this.keyClass = keyClass;
            this.valueSupplier = valueSupplier;
        }
        @SuppressWarnings("unchecked") // does check inside the method
        <U> SupplierHolder<U> cast(Class<U> type) {
            if(type != keyClass) throw new ClassCastException();
            return (SupplierHolder<U>)this;
        }
    }

    Map<Class<?>, SupplierHolder<?>> map = new HashMap<>();

    <T> void put(Class<T> keyClass, Supplier<? extends T> valueSupplier) {
        map.put(keyClass, new SupplierHolder<>(keyClass, valueSupplier));
    }

    <T> Supplier<? extends T> get(Class<T> keyClass) {
        return map.get(keyClass).cast(keyClass).valueSupplier;
    }
}

在這里,未經檢查的類型轉換位於執行實際檢查的方法內部,該方法使讀者可以確信操作的正確性。

這是現實生活代碼中實際使用的模式。 哪一個希望解決您的問題“ 您是否在公共庫中看到了這種未經檢查的演員表(甚至是上面功能強大的演員表? ”。 我不認為任何庫都使用“完全未經檢查”的方法,而是使用具有盡可能窄的可見性且可能適合實際用例的方法。

是的,這意味着“樣板”。 對於開發人員在繼續之前應該花一些時間的操作來說,這並不壞。

請注意,可以將其擴展為完全可用的示例,而無需使用Class作為標記:

interface SomeKey<T> {}

class SupplierMap {
    static final class SupplierHolder<T> {
        final SomeKey<T> keyClass;
        final Supplier<? extends T> valueSupplier;

        SupplierHolder(SomeKey<T> keyToken, Supplier<? extends T> valueSupplier) {
            this.keyClass = keyToken;
            this.valueSupplier = valueSupplier;
        }
        @SuppressWarnings("unchecked") // does check inside the method
        <U> SupplierHolder<U> cast(SomeKey<U> type) {
            if(type != keyClass) throw new ClassCastException();
            return (SupplierHolder<U>)this;
        }
    }

    Map<SomeKey<?>, SupplierHolder<?>> map = new HashMap<>();

    <T> void put(SomeKey<T> keyClass, Supplier<? extends T> valueSupplier) {
        map.put(keyClass, new SupplierHolder<>(keyClass, valueSupplier));
    }

    <T> Supplier<? extends T> get(SomeKey<T> keyClass) {
        return map.get(keyClass).cast(keyClass).valueSupplier;
    }
}

它允許多個具有相同類型的鍵:

enum MyStringKeys implements SomeKey<String> {
    SAY_HELLO, SAY_GOODBYE        
}
public static void main(String[] args) {
    SupplierMap m = new SupplierMap();
    m.put(MyStringKeys.SAY_HELLO, () -> "Guten Tag");
    m.put(MyStringKeys.SAY_GOODBYE, () -> "Auf Wiedersehen");
    System.out.println(m.get(MyStringKeys.SAY_HELLO).get());
    Supplier<? extends String> s = m.get(MyStringKeys.SAY_GOODBYE);
    String str = s.get();
    System.out.println(str);
}

關鍵部分是,現在不可避免的未經檢查的演員表仍會通過對密鑰有效性的實際檢查得到補充。 我絕對不會沒有它。

這並不排除您根本無法檢查正確性的情況。 但是,我們可以很好地認為,像@SuppressWarnings("unchecked")注釋這樣的冗長的工件會在需要的時候指出這一權利。 即使我們有可能將其用法限制為泛型類型,使用便捷方法也只能隱藏仍然存在的問題。


回答問題的先前版本:

這實際上比您想象的要容易:

class ImplMap {
    Map<Class<?>, Class<?>> map;

    <T> void put(Class<T> keyClass, Class<? extends T> valueClass) {
        map.put(keyClass, valueClass);
    }

    <T> Class<? extends T> get(Class<T> keyClass) {
        final Class<?> implClass = map.get(keyClass);
        return implClass.asSubclass(keyClass);
    }
}

這不是一個未經檢查的操作,因為該方法asSubclass真的檢查是否implClass類的子類keyClass 假定僅通過put方法填充了映射,並且沒有任何未經檢查的操作,則此測試將永遠不會失敗。

唯一不同的是對null的處理,例如,當鍵不存在於地圖中時。 與強制轉換不同,這將引發異常,因為它是方法調用。

因此,如果允許使用不存在的鍵調用此方法,並且結果為null ,則必須顯式處理:

<T> Class<? extends T> get(Class<T> keyClass) {
    final Class<?> implClass = map.get(keyClass);
    return implClass == null? null: implClass.asSubclass(keyClass);
}

請注意,同樣

@SupressWarnings("unchecked")
<T> T uncheckedCast(Object o) {
    return (T) o;
}

如果您擁有Class對象,則沒有必要,因為這樣,您可以在其上調用cast

例如,以下內容是對ImplMap類的有效添加:

<T> T getInstance(Class<T> keyClass) {
    try {
        return keyClass.cast(map.get(keyClass).getConstructor().newInstance());
    } catch (ReflectiveOperationException ex) {
        throw new IllegalStateException(ex);
    }
}

另外需要注意的是,通過配置文件注冊接口實現聽起來像您應該看看ServiceLoader API和底層機制。 另請參見Java教程的“ 創建可擴展應用程序”一章。

您為什么不嘗試這樣的事情,而不是單獨聲明函數的類型,而是使類成為泛型,並且類處理此類T類的Class實例。

class ImplMap<T> {

    // Values are already related here
    Map<Class<T>, Class<? extends T>> map;

    // Already compiler aware of the type.
    void put(Class<T> keyClass, Class<? extends T> valueClass) {
        map.put(keyClass, valueClass);
    }

    // Compiler already aware of the type just like with 'put'.
    Class<? extends T> get(Class<T> keyClass) {
        return map.get(keyClass);
    }
}

這不涉及未經檢查的轉換,因為已經使用Map聲明定義了類型關系,並且不需要SuppressWarning (編譯器不會警告)。

盡管如果您未在ImplMap對象創建上定義類型,編譯器會在調用putget函數時警告unchecked調用,因為它根本不接受類型,並且如果您定義類型,則只能將該類型的鍵放入其中映射哪些重復。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM