5.2.2 字符串的格式化
字符串变量的4种赋值方式对于简单的赋值来说完全够用了,即便是两个字符串拼接,也只需通过加号把两个目标串连起来即可。但对于复杂的赋值来说就麻烦了,假设现在需要拼接一个很长的字符串,字符串内部包含各种类型的变量,有整型、双精度型、布尔型、字符型,中间还夹杂着一些起黏合作用的子串,如此一来只能使劲地填写加号,把各种变量努力加上去,就像有时打印日志调用System.out.println一样,加号多到让你眼花缭乱。
为了不让加号如此横行霸道,String类型从Java 5开始额外提供了format方法用来格式化这些准备填入字符串的各种变量。具体地说,是在一个模板字符串中填写类似“%s”“%d”“%f”这样的记号先占几个位置,然后给format方法的输入参数分别指定对应位置的变量名称,表示这些变量值依次替换模板中的“%s”“%d”“%f”等记号。以上模板串用到的占位记号也叫作格式转换符,详细说明见表5-1。
表5-1 字符串模板的格式转换符
下面是利用format方法格式化单个变量值与多个变量值的代码例子(完整代码见本章源码的src\com\string\string\StrFormat.java):
// 往字符串中填入另一个字符串 String fromString=String.format("格式化子串的字符串:%s", "Hello"); System.out.println("fromString="+fromString); // 往字符串中填入字符 String fromChar=String.format("格式化字符的字符串:%c", 'A'); System.out.println("fromChar="+fromChar); // 往字符串中填入布尔值 String fromBoolean=String.format("格式化布尔值的字符串:%b", false); System.out.println("fromBoolean="+fromBoolean); // 往字符串中填入十进制整数 String fromInt=String.format("格式化整型数的字符串:%d", 255); System.out.println("fromInt="+fromInt); // 往字符串中填入十六进制数 String fromOct=String.format("格式化十六进制数的字符串:%o", 255); System.out.println("fromOct="+fromOct); // 往字符串中填入八进制数 String fromHex=String.format("格式化八进制数的字符串:%x", 255); System.out.println("fromHex="+fromHex); // 往字符串中填入浮点数 String fromFloat=String.format("格式化浮点数的字符串:%f", 3.14); System.out.println("fromFloat="+fromFloat); // 格式化字符串的时候,同时填充多个变量 String manyVariable=String.format("以下字符串包括多个变量值:%s,%c,%b,%d, %o,%x,%f", "Hello", 'A', false, 255, 255, 255, 3.14); System.out.println("manyVariable="+manyVariable);
观察上面的代码,可见大部分的基本类型都支持格式化,除了双精度类型外。如果双精度数的精度刚好在浮点数范围之内,能够借助标记%f来格式化,如果双精度数超过了浮点数的精度,还能使用%f格式化吗?接下来通过以下测试代码看看3.1415926这个双精度数会被%f格式化成什么样子:
// 注意,若是通过%f格式化双精度数,则会强制转换成浮点数 String fromDouble=String.format("双精度数格式化后丢失精度的字符串:%f", 3.1415926); System.out.println("fromDouble="+fromDouble);
运行以上的测试代码,打印的日志结果如下:
fromDouble=双精度数格式化后丢失精度的字符串:3.141593
可见使用%f格式化双精度数时,超出范围的小数部分被强行四舍五入了,因而%f并不适合用于直接格式化双精度数。若想让双精度数在格式化时不损失精度,需要事先指定小数点后面保留的位数,比如%.8f表示格式化时保留8位小数部分,f前面的数字越大代表保留的位数越多,双精度数的数值精度就越高。利用%.8f改写之前的双精度数格式化代码,改写后的演示代码如下:
// 格式化双精度数时,需要指定小数点后面保留的位数 String fromDecimal=String.format("格式化双精度数的字符串:%.8f", 3.1415926); System.out.println("fromDecimal="+fromDecimal);
运行以上的演示代码,程序运行结果如下:
fromDecimal=格式化双精度数的字符串:3.14159260
从日志信息可见,此时双精度数的小数部分得以完整地保存下来。
所谓格式化,不单单是按照标记填写具体数值,还要求字符串格式整齐划一。譬如统计世界各国人口,列表中的各国人口数值应当右对齐,这样谁多谁少方能一目了然。既然要求支持对齐,那么得先明确该列数字的最大位数,之后才能在位数范围内选择左对齐还是右对齐。整数部分最大位数的标记方式与小数部分的保留位数类似,唯一的区别是整数位数的标记不加点号,而小数位数的标记要加点号,例如%8d表示待格式化的整数将占据8个字符空间,并且默认右对齐、左补空格。倘若要求左对齐,则格式化标记需添加横线符号,像%-8d表示待格式化的整数在8位空间内左对齐,并且右补空格。有时候数字代表一串编码,即使未达到最大位数也得在左边补0,此时格式化标记要在位数前面补充0,代表空出来的位置填0而不是填空格,如标记%08d表示待格式化的整数要求右对齐、左补0。
下面是各种格式化整数位数的代码例子:
// 对整数分配固定长度,该整数默认右对齐、左补空格 String fromLenth=String.format("格式化固定长度(默认右对齐)的整数字符串:(%8d)", 255); System.out.println("fromLenth="+fromLenth); // 对整数分配固定长度,且该整数左对齐、右补空格 String fromLeft=String.format("格式化固定长度且左对齐的整数字符串:(%-8d)", 255); System.out.println("fromLeft="+fromLeft); // 对整数分配固定长度,该整数默认右对齐、左补0 String fromZero=String.format("格式化固定长度且左补0的整数字符串:(%08d)", 255); System.out.println("fromZero="+fromZero);
运行上述的格式化代码,得到以下日志打印结果:
fromLenth=格式化固定长度(默认右对齐)的整数字符串:( 255) fromLeft=格式化固定长度且左对齐的整数字符串:(255 ) fromZero=格式化固定长度且左补0的整数字符串:(00000255)
由此可见,格式化后的数字既实现了右对齐,又实现了左对齐,还支持在空位补0。
一旦格式化用得多了,便会出现某个变量需要多次填入的情况,比如“重要的事情说3遍”,后面的句子就得输入3次,像以下代码一样,“别迟到”3个字反复写了3次:
// 字符串格式化的时候,可能出现某个变量被多次填入的情况 String fromRepeat1=String.format("重要的事情说3遍:%s,%s,%s", "别迟到", "别迟到", "别迟到"); System.out.println("fromRepeat1="+fromRepeat1);
这种做法无疑非常拖沓,不但写起来费劲,看起来也费神。为此格式化又设计了形如“%n$s”的标记,其中n表示当前标记取的是第几个参数值,尾巴的s就是普通的格式化标记,中间的美元符号$把两者隔开。例如,标记%1$s表示当前要取第一个参数,且该参数类型为字符串,于是前述的“重要的事情说3遍”即可简化为以下代码:
// 重复填入某个变量值,可利用“%数字$”的形式,其中“数字$”表示这里取后面的第几个变量值 String fromRepeat2=String.format("重要的事情说三遍:%1$s,%1$s,%1$s", "别迟到"); System.out.println("fromRepeat2="+fromRepeat2);
现在有一个比较常见的业务要求,金额数字通常都要保留小数点后面两位,像余额宝的每日收益就精确到小数点后两位的单位“分”。此时采取标记%.2f即可实现要求,但是余额宝内部对账可不能仅仅保留两位小数,一般至少保留小数点后3位的单位“厘”,那么对账用的格式化标记就变成了%.3f。这样有的场合要求更高精度,有的场合对精度的要求不高,意味着标记%.nf中间的n值是随时变化着的。若要处理变化的输入数值,则必须通过方法实现相关功能,也就是需要设计一个新方法,该方法的输入参数包括待格式化的数字和需要保留的小数位数,方法的返回值为截取指定小数位的字符串。
对于双精度数字来说,此时要先根据小数位数构建一个形如%.nf的格式化标记串,再依据该标记格式化最终的数值字符串。由于百分号“%”是格式化的保留字符,因此要用两个百分号“%%”来表达一个百分符号“%”,于是双精度数的小数位数格式化代码如下:
// 对双精度类型的变量截取小数位,多余部分的数字默认四舍五入 public static String formatWithDouble(double value, int digit) { // 先根据小数位数构建格式化标记串。两个百分号“%%”可转义为一个百分符号“%” String format=String.format("%%.%df", digit); // 再依据该标记将具体数字格式化为字符串 String result=String.format(format, value); return result; }
对于大小数类型而言,BigDecimal提供了专门的setScale方法,该方法不但允许指定截取的小数位,还支持设置特定的舍入规则,当然通常情况下使用RoundingMode.HALF_UP代表四舍五入即可。下面便是截取大小数的例子:
// 对大小数类型的变量截取小数位,可指定多余部分数字的舍入规则 public static String formatWithBigDecimal(BigDecimal value, int digit) { // 大小数类型的setScale方法需要指定明确的舍入规则,其中HALF_UP表示四舍五入 BigDecimal result=value.setScale(digit, RoundingMode.HALF_UP); return result.toString(); }
接下来,外部分别调用上面的双精度数格式化方法formatWithDouble和大小数格式化方法formatWithBigDecimal,具体的测试调用代码如下:
double normalDecimal=19.895; // 保留双精度数的小数点后面两位 String normalResult=formatWithDouble(normalDecimal, 2); System.out.println("normalResult="+normalResult); BigDecimal bigDecimal=new BigDecimal("123456789012345678.901"); // 保留大小数的小数点后面两位 String bigResult=formatWithBigDecimal(bigDecimal, 2); System.out.println("bigResult="+bigResult);
运行上述的精度格式化代码,输出以下的日志打印信息:
normalResult=19.90 bigResult=123456789012345678.90
可见无论是双精度格式化,还是大小数格式化,都实现了四舍五入保留两位小数的目标。