1995年Java 1.0发布时,File类已经存在。它能创建文件、删除文件、查看文件大小和修改时间——但打不开文件看里面写了什么。就像你有一把钥匙能确认门存在,却拧不开锁。

第一次妥协:Streams的诞生

第一次妥协:Streams的诞生

Java 1.0的解决方案是把一切抽象成"流"(Stream)。InputStream负责数据流入程序,OutputStream负责数据流出。这个设计 borrowed from Unix 哲学:一切皆字节流。

但抽象类本身无法工作。InputStream和OutputStream只是接口骨架,没有操作系统对接层。开发者拿到的是一座设计图纸,不是能住的房子。

JDK 1.1补上了FileInputStream和FileOutputStream,终于能读写文件内容。代价是代码冗长:打开文件、套BufferedInputStream缓冲、手动关闭资源,样板代码占80%。

第二次补丁:NIO的异步野心

第二次补丁:NIO的异步野心

2002年Java 1.4推出NIO(New Input/Output,新输入/输出)。核心变化是Channel(通道)和Buffer(缓冲区)分离,加上Selector(选择器)支持单线程管理多连接。

这个设计瞄准高性能服务器场景。但开发者很快发现:NIO不是Stream的替代品,是并行宇宙。FileChannel能内存映射文件,却不能直接用System.in;Selector擅长网络I/O,对文件I/O支持残缺。

两套API并存了12年。企业代码里Stream和NIO混用成为常态,新人入职第一件事是搞懂该用哪个。

第三次重构:NIO.2的迟到统一

第三次重构:NIO.2的迟到统一

Java 7(2011年)的NIO.2终于引入Path和Files工具类。一行Files.readAllLines()替代了十几行Stream代码,WatchService能监听文件变化,AsynchronousFileChannel支持异步文件操作。

但历史包袱没消失。System.in仍是InputStream,标准输出仍是PrintStream。Java 21的虚拟线程能缓解阻塞问题,却改不了API分裂的底子。

30年间,Java I/O经历了三次范式转移,每次都在修补前代的边界。现在打开GitHub任意Java项目,大概率同时存在File、Path、InputStream、FileChannel四种操作文件的方式——它们来自三个不同的十年,却必须共存于同一个JVM。

如果一门语言的设计遗产是"四种方法做同一件事,各有限制",这是进化还是债务?