我们在掌握了 File 类、字节流、字符流,学会了 IO 操作的套路之后,IO 操作基本上就能处理日常工作中80%的常用问题了。
今天再给大家介绍一下处理流,学会处理流之后,日常工作中的文件操作就都可以应对了,掌握了下面的处理流,你将如虎添翼。
我们知道从 IO 流的功能来划分,IO 流分为:节点流和处理流。其中,节点流是用来包装数据源(File)的,它直接和数据源连接,表示从一个节点读取数据或者把数据写入到一个节点;处理流是用来包装节点流的,它是对一个已经存在的节点流进行连接,处理流通过增加缓存的方式来提高输入输出操作的性能。
处理流按照功能划分,可以分为:缓冲流、转换流、数据处理流、对象处理流。缓冲流是为了提高处理性能的,转换流是字节流转换为字符流用于处理乱码的(解码与编码的字符集问题),数据处理流就是对 8个基本类型和字符串类型数据的直接处理,对象数据处理流就是经常说的序列化与反序列化操作。
1
缓冲流

1、认识字节缓冲流
字节缓冲流就是用缓冲流包裹字节流,也就是说在创建缓冲流对象的时候,需要传入一个字节流的对象,同时会创建一个默认 8KB 的字节数组的缓冲区,通过这个缓冲区进行读写操作,以减少 IO 的次数,从而提高字节流的处理性能。
字节缓冲流分为:字节输入缓冲流 BufferedInputStream 和字节输出缓冲流 BufferedOutputStream,以下代码是字符缓冲流的源码:
// 字节输入缓冲流的构造方法private static int DEFAULT_BUFFER_SIZE = 8192; // 默认8KBpublic 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、用字节缓冲流实现文件拷贝功能
@Testpublic 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; // 默认8KBpublic 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; // 默认8KBpublic 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*/@Testpublic 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 流操作的套路,是不是套路在手,操作不愁啊!
2
转换流

1、乱码产生的原因
我们首先来看两个概念,什么是编码,什么是解码?要区分这两个概念的话,也比较好理解:我们从码的角度出发来认识它们,码就是计算机能看懂的东西,也就是“二进制”,等同于字节,人类能看懂的语言是“字符或者字符串”。
如果是人类能看懂的变为计算机能看懂的就叫编码,也就是说“字符或者字符串”变为字节就是编码,反过来,如果是计算机能看懂的变为人类能看懂的就叫解码,也就是说字节变为“字符或者字符串”就是解码。
大家可以通过加密和解密来对比理解,人看不懂就是加密,人能看到就是解密。
乱码产生的原因有两个:
1、编码与解码的字符集不相同,导致乱码;
2、字节缺少或者长度丢失,导致乱码;
/*** 乱码的原因*/@Testpublic 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); // 6System.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,仔细体会注释的文字
/*** 转换流:字节转为字符<br>* 1、输入流 InputStreamReader 解码<br>* 2、输出流 OutputStreamWriter 编码<br>* 仔细体会注释的文字*/@Testpublic 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();}
运行结果:

3
数据处理流

在 Java IO 中,提供了两个数据(基本数据类型+String)操作流 ,分别是数据输入流 DataInputStream 和数据输出流 DataOutputStream。
下面直接通过一个例子来演示数据处理流的用法:
@Testpublic 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类型输出到文件
4
对象处理流

对象处理流的操作就是我们经常说的序列化与反序列化操作。序列化就是把一个对象变为二进制流的一种方法,通过对象序列化可以方便地实现对象的传输和存储,反过来,如果把一个对象读入到程序的过程及时反序列化。序列化操作需要使用输出流 ObjectOutputStream 进行输出,反序列化操作需要使用输出流 ObjectInputStream 进行读取对象数据。
如果一个类的对象想被序列化,这个类必须实现 Serializable 接口,同时要注意这个类对象的版本兼容问题,一般我们再要进行序列化的类中设置一个固定的 serialVersionUID 常量,这个值只要不修改,序列化和反序列化操作就不会发生版本兼容问题。
为了减少保存对象的使用空间,可以把一个类的某个属性设置为不被序列化,当实现 Serializable 接口实现序列化的时候,可以使用 transient 关键字进行声明。
下面直接通过一个例子来演示对象处理流的用法:
/*** 对象的序列化以及反序列化操作*/@Testpublic 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修饰,没有被序列化,所以为nullSystem.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;}@Overridepublic String toString() {return "User [name=" + name + ", age=" + age + ", address=" + address + "]";}}
运行结果:
User [name=JPM, age=18, address=null]

这是 Java IO 操作的第三篇文章,文章有点长,坚持看下来的小伙伴们也非常不容易,但是我想说,能坚持看完这三篇 IO 文章的同学,你一定掌握了 Java IO 处理的套路,面对未来开发中涉及到的 IO 操作,一定会更加从容自如,如果你能把所有的示例代码手动敲一遍,那你的感觉就会更加美好,不信你试试。
回顾一下:
IO 第一篇:带你认识 File 类 我们对 File 的使用做了详细介绍;
IO 第二篇:IO 流,掌控一切 我们对 IO 流的操作套路有了深刻认识;
IO 第三篇:就是本文,我们对不能不懂的 IO 处理流进行了总结;
Java IO 系列第四篇:IO 操作大结局,敬请期待。




