簡體   English   中英

Java泛型的特殊用法:“破解”或“提高生產力”?

[英]Special use of Java generics: 'hack' or 'nice productivity boost'?

我們在代碼中簡化了泛型的一些定義和用法。 現在我們得到一個有趣的案例,舉個例子:

public class MyWeirdClass {

    public void entryPoint() {
        doSomethingWeird();
    }

    @SuppressWarnings( "unchecked" )
    private <T extends A & B> T getMyClass() {
        if ( System.currentTimeMillis() % 2 == 0 ) {
           return (T) new MyClass_1();
        } else {
           return (T) new MyClass_2();
        }
    }

    private <T extends A & B> void doSomethingWeird() {
        T obj = getMyClass();

        obj.methodFromA();
        obj.methodFromB();
    }

    static interface A {
        void methodFromA();
    }

    static interface B {
        void methodFromB();
    }

    static class MyClass_1 implements A, B {
        public void methodFromA() {};
        public void methodFromB() {};
    }

    static class MyClass_2 implements A, B {
        public void methodFromA() {};
        public void methodFromB() {};
    }
}

現在看看MyWeirdClass中的方法'doSeomthingWeird():這個代碼將使用eclipse JDT編譯器正確編譯,但是在使用Oracle編譯器時會失敗。 由於JDT能夠生成工作字節代碼,這意味着在JVM級別,這是有效的代碼,並且“僅”Oracle編譯器不允許編譯這樣的臟(!?)內容。 我們知道Oracle的編譯器不接受調用'T obj = getMyClass();' 因為T不是真正存在的類型。 但是既然我們知道返回的對象實現了A和B,為什么不允許呢? (JDT編譯器和JVM都這樣做)。 另請注意,由於泛型代碼僅在內部私有方法中使用,因此我們不希望在類級別公開它們,使用泛型定義來污染外部代碼,這是我們不感興趣的(來自類外部)。

學校書籍解決方案將創建一個接口AB擴展A,B然而,因為我們有更多的接口,它們用於不同的組合並來自不同的模塊,所以組合所有組合的接口將顯着增加'虛擬'接口,最后使代碼可讀性降低。 從理論上講,它需要多達不同包裝器接口的N排列才能涵蓋所有情況。 “面向業務的工程師”(其他人稱之為“懶惰工程師”)解決方案是以這種方式保留代碼並開始僅使用JDT來編譯代碼。 編輯:這是Oracle的Javac 6中的一個錯誤,在Oracle的Javac 7上也沒有問題

你什么意思? 采用這種“策略”是否有任何隱患?


添加是為了避免討論(對我來說)不相關的要點: 我不是在問為什么上面的代碼不能在Oracle編譯器上編譯我知道原因而且我不想在沒有很好理由的情況下修改這種代碼它在使用其他編譯器時非常有效。 請專注於'doSomethingWeird()'方法的定義和用法(不提供特定類型)。 有充分的理由,為什么我們不應該只使用允許編寫和編譯此代碼的JDT編譯器並停止使用Oracle編譯器進行編譯,而編譯器不接受上面的代碼? (感謝您輸入)

編輯 :上面的代碼在Oracle Javac 7上正確編譯,但在Javac 6上沒有編譯。這是一個Javac 6錯誤。 所以這意味着我們的代碼沒有任何問題,我們可以堅持下去。 問題已經回答了,在我自己的回答超過兩天之后我會將其標記為。 感謝大家的建設性反饋。

在java中,如果在方法簽名的ParameterReturn Type中使用泛型類型,則可以執行泛型方法。 在您的示例通用doSomethingWeird方法中,但從未在方法簽名中使用它。 見以下樣本:

class MyWeirdClass
{
    public void entryPoint()
    {
        doSomethingWeird(new MyClass_1());
    }

    private <T extends A & B> T getMyClass()
    {
        if (System.currentTimeMillis() % 2 == 0)
        {
            return (T) new MyClass_1();
        }
        else
        {
            return (T) new MyClass_2();
        }
    }

    private <T extends A & B> void doSomethingWeird(T a)
    {
        T obj = getMyClass();

        obj.methodFromA();
        obj.methodFromB();
    }
}

這段代碼很好用。

JLS(Java語言規范)在通用方法部分中說:

