Java IO 基础概念与核心类图

在Java后端开发面试中,Java IO 面试题往往是考察候选人对底层数据传输理解程度的重要环节。Java IO(Input/Output)体系主要用于处理数据的输入和输出操作。Java IO体系的核心在于抽象基类,所有具体的流类都继承自这四个抽象类:

字节流 (Byte Streams)

处理8位字节。基类为 InputStream 和 OutputStream。适用于处理二进制数据,如图片、音频、视频文件等。

字符流 (Character Streams)

处理16位Unicode字符。基类为 Reader 和 Writer。适用于处理文本数据,如配置文件、源代码等,自动处理字符编码。

缓冲流 (Buffered Streams)

包装其他流以提高性能。如 BufferedInputStream。通过减少读写次数来显著提升I/O效率,是面试中常考的性能优化点。

对象流 (Object Streams)

用于序列化对象。ObjectInputStream 和 ObjectOutputStream。考察重点在于 Serializable 接口及 serialVersionUID。

面试官追问:为什么Java IO类库设计成如此复杂的装饰器模式?

答:装饰器模式允许动态地将责任附加到对象上。例如,我们可以将 FileInputStream 包装在 BufferedInputStream 中,再包装在 DataInputStream 中。这种设计使得核心类保持简单,而功能可以通过组合灵活扩展,符合开闭原则。

BIO, NIO, AIO 深度对比分析

这是Java IO面试中最高频的考点之一。面试官通常会要求你解释三种IO模型的区别,以及它们的适用场景。

1. BIO (Blocking IO - 同步阻塞)

特点: 传统的IO模型。当调用 read() 或 write() 时,线程会被阻塞,直到数据读取或写入完成。每个连接需要分配一个独立的线程进行处理。

缺点: 线程资源消耗大。如果并发连接数高,服务器需要创建大量线程,导致上下文切换开销巨大,甚至耗尽内存。

适用场景: 连接数量较少且固定的架构,如对性能要求不高的内部系统。

// BIO 示例
ServerSocket server = new ServerSocket(8080);
while (true) {
    Socket socket = server.accept(); // 阻塞等待连接
    new Thread(() -> handle(socket)).start(); // 为每个连接创建线程
}
                        

2. NIO (Non-blocking IO - 同步非阻塞)

特点: 引入了 Channel(通道)、Buffer(缓冲区)和 Selector(选择器)。
1. 通道是双向的,既可以读也可以写。
2. 非阻塞模式下,线程发起读请求后,如果数据未就绪,不会等待,而是可以去处理其他任务。
3. 一个线程可以通过Selector管理多个Channel,实现多路复用。

优势: 提高了服务器的吞吐量,用较少的线程处理较多的连接。

适用场景: 高并发、连接数量多且连接时间较短的场景(如聊天服务器、网关)。

// NIO 核心组件
Selector selector = Selector.open();
channel.configureBlocking(false);
channel.register(selector, SelectionKey.OP_ACCEPT);
while (true) {
    selector.select(); // 阻塞直到有事件发生
    Set keys = selector.selectedKeys();
    for (SelectionKey key : keys) {
        // 处理事件...
    }
}
                        

3. AIO (Asynchronous IO - 异步非阻塞)

特点: NIO.2 (Java 7) 引入。基于事件和回调机制。应用程序发起IO请求后,完全由操作系统完成剩余操作,完成后通知应用程序。

优势: 进一步减少了线程等待和上下文切换的开销,编程模型更接近异步回调。

缺点: 底层实现依赖于操作系统的异步IO支持(如Linux的AIO或epoll的模拟),在不同平台上的性能表现不一致,且编程复杂度较高。

适用场景: 连接数多且连接周期较长的应用(如文件服务器)。

字节流 vs 字符流:本质区别与转换

