Округлить число в Java можно четырьмя способами: String.format("%.3f", x) и printf (результат это строка, режим HALF_UP), DecimalFormat (тоже строка, по умолчанию режим HALF_EVEN), методы Math.round(), Math.ceil() и Math.floor() (до целого; до нужного знака через умножение на 10 в степени n) и BigDecimal.setScale() с явным RoundingMode (точное число, а не строка). Ниже разберем каждый способ на примерах. Числа с плавающей точкой (float, double) применяются везде, где нужна дробная часть: в физических расчетах, графике, статистике. А вот там, где важна точность до копейки (бухгалтерия, деньги), используют BigDecimal: double хранит десятичные дроби приближенно, и 0.1 + 0.2 в нем не равно 0.3. Но всегда ли нам нужен длинный “хвост” чисел после запятой? Может нам достаточно точности в три знака вещественной части? И есть нас такой вариант устраивает, как правильно выполнить округление? Именно об этом мы сегодня и поговорим: рассмотрим способы округления чисел в Java.

Кратко

  • String.format("%.3f", x), а также printf и format у System.out возвращают строку и округляют по правилу HALF_UP: 7.0005 превращается в 7,001.
  • new DecimalFormat("#.###") тоже дает строку, но по умолчанию округляет по HALF_EVEN и отбрасывает конечные нули (7.000132 превращается в 7); режим меняется через setRoundingMode().
  • Math.ceil() и Math.floor() возвращают double, Math.round() возвращает long (для float на входе int), и все три округляют только до целого; до n знаков число умножают на 10 в степени n, округляют и делят обратно.
  • BigDecimal с setScale(3, RoundingMode.DOWN) или new MathContext(5, RoundingMode.HALF_UP) это единственный способ получить число, а не строку, с явно выбранным режимом; старые константы BigDecimal.ROUND_* с Java 9 объявлены устаревшими.
  • Режимов RoundingMode восемь (UP, DOWN, CEILING, FLOOR, HALF_UP, HALF_DOWN, HALF_EVEN, UNNECESSARY), а разница между ними видна только на «половинках» вроде 2.5 и на отрицательных числах.
  • new BigDecimal(0.1) хранит не 0.1, а 0.1000000000000000055…, поэтому BigDecimal создают из строки или через BigDecimal.valueOf().
Черно-белый кот, свернувшийся в идеально круглый клубок на пледе: намек на округление

String format

В качестве первого способа мы рассмотрим округление double:

double value = 34.766674;
String result = String.format("%.3f",value);
System.out.print(result);//  34,767
В результате мы отформатируем наше число с плавающей запятой 34.766674 с точностью до 3 десятичных разрядов, так как в инструкции для форматирования мы указали три знака после запятой "%.3f". Запятая в выводе появилась из-за русской локали по умолчанию: разделитель дробной части String.format берет из нее. Чтобы получить точку, передайте локаль первым аргументом: String.format(Locale.US, "%.3f", value) выведет 34.767. В свою очередь %f при форматировании строки обозначает тип чисел с плавающей запятой, который включает в себя типы данных double и float в Java. В примере выше мы выводили полученное значение в консоль. Теперь вопрос: как бы можно было это сократить? Все просто: нужно использовать printf, который в свою очередь является format + print. В итоге предыдущий пример у нас сократился бы до:

double value = 34.766674;
System.out.printf("%.3f",value);
У экземпляра out класса PrintStream помимо этого метода есть еще метод format, который работает аналогично:

double value = 34.766674;
System.out.format("%.3f",value);
Округление происходит по режиму HALF_UP: к тому из соседних значений, которое ближе. Если же они равноудалены (отбрасываемая цифра ровно 5), то округление выполняется в большую сторону. Так и записано в документации Formatter, причем округляется не двоичное значение double, а его короткая десятичная запись (та, что выдает Double.toString), поэтому 7.0005 честно превращается в 7,001. Пример:

