原始类型引起的代码重复:如何避免精神错乱?

时间:2012-03-12 11:05:24

标签: java primitive code-duplication

在我的一个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注释e.t.c.有没有一个注释处理器来处理这种情况?如果没有,建立一个会怎样?

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

修改

一个重要的注意事项:除非我有理由,否则我不会使用原始类型。即使是现在,在某些应用程序中使用盒装类型也会产生非常真实的性能和内存影响。

编辑2:

使用max()作为示例允许使用所有数字框类型中可用的compareTo()方法。这有点棘手:

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

怎么可以为所有数字盒装类型支持这种方法而不实际写入六到七次呢?

8 个答案:

答案 0 :(得分:18)

如果我还想要一个原语,我倾向于使用像longdouble这样的“超类型”。性能通常非常接近,避免产生大量变化。 BTW:无论如何,64位JVM中的寄存器都是64位的。

答案 1 :(得分:15)

你为什么要挂上原语?包装器非常轻巧,自动装箱和其他通用设备完成其余工作:

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只能解析整数({1}}没有小数点);虽然总数将是一个整数,但我们必须从Double.toString()中删除小数点,然后再将其发送到这些其他类型的构造函数中。

答案 2 :(得分:5)

  

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

没有

  

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

不是我知道的。

  

如果没有,构建一个会怎样?

时间或金钱。 : - )

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

答案 3 :(得分:4)

如果Java的特殊冗长,请研究一些在JVM上运行并可与Java互操作的新的高级语言,如Clojure,JRuby,Scala等。你失控的原始重复将成为一个无问题。但是它的好处还远远不止于此 - 还有各种方式,只是提到的语言允许您使用不那么详细,重复,容易出错的代码来完成更多工作(与Java相比) )。

如果性能有问题,您可以回退到Java以获得性能关键位(使用基本类型)。但是,您可能会惊讶于您仍然可以在更高级别的语言中获得良好的性能水平。

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

答案 4 :(得分:3)

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

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

嗯,至于方法的内容......我的意思是,如果你所有的方法都很简单,你可以硬编码样式(即如果methodName.equals(“max”)print返回a&gt; b:a: b等。通过反射确定methodName,或者你可以,嗯...嗯。我想象的内容可以轻松地复制粘贴,但这似乎更多的工作。

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

至少,花在编写这个帮助程序上的时间,即使只要完成繁琐的工作,那么,这会更有趣,直到最后?

答案 5 :(得分:1)

你的问题非常详细,因为你似乎已经知道了所有的好事。答案。由于语言设计,我们不允许使用原语作为通用参数类型,最好的实际答案是@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位就足够了。转换(微小)和将值传递到另一个方法(小)时会有性能损失,但是没有创建对象,因为这是使用原始包装器的主要(并且确实很大)惩罚。

我还使用了一个静态方法,因此它早期绑定而不是在运行时绑定,尽管它只是一个,这是JVM优化通常需要处理的东西,但是它赢得了&#39;无论如何都要受伤。可能取决于实际情况。

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

<强>更新 根据@ 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
    }
}

答案 6 :(得分:1)

从性能的角度来看(我也制作了很多CPU绑定算法),我使用自己的不可变的盒子。这允许在ArrayListHashMap等集合中使用可变数字来实现高性能。

需要一个很长的准备步骤来使用重复代码制作所有原始容器,然后您只需使用它们。由于我还处理二维,三维等值,我也为自己创建了这些值。选择是你的。

像:
Vector1i - 1个整数,替换Integer
Vector2i - 2整数,取代PointDimension
Vector2d - 2次加倍,取代Point2D.Double
Vector4i - 4个整数,可以取代Rectangle
Vector2f - 二维浮点矢量
Vector3f - 三维浮点矢量
...等...
所有这些都代表了数学中的广义“向量”,因此是所有这些基元的名称。

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

另一件事是,在Map中将这些类用作时要注意,因为当值发生变化时,排序/散列/比较会变得混乱。

答案 7 :(得分:0)

我同意以前的答案/评论,说没有办法完全按照你想要的方式使用标准的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构建器之前作为构建器运行。现在每当我编辑文件并点击保存;它会在不到四分之一秒的时间内自动更新。因此,这比编译系统中的步骤更像是一种编辑工具。

只是一个想法...