[英]Code duplication caused by primitive types: How to avoid insanity?
在我的一个Java项目中,由于Java处理(而非)原语的方式,我受到代码重复的困扰。 在必须再次手动将相同的更改复制到四个不同的位置( int
, long
, float
, double
) 之后 , 第三次,我一次 又一次地接近(?)捕捉。
在各种形式中,这个问题已经在StackOverflow上提出了:
共识似乎趋向于两种可能的替代方案:
好吧,第二个解决方案就是我现在正在做的事情,它对我的理智慢慢变得危险,就像众所周知的折磨技术一样 。
自从提出这些问题并且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注释等。是否有某处注释处理器来处理这种情况? 如果没有,构建一个会怎样?
也许最近出现了一些其他技巧?
编辑:
一个重要的注意事项:除非我有理由,否则我不会使用原始类型。 即使是现在,在某些应用程序中使用盒装类型也会产生非常真实的性能和内存影响。
编辑2:
使用max()
作为示例允许使用所有数字盒装类型中可用的compareTo()
方法。 这有点棘手:
int sum(int a, int b) {
return a + b;
}
怎么可以为所有数字盒装类型支持这种方法而不实际写入六到七次?
如果我还想要一个原语,我倾向于使用像long
或double
这样的“超类型”。 性能通常非常接近,避免产生大量变化。 BTW:无论如何,64位JVM中的寄存器都是64位的。
你为什么要挂在原始人身上? 包装器非常轻巧,自动装箱,其余的是泛型:
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
构造函数,但Float
和Double
以外的Numbers
只能解析整数(无小数点); 虽然总数将是一个整数,但我们必须从Double.toString()
删除小数点,然后再将其发送到这些其他类型的构造函数中。
Java 7是否有任何可能在这种情况下缓解压力的变化?
没有。
有没有一个注释处理器来处理这种情况?
不是我知道的。
如果没有,构建一个会怎样?
时间还是金钱。 :-)
在我看来,这似乎是一个问题空间,很难找到一个运行良好的通用解决方案......超越琐碎的案例。 传统的源代码生成或(文本)预处理器似乎对我更有希望。 (虽然我不是Annotation处理器专家。)
如果Java的非凡冗长,请研究一些在JVM上运行并可与Java互操作的新的更高级语言,如Clojure,JRuby,Scala等。 你失控的原始重复将成为一个无问题。 但是它的好处还远远不止于此 - 有很多种方法可以让您通过不那么详细,重复,容易出错的代码(与Java相比)来完成更多工作。
如果性能是一个问题,您可以回退到Java以获得性能关键位(使用基本类型)。 但是,您可能会惊讶于您仍然可以在更高级别的语言中获得良好的性能水平。
我个人同时使用JRuby和Clojure; 如果您来自Java / C / C#/ C ++背景,那么两者都有可能改变您对编程的看法。
嘿。 为什么不偷偷摸摸? 通过反射,您可以为方法提取注释(注释类似于您发布的示例)。 然后,您可以使用反射来获取成员名称,并输入相应的类型...在system.out.println语句中。
你可以运行一次,或者每次修改类。 然后可以将输出复制粘贴。这可能会为您节省大量时间,并且不会太难开发。
嗯,至于方法的内容......我的意思是,如果所有的方法都很简单,你可以硬编码样式(即如果methodName.equals(“max”)print返回a> b:a:b等。其中methodName是通过反射确定的,或者你可以,嗯...嗯。 我想象的内容可以很容易地复制粘贴,但这似乎更多的工作。
哦! 没有做另一个称为“内容”的注释,给它一个方法内容的字符串值,将其添加到成员,现在你也可以打印出内容。
至少,花在编写这个帮助程序上的时间,即使只要进行繁琐的工作,也就是说,它会更有趣,riiiight?
你的问题非常详细,因为你似乎已经知道了所有“好”的答案。 由于语言设计,我们不允许使用原语作为通用参数类型,最好的实际答案是@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);
}
}
原始类型列表很小,希望在将来的语言演变中保持不变, 双类型是最广泛 /最普遍的。
在最坏的情况下,您使用64位变量进行计算,其中32位就足够了。 转换(微小)和将值传递到另一个方法(小)时会有性能损失,但是没有创建对象,因为这是使用原始包装器的主要(并且确实很大)惩罚。
我还使用了一个静态方法,所以它是早期绑定而不是在运行时,虽然它只是一个,这是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
}
}
从性能的角度来看(我也制作了很多CPU绑定算法),我使用了自己的不可变的盒子。 这允许在像ArrayList
和HashMap
这样的集合中使用可变数字来实现高性能。
需要一个很长的准备步骤来使用重复代码制作所有原始容器,然后您只需使用它们。 由于我还处理二维,三维等值,我也为自己创建了这些值。 这是你的选择。
喜欢:
Vector1i
- 1个整数,替换Integer
Vector2i
- 2整数,取代了Point
和Dimension
Vector2d
- 2个双打,取代了Point2D.Double
Vector4i
- 4个整数,可以替换Rectangle
Vector2f
- 二维浮动矢量
Vector3f
- 三维浮动矢量
...等等...
所有这些都代表了数学中的广义“向量”,因此是所有这些基元的名称。
一个缺点是你不能做a+b
,你有make方法如a.add(b)
,而a=a+b
我选择命名方法如a.addSelf(b)
。 如果这让你烦恼,那就去看看我最近发现的锡兰 。 它是Java(JVM / Eclispe compatbile)之上的一个层,专门用于解决它的局限性(如运算符重载)。
另外一点,当使用这些类作为Map
的键时要小心,因为当值发生变化时,排序/散列/比较会变得混乱。
我同意以前的答案/评论,说没有办法按照你想要的方式“使用标准的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构建器之前作为构建器运行。 现在每当我编辑文件并点击保存; 它会在不到四分之一秒的时间内自动更新。 因此,这比编译系统中的步骤更像是一种编辑工具。
只是一个想法...
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.