1. Scanner 概述

在本教程中,我们将介绍 Java 中的 Scanner 类,它用于读取输入、查找和跳过模式,支持多种分隔符。

2. 扫描文件内容

首先来看如何使用 Scanner 读取一个文件。

在下面的例子中,我们读取了一个包含 “Hello world” 的文件,并将其拆分为多个 token:

@Test
public void whenReadFileWithScanner_thenCorrect() throws IOException{
    Scanner scanner = new Scanner(new File("test.txt"));

    assertTrue(scanner.hasNext());
    assertEquals("Hello", scanner.next());
    assertEquals("world", scanner.next());

    scanner.close();
}

注意:next() 方法会返回下一个字符串 token。

同时,记得在使用完毕后调用 scanner.close() 关闭资源。

3. 将 InputStream 转换为 String

接着来看如何使用 ScannerInputStream 转换为 String

@Test
public void whenConvertInputStreamToString_thenConverted()
  throws IOException {
    String expectedValue = "Hello world";
    FileInputStream inputStream 
      = new FileInputStream("test.txt");
    
    Scanner scanner = new Scanner(inputStream);
    scanner.useDelimiter("\\A");

    String result = scanner.next();
    assertEquals(expectedValue, result);

    scanner.close();
}

这里我们使用了 useDelimiter("\\A") 来将整个输入流当作一个 token 读取,其中 \A 表示匹配整个输入的开始。

4. Scanner 与 BufferedReader 的区别

现在我们来对比一下 ScannerBufferedReader 的区别:

BufferedReader 通常用于按行读取输入
Scanner 更适合按 token 读取输入

下面是使用 BufferedReader 按行读取文件的示例:

@Test
public void whenReadUsingBufferedReader_thenCorrect() 
  throws IOException {
    String firstLine = "Hello world";
    String secondLine = "Hi, John";
    BufferedReader reader 
      = new BufferedReader(new FileReader("test.txt"));

    String result = reader.readLine();
    assertEquals(firstLine, result);

    result = reader.readLine();
    assertEquals(secondLine, result);

    reader.close();
}

而使用 Scanner 按 token 读取同一个文件:

@Test
public void whenReadUsingScanner_thenCorrect() 
  throws IOException {
    String firstLine = "Hello world";
    FileInputStream inputStream 
      = new FileInputStream("test.txt");
    Scanner scanner = new Scanner(inputStream);

    String result = scanner.nextLine();
    assertEquals(firstLine, result);

    scanner.useDelimiter(", ");
    assertEquals("Hi", scanner.next());
    assertEquals("John", scanner.next());

    scanner.close();
}

注意:我们使用了 nextLine() 方法来读取整行,而 next() 会根据分隔符拆分 token。

5. 从控制台读取输入

使用 new Scanner(System.in) 可以从控制台读取用户输入:

@Test
public void whenReadingInputFromConsole_thenCorrect() {
    String input = "Hello";
    InputStream stdin = System.in;
    System.setIn(new ByteArrayInputStream(input.getBytes()));

    Scanner scanner = new Scanner(System.in);

    String result = scanner.next();
    assertEquals(input, result);

    System.setIn(stdin);
    scanner.close();
}

我们使用 System.setIn(...) 模拟控制台输入。

5.1. nextLine() 方法

该方法读取当前行的所有内容(不包括行尾的换行符):

scanner.nextLine();

读取完后,Scanner 的位置会跳到下一行。⚠️ 重要:nextLine() 会消费掉换行符。

5.2. nextInt() 方法

此方法读取下一个整数 token:

scanner.nextInt();

⚠️ 注意:nextInt() 不会消费换行符。如果后面接着调用 nextLine(),它会读取到前面留下的换行符,导致跳过用户输入。

6. 输入验证

使用 Scanner 的方法可以轻松验证输入是否为特定类型。例如,使用 hasNextInt() 检查输入是否为整数:

@Test
public void whenValidateInputUsingScanner_thenValidated() 
  throws IOException {
    String input = "2000";
    InputStream stdin = System.in;
    System.setIn(new ByteArrayInputStream(input.getBytes()));

    Scanner scanner = new Scanner(System.in);

    boolean isIntInput = scanner.hasNextInt();
    assertTrue(isIntInput);

    System.setIn(stdin);
    scanner.close();
}

7. 扫描字符串

Scanner 也可以直接扫描字符串:

@Test
public void whenScanString_thenCorrect() 
  throws IOException {
    String input = "Hello 1 F 3.5";
    Scanner scanner = new Scanner(input);

    assertEquals("Hello", scanner.next());
    assertEquals(1, scanner.nextInt());
    assertEquals(15, scanner.nextInt(16)); // 读取十六进制
    assertEquals(3.5, scanner.nextDouble(), 0.00000001);

    scanner.close();
}

