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

183

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

questborn
источник
2
Это зависит от того, какие операции вы собираетесь делать. Пожалуйста, предложите больше информации.
Eversor
@eversor Можете ли вы дать мне описание того, какой тип данных должен использоваться для различных операций?
Questborn
1
Я делаю расчеты, которые требуют от меня, чтобы точно представлять центы.
Questborn
Можете ли вы предсказать самую большую сумму денег, с которой будет работать ваше приложение? И, ваши расчеты, они будут простыми (дополнения и т. Д.) Или более сложными финансовыми операциями?
Eversor

Ответы:

133

У Java есть Currencyкласс, который представляет коды валюты ISO 4217. BigDecimalлучший тип для представления десятичных значений валюты.

Joda Money предоставила библиотеку для представления денег.

Бухаке синди
источник
5
Почему мы не можем использовать вместо этого float или double?
Эрран Морад
20
@Borat Sagdiyev Вот почему . Также вы можете сослаться на это .
Бухаке Синди
2
@Borat: вы можете, если знаете, что делаете, посмотреть эту статью Питера Лоури. но, по крайней мере, такая большая сложность для округления, как использование BigDecimals.
Натан Хьюз
35
«Если бы у меня было десять центов за каждый раз, когда я видел, как кто-то использовал FLOAT для хранения валюты, у меня было бы 999,997634 доллара», - Билл Карвин
Коллин Кроулл
36

Вы можете использовать Money and Currency API (JSR 354) . Вы можете использовать этот API, если вы добавите соответствующие зависимости в ваш проект.

Для Java 8 добавьте следующую ссылочную реализацию в качестве зависимости к вашей pom.xml:

<dependency>
    <groupId>org.javamoney</groupId>
    <artifactId>moneta</artifactId>
    <version>1.0</version>
</dependency>

Эта зависимость будет транзитивно добавляться javax.money:money-apiкак зависимость.

Затем вы можете использовать API:

package com.example.money;

import static org.junit.Assert.assertThat;
import static org.hamcrest.CoreMatchers.is;

import java.util.Locale;

import javax.money.Monetary;
import javax.money.MonetaryAmount;
import javax.money.MonetaryRounding;
import javax.money.format.MonetaryAmountFormat;
import javax.money.format.MonetaryFormats;

import org.junit.Test;

public class MoneyTest {

    @Test
    public void testMoneyApi() {
        MonetaryAmount eurAmount1 = Monetary.getDefaultAmountFactory().setNumber(1.1111).setCurrency("EUR").create();
        MonetaryAmount eurAmount2 = Monetary.getDefaultAmountFactory().setNumber(1.1141).setCurrency("EUR").create();

        MonetaryAmount eurAmount3 = eurAmount1.add(eurAmount2);
        assertThat(eurAmount3.toString(), is("EUR 2.2252"));

        MonetaryRounding defaultRounding = Monetary.getDefaultRounding();
        MonetaryAmount eurAmount4 = eurAmount3.with(defaultRounding);
        assertThat(eurAmount4.toString(), is("EUR 2.23"));

        MonetaryAmountFormat germanFormat = MonetaryFormats.getAmountFormat(Locale.GERMAN);
        assertThat(germanFormat.format(eurAmount4), is("EUR 2,23") );
    }
}
Abdull
источник
Как насчет сериализации и сохранения в БД? Какой формат следует использовать для отправки по проводам?
Павел Щур
1
Я считаю, что Oracle посвятил в том числе Java Money в Java 9. Действительно позор. Но отличный ответ. Мы все еще можем использовать его с Maven
borjab
3
Есть ли у вас источник, по которому Oracle может отказаться от включения Java Money в Java 9?
Абдул
25

Интегральный тип, представляющий наименьшее возможное значение. Другими словами, ваша программа должна думать в центах, а не в долларах / евро.

Это не должно мешать вам переводить графический интерфейс обратно в доллары / евро.

чокнутый урод
источник
Имейте в виду , что сумма денег может переполнить размер междунар
eversor
5
@ eversor, для которого потребовалось бы более 20 миллионов долларов, большинству приложений не понадобилось бы столько, если бы они работали долго, и этого будет достаточно, поскольку даже наши правительства не обрабатывают достаточно денег, чтобы переполнить это
чокнутый урод
4
@ratchetfreak Наверное, лучше использовать длинный тогда.
Трогнандерс
5
Многие банки обрабатывают гораздо большие суммы денег, чем 20 000 000 долларов каждый день. Это даже не учитывает такие валюты, как иена, с большими курсами обмена доллара. Целочисленные типы могут быть лучше, чтобы избежать проблем округления, хотя они запутываются в расчетах процента и обменного курса. Однако, в зависимости от приложения, вам может понадобиться 64-битный целочисленный тип.
Алхимик
На самом деле, в идеале микродоллары, как, например, если вы делаете, например, $ 10/3, тогда ошибка округления (3333.3 => 3333.0) не оказывает существенного влияния на конечное значение (в этом случае оно вообще не влияет на реальное значение, хотя это опасно предполагать, что этого никогда не будет). Это особенно важно, если вы выполняете много вычислений подряд, прежде чем ваш пользователь увидит результат, так как ошибки округления будут усугубляться.
Крис Браун
11

