Дублирование кода, вызванное примитивными типами: как избежать безумия? - PullRequest
37 голосов
/ 12 марта 2012

В одном из моих Java-проектов меня преследует повторение кода из-за того, как Java обрабатывает (не) примитивы. После того, как нужно вручную скопировать одно и то же изменение в четыре разных местоположения (int, long, float, double) снова , в течение третьего времени, снова и снова Я очень близко (?) к привязке.

В разных формах эта проблема периодически поднималась в StackOverflow:

Казалось, что консенсус сходится к двум возможным альтернативам:

  • Используйте какой-нибудь генератор кода.
  • Что вы можете сделать? C'est la vie!

Ну, второе решение - это то, что я делаю сейчас, и оно постепенно становится опасным для моего здравомыслия, очень похоже на хорошо известную технику пыток .

Прошло два года с тех пор, как были заданы эти вопросы, и появилась 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, например ,. Есть ли где-нибудь процессор аннотаций для обработки этого случая? Если нет, то что нужно для его создания?

  • Возможно, какой-то другой трюк, который появился недавно?

EDIT:

Важное примечание: я бы не использовал примитивные типы, если бы у меня не было причины. Даже сейчас использование некоторых типов в штучной упаковке оказывает существенное влияние на производительность и память.

РЕДАКТИРОВАТЬ 2:

Использование max() в качестве примера позволяет использовать метод compareTo(), который доступен во всех числовых типах в штучной упаковке. Это немного сложнее:

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

Как можно поддерживать этот метод для всех числовых типов в штучной упаковке, не записывая его шесть или семь раз?

Ответы [ 8 ]

18 голосов
/ 12 марта 2012

Я склонен использовать «супер тип», такой как long или double, если я все еще хочу примитив. Производительность обычно очень близка и позволяет избежать множества вариаций. Кстати: регистры в 64-битной JVM все равно будут 64-битными.

15 голосов
/ 12 марта 2012

Почему ты одержим примитивами? Обертки чрезвычайно легкие и автоматические, а дженерики делают все остальное:

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

Отредактировано

ОП спросил о стандартном автоматическом решении для 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, кроме Float и Double, может анализировать только целое число (без десятичной точки); хотя сумма будет целым числом, мы должны удалить десятичную точку из Double.toString() перед отправкой в ​​конструктор для этих других типов.

5 голосов
/ 12 марта 2012

Есть ли в Java 7 какие-либо изменения, которые могут облегчить нагрузку в таких случаях?

номер

Есть ли где-нибудь процессор аннотаций для обработки этого случая?

Не то, чтобы я знал.

Если нет, что потребуется для его создания?

Время или деньги. : -)

Это кажется мне проблемным пространством, где было бы трудно найти общее решение, которое хорошо работает ... за исключением тривиальных случаев. Обычная генерация исходного кода или (текстовый) препроцессор кажутся мне более перспективными. (Хотя я не эксперт по обработке аннотаций.)

4 голосов
/ 12 марта 2012

Если до вас доходит необычайное многословие Java, посмотрите на некоторые из новых языков высокого уровня, которые работают на JVM и могут взаимодействовать с Java, такие как Clojure, JRuby, Scala и так далее.Ваше неконтролируемое примитивное повторение станет проблемой.Но преимущества будут идти гораздо дальше - есть всевозможные способы, которые только что упомянутые языки позволяют вам делать больше с менее подробным, повторяющимся, подверженным ошибкам кодом (по сравнению с Java).

Если производительность является проблемой, вы можете вернуться к Java для критических для производительности битов (используя примитивные типы).Но вы можете быть удивлены тем, как часто вы все еще можете получить хороший уровень производительности на языке более высокого уровня.

Я лично использую и JRuby, и Clojure;если вы пришли из Java / C / C # / C ++ фона, оба имеют потенциал изменить ваш взгляд на программирование.

3 голосов
/ 19 марта 2012

Хех.Почему бы не стать подлым?С помощью отражения вы можете извлекать аннотации для метода (аннотации, подобные примеру, который вы опубликовали).Затем вы можете использовать отражение, чтобы получить имена членов и вставить соответствующие типы ... В операторе system.out.println.

