不能不懂的 IO 处理流
我们在掌握了 File 类、字节流、字符流,学会了 IO 操作的套路之后,IO 操作基本上就能处理日常工作中80%的常用问题了。
今天再给大家介绍一下处理流,学会处理流之后,日常工作中的文件操作就都可以应对了,掌握了下面的处理流,你将如虎添翼。
我们知道从 IO 流的功能来划分,IO 流分为:节点流和处理流。其中,节点流是用来包装数据源(File)的,它直接和数据源连接,表示从一个节点读取数据或者把数据写入到一个节点;处理流是用来包装节点流的,它是对一个已经存在的节点流进行连接,处理流通过增加缓存的方式来提高输入输出操作的性能。
处理流按照功能划分,可以分为:缓冲流、转换流、数据处理流、对象处理流。缓冲流是为了提高处理性能的,转换流是字节流转换为字符流用于处理乱码的(解码与编码的字符集问题),数据处理流就是对 8个基本类型和字符串类型数据的直接处理,对象数据处理流就是经常说的序列化与反序列化操作。
一、缓冲流
1、认识字节缓冲流
字节缓冲流就是用缓冲流包裹字节流,也就是说在创建缓冲流对象的时候,需要传入一个字节流的对象,同时会创建一个默认 8KB 的字节数组的缓冲区,通过这个缓冲区进行读写操作,以减少 IO 的次数,从而提高字节流的处理性能。
字节缓冲流分为:字节输入缓冲流 BufferedInputStream 和字节输出缓冲流 BufferedOutputStream,以下代码是字符缓冲流的源码:
// 字节输入缓冲流的构造方法
private static int DEFAULT_BUFFER_SIZE = 8192; // 默认8KB
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]; // 建立缓冲区
}
// 节输出缓冲流的构造方法
public BufferedOutputStream(OutputStream out) {
this(out, 8192); // 默认8KB
}
public BufferedOutputStream(OutputStream out, int size) {
super(out);
if (size <= 0) {
throw new IllegalArgumentException("Buffer size <= 0");
}
buf = new byte[size]; // 建立缓冲区
}
2、用字节缓冲流实现文件拷贝功能
@Test
public void testCopy() throws IOException {
// 1、使用File类与文件建立联系
File src = new File("D:/file/image/tomcat.png");
File dest = new File("D:/file/image/tomcat2.jpg");
// 2、选择对应的输入流或者输出流
InputStream is = new BufferedInputStream(new FileInputStream(src)); // 用缓冲流包裹节点流
OutputStream os = new BufferedOutputStream(new FileOutputStream(dest)); // 用缓冲流包裹节点流
// 3、进行读写操作
byte[] b = new byte[1024];
int len = 0;
while ((len = is.read(b)) != -1) {
os.write(b, 0, len);
}
os.flush();
// 4、关闭资源
os.close();
is.close();
}
运行结果:
3、认识字符缓存流
字符缓冲流就是用缓冲流包裹字符流,也就是说在创建缓冲流对象的时候,需要传入一个字符流的对象,同时会创建一个默认 8KB 的字符数组的缓冲区,通过这个缓冲区进行读写操作,以减少 IO 的次数,从而提高字符流的处理性能。
字符缓冲流分为:字符输入缓冲流 BufferedReader 和字符输出缓冲流 BufferedWriter,以下代码是字符缓冲流的源码:
// 字符输入缓冲流的构造方法
private static int defaultCharBufferSize = 8192; // 默认8KB
public BufferedReader(Reader in) {
this(in, defaultCharBufferSize);
}
public BufferedReader(Reader in, int sz) {
super(in);
if (sz <= 0)
throw new IllegalArgumentException("Buffer size <= 0");
this.in = in;
cb = new char[sz]; // 建立缓冲区
nextChar = nChars = 0;
}
// 字符输出缓冲流的构造方法
private static int defaultCharBufferSize = 8192; // 默认8KB
public BufferedWriter(Writer out) {
this(out, defaultCharBufferSize);
}
public BufferedWriter(Writer out, int sz) {
super(out);
if (sz <= 0)
throw new IllegalArgumentException("Buffer size <= 0");
this.out = out;
cb = new char[sz]; // 建立缓冲区
nChars = sz;
nextChar = 0;
lineSeparator = java.security.AccessController.doPrivileged(
new sun.security.action.GetPropertyAction("line.separator"));
}
4、用字符缓冲流实现文件拷贝功能
/**
* 字符缓冲流只能处理纯文本的copy
*/
@Test
public void testCopy1() throws IOException {
// 1、使用File类与文件建立联系
File src = new File("D:/file/txt/output_char.txt");
File dest = new File("D:/file/image/output_char_coppy.txt");
// 2、选择对应的输入流或者输出流
// 想使用新增的readLine方法(不能发生多态)
BufferedReader reader = new BufferedReader(new FileReader(src));// 用缓冲流包裹节点流
BufferedWriter writer = new BufferedWriter(new FileWriter(dest, true));// 用缓冲流包裹节点流
// 3、进行读或写操作
String line = null;
while ((line = reader.readLine()) != null) {
writer.write(line);
writer.newLine(); // 类似于writer.append("\r\n");
}
writer.flush(); // 强制刷出
// 4、关闭资源
writer.close();
reader.close();
}
运行结果:
因为缓冲流可以提高文件操作的性能,所以在以后的开发中,大家尽量要用缓冲流对节点流进行包装,不要直接使用字节流和字符流去操作文件。
通过以上的代码大家再来体会一下 IO 流操作的套路,是不是套路在手,操作不愁啊!
二、转换流
1、乱码产生的原因
我们首先来看两个概念,什么是编码,什么是解码?要区分这两个概念的话,也比较好理解:我们从码的角度出发来认识它们,码就是计算机能看懂的东西,也就是“二进制”,等同于字节,人类能看懂的语言是“字符或者字符串”。
如果是人类能看懂的变为计算机能看懂的就叫编码,也就是说“字符或者字符串”变为字节就是编码,反过来,如果是计算机能看懂的变为人类能看懂的就叫解码,也就是说字节变为“字符或者字符串”就是解码。
大家可以通过加密和解密来对比理解,人看不懂就是加密,人能看到就是解密。
乱码产生的原因有两个:
1、编码与解码的字符集不相同,导致乱码;
2、字节缺少或者长度丢失,导致乱码;
/**
* 乱码的原因
*/
@Test
public void test() throws UnsupportedEncodingException {
// 默认字符集“utf-8”
System.out.println("默认字符集:" + System.getProperty("file.encoding"));
String info = "北京欢迎您!"; // 解码
byte[] data = info.getBytes(); // 编码:char--->byte,字符或者字符串到字节
// 编码与解码字符集统一,都使用工作空间默认的字符集
System.out.println(new String(data)); // 解码:byte--->char,字节到字符或者字符串
// 不统一则出现乱码
System.out.println(new String(data, "GBK"));
// 编码与解码的字符集必须相同,否则乱码
byte[] data2 = "JPM,你好!".getBytes("GBK");// 编码
String info2 = new String(data2, "GBK");// 解码
System.out.println(info2);
// 乱码的原因之二,字节缺少,长度丢失
String str = "北京";
byte[] data3 = str.getBytes();
System.out.println(data3.length); // 6
System.out.println(new String(data3, 0, 5)); // 字节数不完整导致乱码
}
运行结果:
默认字符集:UTF-8
北京欢迎您!
鍖椾含娆㈣繋鎮紒
JPM,你好!
6
北�
2、认识转换流
在 Java IO 中除了字节流和字符流外,还有一组字节流转换位字符流的类,用于处理乱码问题。
字节输入流 InputStreamReader:作用是将输入的字节流变为字符流。
字节输出流 OutputStreamWriter:作用是将输出的字节流变为字符流。
转换流只能是把字节流转为字符流,从而完成它的使命,那是因为字符流不能设置字符集,只能是把字符流变为字节流才能进行字符集的设置,因为字节流才有设置字符集的方法。
// 输入流 InputStreamReader 解码
public InputStreamReader(InputStream in, String charsetName)
throws UnsupportedEncodingException
{
super(in);
if (charsetName == null)
throw new NullPointerException("charsetName");
sd = StreamDecoder.forInputStreamReader(in, this, charsetName);
}
//输出流 OutputStreamWriter 编码
public OutputStreamWriter(OutputStream out, String charsetName)
throws UnsupportedEncodingException
{
super(out);
if (charsetName == null)
throw new NullPointerException("charsetName");
se = StreamEncoder.forOutputStreamWriter(out, this, charsetName);
}
3、转换流的文件拷贝demo,仔细体会注释的文字
/**
* 转换流:字节转为字符
* 1、输入流 InputStreamReader 解码
* 2、输出流 OutputStreamWriter 编码
* 仔细体会注释的文字
*/
@Test
public void test2() throws IOException {
String srcPath = "D:/file/txt/output_char.txt";
String destPath = "D:/file/txt/output_char_convert.txt";
// FileReader(字符流)不能解码,FileInputStream(字节流)才能解码
// BufferedReader br = new BufferedReader(new FileReader(new File(srcPath)));
// 字符流FileReader要换成字节流FileInputStream,但是字节流与字符流不能直接操作,需要通过转换流InputStreamReader来实现
BufferedReader br = new BufferedReader(new InputStreamReader(new FileInputStream(new File(srcPath)), "UTF-8")); // 指定解码字符集
// FileWriter(字符流)不能编码,FileOutputStream(字节流)才能编码
// BufferedWriter writer = new BufferedWriter(new FileWriter(new File(destPath)));
// 字符流FileWriter要换成字节流FileOutputStream,但是字节流与字符流不能直接操作,需要通过转换流OutputStreamWriter来实现
BufferedWriter wr = new BufferedWriter(
new OutputStreamWriter(new FileOutputStream(new File(destPath)), "UTF-8"));// 指定编码字符集
// 读取并写出
String line = null;
while ((line = br.readLine()) != null) {
wr.write(line);
wr.newLine();
}
wr.flush();
wr.close();
br.close();
}
运行结果:
三、数据处理流
在 Java IO 中,提供了两个数据(基本数据类型+String)操作流 ,分别是数据输入流 DataInputStream 和数据输出流 DataOutputStream。
下面直接通过一个例子来演示数据处理流的用法:
@Test
public void test() throws IOException {
write("D:/file/txt/data.txt"); // 写到文件
read("D:/file/txt/data.txt"); // 从文件读取
}
/**
* 基本数据类型+String类型输出到文件
*/
public static void write(String destPath) throws IOException {
int intNum = 100;
long longNum = 999L;
float floatNum = 3.14f;
double doubleNum = 5.50;
String str = "基本数据类型+String类型输出到文件";
File dest = new File(destPath);
DataOutputStream dos = new DataOutputStream(new BufferedOutputStream(new FileOutputStream(dest)));
// 操作:注意写出的顺序,读取要和写出的顺序一致
dos.writeInt(intNum);
dos.writeLong(longNum);
dos.writeFloat(floatNum);
dos.writeDouble(doubleNum);
dos.writeUTF(str);
dos.flush();
dos.close();
}
/**
* 从文件里读取基本数据类型+String类型
*/
public static void read(String srcPath) throws IOException {
File src = new File(srcPath);
DataInputStream dis = new DataInputStream(new BufferedInputStream(new FileInputStream(src)));
int intNum = dis.readInt();
long longNum = dis.readLong();
float floatNum = dis.readFloat();
double doubleNum = dis.readDouble();
String str = dis.readUTF();
dis.close();
// 100---->999---->3.14---->5.5---->基本数据类型+String类型输出到文件
System.out.println(intNum + "---->" + longNum + "---->" + floatNum + "---->" + doubleNum + "---->" + str);
}
运行结果:
100---->999---->3.14---->5.5---->基本数据类型+String类型输出到文件
四、对象处理流
对象处理流的操作就是我们经常说的序列化与反序列化操作。序列化就是把一个对象变为二进制流的一种方法,通过对象序列化可以方便地实现对象的传输和存储,反过来,如果把一个对象读入到程序的过程及时反序列化。序列化操作需要使用输出流 ObjectOutputStream 进行输出,反序列化操作需要使用输出流 ObjectInputStream 进行读取对象数据。
如果一个类的对象想被序列化,这个类必须实现 Serializable 接口,同时要注意这个类对象的版本兼容问题,一般我们再要进行序列化的类中设置一个固定的 serialVersionUID 常量,这个值只要不修改,序列化和反序列化操作就不会发生版本兼容问题。
为了减少保存对象的使用空间,可以把一个类的某个属性设置为不被序列化,当实现 Serializable 接口实现序列化的时候,可以使用 transient 关键字进行声明。
下面直接通过一个例子来演示对象处理流的用法:
/**
* 对象的序列化以及反序列化操作
*/
@Test
public void test() throws FileNotFoundException, IOException, ClassNotFoundException {
String filePath = "D:/file/txt/object.txt";
serializa(filePath);
Object object = UnSerializa(filePath);
if (object instanceof User) {
object = (User) object;
}
// User [name=JPM, age=18, address=null],因为address属性被transient修饰,没有被序列化,所以为null
System.out.println(object.toString());
}
/**
* 对象序列化:对象变为二进制流的方法
*/
public static void serializa(String destPath) throws FileNotFoundException, IOException {
File dest = new File(destPath);
ObjectOutputStream oos = new ObjectOutputStream(new BufferedOutputStream(new FileOutputStream(dest)));
User user = new User("JPM", 18, "中国,北京!");
oos.writeObject(user);
oos.flush();
oos.close();
}
/**
* 对象反序列化:使用对象输入流读取对象数据
*/
public static Object UnSerializa(String srcPath) throws FileNotFoundException, IOException, ClassNotFoundException {
Object object = null;
File scr = new File(srcPath);
ObjectInputStream ois = new ObjectInputStream(new BufferedInputStream(new FileInputStream(scr)));
object = ois.readObject();
ois.close();
return object;
}
/**
* 序列化与反序列化的对象,必须实现Serializable接口
*/
public class User implements Serializable {
private static final long serialVersionUID = -6954786920974801199L;
private String name;
private int age;
// transient修饰的属性不会被序列化
private transient String address;
public User() {
super();
}
public User(String name, int age, String address) {
super();
this.name = name;
this.age = age;
this.address = address;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public String getAddress() {
return address;
}
public void setAddress(String address) {
this.address = address;
}
@Override
public String toString() {
return "User [name=" + name + ", age=" + age + ", address=" + address + "]";
}
}
运行结果:
User [name=JPM, age=18, address=null]
这是 Java IO 操作的第三篇文章,文章有点长,坚持看下来的小伙伴们也非常不容易,但是我想说,能坚持看完这三篇 IO 文章的同学,你一定掌握了 Java IO 处理的套路,面对未来开发中涉及到的 IO 操作,一定会更加从容自如,如果你能把所有的示例代码手动敲一遍,那你的感觉就会更加美好,不信你试试。