前言

本文主要整理java.text包下的NumberFormat与DecimalFormat的关系,以及两者的介绍与使用。这里需要注意NumberFormat与DecimalFormat都是非线程安全的。
首先可以看到两者的关系图是继承关系:
在这里插入图片描述
可以看到在JAVA的Format家族中,主要分为3个分支,分别是

  • 格式化日期时间的DateFormat分支,主要用的其实现类SimpleDateFormat
  • 格式化文本消息的MessageFormat分支,自己就是实现类,常和ChoiceFormat配合使用
  • 格式化数字的NumberFormat分支,主要用的是NumberFormat和DecimalFormat

首先,我们要理解在JAVA中,NumberFormat类和DecimalFormat类究竟是用来干什么的。这一点在源码的注释里已经有答案了。

NumberFormat helps you to format and parse numbers for any locale. Your code can be completely independent of the locale conventions for decimal points, thousands-separators, or even the particular decimal digits used, or whether the number format is even decimal.
翻译:NumberFormat帮助您格式化和解析任何地区的数字。您的代码可以完全独立于小数点、千位分隔符、甚至所使用的特定小数位数的语言环境约定,或者数字格式是否为十进制。

DecimalFormat is a concrete subclass of NumberFormat that formats decimal numbers. It has a variety of features designed to make it possible to parse and format numbers in any locale, including support for Western, Arabic, and Indic digits. It also supports different kinds of numbers, including integers (123), fixed-point numbers (123.4), scientific notation (1.23E4), percentages (12%), and currency amounts ($123). All of these can be localized.
翻译:DecimalFormat是NumberFormat的一个具体子类,用于格式化十进制数字。它具有各种设计用来解析和格式化任何语言环境中的数字的特性,包括对西方数字、阿拉伯数字和印度数字的支持。它还支持不同种类的数字,包括整数(123)、定点数字(123.4)、科学记数法(1.23E4)、百分比(12%)和货币金额($123)。所有这些都可以本地化。

NumberFormat和DecimalFormat是被设计用来格式化所有地区对应数字格式的全能类。在不同的地区,表示数字的习惯千差万别,如果我们的代码要对不同地区的人们输出以不同方式表示的数字,就轮到NumberFormat和DecimalFormat出场了,它们之间的区别就在于前者是格式化数字,而后者是格式化十进制数字。

为了让使用者理解不同地区的数字表示方式究竟“有何不同”,在DecimalFormat的类注释中甚至给出了下面一段示例代码:

import java.text.DecimalFormat;
import java.text.NumberFormat;
import java.text.ParseException;
import java.util.Locale;

public class Test {
    public static void main(String[] s){
        Locale[] locales = NumberFormat.getAvailableLocales();
        double myNumber = -1234.56;
        NumberFormat form;
        for (int j = 0; j < 4; ++j) {
            System.out.println("FORMAT");
            for (int i = 0; i < locales.length; ++i) {
                if (locales[i].getCountry().length() == 0) {
                    continue; // Skip language-only locales
                }
                System.out.print(locales[i].getDisplayName());
                switch (j) {
                    case 0:
                        form = NumberFormat.getInstance(locales[i]);
                        break;
                    case 1:
                        form = NumberFormat.getIntegerInstance(locales[i]);
                        break;
                    case 2:
                        form = NumberFormat.getCurrencyInstance(locales[i]);
                        break;
                    default:
                        form = NumberFormat.getPercentInstance(locales[i]);
                        break;
                }
                if (form instanceof DecimalFormat) {
                    System.out.print(": " + ((DecimalFormat) form).toPattern());
                }
                System.out.print(" -> " + form.format(myNumber));
                try {
                    System.out.println(" -> " + form.parse(form.format(myNumber)));
                } catch (ParseException e) {
                }
            }
        }
    }
}

输出的内容很多,这里只拿其中几条比较有特点的数据来展示。以下是case 0:分支的输出结果,在该分支中座的操作是将-1234.56格式化为小数形式的字符串,再将字符串解析回数字:

阿拉伯文 (阿拉伯联合酋长国): #,##0.###;#,##0.###- -> 1,234.56- -> -1234.56
阿拉伯文 (约旦): #,##0.###;#,##0.###- -> 1,234.56- -> -1234.56
阿拉伯文 (叙利亚): #,##0.###;#,##0.###- -> 1,234.56- -> -1234.56
克罗地亚文 (克罗地亚): #,##0.### -> -1.234,56 -> -1234.56
法文 (比利时): #,##0.### -> -1.234,56 -> -1234.56
西班牙文 (巴拿马): #,##0.### -> -1,234.56 -> -1234.56
西班牙文 (委内瑞拉): #,##0.### -> -1.234,56 -> -1234.56
马耳他文 (马耳他): #,##0.### -> -1,234.56 -> -1234.56
中文 (台湾地区): #,##0.### -> -1,234.56 -> -1234.56

印地文 (印度): #,##0.### -> -१,२३४.५६ -> -1234.56
英文 (马耳他): #,##0.### -> -1,234.56 -> -1234.56
芬兰文 (芬兰): #,##0.### -> -1 234,56 -> -1234.56

