澳门新葡萄京官网首页 1

澳门新葡萄京官网首页你真的会数钱吗?

JSR 354定义了一套新的Java货币API,计划会在Java
9中正式引入。本文中我们将来看一下它的参考实现:JavaMoney的当前进展。

摘要:此前,Oracle公布Java
9首个增强计划集确定会在2016年早些时候发布。而目前,JSR
354定义了一套新的Java货币API,计划会在Java 9中正式引入。JSR
354定义了一套新的Java货币API,计划会在Java
9中正式引入。本文中我们将来看一下它的参考实现:JavaMoney的当前进展。正如我在之前那篇Java
8新的日期时间API一文中那样,本文主要也是通过一些代码来演示下新的API的用法
。在开始之前,我想先用一段话来简短地总结一下规范定义的这套新的API的用意何在:引用对许多应用而言货币价值都是一个关键的特性,但JDK对此却几乎没有任何支持。严格来讲,现有的java.util.Currency类只是代表了当前ISO
4217货币的一个数据结构,但并没有关联的值或者自定义货币。JDK对货币的运算及转换也没有内建的支持,更别说有一个能够代表货币值的标准类型了。如果你用的是Maven的话,只需把下面的引用添加到工里面便能够体验下该参考实现的当前功能了:

 

正如我在之前那篇Java
8新的日期时间API一文中那样,本文主要也是通过一些代码来演示下新的API的用法

dependency groupIdorg.javamoney/groupId artifactIdmoneta/artifactId version0.9/version /dependency 

本文已迁移至:http://thinkinside.tk/2013/01/01/money.html

快年底了,假如你们公司的美国总部给每个人发了一笔201212.21美元的特别奖金,作为程序员的你,
该如何把这笔钱收入囊中?

在开始之前,我想先用一段话来简短地总结一下规范定义的这套新的API的用意何在:

规范中提到的类及接口都在javax.money.*包下面。我们先从核心的两个接口CurrencyUnit与MonetaryAmount开始讲起。CurrencyUnit及MonetaryAmountCurrencyUnit代表的是货币。它有点类似于现在的java.util.Currency类,不同之处在于它支持自定义的实现。从规范的定义来看,java.util.Currency也是可以实现该接口的。CurrencyUnit的实例可以通过MonetaryCurrencies工厂来获取:

Table of Contents

  • 1 美元?美元!
  • 2 存入账户
  • 3 收税
  • 4 转成人民币
  • 5 分钱
  • 6 记账
  • 7 来点高级的
  • 8 其他未尽事宜
  • 9 小结

对许多应用而言货币价值都是一个关键的特性,但JDK对此却几乎没有任何支持。严格来讲,现有的java.util.Currency类只是代表了当前ISO
4217货币的一个数据结构,但并没有关联的值或者自定义货币。JDK对货币的运算及转换也没有内建的支持,更别说有一个能够代表货币值的标准类型了。

// 根据货币代码来获取货币单位 CurrencyUnit euro = MonetaryCurrencies.getCurrency("EUR"); CurrencyUnit usDollar = MonetaryCurrencies.getCurrency("USD"); // 根据国家及地区来获取货币单位 CurrencyUnit yen = MonetaryCurrencies.getCurrency(Locale.JAPAN); CurrencyUnit canadianDollar = MonetaryCurrencies.getCurrency(Locale.CANADA); 

1 美元?美元!

你可能觉得,这根本不是问题。在自己的账户中直接加上一笔“转入”就行了。但是首先就遇到了币种的问题。

一般来说,银行账户都是单币种的。你可能会说不对啊,我的一卡通就能存入不同的币种啊?但那是一个“账号(Account
Number)”对应的多个“账户(Account)”。
通常财务记账的时候,一个“账户(Account)”都使用同一币种。

账户(Account)记录了资金的往来,包含很多条目(Entry)。账户会记录结余,结余等于所有条目中金额的总和。

我们不可能为每个币种设计一种条目,所以需要抽象出一个货币类——Money,适用于各种不同的币种: 

澳门新葡萄京官网首页 1

 

Money类至少要记录金额和币种:

  • 对于金额,由于货币存在最小面额,所以金额的类型可以采用定点小数或者整型。考虑到会对金额进行一些运算,用整数处理应该更方便。如果用java语言实现,可以使用

lang类型。

  • 对于币种,java提供了java.util.Currency类,专门用于表示货币,符合ISO
    4217货币代码标准。Currency使用Singleton模式,需要用getInstance方法获得实例。

主要的方法包括:

    • String getCurrencyCode() 获取货币的ISO 4217货币代码
    • int getDefaultFractionDigits()
      获取与此货币一起使用的默认小数位数
    • static Currency getInstance(Locale locale)
      返回给定语言环境的国家/地区的 Currency 实例
    • static Currency getInstance(String currencyCode)
      返回给定货币代码的 Currency 实例。
    • String getSymbol() 获取默认语言环境的货币符号
    • String getSymbol(Locale locale) 获取指定语言环境的货币符号
    • String toString() 返回此货币的 ISO 4217 货币代码

