Java 字符串格式化详解

版权声明:本文为博主原创文章,未经博主允许不得转载。
微博:厉圣杰
文中如有纰漏,欢迎大家留言指出。

在 Java 的 String 类中,可以使用 format() 方法格式化字符串,该方法有两种重载形式: String.format(String format, Object... args)String.format(Locale locale, String format, Object... args)。两者的唯一区别是前者使用本地语言环境,后者使用指定语言环境
查看源码可以发现,该方法最终调用 java.util.Formatter 类的 format 方法。

public static String format(String format, Object... args) {
   return new Formatter().format(format, args).toString();
}

public static String format(Locale l, String format, Object... args) {
   return new Formatter(l).format(format, args).toString();
}

所以,掌握了 Formatter 的使用,也就掌握了 String.format 的使用,从此 Java 中格式化字符串再无敌手~ 所以我们这里先来讲解 Formatter 的用法。

参考 Java Api 中关于 Formatter 的使用说明,我们可以发现 format 方法的第一个参数是有固定格式的。其格式如下:

%[argument_index$][flags][width][.precision]conversion

argument_index: 可选,是一个十进制整数,用于表明参数在参数列表中的位置。第一个参数由 "1$" 引用,第二个参数由 "2$" 引用,依此类推。

flags: 可选,用来控制输出格式

width: 可选,是一个正整数,表示输出的最小长度

precision:可选,用来限定输出的精度

conversion:必须,用来表示如何格式化参数的字符

参考文档,可以发现 Java 其实把格式化划分为两大类:常规类型格式化和时间日期格式化,下面我们就先来介绍一下常规类型的格式化。

补充:Java 中对格式化其实还有根据类型来分类的,但这里为了方便讲述,只简单的依据格式化的参数类型来讲述,如果以后有机会,会开一篇更详细的博客。

常规类型格式化

在开始之前,这里先放一段 Api 中提供的示例代码,你可以带着示例中的问题去看接下来的内容,也可以看完之后回来看示例,你看懂了多少呢?

StringBuilder sb = new StringBuilder();
// Send all output to the Appendable object sb
Formatter formatter = new Formatter(sb, Locale.US);

// Explicit argument indices may be used to re-order output.
formatter.format("%4$2s %3$2s %2$2s %1$2s", "a", "b", "c", "d")
// -> " d  c  b  a"

// Optional locale as the first argument can be used to get
// locale-specific formatting of numbers.  The precision and width can be
// given to round and align the value.
formatter.format(Locale.FRANCE, "e = %+10.4f", Math.E);
// -> "e =    +2,7183"

// The '(' numeric flag may be used to format negative numbers with
// parentheses rather than a minus sign.  Group separators are
// automatically inserted.
formatter.format("Amount gained or lost since last statement: $ %(,.2f",
               balanceDelta);
// -> "Amount gained or lost since last statement: $ (6,217.58)"

// Writes a formatted string to System.out.
System.out.format("Local time: %tT", Calendar.getInstance());
// -> "Local time: 13:34:18"

// Writes formatted output to System.err.
System.err.printf("Unable to open file '%1$s': %2$s",
                fileName, exception.getMessage());
// -> "Unable to open file 'food': No such file or directory"

Calendar c = new GregorianCalendar(1995, MAY, 23);
String s = String.format("Duke's Birthday: %1$tb %1$te, %1$tY", c);
// -> s == "Duke's Birthday: May 23, 1995"

conversion

从上述内容可以发现,只有 conversion 这个参数是必选的。 conversion 是用来表示如何格式化参数的字符。先来看个例子:

//输出:我的名字叫:小明
System.out.println(String.format("大家好,我叫:%s","小明"));

%s 是一个占位符,s 是一个转换符,指明将参数格式化为字符串。值得注意的是,占位符代表的格式化类型必须与参数的类型相兼容,否则运行时会抛出异常,如:

System.out.println(String.format("大家好,我叫:%d","小明"));

运行这段代码,就会抛出如下异常:

