[Interview]Java 面试宝典系列之 Java IO 流
Posted Spring-_-Bear
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了[Interview]Java 面试宝典系列之 Java IO 流相关的知识,希望对你有一定的参考价值。
文章目录
1. 介绍一下 Java 中的 IO 流
IO(Input Output)用于实现对数据的输入与输出操作,Java 把不同的输入/输出源(键盘、文件、网络等)抽象表述为流(Stream)。流是从起源到接收的有序数据,有了它程序就可以采用同一方式访问不同的输入/输出源。流的分类如下:
-
数据流向:输入流与输出流,其中输入流只能读取数据、不能写入数据,而输出流只能写入数据、不能读取数据
-
数据单位:字节流和字符流,其中字节流操作的数据单元是
8
位的字节,而字符流(Unicode 编码)操作的数据单元是16
位(2 字节)的字符 -
流的角色:字节流和处理流(包装流),其中节点流可以直接从/向一个特定的 IO 设备(磁盘、网络等)读/写数据,也称为低级流,而处理流是对节点流的连接或封装,用于简化数据读/写功能或提高效率,也称为高级流
-
处理流设计理念体现了修饰器模式。列表中前 5 行为节点流,其余行对应的流为处理流
分类 字节输入流 字节输出流 字符输入流 字符输出流 1 抽象基类 InputStream OutputStream Reader Writer 2 访问文件 FileInputStream FileOutputStream FileReader FileWriter 3 访问数组 ByteArrayInputSteam ByteArrayOutputStram CharArrayReader CharArrayWriter 4 访问管道 PipedInputStream PipedOutputStream PipedReader PipedWriter 5 访问字符串 StringReader StringWriter 缓冲流 BufferedInputStream BufferedOutputStream BufferedReader BufferedWriter 转换流 InputStreamReader OutputStreamWriter 对象流 ObjectInputStream ObjectOutputStream 抽象基类 FilterInputStream FilterOutputStream FilterReader FilterWriter 打印流 PirntStream PrintWriter 推回输入流 PushbackInputStream PushbackReader 特殊流 DataInputStream DataOutputStream - 以
File
开头的文件流用于访问文件 - 以
ByteArray/CharArray
开头的流用于访问内存中的数组 - 以
Piped
开头的管道流用于访问管道,实现进程之间的通信 - 以
String
开头的流用于访问内存中的字符串 - 以
Buffered
开头的缓冲流,用于在读写数据时对数据进行缓存,以减少 IO 次数 InputStreamReader、InputStreamWriter
是转换流,用于将字节流转换为字符流- 以
Object
开头的流是对象流,用于实现对象的序列化 - 以
Print
开头的流是打印流,用于简化打印操作 - 以
Pushback
开头的流是推回输入流,用于将已读入的数据推回到缓冲区,从而实现再次读取 - 以
Data
开头的流是特殊流,用于读写 Java 基本类型的数据
- 以
2. 怎么用流打开一个大文件?
打开大文件,应避免直接将文件中的数据全部读取到内存中,可以采用分次读取的方式:
- 使用缓冲流。缓冲流内部维护了一个缓冲区,通过与缓冲区的交互,减少与设备的交互次数
- 使用
NIO
(Non-blocking I/O,在 Java 领域也称为 New I/O)。NIO 采用内存映射文件的方式来处理输入/输出,其将文件或文件的一段区域映射到内存中,这样就可以像访问内存一样来访问文件了(这种方式模拟了操作系统上的虚拟内存的概念),通过这种方式来进行输入/输出比传统的输入/输出要快得多
3. 说说 NIO 的实现原理
Java 的 NIO 主要由三个核心部分组成:Channel、Buffer、Selector
-
Channel :基本上所有的 IO 在 NIO 中都从一个 Channel 开始,数据可以从 Channel 读到 Buffer 中,也可以从 Buffer 写到 Channel 中。Channel 有好几种类型,其中比较常用的有
FileChannel、DatagramChannel、SocketChannel、ServerSocketChannel
等,分别对应 File、UDP、TCP -
Buffer:Buffer 本质上是一块可以读、写数据的内存。这块内存被包装成
NIO Buffer
对象,并提供了一组方法用来方便地访问该块内存。Java NIO 里关键的 Buffer 实现有CharBuffer、ByteBuffer、ShortBuffer、IntBuffer、LongBuffer、FloatBuffer、DoubleBuffer
。这些 Buffer 覆盖了你能通过 IO 发送的基本数据类型,即byte、short、int、long、float、double、char
。Buffer 对象包含三个重要的属性,分别是capacity、position、limit
,其中 position 和 limit 的含义取决于 Buffer 是处在读模式还是写模式写,而 capacity 的含义总是一样的-
capacity:作为一个内存块,Buffer 有个固定的最大值就是 capacity。Buffer 只能写 capacity 个数据,一旦 Buffer 满了需要将其清空才能继续往里写数据
-
position:当写数据到 Buffer 中时,position 表示当前的位置。初始的 position 值为 0。当一个数据写到 Buffer 后, position 会向前移动到下一个可插入数据的 Buffer 单元,position 最大可为 capacity–1。当将 Buffer 从写模式切换到读模式,position 会被重置为 0,从 Buffer 的 position 处读取数据时,position 向前移动到下一个可读的位置
-
limit:在写模式下,Buffer 的 limit 表示最多能往 Buffer 里写多少数据,此时 limit 等于 capacity。当切换 Buffer 到读模式时, limit 表示你最多能读到多少数据,此时 limit 会被设置成写模式下的 position 值
-
-
Selector:Selector 允许单线程处理多个 Channel,如果你的应用打开了多个连接(通道),但每个连接的流量都很低,这时使用 Selector 就会很方便。要使用 Selector 就必须向 Selector 注册 Channel,然后调用它的 select() 方法,该方法会一直阻塞直到某个注册的通道有事件就绪,一旦这个方法返回,线程就可以处理这些事件(例如有新连接进来,数据接收等)。以下是在一个单线程中使用一个 Selector 处理 3 个 Channel 的示意图:
public void nio() throws IOException
RandomAccessFile randomAccessFile = new RandomAccessFile("C:/Users/Admin/Desktop/nio.txt", "rw");
// 文件通道
FileChannel fileChannel = randomAccessFile.getChannel();
// 分配字节缓冲区容量,设置缓冲区 capacity 的值
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
// 从通道中将数据读入到字节缓冲区中,返回读取到的字节数
int bytesRead = fileChannel.read(byteBuffer);
while (bytesRead != -1)
// 设置缓冲区的 limit = position; position = 0;
byteBuffer.flip();
// 循环读取缓冲区中的所有内容
while (byteBuffer.hasRemaining())
System.out.print((char) byteBuffer.get());
/* ------------------------------ buffer.clear() 和 buffer.compact() 的区别 ------------------------------ */
// buffer.clear(); => position = 0; limit = capacity;
// Buffer 被清空了,但 Buffer 中的数据并未被清除,只是这些标记告诉我们可以从哪里开始往 Buffer 里写数据。
// 如果 Buffer 中有一些未读的数据,此时调用 clear() 方法,这些数据将 “被遗忘”,意味着不再有任何标记会告诉你哪些数据被读过,哪些还没有。
//==========================================================================================================
// buffer.compact(); => limit = capacity;
// 如果 Buffer 中仍有未读的数据,且后续还需要这些数据,但是此时想要先写些数据,那么可以使用 compact() 方法。
// compact() 方法将所有未读的数据拷贝到 Buffer 起始处,然后将 position 设到最后一个未读元素的后面,现在 Buffer 准备好写数据了,但是不会覆盖未读的数据。
byteBuffer.compact();
/* ------------------------------- buffer.mark() 和 buffer.rewind() 的区别 ------------------------------- */
// buffer.mark():可以标记 Buffer 中的一个特定的 position,之后可以通过调用 Buffer.reset() 方法恢复到这个 position
// buffer.rewind():将 position 设回 0,所以你可以重读 Buffer 中的所有数据。limit 保持不变,仍然表示能从 Buffer 中读取多少个元素
// 继续从通道中读取数据到缓冲区
bytesRead = fileChannel.read(byteBuffer);
扩展阅读之不同操作系统下 Java NIO 的 Selector 的具体实现:
Java NIO 根据操作系统不同, 针对 NIO 中的 Selector 有不同的实现,Oracle JDK 会自动选择合适的 Selector,也可以自行设置特定的 Selector:-Djava.nio.channels.spi.SelectorProvider=sun.nio.ch.EPollSelectorProvider
- macosx:KQueueSelectorProvider
- solaris:DevPollSelectorProvider
- Linux:EPollSelectorProvider (Linux kernels >= 2.6) 或 PollSelectorProvider
- windows:WindowsSelectorProvider
JDK 在 Linux 中默认使用 epoll
方式,但 JDK 的 epoll 采用的是水平触发,所以自 Netty 4.0.16 起,Netty 重新实现了 epoll 机制,为 Linux 通过 JNI
的方式提供了 native socket transport
,具有以下优点:
- 采用边缘触发方式
- netty epoll transport 暴露了更多的 nio 没有的配置参数,如 TCP_CORK,SO_REUSEADDR 等等
- 优化代码,更少 GC,更少 synchronized
4. 介绍一下 Java 的序列化与反序列化
-
序列化机制可以将对象转换成字节序列,这些字节序列可以保存在磁盘上,也可以在网络中传输,并允许程序将这些字节序列再次恢复成原来的对象。对象的序列化(Serialize)是指将一个 Java 对象写入 IO 流中,对象的反序列化(Deserialize)则是指从 IO 流中恢复该 Java 对象
-
若对象要支持序列化机制,则它的类需要实现
Serializable
接口,该接口是一个标记接口,它没有提供任何方法,只是标明该类是可以序列化的,Java 的很多类已经实现了 Serializable 接口,如八大包装类、String、Date 等 -
若对象要实现序列化,则需要使用对象流
ObjectInputStream
和ObjectOutputStream
。其中,在序列化时需要调用ObjectOutputStream 对象的writeObject()
方法,以输出对象序列。在反序列化时需要调用 ObjectInputStream 对象的readObject()
方法,将对象序列恢复为对象
5. Serializable 接口为什么需要定义 serialVersionUID 变量?
serialVersionUID
代表序列化的版本,通过定义类的序列化版本,在反序列化时只要对象中所存的版本和当前类的版本一致,就允许做数据恢复的操作,否则将会抛出序列化版本不一致的异常 java.io.InvalidClassException
如果不定义序列化版本,在反序列化容易抛出序列化版本不一致的异常,比如:
- 创建该类的实例,并将这个实例序列化,保存在磁盘上
- 升级这个类,例如增加、删除、修改这个类的成员变量
- 反序列化该类的实例,即从磁盘上恢复修改之前保存的数据
在第 3 步恢复数据的时候,当前的类已经和序列化的数据的格式产生了冲突,可能会发生各种意想不到的问题。增加了序列化版本之后,在这种情况下依然可以尝试反序列化该对象,提高了数据的安全性
6. 除了 Java 自带的序列化之外,你还了解哪些序列化工具?
JSON
:目前使用比较频繁的格式化数据工具,简单直观,可读性好,有jackson,gson,fastjson
等Protobuf
:一个用来序列化结构化数据的技术,支持多种语言诸如 C++、Java 以及 Python,可以使用该技术来持久化数据或者序列化成网络传输的数据- 优点:相比较一些其他的 XML 技术而言,该技术的一个明显特点就是更加节省空间(以二进制流存储)、速度更快以及更加灵活
- 缺点:Protobuf 支持的数据类型相对较少,不支持常量类型。由于其设计的理念是纯粹的展现层协议(Presentation Layer),目前并没有一个专门支持 Protobuf 的 RPC 框架
Thrift
:是 Facebook 开源的一个高性能,轻量级 RPC 服务框架,其产生正是为了满足当前大数据量、分布式、跨语言、跨平台数据通讯的需求。Thrift 并不仅仅是序列化协议,还是一个 RPC 框架- 优点:相对于 JSON 和 XML 而言,Thrift 在空间开销和解析性能上有了比较大的提升,对于对性能要求比较高的分布式系统,它是一个优秀的 RPC 解决方案
- 缺点:由于 Thrift 的序列化被嵌入到 Thrift 框架里, Thrift 框架本身并没有透明提供序列化和反序列化接口,这导致其很难和其他传输层协议共同使用(例如 HTTP)
Avro
:提供两种序列化格式,即 JSON 格式或Binary
格式。- Binary 格式在空间开销和解析性能方面可以和
Protobuf
相媲美, JSON 格式方便测试阶段的调试 - Avro 支持的数据类型非常丰富,甚至包括了 C++ 语言里面的 union 类型
- Avro 支持 JSON 格式的 IDL 和类似于 Thrift 和 Protobuf 的 IDL(实验阶段),这两者之间可以互转
- Schema 可以在传输数据的同时发送,加上 JSON 的自我描述属性,这使得 Avro 非常适合动态类型语言
- Avro 在做文件持久化的时候,一般会和 Schema 一起存储,所以 Avro 序列化文件自身具有自我描述属性,所以非常适合于做 Hive、Pig 和 MapReduce 的持久化数据格式。对于不同版本的 Schema,在进行 RPC 调用的时候,服务端和客户端可以在握手阶段对 Schema 进行互相确认,大大提高了最终的数据解析速度
- Binary 格式在空间开销和解析性能方面可以和
7. 如果不用 JSON 工具,该如何实现对实体类的序列化?
-
可以使用 Java 原生的序列化机制,但是效率比较低,适合小项目
-
可以使用其他的一些第三方类库,比如 Protobuf、Thrift、Avro 等
以上是关于[Interview]Java 面试宝典系列之 Java IO 流的主要内容,如果未能解决你的问题,请参考以下文章
[Interview]Java 面试宝典系列之 Spring Boot
[Interview]Java 面试宝典系列之 Java 多线程
[Interview]Java 面试宝典系列之 MyBatis
[Interview]Java 面试宝典系列之 Java 集合类