Java IO 基础知识总结
一、IO 流简介
IO(Input/Output)是计算机中输入和输出操作的总称,描述了数据在计算机内存和外部设备之间的传输。数据的输入是指将外部数据传递到计算机内存,而输出则是指将内存中的数据传输到外部存储介质,如文件、数据库或网络。
IO 流的概念源自于水流,数据传输的过程类似于水的流动,因此称为“流”。
输入流和输出流
在 Java 中,IO 流分为两类:
- 输入流:用于读取数据的流,继承自
InputStream
或Reader
类。 - 输出流:用于写入数据的流,继承自
OutputStream
或Writer
类。
字节流和字符流
Java IO 流进一步根据数据的处理方式分为字节流和字符流:
- 字节流:以字节为单位进行数据传输,适用于所有类型的 I/O 操作(包括二进制文件)。字节流基类为:
InputStream
:字节输入流OutputStream
:字节输出流
- 字符流:以字符为单位进行数据传输,适用于文本文件。字符流基类为:
Reader
:字符输入流Writer
:字符输出流
4 个主要的抽象类
Java IO 流的 40 多个类都是从以下 4 个抽象类派生出来的:
- InputStream:所有字节输入流的基类。
- OutputStream:所有字节输出流的基类。
- Reader:所有字符输入流的基类。
- Writer:所有字符输出流的基类。
二、字节流
InputStream(字节输入流)
InputStream
是 Java 中所有字节输入流的基类,用于从源(通常是文件)读取字节数据到内存。它是 java.io.InputStream
抽象类,定义了多个常用的方法来处理字节数据的读取。
常用方法
read()
:读取一个字节的数据,返回值范围为 0 到 255。如果没有可读取字节,则返回-1
,表示文件结束。read(byte[] b)
:将输入流中的字节读取到数组b
中。读取的字节数最大为b.length
。如果没有可读取字节,返回-1
。read(byte[] b, int off, int len)
:在read(byte[])
基础上增加了偏移量off
和最大读取字节数len
参数。skip(long n)
:跳过输入流中的n
个字节,返回实际跳过的字节数。available()
:返回当前流中可读取的字节数。close()
:关闭输入流,释放相关的资源。
Java 9 新增方法
readAllBytes()
:读取输入流中所有字节,返回一个字节数组。readNBytes(byte[] b, int off, int len)
:阻塞直到读取len
个字节。transferTo(OutputStream out)
:将输入流中的所有字节传递到输出流中。
示例代码
下面的代码展示了如何使用 FileInputStream
来读取文件的字节数据:
try (InputStream fis = new FileInputStream("input.txt")) {
System.out.println("Number of remaining bytes:" + fis.available());
int content;
long skip = fis.skip(2);
System.out.println("The actual number of bytes skipped:" + skip);
System.out.print("The content read from file:");
while ((content = fis.read()) != -1) {
System.out.print((char) content);
}
} catch (IOException e) {
e.printStackTrace();
}
input.txt
文件内容为:
JavaGuide
输出为:
Number of remaining bytes: 11
The actual number of bytes skipped: 2
The content read from file: JavaGuide
使用 BufferedInputStream
通常,我们不会单独使用 FileInputStream
,而是将其与 BufferedInputStream
配合使用,这样可以提高读取效率。例如:
BufferedInputStream bufferedInputStream = new BufferedInputStream(new FileInputStream("input.txt"));
String result = new String(bufferedInputStream.readAllBytes());
System.out.println(result);
DataInputStream
DataInputStream
是专门用于读取原始数据类型(如 int
, boolean
, UTF
等)的流,它需要与其他流(如 FileInputStream
)结合使用。例如:
FileInputStream fileInputStream = new FileInputStream("input.txt");
DataInputStream dataInputStream = new DataInputStream(fileInputStream);
dataInputStream.readBoolean();
dataInputStream.readInt();
dataInputStream.readUTF();
ObjectInputStream
ObjectInputStream
用于从输入流中读取 Java 对象(反序列化)。相应地,ObjectOutputStream
用于将对象写入输出流(序列化)。示例代码:
ObjectInputStream input = new ObjectInputStream(new FileInputStream("object.data"));
MyClass object = (MyClass) input.readObject();
input.close();
需要注意的是,进行序列化和反序列化的类必须实现 Serializable
接口。如果某个属性不希望被序列化,可以使用 transient
关键字来修饰。
OutputStream(字节输出流)
OutputStream
是 Java 中所有字节输出流的基类,用于将字节数据写入到目标(通常是文件或网络)。java.io.OutputStream
是一个抽象类,定义了许多常用的方法来处理字节数据的输出。
常用方法
write(int b)
:将一个字节写入输出流。write(byte[] b)
:将字节数组b
中的数据写入输出流,等价于write(b, 0, b.length)
。write(byte[] b, int off, int len)
:在write(byte[])
的基础上,增加了偏移量off
和最大写入字节数len
参数。flush()
:刷新输出流,将缓冲区中的数据强制写入目标。close()
:关闭输出流,释放相关的资源。
示例代码
下面的代码展示了如何使用 FileOutputStream
来将字节数据写入文件:
try (FileOutputStream output = new FileOutputStream("output.txt")) {
byte[] array = "JavaGuide".getBytes();
output.write(array);
} catch (IOException e) {
e.printStackTrace();
}
运行结果:
JavaGuide
文件 output.txt
中的内容将是:
JavaGuide
使用 BufferedOutputStream
类似于 FileInputStream
,FileOutputStream
通常也会配合 BufferedOutputStream
(字节缓冲输出流)使用,以提高效率。例如:
FileOutputStream fileOutputStream = new FileOutputStream("output.txt");
BufferedOutputStream bos = new BufferedOutputStream(fileOutputStream);
bos.write("BufferedOutputStream Example".getBytes());
bos.flush();
bos.close();
DataOutputStream
DataOutputStream
用于将原始数据类型(如 int
, boolean
, UTF
等)写入流。它不能单独使用,必须与其他流(如 FileOutputStream
)结合使用:
FileOutputStream fileOutputStream = new FileOutputStream("out.txt");
DataOutputStream dataOutputStream = new DataOutputStream(fileOutputStream);
dataOutputStream.writeBoolean(true);
dataOutputStream.writeByte(1);
dataOutputStream.writeInt(123);
dataOutputStream.writeUTF("Hello, World!");
ObjectOutputStream
ObjectOutputStream
用于将 Java 对象(序列化)写入输出流。与之对应的是 ObjectInputStream
,用于从输入流中读取 Java 对象(反序列化)。示例代码如下:
ObjectOutputStream output = new ObjectOutputStream(new FileOutputStream("file.txt"));
Person person = new Person("Guide哥", "JavaGuide作者");
output.writeObject(person);
output.close();
需要注意的是,进行序列化和反序列化的类必须实现 Serializable
接口。
三、字符流
虽然文件读写或网络传输的最小单元是字节,但字符流与字节流的区分主要出于以下两点考虑:
- 转换过程开销:字符流在读取或写入字符数据时,Java 虚拟机需要将字节数据转换为字符,这个过程会增加一定的性能开销。
- 乱码问题:如果没有明确指定编码格式,字节数据直接转换为字符可能会导致乱码,尤其是在处理不同编码的文件时。
乱码问题示例
当我们用 FileInputStream
读取中文文件时,直接将字节数据转为字符,会导致乱码。这是因为字节流没有考虑字符的编码方式,默认使用的是平台的默认编码,而这可能与文件的实际编码不匹配。
例如,假设 input.txt
文件中含有中文内容,使用 FileInputStream
直接读取时可能会看到类似下面的乱码:
Number of remaining bytes:9
The actual number of bytes skipped:2
The content read from file:§å®¶å¥½
字符流的优势
字符流可以直接处理字符,而不需要额外的字节到字符的转换。字符流在内部已经处理了编码问题,默认使用 Unicode
编码,避免了编码和解码的问题。对于文本文件,使用字符流是更为便利和高效的选择。
字符编码
字符流默认使用 Unicode
编码,它为每个字符分配一个唯一的编号。常见的 Unicode
编码方式包括:
- UTF-8:英文字符使用 1 字节,中文字符使用 3 字节。它是一种变长编码。
- UTF-16:大多数字符使用 2 字节,某些字符使用 4 字节。
- UTF-32:每个字符占用 4 字节,固定长度编码。
当字符流处理文本文件时,可以通过构造方法自定义编码方式,以避免因编码不同导致的乱码问题。
Reader(字符输入流)
Reader
是 Java 中所有字符输入流的基类,用于从源(如文件)读取字符数据到内存。java.io.Reader
是一个抽象类,它专门用于处理文本数据,而不像字节流(InputStream
)处理的是原始字节数据。
常用方法
read()
:读取一个字符。read(char[] cbuf)
:将输入流中的字符读取到字符数组cbuf
中,等价于read(cbuf, 0, cbuf.length)
。read(char[] cbuf, int off, int len)
:在read(char[])
方法的基础上,增加了偏移量off
和最大读取字符数len
参数。skip(long n)
:跳过输入流中的n
个字符,返回实际跳过的字符数。close()
:关闭输入流并释放相关的资源。
字节流转换为字符流
InputStreamReader
是字节流到字符流的桥梁,允许你将字节流转换为字符流。它是 Reader
的子类。FileReader
是 InputStreamReader
的一个具体实现,用于从文件读取字符。
// 字节流转换为字符流的桥梁
public class InputStreamReader extends Reader {
}
// 用于读取字符文件
public class FileReader extends InputStreamReader {
}
示例代码:使用 FileReader
下面的代码展示了如何使用 FileReader
来读取文件中的字符数据:
try (FileReader fileReader = new FileReader("input.txt")) {
int content;
long skip = fileReader.skip(3);
System.out.println("The actual number of bytes skipped: " + skip);
System.out.print("The content read from file: ");
while ((content = fileReader.read()) != -1) {
System.out.print((char) content);
}
} catch (IOException e) {
e.printStackTrace();
}
假设 input.txt
文件内容为:
我是Guide。
输出将是:
The actual number of bytes skipped: 3
The content read from file: 我是Guide。
总结
Reader
主要用于处理文本数据(字符),适用于读取和写入文本文件。- 通过
InputStreamReader
将字节流转换为字符流,再通过FileReader
方便地读取字符文件。
Writer(字符输出流)
Writer
是 Java 中所有字符输出流的基类,用于将字符数据写入到目标(如文件)。与字节输出流类似,Writer
处理的是字符数据,而不是原始字节数据。
常用方法
write(int c)
:写入一个字符。write(char[] cbuf)
:写入字符数组cbuf
,等价于write(cbuf, 0, cbuf.length)
。write(char[] cbuf, int off, int len)
:在write(char[])
的基础上,增加了偏移量off
和最大写入字符数len
参数。write(String str)
:写入一个字符串,等价于write(str, 0, str.length())
。write(String str, int off, int len)
:在write(String)
的基础上,增加了偏移量off
和最大写入字符数len
参数。append(CharSequence csq)
:将指定的字符序列附加到当前Writer
中,并返回该Writer
对象。append(char c)
:将指定的字符附加到当前Writer
中,并返回该Writer
对象。flush()
:刷新输出流,强制写出所有缓冲的字符。close()
:关闭输出流并释放相关的资源。
字符流转换为字节流
OutputStreamWriter
是字符流转换为字节流的桥梁,它是 Writer
的子类。FileWriter
是 OutputStreamWriter
的具体实现,用于直接将字符数据写入文件。
// 字符流转换为字节流的桥梁
public class OutputStreamWriter extends Writer {
}
// 用于写入字符到文件
public class FileWriter extends OutputStreamWriter {
}
示例代码:使用 FileWriter
下面的代码展示了如何使用 FileWriter
将字符数据写入文件:
try (Writer output = new FileWriter("output.txt")) {
output.write("你好,我是Guide。");
} catch (IOException e) {
e.printStackTrace();
}
运行该代码时,output.txt
文件的内容将是:
你好,我是Guide。
总结
Writer
是字符输出流的基类,专门用于处理字符数据的输出。- 通过
OutputStreamWriter
,字符流可以转换为字节流。而FileWriter
是用于将字符数据写入文件的常用实现类。
四、字节缓冲流
字节缓冲流的主要目的是提高 I/O 操作的效率。字节流进行文件或数据读取时,通常每次读取一个字节,这样的操作会频繁地进行 I/O 调用,性能较低。而字节缓冲流使用了一个缓冲区(字节数组)来将多个字节集中读取或写入,从而减少 I/O 操作的次数,显著提高性能。
字节缓冲流的原理与使用
字节缓冲流通过装饰器模式增强了 InputStream
和 OutputStream
的功能。在实际应用中,BufferedInputStream
和 BufferedOutputStream
通过包裹普通的字节流(如 FileInputStream
和 FileOutputStream
),提升了性能。
性能对比
当我们使用普通字节流的 read(int b)
或 write(int b)
方法逐字节操作时,字节缓冲流会将数据先加载到缓冲区中,批量读取或写入,减少了 I/O 调用的次数。
例如,使用普通字节流与字节缓冲流分别复制一个 524.9 MB
的 PDF 文件时,耗时差异非常大:
使用缓冲流复制PDF文件总耗时:15428 毫秒
使用普通字节流复制PDF文件总耗时:2555062 毫秒
使用字节缓冲流的时间是普通字节流的 1/165
,显示了字节缓冲流的显著优势。
测试代码(字节流与字节缓冲流对比)
使用 read()
和 write()
方法逐字节读写
@Test
void copy_pdf_to_another_pdf_buffer_stream() {
long start = System.currentTimeMillis();
try (BufferedInputStream bis = new BufferedInputStream(new FileInputStream("input.pdf"));
BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("output.pdf"))) {
int content;
while ((content = bis.read()) != -1) {
bos.write(content);
}
} catch (IOException e) {
e.printStackTrace();
}
long end = System.currentTimeMillis();
System.out.println("使用缓冲流复制PDF文件总耗时:" + (end - start) + " 毫秒");
}
@Test
void copy_pdf_to_another_pdf_stream() {
long start = System.currentTimeMillis();
try (FileInputStream fis = new FileInputStream("input.pdf");
FileOutputStream fos = new FileOutputStream("output.pdf")) {
int content;
while ((content = fis.read()) != -1) {
fos.write(content);
}
} catch (IOException e) {
e.printStackTrace();
}
long end = System.currentTimeMillis();
System.out.println("使用普通流复制PDF文件总耗时:" + (end - start) + " 毫秒");
}
使用 read(byte b[])
和 write(byte b[], int off, int len)
方法批量读写
当使用 read(byte b[])
和 write(byte b[], int off, int len)
这类方法进行数据批量读取与写入时,缓冲流和普通流的性能差距相对较小,但缓冲流仍然略胜一筹。
@Test
void copy_pdf_to_another_pdf_with_byte_array_buffer_stream() {
long start = System.currentTimeMillis();
try (BufferedInputStream bis = new BufferedInputStream(new FileInputStream("input.pdf"));
BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("output.pdf"))) {
int len;
byte[] bytes = new byte[4 * 1024]; // 4 KB 缓冲区
while ((len = bis.read(bytes)) != -1) {
bos.write(bytes, 0, len);
}
} catch (IOException e) {
e.printStackTrace();
}
long end = System.currentTimeMillis();
System.out.println("使用缓冲流复制PDF文件总耗时:" + (end - start) + " 毫秒");
}
@Test
void copy_pdf_to_another_pdf_with_byte_array_stream() {
long start = System.currentTimeMillis();
try (FileInputStream fis = new FileInputStream("input.pdf");
FileOutputStream fos = new FileOutputStream("output.pdf")) {
int len;
byte[] bytes = new byte[4 * 1024]; // 4 KB 缓冲区
while ((len = fis.read(bytes)) != -1) {
fos.write(bytes, 0, len);
}
} catch (IOException e) {
e.printStackTrace();
}
long end = System.currentTimeMillis();
System.out.println("使用普通流复制PDF文件总耗时:" + (end - start) + " 毫秒");
}
总结
- 字节缓冲流 提高了 I/O 操作效率,尤其是对于逐字节操作的场景,通过减少 I/O 操作的次数显著提高了性能。
- 在进行大文件复制、下载或上传等任务时,使用字节缓冲流能够大幅提升效率,尤其在使用
read()
和write()
方法逐字节操作时,缓冲流相比普通流更具优势。 - 在批量读取或写入时,字节缓冲流仍然具有轻微优势,但差距较小,普通字节流和缓冲流的性能差距可以忽略不计。
BufferedInputStream(字节缓冲输入流)
BufferedInputStream
是一个装饰器,用于增强常规字节输入流(如 FileInputStream
)的性能。它通过使用内部缓冲区来减少对底层数据源的直接访问,从而显著提高了读取效率。特别是在读取大文件或进行大量 I/O 操作时,BufferedInputStream
能够减少 I/O 操作的次数,提高应用程序的性能。
主要特点:
- 减少 I/O 次数:
BufferedInputStream
会将数据先读取到内部缓冲区中,再从缓冲区读取数据。这意味着应用程序读取数据时,不需要每次都进行实际的磁盘操作,而是从缓冲区中读取,提高了性能。 - 缓冲区大小:
BufferedInputStream
默认的缓冲区大小是 8192 字节(8KB)。如果需要,可以通过构造函数指定自定义的缓冲区大小。 - 装饰器模式:
BufferedInputStream
实现了装饰器模式,它通过封装底层输入流(如FileInputStream
)来提供额外的功能。
BufferedInputStream
构造方法:
默认缓冲区大小(8192字节):
public BufferedInputStream(InputStream in) { this(in, DEFAULT_BUFFER_SIZE); }
该构造方法使用默认的缓冲区大小(8192字节)来创建
BufferedInputStream
。自定义缓冲区大小:
public BufferedInputStream(InputStream in, int size) { super(in); if (size <= 0) { throw new IllegalArgumentException("Buffer size <= 0"); } buf = new byte[size]; }
如果希望指定不同的缓冲区大小,可以使用此构造方法。
size
参数指定缓冲区的大小,通常选择合适的大小来平衡内存和性能。
示例代码:
import java.io.*;
public class BufferedInputStreamExample {
public static void main(String[] args) {
// 使用默认缓冲区大小读取文件
try (BufferedInputStream bis = new BufferedInputStream(new FileInputStream("input.txt"))) {
int content;
while ((content = bis.read()) != -1) {
System.out.print((char) content);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
该示例读取 input.txt
文件,并使用默认的缓冲区大小(8192字节)进行读取。每次 read()
方法调用会先检查缓冲区中是否有数据可用,如果有数据,它会直接从缓冲区返回,避免频繁的磁盘 I/O 操作。
缓冲区实现原理
BufferedInputStream
内部维护一个字节数组 buf[]
作为缓冲区,默认的缓冲区大小为 8192 字节。当调用 read()
方法时,BufferedInputStream
会先从缓冲区读取数据。如果缓冲区为空,它会触发底层流的读取操作,将数据加载到缓冲区中,再从缓冲区读取数据。这样避免了每次调用 read()
时都直接进行 I/O 操作,从而减少了磁盘访问次数。
public class BufferedInputStream extends FilterInputStream {
protected volatile byte buf[];
private static int DEFAULT_BUFFER_SIZE = 8192;
public BufferedInputStream(InputStream in) {
this(in, DEFAULT_BUFFER_SIZE);
}
public BufferedInputStream(InputStream in, int size) {
super(in);
if (size <= 0) {
throw new IllegalArgumentException("Buffer size <= 0");
}
buf = new byte[size];
}
}
在 BufferedInputStream
中:
buf[]
是字节数组,用作缓冲区。- 默认缓冲区大小为 8192 字节,如果需要更大的缓冲区,可以传入自定义的大小。
使用场景
- 大文件处理:对于大文件或高效读取大量数据的场景,
BufferedInputStream
可以有效减少 I/O 操作,提高性能。 - 频繁读取操作:当程序频繁读取小块数据时,
BufferedInputStream
可以显著减少 I/O 请求的开销,提升性能。
总结
BufferedInputStream
提供了一种通过缓冲区提高字节输入流效率的方式,减少了对底层 I/O 系统的直接调用。- 它适用于大文件读取和频繁 I/O 操作的场景,能够显著提高应用程序的性能。
- 缓冲区大小可以根据需要调整,默认值为 8192 字节,通常对于一般应用来说这个大小是合适的。
BufferedOutputStream(字节缓冲输出流)
BufferedOutputStream
是字节输出流的装饰器,通过在内部维护缓冲区来减少对目标文件的频繁写入操作。它将要写入的数据先存入缓冲区,只有当缓冲区填满或流关闭时,才将数据一次性写入目的地(如文件)。这种机制大大提高了写入效率,特别是在进行大量的输出操作时。
主要特点:
- 减少 I/O 次数:
BufferedOutputStream
会将数据先存放到缓冲区,只有在缓冲区满时或流关闭时,才会将数据写入实际目标(例如文件)。这减少了 I/O 操作的次数。 - 缓冲区大小:默认缓冲区大小为 8192 字节(8KB)。当然,也可以通过构造方法指定一个自定义的缓冲区大小。
- 装饰器模式:
BufferedOutputStream
使用了装饰器模式,增强了普通输出流(如FileOutputStream
)的功能。
构造方法:
默认缓冲区大小(8192字节):
public BufferedOutputStream(OutputStream out) { this(out, DEFAULT_BUFFER_SIZE); }
使用默认的缓冲区大小(8192字节)来创建
BufferedOutputStream
。自定义缓冲区大小:
public BufferedOutputStream(OutputStream out, int size) { super(out); if (size <= 0) { throw new IllegalArgumentException("Buffer size <= 0"); } buf = new byte[size]; }
如果需要指定不同的缓冲区大小,可以使用此构造方法,
size
参数指定缓冲区的大小。
示例代码:
import java.io.*;
public class BufferedOutputStreamExample {
public static void main(String[] args) {
try (BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("output.txt"))) {
byte[] array = "JavaGuide".getBytes();
bos.write(array); // 将字节数组写入到输出流
} catch (IOException e) {
e.printStackTrace();
}
}
}
在此示例中,BufferedOutputStream
使用默认的缓冲区大小(8192字节)将 "JavaGuide" 字符串写入文件 output.txt
。通过缓冲机制,写入操作效率得到了显著提升。
缓冲区实现原理
BufferedOutputStream
内部维护一个字节数组 buf[]
作为缓冲区。每次调用 write()
方法时,数据会先存放在该缓冲区内。当缓冲区被填满时,数据会一次性写入目标输出流。当流关闭时,BufferedOutputStream
会将缓冲区中剩余的数据写入目标输出流。
public class BufferedOutputStream extends FilterOutputStream {
protected byte[] buf;
private static int DEFAULT_BUFFER_SIZE = 8192;
public BufferedOutputStream(OutputStream out) {
this(out, DEFAULT_BUFFER_SIZE);
}
public BufferedOutputStream(OutputStream out, int size) {
super(out);
if (size <= 0) {
throw new IllegalArgumentException("Buffer size <= 0");
}
buf = new byte[size];
}
}
在 BufferedOutputStream
中:
buf[]
是字节数组,用作缓冲区。- 默认缓冲区大小为 8192 字节,可以根据需要调整缓冲区大小。
使用场景
- 大文件写入:在处理大文件或频繁进行输出操作时,
BufferedOutputStream
能显著提高写入效率。 - 频繁写入操作:如果应用程序频繁进行小块数据的写入,使用缓冲输出流能够减少 I/O 操作的开销,从而提升整体性能。
总结
BufferedOutputStream
提供了一种通过缓冲区提高字节输出流效率的方式,减少了对目标文件的直接写入操作。- 它适用于需要频繁进行小块写入的场景,通过减少 I/O 次数显著提高性能。
- 缓冲区大小默认是 8192 字节,通常对于大多数应用来说,这个大小已经足够。也可以根据实际需求调整缓冲区大小。
五、字符缓冲流
BufferedReader
(字符缓冲输入流)和 BufferedWriter
(字符缓冲输出流)类似于 BufferedInputStream
(字节缓冲输入流)和BufferedOutputStream
(字节缓冲输入流),内部都维护了一个字节数组作为缓冲区。不过,前者主要是用来操作字符信息。
字符缓冲流
与字节缓冲流类似,字符缓冲流也是通过缓冲区提高输入和输出效率的流。BufferedReader
(字符缓冲输入流)和 BufferedWriter
(字符缓冲输出流)都使用了缓冲区机制来减少 I/O 操作的次数,显著提高性能。它们的主要区别是,字节缓冲流操作的是字节数据,而字符缓冲流操作的是字符数据(通过字符编码转换为字节)。
1. BufferedReader
(字符缓冲输入流)
BufferedReader
是 Reader
类的子类,它使用内部的缓冲区来提高读取字符的效率,特别是用于读取文本数据时。它通过将字符存入缓冲区,减少了频繁的 I/O 操作,从而提高了性能。
常用方法:
read()
:读取一个字符。read(char[] cbuf)
:读取多个字符,并存入字符数组cbuf
中。readLine()
:读取一行文本(直到遇到换行符为止)。此方法已经被废弃,推荐使用read()
来代替。skip(long n)
:跳过输入流中的n
个字符。close()
:关闭输入流,释放资源。
示例代码:
import java.io.*;
public class BufferedReaderExample {
public static void main(String[] args) {
try (BufferedReader reader = new BufferedReader(new FileReader("input.txt"))) {
String line;
while ((line = reader.readLine()) != null) {
System.out.println(line); // 逐行读取并打印内容
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
在这个例子中,BufferedReader
被用来逐行读取文件 input.txt
的内容,使用 readLine()
方法读取每一行,直到文件结尾。
2. BufferedWriter
(字符缓冲输出流)
BufferedWriter
是 Writer
类的子类,它也采用缓冲区来提高字符写入的效率,减少了每次写入时对硬盘的访问次数。它适用于大量文本数据的写入操作。
常用方法:
write(int c)
:写入一个字符。write(char[] cbuf)
:写入一个字符数组。write(String str)
:写入一个字符串。flush()
:刷新缓冲区,将缓冲区中的数据写入目标输出流。close()
:关闭流并释放资源。
示例代码:
import java.io.*;
public class BufferedWriterExample {
public static void main(String[] args) {
try (BufferedWriter writer = new BufferedWriter(new FileWriter("output.txt"))) {
writer.write("Hello, World!"); // 写入一行文本
writer.newLine(); // 换行
writer.write("This is an example of BufferedWriter.");
} catch (IOException e) {
e.printStackTrace();
}
}
}
在此示例中,BufferedWriter
被用来将字符串写入文件 output.txt
,并且通过 newLine()
方法添加换行符。
BufferedReader
和 BufferedWriter
的工作原理
BufferedReader
和 BufferedWriter
的内部工作原理与字节缓冲流相似。它们都会使用一个字节数组(缓冲区)来存储从源流读取的数据或写入的数据。每次读取时,字符会被批量读取到缓冲区中,从而减少每次读取操作的时间开销。每次写入时,数据会先写入缓冲区,只有当缓冲区满了或者流被关闭时,数据才会被实际写入到文件中。
使用场景
- 读取大量文本文件:
BufferedReader
非常适合用来读取大文件或逐行处理文本数据。在处理大文本时,通过缓冲区可以显著提高读取效率。 - 写入大量文本数据:
BufferedWriter
在需要写入大量文本数据时非常有用。它可以减少频繁的磁盘写入操作,通过缓冲机制提高写入效率。
总结
BufferedReader
和BufferedWriter
提供了高效的字符流操作,尤其适合于读取和写入大量文本数据。- 这两个类都使用缓冲区来减少 I/O 操作的频率,从而显著提高性能。
BufferedReader
用于字符输入,BufferedWriter
用于字符输出。
六、打印流
打印流
打印流是 Java 中用来方便输出数据的流类型,PrintStream
和 PrintWriter
都用于输出数据,但它们的区别在于操作的数据类型:PrintStream
操作的是字节数据,而 PrintWriter
操作的是字符数据。
1. PrintStream
(字节打印流)
PrintStream
是一个字节输出流,可以方便地将数据输出到文件、控制台等。PrintStream
提供了许多便捷的打印方法,如 print()
和 println()
,这些方法可以输出各种类型的数据(如字符串、整数、浮点数等),并且不需要显式转换数据类型。
特性:
PrintStream
可以直接打印各种数据类型(如int
、char
、boolean
、float
等),且不需要显式转换为字符串。- 支持自动刷新,尤其是在向文件写入时,可以通过设置自动刷新来避免手动调用
flush()
。 - 不会抛出
IOException
,而是将错误记录到一个内部的错误流中,因此它可以方便地与其他输出流一起使用。
常用方法:
print()
:输出数据,但不换行。println()
:输出数据,并换行。write(int b)
:将字节b
写入输出流。write(byte[] b)
:将字节数组b
写入输出流。flush()
:刷新输出流,强制将缓冲区中的数据写入目标输出流。
示例代码:
import java.io.*;
public class PrintStreamExample {
public static void main(String[] args) {
try (PrintStream ps = new PrintStream(new FileOutputStream("output.txt"))) {
ps.print("Hello, ");
ps.println("World!"); // 输出并换行
ps.println(123); // 输出整数
ps.println(3.14); // 输出浮点数
ps.println(true); // 输出布尔值
} catch (IOException e) {
e.printStackTrace();
}
}
}
在此示例中,PrintStream
被用来将不同类型的数据写入到文件 output.txt
中,并且会自动根据数据类型转换为字符串。
2. PrintWriter
(字符打印流)
PrintWriter
是一个字符输出流,提供了与 PrintStream
类似的功能,但是它的操作对象是字符流。PrintWriter
提供了一些方便的方法来打印各种数据类型,并且支持字符编码的自定义。与 PrintStream
相比,PrintWriter
适用于字符数据的输出。
特性:
PrintWriter
可以输出任何类型的数据(如String
、char
、int
等),并且提供了类似PrintStream
的print()
和println()
方法。- 支持字符编码,可以在创建时指定编码格式。
- 也支持自动刷新,可以在每次调用
println()
时自动刷新流。
常用方法:
print()
:打印数据,但不换行。println()
:打印数据,并换行。printf()
:格式化输出数据。flush()
:刷新输出流,将缓冲区中的数据写入目标输出流。close()
:关闭流。
示例代码:
import java.io.*;
public class PrintWriterExample {
public static void main(String[] args) {
try (PrintWriter writer = new PrintWriter(new FileWriter("output.txt"))) {
writer.print("Hello, ");
writer.println("Java!"); // 输出并换行
writer.println(456); // 输出整数
writer.printf("Formatted output: %.2f", 3.14159); // 格式化输出
} catch (IOException e) {
e.printStackTrace();
}
}
}
此示例中,PrintWriter
用于输出不同类型的数据到文件 output.txt
中,同时通过 printf()
方法格式化输出浮点数。
PrintStream
vs PrintWriter
特性 | PrintStream | PrintWriter |
---|---|---|
基类 | OutputStream | Writer |
适用数据类型 | 字节数据(byte[] , int 等) | 字符数据(String , char 等) |
输出格式 | 适用于字节输出流 | 适用于字符输出流 |
自动刷新 | 可设置自动刷新 | 可设置自动刷新 |
异常处理 | 不抛出 IOException ,但会记录错误 | 不抛出 IOException ,但会记录错误 |
使用场景
PrintStream
:适用于输出字节数据,比如向文件或网络流输出二进制数据,或向控制台打印信息。PrintWriter
:适用于输出字符数据,比如向文件或控制台输出文本。
总结
PrintStream
和PrintWriter
提供了便捷的打印方法,允许用户打印各种类型的数据。PrintStream
适用于字节流,而PrintWriter
适用于字符流。- 两者都支持自动刷新和格式化输出,是 Java 中常见的打印工具。
七、随机访问流 (RandomAccessFile
)
RandomAccessFile
是 Java 中用于支持对文件进行随机访问的类,允许开发者随时在文件的任意位置读取和写入数据。与传统的输入输出流不同,RandomAccessFile
允许在文件中进行跳转,读取和写入操作的位置可以灵活调整,这使得它非常适合处理需要随机访问的场景,如大文件的断点续传。
构造方法
RandomAccessFile
通过指定文件路径和操作模式来创建,可以指定多种模式来打开文件:
public RandomAccessFile(File file, String mode) throws FileNotFoundException
mode
(模式)参数可以是以下几种:r
:只读模式,不能写入文件。rw
:读写模式,允许读取和写入文件。rws
:读写模式,但每次文件内容或元数据发生变化时会同步更新到存储设备。rwd
:读写模式,但只有文件内容发生变化时会同步更新。
文件指针
RandomAccessFile
中有一个文件指针,指示当前读取或写入的位置。通过 seek(long pos)
方法可以移动文件指针,指定下一个操作的字节位置。可以使用 getFilePointer()
方法获取当前的文件指针位置。
常用方法
seek(long pos)
:将文件指针移动到文件的指定位置(以字节为单位)。getFilePointer()
:获取当前的文件指针位置。read()
:读取一个字节的数据。read(byte[] b)
:读取字节数组的数据。write(int b)
:写入一个字节的数据。write(byte[] b)
:写入字节数组的数据。close()
:关闭文件流。
示例代码
import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
public class RandomAccessFileExample {
public static void main(String[] args) throws IOException {
RandomAccessFile randomAccessFile = new RandomAccessFile(new File("input.txt"), "rw");
// 读取文件指针和字符
System.out.println("读取之前的偏移量:" + randomAccessFile.getFilePointer() + ", 当前读取到的字符:" + (char) randomAccessFile.read() + ",读取之后的偏移量:" + randomAccessFile.getFilePointer());
// 移动文件指针到偏移量6的位置
randomAccessFile.seek(6);
System.out.println("读取之前的偏移量:" + randomAccessFile.getFilePointer() + ", 当前读取到的字符:" + (char) randomAccessFile.read() + ",读取之后的偏移量:" + randomAccessFile.getFilePointer());
// 写入字节数据
randomAccessFile.write(new byte[]{'H', 'I', 'J', 'K'});
// 回到文件开头并读取
randomAccessFile.seek(0);
System.out.println("读取之前的偏移量:" + randomAccessFile.getFilePointer() + ", 当前读取到的字符:" + (char) randomAccessFile.read() + ",读取之后的偏移量:" + randomAccessFile.getFilePointer());
randomAccessFile.close();
}
}
假设 input.txt
文件初始内容如下:
ABCDEFG
运行上述代码后,输出将是:
读取之前的偏移量:0, 当前读取到的字符:A,读取之后的偏移量:1
读取之前的偏移量:6, 当前读取到的字符:G,读取之后的偏移量:7
读取之前的偏移量:0, 当前读取到的字符:A,读取之后的偏移量:1
文件内容将被修改为:
ABCDEFGHIJK
覆盖写入
RandomAccessFile
的 write
方法会覆盖指定位置的数据。如果你将文件指针移动到某个位置并写入数据,原有数据会被新数据替代。
RandomAccessFile randomAccessFile = new RandomAccessFile(new File("input.txt"), "rw");
randomAccessFile.write(new byte[]{'H', 'I', 'J', 'K'});
如果文件 input.txt
中原本有内容 ABCD
,运行上述代码后,文件内容将变为 HIJK
,即写入的数据覆盖了原有的数据。
断点续传
RandomAccessFile
常用于实现大文件的断点续传功能。在这种情况下,我们可以将文件分为多个块,并在文件上传过程中记录每个块的上传进度。若上传中断,只需从中断位置继续上传。
示例:合并文件分片
import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
public class FileMergeExample {
public static void main(String[] args) throws IOException {
File file1 = new File("part1.txt");
File file2 = new File("part2.txt");
File mergedFile = new File("merged.txt");
try (RandomAccessFile raf1 = new RandomAccessFile(file1, "r");
RandomAccessFile raf2 = new RandomAccessFile(file2, "r");
RandomAccessFile mergedRaf = new RandomAccessFile(mergedFile, "rw")) {
// 将 part1.txt 写入合并文件
byte[] buffer = new byte[1024];
int len;
while ((len = raf1.read(buffer)) != -1) {
mergedRaf.write(buffer, 0, len);
}
// 将 part2.txt 写入合并文件
while ((len = raf2.read(buffer)) != -1) {
mergedRaf.write(buffer, 0, len);
}
}
}
}
通过使用 RandomAccessFile
,我们可以按顺序将文件的各个分片合并成一个完整的文件。
总结
RandomAccessFile
使得文件的读写操作更加灵活,支持随机访问,可以指定任意位置读取和写入。它是处理大文件、实现断点续传等场景的理想选择。通过调整文件指针位置,可以高效地访问文件的特定部分。