在Java程序中,通过控制台与用户交互是最基础的能力之一。java.util.Scanner类封装了词法分析逻辑,可以把原始的字节输入流转换成具有类型的令牌,例如整数、浮点数或者一行文本。理解它的构造方式、读取方法以及常见的陷阱,能帮助开发者快速搭建命令行工具。

一、Scanner的基本创建与关闭
Scanner最简单的用法就是包装标准输入流System.in。System.in是一个InputStream,直接读取字节非常麻烦,而Scanner提供了丰富的next方法。在main方法中,我们通常写成下面这样:
import java.util.Scanner;
public class Demo {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
System.out.println("请输入你的名字:");
String name = sc.nextLine();
System.out.println("你好," + name);
sc.close();
}
}
上面的代码创建了一个绑定到控制台的Scanner,调用nextLine读取一整行文本。需要注意的是,Scanner的close方法会关闭底层流,也就是System.in。如果在程序中途关闭了Scanner,后续再创建新的Scanner(System.in)就会失败,因为System.in已经被关掉。因此在单一程序里,建议只创建一个Scanner,并且在程序退出前才关闭,或者直接不关闭交由JVM回收。
另外,Scanner也可以读取文件、字符串等其他数据源。例如new Scanner("hello world")可以从字符串解析,new Scanner(new File("a.txt"))可以从文件读取。这种统一性让它在教学和简单工具中非常受欢迎。但在高并发或者大数据量场景下,它的性能不如BufferedReader,这一点我们后面会提到。
二、常用读取方法对比
Scanner提供了一系列next开头的方法,用来读取不同类型的数据。最常用的是next()、nextLine()、nextInt()、nextDouble()。它们行为差异很大,混淆使用是新手最容易犯的错误。
| 方法 | 读取内容 | 是否消耗换行符 | 遇到空格处理 |
|---|---|---|---|
| next() | 下一个单词 | 否 | 以空白分隔,不读空格 |
| nextLine() | 一整行 | 是 | 读到换行符为止 |
| nextInt() | 整数 | 否 | 只解析数字 |
| nextDouble() | 浮点数 | 否 | 只解析数字 |
可以看到,nextInt这类方法只读取数字部分,不会消耗数字后面的换行符。如果紧接着调用nextLine,它会立刻读到一个空字符串,因为上一行的换行符还在缓冲区。这个问题在下面代码中非常典型:
Scanner sc = new Scanner(System.in);
System.out.print("输入年龄:");
int age = sc.nextInt(); // 输入 20 并回车
System.out.print("输入姓名:");
String name = sc.nextLine(); // 直接读到空行,跳过输入
System.out.println(age + " " + name);
解决方式有两种。一种是在nextInt之后额外调用一次nextLine吃掉换行符;另一种是统一使用nextLine读取,然后自己用Integer.parseInt转换。后一种方式逻辑更清晰,也避免了多种方法混用带来的隐患。在写交互程序时,推荐始终用nextLine接收,再做类型转换和校验。
三、分隔符与异常处理
Scanner默认使用空白字符作为分隔符,但我们可以通过useDelimiter方法自定义。例如读取逗号分隔的数据:
Scanner sc = new Scanner("1,2,3");
sc.useDelimiter(",");
while (sc.hasNextInt()) {
System.out.println(sc.nextInt());
}
sc.close();
当用户输入不符合预期类型时,比如让nextInt去解析字母,会抛出InputMismatchException。因此健壮的程序应该用hasNextInt先做判断,或者用try-catch包裹解析逻辑。下面展示一个安全的整数读取函数:
public static int readInt(Scanner sc, String prompt) {
while (true) {
System.out.print(prompt);
if (sc.hasNextInt()) {
return sc.nextInt();
} else {
sc.next(); // 丢弃错误输入
System.out.println("输入不是整数,请重新输入");
}
}
}
这段代码不断循环,直到用户正确输入整数为止。它先通过hasNextInt确认类型,再读取,避免异常中断程序。对于命令行工具来说,这种防御式输入处理能显著提升用户体验。同时要注意,sc.next()在这里用于消耗掉错误的令牌,否则错误内容一直留在缓冲区会导致死循环。
四、Scanner与BufferedReader的选择
虽然Scanner方便,但它是基于正则做解析的,在大量文本读取时效率偏低。如果只需要按行读取并且自己处理拆分,java.io.BufferedReader性能更好。对比示例如下:
// BufferedReader方式 import java.io.BufferedReader; import java.io.InputStreamReader; BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); String line = br.readLine(); int num = Integer.parseInt(line.trim());
BufferedReader没有nextInt这类方法,所有内容都以字符串返回,开发者要自行转换。但它减少了正则匹配开销,适合处理大文件或高频输入。对于学习阶段和简单交互,Scanner凭借易用性依然是首选。实际项目中可以根据输入规模权衡,小规模用Scanner,大规模用BufferedReader。
总的来说,掌握Scanner的创建、读取方法差异、换行符陷阱以及异常防护,就能应对绝大多数Java控制台输入场景。在写代码时保持单一实例、统一读取策略,可让程序更稳健。