通过Currency类的帮助,我们的Money类看起来大概是这个样子(为了方便,提供多种构造函数):

public class Money {
    private long amount;
    private Currency currency;

    public double getAmount() {
        return BigDecimal.valueOf(amount, currency.getDefaultFractionDigits()).doubleValue();

    }

    public Currency getCurrency() {
        return currency;
    }

    public Money(double amount, Currency currency) {
        this.currency = currency;
        this.amount = Math.round(amount * centFactor());
    }

    public Money(long amount, Currency currency) {
        this.currency = currency;
        this.amount = amount * centFactor();
    }

    private static final int[] cents = new int[] { 1, 10, 100, 1000,10000 };

    private int centFactor() {
        return cents[currency.getDefaultFractionDigits()];
    }
}

用Money类表示我们的$201212.21奖金,就是:

Money myMoney = new Money(201212.21,Currency.getInstance(Locale.US));

 

如果你用的是Maven的话,只需把下面的引用添加到工里面便能够体验下该参考实现的当前功能了:

MontetaryAmount代表的是某种货币的具体金额。通常它都会与某个CurrencyUnit绑定。MontetaryAmount和CurrencyUnit一样,也是一个能支持多种实现的接口。CurrencyUnit与MontetaryAmount的实现必须是不可变,线程安全且可比较的。

2 存入账户

终于解决了币种的问题,可以把钱存入账户了。存入的逻辑是:在条目中记录一笔账目,并计算账户的余额。

不同币种之间相加或相减是没有意义的,为了避免人为错误,在Money的代码中就要禁止这种操作。我们可以采用抛出异常的方式。
为了简单起见,这里不再定义一个单独的”MoneyException”,而是直接使用java.lang.Exception:

public Money add(Money money) throws Exception{
        if(!money.getCurrency().equals(this.currency)){
            throw(new Exception("different currency can't be add"));
        }
        BigDecimal value = this.getAmount().add(money.getAmount());
        Money result = new Money(value.doubleValue(),this.getCurrency());
        return result;
    }

    public Money minus(Money money) throws Exception{
        if(!money.getCurrency().equals(this.currency)){
            throw(new Exception("different currency can't be minus"));
        }

        BigDecimal value =this.getAmount().add(money.getAmount().negate());
        Money result = new Money(value.doubleValue(),this.getCurrency());
        return result;

    }

 

<dependency>
  <groupId>org.javamoney</groupId>
  <artifactId>moneta</artifactId>
  <version>0.9</version>
</dependency>
/ get MonetaryAmount from CurrencyUnit CurrencyUnit euro = MonetaryCurrencies.getCurrency("EUR"); MonetaryAmount fiveEuro = Money.of(5, euro); // get MonetaryAmount from currency code MonetaryAmount tenUsDollar = Money.of(10, "USD"); // FastMoney is an alternative MonetaryAmount factory that focuses on performance MonetaryAmount sevenEuro = FastMoney.of(7, euro); 

3 收税

先不要高兴得太早,这笔钱属于“一次性所得”,需要交20%的个人所得税。税后所得应该是多少?

你可能说:是80%。只要为Money加上一个multiply(double
factor)方法就可以进行计算了。

但是牵扯到了舍入的问题。由于货币存在最小单位,在做乘/除法运算的时候就要考虑到舍入的问题了。最好是能够控制舍入的行为。假如税务部门对于
舍入的计算有明确规定,我们也可以做一个遵纪守法的好公民。

在java.math.BigDecimal中定义了7种舍入模式:

  • ROUNDUP:等于远离0的数。
  • ROUNDDOWN:等于靠近0的数。
  • ROUNDCEILING:等于靠近正无穷的数。
  • ROUNDFLOOR:等于靠近负无穷的数。
  • ROUNDHALFUP:等于靠近的数,若舍入位为5,应用ROUNDUP。
  • ROUNDHALFDOWN:等于靠近的数,若舍入位为5,应用ROUNDDOWN。
  • ROUNDHALFEVEN:舍入位前一位为奇数,应用ROUNDHALFUP;舍入位前一位为偶数,应用ROUNDHALFDOWN。

我们可以借用这些模式作为参数:

public static final int ROUND_UP = BigDecimal.ROUND_UP;
    public static final int ROUND_DOWN = BigDecimal.ROUND_DOWN;
    public static final int ROUND_CEILING = BigDecimal.ROUND_CEILING;
    public static final int ROUND_FLOOR = BigDecimal.ROUND_FLOOR;
    public static final int ROUND_HALF_UP = BigDecimal.ROUND_HALF_UP;
    public static final int ROUND_HALF_DOWN = BigDecimal.ROUND_HALF_DOWN;
    public static final int ROUND_HALF_EVEN = BigDecimal.ROUND_HALF_EVEN;
    public static final int ROUND_UNNECESSARY = BigDecimal.ROUND_UNNECESSARY;


