在 Java 中管理高度重复的代码和文档

高度重复的代码通常是一件坏事,有一些设计模式可以帮助最小化这种情况。然而,有时由于语言本身的限制,这是不可避免的。以 java.util.Arrays为例:

/**
* Assigns the specified long value to each element of the specified
* range of the specified array of longs.  The range to be filled
* extends from index <tt>fromIndex</tt>, inclusive, to index
* <tt>toIndex</tt>, exclusive.  (If <tt>fromIndex==toIndex</tt>, the
* range to be filled is empty.)
*
* @param a the array to be filled
* @param fromIndex the index of the first element (inclusive) to be
*        filled with the specified value
* @param toIndex the index of the last element (exclusive) to be
*        filled with the specified value
* @param val the value to be stored in all elements of the array
* @throws IllegalArgumentException if <tt>fromIndex &gt; toIndex</tt>
* @throws ArrayIndexOutOfBoundsException if <tt>fromIndex &lt; 0</tt> or
*         <tt>toIndex &gt; a.length</tt>
*/
public static void fill(long[] a, int fromIndex, int toIndex, long val) {
rangeCheck(a.length, fromIndex, toIndex);
for (int i=fromIndex; i<toIndex; i++)
a[i] = val;
}

上面的代码片段在源代码中出现了8次,除了 完全相同的方法体之外,文档/方法签名几乎没有变化,对于根数组类型 int[]short[]char[]byte[]boolean[]double[]float[]Object[]各出现一次。

我相信,除非一个人诉诸于反思(这是一个完全不同的主题本身) ,这种重复是不可避免的。我理解作为一个实用程序类,如此高度集中的重复 Java 代码是非常不典型的,但是即使使用最佳实践 重复确实会发生!重构并不总是有效的,因为它并不总是可行的(显而易见的情况是当重复出现在文档中时)。

显然,维护这个源代码是一场噩梦。文档中的一个轻微输入错误,或者实现中的一个小错误,都会乘以重复的次数。事实上,最好的例子恰好涉及到这个类:

谷歌研究博客-额外,额外-阅读所有关于它: 几乎所有的二进制搜索和合并排序被破坏(由 Joshua Bloch,软件工程师)

这个错误非常微妙,很多人认为它只是一个简单明了的算法。

    // int mid =(low + high) / 2; // the bug
int mid = (low + high) >>> 1; // the fix

以上线路 在源代码中出现11次

所以我的问题是:

  • 在实践中,这些重复的 Java 代码/文档是如何处理的? 它们是如何开发、维护和测试的?
    • 你是否从“原创”开始,尽可能使它成熟,然后根据需要复制粘贴,并希望自己没有犯错?
    • 如果你确实在原版中犯了错误,那就到处修复它,除非你愿意删除副本并重复整个复制过程?
    • 您也对测试代码应用了同样的过程吗?
  • 对于这类事情,Java 会从某种限制使用的源代码预处理中获益吗?
    • 也许 Sun 有自己的预处理器来帮助编写、维护、记录和测试这类重复的库代码?

一个注释请求另一个例子,所以我从 Google Collection 中提取了这个例子: Com.google.common.base行276-310(AndPredicate) vs 行312-346(OrPredicate)。

这两个类的来源是相同的,除了:

  • AndPredicateOrPredicate(每个类出现5次)
  • "And("Or("(在各自的 toString()方法中)
  • #and#or(在 @see Javadoc 注释中)
  • true vs false(在 apply中; !可以从表达式中重写)
  • hashCode()中的 -1 /* all bits on */0 /* all bits off */
  • hashCode()中的 &=|=
5664 次浏览

多亏了泛型,现在可以避免许多此类重复。当编写相同的代码时,只有类型会发生变化,它们是天赐之物。

遗憾的是,我认为通用数组仍然没有得到很好的支持。至少现在,使用允许您利用泛型的容器。多态性也是减少这种代码复制的有用工具。

为了回答您关于如何处理绝对必须复制的代码的问题... ... 给每个实例添加容易搜索的注释。现在有一些 Java 预处理器,它们添加了 C 风格的宏。我记得网豆也有一个。

如果您一定要复制代码,请遵循您给出的优秀示例,并将所有这些代码分组到一个地方,以便在必须进行更改时容易找到并修复它们。记录副本,更重要的是记录 复制的原因,这样后面的每个人都会知道这两者。

来自 维基百科不要重复你自己(DRY)或复制是邪恶的(DIE)

在某些情况下,实施 DRY 理念所需的努力可能大于维护数据的单独副本所需的努力。在其他一些上下文中,重复的信息是不可变的,或者保持在一个足够紧密的控制之下,以便不需要 DRY。

可能没有答案或技术来防止这样的问题。

我知道 Sun 必须为 JavaSE 库代码编写这样的文档,也许其他第三方库编写者也需要这样做。

然而,我认为在像这样的文件中复制和粘贴只在内部使用的代码是一种彻底的浪费。我知道很多人不会同意,因为这会让他们的 JavaDocs 看起来不那么干净。然而,取舍是让他们的代码更干净,在我看来,这才是更重要的。

Java 原语类型会让你很不爽,特别是当涉及到数组的时候。如果您特别想了解涉及基元类型的代码,那么我建议您尽量避免使用它们。如果使用装箱类型,Object []方法就足够了。

一般来说,您需要大量的单元测试,而且除了求助于反射之外,实际上没有其他任何事情可以做。就像你说的,这完全是另一回事,但不要太害怕反思。首先编写 DRYest 代码,然后对其进行概要分析,并确定反射性能的下降是否真的严重到需要编写和维护额外的代码。

可以使用代码生成器使用模板构造代码的变体。在这种情况下,Java 源代码是生成器的产品,而真正的代码是模板。

对于那些绝对需要性能的人来说,装箱和拆箱以及一般化的集合等等都是大禁忌。

同样的问题也发生在性能计算中,在浮点数和双精度数的计算中需要同样的复杂度(比如 Goldberd 的 每个计算机科学家应该知道的关于浮点数的知识论文中的一些方法)。

当处理类似数据量时,宝藏TIntIntHashMap绕着 Java 的 HashMap<Integer,Integer>运行是有原因的。

现在,Trove 集合的源代码是如何编写的?

当然是通过使用源代码工具:)