法文 (瑞士): #,##0.### -> -1'234.56 -> -1234.56

捷克文 (捷克共和国): #,##0.## -> -1 234,56 -> -1234.56

西班牙文 (西班牙): #,##0.### -> -1.234,56 -> -1234.56
保加利亚文 (保加利亚): #,##0.### -> -1 234,56 -> -1234.56

泰文 (泰国,TH): #,##0.### -> -๑,๒๓๔.๕๖ -> -1234.56

中文 (中国): #,##0.### -> -1,234.56 -> -1234.56

可以看到,在所有的输出中代码都成功地将-1234.56"format"成了对应的字符串表示,然后又"pase"回了-1234.56,但是在不同的国家和地区,数字的字符串表示方式却差异极大:

  • 中国是我们熟悉的用英文逗号做千分位符,英文句号做小数点,但是芬兰和波兰就是用空格作千分位符,西班牙则是用英文句号做千分位符,英文逗号做小数点,跟中国正好相反;
  • 更奇葩的是泰国和印度,在泰文和印地文中,甚至不用阿拉伯数字来表示数字;
  • 而在阿拉伯数字的起源地,使用阿拉伯文的阿联酋,他们写负数时,是把负号放在数字后边的……

一、NumberFormat

1、概述

NumberFormat官网:
https://docs.oracle.com/javase/7/docs/api/java/text/NumberFormat.html

  • NumberFormat是所有数字格式的抽象基类。此类提供了格式化和解析数字的接口。NumberFormat还提供了确定哪些区域设置具有数字格式以及它们的名称的方法。
  • NumberFormat帮助您格式化和解析任何地区的数字。您的代码可以完全独立于小数点、千位分隔符、甚至所使用的特定小数位数的语言环境约定,或者数字格式是否为十进制。

2、实例化方法

  • NumberFormat是抽象类,可以通过其本身提供的getxxxInstance()静态方法获得实例对象。
  • getxxxInstance()本质是创建了一个DecimalFormat对象,该对象默认使用的是进位方式是RoundingMode.HALF_EVEN,此舍入模式也称为“银行家算法”,主要在美国使用。
  • 银行家算法:四舍六入五考虑,五后非零就进一,五后为零看奇偶,五前为偶应舍去,五前为奇要进一
方法说明
getInstance()、getNumberInstance()返回当前默认语言环境的通用数值格式。
Locale[] getAvailableLocales()返回所有语言环境的数组
getInstance(Locale)、getNumberInstance(Locale)返回指定语言环境的通用数值格式。
getIntegerInstance()获取格式化为整型数的对象 ,通过此实例可以实现截取整数的需求,但是需要注意若对包含小数的数字格式化可能有舍入的问题
getCurrencyInstance()返回当前默认语言环境的格式化为货币样式的对象
getCurrencyInstance(Locale)获取格式化为指定地区的货币样式的对象
getPercentInstance()返回当前默认语言环境的格式化为百分比的对象
getPercentInstance​(Locale inLocale)返回指定语言环境的百分比格式。若是不指定参数,则以默认语言为参数。
NumberFormat.setMinimumIntegerDigits(int)设置数的整数部分所允许的最小位数,不足的位数以0补位,默认位数为1
NumberFormat.setMaximumIntegerDigits(int)设置数的整数部分所允许的最大位数,超出的话从小数点前截取指定位数,默认位数为40,如整数部分最大2位,则123.45->23.45
NumberFormat.setMinimumFractionDigits(int)设置最少小数点位数,不足的位数以0补位; 默认位数为0
NumberFormat.setMaximumFractionDigits(int)设置最多保留小数位数,不足不补0,超出部分四舍六入,默认位数为3
import java.text.NumberFormat;

public class Test1 {
    public static void main(String[] s){
        double number = 12345.67321;

        NumberFormat numberFormat = NumberFormat.getCurrencyInstance();
        System.out.println(numberFormat.format(number));//¥12,345.67 货币格式的实例化对象,若没有设置小数点允许最大位数,默认是2位

        numberFormat.setMaximumFractionDigits(4); //设置小数点允许最大位数4位,超出部分按RoundingMode.HALF_EVEN模式进位
        System.out.println(numberFormat.format(number));//¥12,345.6732
 
        numberFormat.setRoundingMode(RoundingMode.HALF_UP); //设置进位方式为四舍五入
        System.out.println(numberFormat.format(number));//¥12,345.6732

        NumberFormat numberFormat1 = NumberFormat.getInstance();
        System.out.println(numberFormat1.format(number));//12,345.673  数字格式的实例化对象,若没有设置小数点允许最大位数,默认是3位

        numberFormat1.setMinimumIntegerDigits(6);//设置整数部分最小允许为6位,不足左补0,
        System.out.println(numberFormat1.format(number));// 012,345.673

        numberFormat1.setMaximumIntegerDigits(3);//设置整数部分最大允许为3位,超出部分截取掉
        System.out.println(numberFormat1.format(number));// 345.673

        //getxxxInstance()本质是创建了一个DecimalFormat对象,该对象默认使用的是进位方式是RoundingMode.HALF_EVEN,此舍入模式也称为“银行家算法”,主要在美国使用。
        //银行家算法:四舍六入五考虑,五后非零就进一,五后为零看奇偶,五前为偶应舍去,五前为奇要进一
        NumberFormat numberFormat2 = NumberFormat.getIntegerInstance();
        System.out.println(numberFormat2.format(number));//12,346  此处有四舍六入的问题
    }
}

