原创

Scanner 读数字受 Locale 影响:逗号可能是小数点,也可能是分组符

1,234 应当是一千二百三十四,还是一点二三四?如果输入交给 Scanner.nextBigDecimal(),答案与扫描器的 Locale 有关。服务器换了默认地区,程序没改源码,读出的数值却可能改变。

这种问题不该靠“在我电脑上读得对”处理。面向人的地区格式输入与机器交换的数字协议,需要不同的约束。下面用 Java 17 做一个没有外部依赖的实验:同一字符串分别按美国和德国地区解析,再核对坏输入的消费以及 token 和行读取的边界。

让两种解释并排出现

保存为 ScannerLocale.java,执行 java ScannerLocale.java。代码用字符串作为输入,避免真实控制台等待干扰结果,也不修改进程的默认 Locale。

import java.math.BigDecimal;
import java.util.InputMismatchException;
import java.util.Locale;
import java.util.Scanner;
public class ScannerLocale {
    static BigDecimal number(String input, Locale locale) {
        try (Scanner scanner = new Scanner(input).useLocale(locale)) {
            BigDecimal value = scanner.nextBigDecimal();
            if (scanner.hasNext()) throw new IllegalArgumentException("extra token");
            return value;
        }
    }
    public static void main(String[] args) {
        System.out.println("US=" + number("1,234", Locale.US).toPlainString());
        System.out.println("DE=" + number("1,234", Locale.GERMANY).toPlainString());
        try (Scanner scanner = new Scanner("bad 7").useLocale(Locale.ROOT)) {
            try { scanner.nextInt(); }
            catch (InputMismatchException expected) {
                System.out.println("rejected=" + scanner.next());
            }
            System.out.println("next=" + scanner.nextInt());
        }
        try (Scanner scanner = new Scanner("42\nalice\n")) {
            System.out.println("id=" + scanner.nextInt());
            System.out.println("remainder=[" + scanner.nextLine() + "]");
            System.out.println("name=" + scanner.nextLine());
        }
    }
}

输出是:

US=1234
DE=1.234
rejected=bad
next=7
id=42
remainder=[]
name=alice

US=1234 与 DE=1.234 读的是同样的五个字符。小数分隔符、分组符及相关数字形式由 locale-sensitive 数字解析规则决定。显式 useLocale 的意义,是让这项依赖成为代码的一部分;它并不让所有输入都自动符合业务要求。

例如系统协议规定只接受 ASCII 十进制小数、小数点只能是点号,直接采用一套宽松地区数字解析未必合适。应该先约定允许的字符和形式,再使用相应解析器检查完整输入。界面上已经本地化过的金额则不同:用户可能合理地输入地区分组符,强行把逗号全部删除也可能把小数放大一千倍。

BigDecimal 在这里保留解析后的十进制值,便于看见含义变化。它解决不了输入格式的歧义,也不会自动执行金额精度、负数范围或币种规则。把结果类型换成高精度数字,不会修复错误的输入协议。

失败以后,扫描位置不会替你前进

实验中的 bad 不是整数。nextInt 抛出 InputMismatchException 后,出错的 token 仍留在那里。程序显式调用 next() 取走它,再读取 7。若异常处理只打印一句“输入有误”,然后继续执行同一个 nextInt,就可能对同一份输入无限报错。

使用 hasNextInt 也有同样的责任:它负责探测,不负责把坏 token 丢掉。一个交互式循环通常需要三条清楚路径:合法 token 被处理,非法 token 被消费并反馈,输入结束则退出。无论采用异常还是探测,都不能漏掉后两条。

还要考虑读取溢出。字符形式看起来像数字,但大于 int 能容纳的范围,同样可能不满足 nextInt。业务如果需要区分格式错误与范围错误,应自己明确分类流程,不能将一个异常类型直接翻译成“请输入数字”。

nextInt 后的 nextLine 为什么是空串

nextInt 读取一个 token,紧跟着的行尾仍处在随后 nextLine 要处理的范围中。因此实验里的第一次 nextLine 得到空串,第二次才读到 alice。不是 Scanner 随机跳过了用户名,而是代码从 token 读取切换到整行读取时,没有说明剩余行如何处理。

最直接的修正是明确读掉剩余行,再读取下一行,但这不应变成到处复制的一句神秘 nextLine()。如果输入规范允许整数后同一行还有姓名,盲目丢弃剩余行反而会丢数据。适合逐行交互的程序可以统一使用 nextLine,再对整行解析;适合空白分隔数据的程序则保持 token 模式,并将姓名含空格的情况列入协议。

Scanner 默认按空白分词,它不是完整 CSV 解析器。姓名带引号、字段内含分隔符、跨行字段等需求,不能靠换一个简单 delimiter 就假装已经支持。工具选择应该跟随输入格式,而不是让输入格式迁就现有一行代码。

这个样本关闭的是包住字符串的扫描器。若包装的是 System.in,关闭 Scanner 也会影响后续共享输入流,库方法不应擅自关闭调用者仍要使用的资源。把扫描器交给谁管理,与把 Locale 固定在哪里一样,都应该有清楚的作用域。

正文到此结束
评论插件初始化中...
Loading...
本文目录