Exception in thread "main" java.util.IllegalFormatConversionException: d != java.lang.String
   at java.util.Formatter$FormatSpecifier.failConversion(Formatter.java:4302)
   at java.util.Formatter$FormatSpecifier.printInteger(Formatter.java:2793)
   at java.util.Formatter$FormatSpecifier.print(Formatter.java:2747)
   at java.util.Formatter.format(Formatter.java:2520)
   at java.util.Formatter.format(Formatter.java:2455)
   at java.lang.String.format(String.java:2940)

那么,转换符除了 s ,还有哪些呢?那我们就来看下代码:

/**
* conversion 的具体占位符
*/
private static void formatConversion() {
  System.out.println(String.format("'b':将参数格式化为boolean类型输出,`B`的效果相同,但结果中字母为大写。%b", false));
  System.out.println(String.format("'h':将参数格式化为散列输出,原理:Integer.toHexString(arg.hashCode()),`H`的效果相同,但结果中字母为大写。%h", "ABC"));
  System.out.println(String.format("'s':将参数格式化为字符串输出,如果参数实现了 Formattable接口,则调用 formatTo方法。`S`的效果相同。%s", 16));
  System.out.println(String.format("FormatImpl类实现了Formattable接口:%s", new FormatImpl()));
  System.out.println(String.format("'c':将参数格式化为Unicode字符,'C'的效果相同。%c", 'A'));
  System.out.println(String.format("'d':将参数格式化为十进制整数。%d", 11));
  System.out.println(String.format("'o':将参数格式化为八进制整数。%o", 9));
  System.out.println(String.format("'x':将参数格式化为十六进制整数。%x", 17));
  System.out.println(String.format("'e':将参数格式化为科学计数法的浮点数,'E'的效果相同。%E", 10.000001));
  System.out.println(String.format("'f':将参数格式化为十进制浮点数。%f", 10.000001));
  System.out.println(String.format("'g':根据具体情况,自动选择用普通表示方式还是科学计数法方式,'G'效果相同。10.01=%g", 10.01));
  System.out.println(String.format("'g':根据具体情况,自动选择用普通表示方式还是科学计数法方式,'G'效果相同。10.00000000005=%g", 10.00000000005));
  System.out.println(String.format("'a':结果被格式化为带有效位数和指数的十六进制浮点数,'A'效果相同,但结果中字母为大写。%a", 10.1));
  System.out.println(String.format("'t':时间日期格式化前缀,会在后面讲述"));
  System.out.println(String.format("'%%':输出%%。%%"));
  System.out.println(String.format("'n'平台独立的行分隔符。System.getProperty(\"line.separator\")可以取得平台独立的行分隔符,但是用在format中间未免显得过于烦琐了%n已经换行"));
}
   
private static class FormatImpl implements Formattable {
   
  @Override
  public void formatTo(Formatter formatter, int flags, int width, int precision) {
      formatter.format("我是Formattable接口的实现类");
  }
}

输出如下:

'b':将参数格式化为boolean类型输出,`B`的效果相同,但结果中字母为大写。false
'h':将参数格式化为散列输出,原理:Integer.toHexString(arg.hashCode()),`H`的效果相同,但结果中字母为大写。fc42
's':将参数格式化为字符串输出,如果参数实现了 Formattable接口,则调用 formatTo方法。`S`的效果相同。16
FormatImpl类实现了Formattable接口:我是Formattable接口的实现类
'c':将参数格式化为Unicode字符,'C'的效果相同。A
'd':将参数格式化为十进制整数。11
'o':将参数格式化为八进制整数。11
'x':将参数格式化为十六进制整数。11
'e':将参数格式化为科学计数法的浮点数,'E'的效果相同。1.000000E+01
'f':将参数格式化为十进制浮点数。10.000001
'g':根据具体情况,自动选择用普通表示方式还是科学计数法方式,'G'效果相同。10.01=10.0100
'g':根据具体情况,自动选择用普通表示方式还是科学计数法方式,'G'效果相同。10.00000000005=10.0000
'a':结果被格式化为带有效位数和指数的十六进制浮点数,'A'效果相同,但结果中字母为大写。0x1.4333333333333p3
't':时间日期格式化前缀,会在后面讲述
'%':输出%。%
'n':平台独立的行分隔符。System.getProperty("line.separator")可以取得平台独立的行分隔符,但是用在format中间未免显得过于烦琐了
已经换行

