Formatter / Scanner 格式化与解析源码
概述
Formatter 与 Scanner 是 java.util 中一对互补的文本工具:Formatter 把格式化指令(%d、%s 等)应用到参数上输出字符串(System.out.printf 的底层);Scanner 按分隔符把输入流切成标记(token)并解析成数字或字符串。两者都围绕格式串解析展开:一个生成,一个消费。
Formatter 的格式串解析在每次 format 调用时进行,Scanner 则基于正则匹配实现输入切分与类型转换。本文基于 OpenJDK 21 源码拆解两者的内部机制。
核心源码解析
① Formatter.format(String format, Object... args) 的解析流程
java
public Formatter format(String format, Object... args) {
...
int index = 0;
int last = -1;
int lasto = -1;
FormatString[] fsa = parse(format); // ① 一次性解析格式串
for (FormatString fs : fsa) {
int idx = fs.index();
switch (idx) {
case -2: // ② 普通文本
append((String) fs);
break;
case -1: // ③ %n 换行
append(System.lineSeparator());
break;
case 0: // ④ 显式索引
...
default: // ⑤ 隐式索引递增
...
}
}
return this;
}parse(format)返回FormatString[]:普通文本段与格式说明符段交错排列,一次解析全程复用。- 每个格式说明符解析出
FormatSpecifier(实现FormatString接口),记录索引、标志、宽度等元信息。 format的语义:显式索引(%1$s)直接取对应参数;隐式索引按出现顺序递增;-1表示%n、%%等特殊说明符,不消费参数。
② FormatSpecifier 的 6 个部分
java
private class FormatSpecifier implements FormatString {
private int index; // ① 参数索引($ 语法)
private Flags f; // ② 标志位集合
private int width; // ③ 最小宽度
private int precision; // ④ 精度
private boolean dt; // ⑤ 日期时间转换
private char c; // ⑥ 转换类型
}- 格式串结构:
%[argument_index$][flags][width][.precision][t]conversion,六个部分从%到转换字符依次解析。 index:1$/2$显式索引,缺省为 0 表示隐式;flags从合法的标志集合中逐字符读取。width是输出最小宽度(数字),precision在.后解析:对字符串是截断长度,对浮点数是小数位数。dt为 true 时转换字符是日期时间系列(t/T后的Y、m、H等);c记录最终转换字符。
③ Formatter 的 Conversion 类型
java
private static final Conversion CONVERSIONS = new Conversion();
private static class Conversion {
static final char DECIMAL_INTEGER = 'd'; // 十进制整数
static final char HEX_INTEGER = 'x'; // 十六进制整数
static final char FLOAT = 'f'; // 定点浮点数
static final char STRING = 's'; // 字符串(toString)
static final char DATE_TIME = 't'; // 日期时间(配合第二字符)
static final char LINE_SEPARATOR = 'n'; // 平台换行
static final char PERCENT_SIGN = '%'; // 字面量 %
}- 通用转换
s/b(布尔)/h(哈希):参数经Formatter包装后调用其toString/ 布尔判断 /hashCode十六进制。 - 整数转换
d/o(八进制)/x(十六进制):不同进制分支各自走Integer/Long/BigInteger的格式化。 - 浮点转换
f(定点)/e(科学计数)/g(通用):先处理舍入再补零对齐。 t是"组合转换":必须紧跟第二字符(如%tY输出年份),单独出现会抛UnknownFormatConversionException。
④ Formatter 的 Flags 位掩码
java
class Flags {
private int flags;
static final Flags LEFT_JUSTIFY = new Flags(1 << 0); // '-' 左对齐
static final Flags UPPERCASE = new Flags(1 << 1); // '^' 大写
static final Flags ALTERNATE = new Flags(1 << 2); // '#' 备用格式
static final Flags PLUS = new Flags(1 << 3); // '+' 强制符号
static final Flags LEADING_SPACE = new Flags(1 << 4); // ' ' 正数前空格
static final Flags ZERO_PAD = new Flags(1 << 5); // '0' 补零
static final Flags GROUP = new Flags(1 << 6); // ',' 千分位分组
static final Flags PARENTHESES = new Flags(1 << 7); // '(' 负数括号
}- 每个标志一个 bit,
FormatSpecifier解析时对同一标志重复出现会抛DuplicateFormatFlagsException。 - 打印时标志组合生效:如
%-10s(左对齐 + 宽度 10)与%05d(补零 + 宽度 5)——先按宽度生成主体,再按标志决定填充方向与字符。 - 语义冲突在
print阶段校验:LEFT_JUSTIFY与ZERO_PAD同时出现时,补零被忽略(JDK 内部f.checkFlags规则)。
⑤ Formatter 的 BigDecimal 舍入
java
// print(BigDecimal) 路径
BigDecimal value = ...;
if (precision >= 0) {
value = value.setScale(precision, RoundingMode.HALF_UP); // ① 按精度舍入
}- 浮点/
BigDecimal输出前先按precision用RoundingMode.HALF_UP做setScale舍入——%f默认精度 6,%.2f即四舍五入到两位小数。 HALF_UP是 JDK 默认的"四舍五入":5 进位、4 舍去;与Banker's rounding(HALF_EVEN)的区别在精确的 0.5 场景。- 舍入完成后,整数部分再按
GROUP标志插入千分位逗号(printInteger内按 3 位一组从右往左扫描)。
⑥ Scanner.next(Pattern pattern) 的标记解析
java
public String next(Pattern pattern) {
ensureOpen();
if (pattern == null) throw new NullPointerException();
// ① 先跳过分隔符
hasNextPattern = null;
...
String token = getCompleteTokenInBuffer(pattern); // ② 用给定 pattern 匹配
...
return token;
}
private String getCompleteTokenInBuffer(Pattern pattern) {
...
// ③ 从 position 起搜索,pattern 匹配到 delimiter 前的子串
int limit = ...;
Matcher m = pattern.matcher(...);
if (m.lookingAt()) { // ④ 从头匹配
...
return token;
}
...
}next(pattern)语义:跳过所有分隔符后,取第一个与 pattern 匹配的标记;next()无参版本等价于next(delimiterPattern)的补集匹配。- 匹配失败时若输入可能未读完(如缓冲不足),
Scanner会读更多数据重试;确定不匹配则抛NoSuchElementException。 - 匹配结果记录在内部,供
match()方法返回MatchResult。
⑦ Scanner.nextInt() 的数字解析
java
public int nextInt() {
return nextInt(defaultRadix); // ① 默认十进制
}
public int nextInt(int radix) {
...
Pattern integerPattern = integerPattern(); // ② 构造整数匹配 pattern
String s = next(integerPattern); // ③ 取标记
try {
int i = Integer.parseInt(s, radix); // ④ 按进制解析
...
return i;
} catch (NumberFormatException nfe) {
// ⑤ 解析失败:定位错误位置,抛 InputMismatchException
throw new InputMismatchException("For input string: \"" + s + "\"");
}
}integerPattern()依据当前radix动态构造:含正负号、进制前缀(0x)与数字范围的字符类,如-?[0-9]+|0[xX][0-9a-fA-F]+。- 两步转换:正则匹配保证"形似数字",
Integer.parseInt做真正的数值解析;负数、溢出(超过Integer.MAX_VALUE)在parseInt阶段暴露。 - 任一步失败都抛
InputMismatchException,且标记未被消费(Scanner会回退 position),调用方可换一种读取方式。
⑧ Scanner 的 delimiter 默认分隔符
java
public Scanner(Readable source) {
...
delimiterPattern = DefaultDelimiter.DELIMITER_PATTERN; // 默认分隔符
}
private static final String WHITESPACE_PATTERN = "\\p{javaWhitespace}+";
public Scanner useDelimiter(Pattern pattern) {
delimiterPattern = pattern; // 自定义分隔符
return this;
}- 默认分隔符是
\p{javaWhitespace}+:匹配一个或多个Character.isWhitespace判定的空白字符(空格、制表、换行、回车等)。 Scanner内每个"取标记"操作都以hasNext开始:先跳过delimiterPattern命中的前缀,再取到下一个分隔符前的子串作为标记。useDelimiter(String)内部把字符串编译为Pattern(无Pattern.quote时按正则语义解释,特殊字符需转义或使用Pattern.quote)。
⑨ Scanner 的 hasNextLine() / nextLine()
java
public boolean hasNextLine() {
...
if (hasNextPattern == null) {
hasNextPattern = LINE_PATTERN; // ① 换行匹配
}
return hasNext(hasNextPattern);
}
private static Pattern LINE_PATTERN = Pattern.compile(".*(\r\n|[\n\r\u2028\u2029\u0085])|.+$");LINE_PATTERN匹配"任意字符直到换行"或"行尾剩余内容":.*(\r\n|[\n\r...])覆盖\n、\r\n与 Unicode 行分隔符,|.+$兜底无换行的最后一行。hasNextLine()只探测不消费;nextLine()消费到换行符为止并把换行符本身丢弃,返回整行字符串。- 细节:
nextLine()与next()混用时,next()留下的分隔符(换行)会被nextLine()吞掉,产生"多一个空行"的经典坑。