基本类型导致的代码重复:如何避免疯狂?

37
在我的Java项目中,由于Java处理(not)原语的方式,我被代码重复困扰。必须手动将相同的更改复制到四个不同的位置(int,long,float,double),再次第三次再次再次,我真的很接近崩溃。

这个问题以各种形式在StackOverflow上被提出:

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

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

好的,第二个解决方案是我现在正在做的事情,但它正在逐渐危及我的理智,就像众所周知的酷刑技术一样。

自那些问题被提出并且Java 7出现以来已经过去了两年。因此,我对更简单和/或更标准的解决方案充满希望。

  • Does Java 7 have any changes that might ease the strain in such cases? I could not find anything in the condensed change summaries, but perhaps there is some obscure new feature somewhere?

  • While source code generation is an alternative, I'd prefer a solution supported using the standard JDK feature set. Sure, using cpp or another code generator would work, but it adds more dependencies and requires changes to the build system.

    The only code generation system of sorts that seems to be supported by the JDK is via the annotations mechanism. I envision a processor that would expand source code like this:

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

    The ideal output file would contain the four requested variations of this method, preferrably with associated Javadoc comments e.t.c. Is there somewhere an annotation processor to handle this case? If not, what would it take to build one?

  • Perhaps some other trick that has popped up recently?

编辑:

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

编辑2:

max()为例可以使用所有数值包装类型中可用的compareTo()方法。这有点棘手:

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

如何在不写六七次的情况下,支持所有数值装箱类型的此方法?

3
复杂而棘手的问题。点赞! - MarianP
现今大多数代码生成都是在运行时通过ClassLoader生成字节码。而在您的情况下,您需要为自己的代码编译生成类文件...因此我建议您考虑使用编译时注解处理器。 - Yves Martin
这个人提出了一个相当复杂的解决方案,使用注释处理器和使用Velocity生成类。我认为您不需要使用Velocity,只需编写更简单的代码即可。http://deors.wordpress.com/2011/10/08/annotation-processors/ - MarianP
8个回答

18

如果我仍然需要使用基本类型,我倾向于使用像longdouble这样的“超级类型”。性能通常非常接近,并且可以避免创建大量变量。顺便说一下:在64位JVM中,寄存器都将是64位。


2
一个有趣的观点:除了涉及数组的情况,针对 bytecharshort 的代码可能是毫无用处的——根据 JLS,大多数操作都必须将所有这些值提升为 int,因此字节码与 int 的代码相同... - thkala
在溢出情况下,您不会得到意外的结果吗?所有算法都必须考虑原始数字类型的最小/最大值。 - vladimir e.
2
通常情况下,下溢或上溢通常是不可取的,但您通常可以在最后强制转换为适当的类型。 - Peter Lawrey
我会接受这个答案,因为在大多数情况下这就是我最终采取的方法。不幸的是,目前似乎没有其他选项可供生产使用 - 至少不是在不使用不同的编程语言的情况下... - thkala

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构造函数,但除了FloatDouble之外的Numbers只能解析整数(没有小数点);尽管总数将是整数,但我们必须从Double.toString()中删除小数点,然后将其发送到这些其他类型的构造函数中。


16
性能,性能,性能。那次我使用装箱类型后,算法的速度变慢了3-4倍 - 然后就没有内存可用了... - thkala
如果性能是解决方案的关键标准,您应该更新您的问题。虽然你说装箱解决方案慢了3-4倍,但这是否是应用程序中的真正瓶颈?您受CPU限制而不是I/O限制吗? - SteveD
3
使用Oracle服务器VM 1.7.0_02,您的通用方法运行时间比等效的int max(int,int)实现慢7倍,比long max(long,long)实现慢12倍。即使绝对差异很小,相对差异也可能非常重要。至于算术运算:是否可以使用Number实例执行简单的加法和减法? - jarnbjo
1
@thkala,你的装箱类型已经用完了内存。例如,你使用了Integer.valueOf吗?你想要减少全局执行时间,那么你的代码是多线程的吗? - Yves Martin
1
我授予了第一个赏金给这个答案,因为我必须承认我自己选择了 max() 作为例子。我真的很想知道你是否有解决我的更新问题中的 sum() 的方法,即使它不能处理性能问题... - thkala
显示剩余5条评论

5
Java 7是否有任何更改可以缓解这种情况的压力?
没有。
是否有一个注释处理器来处理这种情况?
据我所知没有。
如果没有,建立一个需要什么?
时间或金钱。:-)
对我来说,这似乎是一个问题空间,在这个空间中很难想出一个通用的解决方案,可以很好地工作...除了微不足道的情况之外。传统的源代码生成或(文本)预处理器对我来说更有前途。(我不是Annotation处理器专家。)

