system.in 和 system.out 是 jvm 对操作系统标准文件描述符 0/1 的封装,重定向仅替换 java 层流实例而不改变底层 fd;需在测试中隔离作用域并显式还原,输入需补换行符,输出捕获推荐用 bytearrayoutputstream + utf-8。

System.in 和 System.out 在 JVM 中不是普通 Java 对象,而是与操作系统紧密绑定的桥梁。它们的重定向能力,根植于 JVM 启动时对底层文件描述符(file descriptors)的继承和封装。
底层对应的是操作系统标准文件描述符
在 Unix/Linux 及类 Unix 系统(包括 macOS 和现代 Windows 的 WSL/ConPTY 环境)中:
- System.in 封装了文件描述符 fd 0(stdin),默认指向终端键盘输入,也可被 shell 重定向为文件、管道或 socket
- System.out 封装了文件描述符 fd 1(stdout),默认输出到终端显示区,同样支持重定向到文件或网络目标
- System.err 封装 fd 2(stderr),独立于 stdout,常用于确保错误信息不被 stdout 重定向影响
这些 fd 是进程级资源,由操作系统在 JVM 进程创建时注入,JVM 内部通过 native 方法(如 FileInputStream#initIDs)将它们映射为 Java 层的流对象。
重定向的本质是替换 JVM 内部引用,而非修改 OS fd
调用 System.setIn(InputStream) 或 System.setOut(PrintStream) 并不会改动操作系统的 fd 0/1,而只是替换了 JVM 全局静态变量所持有的 Java 流实例:
- 原始的
System.in是BufferedInputStream包装的FileInputStream,其fd字段指向 OS 的 stdin - 当你传入
new ByteArrayInputStream(...),JVM 就把内部引用指向这个内存字节数组流,后续所有System.in.read()都从该数组读取 - 同理,
System.setOut(new PrintStream(new FileOutputStream("log.txt")))会让所有println写入磁盘文件,但 fd 1 在 OS 层仍保持打开且未被关闭
重定向后仍受原 fd 行为约束的边界情况
尽管 Java 层可自由替换,某些底层特性仍会“透出”:
- 若新设置的
InputStream是阻塞式(如FileInputStream读普通文件),其阻塞行为与原 stdin 无关,但线程挂起效果一致 - 若重定向到网络流(如
Socket.getOutputStream()),write 失败可能触发IOException,这与向终端写失败的语义不同 - 当 JVM 进程被外部中断(如
kill -INT),OS 仍会向 fd 0 发送信号,但若此时System.in已被替换成ByteArrayInputStream,该信号对 Java 层无影响
测试中安全劫持的关键:作用域隔离与显式还原
由于 System.in/out 是 JVM 全局静态变量,重定向具有跨测试污染风险:
- 必须在每个测试方法内完成设置与还原,推荐在
@BeforeEach保存原始流,在@AfterEach或finally块中调用System.setIn(originalIn) - 输入字符串需手动补换行符(
"input\n"),因为Scanner.nextLine()依赖\n切分,而ByteArrayInputStream不自动添加 - 输出捕获建议用
ByteArrayOutputStream+PrintStream组合,避免字符编码歧义(显式指定StandardCharsets.UTF_8)











