【问题标题】:Code duplication caused by primitive types: How to avoid insanity?原始类型导致的代码重复:如何避免精神错乱?
【发布时间】:2012-03-12 11:05:24
【问题描述】:

在我的一个 Java 项目中,由于 Java 处理 (not) 原语的方式,代码重复困扰着我。在不得不手动将相同的更改复制到四个不同的位置(intlongfloatdouble再次之后,第三次再次再次我真的很接近(?)捕捉。

在 StackOverflow 上时不时地以各种形式提出这个问题:

共识似乎收敛到两种可能的选择:

  • 使用某种代码生成器。
  • 你能做什么? C'est la vie!

嗯,第二个解决方案是我现在正在做的事情,它正逐渐对我的理智变得危险,就像well known torture technique一样。

自从提出这些问题并出现 Java 7 以来已经过去了两年。因此,我希望有一个更简单和/或更标准的解决方案。

  • Java 7 是否有任何更改可以缓解这种情况下的压力?我在精简的更改摘要中找不到任何内容,但也许某处有一些晦涩难懂的新功能?

  • 虽然源代码生成是一种替代方法,但我更喜欢使用标准 JDK 功能集支持的解决方案。当然,使用 cpp 或其他代码生成器也可以,但它会增加更多依赖项并需要对构建系统进行更改。

    JDK 似乎支持的唯一一种代码生成系统是通过注释机制。我设想一个处理器可以像这样扩展源代码:

    @Primitives({ "int", "long", "float", "double" })
    @PrimitiveVariable
    int max(@PrimitiveVariable int a, @PrimitiveVariable int b) {
        return (a > b)?a:b;
    }
    

    理想的输出文件将包含此方法的四个请求变体,最好带有相关的 Javadoc cmets 等。是否有注释处理器来处理这种情况?如果没有,建造一个需要什么?

  • 也许最近出现了其他一些技巧?

编辑:

重要提示:除非我有理由,否则我不会使用原始类型。即使是现在,在某些应用程序中使用盒装类型也会对​​性能和内存产生非常实际的影响。

编辑 2:

max() 为例,允许使用在所有数字盒装类型中都可用的compareTo() 方法。这有点棘手:

int sum(int a, int b) {
    return a + b;
}

如果不实际编写六七次,如何才能为所有数字盒装类型支持这种方法?

【问题讨论】:

  • 棘手而详尽的问题。竖起大拇指!
  • 现在使用的大多数代码生成都是在运行时在 ClassLoader 中生成字节码。在您的情况下,您需要用于自己的代码编译的类文件......所以我会看看编译时注释处理器
  • 这个人提出了一个非常精细的解决方案,它带有注释处理器并使用 Velocity 生成类。你不需要使用 Velocity,只需要写一些更简单的 IMO deors.wordpress.com/2011/10/08/annotation-processors

标签: java primitive code-duplication


【解决方案1】:

如果我仍然想要一个原始类型,我倾向于使用像 longdouble 这样的“超级类型”。性能通常非常接近,并且避免了产生大量变化。顺便说一句:64 位 JVM 中的寄存器无论如何都是 64 位的。

【讨论】:

  • +1 有趣的一点:除非它与数组有关,否则具有特定于 bytecharshort 的代码可能是无用的 - 根据 JLS,所有这些值都必须是对于大多数操作,提升为int,因此字节码与int的代码相同...
  • 在溢出的情况下你不会得到意想不到的结果吗?您的所有算法都必须考虑原始数字类型的最小值/最大值。
  • 通常下溢或上溢通常是不可取的,但您通常可以在最后强制转换为适当的类型。
  • 我会接受这个答案,因为在大多数情况下我最终都会这样做。不幸的是,目前似乎没有其他选项可以用于生产 - 至少在不使用不同的编程语言的情况下不会......
【解决方案2】:

你为什么对原语感兴趣?包装器非常轻量级,并且自动装箱,其余部分由泛型完成:

public static <T extends Number & Comparable<T>> T max(T a, T b) {
    return a.compareTo(b) > 0 ? a : b;
}

这一切都能正确编译和运行:

public static void main(String[] args) {
    int i = max(1, 3);
    long l = max(6,7);
    float f = max(5f, 4f);
    double d = max(2d, 4d);
    byte b = max((byte)1, (byte)2);
    short s = max((short)1, (short)2);
}

已编辑

OP 询问了针对 sum() 的通用自动装箱解决方案,并且会在这里。

public static <T extends Number> T sum(T... numbers) throws Exception {
    double total = 0;
    for (Number number : numbers) {
        total += number.doubleValue();
    }
    if (numbers[0] instanceof Float || numbers[0] instanceof Double) {
        return (T) numbers[0].getClass().getConstructor(String.class).newInstance(total + "");
    }
    return (T) numbers[0].getClass().getConstructor(String.class).newInstance((total + "").split("\\.")[0]);
}

这有点蹩脚,但不像做大量instanceof 并委托给一个完全类型化的方法那么蹩脚。 instanceof 是必需的,因为虽然所有Numbers 都有一个String 构造函数,但Numbers 除了FloatDouble 只能解析一个整数(没有小数点);尽管总数将是一个整数,但我们必须在将 Double.toString() 中的小数点发送到其他类型的构造函数之前删除它。

【讨论】:

  • 性能,性能,性能。有一次我使用盒装类型时,我的算法变慢了 3-4 倍 - 然后我的内存用完了......
  • 诚然当时的JVM并没有使用压缩指针,但我认为性能还是会差一些。而且我已经有相当多的现有代码需要大量重构......
  • @Bohemian:使用 Oracle 服务器 VM 1.7.0_02,您的通用方法的运行时间比等效的 int max(int, int) 实现长 7 倍,比 long max(long,长)。即使绝对差异很小,相对差异也很可能是相关的。对于算术运算:是否甚至可以用 Number 实例做简单的事情,比如加减法?
  • @thkala 您的盒装类型内存不足。例如,您是否使用过 Integer.valueOf ?你想减少全局执行时间,你的代码是多线程的吗?
  • 我将第一个赏金授予此答案,因为我必须承认我以选择 max() 为例进行了设置。我真的很想知道您在我更新的问题中是否有针对sum() 的解决方案,即使是不处理性能问题的解决方案...
【解决方案3】:

Java 7 是否有任何改变可以缓解这种情况下的压力?

没有。

是否有注释处理器来处理这种情况?

我不知道。

如果没有,建造一个需要什么?

时间或金钱。 :-)

