字符流、缓冲流与对象序列化:IO 工具如何组合
按字符处理文本,用缓冲与转换流组合 IO,并理解数据格式协议。
配套实验:编码与字节
继续上次小节 →Java 进阶线 · 线路目录
Java 进阶线
对象与类型设计3 站
文件、配置与并发4 站
网络、框架与复习3 站
本页导读
day10|java.io 包下的常用 API:IO 流(二)
day09 解决的是”字节怎么搬运”和”编码是什么”;本日解决的是”文本怎么按字符读写、性能怎么提上来、对象怎么落到文件里”。主线是一条条可以叠起来的管道:原始流负责真正碰磁盘(FileReader/FileWriter),包装流在它外面加缓冲(BufferedReader/BufferedWriter、字节缓冲流),转换流负责在字节与字符之间按指定编码搭桥(InputStreamReader/OutputStreamWriter),打印流、数据流、序列化流则负责把”数据以什么格式写出去”这件事确定下来。学完后应能回答:字符输出流为什么必须 flush()/close()、readLine() 和 newLine() 多了什么能力、四种复制方式的性能差在哪、乱码该用哪一层解决、transient 起什么作用、ObjectOutputStream 写出的文件为什么不能当普通文本改。
本日 PPT 目录(第 2 页,第 13/18/26/29/31/35/40/47/52/58 页重复出现作为进度锚点):IO 流-字符流 → IO 流-缓冲流(字节缓冲流、字符缓冲流、原始流与缓冲流性能分析)→ IO 流-转换流 → IO 流-打印流 → IO 流-数据流 → IO 流-序列化流 → 补充知识:IO 框架。
三点说明(供复习时核对):
- 本日共 62 页幻灯片。课件多处”常用方法”页在抽取文本里只剩
[表格]占位、没有表体,代码示例也以截图形式贴在页面上。本文按课件的题目页与配套示例工程(day10-IO(二)/代码/day10-io-code)还原这些内容,还原处标注”(表格按课件题目与配套示例整理)”。 - PPT 抽出的 28 张图片全部逐张查看过:2 张是几乎空白的损坏图(
day10-s04-04.png143 字节、day10-s15-15.png746 字节),其余 26 张是装饰文字图(如”原始流/低级流""8KB""不同编码读取出现乱码的问题”)、卡通人物与表情包,与知识点无关,已按规范舍弃;只有第 59 页的两张 jar/工程截图有信息量,已归档内嵌。这是本日配图偏少的原因。 - 代码按课件顺序重新拼接并规范缩进。课件里
new FileWriter(..., true)等写法保留了原样;标”(片段)“的表示 PPT 上本身就是不完整片段。
1. IO 流体系回顾与本日路线
1.1 字符流和字节流的分工
课件第 2 页给了最朴素的选择依据:
- 字符流:适合读写文本文件内容(读、写)。
- 字节流:适合复制文件等;不适合读写文本文件。
第 11 页的小结把这句话展开成两行:
- 字节流适合做一切文件数据的拷贝(音视频、文本);字节流不适合读取中文内容输出。
- 字符流适合做文本文件的操作(读、写)。
1.2 四层抽象与实现类
课件第 3、8、14、19、32、37、41、48、53 页反复画同一张体系图,每讲一个专题就往图里加一层。按课件原图整理如下:
| 层次 | 字节输入 | 字节输出 | 字符输入 | 字符输出 |
|---|---|---|---|---|
| 抽象类 | InputStream | OutputStream | Reader | Writer |
| 文件实现类 | FileInputStream | FileOutputStream | FileReader | FileWriter |
| 缓冲包装类 | BufferedInputStream | BufferedOutputStream | BufferedReader | BufferedWriter |
| 转换流 | — | — | InputStreamReader | OutputStreamWriter |
| 打印流 | — | PrintStream | — | PrintWriter |
| 数据流 | DataInputStream | DataOutputStream | — | — |
| 对象流 | ObjectInputStream | ObjectOutputStream | — | — |
课件把四层分别叫”抽象类""实现类""字节(字符)缓冲输入/输出流""字符输入/输出转换流”。课上强调的两个词:原始流/低级流(真正对接磁盘的 FileXxx)和包装流/处理流(对原始流进行包装,以提高原始流读写数据的性能)。同一支只有一种”真正碰磁盘”的实现,其余都是逐层套上去的。
(补充说明)课件图上写的
FileOutputSteam、OutputStreamWrite是笔误,正确拼写为FileOutputStream、OutputStreamWriter。
2. 文件字符输入流 FileReader
2.1 作用与构造
课件第 4 页的定义:以内存为基准,可以把文件中的数据以字符的形式读入到内存中去。 第 4 页配的”磁盘/网络 → 内存 → 程序”示意图说明数据流动方向,以内存为参照,进内存就是”输入”。
构造器(表格按课件题目与配套示例整理):
| 构造器 | 说明 |
|---|---|
FileReader(String fileName) | 按文件路径接通源文件 |
FileReader(File file) | 按 File 对象接通源文件 |
2.2 读字符的两个方法
课件第 6 页的提问是”每次读取一个字符,每次读取多个字符的方法是啥?它们各有什么特点?“对应两个 API:
| 方法 | 返回值 | 说明 |
|---|---|---|
public int read() | 读到的字符编号(如 'a' 得到 97);无数据可读返回 -1 | 每次读一个字符,必须用循环,性能较差 |
public int read(char[] buffer) | 本次实际读取的字符个数;结束返回 -1 | 每次读一批字符,注意用返回长度构造字符串 |
2.3 每次读一个字符
package com.itheima.d1_char_stream;
import java.io.FileReader;
import java.io.Reader;
public class FileReaderDemo1 {
public static void main(String[] args) throws Exception {
// 目标:文件字符输入流的使用:每次读取一个字符。
// 1、创建文件字符输入流管道与源文件接通
Reader fr = new FileReader("day10-io-code\\src\\dlei01.txt");
// 2、读取一个字符编号回来,没有字符可读返回 -1: public int read()
// int c1 = fr.read();
// System.out.println((char) c1);
// 3、使用循环解决
int c;
while ((c = fr.read()) != -1) {
char ch = (char) c;
System.out.print(ch);
}
fr.close();
// 拓展:是否解决了乱码问题:解决了!性能较差,依然是个垃圾代码!
}
}
课件在这里的结论是两句话:中文乱码问题解决了(前提是代码编码与文件编码一致,见第 7 章),但单字符循环性能依然很差。
2.4 每次读多个字符(截至此刻最好的文本读取方案)
package com.itheima.d1_char_stream;
import java.io.FileReader;
import java.io.Reader;
public class FileReaderDemo2 {
public static void main(String[] args) {
// 目标:文件字符输入流的使用:每次读取多个字符。
try (
// 1、创建文件字符输入流管道与源文件接通
Reader fr = new FileReader("day10-io-code\\src\\dlei01.txt");
) {
// 2、定义一个字符数组用于读取多个字符
char[] buffer = new char[1024];
int len; // 记住每次读取了多少个字符
while ((len = fr.read(buffer)) != -1) {
String rs = new String(buffer, 0, len);
System.out.print(rs);
}
// 拓展:可以避免乱码吗?可以。性能可以吗?可以。
// 这是截至此刻我们学过的读取文本内容最好的方案。
} catch (Exception e) {
e.printStackTrace();
}
}
}
注意两点:数组是 char[] 而不是 byte[];new String(buffer, 0, len) 必须带上 len,否则最后一轮会把上一轮的残留字符一起读出来。
(补充说明)
FileReader使用的是平台默认字符集,课件没有提到可以显式传Charset。跨环境读文件时,更稳妥的做法是把这一层换成new InputStreamReader(new FileInputStream(file), charset)(见第 8 章)。
3. 文件字符输出流 FileWriter
3.1 作用与构造
课件第 9 页的定义:以内存为基准,把内存中的数据以字符的形式写出到文件中去。
构造器(表格按课件题目与配套示例整理):
| 构造器 | 说明 |
|---|---|
FileWriter(String fileName) | 覆盖原文件内容 |
FileWriter(String fileName, boolean append) | append 为 true 时在原内容后面追加 |
FileWriter(File file) | 以 File 对象作为输出目标 |
第 12 页的提问正好对应这一节的四个重点:怎么创建管道和追加、写数据有哪些方法、怎么换行、写完必须注意什么。
3.2 写字符的五个重载
| 方法 | 说明 |
|---|---|
public void write(int c) | 写一个字符(可以传字符编号) |
public void write(String s) | 写一个字符串 |
public void write(String s, int pos, int len) | 写字符串的一部分 |
public void write(char[] chars) | 写一个字符数组 |
public void write(char[] chars, int pos, int len) | 写字符数组的一部分 |
3.3 换行、刷新与关闭
- 换行:课件用的写法是
fw.write("\r\n");字符缓冲输出流有newLine()(见第 5 章)。 - 必须刷新或关闭:课件第 11 页原话是”字符输出流写出数据后,必须刷新流,或者关闭流,写出去的数据才能生效”。字符输出流内部有缓冲区,
flush()把缓冲内容推给底层,close()关闭并包含刷新。 - 字节流/字符流的使用场景(第 11 页小结,表格按课件题目整理):
| 目标 | 推荐 | 说明 |
|---|---|---|
| 拷贝任意文件(音视频、文本) | 字节流 | 字节流适合做一切文件数据的拷贝 |
| 读取中文内容输出 | 字符流 | 字节流不适合读取中文内容输出 |
| 文本文件的读、写 | 字符流 | 按字符处理,天然处理中文 |
3.4 完整示例
package com.itheima.d1_char_stream;
import java.io.FileWriter;
public class FileWriteDemo3 {
public static void main(String[] args) {
try (
// 目标:掌握文件字符输出流的使用。
// 1、创建一个文件字符输出流管道与目标文件接通
// Writer fw = new FileWriter("day10-io-code/src/dlei02-out.txt"); // 覆盖
FileWriter fw = new FileWriter("day10-io-code/src/dlei02-out.txt", true); // 追加
) {
// 2、写字符数据出去。
// a.写一个字符出去 : public void write(int c)
fw.write(97);
fw.write('磊');
fw.write("\r\n"); // 换行
// b.写一个字符串出去:public void write(String s)
fw.write("abc我爱你中国666");
fw.write("\r\n"); // 换行
// c、写字符串的一部分出去 : public void write(String s, int pos, int len)
fw.write("abc我爱你中国666", 3, 3);
fw.write("\r\n"); // 换行
// d.写一个字符数组出去:public void write(char[] chars)
char[] chars = "abc我爱你中国666".toCharArray();
fw.write(chars);
fw.write("\r\n"); // 换行
// e、写一个字符数组的一部分出去:public void write(char[] chars, int pos, int len)
fw.write(chars, 6, 2);
fw.write("\r\n"); // 换行
// fw.flush(); // 刷新
// fw.close(); // 关闭包含刷新(这里交给 try-with-resources)
} catch (Exception e) {
e.printStackTrace();
}
}
}
fw.write(97) 写出的是字符 a,不是数字 97 的文本——这一点与后面的打印流正好相反。
4. 缓冲流:给原始流加一个 8KB 的池子
4.1 概念
课件第 13 页引出缓冲流时先给了一组名词:原始流/低级流 与 包装流/处理流——包装流的作用是”对原始流进行包装,以提高原始流读写数据的性能”。第 17 页的提问把要点说全了:
- 缓冲流有几种:字节缓冲输入流
BufferedInputStream、字节缓冲输出流BufferedOutputStream、字符缓冲输入流BufferedReader、字符缓冲输出流BufferedWriter。 - 为什么能提高性能:字节缓冲流自带 8KB 缓冲区;可以提高原始字节流、字符流读写数据的性能。
- 怎么用:把原始流作为构造参数传进去,功能上并无很大变化,性能提升了。
| 包装类 | 包装的流 | 自带缓冲 | 新增能力 |
|---|---|---|---|
BufferedInputStream | InputStream | 8KB | 缓冲读 |
BufferedOutputStream | OutputStream | 8KB | 缓冲写 |
BufferedReader | Reader | 8K(8192)字符 | readLine() 按行读取 |
BufferedWriter | Writer | 8K 字符 | newLine() 换行 |
4.2 构造器
public BufferedOutputStream(OutputStream os)
public BufferedInputStream(InputStream is)
4.3 原理:内存与磁盘之间多了一层缓冲池
课件第 15 页用”C 盘 / D 盘 / 内存 / 字节数组 / 1KB / 16KB”等标注画了字节缓冲流的作用示意:原始字节流每次只搬 1KB 字节数组,一次一次地访问磁盘;套上缓冲流后,缓冲池(8KB)先在内存里攒够一批再和磁盘交互,从而减少访问磁盘的次数。第 20 页给字符缓冲输入流画的是同一张图,只是单位换成”字符数组 1K / 16K 字符”。
4.4 字节缓冲流示例
package com.itheima.d2_buffer_stream;
import java.io.*;
public class BufferedInputStreamDemo1 {
public static void main(String[] args) {
// 目标:使用字节缓冲流提升原始字节流读写数据的性能的。
try (
// 1、创建字节输入流管道与源文件接通
InputStream is = new FileInputStream("E:\\resource\\meinv2.jpg");
// 使用高级的缓冲字节输入流包装低级的字节输入流
InputStream bis = new BufferedInputStream(is);
// 2、创建一个字节输出流管道与目标文件接通
OutputStream os = new FileOutputStream("E:\\resource\\meinv2-bak2.txt");
// 使用高级的缓存字节输出流包装低级的字节输出流
OutputStream bos = new BufferedOutputStream(os);
) {
// 3、准备一个字节数组
byte[] buffer = new byte[1024];
// 4、转移数据
int len;
while ((len = bis.read(buffer)) != -1) {
bos.write(buffer, 0, len);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
5. 字符缓冲流 BufferedReader / BufferedWriter
5.1 BufferedReader
课件第 20、21 页:自带 8K(8192)的字符缓冲池,可以提高字符输入流读取字符数据的性能;除此之外,字符缓冲输入流新增了”按照行读取字符”的功能。
| 构造器 / 方法 | 说明 |
|---|---|
public BufferedReader(Reader r) | 包装一个字符输入流 |
public String readLine() | 一次读取一行;读完了返回 null |
package com.itheima.d2_buffer_stream;
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.Reader;
public class BufferedReaderDemo2 {
public static void main(String[] args) {
// 目标:字符缓冲输入流的使用:提升原始字符输入流的读字符性能,多了按照行读取的能力。
try (
// 1、创建文件字符输入流管道与源文件接通
Reader fr = new FileReader("day10-io-code\\src\\dlei04.txt");
// 把低级的字符输入流包装成高级的缓存字符输入流。
BufferedReader br = new BufferedReader(fr);
) {
// 2、也可以按字符数组读取(这里只演示按行读取)
// char[] buffer = new char[1024];
// int len;
// while ((len = br.read(buffer)) != -1) {
// System.out.print(new String(buffer, 0, len));
// }
// 3、缓存字符输入流多了一个按照行读取内容的功能。(经典代码)
String line;
while ((line = br.readLine()) != null) {
System.out.println(line);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
readLine() 的判空条件必须是 != null(没有”行号”可读时返回 null),不是 != -1;返回的行内容不含换行符。
5.2 BufferedWriter
课件第 22、23 页:自带 8K 的字符缓冲池,可以提高字符输出流写字符数据的性能;新增了”换行”功能。
| 构造器 / 方法 | 说明 |
|---|---|
public BufferedWriter(Writer w) | 包装一个字符输出流 |
public void newLine() | 写一个平台相关的换行符 |
package com.itheima.d2_buffer_stream;
import java.io.BufferedWriter;
import java.io.FileWriter;
import java.io.Writer;
public class BufferedWriterDemo3 {
public static void main(String[] args) {
try (
// 目标:掌握缓冲字符输出流的使用。
// 1、创建一个文件字符输出流管道与目标文件接通
Writer fw = new FileWriter("day10-io-code/src/dlei05-out.txt"); // 覆盖
BufferedWriter bw = new BufferedWriter(fw);
) {
// 2、写字符数据出去。(write 的五个重载与 FileWriter 完全一致)
bw.write(97);
bw.write('磊');
bw.newLine(); // 换行
bw.write("abc我爱你中国666");
bw.newLine();
bw.write("abc我爱你中国666", 3, 3);
bw.newLine();
char[] chars = "abc我爱你中国666".toCharArray();
bw.write(chars);
bw.newLine();
bw.write(chars, 6, 2);
bw.newLine();
} catch (Exception e) {
e.printStackTrace();
}
}
}
第 24 页的自查结论可以背下来:字符缓冲流自带 8K 缓冲区,可以提高原始字符流读写数据的性能;BufferedReader 多了 readLine() 按行读取,BufferedWriter 多了 newLine() 换行。
6. 案例:《出师表》按编号恢复顺序
课件第 25 页的需求:把《出师表》的文章顺序进行恢复到一个新文件中。 课件给的分析步骤是六步:
- 定义一个缓冲(课件写作”缓存”)字符输入流管道与源文件接通。
- 定义一个
List集合存储读取的每行数据。 - 定义一个循环按照行读取数据,存入到
List集合中去。 - 对
List集合中的每行数据按照首字符编号升序排序。 - 定义一个缓冲字符输出管道与目标文件接通。
- 遍历
List集合中的每个元素,用缓冲输出管道写出并换行。
源文件 csb.txt 每行形如 3.侍中、侍郎郭攸之…,编号是乱的(3、8、4、2、1、9、6、7、5)。
package com.itheima.d2_buffer_stream;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.FileReader;
import java.io.FileWriter;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class Test4 {
public static void main(String[] args) {
// 目标:完成出师表案例。
try (
// 2、创建缓冲字符输入流与源文件接通
BufferedReader br = new BufferedReader(new FileReader("day10-io-code\\src\\csb.txt"));
// 6、创建缓冲字符输出流与目标文件接通
BufferedWriter bw = new BufferedWriter(new FileWriter("day10-io-code\\src\\csb-new.txt"));
) {
// 1、准备一个集合,存储原文每段落。
List<String> data = new ArrayList<>();
// 3、开始按照行读取,并存入到集合中去。
String line;
while ((line = br.readLine()) != null) {
data.add(line);
}
System.out.println(data);
// 4、对每段按照首字符编号进行排序
Collections.sort(data);
System.out.println(data);
// 5、把排序好的集合中的每段文章写出到新文件中去,每行都要换行。
for (String ln : data) {
bw.write(ln);
bw.newLine();
}
System.out.println("完成了!");
} catch (Exception e) {
e.printStackTrace();
}
}
}
课件说法:第 4 步是”按照首字符编号升序排序”,示例代码用的是 Collections.sort(data)。更准确的表述:这里实际是按整行字符串的自然顺序排序。本例编号只有 1~9 且每行第二位都是 .,所以字符串排序碰巧得到正确的文章顺序;一旦编号出现两位数,"10.…" 会被排到 "2.…" 前面,结果就错了。要真正按编号排序,应先把 . 前面的完整编号解析成整数再比较。
7. 原始流与缓冲流的性能分析(重点)
课件第 27 页的测试用例是”分别使用原始的字节流,以及字节缓冲流复制一个很大视频”,四种写法:
- 使用低级的字节流按照一个一个字节的形式复制文件。
- 使用低级的字节流按照字节数组的形式复制文件。
- 使用高级的缓冲字节流按照一个一个字节的形式复制文件。
- 使用高级的缓冲字节流按照字节数组的形式复制文件。
package com.itheima.d2_buffer_stream;
import java.io.*;
public class TimeTest5 {
public static final String SRC_VIDEO = "D:\\sziitheima1\\day02-面向对象进阶(二)\\视频\\11、接口的综合案例.avi";
public static final String DEST_PATH = "E:\\resource\\";
public static void main(String[] args) {
// 目标:原始流和缓冲流的性能分析。
// copy01(); // 使用低级的字节流按照一个一个字节的形式复制文件: 简直慢的让人无法忍受,直接淘汰,禁止使用!
copy02(); // 使用低级的字节流按照字节数组的形式复制文件:还可以,但是相对较慢!
// copy03(); // 使用高级的缓冲字节流按照一个一个字节的形式复制文件:还是特别慢,不推荐使用!
copy04(); // 使用高级的缓冲字节流按照字节数组的形式复制文件:极快!推荐使用!
}
// 4、使用高级的缓冲字节流按照字节数组的形式复制文件。
public static void copy04() {
long start = System.currentTimeMillis();
try (
InputStream is = new FileInputStream(SRC_VIDEO);
InputStream bis = new BufferedInputStream(is);
OutputStream os = new FileOutputStream(DEST_PATH + "04.avi");
OutputStream bos = new BufferedOutputStream(os);
) {
byte[] buffer = new byte[32 * 1024];
int len;
while ((len = bis.read(buffer)) != -1) {
bos.write(buffer, 0, len);
}
} catch (Exception e) {
e.printStackTrace();
}
long end = System.currentTimeMillis();
System.out.println("使用高级的缓冲字节流按照字节数组的形式复制文件:" + (end - start) / 1000.0 + "s");
}
// 另外三种写法与上面的区别只在"用哪个流"和"怎么读",核心循环如下(片段)。
// 1、使用低级的字节流按照一个一个字节的形式复制文件(copy01):
// int b;
// while ((b = is.read()) != -1) {
// os.write(b);
// }
// 2、使用低级的字节流按照字节数组的形式复制文件(copy02):
// byte[] buffer = new byte[32 * 1024];
// int len;
// while ((len = is.read(buffer)) != -1) {
// os.write(buffer, 0, len);
// }
// 3、使用高级的缓冲字节流按照一个一个字节的形式复制文件(copy03):
// int b;
// while ((b = bis.read()) != -1) {
// bos.write(b);
// }
}
四个方法都用 System.currentTimeMillis() 取开始/结束时间打印耗时,只有 copy04 是”缓冲流 + 字节数组”的组合。
课件第 28 页的结论:建议使用字节缓冲输入流、字节缓冲输出流,结合字节数组的方式,目前来看是性能最优的组合。
(补充说明)具体秒数取决于文件大小、磁盘类型和系统缓存。这个排序反映的是”单字节调用会产生海量系统调用”这一结构性问题,而缓冲流把每次调用变成数组搬运;换文件大小或换机器,绝对耗时都会变,但相对顺序基本一致。
8. 转换流:用指定编码搭桥
8.1 引出问题:不同编码读取时会乱码
课件第 30 页给出两条判断:
- 如果代码编码和被读取的文本文件编码一致,使用字符流读取文本文件时不会出现乱码。
- 如果不一致,使用字符流读取文本文件时就会出现乱码。
配套示例的注释写得更直接(代码编码 UTF-8):文件编码 UTF-8 时 a我m 正常;文件编码 GBK 时读出来就是乱码。原因和 day09 讲的一样:字节本身没有”编码”属性,字符流必须用一个字符集去解码。
8.2 字符输入转换流 InputStreamReader
课件第 33 页:解决不同编码时,字符流读取文本内容乱码的问题。 解决思路是”先获取文件的原始字节流,再将其按真实的字符集编码转成字符输入流,这样字符输入流中的字符就不乱码了”。
| 构造器 | 说明 |
|---|---|
public InputStreamReader(InputStream is) | 按平台默认字符集解码 |
public InputStreamReader(InputStream is, String charset) | 按指定字符集解码,字符流中的字符不乱码 |
package com.itheima.d3_transfer_stream;
import java.io.*;
public class InputStreamReaderDemo2 {
public static void main(String[] args) {
// 目标:字符输入转换流。
try (
// 1、得到GBK文件的原始字节输入流
InputStream is = new FileInputStream("E:\\resource\\abc.txt");
// 2、通过字符输入转换流把原始字节流按照指定编码转换成字符输入流
Reader isr = new InputStreamReader(is, "GBK");
// 3、把字符输入流包装成高级的缓冲字符输入流。
BufferedReader br = new BufferedReader(isr);
) {
// 4、按照行读取
String line;
while ((line = br.readLine()) != null) {
System.out.println(line);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
对照实验(d3_transfer_stream/Test1.java):把同一份文件交给 new FileReader(...) 直接读,文件是 GBK 时就会乱码——所以乱码不是”缓冲流没用好”,而是解码用的字符集和文件真实编码不一致。
// 目标:不同编码下,字符流读取文本内容的问题。(片段)
// 代码编码:UTF-8 文件编码:UTF-8 a我m ==> o [ooo] o 不乱码
// 代码编码:UTF-8 文件编码:GBK a我m ==> o [oo] o 乱码
Reader fr = new FileReader("E:\\resource\\abc.txt");
BufferedReader br = new BufferedReader(fr);
8.3 字符输出转换流 OutputStreamWriter
课件第 36 页先问”调用 String 提供的 getBytes 方法解决?“并给出一行代码:
String data = "我爱你中国abc";
byte[] bytes = data.getBytes("GBK");
这只是”把字符串变成指定编码的字节”这一半;要写出去还得再套一个字节输出流。课件随后给出更完整的方案——使用”字符输出转换流”实现。
课件第 38 页:OutputStreamWriter 的作用是可以控制写出去的字符使用什么字符集编码;解决思路是”获取字节输出流,再按照指定的字符集编码将其转换成字符输出流,以后写出去的字符就会用该字符集编码了”。
| 构造器 | 说明 |
|---|---|
public OutputStreamWriter(OutputStream os) | 按平台默认字符集编码写出 |
public OutputStreamWriter(OutputStream os, String charset) | 指定写出去的字符编码 |
package com.itheima.d3_transfer_stream;
import java.io.*;
public class OutputStreamWriterDemo3 {
public static void main(String[] args) {
try (
// 目标:掌握字符输出转换流:可以指定写出去的字符编码。
// 1、创建一个文件字节输出流通向目标文件。
OutputStream os = new FileOutputStream("day10-io-code/src/dlei06-out.txt");
// 指定写出去的编码是GBK.
Writer osw = new OutputStreamWriter(os, "GBK");
// 2、把字符输出流包装成缓冲输出流
BufferedWriter bw = new BufferedWriter(osw);
) {
bw.write("莫道桑榆晚");
bw.newLine();
bw.write("为霞尚满天");
bw.newLine();
bw.write("abc666");
bw.newLine();
} catch (Exception e) {
e.printStackTrace();
}
}
}
第 39 页的自查结论:public OutputStreamWriter(OutputStream os, String charset) 可以指定编码把字节输出流转换成字符输出流,从而指定写出去的字符编码。
(补充说明)课件用字符串写编码名(
"GBK")。现代写法更推荐传Charset对象或StandardCharsets.UTF_8,避免拼错的编码名只在运行时才报错。
9. 打印流 PrintStream / PrintWriter
9.1 作用
课件第 42 页:打印流可以实现更方便、更高效的打印数据出去,能实现打印啥出去就是啥出去。
第 41 页的体系图把 PrintStream、PrintWriter 补进 IO 体系:PrintStream 继承自字节输出流 OutputStream,PrintWriter 继承自字符输出流 Writer。
| 类型 | 父类 | 能写什么 | 典型方法 |
|---|---|---|---|
PrintStream | OutputStream | 支持写字节数据的方法 | println(...)、print(...) |
PrintWriter | Writer | 支持写字符数据出去 | println(...)、print(...) |
课件第 44、45 页把区别说得很清楚:打印数据的功能上一模一样(都是使用方便、性能高效,这是核心优势),区别只在继承关系——PrintStream 继承自字节输出流,因此支持写字节数据的方法;PrintWriter 继承自字符输出流,因此支持写字符数据出去。
9.2 打印各种类型的数据
package com.itheima.d4_printstream;
import java.io.PrintStream;
public class PrintStreamDemo1 {
public static void main(String[] args) {
try (
// 目标:打印流:方便,高效的写数据出去。
PrintStream ps = new PrintStream("day10-io-code/src/ps.txt");
// PrintWriter ps = new PrintWriter("day10-io-code/src/ps.txt");
// PrintWriter ps = new PrintWriter(new FileWriter("day10-io-code/src/ps.txt", true));
) {
ps.println(666);
ps.println(97);
ps.println(97.9);
ps.println('a');
ps.println(true);
ps.println("深圳黑马Java!");
} catch (Exception e) {
e.printStackTrace();
}
}
}
打印流可以直接以文件路径为构造参数,不需要自己先建字节输出流。写入文件里的是数据的文本形式(666、97.9、true),这一点与下一章的数据流完全不同。
9.3 输出语句的重定向
课件第 46 页给的场景是”打印流的一种应用:输出语句的重定向”:
PrintStream ps = new PrintStream("文件地址");
System.setOut(ps);
System.out.println("老骥伏枥");
System.out.println("志在千里");
System.out.println("烈士暮年");
System.out.println("壮心不已");
课件说明:可以把输出语句的打印位置改到某个文件中去。 完整示例(追加方式打开,避免每次运行都清空文件):
package com.itheima.d4_printstream;
import java.io.FileOutputStream;
import java.io.PrintStream;
public class PrintStreamDemo2 {
public static void main(String[] args) throws Exception {
// 目标:输出语句的重定向。
System.out.println("红豆生南国");
System.out.println("春来发几枝");
PrintStream ps = new PrintStream(new FileOutputStream("day10-io-code/src/ps2.txt", true));
System.setOut(ps); // 把系统类的打印流改成我的打印流!!
System.out.println("愿君多采撷");
System.out.println("此物最相思");
}
}
前两行仍然打印到控制台,setOut 之后的两行写进了文件——System.out 本身就是一个 PrintStream,只是它的目标被换掉了。
10. 数据流 DataOutputStream / DataInputStream
课件第 49 页:DataOutputStream(数据输出流)允许把数据和其类型一并写出去。第 50 页:DataInputStream(数据输入流)用于读取数据输出流写出去的数据。
| 类 | 方法(示例用到的) | 说明 |
|---|---|---|
DataOutputStream | writeByte、writeBoolean、writeInt、writeChar、writeUTF | 把值和类型一起写出去 |
DataInputStream | readByte、readBoolean、readInt、readChar、readUTF | 按对应方法读回,顺序必须与写出顺序一致 |
package com.itheima.d5_dataOutputStream;
import java.io.DataOutputStream;
import java.io.FileOutputStream;
public class DataOutputStreamDemo1 {
public static void main(String[] args) {
try (
// 目标:掌握数据输出流写数据出去的特点:可以写数据和其类型,而且后面可以读取出来。
DataOutputStream dos = new DataOutputStream(new FileOutputStream("day10-io-code/src/data.txt"));
) {
// 2、写数据和其类型出去
dos.writeByte(97);
dos.writeBoolean(true);
dos.writeInt(1000);
dos.writeChar('a');
dos.writeUTF("我在北京天安门666");
} catch (Exception e) {
e.printStackTrace();
}
}
}
package com.itheima.d5_dataOutputStream;
import java.io.DataInputStream;
import java.io.FileInputStream;
public class DataInputStreamDemo2 {
public static void main(String[] args) {
// 目标:掌握特殊数据输入流读取特殊数据输出流写出去的数据。
try (
// 1、创建高级的特殊数据输入流管道包装低级的字节输入流管道。
DataInputStream dis = new DataInputStream(new FileInputStream("day10-io-code/src/data.txt"));
) {
// 2、开始读取
byte b = dis.readByte();
System.out.println(b);
boolean b1 = dis.readBoolean();
System.out.println(b1);
int i = dis.readInt();
System.out.println(i);
char c = dis.readChar();
System.out.println(c);
String s = dis.readUTF();
System.out.println(s);
} catch (Exception e) {
e.printStackTrace();
}
}
}
数据流写出的文件是二进制协议,不能用记事本直接看;读取时类型和顺序必须和写出时完全一致,否则读到的是错位的字节。第 51 页的自查问题就一句:“数据输入流和数据输出流分别是谁,各自的作用是啥?”
11. 对象序列化流 ObjectOutputStream / ObjectInputStream
11.1 概念
课件第 52 页的两句定义:
- 对象序列化:把 Java 对象写入到文件中去。
- 对象反序列化:把文件里的 Java 对象读出来。
11.2 序列化
课件第 54 页:ObjectOutputStream(对象字节输出流)可以把 Java 对象进行序列化,把 Java 对象存入到文件中去;方法 public void writeObject(Object obj)。注意:对象如果要参与序列化,必须实现序列化接口(java.io.Serializable)。
package com.itheima.d6_object;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
@Data
@NoArgsConstructor
@AllArgsConstructor
// 注意:如果学生对象要参与序列化,必须实现序列化接口
public class Student implements Serializable {
private String name;
private int age;
// transient 修饰的成员变量将不参与序列化
private transient String passWord;
private double height;
}
package com.itheima.d6_object;
import java.io.FileOutputStream;
import java.io.ObjectOutputStream;
public class ObjectOutputStreamDemo1 {
public static void main(String[] args) throws Exception {
// 目标:完成对象的序列化:把Java对象存储到文件中去
// 1、准备一个对象。
Student s = new Student("风驴子", 27, "mazi666", 160);
// 2、创建对象字节输出流管道与目标文件接通
ObjectOutputStream oos =
new ObjectOutputStream(new FileOutputStream("day10-io-code/src/obj.txt"));
// 3、开始写对象出去
oos.writeObject(s);
// 4、关闭资源
oos.close();
}
}
Serializable 是一个标记接口,里面没有任何方法,作用只是告诉序列化机制”这个类的对象允许被写出”。否则运行时会抛 NotSerializableException。
11.3 transient
课件写在 Student 上的注释是:transient 修饰的成员变量将不参与序列化。示例里 passWord 被 transient 修饰,反序列化后读到的会是 null(字符串类型默认值)。它适合放密码、临时状态这类”不值得/不应该落到文件里”的字段。
(补充说明)课件只讲了
transient。更完整的边界是:static字段属于类而不属于对象实例,默认也不参与对象序列化;反序列化要求类在读写两端都存在且兼容。
11.4 反序列化
课件第 55 页:ObjectInputStream(对象字节输入流)可以把 Java 对象进行反序列化,把存储在文件中的 Java 对象读入到内存中来。
package com.itheima.d6_object;
import java.io.FileInputStream;
import java.io.ObjectInputStream;
public class ObjectInputStreamDemo2 {
public static void main(String[] args) throws Exception {
// 目标:对象反序列化 使用对象字节输入流管道把文件中的Java对象又读入到内存中来。
// 1、创建对象字节输入流管道
ObjectInputStream ois =
new ObjectInputStream(new FileInputStream("day10-io-code\\src\\obj.txt"));
// 2、读取对象进来(对象反序列化)
Student s = (Student) ois.readObject();
System.out.println(s);
ois.close();
}
}
readObject() 返回的是 Object,需要强转成具体类型。
11.5 一次序列化多个对象
课件第 56 页给的办法是:用一个 ArrayList 集合存储多个对象,然后直接对集合进行序列化即可;并特别注明”注意:ArrayList 集合已经实现了序列化接口”。因为集合本身可序列化,只要里面的元素也实现了 Serializable,整体就能一次写出、一次读回。
第 57 页的自查结论:序列化是把对象数据存入到文件中去,用 ObjectOutputStream 的 public void writeObject(Object obj),对象必须实现序列化接口;反序列化是把文件中的 Java 对象恢复到内存中的程序里来,用 ObjectInputStream 的 readObject()。
(补充说明)课件没有提到
serialVersionUID。类结构变化后(例如增删字段)反序列化可能因版本不匹配而失败,工程上通常会显式声明serialVersionUID;另外,对一个来源不可信的文件做原生反序列化等于执行对方的对象图,练习只应使用自己生成的文件。
12. 补充知识:IO 框架与 Commons-io
12.1 什么是框架
课件第 59 页给了框架的定义与好处:
- 框架:解决某类问题、编写的一套类、接口等,可以理解成一个半成品,大多框架都是第三方研发的。
- 好处:在框架的基础上开发,可以得到优秀的软件架构,并能提高开发效率。
- 形式:一般是把类、接口等编译成
class形式,再压缩成一个.jar结尾的文件发行出去。
本日讲的 IO 框架是 Commons-io(第 61 页):它是 apache 开源基金组织提供的一组有关 IO 操作的小框架,目的是提高 IO 流的开发效率;它”封装了 Java 提供的对文件、数据进行操作的代码,对外提供了更简单的方式来对文件进行操作,对数据进行读写等”。
12.2 引入步骤
课件第 60 页的四步:
- 在项目中创建一个文件夹:
lib。 - 将
commons-io-2.6.jar文件复制到lib文件夹。 - 在 jar 文件上点右键,选择
Add as Library-> 点击 OK。 - 在类中导包使用。
课件这一页的文字与配图不一致:步骤写的是 commons-io-2.6.jar,而页面上的文件名和配套工程 lib 目录里实际都是 commons-io-2.11.0.jar。以工程实际文件为准,本日使用的是 2.11.0。
这张截图可以看到框架的”半成品”长什么样:comparator、file、filefilter、function、input、monitor、output、serialization 等包,加上 Charsets、CopyUtils、DirectoryWalker、FileDeleteStrategy 等类——它其实就是把前面那些 InputStream/OutputStream 的组合封装成了更省事的工具方法。
12.3 FileUtils 常用操作
课件第 61 页的表格(按课件题目与配套示例整理):
| 方法 | 说明 |
|---|---|
FileUtils.copyFile(File src, File dest) | 复制文件 |
FileUtils.copyDirectory(File srcDir, File destDir) | 复制文件夹(含子目录) |
FileUtils.deleteDirectory(File dir) | 删除文件夹(含内容) |
package com.itheima.d7_commons_io;
/**
* 目标:使用CommonsIO框架进行IO相关的操作。
*/
public class CommonsIOTest1 {
public static void main(String[] args) throws Exception {
// FileUtils.copyFile(new File("day10-io-code/src/dlei04.txt"),
// new File("day10-io-code/src/dlei04-new.txt"));
// FileUtils.copyDirectory(new File("E:\\resource"), new File("E:\\resource-dlei-bak"));
// FileUtils.deleteDirectory(new File("E:\\resource-dlei-bak"));
// JDK 7开始也新增了单行复制相关的计数。
Files.copy(Path.of("day10-io-code/src/dlei04.txt"),
Path.of("day10-io-code/src/dlei04-dlei-666.txt"));
}
}
最后一行提示:JDK 7 起 java.nio.file.Files 也提供了单行复制,简单场景不必为了复制一个文件引入第三方 jar。
12.4 递归复制文件夹
课件这套框架最典型的用法是”复制文件夹”——文件夹里可能还有文件夹,所以要递归。示例中的关键判断(d8_demo/CopyDirectoryDemo1.java):
package com.itheima.d8_demo;
import org.apache.commons.io.FileUtils;
import java.io.File;
public class CopyDirectoryDemo1 {
public static void main(String[] args) {
// 目标:复制文件夹。
// 源文件夹:E:\resource
// 目标位置:D:\
try {
copyDirectory(new File("E:\\resource"), new File("D:\\"));
} catch (Exception e) {
e.printStackTrace();
}
}
public static void copyDirectory(File srcDir, File destDir) throws Exception {
// 1、极端判断:源和目标必须是已存在的目录
if (srcDir == null || destDir == null || !srcDir.exists() || !destDir.exists()
|| srcDir.isFile() || destDir.isFile()) {
return;
}
// 2、先在目标位置创建和源文件夹同名的目录
File destNewDir = new File(destDir, srcDir.getName());
destNewDir.mkdirs();
// 3、拿到一级文件对象
File[] files = srcDir.listFiles();
if (files == null || files.length == 0) return;
// 4、遍历一级文件对象:是文件就复制,是文件夹就递归
for (File file : files) {
if (file.isFile()) {
FileUtils.copyFile(file, new File(destNewDir, file.getName()));
} else {
copyDirectory(file, destNewDir);
}
}
}
}
配套工程里还有一个不依赖 Commons-io 的”删除文件夹”版本(d8_demo/DeleteDirDemo2.java),它把 day08 的递归套路又走了一遍:是文件直接删,是空文件夹直接删,listFiles() 返回 null(无权限)时不删,否则先递归删子项、最后删自己。这两个例子一起说明:框架省掉的是重复代码,不改变”目录遍历 + 递归”这套逻辑本身。
13. 作业与练习
本日无配套作业文档(day10-IO(二) 目录下只有 PPT/、代码/、类库/,没有作业目录)。
课堂练习以课件与配套工程中的示例为准,建议按顺序各写一遍:
FileReader每次读一个字符 / 每次读一个字符数组,比较乱码与性能(d1_char_stream)。FileWriter覆盖与追加两种模式,把五个write重载各用一次(FileWriteDemo3)。- 字节缓冲流 + 字节数组复制文件,再自己计时对比四种复制方式(
d2_buffer_stream/TimeTest5)。 - 用缓冲字符流完成《出师表》按编号恢复顺序(
d2_buffer_stream/Test4,源文件src/csb.txt)。 - 用
InputStreamReader读 GBK 文件解决乱码;用OutputStreamWriter写出 GBK 编码文件。 - 用
PrintStream把System.out重定向到文件。 - 用
DataOutputStream写出多种类型,再用DataInputStream按同序读回。 - 让
Student实现Serializable,观察transient字段反序列化后的值;再把多个对象放进ArrayList一次序列化。 - 用
FileUtils完成文件/文件夹的复制与删除(d7_commons_io、d8_demo)。
需要说明的是,配套工程
d8_demo下还有BeerDemo3.java(10 元买啤酒、2 个空瓶换 1 瓶、4 个盖子换 1 瓶的递归练习),课件正文没有对应的讲解页,只当作课堂补充练习看待。
14. 复习要点
- 选流:拷贝任意文件用字节流(
FileXxx+ 缓冲 + 字节数组);读写文本用字符流;不确定编码时用转换流指定字符集。 - FileReader:
read()返回字符编号、结束返回-1;read(char[])返回本次读取字符数,构造字符串必须带长度。 - FileWriter:写出后必须
flush()或close()才生效;构造器的第二个参数true表示追加;五个write重载要能对上号。 - 缓冲流:四种(字节/字符 × 输入/输出),自带 8KB 缓冲,构造时把原始流包进去;功能不变,性能提升。
- 字符缓冲流新增能力:
readLine()按行读到null结束(不含换行符);newLine()换行。 - 性能结论:单字节读写最慢(海量系统调用),“缓冲字节流 + 字节数组”是最优组合。
- 乱码定位:代码编码与文件编码不一致 → 用
InputStreamReader(is, charset)按真实编码解码;要控制写出编码用OutputStreamWriter(os, charset)。 - 打印流:
PrintStream/PrintWriter打印功能一致,前者继承OutputStream、后者继承Writer;System.setOut(ps)可以重定向输出。 - 数据流:
DataOutputStream写”值 + 类型”,DataInputStream必须按相同类型、相同顺序读回。 - 序列化:对象要实现
Serializable,用writeObject/readObject;transient字段不参与序列化;多个对象可以装进ArrayList一起序列化。 - Commons-io:jar 放进
lib→Add as Library→ 导包使用;FileUtils.copyFile/copyDirectory/deleteDirectory省掉重复代码,逻辑仍是递归遍历目录。