Вы должны выполнить это один раз или каждый раз, когда вы модифицировали класс.Выходные данные могут быть вставлены в копию. Это, вероятно, сэкономит вам значительное время и не будет слишком сложным для разработки.

Хм, что касается содержимого методов ... Я имею в виду, если все вашиметоды тривиальны, вы можете жестко кодировать стиль (т. е. если methodName.equals ("max") print возвращает a> b: a: b и т. д. Где methodName определяется с помощью отражения), или вы можете, ммммм ... Хм.Я представляю, что содержимое можно легко скопировать и вставить, но это, кажется, больше работы.

О!Если вы не сделаете другую аннотацию под названием «содержимое», присвойте ей строковое значение содержимого метода, добавьте его к члену, и теперь вы также можете распечатать содержимое.

По крайней мере, время, потраченное на написание этого помощника, даже если примерно столько же, сколько и утомительной работы, ну, было бы более интересно, riiiight?

1 голос
/ 30 марта 2012

С точки зрения производительности (я также делаю много алгоритмов, связанных с процессором), я использую свои собственные боксы, которые не являются неизменными.Это позволяет использовать изменяемые числа в наборах, таких как ArrayList и HashMap, для работы с высокой производительностью.

Требуется один длинный подготовительный шаг, чтобы создать все примитивные контейнеры с их повторяющимся кодом, а затем просто использовать их,Поскольку я также имею дело с 2-мерными, 3-мерными и т.д. значениями, я также создал их для себя.Выбор за вами.

как:
Vector1i - 1 целое число, заменяет Integer
Vector2i - 2 целое число, заменяет Point и Dimension
Vector2d - 2 двойных, заменяет Point2D.Double
Vector4i - 4 целых числа, может заменить Rectangle
Vector2f - 2-мерный вектор с плавающей точкой
Vector3f - 3-мерный вектор с плавающей точкой
... и т.д ...
Все они представляют обобщенный «вектор» в математике, отсюда и название всех этих примитивов.

Один недостаток в том, что вы не можете сделать a+b, у вас естьсделать методы вроде a.add(b), а для a=a+b я решил назвать методы как a.addSelf(b).Если это вас беспокоит, взгляните на Цейлон , который я обнаружил совсем недавно.Это слой поверх Java (JVM / Eclispe compatbile), созданный специально для устранения его ограничений (например, перегрузка операторов).

Еще одна вещь, обратите внимание при использовании этих классов в качестве ключа в Map, так как сортировка / хеширование / сравнение пойдут наперекосяк при изменении значения.

1 голос
/ 24 марта 2012

Ваш вопрос довольно сложный, так как вы, кажется, уже знаете все «хорошие» ответы.Так как из-за языкового дизайна нам не разрешено использовать примитивы в качестве универсальных типов параметров, лучший практический ответ - куда направляется @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 может представлять только 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
    }
}
0 голосов
/ 02 апреля 2012

Я бы согласился с предыдущими ответами / комментариями о том, что нет способа сделать именно то, что вы хотите ", используя стандартный набор функций JDK." Таким образом, вам придется делать некоторое генерирование кода, хотя это не обязательно потребует изменений в системе сборки. Так как вы спрашиваете:

... Если нет, то что нужно для его создания?

... Для простого случая, не слишком много, я думаю. Предположим, я поместил свои примитивные операции в класс утилит:

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

Это заполнит раздел @GeneratedPrimitiveMethods длинными, плавающими и двойными версиями методов в разделе @PrimitiveMethods.

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

Это намеренно простой пример, и я уверен, что он не охватывает все случаи, но вы понимаете суть и видите, как его можно расширить, например. искать несколько файлов или использовать обычные аннотации и определять конец метода.

Кроме того, хотя вы можете настроить это как шаг в вашей системе сборки, я настроил его для запуска в качестве сборщика перед сборщиком Java в моем проекте eclipse. Теперь всякий раз, когда я редактирую файл и нажимаю сохранить; он обновляется автоматически менее чем за четверть секунды. Таким образом, это становится скорее инструментом редактирования, чем шагом в системе сборки.

Просто мысль ...

...