在我看来,这就像一个问题空间,很难提出一个运行良好的通用解决方案......超越琐碎的案例。传统的源代码生成或(文本)预处理器对我来说似乎更有希望。 (虽然我不是注释处理器专家。)

【讨论】:

    【解决方案4】:

    如果 Java 异常冗长让您着迷,请查看一些在 JVM 上运行并且可以与 Java 互操作的新的高级语言,例如 Clojure、JR​​uby、Scala 等。您失控的原始重复将成为非问题。但是好处远不止这些——有各种刚刚提到的语言允许你用不太详细、重复、容易出错的代码完成更多工作(与 Java 相比) )。

    如果性能是个问题,您可以退回到 Java 中寻找性能关键位(使用原始类型)。但您可能会惊讶于在高级语言中仍能获得良好性能水平的频率。

    我个人同时使用 JRuby 和 Clojure;如果您来自 Java/C/C#/C++ 背景,两者都有可能改变您对编程的看法。

    【讨论】:

    • 或时髦。不要忘记 groovy!
    • 啊,但是 OP 想要“性能、性能、性能”......对于这些特定的算法。
    • @StephenC,这是一个很好的观点。只是我已经看到很多关于 SO 的问题,人们抱怨 Java 有多冗长,并要求如何让他们的代码更简洁,对我来说答案是显而易见的;与其用 Java 做不自然的事情,不如换一种更高级的语言!
    • 赞成:如果该语言感觉笨拙,您可能应该使用另一种。至于性能,可以优化低级,也可以编写易于扩展的代码。在这种情况下,Scala 将是一个非常好的选择。
    【解决方案5】:

    呵呵。为什么不偷偷摸摸?通过反射,您可以提取方法的注释(类似于您发布的示例的注释)。然后,您可以使用反射来获取成员名称,并放入适当的类型...在 system.out.println 语句中。

    你会运行一次,或者每次你修改类。然后可以将输出复制粘贴进去。这可能会为您节省大量时间,而且开发起来也不会太难。

    嗯,至于方法的内容...我的意思是,如果您的所有方法都是微不足道的,您可以对样式进行硬编码(即 if methodName.equals("max") print return a>b:a: b等。其中methodName是通过反射确定的),或者您可以,嗯嗯......嗯。我在想内容可以很容易地复制粘贴,但这似乎更有效。

    哦!何不做另一个名为“内容”的注释,给它一个方法内容的字符串值,将它添加到成员中,现在你也可以打印出内容了。

    至少,编写这个助手所花费的时间,即使大约只要做繁琐的工作,嗯,它会更有趣,对吧?

    【讨论】:

    • 这可能更有趣,但我不希望成为继承维护代码工作的程序员。 (或者更准确地说,我不想成为那个程序员的经理……假设程序员的 Java 技能“一般”。)
    • 编译时注解处理绝对可以用来生成源文件。我看过几个教程,虽然没有一个使用原始源文件作为输入,这是我想看到的。否则,代码将变成一堆无法维护的字符串文字......
    • @thkala - 您不需要从字符串文字生成代码。如果您将源代码转换为 Velocity 或 Freemarker 模板,然后为不同的原始类型组合运行具有不同参数的模板,您将获得更多可维护的代码。
    • @StephenC 纯文本模板会混淆所有 IDE 类型检查、交叉引用和 Javadoc 创建工具。您需要从 Java 开始(例如,长时间编写代码)并让处理器为 int 生成类。
    【解决方案6】:

    您的问题非常详尽,因为您似乎已经知道所有“好”的答案。由于语言设计的原因,我们不允许使用原语作为通用参数类型,因此最佳实用答案是 @PeterLawrey 的目标。

    public class PrimitiveGenerics {
    
        public static double genericMax( double a, double b) {
            return (a > b) ?a:b;
        }
    
    
        public int max( int a, int b) {
            return (int) genericMax(a, b);
        }
        public long max( long a, long b) {
            return (long) genericMax(a, b);
        }
        public float max( float a, float b) {
            return (float) genericMax(a, b);
        }
        public double max( double a, double b) {
            return (double) genericMax(a, b);
        }
    
    
    }
    

    原始类型列表很小,希望在语言的未来发展中保持不变,double 类型是最宽/最通用的类​​型。

    在最坏的情况下,您使用 64 位变量进行计算,而 32 位就足够了。转换(微小)和将值传递给另一种方法(小)会带来性能损失,但不会创建对象,因为这是使用原始包装器的主要(并且非常大)损失。

    我还使用了一个 static 方法,因此它是在早期绑定的,而不是在运行时绑定的,虽然它只是一个,这是 JVM 优化通常会处理的事情,但它不会造成伤害反正。可能取决于实际情况。

    如果有人测试它会很可爱,但我相信这是最好的解决方案。

    更新: 根据@thkala 的评论,double 可能只代表 long-s 直到某个数量级,因为之后它会失去精度(在处理 long-s 时变得不精确):

    public class Asdf2 {
    
        public static void main(String[] args) {
            System.out.println(Double.MAX_VALUE); //1.7976931348623157E308
            System.out.println( Long.MAX_VALUE); //9223372036854775807
            System.out.println((double) Long.MAX_VALUE); //9.223372036854776E18
        }
    }
    

    【讨论】:

    • 应该指出的一点:double 在范围方面可能是最通用的原语,但在精度方面却不是。使用超过 53 位的 long 值不能存储在 double 中而不损失精度...
    • 你绝对是对的,我没有意识到这一点。 System.out.println(Double.MAX_VALUE); 1.7976931348623157E308 System.out.println(Long.MAX_VALUE); 9223372036854775807 System.out.println((double) Long.MAX_VALUE); 9.223372036854776E18
    【解决方案7】:

    从性能的角度来看(我也做了很多 CPU-bound 算法),我使用自己的不可变的拳击。这允许在 ArrayListHashMap 等集合中使用可变数字以实现高性能。

    使用重复代码制作所有原始容器需要一个漫长的准备步骤,然后您就可以使用它们。由于我还处理 2 维、3 维等值,我也为自己创建了这些值。选择权在你。

    like:
    Vector1i - 1 个整数,替换 Integer
    Vector2i - 2 个整数,替换 PointDimension
    Vector2d - 2 个双精度数,替换 @987654330 @
    Vector4i - 4 个整数,可以替换 Rectangle
    Vector2f - 2 维浮点向量
    Vector3f - 3 维浮点向量
    ...等等...
    它们都代表数学中的广义“向量”,因此是所有这些原语的名称。

    一个缺点是你不能做a+b,你有像a.add(b)这样的方法,而对于a=a+b,我选择命名像a.addSelf(b)这样的方法。如果这让您感到困扰,请查看我最近发现的Ceylon。它是 Java(JVM/Eclispe 兼容)之上的一个层,专为解决其局限性(如运算符重载)而创建。

    另一件事,当使用这些类作为Map 中的时要小心,因为当值发生变化时,排序/散列/比较会变得混乱。

    【讨论】:

      【解决方案8】:

      我同意以前的答案/cmets 说没有办法完全按照您的要求“使用标准 JDK 功能集。” 因此,您将不得不进行一些代码生成,尽管它不一定需要更改构建系统。既然你问:

      ...如果没有,建造一个需要什么?

      ...对于一个简单的案例,我认为不会太多。假设我将原始操作放在 util 类中:

      public class NumberUtils {
      
          // @PrimitiveMethodsStart
          /** Find maximum of int inputs */
          public static int max(int a, int b) {
              return (a > b) ? a : b;
          }
      
          /** Sum the int inputs */
          public static int sum(int a, int b) {
              return a + b;
          }
          // @PrimitiveMethodsEnd
      
          // @GeneratedPrimitiveMethodsStart - Do not edit below
          // @GeneratedPrimitiveMethodsEnd
      }
      

      那么我可以用不到 30 行代码编写一个简单的处理器,如下所示:

      public class PrimitiveMethodProcessor {
          private static final String PRIMITIVE_METHODS_START = "@PrimitiveMethodsStart";
          private static final String PRIMITIVE_METHODS_END = "@PrimitiveMethodsEnd";
          private static final String GENERATED_PRIMITIVE_METHODS_START = "@GeneratedPrimitiveMethodsStart";
          private static final String GENERATED_PRIMITIVE_METHODS_END = "@GeneratedPrimitiveMethodsEnd";
      
          public static void main(String[] args) throws Exception {
              String fileName = args[0];
              BufferedReader inputStream = new BufferedReader(new FileReader(fileName));
              PrintWriter outputStream = null;
              StringBuilder outputContents = new StringBuilder();
              StringBuilder methodsToCopy = new StringBuilder();
              boolean inPrimitiveMethodsSection = false; 
              boolean inGeneratedPrimitiveMethodsSection = false; 
              try {
                  for (String line;(line = inputStream.readLine()) != null;) {
                      if(line.contains(PRIMITIVE_METHODS_END)) inPrimitiveMethodsSection = false;
                      if(inPrimitiveMethodsSection)methodsToCopy.append(line).append('\n');
                      if(line.contains(PRIMITIVE_METHODS_START)) inPrimitiveMethodsSection = true;
                      if(line.contains(GENERATED_PRIMITIVE_METHODS_END)) inGeneratedPrimitiveMethodsSection = false;
                      if(!inGeneratedPrimitiveMethodsSection)outputContents.append(line).append('\n');
                      if(line.contains(GENERATED_PRIMITIVE_METHODS_START)) {
                          inGeneratedPrimitiveMethodsSection = true;
                          String methods = methodsToCopy.toString();
                          for (String primative : new String[]{"long", "float", "double"}) {
                              outputContents.append(methods.replaceAll("int\\s", primative + " ")).append('\n');
                          }
                      }
                  }
                  outputStream = new PrintWriter(new FileWriter(fileName));
                  outputStream.print(outputContents.toString());
              } finally {
                  inputStream.close();
                  if(outputStream!= null) outputStream.close();
              }
          }
      }
      

      这将用 @PrimitiveMethods 部分中方法的 long、float 和 double 版本填充 @GeneratedPrimitiveMethods 部分。

          // @GeneratedPrimitiveMethodsStart - Do not edit below
          /** Find maximum of long inputs */
          public static long max(long a, long b) {
              return (a > b) ? a : b;
          }
          ...
      

      这是一个故意的简单示例,我敢肯定它不会涵盖所有情况,但是您明白了这一点,并且可以看到如何扩展它,例如搜索多个文件或使用普通注释并检测方法结束。

      此外,虽然您可以将其设置为构建系统中的一个步骤,但我将其设置为在 eclipse 项目中的 Java 构建器之前作为构建器运行。现在每当我编辑文件并点击保存时;它会在不到四分之一秒的时间内自动更新到位。因此,这更像是一种编辑工具,而不是构建系统中的一个步骤。

      只是一个想法......

      【讨论】:

      • Err...这不是注释处理器 - 无论如何都不是正确的,因为它不能直接与aptjavac 一起使用。如果我要使用这样的东西,我宁愿使用cpp 或任何其他现有工具,而不是自己编写...
      • 确实——这绝对不是注释处理器。你的问题部分是问:我可以避免手动复制原始代码而不需要额外的依赖项或更改我的构建系统吗?这是我能想到的最快/最简单的方法。如果您愿意在构建时添加依赖项,我也会使用现有工具。但是,我过去曾使用简单的自定义代码生成器来自动执行各种繁琐的任务并取得了不错的成功,因此认为它可能值得一提。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2011-08-29
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多