江门市网站建设_网站建设公司_jQuery_seo优化
2026/1/16 15:09:28 网站建设 项目流程

Scanner类关闭资源的正确方式:你真的会用吗?

在Java的世界里,Scanner是每个初学者最早接触的工具之一。它简单、直观,几行代码就能读取用户输入或解析文件内容。但正是这种“傻瓜式”的易用性,让很多人忽略了它背后潜藏的风险——资源管理不当导致的输入流关闭异常、文件句柄泄漏,甚至整个程序卡死

今天我们就来深挖一下这个看似简单的类:Scanner到底要不要关?什么时候能关?怎么关才安全?


一、为什么一个“读字符串”的工具还要管资源?

别被它的表象骗了。虽然Scanner看起来只是帮你切分文本、转换类型(比如把"123"变成int),但它底层其实包装了一个真正的I/O资源,比如:

  • 文件输入流(FileInputStream
  • 标准输入(System.in
  • 网络套接字流(Socket.getInputStream()

这些都不是普通对象,而是操作系统级别的“资源句柄”。如果你打开一个文件却不关闭,系统可能最终耗尽可用的文件描述符,导致后续操作失败。

所以问题来了:

“我只用了new Scanner(System.in),也需要关吗?”
“如果我不关,GC会不会帮我收掉?”

我们一个个来看。


二、Scanner.close() 到底做了什么?

先看结论:

调用scanner.close()会尝试关闭其持有的底层输入源
但并不是所有情况下都会真正关闭物理流

这取决于——Scanner 是否‘拥有’这个资源

源码级真相:所有权决定一切

从 JDK 源码可以发现,Scanner.close()的逻辑是这样的:

public void close() { if (closed) return; if (source instanceof Closeable) { try { ((Closeable) source).close(); } catch (IOException e) { // 忽略异常 } } closed = true; }

关键点在于:
- 它不会主动创建资源,也不会判断资源是否共享;
- 它只负责调用传入sourceclose()方法;
- 如果你传的是自己 new 出来的流(如new FileInputStream("a.txt")),那就会被关;
- 如果你传的是全局静态资源(如System.in),也会被执行close()——但后果很严重!


三、“关闭 System.in” 的代价是什么?

让我们来做个实验:

Scanner scanner1 = new Scanner(System.in); scanner1.nextInt(); scanner1.close(); // ⚠️ 危险操作! Scanner scanner2 = new Scanner(System.in); scanner2.nextLine(); // 抛出 IllegalStateException: Scanner closed

运行结果?直接炸了:

Exception in thread "main" java.lang.IllegalStateException: Scanner closed

为什么会这样?因为System.in虽然是InputStream,但它是一个单例的、全局共享的标准输入流。一旦被某个 Scanner 关闭,JVM 中再没有任何组件能重新打开它。

💡 小知识:System.setIn(...)可以重设标准输入,但正常程序不会这么做。

所以记住一句话:

🔥谁都不该关闭 System.in,哪怕你是最后一个用它的!


四、正确的关闭姿势:三种典型场景

场景1:读文件 → 必须关,且要用 try-with-resources

这是最典型的资源泄漏高发区。

错误写法(常见于老代码):
Scanner scanner = new Scanner(new File("data.txt")); while (scanner.hasNextLine()) { System.out.println(scanner.nextLine()); } // 没有 close!文件句柄一直占着!

在 Windows 上可能导致“文件被占用无法删除”,Linux 下也可能累积大量 open files 导致 Too many open files。

正确写法(推荐):
try (Scanner scanner = new Scanner(new File("data.txt"))) { while (scanner.hasNextLine()) { System.out.println(scanner.nextLine()); } } catch (FileNotFoundException e) { System.err.println("找不到文件"); } // 自动调用 close(),确保资源释放

✅ 利用了 Java 7+ 的try-with-resources机制,编译器自动插入 finally 块并调用close(),万无一失。


场景2:读标准输入 → 不要关,建议复用

命令行交互程序中,通常只需要一个 Scanner 来处理所有用户输入。

推荐做法:静态单例模式
public class ConsoleInput { private static final Scanner SCANNER = new Scanner(System.in); public static String nextLine() { return SCANNER.nextLine(); } public static int nextInt() { return SCANNER.nextInt(); } // 注意:不要提供 shutdown() 或 close() 方法! }

使用时直接调用:

System.out.print("请输入年龄:"); int age = ConsoleInput.nextInt(); System.out.print("请输入姓名:"); String name = ConsoleInput.nextLine();

好处:
- 避免重复创建对象;
- 防止任何人误调close()
- 全局统一入口,便于后期替换为日志记录或其他输入源。


场景3:单元测试 or 字符串解析 → 可以放心关

当你用字符串构造 Scanner 时,其实是包装了一个StringReader,属于内存资源,也应该关闭。

示例:测试数据解析
@Test public void testParseNumbers() { String mockInput = "42\nhello\n3.14"; try (Scanner scanner = new Scanner(mockInput)) { assertEquals(42, scanner.nextInt()); assertEquals("hello", scanner.next()); assertEquals(3.14, scanner.nextDouble(), 0.001); } // 自动关闭,没问题 }

这类用法非常安全,而且必须关闭,否则在频繁执行的测试中会造成资源堆积。


五、陷阱识别:那些年我们踩过的坑

❌ 坑1:方法内部关闭传入的 Scanner

public void readAge(Scanner scanner) { int age = scanner.nextInt(); scanner.close(); // 大错特错! }

调用方还在用,你却提前关门了。这就是典型的“责任错位”——谁创建,谁关闭

✅ 正确做法:由资源创建者负责关闭。


❌ 坑2:多个 Scanner 同时读 System.in

Scanner s1 = new Scanner(System.in); Scanner s2 = new Scanner(System.in); // 允许,但危险! s1.close(); // 关闭了 System.in s2.nextLine(); // 抛异常!

虽然语法允许,但两个 Scanner 实际共用同一个流。第一个调用close()的会把门焊死。

✅ 解决方案:全局只保留一个实例,其他地方引用即可。


❌ 坑3:依赖 GC 回收资源

有人觉得:“不就是没 close 吗?GC 最后会回收吧?”

错!
GC 只回收堆内存,不会自动调用close()。某些情况下,finalize 机制可能会触发关闭,但已被废弃多年,不可靠。

📌 规则:凡实现AutoCloseableCloseable的对象,必须显式关闭。


六、高级技巧:封装输入服务,智能控制关闭

为了更灵活地管理不同类型的输入源,我们可以做一个通用输入处理器:

public class SmartInput implements AutoCloseable { private final Scanner scanner; private final boolean shouldClose; public SmartInput(InputStream in) { this.scanner = new Scanner(in); // 判断是否为 System.in,决定是否允许关闭 this.shouldClose = (in != System.in); } public String nextLine() { return scanner.nextLine(); } public int nextInt() { return scanner.nextInt(); } @Override public void close() { if (shouldClose && scanner != null) { scanner.close(); } // 否则啥也不做,保护 System.in } }

使用示例:

// 读文件 → 会关闭 try (SmartInput input = new SmartInput(new FileInputStream("data.txt"))) { System.out.println(input.nextLine()); } // 读控制台 → 不会关闭 try (SmartInput input = new SmartInput(System.in)) { System.out.println("输入:" + input.nextLine()); } // close() 被拦截,System.in 安然无恙

这种方式实现了“按需关闭”,既保证安全性,又不失灵活性。


七、最佳实践清单

场景是否应关闭推荐方式
读文件✅ 必须关闭try-with-resources
读网络流✅ 必须关闭try-with-resources
读字符串✅ 建议关闭try-with-resources
System.in❌ 绝对不要关全局复用单一实例
作为参数传递❓ 不确定创建者关闭,使用者不关

📌口诀总结

自己开的流自己关,公用的流别乱关;
能用 try 就不用 finally,安全编码少麻烦。


写在最后

Scanner很小,但学问不小。
它教会我们的不仅是如何读一行文字,更是关于资源生命周期管理的基本功。

在现代 Java 开发中,尽管有更强大的工具(如 Jackson、Apache Commons IO、Spring 的 Resource 抽象),但在脚本化任务、教学演示、轻量级 CLI 工具中,Scanner依然是不可或缺的存在。

掌握它的关闭之道,不是为了炫技,而是为了让每一行代码都经得起生产环境的考验。


如果你曾在项目中因“Scanner 已关闭”调试一整天,欢迎留言分享你的故事 😅
也别忘了点赞收藏,下次遇到类似问题,回来翻这篇就够了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询