Java IO流详解:像流水一样读写数据
在Java编程世界中,IO流就像水流一样,不断地在内存和外部存储之间搬运数据。这些数据流可以是字节,也可以是字符。不管是文件读写、网络传输,还是数据处理,IO流总是无处不在。下面我们就来揭开IO流的面纱,看看它是如何工作的,以及在实际开发中有哪些应用场景。
IO流到底是什么?
简单来说,IO流(Input/Output Stream)是处理数据输入和输出的工具。无论是文件、网络,还是其他设备,数据的流动都可以通过IO流实现。IO流就像一条条“管道”,将数据从源头流向目的地。你可以想象一下,当你要从文件中读取数据,或者将数据写入文件时,这些操作就像是在往一个水管里倒水,或者从水管中取水。
IO流的分类
Java中的IO流可以按数据类型和方向分为两大类:字节流和字符流。
1. 字节流
字节流处理的是原始的二进制数据(0和1),适用于处理非文本数据,如图片、音频、视频等。字节流分为字节输入流和字节输出流。
- 字节输入流 (
InputStream
):从外部(文件、网络等)读取字节数据到内存中。 - 字节输出流 (
OutputStream
):将内存中的数据以字节形式输出到外部设备(文件、网络等)。
2. 字符流
字符流专门处理文本数据,也就是我们平时使用的字符、字符串等。字符流能够自动处理字符编码问题,避免字节流处理中文时容易出现的乱码问题。字符流分为字符输入流和字符输出流。
- 字符输入流 (
Reader
):从外部读取字符数据到内存中。 - 字符输出流 (
Writer
):将内存中的字符数据写出到外部设备。
IO流的应用场景
IO流广泛应用于各类程序中,以下是几个典型的应用场景:
- 文件读写:将文件中的数据读取到程序内存,或者将程序数据保存到文件中。
- 网络通信:通过字节流或字符流来进行网络数据的传输,比如上传文件或接收服务器响应。
- 数据流处理:处理大数据文件时,利用IO流可以按需读取或写入数据,避免内存占用过大。
字节输入流与输出流的常见使用方式
文件字节输入流:FileInputStream
FileInputStream
可以将文件中的数据以字节为单位读取到内存中,常用于读取非文本文件,如图片、视频等。
常用构造方法:
public FileInputStream(File file)
public FileInputStream(String pathname)
常用读取方法:
public int read()
:每次读取一个字节,当没有数据可读时返回-1
。public int read(byte[] buffer)
:将数据读取到字节数组中,返回读取的字节数。
为什么每次读取一个字节效率低?
想象一下,你要从一大桶水里一杯一杯地舀水,这样会非常耗时。同样地,每次读取一个字节效率非常低,特别是当处理大文件时,程序的运行速度会显著下降。
优化:一次读取多个字节
为了提高读取效率,我们可以一次读取多个字节,就像用一个大桶取水,性能明显提升。不过,如果处理中文字符,字节流可能会出现乱码,因为中文字符占用多个字节,而字节流不区分字符边界。
一次性读取所有字节
你还可以一次性将整个文件的数据读入内存,通过定义一个与文件大小相同的字节数组,或者使用Java 9引入的readAllBytes()
方法。这样就避免了频繁的磁盘读取操作。
public byte[] readAllBytes() throws IOException
注意:如果文件太大,直接读入内存可能导致内存溢出,就像你用太小的水桶去接一场大暴雨一样,装不下的水自然会溢出来。
readAllBytes()
是 Java 9 引入的方法,属于 InputStream
类,用于一次性将输入流中的所有字节读取到一个字节数组中。它解决了之前版本中必须手动循环读取数据的繁琐操作,是对输入流读取操作的一个便捷优化。
该方法的签名是:
public byte[] readAllBytes() throws IOException
在 Java 9 之前的版本如何实现类似功能?
在 Java 9 之前,没有 readAllBytes()
方法,要实现类似功能通常需要手动编写代码,使用循环来读取数据并存入字节数组。例如:
public static byte[] readAllBytesPreJava9(InputStream is) throws IOException {
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
byte[] data = new byte[1024];
int nRead;
while ((nRead = is.read(data, 0, data.length)) != -1) {
buffer.write(data, 0, nRead);
}
buffer.flush();
return buffer.toByteArray();
}
在 Java 9 之后,readAllBytes()
让代码更加简洁,适合一次性读取小型文件或网络数据流。不过,对于大文件的处理,手动分批读取数据仍然更为合适。
小结
readAllBytes()
是 Java 9 之后新增的一个实用方法,用于简化从输入流中读取所有字节数据的操作。
文件字节输出流:FileOutputStream
FileOutputStream
用于将内存中的数据以字节形式写入文件。就像我们在文件中存储图片、视频时,数据实际上是以字节流的形式写入磁盘的。
常用的写入方法:
public void write(int b)
:写入一个字节。public void write(byte[] buffer)
:写入一个字节数组。public void write(byte[] buffer, int offset, int length)
:写入字节数组的一部分。
文件复制示例
现在我们来看看一个实际的例子——文件复制。你可以想象这个操作就像用水管将一桶水从一个水缸倒入另一个水缸,全部字节不漏地传输。
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
public class FileCopy {
public static void main(String[] args) {
try (FileInputStream fis = new FileInputStream("source.txt");
FileOutputStream fos = new FileOutputStream("destination.txt")) {
byte[] buffer = new byte[1024];
int length;
while ((length = fis.read(buffer)) != -1) {
fos.write(buffer, 0, length);
}
System.out.println("文件复制成功!");
} catch (IOException e) {
e.printStackTrace();
}
}
}
在这个例子中,我们通过FileInputStream
读取文件,再通过FileOutputStream
写入另一个文件,完成了文件的复制操作。
如何避免中文读取乱码?
使用字节流直接读取中文时会出现乱码,因为中文字符占用多个字节,而字节流并不识别字符的边界。为了解决这个问题,字符流可以派上用场。字符流可以根据指定的编码方式读取和写入字符数据。
例如,使用InputStreamReader
结合FileInputStream
可以避免中文乱码:
import java.io.*;
public class ChineseFileReader {
public static void main(String[] args) {
try (InputStreamReader isr = new InputStreamReader(new FileInputStream("chinese.txt"), "UTF-8");
BufferedReader br = new BufferedReader(isr)) {
String line;
while ((line = br.readLine()) != null) {
System.out.println(line);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
在这个示例中,InputStreamReader
根据指定的字符集(如UTF-8
)正确地解码字节数据,避免了乱码问题。
文件字符输入流:FileReader
FileReader
是文件字符输入流,用于从文件中读取字符数据。常用于读取文本文件。
构造器:
public FileReader(File file)
:将字符输入流与源文件接通。public FileReader(String pathname)
:将字符输入流与源文件路径接通。
常用方法:
public int read()
:每次读取一个字符,如果没有数据返回-1
。public int read(char[] buffer)
:每次将字符数据读入字符数组,返回读取的字符数。
优点
使用FileReader
读取文本文件时,可以避免字节流处理中文时的乱码问题,因为FileReader
直接处理字符数据。
读取方式
- 每次读取一个字符:适合处理小文件,操作简单,但性能较低。
- 每次读取多个字符:通过字符数组一次读取多个字符,性能提高。
文件字符输出流:FileWriter
FileWriter
是文件字符输出流,主要用于将字符数据写入文件。适合写入文本文件中的字符数据。
构造器:
public FileWriter(File file)
:将字符输出流与源文件接通。public FileWriter(String filepath)
:将字符输出流与文件路径接通。public FileWriter(File file, boolean append)
:将字符输出流与源文件接通,支持追加写入。
常用方法:
public void write(int c)
:写一个字符。public void write(String str)
:写一个字符串。public void write(char[] cbuf)
:写一个字符数组。public void write(char[] cbuf, int off, int len)
:写一个字符数组的部分内容。
刷新流与关闭流
在使用文件字符输出流(FileWriter)时,写出的数据会暂时保存在内存的缓冲区中,如果不刷新流或关闭流,这些数据可能不会被立即写入文件。因此,写完数据后必须注意以下操作:
刷新流
public void flush()
:将缓冲区中积累的数据立即写入文件。一般在需要保证数据即时写入时使用。
关闭流
public void close()
:关闭流时,系统会自动调用flush()
,确保所有数据写入文件后关闭流。
字符缓冲流的作用
为了提高字符流的读写性能,Java 提供了字符缓冲流,它们可以通过一个8KB的缓冲区来加速字符数据的传输。以下是几种常用的缓冲流:
1. 字符缓冲输入流:BufferedReader
作用:BufferedReader
内置一个8KB缓冲区,可以加快从字符输入流读取字符数据的效率。相比直接使用Reader
,BufferedReader
读取时性能更高,还增加了按行读取的功能。
构造方法:
public BufferedReader(Reader r)
:将低级字符输入流包装为高级缓冲字符输入流。
常用方法:
public String readLine()
:按行读取字符数据,如果没有数据可读返回null
。
2. 字符缓冲输出流:BufferedWriter
作用:BufferedWriter
内置一个8KB的缓冲区,可以加快字符输出流的写入效率。与普通字符输出流不同,BufferedWriter
可以减少写操作的频率,并提供换行功能。
构造方法:
public BufferedWriter(Writer w)
:将低级字符输出流包装为高级缓冲字符输出流。
常用方法:
public void newLine()
:将换行符写入文件,实现换行。
字符缓冲流的使用场景
- 提高性能:缓冲流通过缓冲区减少了与磁盘的直接交互,极大提高了读写效率。
- 按行读取数据:
BufferedReader
提供了readLine()
方法,可以按行读取文件,适合处理文本文件。 - 换行写入数据:
BufferedWriter
提供了newLine()
方法,可以方便地在文件中写入换行符。
字节缓冲流:提高文件拷贝性能
字节缓冲流与字符缓冲流类似,也带有一个缓冲区,能够有效提高数据的读写性能。字节缓冲流适用于处理大文件(如视频、图片等)的拷贝操作。
1. 字节缓冲输入流:BufferedInputStream
作用:为字节输入流提供一个8KB的缓冲区,加快读取字节数据的速度。
public BufferedInputStream(InputStream is)
:将低级字节输入流包装成缓冲字节输入流。
2. 字节缓冲输出流:BufferedOutputStream
作用:为字节输出流提供一个8KB的缓冲区,加快写入字节数据的速度。
public BufferedOutputStream(OutputStream os)
:将低级字节输出流包装成缓冲字节输出流。
文件拷贝的优化
为了验证缓冲流的性能提升,我们可以进行文件复制的测试:
测试步骤:
- 原始字节流:每次读取一个字节,性能低下。
- 原始字节流 + 字节数组:一次读取多个字节,性能提升。
- 字节缓冲流:通过缓冲区加速读写,性能显著提高。
- 字节缓冲流 + 字节数组:结合缓冲流和字节数组,提供最佳性能。
不同编码导致的乱码问题
当文件的编码与程序使用的编码不一致时,字符流读取文本文件时可能会出现乱码。这是因为不同编码方式对字符的字节数和表示不同。
解决方案:InputStreamReader
InputStreamReader
可以将字节流按照指定的字符集编码转换为字符流,从而解决不同编码导致的乱码问题。
构造器:
public InputStreamReader(InputStream is, String charset)
:指定编码将字节流转换成字符流。
示例:
InputStreamReader isr = new InputStreamReader(new FileInputStream("file.txt"), "UTF-8");
字符输出转换流:OutputStreamWriter
在某些场景下,我们希望控制写入文件时使用的字符集编码。OutputStreamWriter
可以将字节输出流转换为字符输出流,并指定写入文件时使用的字符集编码。
构造器:
public OutputStreamWriter(OutputStream os, String charset)
:将字节输出流按照指定编码转换为字符输出流。
示例:
OutputStreamWriter osw = new OutputStreamWriter(new FileOutputStream("file.txt"), "UTF-8");
打印流:PrintStream
和 PrintWriter
打印流是一种特殊的输出流,能够方便地将各种数据类型打印到文件或其他输出设备。常见的打印流有两种:
1. PrintStream
PrintStream
继承自字节输出流OutputStream
,主要用于打印字节数据,但它也可以处理字符数据。
构造器:
public PrintStream(OutputStream out)
public PrintStream(String fileName, Charset charset)
:可以指定字符编码。
常用方法:
public void println(String str)
:打印字符串并换行。
2. PrintWriter
PrintWriter
继承自字符输出流Writer
,适用于打印字符数据。
构造器:
public PrintWriter(OutputStream out)
public PrintWriter(String fileName, Charset charset)
:可以指定字符编码。
常用方法:
public void println(String str)
:打印字符并换行。
打印流的应用场景:
- 控制台输出重定向:可以将
System.out.println
的输出重定向到文件中,保存日志信息。
示例:
PrintStream ps = new PrintStream("log.txt");
System.setOut(ps);
数据流:DataInputStream
和 DataOutputStream
数据流允许将基本数据类型(如int
、double
、byte
等)和对应的数据写入和读取文件,能够方便地进行二进制数据的读写。
1. DataOutputStream
用于将数据以二进制格式写入到字节输出流。
构造器:
public DataOutputStream(OutputStream out)
常用方法:
public void writeInt(int v)
:写入一个整数。public void writeDouble(double v)
:写入一个double
类型数据。
2. DataInputStream
用于读取由DataOutputStream
写入的二进制数据。
构造器:
public DataInputStream(InputStream in)
常用方法:
public int readInt()
:读取一个整数。public double readDouble()
:读取一个double
类型数据。
对象流:ObjectOutputStream
和 ObjectInputStream
Java对象可以通过对象流进行序列化和反序列化。对象序列化的过程是将对象转换为字节数据存储到文件中,反序列化则是将字节数据转换为对象。
1. 对象输出流:ObjectOutputStream
将Java对象写入文件。
构造器:
public ObjectOutputStream(OutputStream out)
常用方法:
public void writeObject(Object obj)
:将对象序列化并写入文件。
注意:对象必须实现Serializable
接口才能被序列化。
2. 对象输入流:ObjectInputStream
从文件中读取Java对象。
构造器:
public ObjectInputStream(InputStream in)
常用方法:
public Object readObject()
:将文件中的字节数据反序列化为Java对象。
结语
通过这篇博客,你应该对Java中的IO流有了一个全面的理解。Java IO流像水管一样,不断在内存和外部存储之间传递数据。通过合理地选择和使用字节流、字符流、缓冲流以及数据流和对象流,我们可以高效地实现文件处理、网络通信、对象序列化等操作。
掌握这些知识,不仅让你能够高效处理大规模的数据流,还能帮助你编写出性能优良、健壮的程序。
标签:字符,Java,字节,文件,读写,写入,IO,public,读取 From: https://www.cnblogs.com/itcq1024/p/18401694