3、货币格式化

  • getCurrencyInstance()。静态方法,建立一个NumberFormat类的对象并返回引用,该引用指定货币格式为系统预设的货币格式。
  • getCurrencyInstance(Locale) 。静态方法,建立一个NumberFormat类的对象,并返回引用,该引用的货币格式由Locale指定。Locale类在java.util包中。
  • getCurrencyInstance()/getCurrencyInstance(Locale) 底层默认创建一个DecimalFormat对象,进位方式默认为RoundingMode.HALF_EVEN。小数点部分默认允许最小位数为0,默认最大位数为指定货币格式的默认位数,如人民币默认小数点2位。
import java.text.NumberFormat;
import java.util.Locale;

public class Test1 {
    public static void main(String[] s){
        double number = 12345.67321;
        //按系统预设的货币格式输出,这里是人民币
        //人民币默认的小数点最小允许位数为0,最大允许位数为2, 不同币种默认值不同
        NumberFormat nf = NumberFormat.getCurrencyInstance();
        System.out.println(((DecimalFormat)nf).toPattern());//输出人民币格式化格式   ¤#,##0.00
        System.out.println(nf.format(number));//¥12,345.67   人民币格式默认小数点后面最大是2位

        nf.setGroupingUsed(false);//设置是否按默认格式进行分组, 人民币中是“,” 芬兰用空格作为分隔符
        System.out.println(nf.format(number));// ¥12345.67

        nf.setMaximumFractionDigits(4);//设置小数点最多允许4位, 超出部分按RoundingMode.HALF_EVEN方式进位
        System.out.println(nf.format(number));//¥12345.6732

        nf.setMaximumIntegerDigits(3);//设置整数最多允许3位,超出部分截取掉
        System.out.println(nf.format(number));//¥345.6732

        nf.setMinimumIntegerDigits(6);//设置整数最少6位,不足左补0
        System.out.println(nf.format(number));//¥012345.6732

        //按指定的货币格式输出,这里是美元
        nf = NumberFormat.getCurrencyInstance(Locale.US);
        System.out.println(nf.format(number));//$12,345.67

        //按指定的货币格式输出,这里是韩元  韩元默认小数点后面的最大位数为0
        nf = NumberFormat.getCurrencyInstance(Locale.KOREA);
        System.out.println(nf.format(number));// ₩12,346
    }
}

4、百分比格式化

  • getPercentInstance()。静态方法,创建一个NumberFormat类的对象并返回其引用。该对象指定百分比格式为系统预设格式。
  • getPercentInstance(Locale)。静态方法,创建一个NumberFormat类的对象并返回引用。该对象的百分比格式由Locale来指定。
  • getPercentInstance()/getPercentInstance(Locale)默认创建一个DecimalFormat对象,进位方式默认为RoundingMode.HALF_EVEN。不同国家百分比的默认小数点位数不同,如人民币默认没有小数点,但可以通过setMinimumFractionDigits/setMaximumFractionDigits设置小数点最小最大位数。
import java.text.NumberFormat;
import java.util.Locale;

public class Test1 {
    public static void main(String[] s){
        double d = 123.456;
        //按系统预设百分比格式输出,这里指人民币,人民币默认的百分比小数点最小允许位数为0,最大允许位数为0
        //不同币种默认的小数点最小最大位数是不同的
        NumberFormat nf = NumberFormat.getPercentInstance();
        System.out.println(((DecimalFormat)nf).toPattern());//输出人民币格式化格式   #,##0%
        System.out.println(nf.format(d));//12,346%

        nf.setMaximumFractionDigits(1);  //设置百分比格式中小数点最大允许位数为1
        System.out.println(nf.format(d));//12,345.6%

        //按指定百分比格式输出,这里是法国格式
        nf = NumberFormat.getPercentInstance(Locale.FRANCE);
        System.out.println(nf.format(d));//12 346 %
    }
}

5、NumberFormat的坑

5.1、不同的格式化对象处理相同数值返回结果不同

问题
import java.text.NumberFormat;

public class Test1 {
    public static void main(String[] s){
        NumberFormat numberFormat = NumberFormat.getCurrencyInstance();
        numberFormat.setMinimumFractionDigits(2);
        System.out.println(numberFormat.getCurrency().getDisplayName());
        System.out.println(numberFormat.format(1234.567));//¥1,234.57, 货币格式的实例化对象,若没有设置小数点允许最大位数,默认是2位

        NumberFormat numberFormat1 = NumberFormat.getInstance();
        numberFormat1.setMinimumFractionDigits(2);
        System.out.println(numberFormat1.getCurrency().getDisplayName());
        System.out.println(numberFormat1.format(1234.567));//1,234.567 数字格式的实例化对象,若没有设置小数点允许最大位数,默认是3位
    }
}

执行结果:

人民币
¥1,234.57
人民币
1,234.567

问题
为什么对于同一个数字1234.567,在两个实例对象中都设置的是小数点后最少两位小数,为什么两种实例处理后的结果不同?

源码分析:
package java.text;

public abstract class NumberFormat extends Format {

    /**
     * 数字整数部分允许的最大位数。默认=40
     */
    private int    maximumIntegerDigits = 40;

    /**
     * 数字整数部分允许的最小位数。默认=1
     */
    private int    minimumIntegerDigits = 1;

    /**
     * 数字小数部分允许的最大位数。默认=3
     */
    private int    maximumFractionDigits = 3;    // invariant, >= minFractionDigits

    /**
     * 数字小数部分允许的最小位数。默认=0
     */
    private int    minimumFractionDigits = 0;

    // Constants used by factory methods to specify a style of format.
    private static final int NUMBERSTYLE = 0;//默认形式
    private static final int CURRENCYSTYLE = 1;//货币形式
    private static final int PERCENTSTYLE = 2;//百分比形式
    private static final int SCIENTIFICSTYLE = 3;//科学记数法形式
    private static final int INTEGERSTYLE = 4;//整数形式

    public final static NumberFormat getInstance() {
        return getInstance(Locale.getDefault(Locale.Category.FORMAT), NUMBERSTYLE);
    }

    public final static NumberFormat getCurrencyInstance() {
        return getInstance(Locale.getDefault(Locale.Category.FORMAT), CURRENCYSTYLE);
    }

    private static NumberFormat getInstance(Locale desiredLocale,
                                            int choice) {
        LocaleProviderAdapter adapter;
        adapter = LocaleProviderAdapter.getAdapter(NumberFormatProvider.class,
                desiredLocale);
        NumberFormat numberFormat = getInstance(adapter, desiredLocale, choice);
        if (numberFormat == null) {
            numberFormat = getInstance(LocaleProviderAdapter.forJRE(),
                    desiredLocale, choice);
        }
        return numberFormat;
    }
	//调用该方法返回实例化后的NumberFormat对象
    private static NumberFormat getInstance(LocaleProviderAdapter adapter,
                                            Locale locale, int choice) {
        NumberFormatProvider provider = adapter.getNumberFormatProvider();
        NumberFormat numberFormat = null;
        switch (choice) {
            case NUMBERSTYLE:
                numberFormat = provider.getNumberInstance(locale);
                break;
            case PERCENTSTYLE:
                numberFormat = provider.getPercentInstance(locale);
                break;
            case CURRENCYSTYLE:
                numberFormat = provider.getCurrencyInstance(locale);
                break;
            case INTEGERSTYLE:
                numberFormat = provider.getIntegerInstance(locale);
                break;
        }
        return numberFormat;
    }
}

从以上代码可以看出,NumberFormat类默认有整数部分与小数部分允许的最小最大位数,接着我们看一下创建实例对象部分的区别。

追踪源码发现,最后全部都调用到了sun.util.locale.provider包下的NumberFormatProviderImpl类中的getInstance(Locale var1, int var2)方法。

private NumberFormat getInstance(Locale var1, int var2) {
   if (var1 == null) {
        throw new NullPointerException();
    } else {
        LocaleProviderAdapter var3 = LocaleProviderAdapter.forType(this.type);
        String[] var4 = var3.getLocaleResources(var1).getNumberPatterns();
        DecimalFormatSymbols var5 = DecimalFormatSymbols.getInstance(var1);
        int var6 = var2 == 4 ? 0 : var2;
        //创建DecimalFormat对象,该对象默认使用的是进位方式是RoundingMode.HALF_EVEN,此舍入模式也称为“银行家算法”,主要在美国使用。

       //银行家算法:四舍六入五考虑,五后非零就进一,五后为零看奇偶,五前为偶应舍去,五前为奇要进一
       //这里会设置整数与小数部分默认的最小最大位数,不同格式的实例化对象位数是不同的,如货币格式与百分比格式的最小最大位数就不同,
       //具体是var3.getLocaleResources(var1).getNumberPatterns();这行代码返回的pattern决定的
        DecimalFormat var7 = new DecimalFormat(var4[var6], var5);        
        if (var2 == 4) {
           //当var2=整数形式,设置小数点部分允许的最大位数为0,这样就没有小数点了,只会有整数部分
            var7.setMaximumFractionDigits(0);
            var7.setDecimalSeparatorAlwaysShown(false);
            var7.setParseIntegerOnly(true);
        } else if (var2 == 1) {
        	//此处就是产生不同的重点:当var2=货币形式的时候,进行了特殊处理
            adjustForCurrencyDefaultFractionDigits(var7, var5);
        }

        return var7;
    }
}