String firstResult = String.format("%.3f",7.0004);// 7,000
String secondResult = String.format("%.3f",7.0005);// 7,001
String thirdResult = String.format("%.3f",7.0006);// 7,001
Более подробно режимы округления мы разберем чуть ниже. Полосатый кот сосредоточенно заглядывает в синюю коробку

DecimalFormat

Еще один вариант: использовать класс DecimalFormat. Он предназначен для форматирования любого числа в Java, будь это целое число или число с плавающей запятой. При создании экземпляра DecimalFormat мы можем передать ему строку форматирования. Она укажет, сколько десятичных разрядов нужно отформатировать для вывода. Как будет выглядеть наш пример с использованием DecimalFormat:

double value = 34.766674;
DecimalFormat decimalFormat = new DecimalFormat( "#.###" );
String result = decimalFormat.format(value);
System.out.print(result);//34,767
Строка #.### это шаблон, который указывает, что мы форматируем передаваемое значение до 3 десятичных знаков. Чтобы изменить шаблон после создания объекта DecimalFormat, можно использовать его методы applyPattern и applyLocalizedPattern:

DecimalFormat decimalFormat = new DecimalFormat("#.###");
decimalFormat.applyPattern("#.#");
decimalFormat.applyLocalizedPattern("#.#####");
Но мы сегодня говорим про округление, не правда ли? При обрезании числа со знаками после запятой, выходящими за заданный шаблон, DecimalFormat округляет число в большую сторону, если первая отбрасываемая цифра больше 5. Но что, если она равна 5? Получается, что оно ровно посередине между ближайшими целыми. Что тогда? В этом случае DecimalFormat смотрит на предыдущую цифру и округляет так, чтобы она стала четной. Это режим HALF_EVEN, его еще называют банковским округлением. Если предыдущая цифра уже четная, число остается как есть:

DecimalFormat decimalFormat = new DecimalFormat("#.##");
String result = decimalFormat.format(7.125);
System.out.println((result));// 7,12
Если нечетная, округляется вверх:

DecimalFormat decimalFormat = new DecimalFormat("#.##");
String result = decimalFormat.format(7.375);
System.out.println((result));// 7,38
Числа в примерах выбраны не случайно: 7.125 и 7.375 хранятся в double точно (это 7 + 1/8 и 7 + 3/8), поэтому «половинка» здесь настоящая. А вот, скажем, 7.4565 в двоичном виде превращается в 7.45650000000000012…, то есть чуть больше половины, и с Java 8 DecimalFormat это учитывает (исправление JDK-7131459): такое число округлится до 7,457 при любой четности предыдущей цифры. Режим округления у DecimalFormat можно поменять методом setRoundingMode (он есть с Java 6): decimalFormat.setRoundingMode(RoundingMode.HALF_UP) заставит его округлять так же, как String.format. Есть небольшая разница между форматированием чисел с плавающей запятой с использованием String.format() и DecimalFormat.format(). Первый всегда будет печатать конечные нули, даже если нет дробной части. К примеру:

String firstResult = String.format("%.3f", 7.000132);
System.out.println((firstResult)); // 7,000

DecimalFormat decimalFormat = new DecimalFormat("#.###");
String secondResult = decimalFormat.format(7.000132);
System.out.println((secondResult));  // 7
Как мы видим, при форматировании числа 7.000132 с точностью до трех десятичных разрядов, метод format() у String выводит 7,000, в то время как у DecimalFormat метод format() выведет 7. То есть вы можете выбирать String.format() или DecimalFormat.format() в зависимости от того, нужны ли вам конечные нули или нет. Применяя вышеописанные способы, мы получали результат в виде строки. Давайте же рассмотрим способы, как можно получить обратно именно числовые значения.

Math