补充

  • 对于浮点转换符 'e' 、'E' 和 'f',精度是小数点分隔符后的位数。如果转换符是 'g' 或 'G',那么精度是舍入计算后所得数值的所有位数。如果转换是 'a' 或 'A',则不必指定精度。
  • 对于部分转换符,如 'b' 和 'B' ,两者转换效果是相似的,但是 'B' 会把输出中的字母都转换为大写,其它相似特性的转换符应该还有好多,大家可以去找一下O(∩_∩)O哈哈~

argument_index

还记得前面那个小明的例子嘛?现在我们来改变一下输出,要求输出如下内容:

//其中,小明、25、小小明都是由参数传递
大家好,我叫:小明,今年:25岁。小明是小小明的爸爸。

看到这个,大家会怎么做呢?也许,你会写成:

System.out.println(String.format("大家好,我叫:%s,今年:%d岁。%s是%s的爸爸。", "小明", 25, "小明", "小小明"));

恩,这样做输出的确没错,但是我们却重复输入参数“小明”。这里,就要用到 argument_index 这个参数。使用 argument_index 可以指定使用第几个参数来替换占位符,一旦使用 argument_index 用于指出参数在参数列表中位置,则所有占位符都要加上,否则会出错。修改之后的代码如下:

System.out.println(String.format("大家好,我叫:%1$s,今年:%2$d岁。%1$s是%3$s的爸爸。", "小明", 25, "小小明"));

补充:对于

System.out.println(String.format("大家好,我叫:%s,今年:%d岁。%s是%s的爸爸。", "小明", 25, "小明", "小小明"));

在 Java 中执行可能没什么问题,但如果把 "大家好,我叫:%s,今年:%d岁。%s是%s的爸爸。" 放到 Android 的 strings.xml 中,则会出现错误,解决办法就是指明每个参数在参数列表中位置。

flags

flags是可选参数,用于控制输出的格式,比如左对齐、金额用逗号隔开。

'-' 在最小宽度内左对齐,不可以与“用0填充”同时使用

'+' 结果总是包括一个符号

' ' 正值前加空格,负值前加负号

'0' 结果将用零来填充

',' 每3位数字之间用“,”分隔(只适用于fgG的转换)

'(' 若参数是负数,则结果中不添加负号而是用圆括号把数字括起来(只适用于eEfgG的转换)
/**
* flags 的标识
*/
private static void formatFlags() {
   System.out.println("'-':在最小宽度内左对齐,不可与\"用0填充\"同时使用。");
   System.out.println(String.format("设置最小宽度为8为,左对齐。%-8d:%-8d:%-8d%n", 1, 22, 99999999));
   System.out.println(String.format("'0':结果将用零来填充。设置最小宽度为8,%08d:%08d:%08d", 1, -22, 99999990));
   System.out.println(String.format("'+':结果总是包括一个符号。%+d:%+d:%+d", 1, -2, 0));
   System.out.println(String.format("' ':正值前加空格,负值前加负号。% d:% d:% d", 1, -2, 0));
   System.out.println(String.format("',':每3位数字之间用“,”分隔(只适用于fgG的转换)。%,d:%,d:%,d", 1, 100, 1000));
   System.out.println(String.format("'(':若参数是负数,则结果中不添加负号而是用圆括号把数字括起来(只适用于eEfgG的转换)。%(d:%(d", 1, -1));
}

输出如下:

'-':在最小宽度内左对齐,不可与"用0填充"同时使用。
设置最小宽度为8为,左对齐。1       :22      :99999999

'0':结果将用零来填充。设置最小宽度为8,00000001:-0000022:99999990
'+':结果总是包括一个符号。+1:-2:+0
' ':正值前加空格,负值前加负号。 1:-2: 0
',':每3位数字之间用“,”分隔(只适用于fgG的转换)。1:100:1,000
'(':若参数是负数,则结果中不添加负号而是用圆括号把数字括起来(只适用于eEfgG的转换)。1:(1)

width

width是可选参数,用于控制输出的宽度。示例如下:

System.out.println(String.format("设置最小宽度为8,不满8位用0填充:%08d:%08d", 1, -10000000));

输出如下:

设置最小宽度为8,不满8位用0填充:00000001:-10000000