Type parameters of generic methods need not be provided explicitly when a
generic method is invoked. Instead, they are almost always inferred as specified in
§15.12.2.7

doSomethingWeird方法簽名中不使用T時,通過此引用,在調用時間中指定T原始類型(在entryPoint方法中)?

我沒有檢查代碼(用兩個編譯器編譯)。 在更基本的層面上,語言規范中有很多奇怪的東西(好吧,檢查數組聲明......)。 但是,我認為上面的設計很少“過度設計”,如果我正確地翻譯了需求,可以使用Factory模式實現所需的功能,或者如果你使用的是一些IoC框架(Spring?)那么查找方法注入可以做給你的魔力。 我認為代碼更直觀,易於閱讀和維護。

我認為原因是不同的。 “T obj = getMyClass();”行上的類型T不是這樣。 是不知道的 - 事實上,由於“T延伸A&B”的定義,它的刪除是A。 這稱為多邊界 ,以下適用:“當使用多邊界時邊界中提到的第一種類型用作類型變量的擦除。”

根據@ MJM的回答,我建議你更新代碼如下。 那么你的代碼將不依賴於JVM的類型推斷。

public void entryPoint() {
    doSomethingWeird(getMyClass());
}

private <T extends A & B> T getMyClass() {
    if (System.currentTimeMillis() % 2 == 0) {
        return (T)new MyClass_1();
    } else {
        return (T)new MyClass_2();
    }
}

private <T extends A & B> void doSomethingWeird(T t) {
    t.methodFromA();
    t.methodFromB();
}

這就是OpenJDK的家伙回答我的問題:

這些失敗是由JDK 6編譯器未正確實現類型推斷的事實引起的。 為了擺脫所有這些問題,JDK 7編譯器付出了很多努力(你的程序在JDK 7中編譯得很好)。 但是,其中一些推理改進需要源代碼不兼容的更改,這就是我們無法在JDK 6版本中向后移植這些修補程序的原因。

所以這對我們意味着:我們的代碼完全沒有問題,Oracle也正式支持。 我們也可以堅持使用這種代碼,並使用帶有target = 1.6的Javac 7作為我們的maven構建,而eclipse中的開發將保證我們不使用Java 7 API:D yaaahyyy !!!

我會再次創建你所謂的“虛擬”接口AB

首先,我根本沒有發現它是假的。 有兩個類具有相同的通用方法定義,在一個地方您需要使用其中一個,而不管它實際上是哪一個。 這是繼承的確切用法。 所以界面AB在這里非常合適。 而泛型是錯誤的解決方案。 泛型從未打算實現繼承。

其次,定義接口將刪除代碼中的所有泛型內容,並使其更具可讀性。 實際上,添加接口(或類)永遠不會使您的代碼可讀性降低。 否則,最好將所有代碼放在一個類中。

您的方法值得懷疑,因為未經檢查的強制轉換犧牲了運行時類型的安全性。 考慮這個例子:

interface A {
    void methodFromA();
}

interface B {
    void methodFromB();
}

class C implements A { // but not B!
    @Override public void methodFromA() {
        // do something
    }
}

class D implements A, B {
    @Override
    public void methodFromA() {
        // TODO implement

    }
    @Override
    public void methodFromB() {
        // do something
    }
}

class Factory {
    @SuppressWarnings( "unchecked" )
    public static <T extends A & B> T getMyClass() {
        if ( System.currentTimeMillis() % 2 == 0 ) {
           return (T) new C();
        } else {
           return (T) new D();
        }
    }
}

public class Innocent {
    public static <T extends A & B> void main(String[] args) {
        T t = Factory.getMyClass();

        // Sometimes this line throws a ClassCastException
        // really weird, there isn't even a cast here!
        //                         The maintenance programmer
        t.methodFromB();
    }
}

(您可能需要多次運行程序才能看到維護程序員的困惑。)

是的,在這個簡單的程序中,錯誤是相當明顯的,但如果對象被傳遞到程序的一半,直到它的接口被錯過怎么辦? 你怎么知道壞物來自哪里?

如果那不能說服你,那么這個:

class NotQuiteInnocent {
    public static void main(String[] args) {
        // Sometimes this line throws a ClassCastException
        D d = Factory.getMyClass();
    }
}

消除幾個接口聲明真的值得嗎?

暫無
暫無

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

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