Java I/O流面试之道
先赞后看,Java进阶一大半
南哥在国外 stackoverflow 看到13年前的这么一个问题:如何使用 Java 逐行读取大型文本文件。大家有什么思路吗?评论区一起讨论讨论。
I need to read a large text file of around 5-6 GB line by line using Java.
How can I do this quickly?
最高赞的回答是名为Peter Lawrey的老哥回答的。
我是南哥,相信对你通关面试、拿下Offer有所帮助。
敲黑板:本文总结了Java I/O流、Java NIO常见的面试题!
⭐⭐⭐收录在《Java学习/进阶/面试指南》:https://github/JavaSouth
1. I/O流
1.1 I/O的理解
面试官:你说下对Java IO的理解?
Java I/O有两个参与对象,一个是I/O源端,一个是想要和I/O
源端通信的各种接收端,比如程序控制IDEA控制台输出、读取文件A写入文件B等,我们程序要保证的就是IO流的顺利读取和顺利写入。JDK把对Java IO的支持都放在了package java.io
包下,南哥数了数,一个有86个类和接口。
我们看下package java.io
包最常用的Reader和Writer接口,他们的作者都是Mark Reinhold。这位老哥是谁?他是Oracle Java平台组的首席架构师,也是字符流读取器和写入器的首席工程师。这么有来头,看来Java I/O的程序设计不简单,我们可以从中学到不少好用的东西。
/**
* @author Mark Reinhold
* @since JDK1.1
*/
public abstract class Reader implements Readable, Closeable { }
public abstract class Writer implements Appendable, Closeable, Flushable { }
1.2 字节输入流抽象基类
面试官:那要怎么读取字节流?
我们先讲输入流,后面再讲下输出流。输入流又分为字节流和字符流,顾名思义,字节流按字节来读取,操作的数据单元是8位的字节;而字符流按字符来读取,操作的数据单元是16位的字符。
读取字节的抽象基类是InputStream,这个基类提供了3个方法给我们来读取字节流。
-
从输入流读取下一个数据字节,值字节以0到255范围内的
int
返回。public abstract int read() throws IOException
-
从输入流读取一定数量的字节并将它们存储到缓冲区数组
b
中。public int read(byte b[]) throws IOException
-
从输入流读取最多
len
个字节的数据到字节数组中。public int read(byte b[], int off, int len) throws IOException
大家注意以上方法的返回参数都是int类型,当正常读取时,int返回的是读取的字节个数;而当int返回-1,就表明输入流到达了末尾。
1.3 字节输入流读取
面试官:你说的这些不是实例,我要的是能真正读取的?
上文的是抽象的接口,本身并不具备实际的功能。真正能够读取文件的是InputStream
抽象基类的子类实现,例如文件流FileInputStream,有了他,我们读取音频、视频、gif等等都不是问题。
// 文件流读取文件
FileInputStream stream = new FileInputStream(SOURCE_PATH);
我们还可以在外面加一层缓存字节流来提高读取效率,在外层套上BufferedInputStream对象,为什么可以提高读取效率我下文会讲到。
BufferedInputStream stream = new BufferedInputStream(new FileInputStream(SOURCE_PATH));
以上通过字节流我们是以n个字节来读取的,如果要用readLine()
读取某一行这种场景下就不适用了。我们可以把缓存字节流换成缓存字符流来承接,使用InputStreamReader
转换流把字节输入流转换成字符输入流。
如下代码所示。
BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream(SOURCE_PATH)));
1.4 提高读取效率
面试官:为什么加一层缓存流就能提高读取效率?
为什么加一层缓存流就能提高读取效率?因为直接使用 FileInputStream
读取文件的话,每次调用 read()
都是从磁盘读取一个字节,而每次读取都是一次系统调用。系统调用是操作系统层面的调用,涉及到用户空间和内核空间之间的上下文切换,这些切换的成本是很昂贵的。
而如果使用缓存流,一次性从文件里读取多个字节到缓存中,减少系统调用同时也减少了磁盘读取,读取的效率明显提高了。
除了Java I/O采用缓存流来提高读取效率,大多应用程序也采用缓存来提升程序性能,例如我们后端在业务开发会使用Redis缓存来减少数据库压力。关于为什么使用缓存来提高应用程序效率
,大家也可以看看国外Quora的回答,解释得很详细。
1.5 字符输入流
面试官:那字符流读取呢?
字符输入流的抽象基类是Reader
,同样是提供了3个方法来支持字符流读取。
-
读取单个字符。
public int read() throws IOException
-
将字符读入数组。
public int read(char cbuf[]) throws IOException
-
将字符读入数组的一部分。
abstract public int read(char cbuf[], int off, int len) throws IOException
字符流读取的实例是FileReader
,同样可以使用缓存字符流提高读取效率。
BufferedReader reader = new BufferedReader(new FileReader(new File(SOURCE_PATH)));
我们来具体实操下,读取C:\\Users\\Desktop\\JavaProGuide\\read
下的所有文件,把他们合并在一起,写入到C:\\Users\\Desktop\\JavaProGuide\\write
下的PRODUCT.txt
文件中。
public class Client {
private static final String PATH = "C:\\Users\\Desktop\\JavaProGuide\\read";
private static final String FILE_OUT = "C:\\Users\\Desktop\\JavaProGuide\\write\\PRODUCT.txt";
public static void main(String[] args) throws IOException {
File file = new File(PATH);
File[] files = file.listFiles();
BufferedWriter writer = new BufferedWriter(new FileWriter(FILE_OUT));
for (File curFile : files) {
BufferedReader reader = new BufferedReader(new FileReader(curFile));
String line;
while ((line = reader.readLine()) != null) {
writer.write(line);
writer.newLine();
}
reader.close();
}
writer.close();
}
}
1.6 输出流
面试官:输出流你也讲一讲?
字节输出流的抽象基类是OutputStream
,字符输出流的抽象基类是Writer
。他们分别提供了以下方法。
字节输出流OutputStream
:
-
将指定字节写入此输出流。
public abstract void write(int b) throws IOException
-
将指定字节数组中的
b.length
字节写入此输出流。public void write(byte b[]) throws IOException
-
将指定字节数组中从偏移量
off
开始的len
个字节写入此输出流。public void write(byte b[], int off, int len) throws IOException
字符输出流Writer
:
-
写入单个字符。
public void write(int c) throws IOException
-
写入字符数组。
public void write(char cbuf[]) throws IOException
-
写入字符数组的一部分。
abstract public void write(char cbuf[], int off, int len) throws IOException
另外字符输出流是使用字符来操作数据,所以可以用字符串来代替字符数组,JDK还支持以下入参是字符串的方法。
-
写入一个字符串。
public void write(String str) throws IOException
-
写入字符串的一部分。
public void write(String str, int off, int len) throws IOException
1.7 字节流和字符流区别
面试官:那字节流和字符流有什么区别?
字节流和字符流的区别主要是三个方面。
- 基本单位不同。字节流以字节(8位二进制数)为基本单位来处理数据,字符流以字符为单位处理数据。
- 使用场景不同。字节流操作可以所有类型的数据,包括文本数据,和非文本数据如图片、音频等;而字符流只适用于处理文本数据。
- 关于性能方面。因为字节流不处理字符编码,所以处理大量文本数据时可能不如字符流高效;而字符流使用到内存缓冲区处理文本数据可以优化读写操作。
2. Java NIO
2.1 NIO介绍
NIO的出现在于提高IO
的速度,它相比传统的输入/输出流速度更快。NIO通过管道Channel
和缓冲器Buffer
来处理数据,可以把管道当成一个矿藏,缓冲器就是矿藏里的卡车。
程序通过管道里的缓冲器进行数据交互,而不直接处理数据。程序要么从缓冲器获取数据,要么输入数据到缓冲器。
2.2 通道和缓冲器
NIO提供了通道和缓冲器这两个核心对象。
(1)管道Channel
:
与传统的IO
流只能只读或只写的单向流不同,NIO通道是双向的,也就是说读写操作可以同时进行,使得数据的处理效率也更高。
(2)缓冲器Buffer
:
传统的输入/输出流一次只处理一个字节,而每一次字节读取都是一次系统调用,涉及到用户空间和内核空间之间的上下文切换,通常来说效率不高。
而NIO
采用内存映射文件方式来处理输入/输出,Channel通过map()
方法把一块数据映射到内存中。程序通过Buffer
进行数据交互,减少了与原始数据源的直接访问。NIO面向块的处理方式使得效率更高。
2.3 非阻塞IO模型
传统的输入/输出流是同步阻塞IO
模型,如果数据源没有数据了,此时程序将进行阻塞。
而NIO
是I/O多路复用模型,线程可以询问通道有没可用的数据,而不需要在没有数据时阻塞掉线程。
2.4 字符流处理字符吗
所有数据包括文本数据最终都是以字节形式存储的,因为计算机底层只能理解二进制数据。
字符最终也是要转换成字节形式,之所以可以在文本文件看到字符,是因为系统将底层的二进制序列转换成了字符。
2.5 Buffer
Buffer里有3个关键变量。
- capcity:表示缓冲器
Buffer
的最大数据容量。 - position:用来指出下一个可以读出/写入
Buffer
的索引位置,也就是记录指针的作用。 - limit:用来表示在
Buffer
里第一个不能被读出/写入的索引位置。
另外Buffer
还提供了get
、put
方法来供我们操作数据,而使用get/put
后,position的指针位置也会随之移动。
public abstract byte get();
public abstract ByteBuffer put(byte b);
2.6 Channel
Channel有常见的3个方法,map()、read()和write()。
// 将通道文件的区域直接映射到字节缓冲区中
public abstract MappedByteBuffer map(MapMode mode, long position, long size)
// 从此Channel通道读取字节序列到给定缓冲区dst
public abstract int read(ByteBuffer dst)
// 将给定缓冲区中src的字节序列写入此Channel通道
public abstract int write(ByteBuffer src)
以下是Channel
的简单使用代码。
public class TestFileChannel {
public static void main(String[] args) {
File f = new File("D:\\JavaGetOffer\\TestFileChannel.java");
try {
FileChannel inChannel = new FileInputStream(f).getChannel();
FileChannel outChannel = new FileOutputStream("a.txt").getChannel();
MappedByteBuffer buffer = inChannel.map(FileChannel.MapMode.READ_ONLY, 0, f.length());
outChannel.write(buffer);
buffer.clear();
CharBuffer charBuffer = StandardCharsets.UTF_8.newDecoder().decode(buffer);
System.out.println(charBuffer);
} catch (IOException ex) {
ex.printStackTrace();
}
}
}
2.7 NIO零拷贝
在NIO
零拷贝出现之前,一个I/O操作会将同一份数据进行多次拷贝。可以看下图,一次I/O操作对数据进行了四次复制,同时来伴随两次内核态和用户态的上下文切换,众所周知上下文切换是很耗费性能的操作。
而零拷贝技术改善了上述的问题。可以对比下图,零拷贝技术减少了对一份数据的拷贝次数,不再需要将数据在内核态和用户态之间进行拷贝,也意味不再进行上下文切换,让数据传输变得更加高效。
⭐⭐⭐本文收录在《Java学习/进阶/面试指南》:https://github/JavaSouth
我是南哥,南就南在Get到你的点赞点赞点赞。
创作不易,不妨点赞、收藏、关注支持一下,各位的支持就是我创作的最大动力❤️