但是 width 的值不能为 0 ,否则会抛出

Exception in thread "main" java.util.DuplicateFormatFlagsException: Flags = '0'
    at java.util.Formatter$Flags.parse(Formatter.java:4443)
    at java.util.Formatter$FormatSpecifier.flags(Formatter.java:2640)
    at java.util.Formatter$FormatSpecifier.(Formatter.java:2709)
    at java.util.Formatter.parse(Formatter.java:2560)
    at java.util.Formatter.format(Formatter.java:2501)
    at java.util.Formatter.format(Formatter.java:2455)
    at java.lang.String.format(String.java:2940)

precision

precision是可选参数,用来限定输出的精度,用于浮点数。示例如下:

/**
* 格式化精度,仅限浮点数
*/
private static void formatPrecision() {
   //System.out.println(String.format("设置精度为2位:%.2d", 1));
   System.out.println(String.format("设置精度为2位:%.2f", 1f));
}

输出如下:

设置精度为2位:1.00

值得注意的是,如果对整型数据设置精度,则会抛出如下异常:

Exception in thread "main" java.util.IllegalFormatPrecisionException: 2
    at java.util.Formatter$FormatSpecifier.checkInteger(Formatter.java:2984)
    at java.util.Formatter$FormatSpecifier.(Formatter.java:2729)
    at java.util.Formatter.parse(Formatter.java:2560)
    at java.util.Formatter.format(Formatter.java:2501)
    at java.util.Formatter.format(Formatter.java:2455)
    at java.lang.String.format(String.java:2940)

时间日期格式化

在平时开发中,经常会碰到要显示时间日期的。以前写过一篇 Android 时间、日期相关类和方法 的博客,里面对 Android 中经常出现的时间日期格式做了总结,但觉得还是过于繁琐,这次总结 Java 中格式化输出,没想到 Java 中已经包含了如此多关于时间的转换符,完全能应对日常开发需要,而且不用复杂的计算。

Java 中时间日期格式化的转换符可以分为三类,分别是:时间格式化转换符、日期格式化转换符、时间日期格式化转换符。相比于日期和时间日期格式化转换符,时间格式化转换符就相对多一点。

时间日期格式化字符串的格式如下:

%[argument_index$][flags][width]conversion

相对于普通的格式,时间日期格式化少了 precision ,而 conversion 是由两个字符组成,且第一个字符固定为 tT

网上部分博文是贴了转换符说明的表格,但是写完代码之后突然发现,Java 格式化输出这部分内容,看代码和输出其实比看表格更直观,谁让我们是程序员呢?下面就用代码来讲述一下时间日期格式化转换符的三种类别。

格式化时间

示例代码如下:

/**
* 格式化时间
*/
private static void formatTime() {
   System.out.println("这是格式化时间相关的,具体输出跟你执行代码时间有关");
   Calendar calendar = Calendar.getInstance();
   System.out.println(String.format("'H':2位数24小时制,不足两位前面补0:%tH(范围:00-23)", calendar));
   System.out.println(String.format("'I':2位数12小时制,不足两位前面补0:%tI(范围:01-12)", calendar));
   System.out.println(String.format("'k':24小时制,不足两位不补0:%tk(范围:0-23)", calendar));
   System.out.println(String.format("'l':12小时制,不足两位不补0:%tl(范围:1-12)", calendar));
   System.out.println(String.format("'M':2位数的分钟,不足两位前面补0:%tM(范围:00-59)", calendar));
   System.out.println(String.format("'S':分钟中的秒,2位数,不足两位前面补0,60是支持闰秒的一个特殊值:%tS(范围:00-60)", calendar));
   System.out.println(String.format("'L':3位数的毫秒,不足三位前面补0:%tL(范围:000-999)", calendar));
   System.out.println(String.format("'N':9位数的微秒,不足九位前面补0:%tN(范围:000000000-999999999)", calendar));

   System.out.println(String.format("'p':输出本地化的上午下午,例如,Locale.US为am或pm,Locale.CHINA为上午或下午", calendar));
   System.out.println(String.format(Locale.US, "Local.US=%tp", calendar));
   System.out.println(String.format(Locale.CHINA, "Local.CHINA=%tp", calendar));
   System.out.println();

   System.out.println(String.format("'z':时区:%tz", calendar));
   System.out.println(String.format("'Z':时区缩写字符串:%tZ", calendar));
   System.out.println(String.format("'s':从1970-1-1 00:00到现在所经历的秒数:%ts", calendar));
   System.out.println(String.format("'Q':从1970-1-1 00:00到现在所经历的豪秒数:%tQ", calendar));
}

