程序中可以用变量、数组、对象、集合存储数据,但是这些数据都是存储在内存中,电脑断电后数据将不存在。如果要持久化存储,必须将数据保存在硬盘的文件中。Java提供File类对文件和目录进行操作,但是对于文件本身的数据的存储和读取,需要IO流。
IO流中的I是指input输入,O指output输出,IO流是指像水流一样将程序中的数据输入或输出数据到文件中,对文件数据进行操作。
input输入,output输出,是相对于程序而言的,从文件中读取数据到内存,是input输入。程序将数据从内存中存入到文件,是output输出。
从数据流向可以划分为输入流、输出流,从数据类型可以划分为字节流、字符流,两两组合可划分为:输入字节流、输出字节流、输入字符流、输出字符流。对应Java中四种抽象类:
- InputStream:输入字节流
- OutputStream:输出字节流
- Reader:输入字符流
- Writer:输出字符流
IO流不仅可以建立程序与文件的数据管道,也可以建立程序与网络存储的数据管道,因此IO流有很多针对不同存储类型的实现类,比如针对文件的实现类为:FileInputStream、FileOutputStream、FileReader、FileWriter。
FileInputStream文件输入字节流
FileInputStream文件输入字节流,顾名思义,是建立与本地文件的,将数据从文件获取到内存的,按照字节进行读取的管道。
FileInputStream构造方法支持通过文件路径字符串(类似File对象构造方法)和通过File对象两种方式创建FileInputStream对象:
java
public class App2 {
public static void main(String[] args) throws IOException {
// 创建文件输入字节流对象
// 方式1:通过文件路径字符串创建
InputStream is1 = new FileInputStream("test.md");
// 方式2:通过文件对象创建
File file = new File("test.md");
InputStream is2 = new FileInputStream(file);
is1.close();
is2.close();
}
}
InputStream抽象类提供三种读取文件数据的方法:
java
int read() // 读取一个字节,返回该字节二进制对应的int类型数字。数据为空时,返回-1
int read(byte b[]) // 读取字节数组长度的字节数量的数据,存储到字节数组,并返回这次读取的字节长度。数据为空时,返回-1
int read(byte b[], int off, int len) // 读取len长度字节的数据,存储到字节数组中,要求从off下标开始存储。
调用int read()会读取一个字节,并返回该字节二进制对应的int类型数字。方法调用后,"指针"会往后移,再次调用方法,会读取下一个字节的数据。
java
public class App3 {
public static void main(String[] args) throws IOException {
// 创建文件输入字节流对象
InputStream is = new FileInputStream("test.md"); // 文件内存储内容:abc
// 一个字节一个字节的读取数据
int data = is.read(); // 读取一个字节,返回该字节二进制对应的int类型数字
System.out.println(data); // 97, 字符a的ASCII码为97,字符集UTF-8兼容ASCII
System.out.println(is.read()); // 98
// 关闭字节流对象
is.close();
}
}
当文件数据全部读完,read()方法会返回-1,作为结束的标志。
一个字节一个字节的读取数据,效率比较低。因此,一般用int read(byte b[])批量读取数据的方法。
java
public class App4 {
public static void main(String[] args) throws IOException {
// 创建一个文件输入字节流对象
InputStream is = new FileInputStream("test.md"); // 文件内存储内容:abcdefghijklmnopqrstuvwxyz
// 创建一个字节数组,用于存储读取到的数据
byte[] buffer = new byte[3]; // 创建一个字节数组,长度为3
// 循环多次从字节流对象中读取数据,并打印出来
while (is.read(buffer) != -1) { // 当数据为空时,返回-1,可以作为循环的结束条件
System.out.print(new String(buffer)); // 将字节数组转换成字符串并打印出来
}
// 关闭字节流对象
is.close();
}
}
InputStream不常用的字节读取方法int read(byte b[], int off, int len),可读取一定长度的字节数据,按一定偏移量存储到字节数组中。
java
public class App5 {
public static void main(String[] args) throws IOException {
// 创建一个文件输入字节流对象
InputStream is = new FileInputStream("test.md"); // 文件内存储内容:abcdefghijklmnopqrstuvwxyz
// 创建一个字节数组,用于存储读取到的数据
byte[] buffer = new byte[6]; // 创建一个字节数组,长度为3
int num = is.read(buffer, 1, 3); // 读取字节流对象中的数据,并保存在字节数组中
System.out.println("读取的字节数为:" + num); // 3 输出读取的字节数
// 循环打印读取的字节数组,可以通过字节数组的索引值获取字节数据
for (int i = 0; i < buffer.length; i++) {
System.out.println("字节数组索引" + i + "存储的数据为: " + buffer[i]);
}
// 关闭字节流对象
is.close();
}
}
shell
读取的字节数为:3
字节数组索引0存储的数据为: 0
字节数组索引1存储的数据为: 97
字节数组索引2存储的数据为: 98
字节数组索引3存储的数据为: 99
字节数组索引4存储的数据为: 0
字节数组索引5存储的数据为: 0
进程已结束,退出代码为 0
某些时候,我们可能有读取文件所有字节的需求,Java提供一个byte[] readAllBytes()方法,读取输入流中剩余的所有字节:
java
public class App6 {
public static void main(String[] args) throws IOException {
// 创建一个文件输入字节流对象
InputStream is = new FileInputStream("test.md"); // 文件内存储内容:abcdefghijklmnopqrstuvwxyz
// 读取文件所有字节
byte[] bytes= is.readAllBytes();
System.out.println(new String(bytes)); // abcdefghijklmnopqrstuvwxyz
// 关闭字节流对象
is.close();
}
}
最后的最后,一定别忘了关闭字节流对象,释放系统资源。
FileOutputStream文件输出字节流
FileOutputStream文件输出字节流与FileInputStream文件输入字节流相似,仅方向相反,是建立与本地文件的,从内存中往文件中写入数据,按照字节进行写入的管道。
与FileInputStream相似,FileOutputStream支持通过文件路径字符串和File对象两种方式创建FileOutputStream对象。除此之外,FileInputStream还支持覆盖和追加两种模式。因此,有4种构造方法:
java
FileOutputStream(String name) // 通过文件路径字符串创建对象,默认为覆盖模式
FileOutputStream(String name, boolean append) // 通过文件路径字符串创建对象,append形参为true为追加模式
FileOutputStream(File file) // 通过File对象创建字节输出流对象,默认为覆盖模式
FileOutputStream(File file, boolean append) // 通过File对象创建字节输出流对象,append形参为true为追加模式
同InputStream抽象类相似,OutputStream提供三种写入文件数据的方法:
java
void write(int b) // 写入一个字节到文件
void write(byte b[]) // 将字节数组中的数据,写入到文件
void write(byte b[], int off, int len) // 将字节数组中从off索引开始,写入长度为len的字节数据到文件
操作示例如下:
java
public class App7 {
public static void main(String[] args) throws IOException {
// 创建文件输出流对象
OutputStream os = new FileOutputStream("outputfile.txt"); // 默认为覆盖模式
// 写入方式1:写入一个字节到文件
os.write(65); // 写入A
// 写入方式2:将字节数组中的数据,写入到文件
byte[] bytes = {66, 67, 68, 69};
os.write(bytes); // 写入BCDE
// 写入方式3:将字节数组中从off索引开始,写入长度为len的字节数据到文件
os.write(bytes, 1, 2); // 写入CD
// 关闭文件输出流对象
os.close();
System.out.println("文件写入成功!");
/*
打印上述写入的内容
*/
// 创建文件输入流对象
InputStream is = new FileInputStream("outputfile.txt");
// 打印文件内容
byte[] all = is.readAllBytes();
for (byte b : all) {
System.out.print((char)b);
} // ABCDECD
is.close();
}
}
使用文件输入字节流和文件输出字节流,可是实现复制文件的操作:
java
public class App8 {
public static void main(String[] args) throws IOException {
/*
使用文件输入字节流和文件输出字节流,复制一个文件
*/
// 创建一个文件输入字节流对象
InputStream is = new FileInputStream("/Users/zhongziqiang/Downloads/33_1.png");
// 创建一个文件输出字节流对象
FileOutputStream fos = new FileOutputStream("/Users/zhongziqiang/Downloads/33_1_copy.png");
// 创建一个字节数组,长度为1024, 用于存储读取到的数据(存储1KB数据)
byte[] buffer = new byte[1024];
int len; // 存储读取到的字节数
// 循环读取文件输入字节流对象中的数据到buffer,并从buffer写入文件输出字节流对象中
while ((len = is.read(buffer)) != -1) {
fos.write(buffer, 0, len);
}
// 关闭文件输入字节流对象和文件输出字节流对象
fos.close();
is.close();
}
}
IO流异常处理
IO流操作可能抛出各类异常,Exception甚至有一类专门的IO异常类IOException。
IO流操作过程中发生异常,需要处理异常,并且需要在各类场景下,要调用close()方法关闭IO流。因此Java提供try-catch-finnally结构处理IO流异常。
下方是使用输入、输出字节流复制文件的程序,捕获了创建字节输入和输出流可能抛出的FileNotFoundException异常、读取和写入数据可能抛出的IOException异常、关闭流可能抛出的IOException异常等。
java
public class App3 {
public static void main(String[] args) {
String sourceFile = "/Users/zhongziqiang/Downloads/Set-Interface-in-Java.webp"; // 源文件路径
String destFile = "/Users/zhongziqiang/Downloads/Set-Interface-in-Java_copy.webp"; // 目标文件路径
FileInputStream fis = null;
FileOutputStream fos = null;
try {
// 1. 创建字节输入流和输出流
fis = new FileInputStream(sourceFile);
fos = new FileOutputStream(destFile);
// 2. 创建缓冲区(一次读取1KB)
byte[] buffer = new byte[1024];
int bytesRead;
// 3. 循环读取并写入数据
while ((bytesRead = fis.read(buffer)) != -1) {
fos.write(buffer, 0, bytesRead);
}
System.out.println("文件复制成功!");
} catch (IOException e) {
System.err.println("文件复制过程中发生错误: " + e.getMessage());
e.printStackTrace();
} finally {
// 4. 在finally块中确保流被关闭
// 代码块非常臃肿
try {
if (fis != null) {
fis.close();
}
} catch (IOException e) {
System.err.println("关闭输入流时出错: " + e.getMessage());
}
try {
if (fos != null) {
fos.close();
}
} catch (IOException e) {
System.err.println("关闭输出流时出错: " + e.getMessage());
}
}
}
}
由于要保证异常运行时也要关闭IO流,要使用finnally代码块,又要捕获调用close()方法时的异常,并且要考虑到字节流对象变量可能是null的场景,因此finally块特别臃肿,可读性很差。
Java7引入了"try-with-resource"的结构,来代替"try-catch-finnally",可以简化代码。具体来说,就是在异常捕获try后增加一个括号,用来编写资源的定义语句。在发生异常后,系统会自动释放资源。
java
try(/* 定义资源、发生异常时系统自动释放资源 */) {
// 可能发生异常的代码块
} catch (Exception e) {
// 处理异常的代码块
}
原复制文件的程序可以改造为:
java
public class App4 {
public static void main(String[] args) {
String sourceFile = "/Users/zhongziqiang/Downloads/Set-Interface-in-Java.webp"; // 源文件路径
String destFile = "/Users/zhongziqiang/Downloads/Set-Interface-in-Java_copy.webp"; // 目标文件路径
try (
// 1. 创建字节输入流和输出流,发生异常时,fis和fos会自动关闭
FileInputStream fis = new FileInputStream(sourceFile);
FileOutputStream fos = new FileOutputStream(destFile);
) {
// 2. 创建缓冲区(一次读取1KB)
byte[] buffer = new byte[1024];
int bytesRead;
// 3. 循环读取并写入数据
while ((bytesRead = fis.read(buffer)) != -1) {
fos.write(buffer, 0, bytesRead);
}
System.out.println("文件复制成功!");
} catch (IOException e) {
System.err.println("文件复制过程中发生错误: " + e.getMessage());
e.printStackTrace();
}
}
}
FileReader文件输入字符流
字节流在处理纯文本文件时,由于主流字符集编码常用可变编码,比如"UTF-8"、"GBK",因此在读取字节并处理时,容易截断编码的字节,出现乱码。
对于纯文本文件的IO,Java推荐使用字符流。
对于读取计算机本地文件的文件输入字符流是FileReader实现类。与FileInputStream文件输入字节流相似,FileReader支持通过文件路径字符串(类似File对象构造方法)和通过File对象两种方式创建对象:
java
public class App5 {
public static void main(String[] args) throws IOException {
// 方式1:通过文件路径字符串创建文件输入字符流对象
Reader reader = new FileReader("test.md");
reader.close();
// 方式2:通过文件对象创建文件输入字符流对象
File file = new File("test.md");
Reader reader2 = new FileReader(file);
reader2.close();
}
}
与FileInputStream文件输入字节流相似,FileReader支持多种读取字符的read()方法。与FileInputStream文件输入字节流读取的最小单位是字节不同,FileReader文件输入字符流读取的最小单位是字符。并且从纯文本文件中读取字符时,使用UTF-16编码,在内存中用char存储。
java
public class App5 {
public static void main(String[] args) throws IOException {
/*
从纯文本文件test.md中读取数据
*/
try(Reader reader = new FileReader("test.md")) {
// 读取方法1:一次读取一个字符
int ch;
ch = reader.read(); // 读取一个字符,返回的字符的Unicode码点,如果没有数据,则返回-1
System.out.print((char) ch); // 打印一个字符
// 读取方法2:一次读取多个字符
char[] buffer = new char[3];
reader.read(buffer); // 读取多个字符到字符数组,返回读取的字符数,如果没有数据,则返回-1
System.out.print(buffer);
// 读取方法3:一次读取多个字符,可指定偏移量和长度
reader.read(buffer, 0, 3); // 读取多个字符到字符数组,返回读取的字符数,如果没有数据,则返回-1。形参1:字符数组,形参2:起始索引,形参3:读取的字符数
System.out.print(buffer);
} catch (IOException e) {
// 捕获异常后:打印"文件读取过程中发生错误",终止程序
System.out.println("文件读取过程中发生错误");
return;
}
}
}
最后提醒,使用try-with-resource结构,让系统自动处理异常并关闭流。
注意:
FileReader有一个重要缺陷:它使用平台默认的字符集(如Windows中文版可能是GBK,Linux可能是UTF-8)来读取文件字节并解码为字符。它不会自动检测文件本身的编码(如UTF-8),也不支持在创建文件输入字符流对象时指定编码。这可能导致读取使用UTF-8等编码保存的文件时产生乱码。因此,在明确知道文件编码(尤其是非默认编码)的情况下,更推荐使用InputStreamReader并指定字符集编码,例如new InputStreamReader(new FileInputStream("test.md"), StandardCharsets.UTF_8)
FileWriter文件输出字符流
FileWriter文件输出字符流与FileReader文件输入字符流相似,仅方向相反,是建立与本地文件的,从内存中往文件中写入数据,按照字符进行写入的管道。
与FileOutputStream文件输出字节流相似,FileWriter文件输出字符流支持通过文件路径字符串和File对象两种方式创建对象,除此之外,还支持覆盖和追加两种模式。因此,有4种构造方法:
java
FileWriter(String fileName) // 通过文件路径字符串创建对象,默认为覆盖模式
FileWriter(String fileName, boolean append) // 通过文件路径字符串创建对象,append形参为true为追加模式
FileWriter(File file) // 通过File对象创建字节输出流对象,默认为覆盖模式
FileWriter(File file, boolean append) // 通过File对象创建字节输出流对象,append形参为true为追加模式
与FileOutputStream文件输出字节流相似,FileWriter提供三种写入文件字符数据的方法:
java
void write(int c) // 写入一个字符到文件,c为写入字符的Unicode码点
void write(char cbuf[]) // 将char数组中的数据,写入到文件
void write(char cbuf[], int off, int len)// 将char数组中的数据,并指定写入的起始索引和结束索引
void write(String str) // 将字符串写入到文件
void write(String str, int off, int len) // 写入一个字符串,并指定写入的字符串的起始索引和结束索引
操作示例如下:
java
public class App6 {
public static void main(String[] args) throws IOException {
Writer writer = new FileWriter("test2.md"); // 创建一个文件输入字符流对象,为覆盖模式。若文件不存在,则会创建该文件。
// 写入方式1:一次写入一个字符
writer.write(97); // 写入字符 a,因为a的Unicode码点为97
// 写入方式2:写入一个字符数组
char[] chars = {'b', 'c', 'd'};
writer.write(chars); // 写入字符数组 chars
// 写入方式3:写入一个字符数组,并指定写入的起始索引和结束索引
writer.write(chars, 1, 2);
// 写入方式4:写入一个字符串
writer.write("efg");
// 写入方式5:写入一个字符串,并指定写入的字符串的起始索引和结束索引
writer.write("hijk", 1, 2);
writer.close();
System.out.println("写入成功!");
}
}
使用字符流复制纯文本文件:
java
public class App7 {
/*
使用字符流复制一个纯文本文件
*/ public static void main(String[] args) {
try(
Reader reader = new FileReader("/Users/zhongziqiang/Downloads/demo.md");
Writer writer = new FileWriter("/Users/zhongziqiang/Downloads/demo_copy.md")
) {
char[] buffer = new char[3];
int len;
while ((len = reader.read(buffer)) != -1) {
writer.write(buffer, 0, len); // 只写入读取到的 len 个字符
}
} catch (IOException e) {
e.printStackTrace();
System.out.println("复制过程中发生错误");
return;
}
}
}