特性 字节流 (Byte Stream) 字符流 (Character Stream)
操作单位 8位字节 (Byte) 16位字符 (Char)
基类 InputStream, OutputStream Reader, Writer
编码处理 不处理编码,直接读写二进制 根据默认或指定编码转换字符
缓冲区 通常无内置缓冲(需手动Buffered) 部分类自带缓冲(如BufferedReader)
典型应用 图片、视频、音频、序列化对象 文本文件、网络文本数据

转换流:桥梁的存在

在面试中,经常会被问到如何在字节流和字符流之间转换。Java提供了两个关键的转换流:

  • InputStreamReader:将 InputStream 转换为 Reader。它是字节流通向字符流的桥梁。
  • OutputStreamWriter:将 OutputStream 转换为 Writer。它是字符流通向字节流的桥梁。

关键考点: 这两个类在构造时可以指定字符集(如UTF-8),从而解决乱码问题。如果不指定,则使用系统默认字符集,这在跨平台部署时可能导致不可预知的Bug。

Java 序列化与反序列化详解

序列化是将对象的状态转换为字节流的过程,而反序列化则是从字节流恢复对象。这在远程调用(RMI)、缓存存储、消息队列中至关重要。

1. 如何实现序列化?

类必须实现 java.io.Serializable 接口。这是一个标记接口,没有方法,仅用于告知JVM该类的实例可以被序列化。

2. serialVersionUID 的作用?

这是序列化版本控制的关键。JVM在反序列化时会检查类的 serialVersionUID 是否与序列化数据中的一致。如果不一致,将抛出 InvalidClassException。

  • 如果未显式定义,JVM会根据类名、字段等自动生成一个哈希值。
  • 最佳实践: 始终显式定义 private static final long serialVersionUID = 1L;,以防止因类结构微小变化导致反序列化失败。

3. 静态变量和瞬态变量 (transient)?

static 变量属于类,不属于对象实例,因此不会被序列化。
transient 关键字用于修饰字段,表示该字段不需要序列化,反序列化时将得到默认值(如int为0,对象为null)。

4. 序列化漏洞 (CVE-2015-4852 等)?

不安全的反序列化是Java安全领域的重大隐患。攻击者可以构造恶意的序列化数据,在反序列化时触发 gadget chain( gadget链),导致远程代码执行(RCE)。面试中提及此点,能体现候选人对安全问题的关注。

Java IO 面试题高频问答汇总

  • Q1: close() 方法内部做了什么?
    close() 方法会关闭流并释放与之关联的系统资源(如文件描述符、网络连接)。对于缓冲流,close() 还会先调用 flush() 将缓冲区剩余数据写出。在实际开发中,推荐使用 try-with-resources 语句自动关闭流,避免资源泄露。
  • Q2: flush() 和 close() 的区别?
    flush() 仅刷新缓冲区,将缓冲区数据写入目标设备,但流本身保持打开状态,可以继续写入。而 close() 在关闭流之前会先刷新缓冲区,之后流将不可再使用。
  • Q3: 为什么读写文件时要使用缓冲流?
    直接读写文件涉及大量的磁盘I/O操作,磁盘I/O是非常昂贵的操作(相对于内存)。缓冲流通过在内存中维护一个缓冲区(默认通常8KB),批量读取或写入数据,从而显著减少磁盘I/O次数,提高性能。
  • Q4: RandomAccessFile 的特点?
    RandomAccessFile 支持随机访问文件,它不继承自 InputStream 或 OutputStream,而是实现了 DataInput 和 DataOutput 接口。它可以移动到文件的任意位置进行读写(通过 seek() 方法),常用于实现断点续传或文件索引。
  • Q5: NIO 中的 Buffer 有哪几种状态?
    Buffer 有四个关键属性:capacity(容量)、position(当前位置)、limit(限制)、mark(标记)。
    1. capacity:缓冲区最大容量。
    2. position:下一个要读写的位置。
    3. limit:不能读写的位置界限。
    4. mark:标记,可通过 reset() 回到标记位置。
    典型流程:put() 后 position++,flip() 后 position=0, limit=position,get() 后 position++,clear() 后 position=0, limit=capacity。