一、Java IO 概述
Java IO(Input/Output,输入 / 输出)是 Java 语言用于实现程序与外部设备(如文件、网络、控制台等)之间数据传输的核心技术。它是 Java 程序与外界交互的重要桥梁,比如读取本地配置文件、向磁盘写入日志、通过网络发送数据等场景,都离不开 Java IO 的支持。
Java IO 体系的设计基于流(Stream) 的概念,流是一个抽象的概念,代表了数据的流动方向。根据数据的传输方向,可将流分为输入流和输出流:
- 输入流(Input Stream):数据从外部设备流向程序,程序读取数据,例如读取文件中的内容到内存。
- 输出流(Output Stream):数据从程序流向外部设备,程序写入数据,例如将内存中的数据写入到文件。
Java IO 体系主要包含两大核心部分:字节流 和字符流,此外还提供了缓冲流、转换流、对象流等功能扩展流,以满足不同的业务需求。
二、字节流(Byte Stream)
字节流是 Java IO 的基础,它以字节(Byte) 为基本单位处理数据,一个字节占 8 位(bit)。字节流可以处理任意类型的文件,包括文本文件、图片、音频、视频等二进制文件,是 Java IO 中最通用的流类型。
字节流的顶层是两个抽象类:InputStream(字节输入流)和 OutputStream(字节输出流),所有的字节输入流都继承自 InputStream,所有的字节输出流都继承自 OutputStream。
1. 字节输入流(InputStream)核心方法
InputStream 是所有字节输入流的父类,定义了字节输入流的通用操作,核心方法如下:
int read():从输入流中读取一个字节的数据,返回读取的字节值(0-255);如果到达流的末尾,返回-1。int read(byte[] b):从输入流中读取一定长度的字节,存储到字节数组b中,返回实际读取的字节数;如果到达流的末尾,返回-1。int read(byte[] b, int off, int len):从输入流中读取长度为len的字节,从字节数组b的下标off开始存储,返回实际读取的字节数。void close():关闭输入流,释放相关的系统资源。注意:流使用完毕后必须关闭,否则会造成资源泄漏。
2. 字节输出流(OutputStream)核心方法
OutputStream 是所有字节输出流的父类,核心方法如下:
void write(int b):将一个字节的数据写入输出流(注意:参数是 int 类型,但实际只写入低 8 位)。void write(byte[] b):将字节数组b中的所有字节写入输出流。void write(byte[] b, int off, int len):将字节数组b中从下标off开始、长度为len的字节写入输出流。void flush():刷新输出流,将缓冲区中的数据强制写入目标设备。对于带缓冲区的输出流,此方法尤为重要。void close():关闭输出流,释放系统资源。关闭前会自动调用flush()方法。
3. 常用字节流实现类
(1)文件字节流:FileInputStream & FileOutputStream
这是最常用的字节流实现类,用于操作本地文件,直接与文件系统交互。
- FileInputStream:从本地文件中读取字节数据。构造方法:
java
// 通过文件路径创建流
FileInputStream fis = new FileInputStream("test.txt");
// 通过 File 对象创建流
File file = new File("test.txt");
FileInputStream fis = new FileInputStream(file);
FileOutputStream :向本地文件中写入字节数据。注意:如果文件不存在,会自动创建;如果文件已存在,默认会覆盖原有内容,也可以通过构造方法指定追加模式。构造方法:
java
// 覆盖模式
FileOutputStream fos = new FileOutputStream("test.txt");
// 追加模式
FileOutputStream fos = new FileOutputStream("test.txt", true);
使用示例:文件复制
java
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
public class FileCopyDemo {
public static void main(String[] args) {
FileInputStream fis = null;
FileOutputStream fos = null;
try {
// 1. 创建输入流和输出流
fis = new FileInputStream("source.jpg");
fos = new FileOutputStream("target.jpg");
// 2. 定义缓冲区,提高读写效率
byte[] buffer = new byte[1024];
int len;
// 3. 循环读取并写入
while ((len = fis.read(buffer)) != -1) {
fos.write(buffer, 0, len);
}
System.out.println("文件复制成功");
} catch (IOException e) {
e.printStackTrace();
} finally {
// 4. 关闭流,先关输出流,再关输入流
try {
if (fos != null) fos.close();
if (fis != null) fis.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
(2)字节数组流:ByteArrayInputStream & ByteArrayOutputStream
字节数组流以内存中的字节数组为操作对象,数据的读取和写入都在内存中完成,不会涉及磁盘 IO,常用于临时数据的处理。
ByteArrayInputStream:从字节数组中读取数据。ByteArrayOutputStream:向内存中的字节数组写入数据,其内部会自动扩容,可通过toByteArray()方法获取写入的字节数组。
java
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
public class ByteArrayStreamDemo {
public static void main(String[] args) throws IOException {
String content = "Hello, Java IO!";
byte[] bytes = content.getBytes();
// 字节数组输入流
ByteArrayInputStream bais = new ByteArrayInputStream(bytes);
// 字节数组输出流
ByteArrayOutputStream baos = new ByteArrayOutputStream();
int len;
byte[] buffer = new byte[10];
while ((len = bais.read(buffer)) != -1) {
baos.write(buffer, 0, len);
}
// 获取写入的字节数组并转换为字符串
String result = baos.toString();
System.out.println(result); // 输出:Hello, Java IO!
// 关闭流(字节数组流关闭后不影响内存数据,可省略,但建议养成习惯)
bais.close();
baos.close();
}
}
三、字符流(Character Stream)
字节流以字节为单位处理数据,在处理文本文件时,需要考虑字符编码(如 UTF-8、GBK),否则容易出现乱码问题。字符流正是为了解决这一问题而设计的,它以字符(Character) 为基本单位处理数据,一个字符通常占 2 个字节(Java 中 char 类型是 16 位),并且会自动处理字符编码问题。
字符流的顶层是两个抽象类:Reader(字符输入流)和 Writer(字符输出流),所有字符输入流都继承自 Reader,所有字符输出流都继承自 Writer。
1. 字符输入流(Reader)核心方法
int read():读取一个字符,返回字符的 Unicode 编码值;如果到达流的末尾,返回-1。int read(char[] cbuf):读取若干字符到字符数组cbuf中,返回实际读取的字符数;末尾返回-1。int read(char[] cbuf, int off, int len):读取长度为len的字符,从字符数组cbuf的下标off开始存储。void close():关闭字符输入流,释放资源。
2. 字符输出流(Writer)核心方法
void write(int c):写入一个字符(参数是 int 类型,实际写入低 16 位)。void write(char[] cbuf):写入字符数组cbuf中的所有字符。void write(char[] cbuf, int off, int len):写入字符数组cbuf中从off开始、长度为len的字符。void write(String str):直接写入一个字符串,这是字符流特有的便捷方法。void write(String str, int off, int len):写入字符串str中从off开始、长度为len的子串。void flush():刷新缓冲区,将数据写入目标设备。void close():关闭输出流,关闭前自动刷新。
3. 常用字符流实现类
(1)文件字符流:FileReader & FileWriter
用于操作本地文本文件,底层会默认使用系统的字符编码,也可以通过指定编码来避免乱码(JDK 11 及以上支持通过构造方法指定编码)。
- FileReader:读取文本文件内容。构造方法:
java
FileReader fr = new FileReader("test.txt");
// 指定编码(JDK 11+)
FileReader fr = new FileReader("test.txt", Charset.forName("UTF-8"));
FileWriter:向文本文件写入内容,支持覆盖和追加模式。构造方法:
java
// 覆盖模式
FileWriter fw = new FileWriter("test.txt");
// 追加模式
FileWriter fw = new FileWriter("test.txt", true);
// 指定编码(JDK 11+)
FileWriter fw = new FileWriter("test.txt", Charset.forName("UTF-8"), true);
使用示例:文本文件读写
java
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
public class FileReaderWriterDemo {
public static void main(String[] args) {
FileReader fr = null;
FileWriter fw = null;
try {
// 1. 创建输入流(读取文件)和输出流(写入文件,追加模式)
fr = new FileReader("source.txt");
fw = new FileWriter("target.txt", true);
// 2. 定义字符缓冲区
char[] cbuf = new char[1024];
int len;
// 3. 循环读取并写入
while ((len = fr.read(cbuf)) != -1) {
fw.write(cbuf, 0, len);
}
System.out.println("文本写入成功");
} catch (IOException e) {
e.printStackTrace();
} finally {
// 4. 关闭流
try {
if (fw != null) fw.close();
if (fr != null) fr.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
(2)缓冲字符流:BufferedReader & BufferedWriter
缓冲字符流是对普通字符流的包装,它在内存中开辟了一个缓冲区,读写数据时先操作缓冲区,减少了与磁盘的交互次数,从而大幅提高读写效率 。同时,BufferedReader 提供了 readLine() 方法,可以一次性读取一行文本,非常适合处理按行存储的文本文件。
-
BufferedReader :包装字符输入流,提供缓冲功能和按行读取方法。构造方法:
java
运行
// 包装 FileReader BufferedReader br = new BufferedReader(new FileReader("test.txt"));特有方法:
String readLine():读取一行文本,返回包含该行内容的字符串;如果到达流的末尾,返回null。 -
BufferedWriter :包装字符输出流,提供缓冲功能和换行方法。构造方法:
java
运行
BufferedWriter bw = new BufferedWriter(new FileWriter("test.txt"));特有方法:
void newLine():写入一个换行符,适配不同操作系统的换行规则(Windows 是\r\n,Linux 是\n)。
使用示例:按行读取文本文件
java
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
public class BufferedReaderDemo {
public static void main(String[] args) {
BufferedReader br = null;
try {
br = new BufferedReader(new FileReader("test.txt"));
String line;
// 按行读取文本,直到末尾
while ((line = br.readLine()) != null) {
System.out.println(line);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (br != null) br.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
四、转换流:InputStreamReader & OutputStreamWriter
转换流是字节流和字符流之间的桥梁,它可以将字节流转换为字符流,并在转换过程中指定字符编码,从而解决字符流的编码问题。
- InputStreamReader :将字节输入流(
InputStream)转换为字符输入流(Reader)。 - OutputStreamWriter :将字节输出流(
OutputStream)转换为字符输出流(Writer)。
构造方法中需要传入字节流对象和指定的字符编码:
java
运行
// 将 FileInputStream 转换为字符输入流,指定 UTF-8 编码
InputStreamReader isr = new InputStreamReader(new FileInputStream("test.txt"), "UTF-8");
// 将 FileOutputStream 转换为字符输出流,指定 GBK 编码
OutputStreamWriter osw = new OutputStreamWriter(new FileOutputStream("test.txt"), "GBK");
使用场景:当需要读取或写入文本文件,且需要明确指定字符编码时,使用转换流可以避免乱码。例如,读取一个 GBK 编码的文本文件:
java
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
public class InputStreamReaderDemo {
public static void main(String[] args) {
InputStreamReader isr = null;
try {
// 指定 GBK 编码读取文件
isr = new InputStreamReader(new FileInputStream("gbk.txt"), "GBK");
char[] cbuf = new char[1024];
int len;
while ((len = isr.read(cbuf)) != -1) {
System.out.println(new String(cbuf, 0, len));
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (isr != null) isr.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
五、缓冲流(Buffered Stream)
缓冲流是对字节流和字符流的增强,它的核心作用是通过缓冲区减少磁盘 IO 次数,提高读写性能。缓冲流分为字节缓冲流 和字符缓冲流。
- 字节缓冲流 :
BufferedInputStream(包装字节输入流)、BufferedOutputStream(包装字节输出流)。 - 字符缓冲流 :
BufferedReader、BufferedWriter(前文已介绍)。
缓冲流的使用规则是包装流:创建缓冲流对象时,传入一个普通的字节流或字符流对象,底层的读写操作由缓冲流优化。
字节缓冲流使用示例:
java
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
public class BufferedStreamDemo {
public static void main(String[] args) {
BufferedInputStream bis = null;
BufferedOutputStream bos = null;
try {
// 包装 FileInputStream 和 FileOutputStream
bis = new BufferedInputStream(new FileInputStream("source.mp4"));
bos = new BufferedOutputStream(new FileOutputStream("target.mp4"));
byte[] buffer = new byte[1024];
int len;
while ((len = bis.read(buffer)) != -1) {
bos.write(buffer, 0, len);
}
System.out.println("视频文件复制成功");
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (bos != null) bos.close();
if (bis != null) bis.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
六、对象流:ObjectInputStream & ObjectOutputStream
对象流用于实现对象的序列化和反序列化。序列化是指将内存中的 Java 对象转换为字节序列,以便存储到文件或通过网络传输;反序列化则是将字节序列恢复为 Java 对象。
要实现序列化,对象所属的类必须实现 Serializable 接口(这是一个标记接口,没有任何方法),否则会抛出 NotSerializableException 异常。
1. 核心方法
- ObjectOutputStream (序列化):
void writeObject(Object obj):将对象写入输出流。 - ObjectInputStream (反序列化):
Object readObject():从输入流中读取对象,返回值需要强制类型转换。
2. 使用示例
步骤 1:定义可序列化的类
java
import java.io.Serializable;
// 实现 Serializable 接口
public class Student implements Serializable {
// 序列化版本号,用于保证序列化和反序列化的类版本一致
private static final long serialVersionUID = 1L;
private String name;
private int age;
public Student(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public String toString() {
return "Student{name='" + name + "', age=" + age + "}";
}
}
步骤 2:对象的序列化和反序列化
java
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
public class ObjectStreamDemo {
public static void main(String[] args) throws Exception {
// 1. 序列化:将 Student 对象写入文件
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("student.txt"));
oos.writeObject(new Student("张三", 20));
oos.close();
// 2. 反序列化:从文件中读取 Student 对象
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("student.txt"));
Student student = (Student) ois.readObject();
System.out.println(student); // 输出:Student{name='张三', age=20}
ois.close();
}
}
七、Java IO 核心注意事项
八、字节流与字符流的区别
| 对比维度 | 字节流 | 字符流 |
|---|---|---|
| 数据单位 | 字节(8 位) | 字符(16 位) |
| 处理对象 | 所有文件(文本、图片、视频等) | 仅文本文件 |
| 编码处理 | 不处理编码,可能出现乱码 | 自动处理编码,避免乱码 |
| 顶层抽象类 | InputStream、OutputStream |
Reader、Writer |
| 特有方法 | 无 | readLine()、write(String)、newLine() |
-
资源关闭 :流使用完毕后必须调用
close()方法关闭,否则会造成系统资源泄漏。在 JDK 7 及以上,可以使用 try-with-resources 语法,自动关闭实现了AutoCloseable接口的资源,简化代码:javatry (FileInputStream fis = new FileInputStream("test.txt"); FileOutputStream fos = new FileOutputStream("test_copy.txt")) { byte[] buffer = new byte[1024]; int len; while ((len = fis.read(buffer)) != -1) { fos.write(buffer, 0, len); } } catch (IOException e) { e.printStackTrace(); } -
编码问题:处理文本文件时,优先使用字符流或转换流,并明确指定字符编码(如 UTF-8),避免因系统默认编码不同导致乱码。
-
缓冲区刷新 :带缓冲区的输出流(如
BufferedOutputStream、BufferedWriter),在数据写入后需要调用flush()方法,确保缓冲区中的数据全部写入目标设备。 -
序列化注意事项 :
- 类的
serialVersionUID建议显式声明,避免类结构变化后反序列化失败。 - 被
transient修饰的成员变量不会被序列化。
- 类的