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


71

高度重复的代码通常是不好的事情,并且有些设计模式可以帮助最大程度地减少这种情况。但是,由于语言本身的限制,有时这是不可避免的。以以下示例为例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代码非常不典型,但是即使采用最佳实践,重复也确实会发生!重构并非总是可行,因为它并非总是可能的(显而易见的情况是文档中有重复项)。

显然,维护此源代码是一场噩梦。无论进行多少次重复,文档中的轻微错字或实现中的次要错误都会成倍增加。实际上,最好的例子恰好涉及此确切的类:

Google Research博客-额外,额外-全部阅读:几乎所有二进制搜索和合并排序均被破坏(软件工程师Joshua Bloch)

该错误是一个令人惊讶的微妙错误,发生在许多人认为只是一种简单明了的算法中。

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

上一行在源代码中出现11次

所以我的问题是:

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

评论要求提供另一个示例,因此我从Google Collections中提取了这个示例:com.google.common.base。谓词AndPredicate为276-310行()和312-346行(OrPredicate)。

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

  • AndPredicatevs OrPredicate(每堂课出现5次)
  • "And("vs Or("(在相应toString()方法中)
  • #andvs #or(在@seeJavadoc注释中)
  • truevs false(输入apply!可以从表达式中重写)
  • -1 /* all bits on */VS0 /* all bits off */hashCode()
  • &=VS|=hashCode()

11
听起来您由于处理原始数组的代码而特别关注重复。就个人而言,我只是通过使用通用集合和自动装箱来避免这种重复(并鼓励其他人也这样做),除非绝对必要,否则避免使用数组和原语。您是否有涉及原始数组的此重复示例?
乔·卡纳汉


由于提供了完全的重载而导致的重复性只是一个示例。我也在非重载和非原始数组处理方案中也看到了这种重复。
polygenelubricants

2
我看了一下java.util.Arrays类,发现以下javadoc片段非常有趣:“这七个基本类型中的每一个的代码在很大程度上都是相同的。
DavidBulté'3

Sun / Oracle Java库对NIO缓冲区使用了一个笨拙的模板系统。
Tom Hawtin-抢险

Answers:


32

对于绝对需要性能的人们来说,装箱和拆箱以及泛滥的收藏品绝对是不行的。

在性能计算中会发生相同的问题,您需要使用相同的复合体才能进行浮点运算和加倍运算(例如,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
 * 
 */

您能否提供有关它们使用哪些代码生成器等的更多详细信息?我对Trove不熟悉。
polygenelubricants 2010年

1
Trove FAQ中对此进行了解释,基本上,他们有一个Ant目标,该目标调用一个进行修改的脚本(如果我没记错的话): trove4j.sourceforge.net/html/faq.html (我正在学习Java高性能计算,已经看过使用过几次的技术了……我们在这里使用它,我们有自己的Java专有代码生成更多的Java代码:)
SyntaxT3rr0r 2010年

1
@polygenelubricants:如果需要使用原语,btw Trove是默认Java API的理想替代品。对于常规集合,您将需要研究Javolution或Google集合等。从很多角度来看,默认的Java集合确实非常糟糕。它适用于简单的项目,但是一旦您开始处理大量数据,它们就会很快显示出它们的极限。
语法T3rr0r 2010年

我碰巧喜欢代码生成...根本不需要讨厌。但是我将生成字节码而不是Java源代码。如果您需要在运行时生成该怎么办,是否要迫使最终用户安装JDK?
CurtainDog 2010年

1
@CurtainDog:Trove之类的项目生成源代码而不生成字节码是有原因的。在某些情况下,字节码检测很好,而源代码检测则更好。对于当前项目中我需要做什么,我们都做到了……如果您真的想在运行时(而不是字节码)进行源代码检测,则另一种选择是简单地生成.java服务器端,对其进行编译并发送它沿着电线。我并不是说您应该在以后的情况中这样做:我不仅说两者都有用途,而且都是常用的。
语法T3rr0r,2010年

16

如果您绝对必须重复代码,请遵循所提供的出色示例,并将所有代码集中在一个地方,以便在需要进行更改时可以轻松查找和修复。记录下重复的内容,更重要的是记录重复原因,以便您之后的每个人都知道这两者。


1
+1通过记录重复来增加重复文档的长度似乎一开始是个坏主意,但是要复制的东西需要修改并且没有有关重复的文档确实更糟。
Tanzelax

6

维基百科上不要重复自己(DRY)或重复就是邪恶(DIE)

在某些情况下,实施DRY原理所需的工作可能大于维护数据的单独副本的工作。在其他一些上下文中,重复的信息是不可变的,或者保持在足够严格的控制之下以至于不需要DRY。

可能没有解决此类问题的答案或技术。


4

甚至像Haskell这样的花哨的裤子语言也具有重复的代码(请参阅有关haskell和序列化的文章

看来这个问题有三种选择:

  1. 使用反思并失去表现
  2. 对您的语言使用类似模板的Haskell或Caml4p之类的预处理,并轻松完成
  3. 或者,如果您的语言支持,我个人最喜欢的使用宏(方案和lisp)

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

我认为Lisp / Scheme宏可以解决许多这些问题。


2

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

但是,我认为将这样的文件复制并粘贴到仅在内部使用的代码中是非常浪费的。我知道许多人会不同意,因为这会使他们自己的JavaDocs看起来不太干净。但是,权衡是使它们的代码更整洁,我认为这更重要。


+1您可以在Javadoc类中为每个重复方法编写一次文档,并使用简短的Javadoc方法,说“请参阅Javadoc类”
Henno Vermeulen 2015年

2

Java基本类型使您感到困惑,尤其是在涉及数组时。如果您是专门询问涉及原始类型的代码,那么我想说就是避免它们。如果使用装箱的类型,则Object []方法就足够了。

通常,您需要进行大量的单元测试,除了依靠反射之外,实际上没有任何其他事情可以做。就像您说的那样,这完全是另一个主题,但不要太怕反射。您可以先编写DRYest代码,然后对其进行概要分析,并确定反射性能的影响是否真的很糟糕,以至于无法保证写出并维护额外的代码。


2

您可以使用代码生成器使用模板来构造代码的变体。在这种情况下,java源代码是生成器的产物,而实际代码是模板。


没错,这就是我所影射的时候我说,也许Sun有自己的预处理器等
polygenelubricants

1
官方批准的方法是使用批注和批注处理器,以便在编译代码时,javac会调用批注处理器,这将依次生成源代码,以供编译器进行编译。一种非官方的方法是让注释处理器在调用时修改内部编译器数据结构。我发现的唯一免费的Java源代码生成库是CodeModel。
msalib 2010年

对于较大的代码片段来说,这似乎是合理的,但是有些重复可能会减少两个弊端,从而增加了构建过程的另一层复杂性。
dsimcha 2010年

2

给定两个声称相似的代码片段,大多数语言具有有限的功能来构造将代码片段统一为一个整体的抽象。要在您的语言无法做到时进行抽象,您必须走出语言:-{

最通用的“抽象”机制是一个完整的宏处理器,可以在实例化“宏主体”时对其应用任意计算(请考虑使用具有Turing功能的Post或字符串重写系统)。 M4GPM是典型的例子。C预处理器不是其中之一。

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

您还可以使用更有限版本的构想,通常称为“代码生成器”。这些通常不具备图灵能力,但是在许多情况下它们运行良好。这取决于您的“宏实例化”需要多么复杂。(人们尽管迷上了C ++模板机制,却还是喜欢它,它具有Turing的功能,所以人们可以用它完成真正的丑陋但令人惊讶的代码生成任务)。这里的另一个答案是Trove,它显然属于较有限但仍然非常有用的类别。

真正的通用宏处理器(例如M4)只处理文本;这使它们功能强大,但它们不能很好地处理编程语言的结构,并且在这样的mcaro处理器中编写一个类不仅可以生成代码,而且可以优化生成的结果,确实很尴尬。我遇到的大多数代码生成器都是“将此字符串插入此字符串模板”,因此无法对生成的结果进行任何优化。如果要生成任意代码并引导高性能,则需要具备Turing功能但了解生成代码结构的内容,以便可以轻松地对其进行操作(例如优化)。

这样的工具称为程序转换系统。这样的工具就像编译器一样对源文本进行解析,然后对其进行分析/转换以达到预期的效果。如果您可以在程序的源文本中放置标记(例如,结构化注释或具有它们的语言中的注释)来指示程序转换工具该做什么,那么您可以使用它来进行此类抽象实例化,代码生成和/或代码优化。(一个张贴者提出的挂钩Java编译器的建议是对该想法的一种变体)。使用通用的puprose转换系统(例如DMS Software Reengineering Tookit)意味着您可以对几乎任何语言进行此操作。


1

由于泛型,现在可以避免很多此类重复。当编写相同的代码(只是类型发生变化)时,它们是天赐的礼物。

不过,令人遗憾的是,我认为通用数组仍然没有得到很好的支持。至少到目前为止,请使用允许您利用泛型的容器。多态性也是减少这种代码重复的有用工具。

要回答有关如何处理绝对必须重复的代码的问题,请使用易于搜索的注释标记每个实例。有一些Java预处理程序,它们添加了C样式的宏。我想我记得netbeans有一个。


3
“但是,不幸的是,我认为通用数组仍然没有得到很好的支持。” -我不确定如何在Java中使用类型擦除来支持通用数组。我认为这是不可能的。
polygenelubricants 2010年

1
我看到了一种解决方法。转换对象数组或使用反射。哪一个都不漂亮,但是它们显然起作用。
patros 2010年

通常,最好避免在Java中使用数组。与数组相比,ArrayList提供了更多的功能,并且通常具有可忽略的性能成本。
贾里德·利维
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.