zoukankan      html  css  js  c++  java
  • 7. JDK拍了拍你:字符串拼接一定记得用MessageFormat#format

    分享、成长,拒绝浅藏辄止。关注公号【BAT的乌托邦】,回复专栏获取原创专栏:重学Spring、重学MyBatis、中间件、云计算...本文已被 https://www.yourbatman.cn 收录。

    ✍前言

    你好,我是A哥(YourBatman)。本文所属专栏:Spring类型转换,公号后台回复专栏名即可获取全部内容。

    在日常开发中,我们经常会有格式化的需求,如日期格式化、数字格式化、钱币格式化等等。

    格式化器的作用似乎跟转换器的作用类似,但是它们的关注点却不一样:

    • 转换器:将类型S转换为类型T,关注的是类型而非格式
    • 格式化器: String <-> Java类型。这么一看它似乎和PropertyEditor类似,但是它的关注点是字符串的格式

    Spring有自己的格式化器抽象org.springframework.format.Formatter,但是谈到格式化器,必然就会联想起来JDK自己的java.text.Format体系。为后文做好铺垫,本文就先介绍下JDK为我们提供了哪些格式化能力。

    版本约定

    • JDK:8

    ✍正文

    Java里从来都缺少不了字符串拼接的活,JDK也提供了多种“工具”供我们使用,如:StringBuffer、StringBuilder以及最直接的+号,相信这些大家都有用过。但这都不是本文的内容,本文将讲解格式化器,给你提供一个新的思路来拼接字符串,并且是推荐方案。

    JDK内置有格式化器,便是java.text.Format体系。它是个抽象类,提供了两个抽象方法:

    public abstract class Format implements Serializable, Cloneable {
        public abstract StringBuffer format(Object obj, StringBuffer toAppendTo, FieldPosition pos);	
    	public abstract Object parseObject (String source, ParsePosition pos);
    }
    
    • format:将Object格式化为String,并将此String放到toAppendTo里面
    • parseObject:讲String转换为Object,是format方法的逆向操作

    Java SE针对于Format抽象类对于常见的应用场景分别提供了三个子类实现:

    DateFormat:日期时间格式化

    抽象类。用于用于格式化日期/时间类型java.util.Date。虽然是抽象类,但它提供了几个静态方法用于获取它的实例:

    // 格式化日期 + 时间
    public final static DateFormat getInstance() {
        return getDateTimeInstance(SHORT, SHORT);
    }
    public final static DateFormat getDateTimeInstance(int dateStyle, int timeStyle, Locale aLocale){
        return get(timeStyle, dateStyle, 3, aLocale);
    }
    
    // 格式化日期
    public final static DateFormat getDateInstance(int style, Locale aLocale) {
        return get(0, style, 2, aLocale);
    }
    // 格式化时间
    public final static DateFormat getTimeInstance(int style, Locale aLocale){
        return get(style, 0, 1, aLocale);
    }
    

    有了这些静态方法,你可在不必关心具体实现的情况下直接使用:

    /**
     * {@link DateFormat}
     */
    @Test
    public void test1() {
        Date curr = new Date();
    
        // 格式化日期 + 时间
        System.out.println(DateFormat.getInstance().getClass() + "-->" + DateFormat.getInstance().format(curr));
        System.out.println(DateFormat.getDateTimeInstance().getClass() + "-->" + DateFormat.getDateTimeInstance().format(curr));
    
        // 格式化日期
        System.out.println(DateFormat.getDateInstance().getClass() + "-->" + DateFormat.getDateInstance().format(curr));
    
        // 格式化时间
        System.out.println(DateFormat.getTimeInstance().getClass() + "-->" + DateFormat.getTimeInstance().format(curr));
    }
    

    运行程序,输出:

    class java.text.SimpleDateFormat-->20-12-25 上午7:19
    class java.text.SimpleDateFormat-->2020-12-25 7:19:30
    class java.text.SimpleDateFormat-->2020-12-25
    class java.text.SimpleDateFormat-->7:19:30
    

    嗯,可以看到底层实现其实是咱们熟悉的SimpleDateFormat。实话说,这种做法不常用,狠一点:基本不会用(框架开发者可能会用做兜底实现)。

    SimpleDateFormat

    一般来说,我们会直接使用SimpleDateFormat来对Date进行格式化,它可以自己指定Pattern,个性化十足。如:

    @Test
    public void test2() {
        DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd"); // yyyy-MM-dd HH:mm:ss
        System.out.println(dateFormat.format(new Date()));
    }
    

    运行程序,输出:

    2020-12-25
    

    关于SimpleDateFormat的使用方式不再啰嗦,不会的就可走自行劝退手续了。此处只提醒一点:SimpleDateFormat线程不安全

    说明:JDK 8以后不再建议使用Date类型,也就不会再使用到DateFormat。同时我个人建议:在项目中可强制严令禁用

    NumberFormat:数字格式化

    抽象类。用于格式化数字,它可以对数字进行任意格式化,如小数、百分数、十进制数等等。它有两个实现类:

    类结构和DateFormat类似,也提供了getXXXInstance静态方法给你直接使用,无需关心底层实现:

    @Test
    public void test41() {
        double myNum = 1220.0455;
    
        System.out.println(NumberFormat.getInstance().getClass() + "-->" + NumberFormat.getInstance().format(myNum));
        System.out.println(NumberFormat.getCurrencyInstance().getClass() + "-->" + NumberFormat.getCurrencyInstance().format(myNum));
        System.out.println(NumberFormat.getIntegerInstance().getClass() + "-->" + NumberFormat.getIntegerInstance().format(myNum));
        System.out.println(NumberFormat.getNumberInstance().getClass() + "-->" + NumberFormat.getNumberInstance().format(myNum));
        System.out.println(NumberFormat.getPercentInstance().getClass() + "-->" + NumberFormat.getPercentInstance().format(myNum));
    }
    

    运行程序,输出:

    class java.text.DecimalFormat-->1,220.045
    class java.text.DecimalFormat-->¥1,220.05
    class java.text.DecimalFormat-->1,220
    class java.text.DecimalFormat-->1,220.045
    class java.text.DecimalFormat-->122,005%
    

    这一看就知道DecimalFormat是NumberFormat的主力了。

    DecimalFormat

    Decimal:小数,小数的,十进位的。

    用于格式化十进制数字。它具有各种特性,可以解析和格式化数字,包括:西方数字、阿拉伯数字和印度数字。它还支持不同种类的数字,包括:整数(123)、小数(123.4)、科学记数法(1.23E4)、百分数(12%)和货币金额($123)。所有这些都可以进行本地化。

    下面是它的构造器:

    其中最为重要的就是这个pattern(不带参数的构造器一般不会用),它表示格式化的模式/模版。一般来说我们对DateFormat的pattern比较熟悉,但对数字格式化的模版符号了解甚少。这里我就帮你整理出这个表格(信息源自JDK官网),记得搜藏哦:

    符号 Localtion 是否本地化 释义
    0 Number Digit
    # Number Digit。若是0就显示为空
    . Number 小数/货币分隔符
    - Number 就代表减号
    , Number 分组分隔符
    E Number 科学计数法分隔符(位数和指数)
    % 前/后缀 乘以100并显示为百分数
    ¤ 前/后缀 货币记号。若连续出现两次就用国际货币符号代替
    ' 前后缀 用于引用特殊字符。作用类似于转义字符

    说明:Number和Digit的区别:

    • Number是个抽象概念,其表达形式可以是数字、手势、声音等等。如1024就是个number
    • Digit是用来表达的单独符号。如0-9这是个digit就可以用来表示number,如1024就是由1、0、2、4这四个digit组成的

    看了这个表格的符号规则,估计很多同学还是一脸懵逼。不啰嗦了,上干货

    一、0和#的使用(最常见使用场景)

    这是最经典、最常见的使用场景,甚至来说你有可能职业生涯会用到此场景。

    /**
     * {@link DecimalFormat}
     */
    @Test
    public void test4() {
        double myNum = 1220.0455;
    
        System.out.println("===============0的使用===============");
        System.out.println("只保留整数部分:" + new DecimalFormat("0").format(myNum));
        System.out.println("保留3位小数:" + new DecimalFormat("0.000").format(myNum));
        System.out.println("整数部分、小数部分都5位。不够的都用0补位(整数高位部,小数低位补):" + new DecimalFormat("00000.00000").format(myNum));
    
        System.out.println("===============#的使用===============");
        System.out.println("只保留整数部分:" + new DecimalFormat("#").format(myNum));
        System.out.println("保留2为小数并以百分比输出:" + new DecimalFormat("#.##%").format(myNum));
    
        // 非标准数字(不建议这么用)
        System.out.println("===============非标准数字的使用===============");
        System.out.println(new DecimalFormat("666").format(myNum));
        System.out.println(new DecimalFormat(".6666").format(myNum));
    }
    

    运行程序,输出:

    ===============0的使用===============
    只保留整数部分:1220
    保留3位小数:1220.045
    整数部分、小数部分都5位。不够的都用0补位(整数高位部,小数低位补):01220.04550
    ===============#的使用===============
    只保留整数部分:1220
    保留2为小数并以百分比输出:122004.55%
    ===============非标准数字的使用===============
    661220
    1220.666
    

    通过此案例,大致可得出如下结论:

    • 整数部分:
      • 0和#都可用于取出全部整数部分
      • 0的个数决定整数部分长度,不够高位补0;#则无此约束,N多个#是一样的效果
    • 小数部分:
      • 可保留小数点后N位(0和#效果一样)
      • 若小数点后位数不够,若使用的0那就低位补0,若使用#就不补(该是几位就是几位)
    • 数字(1-9):并不建议模版里直接写1-9这样的数字,了解下即可

    二、科学计数法E

    如果你不是在证券/银行行业,这个大概率是用不着的(即使在,你估计也不会用它)。来几个例子感受一把就成:

    @Test
    public void test5() {
        double myNum = 1220.0455;
    
        System.out.println(new DecimalFormat("0E0").format(myNum));
        System.out.println(new DecimalFormat("0E00").format(myNum));
        System.out.println(new DecimalFormat("00000E00000").format(myNum));
        System.out.println(new DecimalFormat("#E0").format(myNum));
        System.out.println(new DecimalFormat("#E00").format(myNum));
        System.out.println(new DecimalFormat("#####E00000").format(myNum));
    }
    

    运行程序,输出:

    1E3
    1E03
    12200E-00001
    .1E4
    .1E04
    1220E00000
    

    三、分组分隔符,

    分组分隔符比较常用,它就是我们常看到的逗号,

    @Test
    public void test6() {
        double myNum = 1220.0455;
    
        System.out.println(new DecimalFormat(",###").format(myNum));
        System.out.println(new DecimalFormat(",##").format(myNum));
        System.out.println(new DecimalFormat(",##").format(123456789));
    
        // 分隔符,左边是无效的
        System.out.println(new DecimalFormat("###,##").format(myNum));
    }
    

    运行程序,输出:

    1,220
    12,20
    1,23,45,67,89
    12,20
    

    四、百分号%

    在展示层面也比较常用,用于把一个数字用%形式表示出来。

    @Test
    public void test42() {
        double myNum = 1220.0455;
    
        System.out.println("百分位表示:" + new DecimalFormat("#.##%").format(myNum));
        System.out.println("千分位表示:" + new DecimalFormat("#.##u2030").format(myNum));
    }
    

    运行程序,输出:

    百分位表示:122004.55%
    千分位表示:1220045.5‰
    

    五、本地货币符号¤

    嗯,这个符号¤,键盘竟无法直接输出,得使用软键盘(建议使用copy大法)。

    @Test
    public void test7() {
        double myNum = 1220.0455;
    
        System.out.println(new DecimalFormat(",000.00¤").format(myNum));
        System.out.println(new DecimalFormat(",000.¤00").format(myNum));
        System.out.println(new DecimalFormat("¤,000.00").format(myNum));
        System.out.println(new DecimalFormat("¤,000.¤00").format(myNum));
        // 世界货币表达形式
        System.out.println(new DecimalFormat(",000.00¤¤").format(myNum));
    }
    

    运行程序,输出:

    1,220.05¥
    1,220.05¥
    ¥1,220.05
    1,220.05¥¥
    ¥1,220.05¥
    1,220.05CNY
    

    注意最后一条结果:如果连续出现两次,代表货币符号的国际代号。

    说明:结果默认都做了Locale本地化处理的,若你在其它国家就不会再是¥人名币符号喽

    DecimalFormat就先介绍到这了,其实掌握了它就基本等于掌握了NumberFormat。接下来再简要看看它另外一个“儿子”:ChoiceFormat。

    ChoiceFormat

    Choice:精选的,仔细推敲的。

    这个格式化器非常有意思:相当于以数字为键,字符串为值的键值对。使用一组double类型的数组作为键,一组String类型的数组作为值,两数组相同(不一定必须是相同,见示例)索引值的元素作为一对。

    @Test
    public void test8() {
        double[] limits = {1, 2, 3, 4, 5, 6, 7};
        String[] formats = {"周一", "周二", "周三", "周四", "周五", "周六", "周天"};
        NumberFormat numberFormat = new ChoiceFormat(limits, formats);
    
        System.out.println(numberFormat.format(1));
        System.out.println(numberFormat.format(4.3));
        System.out.println(numberFormat.format(5.8));
        System.out.println(numberFormat.format(9.1));
        System.out.println(numberFormat.format(11));
    }
    

    运行程序,输出:

    周一
    周四
    周五
    周天
    周天
    

    结果解释:

    1. 4.3位于4和5之间,取值4;5.8位于5和6之间,取值5
    2. 9.1和11均超过了数组最大值(或者说找不到匹配的),则取值最后一对键值对

    可能你会想这有什么使用场景???是的,不得不承认它的使用场景较少,本文下面会介绍下它和MessageFormat的一个使用场景。

    如果说DateFormatNumberFormat都用没什么花样,主要记住它的pattern语法格式就成,那么就下来这个格式化器就是本文的主菜了,使用场景非常的广泛,它就是MessageFormat

    MessageFormat:字符串格式化

    MessageFormat提供了一种与语言无关(不管你在中国还是其它国家,效果一样)的方式生成拼接消息/拼接字符串的方法。使用它来构造显示给最终用户的消息。MessageFormat接受一组对象,对它们进行格式化,然后在模式的适当位置插入格式化的字符串。

    先来个最简单的使用示例体验一把:

    /**
     * {@link MessageFormat}
     */
    @Test
    public void test9() {
        String sourceStrPattern = "Hello {0},my name is {1}";
        Object[] args = new Object[]{"girl", "YourBatman"};
    
        String formatedStr = MessageFormat.format(sourceStrPattern, args);
        System.out.println(formatedStr);
    }
    

    运行程序,输出:

    Hello girl,my name is YourBatman
    

    有没有中似曾相似的感觉,是不是和String.format()的作用特别像?是的,它俩的用法区别,到底使用税文下也会讨论。

    要熟悉MessageFormat的使用,主要是要熟悉它的参数模式(你也可以理解为pattern)。

    参数模式

    MessageFormat采用{}来标记需要被替换/插入的部分,其中{}里面的参数结构具有一定模式:

    ArgumentIndex[,FormatType[,FormatStyle]] 
    
    • ArgumentIndex非必须。从0开始的索引值
    • FormatType非必须。使用不同的java.text.Format实现类对入参进行格式化处理。它能有如下值:
      • number:调用NumberFormat进行格式化
      • date:调用DateFormat进行格式化
      • time:调用DateFormat进行格式化
      • choice:调用ChoiceFormat进行格式化
    • FormatStyle非必须。设置FormatType使用的样式。它能有如下值:
      • short、medium、long、full、integer、currency、percent、SubformPattern(如日期格式、数字格式#.##等)

    说明:FormatType和FormatStyle只有在传入值为日期时间、数字、百分比等类型时才有可能需要设置,使用得并不多。毕竟:我在外部格式化好后再放进去不香吗?

    @Test
    public void test10() {
        MessageFormat messageFormat = new MessageFormat("Hello, my name is {0}. I’am {1,number,#.##} years old. Today is {2,date,yyyy-MM-dd HH:mm:ss}");
        // 亦可通过编程式 显示指定某个位置要使用的格式化器
        // messageFormat.setFormatByArgumentIndex(1, new DecimalFormat("#.###"));
    
        System.out.println(messageFormat.format(new Object[]{"YourBatman", 24.123456, new Date()}));
    }
    

    运行程序,输出:

    Hello, my name is YourBatman. I’am 24.12 years old. Today is 2020-12-26 15:24:28
    

    它既可以直接在模版里指定格式化模式类型,也可以通过API方法set指定格式化器,当然你也可以再外部格式化好后再放进去,三种方式均可,任君选择。

    注意事项

    下面基于此示例,对MessageFormat的使用注意事项作出几点强调。

    @Test
    public void test11() {
        System.out.println(MessageFormat.format("{1} - {1}", new Object[]{1})); // {1} - {1}
        System.out.println(MessageFormat.format("{0} - {1}", new Object[]{1})); // 输出:1 - {1}
        System.out.println(MessageFormat.format("{0} - {1}", new Object[]{1, 2, 3})); // 输出:1 - 2
    
        System.out.println("---------------------------------");
    
        System.out.println(MessageFormat.format("'{0} - {1}", new Object[]{1, 2})); // 输出:{0} - {1}
        System.out.println(MessageFormat.format("''{0} - {1}", new Object[]{1, 2})); // 输出:'1 - 2
        System.out.println(MessageFormat.format("'{0}' - {1}", new Object[]{1, 2})); // {0} - 2
        // 若你数据库值两边都需要''包起来,请你这么写
        System.out.println(MessageFormat.format("''{0}'' - {1}", new Object[]{1, 2})); // '1' - 2
    
        System.out.println("---------------------------------");
        System.out.println(MessageFormat.format("0} - {1}", new Object[]{1, 2})); // 0} - 2
        System.out.println(MessageFormat.format("{0 - {1}", new Object[]{1, 2})); // java.lang.IllegalArgumentException: Unmatched braces in the pattern.
    }
    
    1. 参数模式的索引值必须从0开始,否则所有索引值无效
    2. 实际传入的参数个数可以和索引个数不匹配,不报错(能匹配上几个算几个)
    3. 两个单引号''才算作一个',若只写一个将被忽略甚至影响整个表达式
      1. 谨慎使用单引号'
      2. 关注'的匹配关系
    4. {}只写左边报错,只写右边正常输出(注意参数的对应关系)

    static方法的性能问题

    我们知道MessageFormat提供有一个static静态方法,非常方便的的使用:

    public static String format(String pattern, Object ... arguments) {
        MessageFormat temp = new MessageFormat(pattern);
        return temp.format(arguments);
    }
    

    可以清晰看到,该静态方法本质上还是构造了一个MessageFormat实例去做格式化的。因此:若你要多次(如高并发场景)格式化同一个模版(参数可不一样)的话,那么提前创建好一个全局的(非static) MessageFormat实例再执行格式化是最好的,而非一直调用其静态方法。

    说明:若你的系统非高并发场景,此性能损耗基本无需考虑哈,怎么方便怎么来。毕竟朝生夕死的对象对JVM来说没啥压力

    和String.format选谁?

    二者都能用于字符串拼接(格式化)上,撇开MessageFormat支持各种模式不说,我们只需要考虑它俩的性能上差异。

    • MeesageFormat:先分析(模版可提前分析,且可以只分析一次),再在指定位置上插入相应的值
      • 分析:遍历字符串,维护一个{}数组并记录位置
      • 填值
    • String.format:该静态方法是采用运行时用正则表达式 匹配到占位符,然后执行替换的
      • 正则表达式为"%(\d+\$)?([-#+ 0,(\<]*)?(\d+)?(\.\d+)?([tT])?([a-zA-Z%])"
      • 根据正则匹配到占位符列表和位置,然后填值

    一说到正则表达式,我心里就发触,因为它对性能是不友好的,所以孰优孰劣,高下立判。

    说明:还是那句话,没有绝对的谁好谁坏,如果你的系统对性能不敏感,那就是方便第一

    经典使用场景

    这个就很多啦,最常见的有:HTML拼接、SQL拼接、异常信息拼接等等。

    比如下面这个SQL拼接:

    StringBuilder sb =new StringBuilder();
    sb.append("insert into user (");
    sb.append("		name,");
    sb.append("		accountId,");
    sb.append("		zhName,");
    sb.append("		enname,");
    sb.append("		status");
    sb.append(") values (");
    sb.append("		''{0}'',");
    sb.append("		{1},");
    sb.append("		''{2}'',");
    sb.append("		''{3}'',");
    sb.append("		{4},");
    sb.append(")");
    
    Object[] args = {name, accountId, zhName, enname, status};
    
    // 最终SQL
    String sql = MessageFormat.format(sb.toString(), arr);
    

    你看,多工整。

    说明:如果值是字符串需要'包起来,那么请使用两边各两个包起来

    ✍总结

    本文内容介绍了JDK原生的格式化器知识点,主要作用在这三个方面:

    • DateFormat:日期时间格式化
    • NumberFormat:数字格式化
    • MessageFormat:字符串格式化

    Spring是直接面向使用者的框架产品,很显然这些是不够用的,并且JDK的格式化器在设计上存在一些弊端。比如经常被吐槽的:日期/时间类型格式化器SimpleDateFormat为毛在java.text包里,而它格式化的类型Date却在java.util包内,这实为不合适。

    有了JDK格式化器作为基础,下篇我们就可以浩浩荡荡的走进Spring格式化器的大门了,看看它是如何优于JDK进行设计和抽象的。


    ✔✔✔推荐阅读✔✔✔

    【Spring类型转换】系列:

    【Jackson】系列:

    【数据校验Bean Validation】系列:

    【新特性】系列:

    【程序人生】系列:

    还有诸如【Spring配置类】【Spring-static关键字】【Spring数据绑定】【Spring Cloud Netflix】【Feign】【Ribbon】【Hystrix】...更多原创专栏,关注BAT的乌托邦回复专栏二字即可全部获取,也可加我fsx1056342982,交个朋友。

    有些已完结,有些连载中。我是A哥(YourBatman),咱们下期见

  • 相关阅读:
    多表联合查询,利用 concat 模糊搜索
    order by 中利用 case when 排序
    Quartz.NET 3.0.7 + MySql 动态调度作业+动态切换版本+多作业引用同一程序集不同版本+持久化+集群(一)
    ASP.NET Core 2.2 基础知识(十八) 托管和部署 概述
    ASP.NET Core 2.2 基础知识(十七) SignalR 一个极其简陋的聊天室
    ASP.NET Core 2.2 基础知识(十六) SignalR 概述
    ASP.NET Core 2.2 基础知识(十五) Swagger
    ASP.NET Core 2.2 基础知识(十四) WebAPI Action返回类型(未完待续)
    linux磁盘管理 磁盘查看操作
    linux磁盘管理 文件挂载
  • 原文地址:https://www.cnblogs.com/yourbatman/p/14208578.html
Copyright © 2011-2022 走看看