JSR 354: деньги и валюта API

JSR 354 предоставляет API для представления, транспортировки и выполнения комплексных расчетов с деньгами и валютой. Вы можете скачать его по этой ссылке:

JSR 354: деньги и валюта API Скачать

Спецификация состоит из следующих вещей:

  1. API для обработки, например, денежных сумм и валют
  2. API для поддержки взаимозаменяемых реализаций
  3. Фабрики для создания экземпляров классов реализации
  4. Функциональность для расчетов, конвертации и форматирования денежных сумм
  5. Java API для работы с деньгами и валютами, который планируется включить в Java 9.
  6. Все спецификации классов и интерфейсов находятся в пакете javax.money. *.

Примеры JSR 354: деньги и валюта API:

Пример создания MonetaryAmount и его печати на консоли выглядит следующим образом:

MonetaryAmountFactory<?> amountFactory = Monetary.getDefaultAmountFactory();
MonetaryAmount monetaryAmount = amountFactory.setCurrency(Monetary.getCurrency("EUR")).setNumber(12345.67).create();
MonetaryAmountFormat format = MonetaryFormats.getAmountFormat(Locale.getDefault());
System.out.println(format.format(monetaryAmount));

При использовании эталонного API реализации необходимый код намного проще:

MonetaryAmount monetaryAmount = Money.of(12345.67, "EUR");
MonetaryAmountFormat format = MonetaryFormats.getAmountFormat(Locale.getDefault());
System.out.println(format.format(monetaryAmount));

API также поддерживает вычисления с MonetaryAmounts:

MonetaryAmount monetaryAmount = Money.of(12345.67, "EUR");
MonetaryAmount otherMonetaryAmount = monetaryAmount.divide(2).add(Money.of(5, "EUR"));

CurrencyUnit и MonetaryAmount

// getting CurrencyUnits by locale
CurrencyUnit yen = MonetaryCurrencies.getCurrency(Locale.JAPAN);
CurrencyUnit canadianDollar = MonetaryCurrencies.getCurrency(Locale.CANADA);

MonetaryAmount имеет различные методы, которые позволяют получить доступ к назначенной валюте, числовой сумме, ее точности и многим другим:

MonetaryAmount monetaryAmount = Money.of(123.45, euro);
CurrencyUnit currency = monetaryAmount.getCurrency();
NumberValue numberValue = monetaryAmount.getNumber();

int intValue = numberValue.intValue(); // 123
double doubleValue = numberValue.doubleValue(); // 123.45
long fractionDenominator = numberValue.getAmountFractionDenominator(); // 100
long fractionNumerator = numberValue.getAmountFractionNumerator(); // 45
int precision = numberValue.getPrecision(); // 5

// NumberValue extends java.lang.Number. 
// So we assign numberValue to a variable of type Number
Number number = numberValue;

MonetaryAmounts можно округлить с помощью оператора округления:

CurrencyUnit usd = MonetaryCurrencies.getCurrency("USD");
MonetaryAmount dollars = Money.of(12.34567, usd);
MonetaryOperator roundingOperator = MonetaryRoundings.getRounding(usd);
MonetaryAmount roundedDollars = dollars.with(roundingOperator); // USD 12.35

При работе с коллекциями MonetaryAmounts доступны некоторые полезные вспомогательные методы для фильтрации, сортировки и группировки.

List<MonetaryAmount> amounts = new ArrayList<>();
amounts.add(Money.of(2, "EUR"));
amounts.add(Money.of(42, "USD"));
amounts.add(Money.of(7, "USD"));
amounts.add(Money.of(13.37, "JPY"));
amounts.add(Money.of(18, "USD"));

Пользовательские операции MonetaryAmount

// A monetary operator that returns 10% of the input MonetaryAmount
// Implemented using Java 8 Lambdas
MonetaryOperator tenPercentOperator = (MonetaryAmount amount) -> {
  BigDecimal baseAmount = amount.getNumber().numberValue(BigDecimal.class);
  BigDecimal tenPercent = baseAmount.multiply(new BigDecimal("0.1"));
  return Money.of(tenPercent, amount.getCurrency());
};

MonetaryAmount dollars = Money.of(12.34567, "USD");

// apply tenPercentOperator to MonetaryAmount
MonetaryAmount tenPercentDollars = dollars.with(tenPercentOperator); // USD 1.234567

Ресурсы:

Обработка денег и валюты на Java с помощью JSR 354

Изучение API денег и валюты Java 9 (JSR 354)