public Money multiply(double multiplicand, int roundingMode) {
        BigDecimal amount = this.getAmount().multiply(new BigDecimal(multiplicand));
        amount = amount.divide(BigDecimal.ONE,roundingMode);
        return new Money(amount.doubleValue(),this.getCurrency());
    }

public Money divide(double divisor, int roundingMode) {
        BigDecimal amount = this.getAmount().divide(new BigDecimal(divisor),
                roundingMode);
        Money result = new Money(amount.doubleValue(), this.getCurrency());
        return result;
    }

 

规范中提到的类及接口都在javax.money.*包下面。

Money与FastMoney是JavaMoney库中MonetaryAmount的两种实现。Money是默认实现,它使用BigDecimal来存储金额。FastMoney是可选的另一个实现,它用long类型来存储金额。根据文档来看,FastMoney上的操作要比Money的快10到15倍左右。然而,FastMoney的金额大小与精度都受限于long类型。注意了,这里的Money和FastMoney都是具体的实现类。如果你不希望指定具体类型的话,可以通过MonetaryAmountFactory来生成一个MonetaryAmount的实例:

4 转成人民币

尽管各领域的国际化提了十几年,但是在国内想直接用美元消费还是有一定困难。所以你决定将这笔钱换成人民币。

对于账户来说,就是在美元账户和人民币账户分别做一笔转出和转入。
转入和转出的amount值是不同的,因为涉及到币种转换的问题。
显然,账户对象不应该知道如何进行汇率转换,责任又落在了Money类上。

最直观的做法是在Money类上增加一个convertTo(Currency currency)的方法。
但汇率实在是一个复杂的问题:

  1. 汇率是经常变化的;
  2. 汇率转换时的舍入处理会有相关的约定;

这些复杂的问题处理如果直接放在Money类上会显得十分笨重,单独设计一个MoneyConverter类会比较好:

import java.util.Currency;


public interface MoneyConverter {
    Money convertTo(Money money,Currency currency) throws Exception;
}

 

我们实现一个最简单的转化器,使用固定的汇率值:

import java.math.BigDecimal;
import java.util.Currency;
import java.util.Locale;


public class SimpleMoneyConverter implements MoneyConverter {

    private static final BigDecimal DOLLAR_TO_CNY =  new BigDecimal(6.2365);
    private static final Currency DOLLAR = Currency.getInstance(Locale.US);
    private static final Currency CNY = Currency.getInstance(Locale.CHINA);
    @Override
    public Money convertTo(Money money,Currency target) throws Exception{
        if(!known(money.getCurrency()) || !known(target)){
            throw (new Exception("unknown currency"));
        }

        BigDecimal factorSource =BigDecimal.ONE, factorTarget = BigDecimal.ONE;
        if(money.getCurrency().equals(DOLLAR))
                factorSource = DOLLAR_TO_CNY;
        if(target.equals(DOLLAR))
                factorTarget = DOLLAR_TO_CNY;
        BigDecimal value = money.getAmount().multiply(factorSource).divide(factorTarget);

        return new Money(value.doubleValue(),target);
    }

    private boolean known(Currency currency){
        return(currency.equals(DOLLAR) || currency.equals(CNY) );
    }

}

 

可以看到,即使是最简单的转换器,处理起来也比较麻烦。所以千万不要在Money类中做这件事情。

通过转换器可以很容易得到转成人民币后的值。

我们先从核心的两个接口CurrencyUnit与MonetaryAmount开始讲起。

MonetaryAmount specAmount = MonetaryAmounts.getDefaultAmountFactory() .setNumber(123.45) .setCurrency("USD") .create(); 

5 分钱

有好处不能独享。这笔钱你决定和老婆三七开。当然,你三!

这又是一个新的舍入问题:即使你指定各自的舍入计算方法,也不能保证各部分舍入后的值加总后仍等于原值。

前面的“可定制乘除法”似乎不能很好的解决这个问题,所以我们需要一个新的方法:
Money[] allocate(double[] ratioes)

传入分配比例的数组,返回分配结果的数组。

为了保证分配的公平,可以使用伪随机数来处理误差。

该方法的实现如下:

public Money[] allocate(double[] ratioes) throws Exception{
        if(ratioes.length==0){
            throw (new Exception("there is no ratio"));
        }

        double ratioTotal = 0;
        for(double ratio:ratioes){
            ratioTotal += ratio;
        }

        if(0==ratioTotal){
            throw(new Exception("total of ratioes is zero"));
        }


        double total = this.getAmount().doubleValue();
        double delta = total;
        Money[] results = new Money[ratioes.length];

        for(int i=0;i<ratioes.length;i++){
            double amount = total*ratioes[i]/ratioTotal;
            results[i] = new Money(amount,this.getCurrency());
            delta -= results[i].getAmount().doubleValue();
        }

        int i = (int)(Math.random() * ratioes.length); 
        results[i] = results[i].minus(new Money(delta,this.getCurrency()));
        return results;
    }

 

CurrencyUnit及MonetaryAmount

CurrencyUnit代表的是货币。它有点类似于现在的java.util.Currency类,不同之处在于它支持自定义的实现。从规范的定义来看,java.util.Currency也是可以实现该接口的。CurrencyUnit的实例可以通过MonetaryCurrencies工厂来获取:

// 根据货币代码来获取货币单位 CurrencyUnit euro = MonetaryCurrencies.getCurrency("EUR");
    CurrencyUnit usDollar = MonetaryCurrencies.getCurrency("USD"); // 根据国家及地区来获取货币单位
    CurrencyUnit yen = MonetaryCurrencies.getCurrency(Locale.JAPAN); CurrencyUnit
    canadianDollar = MonetaryCurrencies.getCurrency(Locale.CANADA);

MontetaryAmount代表的是某种货币的具体金额。通常它都会与某个CurrencyUnit绑定。MontetaryAmount和CurrencyUnit一样,也是一个能支持多种实现的接口。CurrencyUnit与MontetaryAmount的实现必须是不可变,线程安全且可比较的。

/ get MonetaryAmount from CurrencyUnit
CurrencyUnit euro = MonetaryCurrencies.getCurrency("EUR");
MonetaryAmount fiveEuro = Money.of(5, euro);

// get MonetaryAmount from currency code
MonetaryAmount tenUsDollar = Money.of(10, "USD");

// FastMoney is an alternative MonetaryAmount factory that focuses on performance
MonetaryAmount sevenEuro = FastMoney.of(7, euro);

Money与FastMoney是JavaMoney库中MonetaryAmount的两种实现。Money是默认实现,它使用BigDecimal来存储金额。FastMoney是可选的另一个实现,它用long类型来存储金额。根据文档来看,FastMoney上的操作要比Money的快10到15倍左右。然而,FastMoney的金额大小与精度都受限于long类型。

注意了,这里的Money和FastMoney都是具体的实现类(它们在org.javamoney.moneta.*包下面,而不是javax.money.*)。如果你不希望指定具体类型的话,可以通过MonetaryAmountFactory来生成一个MonetaryAmount的实例:

MonetaryAmount specAmount = MonetaryAmounts.getDefaultAmountFactory()
                .setNumber(123.45) .setCurrency("USD") .create();

当且仅当实现类,货币单位,以及数值全部相等时才认为这两个MontetaryAmount实例是相等的。

MonetaryAmount oneEuro = Money.of(1, MonetaryCurrencies.getCurrency("EUR"));
boolean isEqual = oneEuro.equals(Money.of(1, "EUR")); // true
boolean isEqualFast = oneEuro.equals(FastMoney.of(1, "EUR")); // false

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;

MonetaryAmount的使用

可以在MonetaryAmount上进行算术运算:

MonetaryAmount twelveEuro = fiveEuro.add(sevenEuro); // "EUR 12"
MonetaryAmount twoEuro = sevenEuro.subtract(fiveEuro); // "EUR 2"
MonetaryAmount sevenPointFiveEuro = fiveEuro.multiply(1.5); // "EUR 7.5"

// MonetaryAmount can have a negative NumberValue
MonetaryAmount minusTwoEuro = fiveEuro.subtract(sevenEuro); // "EUR -2"

// some useful utility methods
boolean greaterThan = sevenEuro.isGreaterThan(fiveEuro); // true
boolean positive = sevenEuro.isPositive(); // true
boolean zero = sevenEuro.isZero(); // false

// Note that MonetaryAmounts need to have the same CurrencyUnit to do mathematical operations
// this fails with: javax.money.MonetaryException: Currency mismatch: EUR/USD
fiveEuro.add(tenUsDollar);

舍入操作是金额换算里面非常重要的一部分。MonetaryAmount可以使用舍入操作符来进行四舍五入:

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

这里12.3456美金就会按当前货币默认的舍入规则来进行换算。

在操作MonetaryAmount集合时,有许多实用的工具方法可以用来进行过滤,排序以及分组。这些方法还可以与Java
8的流API一起配套使用。

看一下下面这个集合:

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

我们可以根据CurrencyUnit来进行金额过滤:

CurrencyUnit yen = MonetaryCurrencies.getCurrency("JPY");
CurrencyUnit dollar = MonetaryCurrencies.getCurrency("USD");
// 根据货币过滤,只返回美金
// result is [USD 18, USD 7, USD 42]
List<MonetaryAmount> onlyDollar = amounts.stream()
    .filter(MonetaryFunctions.isCurrency(dollar))
    .collect(Collectors.toList());

// 根据货币过滤,只返回美金和日元
// [USD 18, USD 7, JPY 13.37, USD 42]
List<MonetaryAmount> onlyDollarAndYen = amounts.stream()
    .filter(MonetaryFunctions.isCurrency(dollar, yen))
    .collect(Collectors.toList());

我们还可以过滤出大于或小于某个阈值的金额:

MonetaryAmount tenDollar = Money.of(10, dollar);

// [USD 42, USD 18]
List<MonetaryAmount> greaterThanTenDollar = amounts.stream()
    .filter(MonetaryFunctions.isCurrency(dollar))
    .filter(MonetaryFunctions.isGreaterThan(tenDollar))
    .collect(Collectors.toList());

排序也是类似的:

// Sorting dollar values by number value
// [USD 7, USD 18, USD 42]
List<MonetaryAmount> sortedByAmount = onlyDollar.stream()
    .sorted(MonetaryFunctions.sortNumber())
    .collect(Collectors.toList());

// Sorting by CurrencyUnit
// [EUR 2, JPY 13.37, USD 42, USD 7, USD 18]
List<MonetaryAmount> sortedByCurrencyUnit = amounts.stream()
    .sorted(MonetaryFunctions.sortCurrencyUnit())
    .collect(Collectors.toList());

还有分组操作:

// 按货币单位进行分组
// {USD=[USD 42, USD 7, USD 18], EUR=[EUR 2], JPY=[JPY 13.37]}
Map<CurrencyUnit, List<MonetaryAmount>> groupedByCurrency = amounts.stream()
    .collect(MonetaryFunctions.groupByCurrencyUnit());

// 分组并进行汇总
Map<CurrencyUnit, MonetarySummaryStatistics> summary = amounts.stream()
    .collect(MonetaryFunctions.groupBySummarizingMonetary()).get();

// get summary for CurrencyUnit USD
MonetarySummaryStatistics dollarSummary = summary.get(dollar);
MonetaryAmount average = dollarSummary.getAverage(); // "USD 22.333333333333333333.."
MonetaryAmount min = dollarSummary.getMin(); // "USD 7"
MonetaryAmount max = dollarSummary.getMax(); // "USD 42"
MonetaryAmount sum = dollarSummary.getSum(); // "USD 67"
long count = dollarSummary.getCount(); // 3

MonetaryFunctions还提供了归约函数,可以用来获取最大值,最小值,以及求和:

List<MonetaryAmount> amounts = new ArrayList<>();
amounts.add(Money.of(10, "EUR"));
amounts.add(Money.of(7.5, "EUR"));
amounts.add(Money.of(12, "EUR"));

Optional<MonetaryAmount> max = amounts.stream().reduce(MonetaryFunctions.max()); // "EUR 7.5"
Optional<MonetaryAmount> min = amounts.stream().reduce(MonetaryFunctions.min()); // "EUR 12"
Optional<MonetaryAmount> sum = amounts.stream().reduce(MonetaryFunctions.sum()); //

当且仅当实现类,货币单位,以及数值全部相等时才认为这两个MontetaryAmount实例是相等的。

6 记账

将一切重要的数据保存到数据库是很通常的做法。但是将Money保存到数据库的时候,你要小心了!

Money不能作为单独的实体。如果把Money当做实体来处理,就会产生一些问题:

  1. 会有很多实体关联到Money,比如本文中的Account,Entry等。
  2. 需要非常小心处理对Money对象的引用,避免多个实体引用到同一个Money对象。在第一点的前提下,这会变得很困难。

所以应该把Money嵌入到需要的实体中,而不是把Money作为单独的实体。这样,Money仅仅是实体对象(比如Entry)的一个属性,只不过其具有多个内置的属性值。

在JPA中,可以使用@Embeddable来标注Money类。

更复杂的情况是,由于一个Account中的所有Entry都应该具有相同的Currency,将Currency保存到Account中会更简洁,Entry中只记录ammount。

可以为Money的currency属性增加@Transient标注,在Entry类的getMoney中进行组装。

自定义的MonetaryAmount操作

MonetaryAmount还提供了一个非常友好的扩展点叫作MonetaryOperator。MonetaryOperator是一个函数式接口,它接收一个MonetaryAmount入参并返回一个新的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

标准的API特性都是通过MonetaryOperator的接口来实现的。比方说,前面看到的舍入操作就是以MonetaryOperator接口的形式来提供的。

