Java Scanner 类精讲:经典坑拆解

坑 1:nextInt()/next()后紧跟nextLine(),输入被吞

这是 Scanner 最经典的坑,没有之一。

问题复现

编写如下代码,先读取年龄(整数),再读取用户名(字符串):

public class InputHungryDemo {   public static void main(String\[] args) {   try (Scanner scanner = new Scanner(System.in)) {   System.out.print("请输入您的年龄:");   int age = scanner.nextInt(); // 读取整数用户输入       System.out.print("请输入您的用户名:");   String username = scanner.nextLine(); // 读取字符串用户输入       System.out.println("年龄:" + age + ",用户名:" + username);   }   } }

运行结果:输入完年龄后,按下回车,程序直接跳过用户名输入步骤,输出了空的用户名,或者直接抛出异常。

底层原因分析

这是nextInt()nextLine()对分隔符的处理逻辑不一致导致的,根源在底层的缓冲区处理逻辑:

  1. 当你输入18并按下回车时,控制台的输入缓冲区中,存储的实际字符是18\r\n(Windows 系统)或18\n(Linux/Mac 系统);
  2. nextInt()方法的执行逻辑:从缓冲区中读取字符,直到遇到第一个非数字字符(这里是\r\n),停止读取,将读取到的字符串18转换为整数18,然后将光标停留在分隔符\r/\n的前面,不会吃掉这个分隔符;
  3. 接下来执行nextLine()方法:它会从当前光标位置开始读取,直到遇到第一个换行符,而此时光标后面的第一个字符就是换行符,所以它直接读取到了一个空字符串,将光标移动到换行符之后,结束读取逻辑。

这就相当于,nextInt()把换行符留给了nextLine(),而nextLine()又恰恰以换行符作为结束标记,直接读取了空内容。

修复方案

有两种标准修复方案,本质都是统一分隔符处理逻辑,将残留的分隔符手动消费掉,或者用匹配的方法处理。

修复方案 1:在nextInt()后,额外调用一次nextLine(),吃掉残留的换行符

public class FixInputHungryDemo1 {   public static void main(String\[] args) {   try (Scanner scanner = new Scanner(System.in)) {   System.out.print("请输入您的年龄:");   int age = scanner.nextInt(); // 读取整数,光标停留在换行符之前       // 【修复关键】额外调用一次nextLine(),吃掉残留的换行符,将光标移动到换行符之后   scanner.nextLine();       System.out.print("请输入您的用户名:");   String username = scanner.nextLine(); // 正常读取用户名   System.out.println("年龄:" + age + ",用户名:" + username);   }   } }

修复方案 2:统一使用nextLine()读取所有输入,再手动转换数据类型

这种方案更工业级,因为它完全规避了不同方法对分隔符处理逻辑的差异:

public class FixInputHungryDemo2 {   public static void main(String\[] args) {   try (Scanner scanner = new Scanner(System.in)) {   System.out.print("请输入您的年龄:");   // 用nextLine()读取整行字符串,再手动转换为整数,搭配正则校验格式   int age = Integer.parseInt(scanner.nextLine());       System.out.print("请输入您的用户名:");   String username = scanner.nextLine(); // 正常读取用户名   System.out.println("年龄:" + age + ",用户名:" + username);   }   } }

坑 2:nextLine()读取不到输入内容,读取了空字符串

这是坑 1 的衍生问题,根源是缓冲区中残留了未被消费的分隔符

问题复现

编写如下代码,先读取一个令牌,再读取一行字符串:

public class NextLineEmptyDemo {   public static void main(String\[] args) {   try (Scanner scanner = new Scanner(System.in)) {   System.out.print("请输入一个指令:");   String cmd = scanner.next(); // 读取一个令牌,比如输入:save,按下回车       System.out.print("请输入备注信息:");   String remark = scanner.nextLine(); // 直接读取到空字符串       System.out.println("指令:" + cmd + ",备注:" + remark);   }   } }

底层原因分析

next()方法读取到下一个分隔符(换行符)时,会停止读取,不会吃掉末尾的分隔符,光标停留在分隔符之前。接下来的nextLine()方法,直接读取到了这个残留的分隔符,返回空字符串,将光标移动到分隔符之后。

修复方案

和坑 1 的修复逻辑完全一致,在调用next()方法后,额外调用一次nextLine()方法,吃掉残留的分隔符,或者统一使用nextLine()方法读取所有输入。

坑 3:输入的格式不匹配,抛出InputMismatchException

这是校验逻辑不规范导致的常见异常。

问题复现

编写如下代码,读取用户输入的年龄:

public class InputMismatchDemo {   public static void main(String\[] args) {   try (Scanner scanner = new Scanner(System.in)) {   System.out.print("请输入您的年龄:");   int age = scanner.nextInt(); // 如果输入的不是整数,比如"abc",直接抛出异常   System.out.println("年龄:" + age);   }   } }

运行结果:如果用户输入的不是整数,比如abc18.5十八,程序会直接抛出InputMismatchException异常,没有任何友好提示,流程中断。

底层原因分析

nextInt()方法的底层逻辑,是调用next()读取字符串令牌,再尝试用Integer.parseInt()方法将字符串转换为整数。如果字符串的格式不匹配整数的规则,比如包含字母、特殊字符、小数点,或者超出int类型的取值范围,就会抛出InputMismatchException异常,这是一个非检查型异常,上层如果没有捕获,会直接中断线程的执行。

修复方案

工业级场景下,必须先调用hasNextXxx()方法,预校验输入格式是否匹配,再调用nextXxx()方法读取,搭配if-elsewhile循环,实现重试逻辑,给用户明确的错误提示。修复代码如下:

public class FixInputMismatchDemo {   public static void main(String\[] args) {   try (Scanner scanner = new Scanner(System.in)) {   System.out.print("请输入您的年龄:");   // 【修复关键】先校验下一个令牌是否为合法整数,循环重试,直到输入合法   while (!scanner.hasNextInt()) {   // 输入不匹配,读取并丢弃当前不合法的令牌,打印友好错误提示   String invalidInput = scanner.next();   System.out.printf("输入的「%s」不是合法整数,请重新输入:%n", invalidInput);   }   // 校验通过,再读取合法整数   int age = scanner.nextInt();   System.out.println("年龄:" + age);   }   } }

坑 4:流被关闭后,继续读取 / 底层流被关闭,导致NoSuchElementException

这是资源释放顺序不当导致的异常。

问题复现

编写如下代码,先关闭 Scanner 流,再尝试读取输入:

public class NoSuchElementDemo {   public static void main(String\[] args) {   Scanner scanner = new Scanner(System.in);   System.out.print("请输入用户名:");   String username = scanner.nextLine();       // 不当的资源关闭顺序:提前关闭了Scanner流   scanner.close();       System.out.print("请输入年龄:");   int age = scanner.nextInt(); // 抛出异常,流已被关闭   } }

底层原因分析

调用scanner.close()方法时,会同时关闭 Scanner 底层包裹的InputStream(这里是System.in)。流被关闭后,无法再进行任何读取操作,后续调用任何nextXxx()方法,都会抛出NoSuchElementException异常。

修复方案

