Java泛型的特殊使用:“hack”或者“生产力的大幅提升”?

发布于 2025-01-07 17:38:11 字数 2040 浏览 3 评论 0 原文

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

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 编译器不接受上面的代码? (感谢您的输入)

编辑:上面的代码在 Oracle Javac 7 上可以正确编译,但在 Javac 6 上不能正确编译。这是 Javac 6 的错误。所以这意味着我们的代码没有任何问题,我们可以坚持下去。 问题已得到解答,我将在我自己的答案两天超时后将其标记为这样。 感谢大家的建设性反馈。

we have been simplifying some definition and usage of generics in our code.
Now we got an interesting case, take this example:

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

Now look at the method 'doSeomthingWeird() in MyWeirdClass:
This code will compile correctly using the eclipse JDT compiler, however it will fail when using the Oracle compiler. Since the JDT is able to produce working byte-code, it means that at JVM level, this is valid code and it is 'only' the Oracle compiler not allowing to compile such dirty(!?) stuff.
We understand that Oracle's compiler won't accept the call 'T obj = getMyClass();' since T is not a really existent type. However since we know that the returned object implements A and B, why not allowing it? (The JDT compiler and the JVM do).
Note also that since the generics code is used only internally in private methods, we do not want to expose them at class level, polluting external code with generics definitions, that we are not interested at (from outside the class).

The school book solution will be to create an interface AB extends A,B however since we have a larger number of interfaces which are used in different combinations and coming from different modules, making shared interfaces for all the combinations will significantly increase the number of 'dummy' interfaces and finally make the code less readable. In theory it would require up to N-permutations of different wrapper interfaces in order to cover all the cases.
The 'business-oriented-engineer'(other people call it the 'lazy-engineer') solution would be to leave the code this way and start using only JDT for compiling the code.
Edit: It's a bug in Oracle's Javac 6 and works without problems also on Oracle's Javac 7

What do you mean? Are there any hidden dangers by adopting this 'strategy'?


Addition in order to avoid discussion on (for me) not relevant points:
I am not asking why the code above does not compile on Oracle's compiler I know the reason and I do not want to modify this kind of code without a very good reason if it works perfectly when using another compiler.
Please concentrate on the definition and usage (without giving a specific type) of the method 'doSomethingWeird()'.
Is there a good reason, why we should not use only the JDT compiler that allows writing and compiling this code and stop compiling with the Oracle's compiler, which will not accept the code above?
(Thanks for input)

Edit: The code above compiles correctly on Oracle Javac 7 but not on Javac 6. It is a Javac 6 bug. So this means that there is nothing wrong in our code and we can stick on it.
Question is answered, and I'll mark it as such after the two days timeout on my own answer.
Thanks everybody for the constructive feedback.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(7

べ映画 2025-01-14 17:38:11

在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

通过这个引用,当你在doSomethingWeird方法签名中不使用T时,你会做什么在调用时指定T的原始类型(在entryPoint方法中)?

In java you can do generic methods if generic type used in Parameter or Return Type of method signature. In your sample generic doSomethingWeird method but never used it in method signature.
see following sample:

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

This code work fine.

JLS(Java Language Specification) says in Generic Method part:

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

By this quotation when you don't use T in doSomethingWeird method signature,What you specify raw type of T in invoking time(in entryPoint method)?

丘比特射中我 2025-01-14 17:38:11

我没有检查代码(用两个编译器编译)。在更基础的语言规范中有很多奇怪的东西(好吧,检查数组声明......)。但是,我相信上面的设计有点“过度设计”,如果我正确地翻译了需求,则可以通过 工厂 模式或者如果您正在使用一些 IoC 框架(Spring?),那么查找方法注入可以为您带来魔力。我认为代码会更加直观并且易于阅读和维护。

I did not check the code (compile with both compilers). There is a lot of weird stuff in language specification at even more basic level (well, check the array declaration...). However, I believe that the design above is little "over-engineered" and if I translate the need correctly, the required functionality can be achieved with Factory pattern or if you are using some IoC framework (Spring?) then lookup method injection can do the magic for you. I think the code will be more intuitive and easy to read and to maintain.

笑看君怀她人 2025-01-14 17:38:11

我认为原因不同。 “T obj = getMyClass();”行中的类型 T 是不正确的。未知 - 事实上,由于定义“T extends A & B”,其擦除是 A。这称为多重边界,并且适用以下内容:“使用多重边界时,边界中提到的第一个类型将用作类型变量的擦除。”

I think the cause is different. That's not true that the type T on line "T obj = getMyClass();" is unknow - in fact, because of definition "T extends A & B", its erasure is A. This is called multiple bounds and following applies: "When a multiple bound is used, the first type mentioned in the bound is used as the erasure of the type variable."

音盲 2025-01-14 17:38:11

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

Based on @MJM's answer, I suggest you to update the code as below. Then your code will not rely on JVM's type infer.

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();
}
梦情居士 2025-01-14 17:38:11

