Потокобезопасный динамический шаблон для NumberFormat/DecimalFormat

Не удалось найти подходящее решение в Интернете, поэтому я решил спросить, правильный ли мой способ использования формата java.

1) В документации NumberFormat.java говорится, что

Числовые форматы обычно не синхронизируются. Рекомендуется создавать отдельные экземпляры формата для каждого потока.

Мы использовали объекты форматирования (статически инициализированные) в многопоточной среде без каких-либо проблем. Возможно, это связано с тем, что после определения форматов их состояние не изменяется (т.е. после этого не вызываются сеттеры)

2) Теперь мне нужно определить новый формат, который должен выводить одну или две значащие цифры после запятой, в зависимости от некоторой дополнительной логики. То, как я это сделал, заключалось в том, чтобы определить новую оболочку формата и делегировать два разных DecimalFormat в зависимости от случая в перезаписанном методе #format(double, StringBuffer, FieldPosition). Вот код для этого:

private final NumberFormat FORMAT = new DecimalFormat() {
    private final NumberFormat DECIMAL_FORMAT = new DecimalFormat("0.##");
    private final NumberFormat DECIMAL_FORMAT_DIGIT = new DecimalFormat(
                    "0.0#");
    public StringBuffer format(double number, StringBuffer result, java.text.FieldPosition fieldPosition) {
        if ((number >= 10 && Math.ceil(number) == number)) {
            return DECIMAL_FORMAT.format(number, result, fieldPosition);
        } else {
            return DECIMAL_FORMAT_DIGIT.format(number, result, fieldPosition);
        }
    }
};

Это лучшая практика? У меня есть опасения по поводу фактического использования класса-оболочки (он служит только для соответствия интерфейсу NumberFormat и делегирует всю работу над внутренними форматами). Я не хочу вызывать DecimalFormat#applyPattern(), поскольку я думаю, что это поставит под угрозу изменчивый параллелизм.

Спасибо


person d56    schedule 30.05.2016    source источник
comment
Не рекомендуется использовать/совместно использовать экземпляры NumberFormat в нескольких потоках. Даже если вы не сталкивались с какими-либо проблемами, до сих пор они будут возникать, как только ваше приложение достигнет истинной параллельной обработки. У нас был аналогичный вариант использования DateFormat, где использовались статически созданные форматы даты, но мы масштабировали наше приложение для обработки огромных данных, когда n-параллельных процессов производят данные, мы обнаружили, что формат может дать вам очень странный и неожиданный результат. Они могут не потерпеть неудачу, но получат неожиданное значение   -  person Sangram Jadhav    schedule 30.05.2016
comment
@SangramJadhav, так что вы, вероятно, храните многоразовый пул форматов потоков, верно?   -  person d56    schedule 30.05.2016
comment
Если вы хотите поделиться форматом, вам необходимо обеспечить нашу собственную синхронизацию. Или вы можете использовать ThreadLocal для объявления экземпляра формата, таким образом, каждый поток будет иметь свою собственную копию средства форматирования, и вам не нужно обеспечивать синхронизацию.   -  person Sangram Jadhav    schedule 30.05.2016
comment
да, если, то я пойду с ThreadLocal. Спасибо. Может быть, у вас есть предложения по пункту 2?   -  person d56    schedule 30.05.2016


Ответы (3)


  1. Мы использовали объекты форматирования (статически инициализированные) в многопоточной среде без каких-либо проблем. Возможно, это связано с тем, что после определения форматов их состояние не изменяется (т.е. после этого не вызываются сеттеры)

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

  • Вы не используете ни один из путей кода в DecimalFormat, которые используют изменяемые переменные экземпляра;
  • Вы случайно применяете взаимное исключение, поэтому вы никогда не используете экземпляр более чем в одном потоке за раз;
  • Вы используете реализацию, которая на самом деле правильно синхронизируется (обратите внимание, что в Javadoc говорится, что обычно не синхронизируется, а не никогда не синхронизируется);
  • На самом деле у вас есть проблемы, но вы просто не отслеживаете их должным образом;
  • и Т. Д.

Дело в том, что проблемы с синхронизацией, как я видел вчера, кто-то еще прокомментировал, заключается в том, что вы не гарантируете, что увидите проблему, если не синхронизируетесь; просто нет гарантии, что вы их не увидите.

Дело в том, что если вы не применяете синхронизацию, вы находитесь во власти любого количества тонких изменений, о которых вы можете просто не знать. Сегодня это работает, завтра нет; у вас будет одна всемогущая работа, выясняющая, почему.

  1. Это лучшая практика?

Есть несколько проблем, о которых я могу думать здесь:

  1. Расширяя класс, вы рискуете столкнуться с проблемой хрупкого базового класса.

    В двух словах, если вы на самом деле явно не вызываете метод public StringBuffer format(double, StringBuffer, java.text.FieldPosition ) в своем экземпляре DecimalFormat, вы не можете достоверно знать, действительно ли вызванный вами переопределенный метод: изменение реализации базового класса (DecimalFormat) может изменить логику вы полагаетесь на вызов этого метода.

  2. У вас есть три изменяемых экземпляра — FORMAT, DECIMAL_FORMAT и DECIMAL_FORMAT_DIGIT — у которых есть всевозможные сеттеры для изменения их поведения.

    Вы должны распространить все эти сеттеры на все экземпляры, чтобы они вели себя согласованно, например. если вы вызываете setPositivePrefix для FORMAT, вы также должны вызывать тот же метод для DECIMAL_FORMAT и DECIMAL_FORMAT_DIGIT.