输出结果如下:

这是格式化时间相关的,具体输出跟你执行代码时间有关
'H':2位数24小时制,不足两位前面补0:21(范围:00-23)
'I':2位数12小时制,不足两位前面补0:09(范围:01-12)
'k':24小时制,不足两位不补0:21(范围:0-23)
'l':12小时制,不足两位不补0:9(范围:1-12)
'M':2位数的分钟,不足两位前面补0:26(范围:00-59)
'S':分钟中的秒,2位数,不足两位前面补0,60是支持闰秒的一个特殊值:44(范围:00-60)
'L':3位数的毫秒,不足三位前面补0:502(范围:000-999)
'N':9位数的微秒,不足九位前面补0:502000000(范围:000000000-999999999)
'p':输出本地化的上午下午,例如,Locale.US为am或pm,Locale.CHINA为上午或下午
Local.US=pm
Local.CHINA=下午

'z':时区:+0800
'Z':时区缩写字符串:CST
's':从1970-1-1 00:00到现在所经历的秒数:1476883604
'Q':从1970-1-1 00:00到现在所经历的豪秒数:1476883604502

格式化日期

示例代码如下:

/**
 * 格式化日期
 */
private static void formatDate() {
    System.out.println("-----------------------我是微笑的分割线O(∩_∩)O哈哈~-----------------------------");
    System.out.println("这是格式化时间相关的,具体输出跟你执行代码时间有关");
    Calendar calendar = Calendar.getInstance();
    System.out.println(String.format("'B':本地化显示月份字符串,如:January、February"));
    System.out.println(String.format("'b':本地化显示月份字符串的缩写,如:Jan、Feb"));
    System.out.println(String.format("'h':本地化显示月份字符串的缩写,效果同'b'"));
    System.out.println(String.format(Locale.US, "Locale.US 月份=%1$tB,缩写=%1$tb", calendar));
    System.out.println(String.format(Locale.CHINA, "Locale.CHINA 月份=%1$tB,缩写=%1$tb", calendar));

    System.out.println(String.format("'A':本地化显示星期几字符串,如:Sunday、Monday"));
    System.out.println(String.format("'a':本地化显示星期几字符串的缩写,如:Sun、Mon"));
    System.out.println(String.format(Locale.US, "Locale.US 星期几=%1$tA,缩写=%1$ta", calendar));
    System.out.println(String.format(Locale.CHINA, "Locale.CHINA 星期几=%1$tA,缩写=%1$ta", calendar));

    System.out.println(String.format("'C':年份除以100的结果,显示两位数,不足两位前面补0:%tC(范围:00-99)", calendar));
    System.out.println(String.format("'Y':显示四位数的年份,格利高里历,即公历。不足四位前面补0:%tY", calendar));
    System.out.println(String.format("'y':显示年份的后两位:%ty(范围:00-99)", calendar));
    System.out.println(String.format("'j':显示当前公历年的天数:第%tj天(范围:001-366)", calendar));
    System.out.println(String.format("'m':显示当前月份:%tm月(范围:01-13?怎么会有13个月?)", calendar));
    System.out.println(String.format("'d':显示是当前月的第几天,不足两位前面补0:%1$tm月第%1$td天(范围:01-31)", calendar));
    System.out.println(String.format("'e':显示是当前月的第几天:%1$tm月第%1$te天(范围:1-31)", calendar));
}

输出结果如下:

-----------------------我是微笑的分割线O(∩_∩)O哈哈~-----------------------------
这是格式化时间相关的,具体输出跟你执行代码时间有关
'B':本地化显示月份字符串,如:January、February
'b':本地化显示月份字符串的缩写,如:Jan、Feb
'h':本地化显示月份字符串的缩写,效果同'b'
Locale.US 月份=October,缩写=Oct
Locale.CHINA 月份=十月,缩写=十月
'A':本地化显示星期几字符串,如:Sunday、Monday
'a':本地化显示星期几字符串的缩写,如:Sun、Mon
Locale.US 星期几=Wednesday,缩写=Wed
Locale.CHINA 星期几=星期三,缩写=星期三
'C':年份除以100的结果,显示两位数,不足两位前面补0:20(范围:00-99)
'Y':显示四位数的年份,格利高里历,即公历。不足四位前面补0:2016
'y':显示年份的后两位:16(范围:00-99)
'j':显示当前公历年的天数:第293天(范围:001-366)
'm':显示当前月份:10月(范围:01-13?怎么会有13个月?)
'd':显示是当前月的第几天,不足两位前面补0:10月第19天(范围:01-31)
'e':显示是当前月的第几天:10月第19天(范围:1-31)

补充:在文档中发现一个略微奇怪的问题,就是 m 转换符,文档上的大意是:该转换符用于显示当前月是这一年的第几个月,文档里给的范围竟然是01-13,一年怎么会有13个月,告诉我不是我理解错了?

格式化时间日期

示例代码如下:

/**
 * 格式化时间日期
 */
private static void formatTimeAndDate() {
    System.out.println("-----------------------我是微笑的分割线O(∩_∩)O哈哈~-----------------------------");
    System.out.println("这是格式化时间相关的,具体输出跟你执行代码时间有关");
    Calendar calendar = Calendar.getInstance();
    //%tH:%tM的缩写
    System.out.println(String.format("'R':将时间格式化为:HH:MM(24小时制)。输出:%tR", calendar));
    //%tH:%tM:%tS的缩写
    System.out.println(String.format("'T':将时间格式化为:HH:MM:SS(24小时制)。输出:%tT", calendar));
    //%tI:%tM:%tS %Tp的缩写,输出形如:
    System.out.println(String.format("'r':将时间格式化为:09:23:15 下午,跟设置的语言地区有关。输出:%tr", calendar));
    //%tm/%td/%ty的缩写,输出形如
    System.out.println(String.format("'D':将时间格式化为:10/19/16。输出:%tD", calendar));
    //%tY-%tm-%td,输出形如:
    System.out.println(String.format("'F':将时间格式化为:2016-10-19。输出:%tF", calendar));
    //%ta %tb %td %tT %tZ %tY,输出形如:Sun Jul 20 16:17:00 EDT 1969
    System.out.println(String.format("'c':将时间格式化为\"Sun Jul 20 16:17:00 EDT 1969\"。输出:%tc", calendar));
}

输出结果如下:

-----------------------我是微笑的分割线O(∩_∩)O哈哈~-----------------------------
这是格式化时间相关的,具体输出跟你执行代码时间有关
'R':将时间格式化为:HH:MM(24小时制)。输出:21:26
'T':将时间格式化为:HH:MM:SS(24小时制)。输出:21:26:44
'r':将时间格式化为:09:23:15 下午,跟设置的语言地区有关。输出:09:26:44 下午
'D':将时间格式化为:10/19/16。输出:10/19/16
'F':将时间格式化为:2016-10-19。输出:2016-10-19
'c':将时间格式化为"Sun Jul 20 16:17:00 EDT 1969"。输出:星期三 十月 19 21:26:44 CST 2016

总结

系统的介绍了 Java 中格式化字符串的方式及相关的转换符。不过只是对此有印象,方便日后温习,还得在日常中不断加强使用。

虽然 Formatter 中提供了很多关于时间日期的转换符,而且能满足日常的绝大部分使用,但还是存在限制,所以对于有特殊要求的时间格式,还是要学会自己定制。

String.format 这个方法很实用,但如果是大批量进行字符串格式化,就需要考虑到性能方面的问题,因为每次调用 format() 方法都会 new 一个 Formatter 对象。而在 Java 中频繁创建对象需要大量时间,而且还要花时间对这些对象进行垃圾回收和处理。最好的办法就是自己把 DateFormat 、 NumberFormat 、 MessageFormat 这些类封装成静态工具。

参考

  1. Java Api -- Formatter
  2. String.format()方法使用说明
  3. JAVA字符串格式化-String.format()的使用

你可能感兴趣的:(Java 字符串格式化详解)