private static void adjustForCurrencyDefaultFractionDigits(DecimalFormat var0, DecimalFormatSymbols var1) {
  //返回指定语言的货币信息,如中国的货币信息为:
  //currencyCode: CNY
  //defaultFractionDigits: 2
  //numericCode: 156
  Currency var2 = var1.getCurrency();
   if (var2 == null) {
       try {
           var2 = Currency.getInstance(var1.getInternationalCurrencySymbol());
       } catch (IllegalArgumentException var5) {
           ;
       }
   }

   if (var2 != null) {
       int var3 = var2.getDefaultFractionDigits();
       if (var3 != -1) {
       	   //获取小数部分允许的最小位数
           int var4 = var0.getMinimumFractionDigits();
           //判断小数部分允许的最小位数与最大位数是否相等
           if (var4 == var0.getMaximumFractionDigits()) {
               //若最小位数与最大位数相等,则设置最小位数与最大位数为同一值
               var0.setMinimumFractionDigits(var3);
               var0.setMaximumFractionDigits(var3);
           } else {
               //若最小位数与最大位数不相等
               //则设置最小允许位数为指定语言所允许的默认位数与自己设置的最小值,对于人民币来说,该值不会超过2
               var0.setMinimumFractionDigits(Math.min(var3, var4));
               //设置最大允许位数为指定语言所允许的默认位数,如人民币允许的默认位数为2
               var0.setMaximumFractionDigits(var3);
           }
       }
   }

}

Currency表示货币。货币由 ISO 4217 货币代码标识。如下代码返回 ISO 4217 货币的所有信息,包含不同货币的默认小数点位数

import java.util.Currency;
import java.util.Set;

public class Test1 {
    public static void main(String[] s){
        Set<Currency> currencys =  Currency.getAvailableCurrencies();
        currencys.forEach(item->{
            System.out.println("货币名称:"+item.getDisplayName()+";货币代码:"+item.getCurrencyCode()+";默认小数点位数:"+item.getDefaultFractionDigits());
        });       
    }
}

执行结果(截取部分内容)如下图:

货币名称:比利时法郎;货币代码:BEF;默认小数点位数:0
货币名称:UYI;货币代码:UYI;默认小数点位数:0
货币名称:缅甸开亚特;货币代码:MMK;默认小数点位数:2
货币名称:朝鲜圆;货币代码:KPW;默认小数点位数:2
货币名称:塔吉克斯坦索莫尼;货币代码:TJS;默认小数点位数:2
货币名称:塞浦路斯镑;货币代码:CYP;默认小数点位数:2
货币名称:立陶宛立特;货币代码:LTL;默认小数点位数:2
货币名称:巴基斯坦卢比;货币代码:PKR;默认小数点位数:2
货币名称:帝汶埃斯库多;货币代码:TPE;默认小数点位数:0

货币名称:伊拉克第纳尔;货币代码:IQD;默认小数点位数:3
货币名称:智利 Unidades de Fomento(资金);货币代码:CLF;默认小数点位数:4
货币名称:美元(次日);货币代码:USN;默认小数点位数:2

货币名称:斯洛文尼亚托拉尔;货币代码:SIT;默认小数点位数:2
货币名称:安道尔比塞塔;货币代码:ADP;默认小数点位数:0
货币名称:阿曼里亚尔;货币代码:OMR;默认小数点位数:3
货币名称:葡萄牙埃斯库多;货币代码:PTE;默认小数点位数:0
货币名称:亚美尼亚德拉姆;货币代码:AMD;默认小数点位数:2
货币名称:欧洲计算单位 (XBD);货币代码:XBD;默认小数点位数:-1
货币名称:墨西哥比索;货币代码:MXN;默认小数点位数:2
货币名称:加拿大元;货币代码:CAD;默认小数点位数:2
货币名称:突尼斯第纳尔;货币代码:TND;默认小数点位数:3
货币名称:牙买加元;货币代码:JMD;默认小数点位数:2
货币名称:ADB Unit of Account;货币代码:XUA;默认小数点位数:-1
货币名称:阿富汗尼 (1927-2002);货币代码:AFA;默认小数点位数:2
货币名称:苏里南盾;货币代码:SRG;默认小数点位数:2
货币名称:冰岛克朗;货币代码:ISK;默认小数点位数:0
货币名称:South Sudanese Pound;货币代码:SSP;默认小数点位数:2
货币名称:罗马尼亚列伊;货币代码:RON;默认小数点位数:2
货币名称:俄国卢布 (1991-1998);货币代码:RUR;默认小数点位数:2
货币名称:越南盾;货币代码:VND;默认小数点位数:0
货币名称:新台币;货币代码:TWD;默认小数点位数:2
货币名称:欧元;货币代码:EUR;默认小数点位数:2
货币名称:阿尔巴尼亚列克;货币代码:ALL;默认小数点位数:2
货币名称:保加利亚新列弗;货币代码:BGN;默认小数点位数:2
货币名称:南非兰特;货币代码:ZAR;默认小数点位数:2
货币名称:直布罗陀镑;货币代码:GIP;默认小数点位数:2
货币名称:波兰兹罗提;货币代码:PLN;默认小数点位数:2
货币名称:希腊德拉克马;货币代码:GRD;默认小数点位数:0
货币名称:旧罗马尼亚列伊;货币代码:ROL;默认小数点位数:2
货币名称:日元;货币代码:JPY;默认小数点位数:0
货币名称:人民币;货币代码:CNY;默认小数点位数:2
货币名称:尼泊尔卢比;货币代码:NPR;默认小数点位数:2