Если вам на самом деле не нужно передавать FORMAT в качестве параметра метода, было бы намного надежнее, если бы вы просто определили простой старый метод, который вызывает вашу логику: в основном, переместите метод, который вы переопределяете, из анонимного подкласса:

private static StringBuffer formatWithSpecialLogic(double number, StringBuffer result, java.text.FieldPosition fieldPosition) {

Затем вам нужно вызвать этот метод явно, если вы хотите использовать эту специальную логику.

person Andy Turner    schedule 03.06.2016
comment
1. Я думаю, что вы ошибаетесь. Метод NumberFormat#format(double), из которого расширяется DecimalFormat, помечается как final, чтобы запретить его переопределение. Для меня это означает, что создатели хотели, чтобы потребители переопределяли другой формат метода (double, StringBuffer, FieldPosition). 2. Я не вызываю никаких сеттеров для результирующего формата. Он также будет не потокобезопасным (ну, тем более, чем сейчас). 3. Я не могу вытащить formatWithSpeicalLogic из класса, потому что format(double) вызывается неявно классом javax.swing.text.NumberFormatter - person d56; 03.06.2016
comment
1. Я думаю, что вы ошибаетесь в том, что не ошибаетесь, просто, возможно, слишком пессимистично настроены; FBCP является неизбежным следствием extends. Вы просто думаете, что этого не произойдет. И, вероятно, не будет; Я бы не стал ставить на это дом. 2. Я не звоню сеттерам. А вы гарантируете, что никогда не будете звонить? 3) Эту деталь вы упустили из виду. - person Andy Turner; 03.06.2016

У меня нет репутации, чтобы комментировать, но что касается пункта 2, самый большой недостаток, который я вижу, заключается в том, что вы не переопределили все поведение класса DecimalFormat, поэтому ваш результирующий экземпляр будет вести себя непоследовательно. Например:

final StringBuffer buffer = new StringBuffer();
FORMAT.format(0.111d, buffer, new FieldPosition(0));
System.out.println(buffer.toString());

final StringBuffer buffer2 = new StringBuffer();
FORMAT.format(new BigDecimal(0.111d), buffer2, new FieldPosition(0));
System.out.println(buffer2.toString());

урожаи

0.11
0.111

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

private final ThreadLocal<Function<Double, String>> DECIMAL_FORMATTER = new ThreadLocal<Function<Double, String>>() {
  @Override
  protected Function<Double, String> initialValue() {
    final DecimalFormat decimalFormat = new DecimalFormat("0.##");
    final DecimalFormat decimalFormatDigit = new DecimalFormat("0.0#");
    return (number) -> {
      if ((number >= 10 && Math.ceil(number) == number)) {
        return decimalFormat.format(number);
      } else {
        return decimalFormatDigit.format(number);
      }
    };
  }
};

System.out.println(DECIMAL_FORMATTER.get().apply(0.111d));
person marty.christiansen    schedule 02.06.2016
comment
Проблема в том, что мне нужен только один объект формата, который выполняет всю логику. Причина этого в том, что javax.swing.text.NumberFormatter ожидает объект java.text.NumberFormat в своем конструкторе. Поэтому я пошел за наследством, а не за композицией. Я не переопределял остальные методы, так как нас интересуют только двойные входы. Тем не менее, я немного обеспокоен неуклюжим получающимся наследованием - 2 NumberFormat внутри третьего; ( - person d56; 03.06.2016

Поскольку вы говорите, что вам нужен экземпляр NumberFormat, я бы посоветовал вам расширить этот класс и реализовать необходимые методы, а не расширять DecimalFormat, который не предназначен для наследования:

private final NumberFormat FORMAT = new NumberFormat() {
    private final NumberFormat DECIMAL_FORMAT = new DecimalFormat("0.##");
    private final NumberFormat DECIMAL_FORMAT_DIGIT = new DecimalFormat("0.0#");

    @Override
    public StringBuffer format(double number, StringBuffer result, FieldPosition fieldPosition) {
        if ((number >= 10 && Math.ceil(number) == number)) {    // or number % 1 == 0
            return DECIMAL_FORMAT.format(number, result, fieldPosition);
        } else {
            return DECIMAL_FORMAT_DIGIT.format(number, result, fieldPosition);
        }
    }

    @Override
    public StringBuffer format(long number, StringBuffer result, FieldPosition fieldPosition) {
        return format((double)number, result, fieldPosition);
    }

    @Override
    public Number parse(String source, ParsePosition parsePosition) {
        return DECIMAL_FORMAT.parse(source, parsePosition);
    }
};

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

person shmosel    schedule 03.06.2016