Смотрите также: JSR 354 - Валюта и деньги

AFFY
источник
Все это хорошо, но, как Федерико предложил выше, это выглядит медленнее, чем BigDecimal :-)) плохая шутка только тогда, но я дам это испытание сейчас, 1 год спустя ...
kensai
6

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

Сандип Патхак
источник
6

Я бы использовал Joda Money

Это все еще в версии 0.6, но выглядит очень многообещающе

Ливиу Т.
источник
6

Я сделал микробенчмарк (JMH) для сравнения Moneta (реализация Java JSR 354) с BigDecimal с точки зрения производительности.

Удивительно, но производительность BigDecimal кажется лучше, чем у Moneta. Я использовал следующие настройки Moneta:

org.javamoney.moneta.Money.defaults.precision = 19 org.javamoney.moneta.Money.defaults.roundingMode = HALF_UP

package com.despegar.bookedia.money;

import org.javamoney.moneta.FastMoney;
import org.javamoney.moneta.Money;
import org.openjdk.jmh.annotations.*;

import java.math.BigDecimal;
import java.math.MathContext;
import java.math.RoundingMode;
import java.util.concurrent.TimeUnit;

@Measurement(batchSize = 5000, iterations = 10, time = 2, timeUnit =     TimeUnit.SECONDS)
@Warmup(iterations = 2)
@Threads(value = 1)
@Fork(value = 1)
@State(Scope.Benchmark)
@BenchmarkMode(Mode.Throughput)
public class BigDecimalBenchmark {

private static final Money MONEY_BASE = Money.of(1234567.3444, "EUR");
private static final Money MONEY_SUBSTRACT = Money.of(232323, "EUR");
private static final FastMoney FAST_MONEY_SUBSTRACT = FastMoney.of(232323, "EUR");
private static final FastMoney FAST_MONEY_BASE = FastMoney.of(1234567.3444, "EUR");
MathContext mc = new MathContext(10, RoundingMode.HALF_UP);

@Benchmark
public void bigdecimal_string() {
    new BigDecimal("1234567.3444").subtract(new BigDecimal("232323")).multiply(new BigDecimal("3.4"), mc).divide(new BigDecimal("5.456"), mc);
}

@Benchmark
public void bigdecimal_valueOf() {
    BigDecimal.valueOf(12345673444L, 4).subtract(BigDecimal.valueOf(232323L)).multiply(BigDecimal.valueOf(34, 1), mc).divide(BigDecimal.valueOf(5456, 3), mc);
}
@Benchmark
public void fastmoney() {
    FastMoney.of(1234567.3444, "EUR").subtract(FastMoney.of(232323, "EUR")).multiply(3.4).divide(5.456);
}

@Benchmark
public void money() {
    Money.of(1234567.3444, "EUR").subtract(Money.of(232323, "EUR")).multiply(3.4).divide(5.456);
}

@Benchmark
public void money_static(){
    MONEY_BASE.subtract(MONEY_SUBSTRACT).multiply(3.4).divide(5.456);
}

@Benchmark
public void fastmoney_static() {
    FAST_MONEY_BASE.subtract(FAST_MONEY_SUBSTRACT).multiply(3.4).divide(5.456);
    }
}

В результате чего

Benchmark                                Mode  Cnt     Score    Error  Units
BigDecimalBenchmark.bigdecimal_string   thrpt   10   479.465 ± 26.821  ops/s
BigDecimalBenchmark.bigdecimal_valueOf  thrpt   10  1066.754 ± 40.997  ops/s
BigDecimalBenchmark.fastmoney           thrpt   10    83.917 ±  4.612  ops/s
BigDecimalBenchmark.fastmoney_static    thrpt   10   504.676 ± 21.642  ops/s
BigDecimalBenchmark.money               thrpt   10    59.897 ±  3.061  ops/s
BigDecimalBenchmark.money_static        thrpt   10   184.767 ±  7.017  ops/s

Пожалуйста, не стесняйтесь поправлять меня, если я что-то упустил

Федерико Гауле Паломбарани
источник
Интересно, что я проведу тот же тест с последним материалом на JDK9
kensai
4

Для простого случая (одна валюты) it' достаточно Integer/ Long. Храните деньги в центах (...) или в сотых / тысячных центах (любая точность, которая вам нужна с фиксированным делителем)

Григорий Кислин
источник
3

BigDecimal - лучший тип данных для валюты.

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

Энтони Блейк
источник
2

Мне нравится использовать Tiny Types, которые обертывают либо double, BigDecimal, либо int, как предлагали предыдущие ответы. (Я бы использовал двойной, если не возникнут проблемы с точностью).

Tiny Type дает вам безопасность типов, поэтому вы не перепутаете двойные деньги с другими двойниками.

Гарретт Смит
источник
6
Хотя мне тоже нравятся крошечные типы, вы никогда не должны использовать двойные для хранения денежной стоимости.
Ориентир