何为Java流
Java 中的流(Stream) 是用于在程序中读取或写入数据的抽象概念。流可以从不同的数据源(输入流)读取数据,也可以将数据写入到不同的目标(输出流)。流提供了一种统一的方式来处理不同类型的数据,例如文件、网络数据、内存数据等。
Java IO 流
在 Java 中,流分为输入流(InputStream) 和 输出流(OutputStream)。输入流用于从数据源读取数据,而输出流用于将数据写入到目标。
IO 即 Input/Output
,输入和输出。数据输入到计算机内存的过程即输入,反之输出到外部存储(比如数据库,文件,远程主机)的过程即输出。数据传输过程类似于水流,因此称为 IO 流。IO 流在 Java 中分为输入流和输出流,而根据数据的处理方式又分为字节流和字符流。
如果音频文件、图片等媒体文件用字节流比较好,如果涉及到字符的话使用字符流比较好。
Java IO 流的 40 多个类都是从如下 4 个抽象类基类中派生出来的。
-
InputStream
/Reader
: 所有的输入流的基类,前者是字节输入流,后者是字符输入流 -
OutputStream
/Writer
: 所有输出流的基类,前者是字节输出流,后者是字符输出流
字节操作
Java提供了两个抽象类来表示字节流输入输出:InputStream
和OutputStream
,二者不能直接实例化,通过其子类来创建实例
字节流 (Byte Stream)
字节流以字节为单位进行输入和输出的操作,主要用于处理二进制数据。它以字节为单位进行读取和写入操作,适用于处理二进制文件、图片、音频、视频等非文本数据。
InputStream 字节输入流
InputStream是所有字节输入流类的超类,它定义了读取字节的基本方法
用于从源头(通常是文件)读取数据(字节信息)到内存中
常用方法
InputStream
常用方法:
-
read()
:返回输入流中下一个字节的数据。返回的值介于 0 到 255 之间。如果未读取任何字节,则代码返回-1
,表示文件结束。 -
read(byte b[ ])
: 从输入流中读取一些字节存储到数组b
中。如果数组b
的长度为零,则不读取。如果没有可用字节读取,返回-1
。
如果有可用字节读取,则最多读取的字节数最多等于b.length
, 返回读取的字节数。这个方法等价于read(b, 0, b.length)
。 -
read(byte b[], int off, int len)
:在read(byte b[ ])
方法的基础上增加了off
参数(偏移量)和len
参数(要读取的最大字节数)。 -
skip(long n)
:忽略输入流中的 n 个字节 ,返回实际忽略的字节数。 -
available()
:返回输入流中可以读取的字节数。 -
close()
:关闭输入流释放相关的系统资源。
从 Java 9 开始,InputStream
新增加了多个实用的方法:
-
readAllBytes()
:读取输入流中的所有字节,返回字节数组。 -
readNBytes(byte[] b, int off, int len)
:阻塞直到读取len
个字节。 -
transferTo(OutputStream out)
:将所有字节从一个输入流传递到一个输出流。
常见实现子类
DataInputStream
DataInputStream:用于读取基本数据类型的二进制表示,不能单独使用,必须结合其它流,比如 FileInputStream
:
构造方法
-
FileInputStream(String name)
:创建一个FileInputStream
对象,并打开指定名称的文件进行读取。文件名由 name 参数指定。如果文件不存在,将会抛出FileNotFoundException
异常。 -
FileInputStream(File file)
:创建一个FileInputStream
对象,并打开指定的 File 对象表示的文件进行读取。
FileInputStream
FileInputStream
:可直接指定文件路径,可以直接读取单字节数据,也可以读取至字节数组中。
一般不会直接单独使用 FileInputStream
,通常会配合 BufferedInputStream
字节缓冲输入流来使用
例如,通过 readAllBytes()
读取输入流所有字节并将其直接赋值给一个 String 对象:
ByteArrayInputStream
ByteArrayInputStream
:用于从内存中读取字节数据。它的数据源是一个内存中的字节数组。将一个字节数组作为输入流,然后按字节顺序读取数组中的数据
提供了一系列用于读取字节的方法,如 read()
、read(byte[] buffer)
、read(byte[] buffer, int offset, int length)
,可以读取单个字节、一组字节,或者指定长度的字节块
下面的示例使用了 ByteArrayInputStream
逐字节读取字节数组中的数据,并转换为字符并打印出来
import java.io.ByteArrayInputStream;
import java.io.IOException;
public class ByteArrayInputStreamExample {
public static void main(String[] args) {
// 创建一个字节数组
byte[] data = "Hello, World!".getBytes();
// 创建 ByteArrayInputStream 对象
ByteArrayInputStream bis = new ByteArrayInputStream(data);
// 读取数据
try {
int b;
while ((b = bis.read()) != -1) {
System.out.print((char) b);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
// ByteArrayInputStream 不需要关闭,因为它不会占用系统资源
}
}
}
OutputStream 字节输出流
OutputStream是所有字节输出流类的超类,它定义了写入字节的基本方法
用于将数据(字节信息)写入到目的地(通常是文件)
常用方法
write(int b)
:将特定字节写入输出流。
当使用
write(int b)
方法写出一个字节时,参数 b 表示要写出的字节的整数值。由于一个字节只有8位,因此参数 b 的取值范围应该在 0 到 255 之间,超出这个范围的值将会被截断。例如,如果参数 b 的值为 -1,那么它会被截断为 255,如果参数 b 的值为 256,那么它会被截断为 0。
-
write(byte b[ ])
: 将数组b
写入到输出流,等价于write(b, 0, b.length)
。 -
write(byte[] b, int off, int len)
: 在write(byte b[ ])
方法的基础上增加了off
参数(偏移量)和len
参数(要读取的最大字节数)。 -
flush()
:刷新此输出流并强制写出所有缓冲的输出字节。 -
close()
:关闭输出流释放相关的系统资源。
FileOutputStream
是最常用的字节输出流对象,可直接指定文件路径,可以直接输出单字节数据,也可以输出指定的字节数组。
常见实现子类
FileOutputStream
FileOutputStream 是最常用的字节输出流对象,可直接指定文件路径,可以直接输出单字节数据,也可以输出指定的字节数组。
类似于 FileInputStream
,FileOutputStream
通常也会配合 BufferedOutputStream
(字节缓冲输出流)
构造方法
- 使用文件名创建
FileOutputStream fos = new FileOutputStream(fileName);
如果文件不存在,则创建一个新文件;如果文件已经存在,则覆盖原有文件。
- 使用文件对象创建
FileOutputStream fos = new FileOutputStream(file);
- 借助追加模式实现数据的追加
FileOutputStream fos = new FileOutputStream
(file/filename, true/false);
如果文件不存在,则创建一个新文件;如果文件已经存在,则在文件末尾追加数据。
第二个参数中都需要传入一个boolean类型的值,true
表示追加数据,false
表示不追加也就是清空原有数据。
若要实现追加数据的换行,需要在追加的内容字符串中添加 \r\n
(Windows)\n
(macOS)符号
DataOutputStream
DataOutputStream
(FilterOutputStream
的子类)用于写入指定类型数据,不能单独使用,必须结合其它流,比如 FileOutputStream
-
ByteArrayOutputStream
:用于向内存中的字节数组写入字节数据。除了基本的写入操作(
write()
)外,ByteArrayOutputStream还提供了其他一些方法,如:reset()
用于重置流,以便重新使用size()
用于获取当前写入的字节数toString()
用于将写入的字节数组转换为字符串
字节缓冲流
IO 操作是很消耗性能的,缓冲流将数据加载至缓冲区,一次性读取/写入多个字节,从而避免频繁的 IO 操作,提高流的传输效率。
字节缓冲流这里采用了 Java IO 设计模式中的装饰器模式来增强 InputStream
和OutputStream
子类对象的功能。
字节流和字节缓冲流的性能差别主要体现在使用两者的时候都是调用 write(int b)
和 read()
这两个一次只读取一个字节的方法的时候。
由于字节缓冲流内部有缓冲区(字节数组),因此,字节缓冲流会先将读取到的字节存放在缓存区,大幅减少 IO 次数,提高读取效率。
性能差别
使用 write(int b)
和 read()
方法,分别通过字节流和字节缓冲流复制一个 524.9 mb 的 PDF 文件耗时对比如下:
使用缓冲流复制PDF文件总耗时: 15428 毫秒
使用普通字节流复制PDF文件总耗时: 2555062 毫秒
两者耗时差别非常大,缓冲流耗费的时间是字节流的 1/165。
如果是调用 read(byte b[])
和 write(byte b[], int off, int len)
这两个写入一个字节数组的方法的话,只要字节数组的大小合适,两者的性能差距其实不大,基本可以忽略。
使用 read(byte b[])
和 write(byte b[], int off, int len)
方法,分别通过字节流和字节缓冲流复制一个 524.9 mb 的 PDF 文件耗时对比如下:
使用缓冲流复制PDF文件总耗时: 695 毫秒
使用普通字节流复制PDF文件总耗时: 989 毫秒
两者耗时差别不是很大,缓冲流的性能要略微好一点点。
BufferInputStream 字节缓冲输入流
BufferedInputStream
从源头(通常是文件)读取数据(字节信息)到内存的过程中不会一个字节一个字节的读取,而是会先将读取到的字节存放在缓存区,并从内部缓冲区中单独读取字节。这样大幅减少了 IO 次数,提高了读取效率。
BufferedInputStream
内部维护了一个缓冲区,这个缓冲区实际就是一个字节数组
缓冲区的大小默认为 8192 字节,也可以通过构造方法BufferedInputStream(InputStream in, int size)
来指定缓冲区的大小。
BufferInputStream 使用示例
import java.io.BufferedInputStream;
import java.io.FileInputStream;
import java.io.IOException;
public class BufferedInputStreamExample {
public static void main(String[] args) {
// 指定文件路径
String filePath = "example.txt";
try (FileInputStream fis = new FileInputStream(filePath);
BufferedInputStream bis = new BufferedInputStream(fis)) {
int b;
while ((b = bis.read()) != -1) {
System.out.print((char) b);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
-
创建
FileInputStream
:创建了一个FileInputStream
对象fis
,指向文件example.txt
。 -
创建
BufferedInputStream
:使用fis
作为参数创建了一个BufferedInputStream
对象bis
。 -
读取数据:通过调用
bis.read()
方法逐字节读取文件内容。如果read()
返回-1
,表示已到达文件末尾。 -
打印数据:将读取到的字节转换为字符并打印出来
BufferOutputStream 字节缓冲输出流
BufferedOutputStream
将数据(字节信息)写入到目的地(通常是文件)的过程中不会一个字节一个字节的写入,而是会先将要写入的字节存放在缓存区,并从内部缓冲区中单独写入字节。这样大幅减少了 IO 次数,提高了读取效率
BufferedOutputStream
内部也维护了一个缓冲区,并且,这个缓存区的大小也是 8192 字节。也可以通过构造方法
BufferedOutputStream(OutputStream in, int size)
来指定缓冲区的大小。
BufferOutputStream 使用示例
import java.io.BufferedOutputStream;
import java.io.FileOutputStream;
import java.io.IOException;
public class BufferedOutputStreamExample {
public static void main(String[] args) {
// 指定文件路径
String filePath = "example.txt";
String content = "Hello, World!";
try (FileOutputStream fos = new FileOutputStream(filePath);
BufferedOutputStream bos = new BufferedOutputStream(fos)) {
// 将字符串转换为字节数组并写入缓冲区
bos.write(content.getBytes());
} catch (IOException e) {
e.printStackTrace();
}
}
}
代码说明:
-
创建
FileOutputStream
:创建了一个FileOutputStream
对象fos
,指向文件example.txt
。 -
创建
BufferedOutputStream
:使用fos
作为参数创建了一个BufferedOutputStream
对象bos
。 -
写入数据:将字符串
content
转换为字节数组,并通过bos.write()
方法写入缓冲区。 -
自动刷新:由于使用了
try-with-resources
语句,BufferedOutputStream
会自动关闭并刷新缓冲区,将所有数据写入文件。
随机访问流
随机访问流指的是支持随意跳转到文件的任意位置进行读写的 RandomAccessFile
RandomAccessFile
的构造方法如下,可以指定 mode
(读写模式)。
// openAndDelete 参数默认为 false 表示打开文件并且这个文件不会被删除
public RandomAccessFile(File file, String mode)
throws FileNotFoundException {
this(file, mode, false);
}
// 私有方法
private RandomAccessFile(File file, String mode, boolean openAndDelete) throws FileNotFoundException{
// 省略大部分代码
}
读写模式主要有下面四种:
r
: 只读模式。rw
: 读写模式rws
: 相对于rw
,rws
同步更新对“文件的内容”或“元数据”的修改到外部存储设备。rwd
: 相对于rw
,rwd
同步更新对“文件的内容”的修改到外部存储设备。
文件内容指的是文件中实际保存的数据,元数据则是用来描述文件属性比如文件的大小信息、创建和修改时间。
RandomAccessFile
中有一个文件指针用来表示下一个将要被写入或者读取的字节所处的位置。我们可以通过 RandomAccessFile
的 seek(long pos)
方法来设置文件指针的偏移量(距文件开头 pos
个字节处)。如果想要获取文件指针当前的位置的话,可以使用 getFilePointer()
方法。
实际应用
RandomAccessFile
比较常见的一个应用就是实现大文件的 断点续传 。
简单来说就是上传文件中途暂停或失败(比如遇到网络问题)之后,不需要重新上传,只需要上传那些未成功上传的文件分片即可。分片(先将文件切分成多个文件分片)上传是断点续传的基础。
RandomAccessFile
可以帮助我们合并文件分片,示例代码如下:
字节流的对象操作
ObjectInputStream
用于从输入流中读取 Java 对象,反序列化
ObjectOutputStream
用于将 Java 对象写入到输出流,序列化