java

关注公众号 jb51net

关闭
首页 > 软件编程 > java > Java Scanner类踩坑

Java Scanner类的一些经典坑拆解

作者:落叶-IT

Scanner自JDK 1.5起就被引入java.util包,但它常被误解为只能用来读控制台输入,下面这篇文章主要介绍了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()对分隔符的处理逻辑不一致导致的,根源在底层的缓冲区处理逻辑:

这就相当于,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异常。

修复方案

坑 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);
       }
   }
}

总结 

到此这篇关于Java Scanner类的一些经典坑拆解的文章就介绍到这了,更多相关Java Scanner类踩坑内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:
阅读全文