1. IO流简介
IO即input/output
,输入和输出。这里的"入"与"出"是以计算机内存的角度来看的。数据从外部到计算机内存的过程就是输入,反之由计算机内存到外部存储(如数据库、文件、网络连接)的过程就是输出。
数据在传输的过程类似于水流,所以我们称之为IO流。IO流在Java中分为输入流和输出流,而根据数据的处理方式又分为字节流和字符流。
来看一张关于Java常用IO流的体系图,好对整个框架有个整体印象:
看着很复杂,但我们在实际运用中常用的就那么几个。
本篇博客将为大家树立一个基本概念,后面的博客我们将针对常用的流进行实战。
2. 字节流&字符流
数据的处理方式有两种:字节和字符。我们首先需要搞懂它们两者的区别。
字节(byte)是计算机中表示存储容量的一个计量单位,通常情况下,一个字节等于8位,即 1 byte = 8 bit。
字符(char)则是计算机中使用的字母、汉字、数字、符号等,比如说"A"、“我”、“1”、“¥”。通常情况下,一个英文字母、数字和符号都是占一个字节,而一个汉字是占两个字节(在utf-8编码下,汉字占3个字节)。
关于字符的编码问题,知道上面说的就不影响对于本篇博客的理解。如果想要深入了解一下的,可以看我的另一篇博客【字符串编码全攻略(含Java代码实战),从此告别"锟斤拷"】,这里就不展开了。
明白了字节和字符的区别,我们就可以来看字节流和字符流了。
字节流可以用来处理所有二进制文件,比如说图片、mp3音乐、视频等。
字符流可以用来处理文本文件,文本文件可以看作是一种特殊的二进制文件(经过字符编码,便于人们阅读)。
简单来说就是,字节流可以处理一切文件,而字符流只能处理文本。
根据本篇博客最开始的那个总览图看,我们可以发现,IO流中最核心的就是4个抽象类:属于字节流的InputStream、OutputStream,和属于字符流的Reader、Writer。
对于IO流,最核心的方法就是:read和write。
InputStream类
int read()
:读取数据;int read(byte b[], int off, int len)
:从第 off 位置开始读,读取 len 长度的字节,然后放入数组 b 中;long skip(long n)
:跳过指定个数的字节;int available()
:返回可读的字节数;void close()
:关闭流,释放资源;
OutputStream 类
void write(int b)
: 写入一个字节,虽然参数是一个 int 类型,但只有低 8 位才会写入,高 24 位会舍弃(这块后面再讲);void write(byte b[], int off, int len)
: 将数组 b 中的从 off 位置开始,长度为 len 的字节写入;void flush()
: 强制刷新,将缓冲区的数据写入;void close()
:关闭流;
Reader 类
int read()
:读取单个字符;int read(char cbuf[], int off, int len)
:从第 off 位置开始读,读取 len 长度的字符,然后放入数组 b 中;long skip(long n)
:跳过指定个数的字符;int ready()
:是否可以读了;void close()
:关闭流,释放资源;
Writer 类
void write(int c)
: 写入一个字符;void write(char cbuf[], int off, int len)
: 将数组 cbuf 中的从 off 位置开始,长度为 len 的字符写入;void flush()
: 强制刷新,将缓冲区的数据写入;void close()
:关闭流;
字节流和字符流的区别:
- 字节流一般用于处理图像、视频、音频等类型的文件。而字符流一般用于处理纯文本文件。
- 字节流本身没有缓冲区,缓冲字节流(
BufferedInputStream/BufferedOutputStream
)对字节流的效率提升比较高。而字符流自带缓冲区,缓冲字符流(BufferedReader/BufferedWriter
)对字符流的效率提升就没那么高了。
3. IO流的操作对象
让我们再回顾一下IO:
- Input:将外部的数据读入内存,比如说把文件从硬盘读取到内存,从网络读取数据到内存等等
- Output:将内存中的数据写入到外部,比如说把数据从内存写入到文件,把数据从内存输出到网络等等。
所有的程序,在执行的时候,都是在内存上进行的,一旦关机,内存中的数据就没了,那如果想要持久化,就需要把内存中的数据输出到外部。
IO流可操作的对象有很多,如:文件、数组、管道、基本数据类型、缓冲、打印、对象序列化/反序列化,以及转换等。
(1)文件
文件流也就是直接操作文件的流,可以细分为字节流(FileInputStream 和 FileOuputStream)和字符流(FileReader 和 FileWriter)。
除了正常的读取文件内容,文件流还可以用于创建、删除、重命名文件等操作。FileOutputStream 和 FileWriter 构造函数的第二个参数可以指定是否追加数据到文件末尾。
(2)数组
通常来说,针对文件的读写操作,使用文件流配合缓冲流就够用了,但为了提升效率,频繁地读写文件并不是太好,那么就出现了数组流,有时候也称为内存流(ByteArrayInputStream、ByteArrayOutputStream)。
不过实际上,数组流(内存流)不太常用,不然我们最开始的体系框架图上也不会没有了。
数组流可以用于在内存中读写数据,比如将数据存储在字节数组中进行压缩、加密、序列化等操作。它的优点是不需要创建临时文件,可以提高程序的效率。但是,数组流也有缺点,它只能存储有限的数据量,如果存储的数据量过大,会导致内存溢出。
(3)管道
Java 中的管道和 Unix/Linux 中的管道不同,在 Unix/Linux 中,不同的进程之间可以通过管道来通信,但 Java 中,通信的双方必须在同一个进程中,也就是在同一个 JVM 中,管道为线程之间的通信提供了通信能力。一个线程通过 PipedOutputStream 写入的数据可以被另外一个线程通过相关联的 PipedInputStream 读取出来。
使用管道流可以实现不同线程之间的数据传输,可以用于线程间的通信、数据的传递等。但是,管道流也有一些局限性,比如只能在同一个 JVM 中的线程之间使用,不能跨越不同的 JVM 进程。
(4)基本数据类型
基本数据类型输入输出流(DataInputStream/DataOutputStream)是一个字节流,该流不仅可以读写字节和字符,还可以读写基本数据类型。
DataInputStream 提供了一系列可以读基本数据类型的方法,DataOutputStream 提供了一系列可以写基本数据类型的方法。
除了 DataInputStream 和 DataOutputStream,Java IO 还提供了其他一些读写基本数据类型和字符串的流类,包括 ObjectInputStream 和 ObjectOutputStream(用于读写对象)。
(5)缓冲
CPU 很快,它比内存快 100 倍,比磁盘快百万倍。那也就意味着,程序和内存交互会很快,和硬盘交互相对就很慢,这样就会导致性能问题。
为了减少程序和硬盘的交互,提升程序的效率,就引入了缓冲流,也就是类名前缀带有 Buffer 的那些,比如说 BufferedInputStream、BufferedOutputStream、BufferedReader、BufferedWriter。
缓冲流在内存中设置了一个缓冲区,只有缓冲区存储了足够多的带操作的数据后,才会和内存或者硬盘进行交互。简单来说,就是一次多读/写点,少读/写几次,这样程序的性能就会提高。
使用缓冲流可以提高读写效率,减少了频繁的读写磁盘或网络的次数,从而提高了程序的性能。但是,在使用缓冲流时需要注意缓冲区的大小和清空缓冲区的时机,以避免数据丢失或不完整的问题。
(6)打印
Java 的打印流是一组用于打印输出数据的类,包括 PrintStream 和 PrintWriter 两个类。
恐怕 Java 程序员一生当中最常用的就是打印流了:System.out 其实返回的就是一个 PrintStream 对象,可以用来打印各式各样的对象。
(7)对象序列化/反序列化
序列化本质上是将一个 Java 对象转成字节数组,然后可以将其保存到文件中,或者通过网络传输到远程。与其对应的,有序列化,就有反序列化,也就是再将字节数组转成 Java 对象的过程。
(8)转换
InputStreamReader 是从字节流到字符流的桥连接,它使用指定的字符集读取字节并将它们解码为字符。OutputStreamWriter 将一个字符流的输出对象变为字节流的输出对象,是字符流通向字节流的桥梁。
使用转换流可以方便地在字节流和字符流之间进行转换。在进行文本文件读写时,通常使用字符流进行操作,而在进行网络传输或与设备进行通信时,通常使用字节流进行操作。
另外,在使用转换流时需要注意字符编码的问题。如果不指定字符编码,则使用默认的字符编码,可能会出现乱码问题。因此,建议在使用转换流时,始终指定正确的字符编码,以避免出现乱码问题。