MonetaryAmount oneEuro = Money.of(1, MonetaryCurrencies.getCurrency("EUR")); boolean isEqual = oneEuro.equals(Money.of(1, "EUR")); // true boolean isEqualFast = oneEuro.equals(FastMoney.of(1, "EUR")); // false 

7 来点高级的

在DDD(领域驱动设计)中,Money是典型的值对象(Value
Object)。值对象与实体的根本区别是:值对象不需要进行标识(ID)。

这会带来一些处理上的不同:

  1. 实体对象根据ID判断是否相等,值对象只根据内部属性值判断是否相等
  2. 值对象通常小而且简单,创建的代价较小
  3. 值对象只传递值,不传递对象引用,不用判断值对象是否指向同一个物理对象
  4. 通常将值对象设计为通过构造函数进行属性设置,一旦创建就无法改变其属性值

由于值对象根据内部属性值判等,我们要为Money类覆盖equals方法: public
boolean equals(Object other)

汇率

货币兑换率可以通过ExchangeRateProvider来获取。JavaMoney自带了多个不同的ExchangeRateProvider的实现。其中最重要的两个是ECBCurrentRateProvider与
IMFRateProvider。

ECBCurrentRateProvider查询的是欧洲中央银行(European Central
Bank,ECB)的数据而IMFRateProvider查询的是国际货币基金组织(International
Monetary Fund,IMF)的汇率。

// get the default ExchangeRateProvider (CompoundRateProvider)
ExchangeRateProvider exchangeRateProvider = MonetaryConversions.getExchangeRateProvider();

// get the names of the default provider chain
// [IDENT, ECB, IMF, ECB-HIST]
List<String> defaultProviderChain = MonetaryConversions.getDefaultProviderChain();

// get a specific ExchangeRateProvider (here ECB)
ExchangeRateProvider ecbExchangeRateProvider = MonetaryConversions.getExchangeRateProvider("ECB");

如果没有指定ExchangeRateProvider的话返回的就是CompoundRateProvider。CompoundRateProvider会将汇率转换请求委派给一个ExchangeRateProvider链并将第一个返回准确结果的提供商的数据返回。

// get the exchange rate from euro to us dollar
ExchangeRate rate = exchangeRateProvider.getExchangeRate("EUR", "USD");

NumberValue factor = rate.getFactor(); // 1.2537 (at time writing)
CurrencyUnit baseCurrency = rate.getBaseCurrency(); // EUR
CurrencyUnit targetCurrency = rate.getCurrency(); // USD

MonetaryAmount内包含丰富的方法,可以用来获取具体的货币,金额,精度等等:

8 其他未尽事宜

  • 我们还可以为Money类增加互相比较的方法(略)
  • 可以在构造函数中进行格式校验(略)
  • 可以增加一些帮助显式的方法 使用currency的getSymbol(Locale
    locale)方法、和NumberFormat的format方法,比如:

    NumberFormat nf=NumberFormat.getCurrencyInstance(Locale.CHINA);

    String s=nf.format(73084.803984);// result:¥73,084.80

货币转换

不同货币间的转换可以通过ExchangeRateProvider返回的CurrencyConversions来完成。

// get the CurrencyConversion from the default provider chain
CurrencyConversion dollarConversion = MonetaryConversions.getConversion("USD");

// get the CurrencyConversion from a specific provider
CurrencyConversion ecbDollarConversion = ecbExchangeRateProvider.getCurrencyConversion("USD");

MonetaryAmount tenEuro = Money.of(10, "EUR");

// convert 10 euro to us dollar 
MonetaryAmount inDollar = tenEuro.with(dollarConversion); // "USD 12.537" (at the time writing)

请注意CurrencyConversion也实现了MonetaryOperator接口。正如其它操作一样,它也能通过MonetaryAmount.with()方法来调用。

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; 

9 小结

本文探讨如何在应用中处理货币类型,包括币种转换、各种计算、如何持久化等内容。

货币类型是典型的值对象,本文也介绍了一点值对象的特点。更多的内容可以参考DDD。

Date: 2013-01-01 02:27:05 CST

Author: Holbrook

Org version 7.8.11 with Emacs version 24

Validate XHTML 1.0

格式化及解析

MonetaryAmount可以通过MonetaryAmountFormat来与字符串进行解析/格式化。

// formatting by locale specific formats
MonetaryAmountFormat germanFormat = MonetaryFormats.getAmountFormat(Locale.GERMANY);
MonetaryAmountFormat usFormat = MonetaryFormats.getAmountFormat(Locale.CANADA);

MonetaryAmount amount = Money.of(12345.67, "USD");

String usFormatted = usFormat.format(amount); // "USD12,345.67"
String germanFormatted = germanFormat.format(amount); // 12.345,67 USD

// A MonetaryAmountFormat can also be used to parse MonetaryAmounts from strings
MonetaryAmount parsed = germanFormat.parse("12,4 USD");

