Java 常用的 IO 流主要有 3 种:字节流、字符流、转换流。其中对于字节流和字符流,其内部又分为低级流和高级流。
对于 Java 常用的 IO 流来说,我们重点掌握高级流就可以了,低级流在实际工作中很少用到。低级流的绝大部分用途,就是实例化后作为参数传递给高级流,高级流封装了低级流之后,提供了很多实用的方法,不但简化了我们对 IO 流的编码操作,也极大的提高的读写效率,所以我们在实际工作中,尽量使用高级流。
下面我们就一起演示一下这 3 种 IO 流(字节流、字符流、转换流)的使用方式吧。
一、字节流
字节流有两个抽象基类:
- InputStream:这个抽象类是字节输入流的父类
- OutputStream:这个抽象类是字节输出流的父类
在介绍字节流之前,我们首先要明确一点:字节流操作类是以内存为主角的,具体细节如下:
字节流的 Input (输入、读操作) :是指数据输入到内存,或者内存读取数据。
字节流的 Output (输出、写操作):是指内存输出数据,或者数据从内存写入到硬盘文件等。
理解了 Input 和 Output 的概念后,下面介绍字节流的低级流和高级流的使用,就比较容易理解了。
字节流的低级流:FileInputStream 和 FileOutputStream
字节流的高级流(字节缓冲流):BufferedInputStream 和 BufferedOutputStream
下面我们使用代码,以复制文件为例,演示一下这两种字节流的使用方法:
import java.io.*;
public class ByteIOTest {
public static void main(String[] args) {
//低级流逐个字节复制
long start1 = System.currentTimeMillis();
try (FileInputStream fis = new FileInputStream("D:\\天蓬风云录.avi");
FileOutputStream fos = new FileOutputStream("D:\\宫廷内斗史01.avi")) {
int b;
while ((b = fis.read()) != -1) {
fos.write(b);
}
} catch (IOException e) {
e.printStackTrace();
}
long end1 = System.currentTimeMillis();
System.out.println("低级流【逐个字节】复制文件耗时:" + (end1 - start1) + "毫秒");
//----------------------------------------
//低级流采用数组复制
long start2 = System.currentTimeMillis();
try (FileInputStream fis = new FileInputStream("D:\\天蓬风云录.avi");
FileOutputStream fos = new FileOutputStream("D:\\宫廷内斗史02.avi")) {
byte[] bArr = new byte[1024];
int len;
while ((len = fis.read(bArr)) != -1) {
fos.write(bArr, 0, len);
}
} catch (IOException e) {
e.printStackTrace();
}
long end2 = System.currentTimeMillis();
System.out.println("低级流【采用数组】复制文件耗时:" + (end2 - start2) + "毫秒");
//----------------------------------------
//高级流逐个字节复制
long start3 = System.currentTimeMillis();
//低级字节流对象,作为高级字节流实例化的参数
try (BufferedInputStream bis = new BufferedInputStream(
new FileInputStream("D:\\天蓬风云录.avi"));
BufferedOutputStream bos = new BufferedOutputStream(
new FileOutputStream("D:\\宫廷内斗史03.avi"))) {
int b;
while ((b = bis.read()) != -1) {
bos.write(b);
}
} catch (IOException e) {
e.printStackTrace();
}
long end3 = System.currentTimeMillis();
System.out.println("高级流【逐个字节】复制时间:" + (end3 - start3) + "毫秒");
//----------------------------------------
//高级流采用数组复制
long start4 = System.currentTimeMillis();
//低级字节流对象,作为高级字节流实例化的参数
try (BufferedInputStream bis = new BufferedInputStream(
new FileInputStream("D:\\天蓬风云录.avi"));
BufferedOutputStream bos = new BufferedOutputStream(
new FileOutputStream("D:\\宫廷内斗史04.avi"))) {
byte[] bArr = new byte[1024];
int len;
while ((len = bis.read(bArr)) != -1) {
bos.write(bArr, 0, len);
}
} catch (IOException e) {
e.printStackTrace();
}
long end4 = System.currentTimeMillis();
System.out.println("高级流【采用数组】复制时间:" + (end4 - start4) + "毫秒");
}
}
/*
打印出的结果如下所示:
低级流【逐个字节】复制文件耗时:13394毫秒
低级流【采用数组】复制文件耗时:29毫秒
高级流【逐个字节】复制时间:12毫秒
高级流【采用数组】复制时间:3毫秒
*/
上面要复制的文件(天蓬风云录.avi)总大小为 1.13M,从代码的执行结果就能够很轻松的对比出执行效率:
【低级字节流】即使采用数组进行字节缓冲,其性能效率也比【高级字节流】逐个字节复制要慢一倍以上。
其实高级字节流(字节缓冲流)即使采用逐个字节复制,其底层也是采用数组进行缓冲处理的。在不考虑底层实现的情况下,我们在使用高级流复制文件时,继续使用数组缓冲字节,其执行效率又能实现极大的提高(从上面的执行结果就很容易发现)。
在 JDK8 (包含 JDK8) 以后,代码中使用 try 的小括号包裹 IO 流对象,可以不用考虑 IO 流的关闭,代码执行出 try 的代码块后,IO 流会自动释放关闭(即使 IO 流对象在执行过程中出现异常,也会自动关闭 IO 流),这个特性比较类似于 C# 语言中的 Using 关键字,使用起来比较方便。
二、字符流
字符流主要用于读写字符,因此这就涉及到字符集,中文版 Windows 操作系统的默认字符集是 GBK,除此之外我们常见的字符集有 ASCII、GB2312,Unicode 等字符集,其中最常用的就是 Unicode 中的 UTF-8 编码。字符流相比字节流而言,比较容易理解,使用很频繁,而且非常容易。
字符流有两个抽象类:
- Reader:这个抽象类是读取字符流的父类
- Writer:这个抽象类写入字符流的父类
字符流的两个低级流为:FileReader 和 FileWriter
字符流的两个高级流(字符缓冲流)为:BufferedReader 和 BufferedWriter
还是那句话,低级流一般很少用,主要用途就是作为高级流的实例化参数,我们经常使用的是高级流。字符流的低级流,在实际测试中,其执行效率同样也是远远低于字符高级流(字符缓冲流),这里就进行代码演示了。这里只演示 BufferedReader 和 BufferedWriter 这两个类的使用。
BufferedReader 有一个非常常用的特有的方法:
方法名 | 说明 |
---|---|
String readLine() | 读去文件中的一行文字。 不包括任何行终止字符,如果已经到达流的结尾,则返回 null |
BufferedWriter 有一个非常常用的特有的方法:
方法名 | 说明 |
---|---|
void newLine() | 写一个换行符,换行符会根据操作系统而定。windows 是 \r\n,linux 是 \r ,mac 是 \n |
代码示例如下:
import java.io.*;
//将一个文本文件的内容,写到另一个文本文件中
public class CharIOTest {
public static void main(String[] args) {
try (BufferedReader br = new BufferedReader(new FileReader("d:\\天蓬风云录.txt"));
BufferedWriter bw = new BufferedWriter(new FileWriter("d:\\天蓬宫廷历险记.txt"))) {
String line;
//一次读取一行,直到文件末尾为止
while((line = br.readLine()) != null){
bw.write(line);
bw.newLine(); //写一个换行符
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
另外 BufferedReader 也可以代替 Scanner 等待用户在控制台中输入一行字符串,建议使用 BufferedReader ,毕竟是 BufferedReader 是比较高级的类,执行效率比 Scanner 高,功能比 Scanner 要强大一些。具体细节这里就不演示了,这里这列出两个代码实现方式:
import java.io.*;
import java.util.Scanner;
public class CharIOTest {
public static void main(String[] args) {
//使用 Scanner 实现用户输入与控制台的交互
try (Scanner sc = new Scanner(System.in);
BufferedWriter bw = new BufferedWriter(new FileWriter("d:\\人员信息01.txt"))) {
System.out.println("请录入姓名:");
String username = sc.next();
System.out.println("请录入年龄:");
String userage = sc.next();
//将【姓名】和【年龄】写入文件中
bw.write(username);
bw.newLine();
bw.write(userage);
} catch (IOException e) {
e.printStackTrace();
}
//----------------------------------------
//使用 BufferedReader 实现用户输入与控制台的交互
try (BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
BufferedWriter bw = new BufferedWriter(new FileWriter("d:\\人员信息02.txt"))) {
System.out.println("请输入你的姓名:");
String name = br.readLine();
System.out.println("请输入你的年龄:");
String age = br.readLine();
//将【姓名】和【年龄】写入文件中
bw.write(name);
bw.newLine();
bw.write(age);
} catch (Exception e) {
e.printStackTrace();
}
}
}
三、字符转换流
字符转换流的作用就是:
- 在读取字符文件时,可以按照指定的字符集编码进行读取,具体的类是:InputStreamReader
- 在写入字符文件时,可以按照指定的字符集编码进行写入,具体的类是:OutputStreamWriter
我们在上面的所有代码示例中,使用的都是 IDEA 工具,从 IDEA 工具的右下角可以发现,默认使用的是 UTF-8 编码:
如上图所示:如果你新建一个 aaa.txt 文件,用记事本打开,录入一些中文字符,保存的时候使用 ANSI 编码保存,ANSI 编码是 Windows 系统的默认编码,对于中文的 Windows 操作系统,其默认编码是 GBK ,因此采用 ANSI 编码保存,也就是采用 GBK 保存所录入的中文字符。
然后运行上图中的代码,生成一个 bbb.txt 文件,打开 bbb.txt 就会发现是乱码(无论你使用记事本代开,还是用 Notepad++ 等其它第三方软件打开,无论你怎么调整编码,显示的都是乱码),导致 bbb.txt 是乱码的原因,就是 bbb.txt 是 GBK 编码格式保存的,Java 程序是以 UTF-8 读取的,读取编码错误。
怎么解决以上问题呢?答案就是我们的 Java 程序需要使用 GBK 读取 aaa.txt 文件(这一点很重要,读取的时候必须采用正确的字符集编码,否则后续无论使用什么字符集编码写入 bbb.txt 文件,都是乱码)
InputStreamReader 和 OutputStreamWriter 的构造方法为:
方法名 | 说明 |
---|---|
InputStreamReader(InputStream in) | 使用默认字符编码创建 InputStreamReader 对象 |
InputStreamReader(InputStream in,String chatset) | 使用指定的字符编码创建 InputStreamReader 对象 |
OutputStreamWriter(OutputStream out) | 使用默认字符编码创建 OutputStreamWriter 对象 |
OutputStreamWriter(OutputStream out,String charset) | 使用指定的字符编码创建 OutputStreamWriter 对象 |
下面我们采用 GBK 编码读取 aaa.txt 文件,采用 GB2312 格式写入 bbb.txt 文件,解决 bbb.txt 文本内容乱码问题:
import java.io.*;
public class CharIOTest {
public static void main(String[] args) {
/*
//下面的这段代码,采用的是 UTF-8 读取 aaa.txt,采用 UTF-8 写入 bbb.txt
//由于读取 aaa.txt 采用的是 GBK 编码,所以 UTF-8 读取出来本身就是乱码
//所以导致生成的 bbb.txt 无论用什么软件打开,无论如何转换 bbb.txt 编码格式,都显示乱码
try (BufferedReader br = new BufferedReader(
new FileReader("d:\\aaa.txt"));
BufferedWriter bw = new BufferedWriter(
new FileWriter("d:\\bbb.txt"))) {
String line;
while ((line = br.readLine()) != null) {
bw.write(line);
bw.newLine();
}
} catch (Exception e) {
e.printStackTrace();
}
*/
//----------------------------------------
//采用 InputStreamReader 以 GBK 编码读取 aaa.txt
//采用 OutputStreamWriter 以 GB2312 编码生成 bbb.txt
//传入的参数:字符集编码字符串不区分大小写,比如:gbk 和 GBK 是一样的效果
//以下代码解决了 bbb.txt 乱码问题
try (BufferedReader br = new BufferedReader(
new InputStreamReader(new FileInputStream("d:\\aaa.txt"), "gbk"));
BufferedWriter bw = new BufferedWriter(
new OutputStreamWriter(new FileOutputStream("d:\\bbb.txt"), "gb2312"))) {
String line;
while ((line = br.readLine()) != null) {
bw.write(line);
bw.newLine();
}
} catch (Exception e) {
e.printStackTrace();
}
//上面演示的代码,在生成 bbb.txt 时,你可以使用任意字符集编码,
//只要你读取 aaa.txt 的时候,使用的是正确的字符集编码读取就行。
}
}
到此为止,有关 Java 的常用 IO 流操作,已经演示的差不多了,以上代码都经过测试无误,希望大家能够轻松看懂。
最后的总结就是:处理字符文件,优先使用字符流和转换流;处理音频、视频、图片等文件可以使用字节流,字节流是万能流。