在一个安静的下午,你坐在书桌前,思考如何让你的Java代码更加高效。忽然,你想到了I/O操作,这是每个Java开发者绕不过去的难题。你可能曾因为慢如蜗牛的文件读写速度而头疼不已,也可能在处理流时犯了错,导致程序崩溃。这些都源自对Java I/O的理解不够深入。今天,我们将深入探讨Java I/O流的奥秘,让你不仅知其然,更知其所以然。
一、Java IO流基础知识
1. Java I/O的理解
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 { }
2. 输入流
2.1 字节输入流抽象基类
我们先讲输入流,后面再讲下输出流。输入流又分为字节流和字符流,顾名思义,字节流按字节来读取,操作的数据单元是8位的字节;而字符流按字符来读取,操作的数据单元是16位的字符。
读取字节的抽象基类是InputStream,这个基类提供了3个方法给我们来读取字节流。
(1)从输入流读取下一个数据字节,值字节以0到255范围内的int
返回。
public abstract int read() throws IOException
(2)从输入流读取一定数量的字节并将它们存储到缓冲区数组b
中。
public int read(byte b[]) throws IOException
(3)从输入流读取最多len
个字节的数据到字节数组中。
public int read(byte b[], int off, int len) throws IOException
大家注意以上方法的返回参数都是int类型,当正常读取时,int返回的是读取的字节个数;而当int返回-1,就表明输入流到达了末尾。
2.2 字节输入流读取
上文的是抽象的接口,本身并不具备实际的功能。真正能够读取文件的是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)));
2.3 提高读取效率
为什么加一层缓存流就能提高读取效率?因为直接使用 FileInputStream
读取文件的话,每次调用 read()
都是从磁盘读取一个字节,而每次读取都是一次系统调用。系统调用是操作系统层面的调用,涉及到用户空间和内核空间之间的上下文切换,这些切换的成本是很昂贵的。
而如果使用缓存流,一次性从文件里读取多个字节到缓存中,减少系统调用同时也减少了磁盘读取,读取的效率明显提高了。
除了Java I/O采用缓存流来提高读取效率,大多应用程序也采用缓存来提升程序性能,例如我们后端在业务开发会使用Redis缓存来减少数据库压力。关于为什么使用缓存来提高应用程序效率
,大家也可以看看国外Quora的回答,解释得很详细。
2.4 字符输入流
字符输入流的抽象基类是Reader
,同样是提供了3个方法来支持字符流读取。
(1)读取单个字符。
public int read() throws IOException
(2)将字符读入数组。
public int read(char cbuf[]) throws IOException
(3)将字符读入数组的一部分。
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();
}
}
3. 输出流
3.1 输出流
字节输出流的抽象基类是OutputStream
,字符输出流的抽象基类是Writer
。他们分别提供了以下方法。
字节输出流OutputStream
:
(1)将指定字节写入此输出流。
public abstract void write(int b) throws IOException
(2)将指定字节数组中的b.length
字节写入此输出流。
public void write(byte b[]) throws IOException
(3)将指定字节数组中从偏移量off
开始的len
个字节写入此输出流。
public void write(byte b[], int off, int len) throws IOException
字符输出流Writer
:
(1)写入单个字符。
public void write(int c) throws IOException
(2)写入字符数组。
public void write(char cbuf[]) throws IOException
(3)写入字符数组的一部分。
abstract public void write(char cbuf[], int off, int len) throws IOException
另外字符输出流是使用字符来操作数据,所以可以用字符串来代替字符数组,JDK还支持以下入参是字符串的方法。
(1)写入一个字符串。
public void write(String str) throws IOException
(2)写入字符串的一部分。
public void write(String str, int off, int len) throws IOException
4. 字节流和字符流区别
字节流和字符流的区别主要是三个方面。
- 基本单位不同。字节流以字节(8位二进制数)为基本单位来处理数据,字符流以字符为单位处理数据。
- 使用场景不同。字节流操作可以所有类型的数据,包括文本数据,和非文本数据如图片、音频等;而字符流只适用于处理文本数据。
- 关于性能方面。因为字节流不处理字符编码,所以处理大量文本数据时可能不如字符流高效;而字符流使用到内存缓冲区处理文本数据可以优化读写操作。
5.Java NIO是什么?零拷贝的概念
NIO具体文章请点击此处https://blog.csdn.net/weixin_68811816/article/details/141614638
二、Java I/O流常见面试题
1. Java I/O流面试题精选
1. Java I/O流的基本概念是什么?
回答: Java I/O(输入/输出)流用于处理数据的读写操作。Java I/O主要分为字节流和字符流两种类型。字节流用于处理所有类型的I/O,包括文本、音频、视频等,操作单位是字节(8位)。字符流专门处理文本数据,操作单位是字符(16位),通常用于处理文本文件。
2. 字节流和字符流的主要区别是什么?
回答: 字节流(如InputStream
和OutputStream
)以字节为单位进行数据读写,适合处理所有类型的数据,包括文本和二进制文件;字符流(如Reader
和Writer
)以字符为单位进行数据读写,专门处理文本数据,能够自动处理字符编码问题。
3. 什么是缓冲流?它有什么作用?
回答: 缓冲流(如BufferedInputStream
和BufferedOutputStream
)在普通流的基础上增加了一个缓冲区,用于临时存储数据。这样可以减少直接与底层I/O操作的次数,提高效率。缓冲流通过批量读取和写入数据来减少系统调用的次数,从而提升性能。
4. 如何使用BufferedReader
和BufferedWriter
来读取和写入文件?
回答:
// 读取文件
BufferedReader reader = new BufferedReader(new FileReader("input.txt"));
String line;
while ((line = reader.readLine()) != null) {
System.out.println(line);
}
reader.close();
// 写入文件
BufferedWriter writer = new BufferedWriter(new FileWriter("output.txt"));
writer.write("Hello, World!");
writer.newLine();
writer.close();
BufferedReader
用于按行读取文本文件,而BufferedWriter
用于写入文本数据,并支持字符流的缓冲功能。
5. 什么是PrintWriter
,它有什么特点?
回答: PrintWriter
是Writer
的一个子类,提供了更为灵活的输出方式。它支持多种数据类型的打印方法,例如print()
、println()
等,能够以格式化的方式写入文本。与BufferedWriter
不同的是,PrintWriter
没有抛出IOException
,而是将错误输出到标准错误流。
6. 解释一下Java NIO的主要概念?
回答: Java NIO(New I/O)引入了Channel
(通道)和Buffer
(缓冲区)的概念。Channel
是双向的,可以进行读写操作;Buffer
是用于存储数据的容器,数据在Buffer
中进行读写。NIO还引入了非阻塞I/O和选择器(Selector)等功能,以提高I/O操作的效率和灵活性。
7. FileChannel
的map()
方法的作用是什么?
回答: FileChannel
的map()
方法将文件的部分或全部内容映射到内存中。这种内存映射可以减少I/O操作的开销,提高读写效率。通过映射文件到内存,应用程序可以直接在内存中访问文件数据,而不需要频繁的I/O操作。
8. 解释一下什么是零拷贝(Zero-Copy)技术?
回答: 零拷贝技术减少了数据在内核态和用户态之间的拷贝次数。传统的I/O操作可能会将数据多次拷贝,而零拷贝技术通过将数据直接从一个文件描述符传输到另一个文件描述符,避免了多次内存拷贝,从而提高了性能。
9. 什么是ObjectInputStream
和ObjectOutputStream
?
回答: ObjectInputStream
和ObjectOutputStream
分别用于对象的反序列化和序列化。ObjectOutputStream
用于将对象写入流中并进行持久化;ObjectInputStream
用于从流中读取对象并恢复其状态。它们主要用于对象的持久化存储和网络传输。
10. DataInputStream
和DataOutputStream
有什么作用?
回答: DataInputStream
和DataOutputStream
用于读取和写入Java原始数据类型(如int
、float
等),它们支持从流中读写数据类型而不仅仅是字节。这些流提供了方法来读取和写入Java数据类型的二进制表示形式。
11. 如何在Java中实现文件的随机访问?
回答: 使用RandomAccessFile
类可以实现对文件的随机访问。RandomAccessFile
允许在文件的任意位置进行读取和写入操作,不需要按顺序访问文件。例如:
RandomAccessFile file = new RandomAccessFile("example.txt", "rw");
file.seek(100); // 跳到文件的第100个字节
file.writeUTF("Hello, RandomAccessFile");
file.close();
12. 什么是FileInputStream
和FileOutputStream
?
回答: FileInputStream
和FileOutputStream
分别用于读取和写入文件中的字节数据。FileInputStream
用于从文件中读取字节,而FileOutputStream
用于将字节写入文件。这些类适用于处理所有类型的字节数据。
13. 解释一下FileReader
和FileWriter
的用途。
回答: FileReader
和FileWriter
用于处理文件中的字符数据。FileReader
用于从文件中读取字符数据,FileWriter
用于将字符数据写入文件。这些类适合处理文本文件,并且能够处理字符编码转换。
14. 如何在Java中使用BufferedInputStream
和BufferedOutputStream
?
回答:
// 使用BufferedInputStream读取文件
BufferedInputStream bis = new BufferedInputStream(new FileInputStream("input.txt"));
int data;
while ((data = bis.read()) != -1) {
System.out.print((char) data);
}
bis.close();
// 使用BufferedOutputStream写入文件
BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("output.txt"));
bos.write("Hello, BufferedOutputStream".getBytes());
bos.close();
BufferedInputStream
和BufferedOutputStream
分别用于提升读取和写入文件的效率。
15. File
类有哪些常用方法?
回答: File
类提供了多种方法来操作文件和目录,例如:exists()
(检查文件是否存在)、createNewFile()
(创建新文件)、delete()
(删除文件或目录)、listFiles()
(列出目录下的所有文件和目录)、renameTo(File dest)
(重命名文件)。
16. 解释一下ObjectOutputStream
的writeObject()
方法?
回答: ObjectOutputStream
的writeObject()
方法将Java对象写入流中进行序列化。这个方法会将对象的状态保存到流中,使得对象可以被持久化到文件或通过网络传输。
17. 如何使用ByteArrayInputStream
和ByteArrayOutputStream
?
回答:
// 使用ByteArrayInputStream读取数据
ByteArrayInputStream bais = new ByteArrayInputStream("Hello, ByteArrayInputStream".getBytes());
int data;
while ((data = bais.read()) != -1) {
System.out.print((char) data);
}
bais.close();
// 使用ByteArrayOutputStream写入数据
ByteArrayOutputStream baos = new ByteArrayOutputStream();
baos.write("Hello, ByteArrayOutputStream".getBytes());
byte[] byteArray = baos.toByteArray();
System.out.println(new String(byteArray));
baos.close();
ByteArrayInputStream
和ByteArrayOutputStream
分别用于在内存中处理字节数据。
18. 什么是FileChannel
的position()
和size()
方法?
回答: FileChannel
的position()
方法返回当前文件指针的位置,而size()
方法返回文件的总大小。position()
可以用来获取或设置文件指针的位置,size()
用于确定文件的长度。
19. 如何使用Charset
类进行字符编码转换?
回答: Charset
类用于处理字符编码转换。可以使用Charset
对象创建CharsetEncoder
和CharsetDecoder
来编码和解码字符。例如:
Charset charset = StandardCharsets.UTF_8;
CharsetEncoder encoder = charset.newEncoder();
CharsetDecoder decoder = charset.newDecoder();
通过这些对象可以进行字符编码和解码操作。
20. ObjectInputStream
的readObject()
方法如何使用?
回答: ObjectInputStream
的readObject()
方法从流中读取对象并将其反序列化为Java对象。示例:
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("object.dat"));
MyObject obj = (MyObject) ois.readObject();
ois.close();
readObject()
方法将字节流转换回对象,要求对象的类实现Serializable
接口。
结束语
通过这篇文章的学习,你应该对Java I/O流有了更深的理解。无论是字节流还是字符流,每一种流的选择和使用都有其独特的场景。掌握这些知识后,你将不再畏惧复杂的I/O操作,而是能够轻松应对。记住,只有真正理解并运用这些知识,才能在Java的世界里游刃有余。继续加油,把这些技巧融入到你的代码中,打造出更高效、更可靠的应用程序!
让我们一起学习,一起进步!期待在评论区与你们见面。
祝学习愉快!