可以通过AmountFormatQueryBuilder来生成自定义的格式。

// Creating a custom MonetaryAmountFormat
MonetaryAmountFormat customFormat = MonetaryFormats.getAmountFormat(
    AmountFormatQueryBuilder.of(Locale.US)
        .set(CurrencyStyle.NAME)
        .set("pattern", "00,00,00,00.00 ¤")
        .build());

// results in "00,01,23,45.67 US Dollar"
String formatted = customFormat.format(amount);

注意,这里的¤符号在模式串中是作为货币的占位符。

MonetaryAmount的使用可以在MonetaryAmount上进行算术运算:

总结

新的货币API这里已经介绍得差不多了。并且目前它的实现也已经相对稳定了(但还需要多补充些文档)。期待能在Java
9中看到这套新的接口!

上述示例可在Github中下载到。

MonetaryAmount twelveEuro = fiveEuro.add(sevenEuro); // "EUR 12" MonetaryAmount twoEuro = sevenEuro.subtract(fiveEuro); // "EUR 2" MonetaryAmount sevenPointFiveEuro = fiveEuro.multiply(1.5); // "EUR 7.5" // MonetaryAmount can have a negative NumberValue MonetaryAmount minusTwoEuro = fiveEuro.subtract(sevenEuro); // "EUR -2" // some useful utility methods boolean greaterThan = sevenEuro.isGreaterThan(fiveEuro); // true boolean positive = sevenEuro.isPositive(); // true boolean zero = sevenEuro.isZero(); // false // Note that MonetaryAmounts need to have the same CurrencyUnit to do mathematical operations // this fails with: javax.money.MonetaryException: Currency mismatch: EUR/USD fiveEuro.add(tenUsDollar); 

舍入操作是金额换算里面非常重要的一部分。MonetaryAmount可以使用舍入操作符来进行四舍五入:

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 

这里12.3456美金就会按当前货币默认的舍入规则来进行换算。在操作MonetaryAmount集合时,有许多实用的工具方法可以用来进行过滤,排序以及分组。这些方法还可以与Java
8的流API一起配套使用。看一下下面这个集合:

ListMonetaryAmount 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")); 

我们可以根据CurrencyUnit来进行金额过滤:

CurrencyUnit yen = MonetaryCurrencies.getCurrency("JPY"); CurrencyUnit dollar = MonetaryCurrencies.getCurrency("USD"); // 根据货币过滤,只返回美金 // result is [USD 18, USD 7, USD 42] ListMonetaryAmount onlyDollar = amounts.stream() .filter(MonetaryFunctions.isCurrency(dollar)) .collect(Collectors.toList()); // 根据货币过滤,只返回美金和日元 // [USD 18, USD 7, JPY 13.37, USD 42] ListMonetaryAmount onlyDollarAndYen = amounts.stream() .filter(MonetaryFunctions.isCurrency(dollar, yen)) .collect(Collectors.toList()); 

我们还可以过滤出大于或小于某个阈值的金额:

MonetaryAmount tenDollar = Money.of(10, dollar); // [USD 42, USD 18] ListMonetaryAmount greaterThanTenDollar = amounts.stream() .filter(MonetaryFunctions.isCurrency(dollar)) .filter(MonetaryFunctions.isGreaterThan(tenDollar)) .collect(Collectors.toList()); 

排序也是类似的:

// Sorting dollar values by number value // [USD 7, USD 18, USD 42] ListMonetaryAmount sortedByAmount = onlyDollar.stream() .sorted(MonetaryFunctions.sortNumber()) .collect(Collectors.toList()); // Sorting by CurrencyUnit // [EUR 2, JPY 13.37, USD 42, USD 7, USD 18] ListMonetaryAmount sortedByCurrencyUnit = amounts.stream() .sorted(MonetaryFunctions.sortCurrencyUnit()) .collect(Collectors.toList()); 

还有分组操作:

// 按货币单位进行分组 // {USD=[USD 42, USD 7, USD 18], EUR=[EUR 2], JPY=[JPY 13.37]} MapCurrencyUnit, ListMonetaryAmount groupedByCurrency = amounts.stream() .collect(MonetaryFunctions.groupByCurrencyUnit()); // 分组并进行汇总 MapCurrencyUnit, MonetarySummaryStatistics summary = amounts.stream() .collect(MonetaryFunctions.groupBySummarizingMonetary()).get(); // get summary for CurrencyUnit USD MonetarySummaryStatistics dollarSummary = summary.get(dollar); MonetaryAmount average = dollarSummary.getAverage(); // "USD 22.333333333333333333.." MonetaryAmount min = dollarSummary.getMin(); // "USD 7" MonetaryAmount max = dollarSummary.getMax(); // "USD 42" MonetaryAmount sum = dollarSummary.getSum(); // "USD 67" long count = dollarSummary.getCount(); // 3 

