JAVA NumberFormat和DecimalFormat小结

JAVA NumberFormat和DecimalFormat小结,第1张

JAVA NumberFormat和DecimalFormat小结

中文互联网上很多介绍这两个类的博客质量真是一眼难尽,遇到什么问题想百度的时候发现就是屎里淘金,非常浪费时间。格式化数字这种不常用但是一定有机会遇到的场景,还是提前做好功课为好。
本篇文章简单说明一下NumberFormat和DecimalFormat这两个类,主要是我对这两个类用法的一些理解。

首先是类的继承关系:

可以看到在JAVA的Format家族中,主要分为3个分支,分别是

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

在上面的界面右键-Show Categories-选择Methods,再右键-Change Visiable Level-选择Public,可以看到类中所有的public方法

可以看到里面的方法相当的多,但是由于我们使用Format类家族的时候,目的都是为了将对象转化为对应的字符串表示形式,或者反过来将字符串转化为对应的对象。听起来有点像序列化和反序列化,不过实际还是差别很大的(序列化是将对象转变成二进制的字节数组,以保存成文件或者通过网络传输,序列化后的文件人类是看不懂的。而format则是将对象变成它的字符串表现形式,这个字符串就是专门设计为让人类可以看懂的)

所以对于Format类家族中的方法,我们主要只需要关心在抽象父类Format中定义的两组重载方法:

  • format()
  • parseObject()

顾名思义,format中文意思是格式化,就是是将某个对象格式化为字符串;parse中文意思是解析,就是将字符串转化为对象。理论上来说,将一个对象a先用format格式化为字符串,再将字符串解析为对象b,对象a和b应该是相等的。读者可以实验一下,下面的代码运行结果是true。

public void test() throws ParseException {
    Format format = new DecimalFormat();
    Object obj = 12.3;
    Object newObj = format.parseObject(format.format(obj));
    System.out.println(newObj.equals(obj));
}

当然,由于parseObject()方法的返回值类型是Object,为了使用上的方便,各个子类都有其对应的parse()方法以返回更具体的对象,比如NumberFormat:

再比如DateFormat:

在format()和parse()两组方法之中,其实以format()方法的使用场景占多数。因为本文讨论的是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是被设计用来格式化所有地区对应数字格式的全能类。简单解释一下:在JAVA中,数字只有那几种表示数字的基本类型和它们对应的包装类(int,Integer,long,Long等),但是这些对象只能存在于JAVA虚拟机中,要让人类看见这些数字,就需要将它们转化成字符串。在不同的地区,表示数字的习惯千差万别,如果我们的代码要对不同地区的人们输出以不同方式表示的数字,就轮到NumberFormat和DecimalFormat出场了,它们之间的区别就在于前者是格式化数字,而后者是格式化十进制数字。
为了让使用者理解不同地区的数字表示方式究竟“有何不同”,在DecimalFormat的类注释中甚至给出了下面一段示例代码:

@Test
public void test() throws ParseException {
    // Print out a number using the localized number, integer, currency,
    // and percent format for each locale
    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.### -> -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
泰文 (泰国,TH): #,##0.### -> -๑,๒๓๔.๕๖ -> -1234.56
印地文 (印度): #,##0.### -> -१,२३४.५६ -> -1234.56

可以看到,在所有的输出中代码都成功地将-1234.56"format"成了对应的字符串表示,然后又"pase"回了-1234.56,但是在不同的国家和地区,数字的字符串表示方式却差异极大:
中国是我们熟悉的用英文逗号做千分位符,英文句号做小数点,但是芬兰和波兰就是用空格作千分位符,瑞内瑞拉则是用英文句号做千分位符,英文逗号做小数点,跟中国正好相反;
同样使用法文的三个国家——瑞士、卢森堡、比利时——它们对同一个数字的表示方式还全都不一样;
更奇葩的是泰国和印度,在泰文和印地文中,甚至不用阿拉伯数字来表示数字;
而在阿拉伯数字的起源地,使用阿拉伯文的阿联酋,他们写负数时,是把负号放在数字后边的……

看了我摘选出来的几个例子,我想读者应该能理解在表现数字的方式上,“世界的参差”了吧。而这,只是case 0:分支,将-1234.56格式化为小数后的字符串表示。还有三个分支分别是转成整数、货币、百分数,我就不贴上来了,有兴趣的自己复制代码执行看一下吧。

可以想象,如果JAVA不给我们提供NumberFormat类和DecimalFormat类,要我们自己实现“针对不同地区的用户,提供对应的个性化的表示数字的字符串输出”这一功能会有多复杂了。

很可惜的一件事是,虽然我前面写了这么多,说明了Format类“地区化输出”以及将“地区化输出”的字符串无损解析成JAVA数字对象的功能有多么强大,但是对于绝大多数开发者来说,其实是很少用到“地区化输出”这一功能的。相反,在我们的日常工作中,实际的需求可能是“保留xx位小数”、“百分数和小数的相互转化”、“将数字转化成包含万分位符的格式”等等。所以尽管NumberFormat可以在无其它配置的情况下将-1234.56正确地格式化为" -1,234.56"字符串,由于默认格式无法满足需求,我们往往需要自定义格式化模板,就是在DecimalFormat的构造方法中传入一个符合预设语法标准的字符串,告诉DecimalFormat要如何格式化数字。

以下是我总结的NumberFormat和DecimalFormat的format()方法自定义模板时的注意要点:

1、Format对象的声明类型什么时候用NumberFormat,什么时候用DecimalFormat
2、如何实例化NumberFormat和DecimalFormat对象
目前Format对象的获取有2种方式:

// 方式1:实例的声明类型为NumberFormat
NumberFormat nf = NumberFormat.getInstance();
// 方式2:实例的声明类型为DecimalFormat,pattern可以为任何符合规则的模板
String pattern = "#.##";
DecimalFormat df = new DecimalFormat();

实际上,由于所有NumberFormat.getInstance()方法拿到的NumberFormat实例的实际类型都是DecimalFormat,所以两种获取实例方式的区别仅在于可自定义程度的大小。使用方式1获取实例,由于声明类型是NumberFormat,所以只能通过几个set方法自定义几个有限的参数,比如整数与小数部分的最大和最小位数、舍入方式、货币类型、是否展示千分位符等;使用方式2获取实例,除了可以自定义上述参数外,还可以实现很多其它的自定义模板,比如科学计数法、千分数、使用万分位符等等。
所以,对于上面两个问题,我的建议是:如果格式化的是货币、百分数等没有太大自定义需求的字符串,可以使用方式1获取声明类型为NumberFormat的实例,否则,就用方式2,获取DecimalFormat实例。

3、模板字符串中几个常用符号的说明
下表在DecimalFormat类的注释里有,我只是对每个符号的含义添加了中文说明

SymbolLocationLocalized?Meaning中文含义0NumberYesDigit数字#NumberYesDigit, zero shows as absent数字,如果是0则不展示.NumberYesDecimal separator or monetary decimal separator数字或者货币的小数位分隔符-NumberYesMinus sign负号,NumberYesGrouping separator英文逗号。分组分隔符(千分位符、万分位符)ENumberYesSeparates mantissa and exponent in scientific notation. Need not be quoted in prefix or suffix.科学计数法中用来分离尾数和指数的符号。不能用在前缀或者后缀中。(科学计数法中a· 1 0 n 10^n 10n记作aEn,其中a叫做底数,n叫做指数);Subpattern boundaryYesSeparates positive and negative subpatterns分隔正数和负数子模式%Prefix or suffixYesMultiply by 100 and show as percentage乘以100并以百分数显示u2030Prefix or suffixYesMultiply by 1000 and show as per mille value乘以1000并以千分数显示¤ (u00A4)Prefix or suffixNoCurrency sign, replaced by currency symbol. If doubled, replaced by international currency symbol. If present in a pattern, the monetary decimal separator is used instead of the decimal separator.货币记号,由货币符号替换。如果两个同时出现,则用国际货币符号替换。如果出现在某个模式中,则使用货币小数分隔符,而不使用小数分隔符’Prefix or suffixNoUsed to quote special characters in a prefix or suffix, for example, “’#’#” formats 123 to “#123”. To create a single quote itself, use two in a row: “# o’'clock”.英文单引号。在特殊字符左右用单引号围起来可以用于标识特殊字符,比如使用模式"’#’#“可以将123格式化为”#123"。如果想要表示单引号本身,则使用两个单引号,如"# o’'clock" 3.1、符号0和#的区别

两者是自定义模板时最常用到的符号,区别在于,在数字前后遇到0时,如果用"0"会强制显示,如果用"#"则会省略。

// 需求:将精度为6位小数的数字截取为保留4位小数
DecimalFormat df1 = new DecimalFormat("#.0000");
DecimalFormat df2 = new DecimalFormat("#.####");
double d1 = 12.345678;
double d2 = 12.340000;
System.out.println("使用"#.0000"模板得到的结果:");
System.out.println(df1.format(d1));
System.out.println(df1.format(d2));
System.out.println("使用"#.####"模板得到的结果:");
System.out.println(df2.format(d1));
System.out.println(df2.format(d2));

输出结果如下:

使用"#.0000"模板得到的结果:
12.3457
12.3400
使用"#.####"模板得到的结果:
12.3457
12.34

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

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

double d1 = 123.4567;
double d2 = -123.4567;
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));

输出结果如下:

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

3.3、百分数符号"%" 与千分数符号"u2030"

由于千分符号"‰"不方便在普通键盘上打出,所以DecimalFormat的设计者使用它的Unicode编码来代替。在使用上,百分数符号和千分数符号没有什么不同

double d1 = 12.34567;
double d2 = -12.34567;
DecimalFormat df1 = new DecimalFormat("0.00%");
System.out.println("使用"0.00%"模板得到的结果:");
System.out.println(df1.format(d1));
System.out.println(df1.format(d2));
DecimalFormat df2 = new DecimalFormat("0.00u2030");
System.out.println("使用"0.00u2030"模板得到的结果:");
System.out.println(df2.format(d1));
System.out.println(df2.format(d2));

输出结果如下:

使用"0.00%"模板得到的结果:
1234.57%
-1234.57%
使用"0.00‰"模板得到的结果:
12345.67‰
-12345.67‰

3.4、自定义千分位符、万分位符

由于中国在读数字时习惯以万为单位分隔大数,所以将数字以万分隔是很常见的需求

double d = 123456789.87654;
DecimalFormat df1 = new DecimalFormat("#,####.#");
DecimalFormat df2 = new DecimalFormat("#,###.#");
System.out.println("使用"#,####.#"模板得到的结果:");
System.out.println(df1.format(d));
System.out.println("使用"#,###.#"模板得到的结果:");
System.out.println(df2.format(d));

输出结果如下:

使用"#,####.#“模板得到的结果:
1,2345,6789.9
使用”#,###.#"模板得到的结果:
123,456,789.9

欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/zaji/4874670.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-11-11
下一篇 2022-11-11

发表评论

登录后才能评论

评论列表(0条)

保存