1.1 什么是 Scanner?
Scanner 是 Java 5.0 新增的基于字符流的解析工具类,位于java.util包下。它的核心设计目标是简化用户交互和数据读取场景的开发工作:
- 把底层的字节流(InputStream),自动转换为字符流,支持多种字符集编码;
- 基于自定义分隔符(默认空格、换行符),将输入流切分成独立的令牌;
- 将字符串类型的令牌,自动转换成开发者需要的基本数据类型(int、long、double、boolean 等)和字符串类型,内置正则校验逻辑。
1.2 Scanner 的核心应用场景
Scanner 不是用来处理高性能 IO 的工具类,它的核心适配场景,集中在人机交互和简单数据读取,这也是我们今天重点覆盖的范围:
| 场景分类 | 具体用途 | 工业级使用限制 |
|---|---|---|
| 控制台用户交互 | 读取用户输入的的用户名、密码、年龄、订单号、确认指令(y/n) | 不推荐在高并发场景的后端服务中使用,仅用于控制台程序或内部工具的人机交互; |
| 简单文件读取 | 读取配置文件、小型日志文件、批量导入的文本数据(如 csv、txt) | 超过 100MB 的大文件,必须搭配缓冲区优化,或改用BufferedReader; |
| 网络请求流解析 | 读取简单的 HTTP 响应、Socket 输入流,按分隔符拆分数据 | 复杂网络协议或高并发场景,必须改用 Netty 等 NIO 框架; |
| 字符串分词处理 | 拆分复杂格式的字符串(如逗号分隔的 ID 列表、JSON 片段提取) | 复杂结构化数据,推荐使用 Jackson、Gson 等专业序列化工具; |
1.3 Scanner vs BufferedReader:核心差异
很多开发者会混淆这两个读取工具类,二者在设计目标、底层逻辑、性能表现上完全不同,选型错误是线上故障的根源之一:
| 对比维度 | Scanner | BufferedReader |
|---|---|---|
| 核心设计目标 | 简化数据解析:将字节流转换为字符流,自动拆分令牌、转换数据类型 | 提升 IO 读取性能:为底层字符流提供 8KB 的缓冲能力,减少 IO 交互次数; |
| 处理流类型 | 字节流(InputStream)、字符流(Readable) | 字符流(Reader),可通过InputStreamReader适配字节流; |
| 数据读取方式 | 基于自定义分隔符,将流拆分为令牌,再进行类型转换 | 按字符数组、行、固定长度读取流,无内置令牌拆分逻辑; |
| 数据类型转换 | 内置支持基本数据类型、字符串转换,内置正则校验 | 仅支持读取字符或字符串,需要手动进行类型转换; |
| 读取效率 | 慢:final 类,无锁设计,内置解析逻辑,单次读取涉及多次 IO 操作 | 快:abstract 类,采用装饰器模式,单次读取涉及少量 IO 操作; |
| 线程安全性 | 非线程安全,多线程场景下必须做同步处理 | 非线程安全,多线程场景下必须做同步处理; |
| 工业级选型场景 | 控制台交互、简单文件解析、低并发场景的数据流处理 | 大文件读取、高并发场景的数据流处理、高性能 IO 场景; |
【博主经验批注】:我在生产环境见过用 Scanner 读取 10GB 级别的日志文件,导致整个服务的 CPU 占用率飙升至 100%,最终触发 OOM。核心原因就是 Scanner 的设计目标是简化解析,而不是高性能读取。
2.1 基本使用步骤
Scanner 的工作逻辑非常固定,所有场景下的编码步骤都是标准的 4 步模板:
// 1. 引入Scanner类,必须在文件顶部导入 import java.util.Scanner; public class ConsoleInteractionDemo {   public static void main(String\[] args) {   // 2. 构造Scanner对象,传入底层字节流,这里用System.in读取控制台输入   Scanner scanner = new Scanner(System.in);       // 3. 调用nextXxx()方法,读取并解析用户输入,转换成目标类型   System.out.print("请输入您的用户名:");   String username = scanner.nextLine(); // 读取一行字符串,包含空格,直到遇到换行符       System.out.print("请输入您的年龄:");   int age = scanner.nextInt(); // 读取一个整数,自动跳过分隔符,做数字格式校验       System.out.print("请输入您的身高(单位:米):");   double height = scanner.nextDouble(); // 读取一个浮点数,自动做格式校验       System.out.print("请输入您的确认指令(y/n):");   String confirm = scanner.next(); // 读取一个令牌,默认以空格、换行符为分隔符       // 4. 关闭Scanner流,释放底层IO资源,避免资源泄漏   scanner.close();   } }2.2 核心 API 用法差异解析
这是初学者最容易搞混的点,不同的读取方法,对分隔符的处理逻辑完全不同—— 理解这一点,是后续规避经典坑的前提。
| 方法名 | 作用描述 | 分隔符处理逻辑 | 返回值 | 适配场景 |
|---|---|---|---|---|
nextLine() | 读取一整行输入,包含空格,直到遇到换行符 | 会读取当前位置到换行符之间的所有字符,会吃掉换行符本身,将光标移动到换行符之后的下一个位置 | 字符串,不含末尾的换行符 | 读取包含空格的用户名、地址、多行备注信息; |
next() | 读取下一个有效令牌,默认以空格、换行符、制表符为分隔符 | 会自动跳过开头的所有分隔符,然后读取直到下一个分隔符之前的所有字符,不会吃掉末尾的分隔符,光标停在分隔符之前 | 字符串,不含首尾的分隔符 | 读取不包含空格的指令、订单号、手机号、验证码; |
nextInt()/nextLong()/nextDouble() | 读取并解析下一个令牌,转换成对应的基本数据类型 | 内部调用next()读取字符串令牌,再尝试转换为目标类型,不会吃掉末尾的分隔符,光标停在分隔符之前 | 对应的基本数据类型 | 读取年龄、金额、数量、分页参数等数字类输入; |
nextBoolean() | 读取并解析下一个令牌,转换为布尔类型 | 内部调用next()读取字符串令牌,再校验是否为true/false(忽略大小写) | 布尔类型 | 读取确认状态、开关标记、布尔类指令; |
hasNext()/hasNextXxx() | 校验输入流中是否还有下一个令牌,是否匹配指定的正则表达式 | 会阻塞等待用户输入,直到检测到分隔符或流结束标记,不会移动光标 | 布尔类型,有下一个有效令牌则返回true | 循环读取输入、做输入合法性校验,避免异常; |
2.3 正确关闭流资源
Scanner 实现了Closeable接口,必须调用close()方法关闭流,释放底层 IO 资源,否则会导致资源泄漏,长期运行会导致文件句柄或 socket 连接耗尽。
工业级场景下,必须使用try-with-resources语法,自动关闭流资源,保证异常发生时也能正常释放资源,不用手动调用close()方法。代码示例如下:
public class TryWithResourcesDemo {   public static void main(String\[] args) {   // try-with-resources语法:在try关键字后的括号中声明Scanner对象,try代码块执行完后会自动关闭   try (Scanner scanner = new Scanner(System.in)) {   System.out.print("请输入您的用户名:");   String username = scanner.nextLine();   System.out.println("您输入的用户名是:" + username);   } // 无论是否发生异常,这里都会自动调用scanner.close()方法,释放IO资源   } }