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

时间:2012-02-21 07:58:23

标签: java generics compilation

我们在代码中简化了泛型的一些定义和用法。 现在我们得到一个有趣的案例,举个例子:

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错误。所以这意味着我们的代码没有任何问题,我们可以坚持下去。 问题已经回答了,在我自己的回答超过两天之后我会将其标记为。 感谢大家的建设性反馈。

7 个答案:

答案 0 :(得分:10)

在java中,如果在方法签名的参数返回类型中使用泛型类型,则可以执行泛型方法。在您的示例通用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

当您在T方法签名中不使用doSomethingWeird时使用此引文,您在调用时间(T方法)中指定entryPoint的原始类型是什么?

答案 1 :(得分:0)

我没有检查代码(用两个编译器编译)。在更基本的层面上,语言规范中有很多奇怪的东西(好吧,检查数组声明......)。但是,我认为上面的设计很少“过度设计”,如果我正确地翻译了需求,可以用Factory模式实现所需的功能,或者如果你使用的是一些IoC框架(Spring?),那么查找方法注入可以为你做神奇的事。我认为代码更直观,易于阅读和维护。

答案 2 :(得分:0)

我认为原因是不同的。 “T obj = getMyClass();”行上的类型T不是这样。是不知道的 - 事实上,由于定义“T延伸A&amp; B”,其擦除是A 。这称为多边界,并且适用:“当使用多边界时,边界中提到的第一种类型用作类型变量的擦除。”

答案 3 :(得分:0)

根据@ 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();
}

答案 4 :(得分:0)

这就是OpenJDK的人回答我的问题:

  

这些失败是由JDK 6编译器没有造成的   正确实现类型推断。已经付出了很多努力   JDK 7编译器为了摆脱所有这些问题(你的程序   在JDK中编译好7)。但是,有些推论有所改进   需要源代码不兼容的更改,这就是我们无法向后移植的原因   这些修复在JDK 6发行版中。

所以这对我们意味着:我们的代码完全没有问题,Oracle也正式支持。我们也可以坚持使用这种代码,并使用目标= 1.6的Javac 7作为我们的maven构建,而eclipse中的开发将保证我们不使用Java 7 API:D yaaahyyy !!!

答案 5 :(得分:0)

我会再次创建你所谓的“虚拟”界面AB

首先,我根本没有发现它是假的。有两个类具有相同的通用方法定义,在一个地方您需要使用其中一个,而不管它实际上是哪一个。这是继承的确切用法。所以接口AB在这里非常合适。而泛型是错误的解决方案。泛型从未打算实现继承。

其次,定义接口将删除代码中的所有泛型内容,并使其更具可读性。实际上,添加接口(或类)永远不会使您的代码可读性降低。否则,最好将所有代码放在一个类中。

答案 6 :(得分:0)

您的方法值得怀疑,因为未经检查的演员会牺牲运行时类型的安全性。考虑这个例子:

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();
    }
}

消除几个接口声明真的值得吗?