【问题标题】:Special use of Java generics: 'hack' or 'nice productivity boost'?Java 泛型的特殊用途:“hack”还是“提高生产力”?
【发布时间】:2012-02-21 07:58:22
【问题描述】:

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

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

【问题讨论】:

  • oracle 编译器是标准的,默认与外部工具(Maven、Ant 等)一起使用。而且看起来 Oracle 是对的,而 Eclipse 编译器是错误的并且有一个漏洞。因此,这个错误很有可能在未来的版本中得到修复,这将使您的代码完全无法编译。你不想这样,是吗?
  • 您的示例根本没有显示使用泛型的相关性。你引入一个虚拟泛型,选择你的词,避免一个虚拟接口?对不起 - 这没有任何意义。看你的代码,没有看你的评论,我的想法就是创建一个接口 AB,它简化了整个事情。你的 doSomethingWeird 方法有一个从不使用的类型参数,并且总是返回相同的东西,忽略这个无用的参数。
  • 您好,感谢您的反馈。这是我的担心,我不想走冒险的路,这可能会导致将来出现一些大问题。问题是,不清楚这是否是 JDT 编译器中的“错误”,据我了解,这是 JLS 未涵盖的情况,所以如果我有一些保证,至少JDT,将继续支持这个...功能。然后我们可能会在我们的代码中保留一些带有这些泛型(错误)使用的案例。我最好在 JDT 和 OpenJDK 邮件列表上问这个问题...
  • @DaniloTommasina 有一个完整的术语来表示“[此处的语言规范名称] 未涵盖”,它是未定义的行为。它在 Java 中并不常见,因为它的回旋余地比 C++ 小,但在某些情况下,这就是其中之一。要回答您的问题 -- ,您不能确定 JDT 是否会继续支持这一点。它现在可能会生成“工作”(无论如何根据您的定义)代码,明天可以免费生成独角兽。
  • @TC1 是的,我同意这一点。我在 JDT 和 OpenJDK 编译器邮件列表上都询问过……让我们看看他们对此有何看法;)

标签: java generics compilation


【解决方案1】:

在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 Language Specification) 在 Generic Method 部分说:

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 方法中)?

【讨论】:

  • 嗨,这正是重点......我们在调用 doSomethingWeird() 时没有指定类型(来自我的原始帖子),这正是 Oracle 编译器不喜欢的“hack” (可以理解),但是 JDT 编译器允许这样做,我的问题是:如果它提高了我们的生产力,是否有充分的理由,为什么我们不应该使用它?
  • 问题在于调用doSomethingWeird 方法,而不是声明它。您的评论是正确的,但是当您想调用 doSomethingWeird 时会发生什么(您知道不能在调用时指定方法原始类型)?当您调用 doSomethingWeird 时,原始类型是什么?没有任何原始类型?
  • 是的,我知道...调用“doSomethingWeird”时没有明确定义的类型,这是 Oracle 编译器不喜欢的,但是 JDT 编译器对此没有问题...但是在这种情况下,我不需要在调用 'doSomethingWeird()' 方法时声明一个类型
  • 我通过 Eclipse JDK 编译了您的原始示例,但出现编译错误。
  • 您可以进行显式调用,而不是将参数传递给doSomethingWeirdthis.&lt;MyClass_2&gt;doSomethingWeird(); (我知道这对你来说可能不是有用的设计)。
【解决方案2】:

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

【讨论】:

  • mmh,忘记创建 MyClass_X 实例的部分,这只是示例代码,如何创建实例与我的问题无关。只关注我对“doSomethingWeird()”方法的原始定义的泛型的奇怪用法。该方法使用了一种“虚拟”类型,实际上它并不存在于“通用/共享”类中,而只是作为 A 和 B 接口的具体实现。
【解决方案3】:

我认为原因不同。 “T obj = getMyClass();”行上的类型 T 不是真的是未知的——事实上,由于定义“T extends A & B”,它的擦除是A。这称为多重界限,适用于:“当使用多重界限时,界限中提到的第一个类型被用作类型变量的擦除。”

【讨论】:

  • 是的,“T obj = getMyClass();”众所周知!但是调用“doSomethingWeird()”时推断的类型是未知的,会导致 Oracle 编译器出现问题。
  • 我认为这会有所帮助,即第二个回复:stackoverflow.com/questions/6429843/…
  • @binary_runner:您能否直接链接到“第二个”答案?我怀疑每个用户的顺序总是相同的:-)
  • @binary_runner 感谢您的链接。
【解决方案4】:

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

【讨论】:

    【解决方案5】:

    我会再次创建你所谓的“虚拟”接口AB

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

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

    【讨论】:

    • 好吧,这样看...情况下(也是我们的情况)代码肯定会变得不可读。
    • 这样使用有什么难读的?拥有 10 个或更多虚拟包装器接口,编写和维护所有不必要的代码,这些代码被划分在不同的 maven 模块中……您真的是说这会使代码更具可读性或更易于维护吗?这里泛型的使用被封装在它的类中,没有无关的东西对外暴露,实际上我可能会想到称之为“干净的设计”......:S
    • 啊,顺便说一句……即使是手机上的短信也从来没有作为客户的消息系统,但它变成了它,让电信公司用它赚了很多钱。因此,即使泛型不是为了支持这种特殊情况,它们也会支持,如果它提高了我们的生产力,我看不出有什么理由不应该这样使用它们:)
    【解决方案6】:

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

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

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

    【讨论】:

      【解决方案7】:

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

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

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

      【讨论】:

      • 这段代码真的可以编译吗?返回 (T) 新 C(); T 与 C 类不兼容,编译器应该能够看到并说即使使用 SuppressWarning 注释也无法进行强制转换
      • 啊,顺便说一句...是的,它只是几个接口,我还没有问过这个问题。 :) 在我们的例子中,许多接口本身包含一些泛型声明。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2010-11-20
      • 1970-01-01
      • 1970-01-01
      • 2013-04-09
      • 1970-01-01
      • 2010-10-13
      • 2010-11-29
      相关资源
      最近更新 更多