Code duplication caused by primitive types: How to avoid insanity?

那年仲夏 提交于 2020-01-10 06:56:27

问题


In one of my Java projects I am plagued by code repetition due to the way Java handles (not) primitives. After having to manually copy the same change to four different locations (int, long, float, double) again, for the third time, again and again I came really close (?) to snapping.

In various forms, this issue has been brought up now and then on StackOverflow:

  • Managing highly repetitive code and documentation in Java
  • How to avoid repetition when working with primitive types?
  • Passing dynamic list of primitives to a Java method

The consensus seemed to converge to two possible alternatives:

  • Use some sort of code generator.
  • What can you do? C'est la vie!

Well, the second solution is what I am doing now and it is slowly becoming dangerous for my sanity, much like the well known torture technique.

Two years have passed since these questions were asked and Java 7 came along. I am, therefore, hopeful for an easier and/or more standard solution.

  • 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?

EDIT:

An important note: I would not be using primitive types unless I had a reason. Even now there is a very real performance and memory impact by the use of boxed types in some applications.

EDIT 2:

Using max() as an example allows the use of the compareTo() method that is available in all numeric boxed types. This is a bit trickier:

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

How could one go about supporting this method for all numeric boxed types without actually writing it six or seven times?


回答1:


I tend to use a "super type" like long or double if I still want a primitive. The performance is usually very close and it avoids creating lots of variations. BTW: registers in a 64-bit JVM will all be 64-bit anyway.




回答2:


Why are you hung up on primitives? The wrappers are extremely lightweight and auto-boxing and generics does the rest:

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

This all compiles and runs correctly:

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

Edited

OP has asked about a generic, auto-boxed solution for sum(), and will here it is.

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]);
}

It's a little lame, but not as lame as doing a large series of instanceof and delegating to a fully typed method. The instanceof is required because while all Numbers have a String constructor, Numbers other than Float and Double can only parse a whole number (no decimal point); although the total will be a whole number, we must remove the decimal point from the Double.toString() before sending it into the constructor for these other types.




回答3:


Does Java 7 have any changes that might ease the strain in such cases?

No.

Is there somewhere an annotation processor to handle this case?

Not that I am aware of.

If not, what would it take to build one?

Time, or money. :-)

This seems to me like a problem-space where it would be difficult to come up with a general solution that works well ... beyond trivial cases. Conventional source code generation or a (textual) preprocessor seems more promising to me. (I'm not an Annotation processor expert though.)




回答4:


If the extraordinary verbosity of Java is getting to you, look into some of the new, higher-level languages which run on the JVM and can interoperate with Java, like Clojure, JRuby, Scala, and so on. Your out-of-control primitive repetition will become a non-issue. But the benefits will go much further than that -- there are all kinds of ways which the languages just mentioned allow you to get more done with less detailed, repetitive, error-prone code (as compared to Java).

If performance is a problem, you can drop back into Java for the performance-critical bits (using primitive types). But you might be surprised at how often you can still get a good level of performance in the higher-level language.

I personally use both JRuby and Clojure; if you are coming from a Java/C/C#/C++ background, both have the potential to change the way you think about programming.




回答5:


Heh. Why not get sneaky? With reflection, you can pull the annotations for a method (annotations similar to the example you've posted). You can then use reflection to get the member names, and put in the appropriate types... In a system.out.println statement.

You would run this once, or each time you modded the class. The output could then be copy-pasted in. This would probably save you significant time, and not be too hard to develop.

Hm ,as for the contents of the methods... I mean, if all your methods are trivial, you could hard code the style (ie if methodName.equals("max") print return a>b:a:b etc. Where methodName is determined via reflection), or you could, ummmmm... Hm. I'm imagining the contents can be easily copy pasted, but that just seems more work.

Oh! Whty not make another annotation called " contents ", give it a string value of the method contents, add that to the member, and now you can print out the contents too.

In the very least, the time spent coding up this helper, even if about as long as doing the tedious work, well, it would be more interesting, riiiight?




回答6:


Your question is pretty elaborate as you already seem to know all the 'good' answers. Since due to language design we are not allowed to use primitives as generic parameter types, the best practical answer is where @PeterLawrey is heading.

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


}

The list of primitive types is small and hopefully constant in future evolution of the language and double type is the widest/most general.

In the worst case, you compute using 64 bit variables where 32 bit would suffice. There is a performance penalty for conversion(tiny) and for pass by value into one more method (small), but no Objects are created as this is the main (and really huge) penalty for using primitive wrappers.

I also used a static method so it is bound early and not in run-time, although it is just one and which is something that JVM optimization usually takes care of but it won't hurt anyway. May depend on real case scenario.

Would be lovely if someone tested it, but I believe this is the best solution.

UPDATE: Based on @thkala's comment, double may only represent long-s until certain magnitude as it loses precision (becomes imprecise when dealing with long-s) after that:

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



回答7:


From the performance point of view (I make a lot of CPU-bound algorithms too), I use my own boxings that are not immutable. This allows using mutable numbers in sets like ArrayList and HashMap to work with high performance.

It takes one long preparation step to make all the primitive containers with their repetitive code, and then you just use them. As I also deal with 2-dimensional, 3-dimensional etc values, I also created those for myself. The choice is yours.

like:
Vector1i - 1 integer, replaces Integer
Vector2i - 2 integer, replaces Point and Dimension
Vector2d - 2 doubles, replaces Point2D.Double
Vector4i - 4 integers, could replace Rectangle
Vector2f - 2-dimensional float vector
Vector3f - 3-dimensional float vector
...etc...
All of them represent a generalized 'vector' in mathematics, hence the name for all these primitives.

One downside is that you cannot do a+b, you have make methods like a.add(b), and for a=a+b I chose to name the methods like a.addSelf(b). If this bothers you, take a look at Ceylon, which I discovered very recently. It's a layer on top of Java (JVM/Eclispe compatbile) created especially to address it's limitations (like operator overloading).

One other thing, watch out when using these classes as a key in a Map, as sorting/hashing/comparing will go haywire when the value changes.




回答8:


I'd agree with previous answers/comments that say there isn't a way to do exactly what you want "using the standard JDK feature set." Thus, you are going to have to do some code generation, although it won't necessarily require changes to the build system. Since you ask:

... If not, what would it take to build one?

... For a simple case, not too much, I think. Suppose I put my primitive operations in a util class:

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
}

Then I can write a simple processor in less than 30 lines as follows:

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();
        }
    }
}

This will fill the @GeneratedPrimitiveMethods section with long, float and double versions of the methods in the @PrimitiveMethods section.

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

This is an intentionally a simple example, and I'm sure it doesn't cover all cases, but you get the point and can see how it could be extended e.g. to search multiple files or use normal annotations and detect method ends.

Furthermore, whilst you could set this up as a step in your build system, I set this up to run as a builder before the Java builder in my eclipse project. Now whenever I edit the file and hit save; it's updated automatically, in place, in less than a quarter of a second. Thus, this becomes more of a editing tool, than a step in the build system.

Just a thought...



来源:https://stackoverflow.com/questions/9665908/code-duplication-caused-by-primitive-types-how-to-avoid-insanity

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!