我会再次创建您所说的“虚拟”接口AB

首先,我一点也不觉得它是假的。有两个类具有相同的通用方法定义,并且在某个地方您需要使用其中之一,而不管它实际上是哪一个。这就是继承的确切用途。所以接口 AB 非常适合这里。泛型在这里是错误的解决方案。泛型从来就不是用来实现继承的。

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

I would go again for creating what you called a "dummy" interface AB.

First of all, I don't find it dummy at all. There are two classes with the same common method definitions and at one place you need to use one of them regardless of which one it is actually. That is the exact useage of inheritence. So the interface AB fits perfect here. And generics is the wrong solution here. Generics were never meant to implement inheritance.

Second, defining the interface will remove all the generics stuff in your code, and will make it much more readable. Actually adding an interface (or class) never makes your code less readable. Otherwise, it would be better to put all the code in a single class.

尸血腥色 2025-01-14 17:38:11

这是 OpenJDK 人员对我的问题的回答:

这些失败是由于 JDK 6 编译器不支持
正确实现类型推断。已经付出了很多努力
JDK 7编译器为了摆脱所有这些问题(你的程序
在 JDK 7 中编译良好)。然而,其中一些推理改进
需要源不兼容的更改,这就是我们无法向后移植的原因
JDK 6 版本中修复了这些问题。

所以这对我们来说意味着:我们的代码绝对没有任何问题,并且也得到了 Oracle 的官方支持。我们也可以坚持这种类型的代码,并使用 Javac 7 和 target=1.6 进行我们的 Maven 构建,而在 Eclipse 中进行开发将保证我们不使用 Java 7 API :D yaaahyyy!!!

This is what the OpenJDK guys answered to my question:

These failures are caused by the fact that JDK 6 compiler doesn't
implement type-inference correctly. A lot of effort has been put into
JDK 7 compiler in order to get rid of all these problems (your program
compiles fine in JDK 7). However, some of those inference improvements
require source incompatible changes, which is why we cannot backport
these fixes in the JDK 6 release.

So this means for us: There is absolutely nothing wrong with our code and is officially supported also by Oracle. We can also stick to this kind of code and use Javac 7 with target=1.6 for our maven builds while development in eclipse will guarantee that we do not use Java 7 APIs :D yaaahyyy!!!

能否归途做我良人 2025-01-14 17:38:11

您的方法是有问题的,因为未经检查的强制转换牺牲了运行时类型安全性。考虑这个例子:(

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

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

Your approach is questionable, because the unchecked casts sacrifice runtime type safety. Consider this example:

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

(You may have to run the program several times to see what confused the maintenance programmer.)

Yes, in this simple program the error is rather obvious, but what if the object is passed around half your program until its interface is missed? How would you find out where the bad object came from?

If that didn't convince you, what about this:

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

Is eliminating a couple interface declarations really worth that?

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文