结论
对于货币格式化的NumberFormat对象,小数点后在最大允许位数为其指定语言货币允许的默认小数点位数。如人民币货币格式化的NumberFormat对象,小数点后最大允许位数默认且只能为2。

二、DecimalFormat

1、概述

DecimalFormat官网:
https://docs.oracle.com/javase/7/docs/api/java/text/DecimalFormat.html
Java 1.7 API中这样定义:

DecimalFormat是NumberFormat的具体子类,用于格式化十进制数字。它具有多种功能,可以在任何语言环境中解析和格式化数字,包括支持西方数字、阿拉伯数字和印度数字。它还支持不同类型的数字,包括整数(123)、定点数字(123.4)、科学符号(1.23E4)、百分比(12%)和货币金额(123美元)。所有这些都可以本地化。

2、常用方法

  • DecimalFormat() 使用默认语言环境的默认模式和符号创建对象。
  • DecimalFormat(String pattern) 使用给定模式和默认语言环境的符号创建对象。
  • DecimalFormat默认创建一个格式化的实例对象,进位方式为RoundingMode.HALF_EVEN,此舍入模式也称为“银行家算法”。
DecimalFormat df = new DecimalFormat();
double d = 123456.635;
//输出当前系统环境默认的语言环境及格式,  不同国家的pattern格式是不相同的
System.out.println(Locale.getDefault().getDisplayName()+";"+df.toPattern());//中文 (中国);#,##0.###
System.out.println(df.format(d));//123,456.635

//applyPattern设置格式模式, 这里设置保留两位小数,整数部分每两位进行分组
df.applyPattern("#,#0.00");
System.out.println(df.format(d));//12,34,56.63

//设置不使用,进行分组
df.setGroupingUsed(false);
System.out.println(df.format(d));//123456.63

//设置进位方式为四舍五入
df.setRoundingMode(RoundingMode.HALF_UP);
System.out.println(df.format(Double.valueOf(d))); //123456.63  double类型会导致精度问题,如下,同一个数字,double类型与BigDecimal类型的结果不同
System.out.println(df.format(new BigDecimal("123456.635")));//123456.64

//创建一个国际化的格式化对象,这里使用朝鲜的格式化对象
df = (DecimalFormat) NumberFormat.getInstance(Locale.KOREA);
System.out.println(df.format(d));//123,456.635

df.setMaximumFractionDigits(2);//设置小数点允许最大位数为2
System.out.println(df.format(d));//123,456.63

3、字符及含义

字符位置本地化说明
0数字代表阿拉伯数字,使用特殊字符“0”表示数字的一位阿拉伯数字,如果该位不存在数字,则显示0
#数字数字,被格式化数值不够的位数忽略,若够则不变
.数字小数分隔符或货币小数分隔符
-数字负号,缺省负数前缀
,数字分组分隔符
E数字用科学记数法分隔尾数和指数。 不需要在前缀或后缀中引用。
;子模式边界将正面和负面的子图案分开
%字首或字尾乘以100并显示为百分比
\u2030字首或字尾乘以1000并显示为千分数,显示出来为‰
¤(\u00A4)字首或字尾货币符号,由货币符号取代。如果连续出现2个,则用国际货币符号代替。如果出现在某个模式中,则使用货币小数分隔符而不是小数分隔符。
字首或字尾用于引用前缀或后缀中的特殊字符,例如,“’#’#“格式为123到 “#123”。要创建单引号本身,请连续使用两个:”# o’'clock”。
DecimalFormat df = new DecimalFormat();
double data = 1234.56789;
System.out.println("格式化之前的数字: " + data);

// 定义要显示的数字的格式,模式中的"#"表示如果位数不足则以 0填充
String style = "0.00000000";
df.applyPattern(style);   // 将格式应用于格式化器
System.out.println("采用style: " + style + "  格式化之后: " + df.format(data));

// 模式中的"#"表示如果该位存在字符,则显示字符,如果不存在,则不显示。
style = "######.##########";
df.applyPattern(style);
System.out.println("采用style: " + style + "  格式化之后: " + df.format(data));

// 在格式后添加单位字符
style = "00000.000 kg";
df.applyPattern(style);
System.out.println("采用style: " + style + "  格式化之后: " + df.format(data));

// 模式中的"-"表示输出为负数,要放在最前面
style = "-000.000";
df.applyPattern(style);
System.out.println("采用style: " + style + "  格式化之后: " + df.format(data));

// 模式中的","在数字中添加逗号,方便读数字
style = "-0,000.0#";
df.applyPattern(style);
System.out.println("采用style: " + style + "  格式化之后: " + df.format(data));

// 模式中的"E"表示输出为指数,"E"之前的字符串是底数的格式,"E"之后的是字符串是指数的格式
style = "0.000E000";
df.applyPattern(style);
System.out.println("采用style: " + style + "  格式化之后: " + df.format(data));

// 模式中的"%"表示乘以100并显示为百分数,要放在最后。
style = "0.00 %";
df.applyPattern(style);
System.out.println("采用style: " + style + "  格式化之后: " + df.format(data));