有几个性能更高的 Java 库(比默认的 Java 库高得多)使用代码生成器来创建重复的源代码。

我们都知道“源代码插装”是邪恶的,代码生成是垃圾,但是那些真正知道自己在做什么的人(比如那些写 Trove 的人)就是这么做的:)

值得一提的是,我们生成的源代码包含如下重大警告:

/*
* This .java source file has been auto-generated from the template xxxxx
*
* DO NOT MODIFY THIS FILE FOR IT SHALL GET OVERWRITTEN
*
*/

鉴于两个代码片段声称是相似的,大多数语言构建抽象的工具都很有限,这些抽象将代码片段统一为一个整体。当你的语言做不到抽象时,你必须走出语言:-{

最通用的“抽象”机制是一个完整的宏处理器,它可以在实例化“宏体”时对其应用任意的计算(想想 后期或字符串重写系统,它具有图灵功能)。 M4 GPM是典型的例子,C 预处理器不是其中之一

如果您有这样一个宏处理器,您可以构造一个“抽象”作为一个宏,并在您的“抽象”源文本上运行宏处理器来生成您编译和运行的实际源代码。

您还可以使用更有限的版本的想法,通常称为“代码生成器”。这些通常不是图灵能力,但在许多情况下,他们工作得足够好。这取决于您的“宏实例化”需要多么复杂。(人们之所以迷恋 C + + 模板机制,是因为尽管它很丑陋,但它具有 图灵功能,因此人们可以用它完成真正丑陋但令人惊叹的代码生成任务)。 这里的另一个答案提到了 Trove,它显然属于更有限但仍然非常有用的类别。

真正通用的宏处理器(比如 M4)只处理文本; 这使得它们功能强大,但是它们不能很好地处理编程语言的结构,而且在这样一个宏处理器中编写一个不仅可以生成代码,而且可以优化生成结果的生成器是非常尴尬的。我遇到的大多数代码生成器都是“将此字符串插入此字符串模板”,因此不能对生成的结果进行任何优化。 如果你想生成任意的代码和高性能的引导,你需要一些东西是图灵能力,但理解生成的代码的结构,以便它可以很容易地操作(例如,优化)。

这样的工具称为 程序转换系统。这种工具像编译器一样解析源文本,然后在其上进行分析/转换,以达到预期的效果。如果你可以在程序的源文本中加入标记(例如,有结构化注释的语言中的注释)来指导程序转换工具做什么,那么你就可以使用它来执行这样的抽象实例化、代码生成和/或代码优化。(一位发帖人建议连接到 Java 编译器,就是这种想法的一种变体)。使用一个通用的普通转换系统(例如 软件再造工具包)意味着您可以对基本上任何语言进行这种转换。

甚至像 Haskell 这样的花哨的裤子语言也有重复代码(请看我关于 Haskell 和连载的文章)

这个问题似乎有三种选择:

  1. 使用反射会导致性能下降
  2. 对于你的语言,使用类似 Template Haskell 或 Caml4p 等效的预处理,并忍受它们的肮脏
  3. 或者如果您的语言支持的话,我个人最喜欢使用宏(scheme 和 lisp)

我认为宏与预处理不同,因为宏通常使用与目标语言相同的语言,因为预处理是不同的语言。

我认为 Lisp/Scheme 宏可以解决很多这样的问题。