Нельзя не упомянуть специальный класс, заточенный под разные арифметические операции с числами: Math (обзор его методов есть в отдельной статье). Доска, исписанная мелом математическими формулами с пределами и последовательностямиВ этом классе есть и методы для округления, но в отличие от уже описанных, они не позволяют задать определенное число знаков после запятой, а округляют до целого числа:
  • Math.ceil() округляет до ближайшего целого числа вверх, но отдает не целочисленный тип, а double:

    
    double value = 34.777774;
    double result = Math.ceil(value);
    System.out.println((result)); //35.0
    

    Даже если у нас будет 34.00000001, все равно после использования Math.ceil мы получим 35,0.

  • Math.floor() округляет до ближайшего целого вниз, также результатом отдает double:

    
    double value = 34.777774;
    double result = Math.floor(value);
    System.out.println((result)); //34.0
    

    Опять же, даже если у нас будет значение 34.999999999, то после использования Math.floor мы получим 34,0.

  • Math.round() округляет до ближайшего целого числа, но результат отдает не int, а long (int будет только для float на входе):

    
    double value = 34.777774;
    long result = Math.round(value);
    System.out.println((result)); //35
    

    Если у нас число 34.5, округление выполняется до 35, если же чуть-чуть меньше, например 34.499, число обрезается до 34.

    Чтобы не просто обрезать всю вещественную часть, а регулировать этот процесс до определенного количества знаков и при этом округлять, число умножают на 10^n (10 в степени n), где n равно количеству необходимых знаков после запятой. После этого применяют какой-нибудь метод класса Math для округления, ну а затем снова делят на 10^n:

    
    double value = 34.777774;
    double scale = Math.pow(10, 3);
    double result = Math.ceil(value * scale) / scale;
    System.out.println((result)); //34.778
    

    Math.pow принимает два аргумента: первый это число, второй это степень, в которую его нужно возвести.

Округление с BigDecimal

BigDecimal это класс, который позволяет работать с десятичными дробями произвольной точности. В частности, его основной фишкой является то, что в нем можно хранить дробные числа произвольной длины (то есть нет ограничения в диапазоне числа). Помимо этого, этот класс хранит различные методы для арифметической обработки, включая округление. Объект этого класса можно создать, передав в конструктор double, строковое представление числа, double вместе с MathContext и так далее. Только осторожно с конструктором от double: new BigDecimal(0.1) создаст не 0.1, а 0.1000000000000000055511151231257827…, потому что 0.1 в двоичном виде точно не представимо (почему так устроены вещественные числа, разобрано в отдельной статье). Документация Oracle прямо советует строковый конструктор new BigDecimal("0.1") или BigDecimal.valueOf(0.1), который берет короткую запись числа из Double.toString. Поэтому в примерах ниже число передается в конструктор строкой. MathContext это комбинация правила округления RoundingMode и числа, задающего общее количество значащих цифр в результате. Правила округления RoundingMode (в третьей колонке показано, во что превращаются 2.5, 2.4 и -2.5 при округлении до целого):
Режим Правило 2.5 / 2.4 / -2.5
UP от нуля: любая ненулевая отбрасываемая часть увеличивает последнюю цифру 3 / 3 / -3
DOWN к нулю: отбрасываемая часть просто отсекается 2 / 2 / -2
CEILING в сторону положительной бесконечности 3 / 3 / -2
FLOOR в сторону отрицательной бесконечности 2 / 2 / -3
HALF_UP к ближайшему соседу; если оба соседа равноудалены (отбрасывается ровно 5), то от нуля. Так учат округлять в школе 3 / 2 / -3
HALF_DOWN к ближайшему соседу; если оба соседа равноудалены, то к нулю 2 / 2 / -2
HALF_EVEN к ближайшему соседу; если оба соседа равноудалены, то к тому, у которого последняя цифра четная («банковское округление», именно так по умолчанию работает описанный выше DecimalFormat) 2 / 2 / -2
UNNECESSARY утверждает, что результат точный и округлять ничего не нужно; если округление все же потребовалось, бросает ArithmeticException исключение во всех трех случаях
Пример:

MathContext context = new MathContext(5, RoundingMode.HALF_UP);
BigDecimal result = new BigDecimal("34.777554", context);
System.out.println(result); //34.778
Помимо возможности задавать правило округления в конструкторе, BigDecimal позволяет задать режим округления уже после создания экземпляра. Для этого используется метод setScale, в который нужно задать количество знаков после запятой и правила округления:

BigDecimal result = new BigDecimal("34.777554");
result = result.setScale(3, RoundingMode.DOWN);
System.out.println(result); //34.777
В старом коде можно встретить и целочисленные константы BigDecimal для задания режима округления (ROUND_DOWN, ROUND_CEILING, ROUND_FLOOR...). Это те же правила, что в классе RoundingMode, и раньше их точно так же передавали в setScale:

BigDecimal result = new BigDecimal("34.777554");
result = result.setScale(3, BigDecimal.ROUND_DOWN);
С Java 9 и сами константы, и перегрузка setScale(int, int) помечены как @Deprecated: документация отсылает к setScale(int, RoundingMode). В новом коде используйте только RoundingMode, а этот вариант держите в голове, чтобы узнавать его в чужих проектах. Подробнее о классе BigDecimal в этой статье.

Вопросы и ответы

Как округлить double до двух знаков после запятой в Java?

Смотря что нужно на выходе. Строку для вывода дает String.format("%.2f", x) (режим HALF_UP) или new DecimalFormat("#.##").format(x) (HALF_EVEN по умолчанию, конечные нули отбрасываются). Число для дальнейших расчетов дает BigDecimal.valueOf(x).setScale(2, RoundingMode.HALF_UP) (при необходимости с .doubleValue() в конце) либо трюк Math.round(x * 100) / 100.0, но у него результат снова double со всеми его погрешностями.

Что возвращает Math.round и как он округляет половинки?

Math.round(double) возвращает long, Math.round(float) возвращает int, поэтому int r = Math.round(3.7) не скомпилируется без приведения типа. Половинки он округляет в сторону плюс бесконечности: Math.round(2.5) это 3, но Math.round(-2.5) это -2. Если нужен результат типа double и банковское округление, есть Math.rint(): Math.rint(2.5) даст 2.0, а Math.rint(3.5) даст 4.0.

Почему String.format и DecimalFormat округляют одно и то же число по-разному?

У них разные режимы по умолчанию. String.format и printf работают через Formatter, который округляет по HALF_UP короткую десятичную запись числа. DecimalFormat по умолчанию использует HALF_EVEN и с Java 8 учитывает точное двоичное значение double. Поэтому String.format("%.2f", 0.125) даст 0,13, а new DecimalFormat("#.##").format(0.125) даст 0,12. Уравнять их можно вызовом decimalFormat.setRoundingMode(RoundingMode.HALF_UP).

Как получить точку вместо запятой в String.format?

Разделитель дробной части берется из локали по умолчанию, в русской это запятая. Передайте нужную локаль первым аргументом: String.format(Locale.US, "%.3f", value) выведет 34.767. У DecimalFormat то же самое делается через набор символов: new DecimalFormat("#.###", DecimalFormatSymbols.getInstance(Locale.US)).

Почему new BigDecimal(0.1) хранит не 0.1?

Потому что конструктор от double берет точное двоичное значение аргумента, а 0.1 в двоичной системе точно не представимо: получается 0.1000000000000000055511151231257827021181583404541015625. Документация Oracle советует строковый конструктор new BigDecimal("0.1") или BigDecimal.valueOf(0.1): оба дают ровно 0.1, потому что отталкиваются от десятичной записи числа.

Какой способ округления выбрать для денег?

BigDecimal с явным режимом: sum.setScale(2, RoundingMode.HALF_UP) (или HALF_EVEN, если так требуют правила учета). Тип double для денег не подходит: 0.1 + 0.2 в нем равно 0.30000000000000004, и после тысяч операций копейки расходятся. Сами объекты BigDecimal создавайте из строк или через valueOf, а не из double.

Читайте также