8. 查找模式

使用 findInLine() 可以在当前行查找匹配的模式:

@Test
public void whenFindPatternUsingScanner_thenFound() throws IOException {
    String expectedValue = "world";
    FileInputStream inputStream = new FileInputStream("test.txt");
    Scanner scanner = new Scanner(inputStream);

    String result = scanner.findInLine("wo..d");
    assertEquals(expectedValue, result);

    scanner.close();
}

也可以使用 findWithinHorizon() 在指定字符范围内查找:

@Test
public void whenFindPatternInHorizon_thenFound() 
  throws IOException {
    String expectedValue = "world";
    FileInputStream inputStream = new FileInputStream("test.txt");
    Scanner scanner = new Scanner(inputStream);

    String result = scanner.findWithinHorizon("wo..d", 5);
    assertNull(result);

    result = scanner.findWithinHorizon("wo..d", 100);
    assertEquals(expectedValue, result);

    scanner.close();
}

🔍 搜索范围是字符数,不是行数。

9. 跳过模式

使用 skip() 方法可以跳过匹配特定模式的 token:

@Test
public void whenSkipPatternUsingScanner_thenSkipped() 
  throws IOException {
    FileInputStream inputStream = new FileInputStream("test.txt");
    Scanner scanner = new Scanner(inputStream);

    scanner.skip(".e.lo");

    assertEquals("world", scanner.next());

    scanner.close();
}

10. 修改 Scanner 分隔符

默认分隔符是空白字符,可以通过 useDelimiter() 修改:

@Test
public void whenChangeScannerDelimiter_thenChanged() 
  throws IOException {
    String expectedValue = "Hello world";
    String[] splited = expectedValue.split("o");

    FileInputStream inputStream = new FileInputStream("test.txt");
    Scanner scanner = new Scanner(inputStream);
    scanner.useDelimiter("o");

    assertEquals(splited[0], scanner.next());
    assertEquals(splited[1], scanner.next());
    assertEquals(splited[2], scanner.next());

    scanner.close();
}

还可以使用多个分隔符:

@Test
public void whenReadWithScannerTwoDelimiters_thenCorrect() 
  throws IOException {
    Scanner scanner = new Scanner(new File("test.txt"));
    scanner.useDelimiter(",|-");

    assertEquals("John", scanner.next());
    assertEquals("Adam", scanner.next());
    assertEquals("Tom", scanner.next());

    scanner.close();
}

11. 处理 NoSuchElementException

11.1. 异常说明

NoSuchElementException 通常在尝试读取不存在的元素时抛出。

11.2. 复现异常

下面的例子展示了两个 Scanner 实例共享同一个 InputStream 时的问题:

public void givenClosingScanner_whenReading_thenThrowException() throws IOException {
    final FileInputStream inputStream = new FileInputStream("src/test/resources/test_read.in");

    final Scanner scanner = new Scanner(inputStream);
    scanner.next();
    scanner.close();

    final Scanner scanner2 = new Scanner(inputStream);
    scanner2.next();
    scanner2.close();
}

运行时会抛出:

java.util.NoSuchElementException

11.3. 原因分析

Scanner 被关闭时,如果其输入源实现了 Closeable,那么输入源也会被关闭。

11.4. 解决方案

避免多个 Scanner 共享同一个资源。推荐使用单个 Scanner 实例读取,最后再关闭。

12. nextLine() 与 nextXXX() 的坑

常见问题:nextInt() 等方法不会消费换行符,导致后续 nextLine() 被跳过:

Scanner scanner = new Scanner(System.in);
System.out.print("Enter your age: ");
int age = scanner.nextInt();
System.out.print("Enter your first name: ");
String firstName = scanner.nextLine(); // ❌ 被跳过

✅ 正确做法是插入一个 nextLine() 消费换行符:

int age = scanner.nextInt();
scanner.nextLine(); // 消费换行符
String firstName = scanner.nextLine(); // ✅ 正常读取

或者直接统一使用 nextLine() 并手动转换类型:

int age = Integer.parseInt(scanner.nextLine());
String firstName = scanner.nextLine();

或者使用 skip() 跳过换行符:

int age = scanner.nextInt();
scanner.skip("\\R"); // 跳过换行符
String firstName = scanner.nextLine();

13. 总结

在本教程中,我们通过多个示例讲解了 Java 中 Scanner 类的常见用法:

  • ✅ 从文件、控制台或字符串中读取输入
  • ✅ 查找和跳过特定模式
  • ✅ 自定义分隔符
  • ✅ 避免常见的坑,如 NoSuchElementException 和换行符问题

所有示例代码可在 GitHub 找到。


原始标题:Java Scanner