// 模式中的"\u2030"表示乘以1000并显示为千分数,要放在最后。
style = "0.00 \u2030";
DecimalFormat df1 = new DecimalFormat(style);  //在构造函数中设置数字格式
System.out.println("采用style: " + style + "  格式化之后: " + df1.format(data));

// 以逗号分隔
// 如果使用具有多个分组字符的模式,则最后一个分隔符和整数结尾之间的间隔才是使用的分组大小。
// 所以 "#,##,###,####" == "######,####" == "##,####,####"。
style = "000,0,00.##########";
df.applyPattern(style);
System.out.println("采用style: " + style + "  格式化之后: " + df.format(data));

// 嵌入文本中
style = "这件衣服的价格是 ##.## 元";
df.applyPattern(style);
System.out.println("采用style: " + style + "  格式化之后: " + df.format(data));

// 货币符号
style = "##.##\u00A4\u00A4";
df.applyPattern(style);
System.out.println("采用style: " + style + "  格式化之后: " + df.format(data));

// 特殊符号
style = "'#'##.##";
df.applyPattern(style);
System.out.println("采用style: " + style + "  格式化之后: " + df.format(data));

执行结果:

格式化之前的数字: 1234.56789
采用style: 0.00000000  格式化之后: 1234.56789000
采用style: ######.##########  格式化之后: 1234.56789
采用style: 00000.000 kg  格式化之后: 01234.568 kg
采用style: -000.000  格式化之后: -1234.568
采用style: -0,000.0#  格式化之后: -1,234.57
采用style: 0.000E000  格式化之后: 1.235E003
采用style: 0.00 %  格式化之后: 123456.79 %
采用style: 0.00 ‰  格式化之后: 1234567.89 ‰
采用style: 000,0,00.##########  格式化之后: 00,12,34.56789
采用style: 这件衣服的价格是 ##.## 元  格式化之后: 这件衣服的价格是 1234.57 元
采用style: ##.##¤¤  格式化之后: 1234.57CNY
采用style: '#'##.##  格式化之后: #1234.57

0与#的区别

#:表示该位没有数字时填空显示,有则直接显示;出现在小数位部分时,n个#只保留n位有效小数(比如1.00不保留,1.11则保留),当小数位数大于#的个数时四舍五入;

0: 表示该位没有数字时补零显示,有则直接显示,即强制按格式对齐;出现在小数位部分时,n个0表示保留n位小数,当小数位数大于#的个数时四舍五入;

另外,还有一个神奇的特例现象,不过总体表现就是上述的几个特点:

  • #.00:小数点左边的#表示在没有数字时或只有个位且个位为零的时候填位为空。如0.11–> .11
  • #.##:小数点左边的#表示在没有数字时填位为0 如 .11–>0.11
double d = 0.127;
System.out.println(new DecimalFormat("#.##").format(d));//0.13
//小数点左边的#表示在没有数字时或只有个位且个位为零的时候填位为空
System.out.println(new DecimalFormat("#.00").format(d));//.13
System.out.println(new DecimalFormat("0.##").format(d));//0.13
System.out.println(new DecimalFormat("0.00").format(d));//0.13

d = 0.10;
System.out.println(new DecimalFormat("#.##").format(d));//0.1
//小数点左边的#表示在没有数字时或只有个位且个位为零的时候填位为空
System.out.println(new DecimalFormat("#.00").format(d));//.10
System.out.println(new DecimalFormat("0.##").format(d));//0.1
System.out.println(new DecimalFormat("0.00").format(d));//0.10

d = .10;
//小数点左边的#表示在没有数字时填位为0
System.out.println(new DecimalFormat("#.##").format(d));//0.1
System.out.println(new DecimalFormat("#.00").format(d));//.10
System.out.println(new DecimalFormat("0.##").format(d));//0.1
System.out.println(new DecimalFormat("0.00").format(d));//0.10

 //整数部分超过pattern中的整数部分时原文显示,小数部分超过pattern中的小数部分时以RoundingMode.HALF_EVEN方式进位
d = 12.357;
System.out.println(new DecimalFormat("#.##").format(d));//12.36
System.out.println(new DecimalFormat("#.00").format(d));//12.36
System.out.println(new DecimalFormat("0.##").format(d));//12.36
System.out.println(new DecimalFormat("0.00").format(d));//12.36

一般,可以使用0.00或0.##或#0.00等类似的模式满足需求,我这里仅保留两位小数,既可以有保留功能也可以避免位上空现象。

注意:
整数部分的#写太多其实并没有什么意义,一般写的多是用来分组,如“#,##0.00”指的是小数点后保留两位小数,整数部分每3个数字用“,”分隔;

分组分隔符的使用

pattern模式中“,”一般作为分组分隔符,如果使用具有多个分组字符的模式,则最后一个分隔符和整数结尾之间的间隔才是使用的分组大小。如"#,##,###,####"的分组为4。

代码示例

import java.text.DecimalFormat;

public class Test {
    public static void main(String[] s){
        DecimalFormat df = new DecimalFormat("#,##,###,####");
        System.out.println(df.format(12345));//1,2345
        System.out.println(df.format(1234567));//123,4567
    }
}

“%” 将数字乘以100