MonetaryFunctions还提供了归约函数,可以用来获取最大值,最小值,以及求和:

ListMonetaryAmount amounts = new ArrayList(); amounts.add(Money.of(10, "EUR")); amounts.add(Money.of(7.5, "EUR")); amounts.add(Money.of(12, "EUR")); OptionalMonetaryAmount max = amounts.stream().reduce(MonetaryFunctions.max()); // "EUR 7.5" OptionalMonetaryAmount min = amounts.stream().reduce(MonetaryFunctions.min()); // "EUR 12" OptionalMonetaryAmount sum = amounts.stream().reduce(MonetaryFunctions.sum()); // 

自定义的MonetaryAmount操作MonetaryAmount还提供了一个非常友好的扩展点叫作MonetaryOperator。MonetaryOperator是一个函数式接口,它接收一个MonetaryAmount入参并返回一个新的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 

标准的API特性都是通过MonetaryOperator的接口来实现的。比方说,前面看到的舍入操作就是以MonetaryOperator接口的形式来提供的。汇率货币兑换率可以通过ExchangeRateProvider来获取。JavaMoney自带了多个不同的ExchangeRateProvider的实现。其中最重要的两个是ECBCurrentRateProvider与
IMFRateProvider。ECBCurrentRateProvider查询的是欧洲中央银行(European
Central Bank,ECB)的数据而IMFRateProvider查询的是国际货币基金组织的汇率。

// get the default ExchangeRateProvider (CompoundRateProvider) ExchangeRateProvider exchangeRateProvider = MonetaryConversions.getExchangeRateProvider(); // get the names of the default provider chain // [IDENT, ECB, IMF, ECB-HIST] ListString defaultProviderChain = MonetaryConversions.getDefaultProviderChain(); // get a specific ExchangeRateProvider (here ECB) ExchangeRateProvider ecbExchangeRateProvider = MonetaryConversions.getExchangeRateProvider("ECB"); 

如果没有指定ExchangeRateProvider的话返回的就是CompoundRateProvider。CompoundRateProvider会将汇率转换请求委派给一个ExchangeRateProvider链并将第一个返回准确结果的提供商的数据返回。

// get the exchange rate from euro to us dollar ExchangeRate rate = exchangeRateProvider.getExchangeRate("EUR", "USD"); NumberValue factor = rate.getFactor(); // 1.2537 (at time writing) CurrencyUnit baseCurrency = rate.getBaseCurrency(); // EUR CurrencyUnit targetCurrency = rate.getCurrency(); // USD 

货币转换不同货币间的转换可以通过ExchangeRateProvider返回的CurrencyConversions来完成。

// get the CurrencyConversion from the default provider chain CurrencyConversion dollarConversion = MonetaryConversions.getConversion("USD"); // get the CurrencyConversion from a specific provider CurrencyConversion ecbDollarConversion = ecbExchangeRateProvider.getCurrencyConversion("USD"); MonetaryAmount tenEuro = Money.of(10, "EUR"); // convert 10 euro to us dollar MonetaryAmount inDollar = tenEuro.with(dollarConversion); // "USD 12.537" (at the time writing) 

请注意CurrencyConversion也实现了MonetaryOperator接口。正如其它操作一样,它也能通过MonetaryAmount.with()方法来调用。格式化及解析MonetaryAmount可以通过MonetaryAmountFormat来与字符串进行解析/格式化。

// formatting by locale specific formats MonetaryAmountFormat germanFormat = MonetaryFormats.getAmountFormat(Locale.GERMANY); MonetaryAmountFormat usFormat = MonetaryFormats.getAmountFormat(Locale.CANADA); MonetaryAmount amount = Money.of(12345.67, "USD"); String usFormatted = usFormat.format(amount); // "USD12,345.67" String germanFormatted = germanFormat.format(amount); // 12.345,67 USD // A MonetaryAmountFormat can also be used to parse MonetaryAmounts from strings MonetaryAmount parsed = germanFormat.parse("12,4 USD"); 

可以通过AmountFormatQueryBuilder来生成自定义的格式。

// Creating a custom MonetaryAmountFormat MonetaryAmountFormat customFormat = MonetaryFormats.getAmountFormat( AmountFormatQueryBuilder.of(Locale.US) .set(CurrencyStyle.NAME) .set("pattern", "00,00,00,00.00 ¤") .build()); // results in "00,01,23,45.67 US Dollar" String formatted = customFormat.format(amount); 

注意,这里的¤符号在模式串中是作为货币的占位符。总结新的货币API这里已经介绍得差不多了。并且目前它的实现也已经相对稳定了。期待能在Java
9中看到这套新的接口!上述示例可在Github中下载到。原文出处:Michael
Scharhag

发表评论

电子邮件地址不会被公开。 必填项已用*标注