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()对分隔符的处理逻辑不一致导致的,根源在底层的缓冲区处理逻辑:
- 当你输入
18并按下回车时,控制台的输入缓冲区中,存储的实际字符是18\r\n(Windows 系统)或18\n(Linux/Mac 系统); nextInt()方法的执行逻辑:从缓冲区中读取字符,直到遇到第一个非数字字符(这里是\r或\n),停止读取,将读取到的字符串18转换为整数18,然后将光标停留在分隔符\r/\n的前面,不会吃掉这个分隔符;- 接下来执行
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);
  }
  }
}
运行结果:如果用户输入的不是整数,比如abc、18.5、十八,程序会直接抛出InputMismatchException异常,没有任何友好提示,流程中断。
底层原因分析
nextInt()方法的底层逻辑,是调用next()读取字符串令牌,再尝试用Integer.parseInt()方法将字符串转换为整数。如果字符串的格式不匹配整数的规则,比如包含字母、特殊字符、小数点,或者超出int类型的取值范围,就会抛出InputMismatchException异常,这是一个非检查型异常,上层如果没有捕获,会直接中断线程的执行。
修复方案
工业级场景下,必须先调用hasNextXxx()方法,预校验输入格式是否匹配,再调用nextXxx()方法读取,搭配if-else或while循环,实现重试逻辑,给用户明确的错误提示。修复代码如下:
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);
  }
  }
}
更多推荐



所有评论(0)