double d= 0.1234;
System.out.println(new DecimalFormat("0.00%").format(d));//12.34%
System.out.println(new DecimalFormat("0%.00").format(d));//12.34%
System.out.println(new DecimalFormat("%0.00").format(d));//%12.34

“\u2030” 将数字乘以1000

double d = 0.1234;
System.out.println(new DecimalFormat("0.00\u2030").format(d));//123.40‰
System.out.println(new DecimalFormat("0.0\u20300").format(d));//123.40‰
System.out.println(new DecimalFormat("\u20300.00").format(d));//‰123.40

“¤(\u00A4)” 本地化货币符号

如果连续出现2次,代表货币符号的国际代号。

double d= 1234.5678;
System.out.println(new DecimalFormat(",000.00¤").format(d));//1,234.57¥
System.out.println(new DecimalFormat(",000.¤00").format(d));//1,234.57¥
System.out.println(new DecimalFormat("¤,000.00").format(d));//¥1,234.57
System.out.println(new DecimalFormat(",00¤0.¤00").format(d));//1,234.57¥¥
System.out.println(new DecimalFormat("¤,000.¤00").format(d));//¥1,234.57¥
System.out.println(new DecimalFormat(",000.00¤¤").format(d));//1,234.57CNY

负号"-“与子模式分隔符”;"的使用

一般来说,这两个符号是组合使用的。在默认情况下,DecimalFormat在格式化负数时,会自动在前面加上一个符号"-“,但是如果你想自定义负号的位置(如123.45-),就需要再写一个负数子模式,放在正数子模式后面,中间用”;"分隔。

代码示例:

public static void main(String[] s){
    double d1 = 123.4567;
    double d2 = -123.4567;
    
    DecimalFormat df = new DecimalFormat("-#.00");
    System.out.println(df.format(d1));
    System.out.println(df.format(d2));

    DecimalFormat df1 = new DecimalFormat("#.00");
    DecimalFormat df2 = new DecimalFormat("#.00;#.00-");
    System.out.println("使用\"#.00\"模板得到的结果:");
    System.out.println(df1.format(d1));
    System.out.println(df1.format(d2));
    System.out.println("使用\"#.00;#.00-\"模板得到的结果:");
    System.out.println(df2.format(d1));
    System.out.println(df2.format(d2));
}

执行结果:

-123.46
--123.46
使用"#.00"模板得到的结果:
123.46
-123.46
使用"#.00;#.00-"模板得到的结果:
123.46
123.46-

注意

若用DecimalFormat(“-0.00”)对负数进行格式化时,负数本身有个负号,格式化pattern中也有负号,会导致有两个负号。

科学计数法 “E”

double d = 123456.3456;
System.out.println(new DecimalFormat("0E0").format(d));//1E5
System.out.println(new DecimalFormat("0E00").format(d));//1E05
System.out.println(new DecimalFormat("#E0").format(d));//.1E6
System.out.println(new DecimalFormat("##E0").format(d));//12E4
System.out.println(new DecimalFormat("###E0").format(d));//123E3
System.out.println(new DecimalFormat("####E0").format(d));//12.35E4
System.out.println(new DecimalFormat("#####E0").format(d));//1.2346E5
System.out.println(new DecimalFormat("######E0").format(d));//123456E0
System.out.println(new DecimalFormat("#######E0").format(d));//123456.3E0

/**
 * 0的个数决定最后输出结果的位数
 * 并且与0的位置无关
 */
d = 12345;
System.out.println(new DecimalFormat("###.##E0").format(d));//12.345E3
System.out.println(new DecimalFormat("##0.##E0").format(d));//12.345E3
System.out.println(new DecimalFormat("##0.0##E0").format(d));//12.345E3
System.out.println(new DecimalFormat("##0.00000##E0").format(d));//12.3450E3
System.out.println(new DecimalFormat("#00.0000##E0").format(d));//12.3450E3
System.out.println(new DecimalFormat("#00.00000##E0").format(d));//12.34500E3

总结:

  • 使用科学计数法,首先保证E前面有0或者#,否则就不是科学计数法。
  • E后面必须是0,0的个数对后面的显示是有影响的,多余就会填充0.
  • E前面只有一个#,得到的结果肯定是.开头的结果。
  • E前面#与0的总个数决定后面的指数,具体:总个数和指数比较,如果指数的值大于总个数,那么得到的指数的值是个数的倍数;如果指数的值小于等于总个数,那么得到的指数的值等于总个数;
  • 整个模式中的0的总个数决定最后输出结果的位数,并且与0的位置无关。
  • 如果整数部分需要保留几位数,就使用几个0。

" ’ " 用于引用特殊的字符,作为前缀或后缀。

double d= 1.5678;
System.out.println(new DecimalFormat("'#'0.00").format(d));//#1.57
System.out.println(new DecimalFormat("'^_^'0.00").format(d));//^_^1.57
//使用'本身作为前缀或后缀
System.out.println(new DecimalFormat("''0.00").format(pi));//'1.57
Logo

旨在为数千万中国开发者提供一个无缝且高效的云端环境,以支持学习、使用和贡献开源项目。

更多推荐