Java Scanner 类精讲:经典坑拆解
坑 1nextInt()/next()后紧跟nextLine()输入被吞这是 Scanner 最经典的坑没有之一。问题复现编写如下代码先读取年龄整数再读取用户名字符串public class InputHungryDemo { #x20; public static void main(String\[] args) { #x20; try (Scanner scanner new Scanner(System.in)) { #x20; System.out.print(请输入您的年龄); #x20; int age scanner.nextInt(); // 读取整数用户输入 #x20; #x20; #x20; System.out.print(请输入您的用户名); #x20; String username scanner.nextLine(); // 读取字符串用户输入 #x20; #x20; #x20; System.out.println(年龄 age 用户名 username); #x20; } #x20; } }运行结果输入完年龄后按下回车程序直接跳过用户名输入步骤输出了空的用户名或者直接抛出异常。底层原因分析这是nextInt()和nextLine()对分隔符的处理逻辑不一致导致的根源在底层的缓冲区处理逻辑当你输入18并按下回车时控制台的输入缓冲区中存储的实际字符是18\r\nWindows 系统或18\nLinux/Mac 系统nextInt()方法的执行逻辑从缓冲区中读取字符直到遇到第一个非数字字符这里是\r或\n停止读取将读取到的字符串18转换为整数18然后将光标停留在分隔符\r/\n的前面不会吃掉这个分隔符接下来执行nextLine()方法它会从当前光标位置开始读取直到遇到第一个换行符而此时光标后面的第一个字符就是换行符所以它直接读取到了一个空字符串将光标移动到换行符之后结束读取逻辑。这就相当于nextInt()把换行符留给了nextLine()而nextLine()又恰恰以换行符作为结束标记直接读取了空内容。修复方案有两种标准修复方案本质都是统一分隔符处理逻辑将残留的分隔符手动消费掉或者用匹配的方法处理。修复方案 1在nextInt()后额外调用一次nextLine()吃掉残留的换行符public class FixInputHungryDemo1 { #x20; public static void main(String\[] args) { #x20; try (Scanner scanner new Scanner(System.in)) { #x20; System.out.print(请输入您的年龄); #x20; int age scanner.nextInt(); // 读取整数光标停留在换行符之前 #x20; #x20; #x20; // 【修复关键】额外调用一次nextLine()吃掉残留的换行符将光标移动到换行符之后 #x20; scanner.nextLine(); #x20; #x20; #x20; System.out.print(请输入您的用户名); #x20; String username scanner.nextLine(); // 正常读取用户名 #x20; System.out.println(年龄 age 用户名 username); #x20; } #x20; } }修复方案 2统一使用nextLine()读取所有输入再手动转换数据类型这种方案更工业级因为它完全规避了不同方法对分隔符处理逻辑的差异public class FixInputHungryDemo2 { #x20; public static void main(String\[] args) { #x20; try (Scanner scanner new Scanner(System.in)) { #x20; System.out.print(请输入您的年龄); #x20; // 用nextLine()读取整行字符串再手动转换为整数搭配正则校验格式 #x20; int age Integer.parseInt(scanner.nextLine()); #x20; #x20; #x20; System.out.print(请输入您的用户名); #x20; String username scanner.nextLine(); // 正常读取用户名 #x20; System.out.println(年龄 age 用户名 username); #x20; } #x20; } }坑 2nextLine()读取不到输入内容读取了空字符串这是坑 1 的衍生问题根源是缓冲区中残留了未被消费的分隔符。问题复现编写如下代码先读取一个令牌再读取一行字符串public class NextLineEmptyDemo { #x20; public static void main(String\[] args) { #x20; try (Scanner scanner new Scanner(System.in)) { #x20; System.out.print(请输入一个指令); #x20; String cmd scanner.next(); // 读取一个令牌比如输入save按下回车 #x20; #x20; #x20; System.out.print(请输入备注信息); #x20; String remark scanner.nextLine(); // 直接读取到空字符串 #x20; #x20; #x20; System.out.println(指令 cmd 备注 remark); #x20; } #x20; } }底层原因分析next()方法读取到下一个分隔符换行符时会停止读取不会吃掉末尾的分隔符光标停留在分隔符之前。接下来的nextLine()方法直接读取到了这个残留的分隔符返回空字符串将光标移动到分隔符之后。修复方案和坑 1 的修复逻辑完全一致在调用next()方法后额外调用一次nextLine()方法吃掉残留的分隔符或者统一使用nextLine()方法读取所有输入。坑 3输入的格式不匹配抛出InputMismatchException这是校验逻辑不规范导致的常见异常。问题复现编写如下代码读取用户输入的年龄public class InputMismatchDemo { #x20; public static void main(String\[] args) { #x20; try (Scanner scanner new Scanner(System.in)) { #x20; System.out.print(请输入您的年龄); #x20; int age scanner.nextInt(); // 如果输入的不是整数比如abc直接抛出异常 #x20; System.out.println(年龄 age); #x20; } #x20; } }运行结果如果用户输入的不是整数比如abc、18.5、十八程序会直接抛出InputMismatchException异常没有任何友好提示流程中断。底层原因分析nextInt()方法的底层逻辑是调用next()读取字符串令牌再尝试用Integer.parseInt()方法将字符串转换为整数。如果字符串的格式不匹配整数的规则比如包含字母、特殊字符、小数点或者超出int类型的取值范围就会抛出InputMismatchException异常这是一个非检查型异常上层如果没有捕获会直接中断线程的执行。修复方案工业级场景下必须先调用hasNextXxx()方法预校验输入格式是否匹配再调用nextXxx()方法读取搭配if-else或while循环实现重试逻辑给用户明确的错误提示。修复代码如下public class FixInputMismatchDemo { #x20; public static void main(String\[] args) { #x20; try (Scanner scanner new Scanner(System.in)) { #x20; System.out.print(请输入您的年龄); #x20; // 【修复关键】先校验下一个令牌是否为合法整数循环重试直到输入合法 #x20; while (!scanner.hasNextInt()) { #x20; // 输入不匹配读取并丢弃当前不合法的令牌打印友好错误提示 #x20; String invalidInput scanner.next(); #x20; System.out.printf(输入的「%s」不是合法整数请重新输入%n, invalidInput); #x20; } #x20; // 校验通过再读取合法整数 #x20; int age scanner.nextInt(); #x20; System.out.println(年龄 age); #x20; } #x20; } }坑 4流被关闭后继续读取 / 底层流被关闭导致NoSuchElementException这是资源释放顺序不当导致的异常。问题复现编写如下代码先关闭 Scanner 流再尝试读取输入public class NoSuchElementDemo { #x20; public static void main(String\[] args) { #x20; Scanner scanner new Scanner(System.in); #x20; System.out.print(请输入用户名); #x20; String username scanner.nextLine(); #x20; #x20; #x20; // 不当的资源关闭顺序提前关闭了Scanner流 #x20; scanner.close(); #x20; #x20; #x20; System.out.print(请输入年龄); #x20; int age scanner.nextInt(); // 抛出异常流已被关闭 #x20; } }底层原因分析调用scanner.close()方法时会同时关闭 Scanner 底层包裹的InputStream这里是System.in。流被关闭后无法再进行任何读取操作后续调用任何nextXxx()方法都会抛出NoSuchElementException异常。修复方案必须保证所有读取操作完成后再关闭流优先使用 try-with-resources 语法由编译器自动管理流的关闭顺序保证读取完成后再释放资源多个 Scanner 包裹同一个底层流如System.in时不能提前关闭要在所有读取操作完成后统一关闭一次。坑 5读取包含中文的文件 / 控制台输入出现乱码问题这是字符集编码不匹配导致的经典问题。问题复现编写如下代码读取包含中文的 GBK 编码的文件public class CharsetMismatchDemo { #x20; public static void main(String\[] args) throws FileNotFoundException { #x20; // 读取GBK编码的文件中文会乱码 #x20; File file new File(test\_gbk.txt); #x20; try (Scanner scanner new Scanner(file)) { #x20; while (scanner.hasNextLine()) { #x20; String line scanner.nextLine(); #x20; System.out.println(line); // 中文乱码 #x20; } #x20; } #x20; } }底层原因分析Scanner默认使用当前操作系统的字符集编码读取底层流。比如中文 Windows 系统的默认字符集是 GBK而 Linux/Mac 系统的默认字符集是 UTF-8。如果文件的实际编码和 Scanner 设置的编码不一致就会出现字节转字符时的乱码问题。修复方案工业级场景下必须手动指定与输入流匹配的字符集编码不能依赖系统默认编码保证跨平台、跨环境的一致性。Java 10 及以上版本提供了传入Charset的构造方法直接指定编码格式Java 8 及以下版本需要用InputStreamReader适配流和编码格式。修复代码如下public class FixCharsetMismatchDemo { #x20; public static void main(String\[] args) throws FileNotFoundException { #x20; File file new File(test\_gbk.txt); #x20; // 【修复关键1】手动指定与文件一致的GBK字符集编码 #x20; try (Scanner scanner new Scanner(file, GBK)) { #x20; while (scanner.hasNextLine()) { #x20; String line scanner.nextLine(); #x20; System.out.println(line); // 中文正常显示 #x20; } #x20; } #x20; #x20; #x20; // 【修复关键2】读取控制台输入时指定全局标准编码为UTF-8 #x20; try (Scanner scanner new Scanner(System.in, StandardCharsets.UTF\_8.name())) { #x20; System.out.print(请输入包含中文的用户名); #x20; String username scanner.nextLine(); #x20; System.out.println(用户名 username); #x20; } #x20; } }