IO流的简单讲解与使用
IO 流在 Java 中:
- 根据操作数据单位 的不同,IO 流分为字节流和字符流;
- 根据数据流的流向 不同,分为输入流和输出流;
- 根据流的角色 不同,分为节点流和处理流。
字节和字符很好理解。输入输出也很好理解。
不过节点流和处理流可能听说的比较少了,因此在此贴出以下解释:
在 Java I/O 中,节点流(Node Stream)和处理流(Processing Stream)是两种不同类型的流。
节点流(Node Stream)也称为原始流,是直接与数据源或目标进行交互的流。它们提供了直接读取或写入数据的方法,可以从文件、网络连接、输入设备或输出设备读取或写入数据。节点流是 I/O 操作的起点和终点,可以与底层数据源或目标直接交互。
处理流(Processing Stream)也称为包装流,它们是对节点流的包装或装饰。处理流本身并不直接与数据源或目标进行交互,而是通过包装节点流来提供额外的功能或简化操作。处理流可以对数据进行缓冲、压缩、解密、序列化等处理,以提供更高级的功能或简化开发。
Java IO 流的 40 多个类都是从如下 4 个抽象类基类中派生出来的,派生的子类名称都是以其父类名作为子类名的后缀。
- InputStream/OutputStream: 所有的字节流
- Reader/Writer: 所有的字符流
不过在正式进入IO流的讲解之前,需要先介绍IO中的几个base接口。
常用接口
Serializable
Serializable接口是Java中的一个标记接口(Marker Interface),用于标识类的实例可以被序列化和反序列化。
当一个类实现Serializable接口时,它表示该类的对象可以被序列化。接口本身没有定义任何方法,只是作为一个标记,告诉Java运行时环境该类可以进行序列化操作。Java的序列化机制会自动处理对象的字段和层次结构,将对象及其关联的数据进行序列化和反序列化。
java
public interface Serializable {
}
AutoCloseable
走入源码:
csharp
public interface AutoCloseable {
void close() throws Exception;
}
去掉注释来说,AutoCloseable接口仅仅是定义一个close方法,声明会抛出Exception异常,这显而不利于我们理解源码,接下来我将带大家一起看他的类注释和方法注释。
- 类注释
强烈建议大家可以自己从头到尾看一遍,再看本人翻译来比对。
vbnet
An object that may hold resources (such as file or socket handles) until it is closed. The close() method of an AutoCloseable object is called automatically when exiting a try-with-resources block for which the object has been declared in the resource specification header. This construction ensures prompt release, avoiding resource exhaustion exceptions and errors that may otherwise occur.
API Note:
It is possible, and in fact common, for a base class to implement AutoCloseable even though not all of its subclasses or instances will hold releasable resources. For code that must operate in complete generality, or when it is known that the AutoCloseable instance requires resource release, it is recommended to use try-with-resources constructions. However, when using facilities such as java.util.stream.Stream that support both I/O-based and non-I/O-based forms, try-with-resources blocks are in general unnecessary when using non-I/O-based forms.
Since:
1.7
Author:
Josh Bloch
介绍:
An object that may hold resources (such as file or socket handles) until it is closed. 一个对象,会一直持有资源直到他被关闭,例如file和socket处理对象
The close() method of an AutoCloseable object is called automatically when exiting a try-with-resources block for which the object has been declared in the resource specification header. 好这一段是一个英语长句,比较反直觉的单词有:
- call 管理
- specification 规格 翻译如下: 如果一个实现了AutoCloseable接口的对象在try-with-resources 代码块的 资源规格头部 定义,这个对象的close方法会被自动的管理
This construction ensures prompt release, avoiding resource exhaustion exceptions and errors that may otherwise occur. 比较反直觉的单词有:
- construction 构造 constructor 构造器 这个应该要认识。
- prompt 促使 这种 构造 确保促使资源释放,以避免资源耗尽异常和可能发生的错误
使用建议:
It is possible, and in fact common, for a base class to implement AutoCloseable even though not all of its subclasses or instances will hold releasable resources. 在很多常见的情况下,一个基类对象很可能去实现AutoCloseable接口,尽管并不是所有他的子类会持有 可释放的资源
For code that must operate in complete generality, or when it is known that the AutoCloseable instance requires resource release, it is recommended to use try-with-resources constructions. 对于必须完全通用性操作的代码,或者当知道AutoCloseable实例需要资源释放时,建议使用try-with-resources构造。
However, when using facilities such as java.util.stream.Stream that support both I/O-based and non-I/O-based forms, try-with-resources blocks are in general unnecessary when using non-I/O-based forms. 但是,当使用Stream这种支持度比较好的工具时,没有必要使用try-with-resources
其他: 编写于jdk7,作者Josh Bloch 这大哥写了很多集合类、包装类啥的。
Closeable
java
public interface Closeable extends AutoCloseable {
public void close() throws IOException;
}
Closable接口继承了AutoCloseable接口,它与父类的不同在于抛出的异常被限制在了IOException,这也是为什么大部分IO接口都是实现Closable而不是AutoCloseable。
字节流
字节流是以字节为单位进行输入和输出的流。
它们处理的是二进制数据 ,没有对数据进行解释或编码。字节流可以用于读取和写入任何类型的数据,包括文本文件、图像文件、音频文件等。
OutputStream(字节输出流)
OutputStream 实现了 Closeable, Flushable
两个接口,其中Closeable
在上文已经介绍过啦。
OutputStream 类定义了一组抽象方法,派生类必须实现这些方法来提供具体的写入操作。以下是一些常用的 OutputStream 方法:
void write(int b) throws IOException
:将一个字节写入输出流。参数 b 是要写入的字节,只写入低8位数据。void write(byte[] b) throws IOException
:将一个字节数组的所有字节写入输出流。void write(byte[] b, int off, int len) throws IOException
:将字节数组的一部分写入输出流。参数 off 是数组的起始偏移量,参数 len 是要写入的字节数。void flush() throws IOException
:刷新输出流,将缓冲区中的数据立即写入目标。void close() throws IOException
:关闭输出流,释放相关的系统资源。
FileOutputStream
FileOutputStream
是最常用的字节输出流子类,可直接指定文件路径
以下是 FileOutputStream 的常用构造函数:
FileOutputStream(String name)
:使用指定的文件名创建 FileOutputStream 对象。FileOutputStream(String name, boolean append)
:使用指定的文件名创建 FileOutputStream 对象,并指定是否以追加模式打开文件。FileOutputStream(File file)
:使用指定的 File 对象创建 FileOutputStream 对象。FileOutputStream(File file, boolean append)
:使用指定的 File 对象创建 FileOutputStream 对象,并指定是否以追加模式打开文件。
append为true时表示为追加写入,追加写入的意思是当路径中,文件存在时,对文件进行写入操作不会删除文件中的旧数据。
使用案例代码:
ini
File file = new File("a.txt");
try(OutputStream outputStream = new FileOutputStream(file);){
String str = "Hi,Mrpersimmon!";
outputStream.write(str.getBytes("UTF-8"));
}catch (Exception e){
}
DataOutputStream
DataOutputStream 是 Java 中的一个包装类,它实现了 DataOutput 接口 ,并继承自 FilterOutputStream 类。
DataOutput 接口提供了一系列的方法,用于写入不同类型的数据到输出流中。
DataOutputStream 的构造函数接受一个 OutputStream 对象作为参数,用于指定要包装的输出流。它可以包装任何 OutputStream 子类的对象,例如 FileOutputStream、ByteArrayOutputStream 等。
DataOutputStream 提供了一些常用的方法来写入不同类型的数据到输出流中:
void write(int b)
:将一个字节写入输出流。void write(byte[] b)
:将一个字节数组的所有字节写入输出流。void write(byte[] b, int off, int len)
:将字节数组的一部分写入输出流。void writeBoolean(boolean v)
:将一个 boolean 值写入输出流。void writeByte(int v)
:将一个字节值写入输出流。void writeShort(int v)
:将一个短整型值写入输出流。void writeInt(int v)
:将一个整型值写入输出流。void writeLong(long v)
:将一个长整型值写入输出流。void writeFloat(float v)
:将一个浮点型值写入输出流。void writeDouble(double v)
:将一个双精度浮点型值写入输出流。void writeUTF(String str)
:将一个 UTF-8 编码的字符串写入输出流。
ObjectOutputStream
ObjectOutputStream 是 Java 中的一个类,它继承自 OutputStream 类,并实现了 ObjectOutput 和 ObjectStreamConstants 接口。
ObjectOutput 接口扩展了 DataOutput 接口,并定义了一组方法,用于将对象以二进制形式写入输出流。 ObjectStreamConstants 接口是 Java 中的一个接口,它定义了一些常量,用于处理对象序列化和反序列化过程中的标志和状态
ObjectOutputStream 用于将对象写入到输出流。(这里涉及到如何让对象序列化和反序列化,挖坑,以后埋)
InputStream
InputStream 用于从文件读取数据(字节信息)到内存中,java.io.InputStream 抽象类是所有字节输入流的父类。
InputStream 类定义了以下常用的方法:
int read()
:从输入流读取下一个字节的数据,并返回所读取的字节值。如果已经到达输入流的末尾,则返回 -1。int read(byte[] b)
:从输入流读取最多 b.length 个字节的数据,并将其存储到字节数组 b 中。返回实际读取的字节数。如果已经到达输入流的末尾,则返回 -1。int read(byte[] b, int off, int len)
:从输入流读取最多 len 个字节的数据,并将其存储到字节数组 b 的指定偏移量 off 处开始的位置。返回实际读取的字节数。如果已经到达输入流的末尾,则返回 -1。long skip(long n)
:从输入流中跳过并丢弃 n 个字节的数据。返回实际跳过的字节数。int available()
:返回可以从输入流中读取(或跳过)而不会被阻塞的字节数。void close()
:关闭输入流,释放与其关联的资源。- 用于实现输入流的标记和重置操作 。
- mark(int readlimit)
- reset()
- markSupported()
- jdk9增加
readAllBytes()
读取输入流中的所有字节,返回字节数组。byte[]readNBytes(byte[] b, int off, int len)
阻塞直到读取 len 个字节。longtransferTo(OutputStream out)
将所有字节从一个输入流传递到一个输出流。
FileInputStream
FileInputStream 是一个比较常用的字节输入流子类,可直接指定文件路径
接下来将对于以下操作进行讲解与示例:
- 完整读取文件
- 输入流的标记和重置
完整读取文件
read()
一个字节一个字节的读取:
ini
File file = new File("a.txt");
try (InputStream inputStream = new FileInputStream(file)) {
int content;
while ((content = inputStream.read()) != -1) {
System.out.print((char) content);
}
} catch (IOException e) {
e.printStackTrace();
}
read(byte[] b)
一个数组一个数组的读
ini
File file = new File("a.txt");
try (InputStream inputStream = new FileInputStream(file)) {
byte[] buf = new byte[8]; // 一次读取 8 字节
int content;
while ((content = inputStream.read(buf)) != -1) {
System.out.print(new String(buf, 0, content)); // 将字符数组 buf 转换成字符串
}
} catch (IOException e) {
e.printStackTrace();
}
readAllBytes()
一次性读完
ini
File file = new File("a.txt");
try (InputStream inputStream = new FileInputStream(file)) {
byte[] bytes = inputStream.readAllBytes();
System.out.println(new String(bytes,0,bytes.length));
} catch (IOException e) {
e.printStackTrace();
}
输入流的标记与重置
在使用输入流的标记和重置功能时,需要注意输入流是否支持这些操作,并遵循相应的使用要求。
ini
try {
FileInputStream fis = new FileInputStream("file.txt");
// 标记输入流的当前位置
fis.mark(1024);
// 读取并输出前5个字节
for (int i = 0; i < 5; i++) {
int data = fis.read();
System.out.print((char) data);
}
// 重置输入流的位置
fis.reset();
// 重新读取并输出前5个字节
for (int i = 0; i < 5; i++) {
int data = fis.read();
System.out.print((char) data);
}
fis.close();
} catch (IOException e) {
e.printStackTrace();
}
DataInputStream
实现和继承结构大致与OutputStream那边相似,不赘述了。
ObjectInputStream
实现和继承结构大致与OutputStream那边相似,不赘述了。
字符流
字节流在不知道编码类型的情况下很容易出现乱码问题,因此我们需要字符流来读取文本文件。
字符流默认采用的是 Unicode 编码,我们可以通过构造方法自定义编码。
- utf8,英文占 1 字节,中文占 3 字节;
- unicode:任何字符都占 2 个字节;
- gbk:英文占 1 字节,中文占 2 字节。
Writer(字符输出流)
java.io.Writer抽象类是所有字符输出流的父类,Writer 类提供了一组方法,用于将字符数据写入目标位置,如文件、字符数组、字符串等。
以下是 Writer 类常用的方法:
void write(int c)
:将指定的字符写入输出流。void write(char[] cbuf)
:将字符数组的所有字符写入输出流。void write(char[] cbuf, int off, int len)
:将字符数组的一部分字符写入输出流,从偏移量 off 处开始写入 len 个字符。void write(String str)
:将字符串的所有字符写入输出流。void write(String str, int off, int len)
:将字符串的一部分字符写入输出流,从偏移量 off 处开始写入 len 个字符。void flush()
:刷新输出流,将缓冲区中的数据强制写入目标位置。void close()
:关闭输出流,释放相关的资源。
FileWriter
FileWriter 继承于 OutputStreamWriter, 是基于该基础上的封装,可以直接将字符写入到文件。
OutputStreamWriter 是字符流转换为字节流的桥梁
ini
File file = new File("a.txt");
try (Writer writer = new FileWriter(file)) {
writer.write("你好");
} catch (IOException e) {
e.printStackTrace();
}
Reader(字符输入流)
java.io.Reader抽象类是所有字符输入流的父类,Reader 类提供了一组方法,用于从输入源中读取字符数据,如文件、字符数组、字符串等。
以下是 Reader 类常用的方法:
int read()
:读取下一个字符的数据,并返回所读取的字符的 ASCII 值。如果已经到达流的末尾,则返回 -1。int read(char[] cbuf)
:将字符数据读取到字符数组 cbuf 中,并返回实际读取的字符数。如果已经到达流的末尾,则返回 -1。int read(char[] cbuf, int off, int len)
:将字符数据的一部分读取到字符数组 cbuf 中,从偏移量 off 处开始读取 len 个字符,并返回实际读取的字符数。如果已经到达流的末尾,则返回 -1。long skip(long n)
:跳过 n 个字符的数据,并返回实际跳过的字符数。boolean ready()
:检查输入流是否准备好被读取。void close()
:关闭输入流,释放相关的资源。
FileReader
FileReader 继承于 InputStreamReader 是基于该基础上的封装,可以直接操作字符文件。
InputStreamReader 是字节流转换为字符流的桥梁,
ini
File file = new File("a.txt");
try (Reader reader = new FileReader(file)) {
char[] cbuf = new char[8];
int readLen = 0;
while ((readLen = reader.read(cbuf)) != -1) {
System.out.print(new String(cbuf, 0, readLen));
}
} catch (IOException e) {
e.printStackTrace();
}
缓冲流
IO 操作是很消耗性能的,缓冲流将数据加载至缓冲区,一次性读取/写入多个字节/字符,从而避免频繁的 IO 操作,提高流的传输效率。
字节缓冲流这里采用了装饰器模式来增强 InputStream 和OutputStream子类对象的功能。字符缓冲流同理。
随机访问流
RandomAccessFile 是 Java 中用于随机访问文件的类。它提供了对文件进行读取和写入的灵活性,可以在文件中随机定位并读取或写入数据。
以下是 RandomAccessFile 类常用的构造方法:
- RandomAccessFile(File file, String mode):根据给定的 File 对象和访问模式创建一个 RandomAccessFile 对象。
- RandomAccessFile(String fileName, String mode):根据给定的文件名和访问模式创建一个 RandomAccessFile 对象。
我们重点介绍输入参数 mode(读写模式)。根据源码中的注释,读写模式共四种:
- "r"(只读模式):
- 以只读方式打开文件。
- 在这种模式下,你只能读取文件中的数据,不能修改或写入数据。
- 如果文件不存在,则会抛出 FileNotFoundException 异常。
- "rw"(读写模式):
- 以读写方式打开文件。
- 在这种模式下,你既可以读取文件中的数据,也可以修改或写入数据。
- 如果文件不存在,则会尝试创建新文件。
- "rws"(同步读写模式):
- 以读写方式打开文件,并在每次写入操作完成后,立即将数据同步写入磁盘。
- 这种模式下,每次写入操作都会导致相关的元数据(如文件大小、修改时间等)被写入磁盘。
- 这可以确保数据的持久性,但可能会对性能产生一定的影响。
- "rwd"(同步写模式):
- 以读写方式打开文件,并在每次写入操作完成后,立即将数据同步写入磁盘的文件内容部分(不包括元数据)。
- 这种模式下,只有数据部分被同步写入磁盘,而元数据可能在稍后的时间被写入。
- 相对于 "rws" 模式,"rwd" 模式具有更好的性能,但在某些情况下可能会导致文件内容和元数据不一致。
元数据指的是描述数据的数据,即关于数据的数据。它提供了关于数据的属性、特征、结构和其他相关信息的描述,帮助我们理解和管理数据。
RandomAccessFile 类提供了一些常用的方法,如:
void seek(long pos)
:将文件指针定位到指定位置。long getFilePointer()
:获取当前文件指针的位置。int read()
:从当前文件指针位置读取一个字节的数据。int read(byte[] buffer, int offset, int length)
:从当前文件指针位置读取字节数组中指定长度的数据。void write(int b)
:将一个字节的数据写入文件的当前位置。void write(byte[] buffer, int offset, int length)
:将字节数组中指定长度的数据写入文件的当前位置。
文件指针
RandomAccessFile 中有一个文件指针用于表示下一个将要被写入或者读取的字节所处的位置。 我们可以通过 seek(long pos) 设置文件指针的偏移量。 如果想要获取文件指针当前位置的话,可以使用 getFilePointer() 方法。
案例之实现断点上传
java
import java.io.*;
public class FileDownloader {
public static void main(String[] args) {
String sourceUrl = "http://example.com/largefile.bin"; // 源文件 URL
String destinationPath = "C:/destination/largefile.bin"; // 目标文件路径
int bufferSize = 1024; // 缓冲区大小
try {
URL url = new URL(sourceUrl);
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
// 检查是否支持断点续传
boolean supportResume = connection.getHeaderField("Accept-Ranges").equals("bytes");
if (supportResume) {
// 获取已下载的文件大小
File file = new File(destinationPath);
long downloadedSize = 0;
if (file.exists()) {
downloadedSize = file.length();
connection.setRequestProperty("Range", "bytes=" + downloadedSize + "-");
}
connection.connect();
// 获取文件总大小
long fileSize = connection.getContentLength();
long totalSize = downloadedSize + fileSize;
System.out.println("File size: " + totalSize);
// 创建 RandomAccessFile 对象
RandomAccessFile randomAccessFile = new RandomAccessFile(destinationPath, "rw");
// 设置文件大小
randomAccessFile.setLength(totalSize);
// 定位文件指针到已下载的位置
randomAccessFile.seek(downloadedSize);
// 创建输入流
InputStream inputStream = connection.getInputStream();
byte[] buffer = new byte[bufferSize];
int bytesRead;
// 读取数据并写入文件
while ((bytesRead = inputStream.read(buffer)) != -1) {
randomAccessFile.write(buffer, 0, bytesRead);
}
// 关闭流
inputStream.close();
randomAccessFile.close();
System.out.println("Download completed!");
} else {
System.out.println("断点续传不受支持。");
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
IO源码解析
Reader的实现-适配器模式的使用
对于字节流: io中对于字节流的实现(InputStream)是比较容易理清楚的,他的read方法实现是放在InputStream的子类中的,例如FileInputStream便实现了,他的方式是通过调用native方法来实现的。
而Reader的结构更加复杂一点,通过阅读源码可以发现,Reader下的子实现类,例如FileReader,他们并不是直接继承或实现Reader接口,而是通过继承 InputStreamReader
来实现。
如果你阅读过他们的源码就会发现,与 字节流 的实现不同,Reader对于read方法的实现,是统一由InputStreamReader来实现的,大部分 InputStreamReader 的子类的实现并不对read方法进行override。
因此我们聚焦到 InputStreamReader 这个类来。
InputStreamReader 和 OutputStreamWriter 就是两个适配器(Adapter), 同时,它们两个也是字节流和字符流之间的桥梁。
两者核心理念相同,所以这里仅用InputStreamReader举例:
在 InputStreamReader 中使用 StreamDecoder
(流解码器)对字节进行解码,实现字节流到字符流的转换
这其中就涉及到适配器模式的使用,
背景:计算机系统存储文件是用二进制字节的形式存储的,jvm也只为我们提供了操作字节的native方法,所以,使用java实现读取字符,本质上是先读取字节,再通过解码生成字符。
因此,对于Reader的read实现来说,就必然会存在,调用inputstream去读取字节,再进行解码,生成字符这个流程。
还记得我上文概述说的适配器模式适用的情况吗?这里以此作为例子再说一遍。
现有系统拥有的是读取字节流的能力(InputStream),对接第三方接口需要传递一个读取字符流功能的能力,第三方接口给了我们一个Reader类,定义了规范,需要我们自己去实现。
因此我们定义类A继承Reader类,在类A中聚合InputStream,在重写的read方法中,先调用InputStream的read方法,再加上解码逻辑,这样就实现了Reader要求的读取字符流,这便是适配器模式。
而这个类A在java中的实现便是 StreamDecoder
。具体代码感兴趣可从FileReader一层一层往上看。
而InputStreamReader通过聚合StreamDecoder来实现对Reader接口的适配。
FilterInputStream的实现-装饰者模式的使用
说到 FilterInputStream
大家可能不能意识到这是什么,但是说 BufferedInputStream
大家一定能知道。
BufferedInputStream 是io包下使用非常频繁的缓冲流,能加快io速度。
在java的io包下,像这样的为io类增加新功能,但是又不影响使用的类有很多:
- PushbackInputStream:允许将读取的字节退回到输入流中,以便稍后再次读取。它提供了 unread() 方法来实现这个功能。
- LineNumberInputStream:可以对输入流进行逐行编号,它提供了 getLineNumber() 方法来获取当前行号。
- ObjectInputStream:用于从输入流中读取对象。它提供了反序列化的功能,可以将字节流转换为对象。
- SequenceInputStream:可以将多个输入流组合成一个逻辑上的连续输入流。它按照它们被传递给构造函数的顺序依次读取这些输入流。
而这些类都是继承于FilterInputStream,我们点进FilterInputStream中去看,他继承了InputStream,又聚合了InputStream,在重写的方法中都是调用in中的函数,这便是经典的装饰者模式。
Java IO模型
IO 模型一共有 5 种:
- 同步阻塞(Synchronous Blocking):
- 在同步阻塞模型中,应用程序执行 I/O 操作时会被阻塞,直到操作完成。
- 这意味着应用程序在等待 I/O 操作完成期间无法执行其他任务。
- I/O 同步非阻塞(Synchronous Non-blocking):
- 在这种模型中,应用程序执行 I/O 操作时不会被阻塞,但它需要主动轮询(poll)或查询(query)操作的状态,以确定操作是否完成。
- 这样可以避免阻塞,但应用程序仍需要主动等待操作完成。
- I/O 多路复用(I/O Multiplexing):
- I/O 多路复用模型使用单个线程来同时处理多个 I/O 操作。
- 它通过使用特定的系统调用(如 select、poll 或 epoll)来监视多个 I/O 事件,并在有可用事件时进行处理。
- 这种模型可以提高系统的并发性能。
- 信号驱动 I/O(Signal-driven I/O):
- 在信号驱动 I/O 模型中,应用程序通过注册信号处理函数来指定在 I/O 操作完成时接收通知。
- 当 I/O 操作完成时,操作系统会发送一个信号给应用程序,应用程序可以捕获该信号并执行相应的处理逻辑。
- 异步 I/O(Asynchronous I/O):
- 在异步 I/O 模型中,应用程序发起 I/O 操作后就可以继续执行其他任务,而无需等待操作完成。
- 当操作完成时,操作系统会通知应用程序,应用程序可以处理已完成的 I/O 操作。
- 异步 I/O 模型通常使用回调函数或者事件循环来处理已完成的操作。
java中有三种io模型
- BIO
- BIO又称为同步阻塞IO
- 同步阻塞 IO 模型中,应用程序发起 read 调用后,会一直阻塞,直到内核把数据拷贝到用户空间。
- NIO
- Java 中的 NIO 可以看作是 I/O 多路复用模型。
- AIO
- AIO 也就是 NIO 2。Java 7 中引入了 NIO 的改进版 NIO 2,它是异步 IO 模型。