4
如果Java的冗长让你感到疲惫,那么可以尝试一些运行在JVM上并且可以与Java互操作的新的高级语言,例如Clojure、JRuby、Scala等等。这样你就不再需要过多地使用原始类型了。但是好处远不止于此——与Java相比,这些语言可以让你用更少的重复代码来完成更多的工作,并且减少出错的可能性。
如果性能成为问题,你可以回到Java中处理性能关键的部分(使用原始类型)。但是你会惊讶地发现,在高级语言中仍然可以获得良好的性能水平。
我个人同时使用JRuby和Clojure;如果你来自Java/C/C#/C++背景,这两种语言都有改变你对编程思考方式的潜力。

或者 Groovy。别忘了 Groovy! - bharal
啊,但是这位楼主想要的是“性能,性能,性能”……对于这些特定的算法。 - Stephen C
1
@StephenC,这是一个很好的观点。只是我已经在SO上看到很多人抱怨Java的冗长和要求使其代码更简洁的方法,对我来说,答案是显而易见的;与其用Java做不自然的事情,不如切换到一种更高级别的语言! - Alex D
1
如果编程语言感觉笨拙,那么你应该考虑使用另一种语言。至于性能方面,可以进行低级别的优化,或者编写易于扩展的代码。在这种情况下,Scala将是一个非常好的选择。 - cthulhu

3
嘿。为什么不偷偷摸摸地做呢?使用反射,您可以提取方法的注释(与您发布的示例类似)。然后,您可以使用反射获取成员名称,并在适当的类型中添加...在system.out.println语句中。
您可以运行此代码一次,或每次修改类时运行。然后可以复制粘贴输出结果。这可能会节省您大量时间,并且开发难度不大。
至于方法的内容...我是说,如果您的所有方法都很简单,您可以硬编码样式(即if methodName.equals("max")print return a>b:a:b等,其中methodName通过反射确定),或者您可以...嗯呢。我想象一下,内容可以轻松地复制粘贴,但那只会更费力。
哦!为什么不再创建一个名为“内容”的注释,给它一个字符串值以表示方法内容,将其添加到成员中,现在您也可以打印出内容了。
至少,即使编写此辅助程序所花费的时间与执行繁琐的工作一样长,它也会更有趣,对吧?

2
可能更有趣,但我宁愿不成为接手维护代码工作的程序员。 (更准确地说,我不想成为那位拥有“平均”Java技能的程序员的经理。) - Stephen C
编译时注解处理绝对可以用于生成源文件。我看过几个教程,但没有一个使用原始源文件作为输入,这正是我想看到的。否则,代码将转换为一堆字符串文字,难以维护... - thkala
@thkala - 你不需要从字符串字面量中进行代码生成。如果你将源代码转换成Velocity或Freemarker模板,然后使用不同的参数为不同组合的原始类型运行模板,你会得到更易于维护的代码。 - Stephen C
@StephenC 纯文本模板会混淆所有的IDE类型检查、交叉引用和Javadoc创建工具。你需要从Java开始(例如编写 long 类型的代码),然后让处理器生成 int 类型的类。 - toolforger

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 优化通常会处理好这个问题,但无论如何都不会有害。可能取决于实际情况。

如果有人测试过,那将是非常棒的,但我相信这是最佳解决方案。

更新: 根据@thkala的评论,double 可能只表示长整数直到某个幂级别,之后便会失去精度(在处理长整数时会变得不精确):

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
    }
}

3
需要指出的是: double 可能在范围方面是最通用的原始数据类型,但在精度方面却不是。超过 53 位比特的 long 值在存储到 double 中时会出现精度损失... - thkala
你说得对,我没有意识到这一点。System.out.println(Double.MAX_VALUE); 1.7976931348623157E308 System.out.println(Long.MAX_VALUE); 9223372036854775807 System.out.println((double) Long.MAX_VALUE); 9.223372036854776E18 - MarianP

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/Eclipse兼容)之上的一种层,专门用来解决Java的限制(比如运算符重载)。

还有一件事,注意在使用这些类作为key放入Map时,值的改变可能会导致排序、哈希和比较出现问题。


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部分中的长整型、浮点型和双精度版本填充@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构建器之前运行的构建器。现在,每当我编辑文件并保存时,它会自动更新,不用离开编辑器,在不到四分之一秒的时间内完成。因此,这更像是一个编辑工具,而不是构建系统中的一步。

仅供参考...


1
嗯...这不是一个注解处理器 - 至少不是一个正确的注解处理器,因为它不能直接与aptjavac一起使用。如果我要使用类似于这样的东西,我宁愿使用cpp或任何其他现有工具,而不是编写自己的。 - thkala
确实 - 这绝对不是一个注解处理器。你的问题部分地问到:我能否在不添加额外依赖或更改构建系统的情况下避免手动原始代码重复?这是我能想到的最快/最简单的方法。如果您愿意在构建时添加依赖项,我会使用现有的工具。然而,我过去曾经使用简单的自定义代码生成器来自动化各种繁琐的任务,并取得了良好的成功,所以认为值得一提。 - Ian Jones

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接