  • 必须保证所有读取操作完成后,再关闭流
  • 优先使用 try-with-resources 语法,由编译器自动管理流的关闭顺序,保证读取完成后再释放资源;
  • 多个 Scanner 包裹同一个底层流(如System.in)时,不能提前关闭,要在所有读取操作完成后,统一关闭一次。

坑 5:读取包含中文的文件 / 控制台输入,出现乱码问题

这是字符集编码不匹配导致的经典问题。

问题复现

编写如下代码,读取包含中文的 GBK 编码的文件:

public class CharsetMismatchDemo {   public static void main(String\[] args) throws FileNotFoundException {   // 读取GBK编码的文件,中文会乱码   File file = new File("test\_gbk.txt");   try (Scanner scanner = new Scanner(file)) {   while (scanner.hasNextLine()) {   String line = scanner.nextLine();   System.out.println(line); // 中文乱码   }   }   } }

底层原因分析

Scanner默认使用当前操作系统的字符集编码读取底层流。比如,中文 Windows 系统的默认字符集是 GBK,而 Linux/Mac 系统的默认字符集是 UTF-8。如果文件的实际编码,和 Scanner 设置的编码不一致,就会出现字节转字符时的乱码问题。

修复方案

工业级场景下,必须手动指定与输入流匹配的字符集编码,不能依赖系统默认编码,保证跨平台、跨环境的一致性。

Java 10 及以上版本,提供了传入Charset的构造方法,直接指定编码格式;Java 8 及以下版本,需要用InputStreamReader适配流和编码格式。修复代码如下:

public class FixCharsetMismatchDemo {   public static void main(String\[] args) throws FileNotFoundException {   File file = new File("test\_gbk.txt");   // 【修复关键1】手动指定与文件一致的GBK字符集编码   try (Scanner scanner = new Scanner(file, "GBK")) {   while (scanner.hasNextLine()) {   String line = scanner.nextLine();   System.out.println(line); // 中文正常显示   }   }       // 【修复关键2】读取控制台输入时,指定全局标准编码为UTF-8   try (Scanner scanner = new Scanner(System.in, StandardCharsets.UTF\_8.name())) {   System.out.print("请输入包含中文的用户名:");   String username = scanner.nextLine();   System.out.println("用户名:" + username);   }   } }