Java I/O的字符流与字节流解析
Java I/O 基础概念
在 Java 编程中,输入输出(I/O)操作是非常重要的一部分,它涉及到从外部数据源读取数据以及将数据写入到外部目标。数据源和目标可以是文件、网络连接、内存缓冲区等。Java 的 I/O 类库提供了丰富的功能来处理各种 I/O 任务,其中字符流和字节流是两个重要的概念。
流的概念
流(Stream)是一个抽象的概念,表示从数据源到数据目的地的一个有序的数据序列。可以把流想象成一条管道,数据就像水流一样在管道中流动。根据数据的类型不同,流可以分为字节流和字符流。字节流主要用于处理字节数据,比如二进制文件、图像、音频等;字符流则主要用于处理字符数据,比如文本文件。
I/O 类库结构
Java 的 I/O 类库位于 java.io
包中,其结构层次分明。字节流的顶层抽象类是 InputStream
和 OutputStream
,而字符流的顶层抽象类是 Reader
和 Writer
。从这四个顶层抽象类派生出来了许多具体的类,以满足不同的 I/O 需求。
字节流
字节流用于处理 8 位字节数据,它是最基础的 I/O 流类型。字节流的主要类是 InputStream
和 OutputStream
,它们是抽象类,具体的操作由它们的子类来实现。
InputStream 类
InputStream
类是所有字节输入流的超类,它定义了一些基本的读取字节数据的方法。以下是一些重要的方法:
int read()
:从输入流中读取一个字节的数据,返回值是读取的字节数据,以整数形式表示(0 - 255)。如果到达流的末尾,则返回 -1。int read(byte[] b)
:从输入流中读取一定数量的字节,并将其存储在字节数组b
中。返回值是实际读取的字节数,如果到达流的末尾,则返回 -1。int read(byte[] b, int off, int len)
:从输入流中读取最多len
个字节的数据,并将其存储在字节数组b
中,从偏移量off
开始存储。返回值是实际读取的字节数,如果到达流的末尾,则返回 -1。
以下是一个使用 FileInputStream
(InputStream
的子类)从文件中读取字节数据的示例:
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
public class ByteStreamReadExample {
public static void main(String[] args) {
try (InputStream inputStream = new FileInputStream("example.txt")) {
int data;
while ((data = inputStream.read()) != -1) {
System.out.print((char) data);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
在上述示例中,FileInputStream
用于从名为 example.txt
的文件中读取字节数据。read()
方法每次读取一个字节,并将其转换为字符后输出。当读取到文件末尾时,read()
方法返回 -1,从而结束循环。
OutputStream 类
OutputStream
类是所有字节输出流的超类,它定义了一些基本的写入字节数据的方法。以下是一些重要的方法:
void write(int b)
:将指定的字节数据写入到输出流中。参数b
是要写入的字节数据,只使用低 8 位。void write(byte[] b)
:将字节数组b
中的所有字节数据写入到输出流中。void write(byte[] b, int off, int len)
:将字节数组b
中从偏移量off
开始的len
个字节数据写入到输出流中。
以下是一个使用 FileOutputStream
(OutputStream
的子类)将字节数据写入文件的示例:
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
public class ByteStreamWriteExample {
public static void main(String[] args) {
String message = "Hello, Byte Stream!";
byte[] bytes = message.getBytes();
try (OutputStream outputStream = new FileOutputStream("output.txt")) {
outputStream.write(bytes);
} catch (IOException e) {
e.printStackTrace();
}
}
}
在上述示例中,首先将字符串转换为字节数组,然后使用 FileOutputStream
将字节数组中的数据写入到名为 output.txt
的文件中。
字符流
字符流用于处理 16 位 Unicode 字符数据,它是在字节流的基础上构建的,提供了更方便的字符处理功能。字符流的主要类是 Reader
和 Writer
,它们也是抽象类,具体的操作由它们的子类来实现。
Reader 类
Reader
类是所有字符输入流的超类,它定义了一些基本的读取字符数据的方法。以下是一些重要的方法:
int read()
:从输入流中读取一个字符的数据,返回值是读取的字符数据,以整数形式表示(0 - 65535)。如果到达流的末尾,则返回 -1。int read(char[] cbuf)
:从输入流中读取一定数量的字符,并将其存储在字符数组cbuf
中。返回值是实际读取的字符数,如果到达流的末尾,则返回 -1。int read(char[] cbuf, int off, int len)
:从输入流中读取最多len
个字符的数据,并将其存储在字符数组cbuf
中,从偏移量off
开始存储。返回值是实际读取的字符数,如果到达流的末尾,则返回 -1。
以下是一个使用 FileReader
(Reader
的子类)从文件中读取字符数据的示例:
import java.io.FileReader;
import java.io.IOException;
import java.io.Reader;
public class CharStreamReadExample {
public static void main(String[] args) {
try (Reader reader = new FileReader("example.txt")) {
int data;
while ((data = reader.read()) != -1) {
System.out.print((char) data);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
在上述示例中,FileReader
用于从名为 example.txt
的文件中读取字符数据。read()
方法每次读取一个字符,并将其输出。当读取到文件末尾时,read()
方法返回 -1,从而结束循环。
Writer 类
Writer
类是所有字符输出流的超类,它定义了一些基本的写入字符数据的方法。以下是一些重要的方法:
void write(int c)
:将指定的字符数据写入到输出流中。参数c
是要写入的字符数据,只使用低 16 位。void write(char[] cbuf)
:将字符数组cbuf
中的所有字符数据写入到输出流中。void write(char[] cbuf, int off, int len)
:将字符数组cbuf
中从偏移量off
开始的len
个字符数据写入到输出流中。void write(String str)
:将字符串str
中的所有字符数据写入到输出流中。void write(String str, int off, int len)
:将字符串str
中从偏移量off
开始的len
个字符数据写入到输出流中。
以下是一个使用 FileWriter
(Writer
的子类)将字符数据写入文件的示例:
import java.io.FileWriter;
import java.io.IOException;
import java.io.Writer;
public class CharStreamWriteExample {
public static void main(String[] args) {
String message = "Hello, Char Stream!";
try (Writer writer = new FileWriter("output.txt")) {
writer.write(message);
} catch (IOException e) {
e.printStackTrace();
}
}
}
在上述示例中,使用 FileWriter
将字符串 message
中的字符数据写入到名为 output.txt
的文件中。
字节流与字符流的区别
字节流和字符流虽然都是用于 I/O 操作,但它们在很多方面存在区别。
处理数据类型
字节流处理的是 8 位字节数据,适用于处理二进制数据,如图片、音频、视频等文件。而字符流处理的是 16 位 Unicode 字符数据,主要用于处理文本数据,如文本文件、字符串等。
缓冲机制
字节流本身没有内置的缓冲机制,需要手动使用 BufferedInputStream
和 BufferedOutputStream
来实现缓冲。而字符流的一些子类,如 BufferedReader
和 BufferedWriter
,默认就提供了缓冲机制,这使得字符流在处理文本数据时效率更高。
编码问题
字节流不涉及字符编码问题,它只是简单地读取和写入字节数据。而字符流在处理字符数据时,需要考虑字符编码。例如,FileReader
和 FileWriter
默认使用系统的默认字符编码,而 InputStreamReader
和 OutputStreamWriter
可以指定字符编码。
字节流与字符流的转换
在实际应用中,有时需要在字节流和字符流之间进行转换。Java 提供了 InputStreamReader
和 OutputStreamWriter
类来实现这种转换。
InputStreamReader
InputStreamReader
是从字节流到字符流的桥梁,它将 InputStream
转换为 Reader
。通过 InputStreamReader
,可以指定字符编码来读取字节数据并将其转换为字符数据。
以下是一个使用 InputStreamReader
将 FileInputStream
转换为 Reader
并指定字符编码的示例:
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Reader;
public class ByteToCharConversionExample {
public static void main(String[] args) {
try (Reader reader = new InputStreamReader(new FileInputStream("example.txt"), "UTF-8")) {
int data;
while ((data = reader.read()) != -1) {
System.out.print((char) data);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
在上述示例中,InputStreamReader
将 FileInputStream
包装起来,并指定使用 UTF - 8
字符编码将字节数据转换为字符数据。
OutputStreamWriter
OutputStreamWriter
是从字符流到字节流的桥梁,它将 Writer
转换为 OutputStream
。通过 OutputStreamWriter
,可以指定字符编码来将字符数据转换为字节数据并写入到输出流中。
以下是一个使用 OutputStreamWriter
将 FileWriter
转换为 OutputStream
并指定字符编码的示例:
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.Writer;
public class CharToByteConversionExample {
public static void main(String[] args) {
String message = "Hello, Conversion!";
try (Writer writer = new OutputStreamWriter(new FileOutputStream("output.txt"), "UTF-8")) {
writer.write(message);
} catch (IOException e) {
e.printStackTrace();
}
}
}
在上述示例中,OutputStreamWriter
将 FileOutputStream
包装起来,并指定使用 UTF - 8
字符编码将字符数据转换为字节数据并写入到文件中。
缓冲流
无论是字节流还是字符流,缓冲流都可以显著提高 I/O 操作的性能。
字节缓冲流
字节缓冲流包括 BufferedInputStream
和 BufferedOutputStream
。BufferedInputStream
内部有一个缓冲区,它会一次性从底层输入流中读取多个字节到缓冲区中,当调用 read()
方法时,首先从缓冲区中读取数据,当缓冲区中的数据读完后,再从底层输入流中读取数据填充缓冲区。BufferedOutputStream
类似,它会将数据先写入缓冲区,当缓冲区满或者调用 flush()
方法时,才将缓冲区中的数据写入到底层输出流中。
以下是一个使用 BufferedInputStream
和 BufferedOutputStream
复制文件的示例:
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
public class ByteBufferedStreamExample {
public static void main(String[] args) {
try (BufferedInputStream bis = new BufferedInputStream(new FileInputStream("source.txt"));
BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("destination.txt"))) {
int data;
while ((data = bis.read()) != -1) {
bos.write(data);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
在上述示例中,BufferedInputStream
从 source.txt
文件中读取数据,BufferedOutputStream
将数据写入到 destination.txt
文件中。由于缓冲流的使用,减少了实际的 I/O 操作次数,提高了复制文件的效率。
字符缓冲流
字符缓冲流包括 BufferedReader
和 BufferedWriter
。BufferedReader
提供了更方便的读取字符数据的方法,如 readLine()
方法可以读取一行文本。BufferedWriter
同样提供了缓冲区,提高了写入字符数据的效率。
以下是一个使用 BufferedReader
和 BufferedWriter
复制文本文件的示例:
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
public class CharBufferedStreamExample {
public static void main(String[] args) {
try (BufferedReader br = new BufferedReader(new FileReader("source.txt"));
BufferedWriter bw = new BufferedWriter(new FileWriter("destination.txt"))) {
String line;
while ((line = br.readLine()) != null) {
bw.write(line);
bw.newLine();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
在上述示例中,BufferedReader
使用 readLine()
方法逐行读取 source.txt
文件中的文本,BufferedWriter
将读取到的每一行文本写入到 destination.txt
文件中,并使用 newLine()
方法写入换行符。
数据流
数据流是一种特殊的字节流,它提供了读取和写入基本数据类型(如 int
、double
、boolean
等)以及字符串的功能。数据流类包括 DataInputStream
和 DataOutputStream
。
DataInputStream
DataInputStream
用于从输入流中读取基本数据类型和字符串。它提供了一系列的读取方法,如 readInt()
、readDouble()
、readUTF()
等。
以下是一个使用 DataInputStream
读取数据的示例:
import java.io.DataInputStream;
import java.io.FileInputStream;
import java.io.IOException;
public class DataInputStreamExample {
public static void main(String[] args) {
try (DataInputStream dis = new DataInputStream(new FileInputStream("data.txt"))) {
int number = dis.readInt();
double value = dis.readDouble();
String str = dis.readUTF();
System.out.println("Number: " + number);
System.out.println("Value: " + value);
System.out.println("String: " + str);
} catch (IOException e) {
e.printStackTrace();
}
}
}
在上述示例中,DataInputStream
从 data.txt
文件中读取一个整数、一个双精度浮点数和一个字符串。文件中的数据应该是使用 DataOutputStream
按照相应格式写入的。
DataOutputStream
DataOutputStream
用于将基本数据类型和字符串写入到输出流中。它提供了一系列的写入方法,如 writeInt()
、writeDouble()
、writeUTF()
等。
以下是一个使用 DataOutputStream
写入数据的示例:
import java.io.DataOutputStream;
import java.io.FileOutputStream;
import java.io.IOException;
public class DataOutputStreamExample {
public static void main(String[] args) {
try (DataOutputStream dos = new DataOutputStream(new FileOutputStream("data.txt"))) {
dos.writeInt(100);
dos.writeDouble(3.14159);
dos.writeUTF("Hello, Data Stream!");
} catch (IOException e) {
e.printStackTrace();
}
}
}
在上述示例中,DataOutputStream
将一个整数、一个双精度浮点数和一个字符串写入到 data.txt
文件中。
对象流
对象流用于将对象进行序列化和反序列化操作。序列化是将对象转换为字节序列的过程,反序列化则是将字节序列恢复为对象的过程。对象流类包括 ObjectInputStream
和 ObjectOutputStream
。
ObjectOutputStream
ObjectOutputStream
用于将对象写入到输出流中。要使一个对象能够被序列化,该对象的类必须实现 Serializable
接口。
以下是一个使用 ObjectOutputStream
序列化对象的示例:
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
class Person implements java.io.Serializable {
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public int getAge() {
return age;
}
}
public class ObjectOutputStreamExample {
public static void main(String[] args) {
Person person = new Person("Alice", 25);
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("person.ser"))) {
oos.writeObject(person);
} catch (IOException e) {
e.printStackTrace();
}
}
}
在上述示例中,Person
类实现了 Serializable
接口,ObjectOutputStream
将 Person
对象写入到名为 person.ser
的文件中。
ObjectInputStream
ObjectInputStream
用于从输入流中读取对象并进行反序列化。
以下是一个使用 ObjectInputStream
反序列化对象的示例:
import java.io.FileInputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
public class ObjectInputStreamExample {
public static void main(String[] args) {
try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("person.ser"))) {
Person person = (Person) ois.readObject();
System.out.println("Name: " + person.getName());
System.out.println("Age: " + person.getAge());
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
}
}
}
在上述示例中,ObjectInputStream
从 person.ser
文件中读取对象,并将其转换为 Person
对象,然后输出对象的属性。
随机访问文件流
随机访问文件流 RandomAccessFile
提供了对文件的随机访问功能,可以在文件的任意位置进行读写操作。它既可以像字节输入流一样读取数据,也可以像字节输出流一样写入数据。
随机访问文件流的使用
RandomAccessFile
有两个构造函数,一个接受文件名,另一个接受文件名和访问模式。访问模式有 "r"
(只读)、"rw"
(读写)、"rws"
(读写,同步文件内容和元数据)和 "rwd"
(读写,同步文件内容)。
以下是一个使用 RandomAccessFile
在文件中随机读写数据的示例:
import java.io.IOException;
import java.io.RandomAccessFile;
public class RandomAccessFileExample {
public static void main(String[] args) {
try (RandomAccessFile raf = new RandomAccessFile("example.txt", "rw")) {
// 写入数据
raf.writeUTF("Hello, Random Access!");
raf.writeInt(42);
// 移动文件指针到文件开头
raf.seek(0);
// 读取数据
String str = raf.readUTF();
int number = raf.readInt();
System.out.println("String: " + str);
System.out.println("Number: " + number);
} catch (IOException e) {
e.printStackTrace();
}
}
}
在上述示例中,首先使用 RandomAccessFile
以读写模式打开 example.txt
文件,写入一个字符串和一个整数。然后通过 seek()
方法将文件指针移动到文件开头,再读取之前写入的数据。
总结
Java 的 I/O 流体系非常庞大且功能强大,字节流和字符流是其中的核心部分。字节流适用于处理二进制数据,字符流适用于处理文本数据。通过缓冲流、数据流、对象流和随机访问文件流等不同类型的流,可以满足各种复杂的 I/O 需求。在实际应用中,需要根据具体的场景选择合适的流来实现高效、准确的 I/O 操作。同时,要注意资源的正确关闭,以避免资源泄漏问题。通过深入理解和熟练运用这些 I/O 流,开发者能够更好地处理与外部数据源和目标之间的数据交互,为开发健壮、高效的 Java 应用程序奠定坚实的基础。在实际项目中,还需要考虑性能优化、异常处理等方面,以确保 I/O 操作的稳定性和可靠性。