主要内容
- 异常处理
- 缓冲流
- 转换流
- 序列化流
- 打印流
- 属性集
第一章 缓冲流
上述学习了基本的一些流,作为IO流的入门,今天我们要见识一些更强大的流。比如能够高效读写的缓冲流,能够转换编码的转换流,能够持久化存储对象的序列化流等等。这些功能更为强大的流,都是在基本的流对象基础之上创建而来的,就像穿上铠甲的武士一样,相当于是对基本流对象的一种增强。
1.1 概述
缓冲流是对4个基本的FileXxx
流的增强,所以也是4个流,按照流操作数据的数据类型分类:
- 字节缓冲流:
BufferedInputStream
,BufferedOutputStream
- 字符缓冲流:
BufferedReader
,BufferedWriter
缓冲流的基本原理,是在创建流对象时,会创建一个内置的默认大小的缓冲区数组,通过缓冲区读写,减少系统IO次数,从而提高读写的效率。
1.2 字节缓冲流
构造方法
-
public BufferedInputStream(InputStream in)
:创建一个 新的字节缓冲输入流。 -
public BufferedOutputStream(OutputStream out)
: 创建一个新的字节缓冲输出流。说明:我们发现上述缓冲流的参数依然需要对应的字节流对象。
这是因为缓冲区流是为了 高效而设计的,缓冲区流本身仅仅是维护了一个数组。不具备读和写的功能。真正的读写还是要依赖普通的字节流。
构造举例,代码如下:
// 创建字节缓冲输入流
BufferedInputStream bis = new BufferedInputStream(new FileInputStream("day09\\bis.txt"));
// 创建字节缓冲输出流
BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("day09\\bos.txt"));
效率测试
查询API,缓冲流读写方法与基本的流是一致的,我们通过复制大文件,测试它的效率。
- 基本流,代码如下:
public class BufferedDemo {
public static void main(String[] args) {
// 记录开始时间
long start = System.currentTimeMillis();
// 创建流对象
try (
FileInputStream fis = new FileInputStream("D:\\test\\制服诱惑2.mp4");
FileOutputStream fos = new FileOutputStream("F:\\制服诱惑2.mp4")
){
// 读写数据
int b;
while ((b = fis.read()) != -1) {
fos.write(b);
}
} catch (Exception e) {
e.printStackTrace();
}
// 记录结束时间
long end = System.currentTimeMillis();
System.out.println("普通流复制时间:"+(end - start)+" 毫秒");
}
}
普通流复制时间:288242 毫秒
2.缓冲流,代码如下:
public class BufferedDemo {
public static void main(String[] args){
// 记录开始时间
long start = System.currentTimeMillis();
// 创建流对象
try (
BufferedInputStream bis = new BufferedInputStream(new FileInputStream("D:\\test\\制服诱惑2.mp4"));
BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("F:\\制服诱惑2.mp4"));
){
// 读写数据
int b;
while ((b = bis.read()) != -1) {
bos.write(b);
}
} catch (Exception e) {
e.printStackTrace();
}
// 记录结束时间
long end = System.currentTimeMillis();
System.out.println("缓冲流复制时间:"+(end - start)+" 毫秒");
}
}
缓冲流复制时间:2108 毫秒
如何更快呢?
使用数组的方式,代码如下:
public class BufferedDemo {
public static void main(String[] args) throws FileNotFoundException {
// 记录开始时间
long start = System.currentTimeMillis();
// 创建流对象
try (
BufferedInputStream bis = new BufferedInputStream(new FileInputStream("D:\\test\\制服诱惑2.mp4"));
BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("F:\\制服诱惑2.mp4"));
){
// 读写数据
int len;
byte[] bytes = new byte[8*1024];
while ((len = bis.read(bytes)) != -1) {
bos.write(bytes, 0 , len);
}
} catch (IOException e) {
e.printStackTrace();
}
// 记录结束时间
long end = System.currentTimeMillis();
System.out.println("缓冲流使用数组复制时间:"+(end - start)+" 毫秒");
}
}
缓冲流使用数组复制时间:115 毫秒
1.3 字符缓冲流
构造方法
public BufferedReader(Reader in)
:创建一个 新的字符缓冲输入流。public BufferedWriter(Writer out)
: 创建一个新的字符缓冲输出流。
构造举例,代码如下:
// 创建字符缓冲输入流
BufferedReader br = new BufferedReader(new FileReader("E:\\br.txt"));
// 创建字符缓冲输出流
BufferedWriter bw = new BufferedWriter(new FileWriter("E:\\bw.txt"));
特有方法
字符缓冲流的基本方法与普通字符流调用方式一致,不再阐述,我们来看它们具备的特有方法。
-
BufferedReader:
public String readLine()
: 经常使用它读取一行数据。readLine方法可以按照行读取,读取的结束标记’\r’’\n’,返回的结果是读到这一行的所有文字。如果读取到文件的末尾返回 null。 -
BufferedWriter:
public void newLine()
: 写一行行分隔符。就是换行。
readLine
方法演示,代码如下:
public class BufferedReaderDemo {
public static void main(String[] args) throws IOException {
// 创建流对象
BufferedReader br = new BufferedReader(new FileReader("E:\\in.txt"));
// 定义字符串,保存读取的一行文字
String line = null;
// 循环读取,读取到最后返回null
while ((line = br.readLine())!=null) {
System.out.print(line);
System.out.println("------");
}
// 释放资源
br.close();
}
}
newLine
方法演示,代码如下:
public class BufferedWriterDemo throws IOException {
public static void main(String[] args) throws IOException {
// 创建流对象
BufferedWriter bw = new BufferedWriter(new FileWriter("E:\\out.txt"));
// 写出数据
bw.write("黑马");
// 写出换行
bw.newLine();
bw.write("程序");
bw.newLine();
bw.write("员");
bw.newLine();
// 释放资源
bw.close();
}
}
输出效果:
黑马
程序
员
1.4 练习:文本排序
请将文本信息恢复顺序。
3.侍中、侍郎郭攸之、费祎、董允等,此皆良实,志虑忠纯,是以先帝简拔以遗陛下。愚以为宫中之事,事无大小,悉以咨之,然后施行,必得裨补阙漏,有所广益。
8.愿陛下托臣以讨贼兴复之效,不效,则治臣之罪,以告先帝之灵。若无兴德之言,则责攸之、祎、允等之慢,以彰其咎;陛下亦宜自谋,以咨诹善道,察纳雅言,深追先帝遗诏,臣不胜受恩感激。
4.将军向宠,性行淑均,晓畅军事,试用之于昔日,先帝称之曰能,是以众议举宠为督。愚以为营中之事,悉以咨之,必能使行阵和睦,优劣得所。
2.宫中府中,俱为一体,陟罚臧否,不宜异同。若有作奸犯科及为忠善者,宜付有司论其刑赏,以昭陛下平明之理,不宜偏私,使内外异法也。
1.先帝创业未半而中道崩殂,今天下三分,益州疲弊,此诚危急存亡之秋也。然侍卫之臣不懈于内,忠志之士忘身于外者,盖追先帝之殊遇,欲报之于陛下也。诚宜开张圣听,以光先帝遗德,恢弘志士之气,不宜妄自菲薄,引喻失义,以塞忠谏之路也。
9.今当远离,临表涕零,不知所言。
6.臣本布衣,躬耕于南阳,苟全性命于乱世,不求闻达于诸侯。先帝不以臣卑鄙,猥自枉屈,三顾臣于草庐之中,咨臣以当世之事,由是感激,遂许先帝以驱驰。后值倾覆,受任于败军之际,奉命于危难之间,尔来二十有一年矣。
7.先帝知臣谨慎,故临崩寄臣以大事也。受命以来,夙夜忧叹,恐付托不效,以伤先帝之明,故五月渡泸,深入不毛。今南方已定,兵甲已足,当奖率三军,北定中原,庶竭驽钝,攘除奸凶,兴复汉室,还于旧都。此臣所以报先帝而忠陛下之职分也。至于斟酌损益,进尽忠言,则攸之、祎、允之任也。
5.亲贤臣,远小人,此先汉所以兴隆也;亲小人,远贤臣,此后汉所以倾颓也。先帝在时,每与臣论此事,未尝不叹息痛恨于桓、灵也。侍中、尚书、长史、参军,此悉贞良死节之臣,愿陛下亲之信之,则汉室之隆,可计日而待也。
准备工作:
在当前项目下面新建一个文件,叫做in.txt。然后将上述内容复制到in.txt文件中。注意每个序号后面要是一行数据。每个序号之间的数据要换行。效果如下图所示。
案例分析
- 逐行读取文本信息。
- 解析文本信息到集合中。
- 遍历集合,按顺序,写出文本信息。
案例实现
/*
分析:原来文本文件中的数据是乱序的,我们现在要求将文本文件in.txt中的无序的数据按照顺序写到一个文件out.txt中。
1. 逐行读取文本信息。
数据源文件in.txt属于文本文件,使用字符输入流---一行一行---高效的字符输入流BufferedReader 方法 readLine()
2. 解析文本信息到集合中。
每一行数据有个特点: 3.侍中、侍郎郭攸之、费祎
数字+"."+文本数据
读取到这一行数据,将其进行切割 使用String类中 split("\\.")
注意:这里要书写 \\. 不能直接书写 . 因为在这里.具备特殊含义 表示任意的字符
而我们这里希望是真正的 点,所以需要使用\将点进行转义,但是在java中\\表示一个真正的\
总结:\\.表示我们生活中的一个 .
3.侍中、侍郎郭攸之、费祎、董允等,....
8.愿陛下托臣以讨贼兴复之效。。。
按照上述做法切割完成之后,数组长度是2
索引 数据
0 3
1 侍中、侍郎郭攸之、费祎、董允等,....
将截取的数据存放到Map集合中
序号作为key 文本数据作为value
HashMap<Integer,String> hm = new HashMap<Integer,String>();
hm.put(3,"侍中、侍郎郭攸之、费祎、董允等,....");----->hm.put(Integer.parseInt(arr[0]),arr[1]);
3. 遍历集合,按顺序,写出文本信息。
//遍历
for(int i=1;i<=hm.size();i++)
{
//i赋值给key
Integer key=i;
//根据key获取值
String value = hm.get(key);
//写文本信息 一行一行 BufferedWriter 换行 newLine()
每一行信息包括: bw.write(key+"."+value);
}
*/
public class BufferedTest {
public static void main(String[] args) throws Exception {
//1. 逐行读取文本信息。
//1.1 创建高效字符输入流对象关联数据源文件
BufferedReader br = new BufferedReader(new FileReader("in.txt"));
//1.2 调用方法读取数据
//创建集合对象保存切割后的序号和文本数据 key : 存储序号 3 value : 存储文本数据
HashMap<Integer, String> hm = new HashMap<>();
String line = null;
while ((line = br.readLine()) != null) {
//1.3 截取读取的数据
String[] arr = line.split("\\.");
//2. 解析文本信息到集合中。 3.侍中、侍郎郭攸之、费祎、董允等,....
//由于key位置是Integer类型,而 数组中第一个数据是字符串,所以需要转换
int key = Integer.parseInt(arr[0]);
//添加到集合中
hm.put(key, arr[1]);//key:3 value:侍中、侍郎郭攸之、费祎、董允等,....
}
//关闭资源
br.close();
//创建字符高效输出流对象
BufferedWriter bw = new BufferedWriter(new FileWriter("out.txt"));
//3. 遍历集合,按顺序,写出文本信息。
for (int i = 1; i <= hm.size(); i++) {
//获取i就是key,i赋值给key
Integer key = i;
//根据key获取value
String value = hm.get(key);
//将遍历出来的key和value写到out.txt中
bw.write(key+"."+value);
//换行
bw.newLine();
}
//释放资源
bw.close();
}
}
第二章 转换流
2.1 字符编码和字符集
字符编码
计算机中储存的信息都是用二进制数表示的,而我们在屏幕上看到的数字、英文、标点符号、汉字等字符是二进制数转换之后的结果。按照某种规则,将字符存储到计算机中,称为编码 。反之,将存储在计算机中的二进制数按照某种规则解析显示出来,称为解码 。比如说,按照A规则存储,同样按照A规则解析,那么就能显示正确的文本f符号。反之,按照A规则存储,再按照B规则解析,就会导致乱码现象。
- 字符编码
Character Encoding
: 就是一套我们人类语言的字符与二进制数之间的对应规则。
字符集
- 字符集
Charset
:是一个系统支持的所有字符的集合,包括各国家文字、标点符号、数字等。
计算机要准确的存储和识别各种字符集符号,需要进行字符编码,一套字符集必然至少有一套字符编码。常见字符集有ASCII字符集、GBK字符集、Unicode字符集等。
可见,当指定了编码,它所对应的字符集自然就指定了,所以编码才是我们最终要关心的。
-
ASCII字符集 :
- ASCII(American Standard Code for Information Interchange,美国信息交换标准代码)是基于拉丁字母的一套电脑编码系统,用于显示现代英语,主要包括控制字符(回车键、退格、换行键等)和可显示字符(英文大小写字符、阿拉伯数字)。
- 基本的ASCII字符集,使用7位(bits)表示一个字符,共128字符。ASCII的扩展字符集使用8位(bits)表示一个字符,共256字符,方便支持欧洲常用字符。
ASCII码表1个字节表示一个字符。
生活中的字符 | 十进制 | 二进制 |
---|---|---|
A | 65 | 01000001 |
B | 66 | 01000010 |
………………
补充:(了解)
在美式英语中使用的特殊控制字符。其中:
0~31及127(共33个)是控制字符或通信专用字符(其余为可显示字符),如控制符:LF(换行)、CR(回车)、FF(换页)、DEL(删除)、BS(退格)、BEL(响铃)等;通信专用字符:SOH(文头)、EOT(文尾)、ACK(确认)等;ASCII值为8、9、10 和13 分别转换为退格、制表、换行和回车字符。它们并没有特定的图形显示,但会依不同的应用程序,而对文本显示有不同的影响。
32表示空格。
-
ISO-8859-1字符集:
-
拉丁码表,别名Latin-1,用于显示欧洲使用的语言,包括荷兰、丹麦、德语、意大利语、西班牙语等。
-
ISO-5559-1使用单字节编码,兼容ASCII编码。
-
ISO-8859-1编码表1个字节表示一个字符
-
ISO:International Standards Organization,国际标准化组织。
-
-
GBxxx字符集:
- GB就是国标的意思,是为了显示中文而设计的一套字符集。
- GB2312:简体中文码表。叫做信息交换用汉字编码字符集。它识别六七千的文字,兼容ASCII编码表。2个字节表示一个汉字字符。
- GBK:叫做汉字编码字符集,也可以叫做国标。是GB2312的升级版,识别2万多个中文字符。2个字节表示一个汉字字符。目前主流的编码表。
- GB18030:是GBK的升级版,包含了大部分的中文(简体、繁体、甲骨文、象形文等等),还有少数民族的文字,识别更多。每个字可以由1个、2个 或4个字节组成。
-
Unicode字符集 :
因为世界上各个国家都开发了属于自己的编码表,造成编码表不统一,世界计算机协会就为编码统一,制定了一张国际通用的编码表:unicode码表
-
Unicode编码系统为表达任意语言的任意字符而设计,是业界的一种标准,也称为统一码。标准万国码。
-
有三种编码方案,UTF-8、UTF-16和UTF-32。最为常用的UTF-8编码。
-
UTF-8编码,可以用来表示Unicode标准中任何字符,它是电子邮件、网页及其他存储或传送文字的应用中,优先采用的编码。互联网工程工作小组(IETF)要求所有互联网协议都必须支持UTF-8编码。所以,我们开发Web应用,也要使用UTF-8编码。它使用一至四个字节为每个字符编码,编码规则:
-
-
128个US-ASCII字符,只需一个字节编码。
-
拉丁文等字符,需要二个字节编码。
-
大部分常用字(含中文),使用三个字节编码。
-
其他极少使用的Unicode辅助字符,使用四字节编码。
-
-
可以识别汉字的编码表有哪些:
GB2312、GBK、GB18030、unicode、UTF-8
在开发中常用的码表:ISO-8859-1、GBK、UTF-8、ASCII。
文字--->(数字) :编码: 就是把能看懂内容,转换成看不懂的内容。
A 65 01000001
(数字)--->文字 : 解码: 就是把看不懂的内容,转换成看懂的内容。
01000001 65 A
乱码:编码和解码使用的编码表不一致导致的。
2.2 编码引出的问题
在IDEA中,使用FileReader
读取项目中的文本文件。由于IDEA的设置,都是默认的UTF-8
编码,所以没有任何问题。但是,当读取Windows系统中创建的文本文件时,由于Windows系统的默认是GBK编码,就会出现乱码。
需求:使用字符输入流读取E:\1.txt文件中的数据,要求:1.txt使用GBK编码方式保存。
public class ReaderDemo {
public static void main(String[] args) throws IOException {
FileReader fileReader = new FileReader("E:\\1.txt");
int read;
while ((read = fileReader.read()) != -1) {
System.out.print((char)read);
}
fileReader.close();
}
}
输出结果:
���
那么如何读取GBK编码的文件呢?
2.3 InputStreamReader类
转换流java.io.InputStreamReader
,是Reader的子类,是从字节流到字符流的桥梁。它读取字节,并使用指定的字符集将其解码为字符。它的字符集可以由名称指定,也可以接受平台的默认字符集。
构造方法
InputStreamReader(InputStream in)
: 创建一个使用默认字符集的字符流。InputStreamReader(InputStream in, String charsetName)
: 创建一个指定字符集的字符流。
构造举例,代码如下:
InputStreamReader isr = new InputStreamReader(new FileInputStream("in.txt"));
InputStreamReader isr2 = new InputStreamReader(new FileInputStream("in.txt") , "GBK");
指定编码读取
public class ReaderDemo2 {
public static void main(String[] args) throws IOException {
// 定义文件路径,文件为gbk编码
String FileName = "E:\\1.txt";
// 创建流对象,默认UTF8编码
InputStreamReader isr = new InputStreamReader(new FileInputStream(FileName));
// 创建流对象,指定GBK编码
InputStreamReader isr2 = new InputStreamReader(new FileInputStream(FileName) , "GBK");
// 定义变量,保存字符
int read=0;
// 使用默认编码字符流读取,乱码
while ((read = isr.read()) != -1) {
System.out.print((char)read); // ��
}
isr.close();
// 使用指定编码字符流读取,正常解析
while ((read = isr2.read()) != -1) {
System.out.print((char)read);// 你好
}
isr2.close();
}
}
2.4 OutputStreamWriter类
转换流java.io.OutputStreamWriter
,是Writer的子类,是从字符流到字节流的桥梁。使用指定的字符集将字符编码转换为字节。它的字符集可以由名称指定,也可以接受平台的默认字符集。
构造方法
OutputStreamWriter(OutputStream in)
: 创建一个使用默认字符集的字符流。OutputStreamWriter(OutputStream in, String charsetName)
: 创建一个指定字符集的字符流。
构造举例,代码如下:
OutputStreamWriter isr = new OutputStreamWriter(new FileOutputStream("day10\\out.txt"));
OutputStreamWriter isr2 = new OutputStreamWriter(new FileOutputStream("day10\\out.txt") , "GBK");
指定编码写出
public class OutputDemo {
public static void main(String[] args) throws IOException {
// 定义文件路径
String FileName = "E:\\out.txt";
// 创建流对象,默认UTF8编码
OutputStreamWriter osw = new OutputStreamWriter(new FileOutputStream(FileName));
// 写出数据
osw.write("你好");
osw.close();
// 定义文件路径
String FileName2 = "E:\\out2.txt";
// 创建流对象,指定GBK编码
OutputStreamWriter osw2 = new OutputStreamWriter(new FileOutputStream(FileName2),"GBK");
// 写出数据
osw2.write("你好");
osw2.close();
}
}
转换流理解图解
转换流是字节与字符间的桥梁!
字节存在硬盘上。字符存在内存中。
总结
转换流有两个作用:
1. 指定读写的码表
2.可以把字节流对象变成字符流对象
2.5.练习:把UTF-8文件转成GBK
需求:把D:\out.txt以UTF-8编码的文件转换为 以GBK编码的文件
/*
需求:把D:\\out.txt以UTF-8编码的文件转换为 以GBK编码的文件
*/
public class Test01 {
public static void main(String[] args) throws Exception {
//把这个文件换成GBK编码 内容不变
//创建输入流
//默认按照UTF-8方式读取
InputStreamReader isr = new InputStreamReader(new FileInputStream("D:\\out.txt"));
//创建输出流
OutputStreamWriter osw = new OutputStreamWriter(new FileOutputStream("D:\\123.txt"),"GBK");
//循环读写
int a;
//每次读取一个字符
while((a=isr.read()) != -1){
//写出一个字符
osw.write(a);
}
//关流
osw.close();
isr.close();
}
}
第三章 序列化
3.1 概述
Java 提供了一种对象序列化的机制。用一个字节序列可以表示一个对象,该字节序列包含该对象的数据
、对象的类型
和对象中存储的数据
等信息。字节序列写出到文件之后,相当于文件中持久保存了一个对象的信息。
反之,该字节序列还可以从文件中读取回来,重构对象,对它进行反序列化。对象的数据
、对象的类型
和对象中存储的数据
信息,都可以用来在内存中创建对象。看图理解序列化:
3.2 ObjectOutputStream类
java.io.ObjectOutputStream
类,将Java对象的原始数据类型写出到文件,实现对象的持久存储。
构造方法
public ObjectOutputStream(OutputStream out)
: 创建一个指定OutputStream的ObjectOutputStream。
构造举例,代码如下:
FileOutputStream fileOut = new FileOutputStream("E:\\s.txt");
ObjectOutputStream out = new ObjectOutputStream(fileOut);
序列化操作
- 一个对象要想序列化,必须满足两个条件:
-
该类必须实现
java.io.Serializable
接口,Serializable
是一个标记接口,不实现此接口的类将不会使任何状态序列化或反序列化,会抛出NotSerializableException
。说明:Serializable:它是一个标记性接口。这个接口中没有任何的方法,这种接口称为标记型接口!它仅仅是一个标识。是一个标记接口为了启动一个序列化功能。只有具备了这个接口标识的类才能通过Java中的序列化和反序列化流操作这个对象。
注意:只要一个类实现了Serializable接口,那么都会给每个实现类分配一个序列版本号作为唯一标识。
-
该类的所有属性必须是可序列化的。如果有一个属性不需要可序列化的,则该属性必须注明是瞬态的,使用
transient
关键字修饰。
//为了保证学生对象可以被序列化,我们让Student类来实现Serializable接口
public class Student implements Serializable {
//属性
private String name;
private transient int age; // transient瞬态修饰成员,不会被序列化
public Student(String name, int age) {
this.name = name;
this.age = age;
}
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;
}
}
2.写出对象方法
public final void writeObject (Object obj)
: 将指定的对象写出。
public class SerializeDemo{
public static void main(String [] args) throws IOException {
//创建学生对象
Student s = new Student("黑旋风",18);
//把创建出来的学生对象持久化保存在硬盘中
//创建序列化对象 创建输出流对象并关联目标文件
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("E:\\s.txt"));
//使用序列化对象中的方法持久化学生对象
oos.writeObject(s);// 姓名被序列化,年龄没有被序列化。
//关闭资源
oos.close();
}
}
3.3 ObjectInputStream类
ObjectInputStream反序列化流,将之前使用ObjectOutputStream序列化的原始数据恢复为对象。
这种操作叫做反序列化。
反序列化:可以把序列化后的对象(硬盘上的文件中的对象数据),读取到内存中,然后就可以直接使用对象。这样做的好处是不用再一次创建对象了,直接反序列化就可以了。
构造方法
public ObjectInputStream(InputStream in)
: 创建一个指定InputStream的ObjectInputStream。
反序列化操作1
如果能找到一个对象的class文件,我们可以进行反序列化操作,调用ObjectInputStream
读取对象的方法:
public final Object readObject ()
: 读取一个对象。
public class DeserializeDemo {
public static void main(String [] args) throws IOException, ClassNotFoundException {
//创建反序列化对象,指定一个字节输入流用来读取持久文件
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("E:\\s.txt"));
//使用反序列化对象ois调用函数进行读取数据
Student s = (Student) ois.readObject();
System.out.println(s.getName()+"==="+s.getAge());
//关闭资源
ois.close();
}
}
输出结果:
黑旋风===0
反序列化操作2
当在反序列化对象之前,我们对Student类做了一些简单的修改,无关紧要的修改。例如给Student类添加一个属性字段或者函数都可以,再次反序列化,就出问题了,报如下图所示的异常:
分析异常:
通过以上发生的异常我们发现是由于readObject()函数发生的异常,所以我们接下来会查看readObject()函数。
接下来查看InvalidClassException 异常类:
问题一:什么是该类的序列版本号呢?
类要进行序列化操作时,需要实现Serializable接口。(Serializable接口也称为标记接口),实现了标记接口的类,该类会存在一个标记值。这个标记值就是该类的序列版本号。这个版本号和该类相关联。
说明:也就是说只要一个类实现Serializable接口,那么在编译源文件时,生成的class文件中就会生成一个和该类相关联的序列版本号。
问题二:这个版本号有什么作用呢?
在序列化时,这个版本号会随着对象一起被序列化到本地文件中。在反序列化的时候,使用流从硬盘上会读取之前序列化的文件,那么jvm会拿着使用流读取到的序列化的版本号和本地硬盘上即class文件中的序列化版本号进行匹配,如果不匹配就会抛异常。Java.io.InvalidClassException。
所以可以理解这个序列化版本号serialVersionUID是用来验证的,防止反序列化的对象和本地的类不匹配。
问题三:这个序列化版本号是如何生成的呢?
由于实现类已经实现了Serializable接口,那么只要重新编译源文件的时候,编译器就会根据类的各个方面(如类的成员变量、成员函数、修饰符、函数返回值类型等)计算成为该类的默认 serialVersionUID 值(版本号)。
serialVersionUID 称为序列版本编号(标记值)。
上述代码如果在反序列化之前修改Student类时,会在报的异常中出现如下图所示的提示信息:
cn.itcast.sh.otherstream.demo.Student;
local class incompatible: stream classdesc serialVersionUID = -7620680200402190796,
local class serialVersionUID = 6830451359866645744
stream classdesc serialVersionUID = -7620680200402190796, 表示从流中读取的版本号(反序列化时从硬盘文件中读取的)
local class serialVersionUID = 6830451359866645744 ,表示Student类的序列版本号(表示Student类实现Serializable接口时,编译后在生成的class文件中的序列化版本号)
上述两个版本号不一致,所以报异常。
通过以上分析,可以得到一个结论:
如果可以保证反序列化对象和序列化对象的标记值相同,就可以避免异常的发生。
那么我们如何做才能保证反序列化对象和序列化对象的标记值相同呢?
我们修改Student类是无关紧要的。在我们修改Student类的时候,我们不希望它抛异常。我们可以给类定义一个默认的版本号,即给Student类添加标记值也就是版本号serialVersionUID。这样一来,添加的标记值即版本号会随着对象的序列化持久保存。无论是序列化,还是反序列化,都不会再根据类的各个方面计算版本号了。序列化和反序列化的版本号会永远一致,所以不会抛出异常,这样就可以避免InvalidClassException异常的发生了。
但是,这样一来,类的安全问题,只能自己来维护。因为已经将类的对象序列化之后,由于类中已经显示定义了版本号,那么反序列化的时候即使修改了Student类,也不会报异常了。
在idea中给自定义类Student添加版本号方法如下所示:
1)File----->Settings
2)Editor–>inspections→serialization issues→选择图中的选项。勾上serializable class without ‘serialVersionUID’
3)点击确定,将光标放到实体类上,按alt+回车就可以出现生成序列版本ID了
总结,记住:
1、当一个对象需要被序列化 或 反序列化的时候对象所属的类需要实现Serializable接口。
2、被序列化的类中需要添加一个serialVersionUID。
3.4 练习:序列化集合
- 将存有多个自定义对象的集合序列化操作,保存到
list.txt
文件中。 - 反序列化
list.txt
,并遍历集合,打印对象信息。
案例分析
- 把若干学习对象 ,保存到集合中。
- 把集合序列化。
- 反序列化读取时,只需要读取一次,转换为集合类型。
- 遍历集合,可以打印所有的学生信息
案例实现
public class SerTest {
public static void main(String[] args) throws Exception {
// 创建 学生对象
Student student = new Student("老王", "laow");
Student student2 = new Student("老张", "laoz");
Student student3 = new Student("老李", "laol");
ArrayList<Student> arrayList = new ArrayList<>();
arrayList.add(student);
arrayList.add(student2);
arrayList.add(student3);
// 序列化操作
serializ(arrayList);
// 反序列化
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("list.txt"));
// 读取对象,强转为ArrayList类型
ArrayList<Student> list = (ArrayList<Student>)ois.readObject();
for (int i = 0; i < list.size(); i++ ){
Student s = list.get(i);
System.out.println(s.getName()+"--"+ s.getPwd());
}
}
private static void serializ(ArrayList<Student> arrayList) throws Exception {
// 创建 序列化流
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("list.txt"));
// 写出对象
oos.writeObject(arrayList);
// 释放资源
oos.close();
}
}
第四章 打印流(了解)
4.1 概述
平时我们在控制台打印输出,是调用print
方法和println
方法完成的,这两个方法都来自于java.io.PrintStream
类,该类能够方便地打印各种数据类型的值,是一种便捷的输出方式。
4.2 PrintStream类
构造方法
public PrintStream(String fileName)
: 使用指定的文件名创建一个新的打印流。
构造举例,代码如下:
PrintStream ps = new PrintStream("day10\\ps.txt");
改变打印流向
System.out
就是PrintStream
类型的,只不过它的流向是系统规定的,打印在控制台上。
public class Test01 {
public static void main(String[] args) throws Exception {
// 调用系统的打印流,控制台直接输出97
System.out.println(97);
// 创建打印流,指定文件的名称
PrintStream ps = new PrintStream("ps.txt");
//直接使用对象调用方法输出到指定文件中
ps.println(100);
//设置系统的打印流流向,输出到ps.txt
System.setOut(ps);
System.out.println("你爱我");
//打印流是自动刷新
System.out.println(1);
System.out.println(true);
System.out.println("哈哈哈");
ps.close();
}
}
说明:System类中的static void setOut(PrintStream out) 方法表示重新分配“标准”输出流。 改变流的输出方向。
第五章 装饰设计模式
在我们今天所学的缓冲流中涉及到java的一种设计模式,叫做装饰模式,我们来认识并学习一下这个设计模式。
1.概述
装饰模式指的是在不改变原类, 不使用继承的基础上,动态地扩展一个对象的功能,功能就是方法。
2.装饰设计模式写法
- 装饰类和被装饰类必须实现相同的接口 (必须有接口)
- 在装饰类中必须传入被装饰类的引用(对象)
- 在装饰类中对需要扩展的方法进行扩展
- 在装饰类中对不需要扩展的方法调用被装饰类中的同名方法
3.代码演示
//明星接口 共同父接口
public interface Star {
//唱歌
public abstract void sing();
//演戏
public abstract void play();
}
//刘德华 被装饰类
public class LiuDeHua implements Star {
@Override
public void sing() {
System.out.println("刘德华唱冰雨");
}
@Override
public void play() {
System.out.println("刘德华演无间道");
}
}
//刘德华增强类 装饰类
public class BufferedLiuDeHua implements Star {
//LiuDeHua类(被装饰类)的对象作为成员变量
private LiuDeHua ldh;
//构造方法
public BufferedLiuDeHua(LiuDeHua ldh) {
this.ldh = ldh;
}
@Override
public void sing() {
System.out.println("刘德华唱rap 淡黄的长裙 蓬松的头发");
}
@Override
public void play() {
//调用原来的方法
ldh.play();
}
}
public class Demo测试类 {
public static void main(String[] args) {
//测试刘德华这个类
LiuDeHua ldh = new LiuDeHua();
ldh.sing();
ldh.play();
//-------------------------------
//测试刘德华增强类
BufferedLiuDeHua bl = new BufferedLiuDeHua(ldh);
bl.sing();
bl.play();
}
}
说明:
第六章 commons-io包
1.概述
commons-io是apache开源基金组织提供的一组有关IO操作的类库,可以挺提高IO功能开发的效率。
这些内容不在JDK里面,在使用的时候单独导入。
2.使用方式
-
下载commons-io相关jar包;http://commons.apache.org/proper/commons-io/
你无需下载,直接到今日下发的资料中找到使用即可。
-
把commons-io-2.6.jar包复制到指定的项目的lib目录中
说明:以后我们只要使用不是jdk的内容即第三方(oracle属于第一方,我们属于第二方,其他的都是第三方)的技术都需要导包。
导包步骤:
1)在当前项目根目录下新建一个lib(是library库的缩写)文件夹
2)将第三方jar包放到lib文件夹下
- 将commons-io-2.6.jar加入到classpath
选中jar包右键加入到classpath下。选择add as library
注意:必须按照上面去做,否则无法使用。
3.方法
-
IOUtils类中
- public static int copy(InputStream in, OutputStream out) 文件复制(适合文件大小为2GB以下)
- public static long copyLarge(InputStream in, OutputStream out) 文件复制(适合文件大小为2GB以上)
public class Test01 { public static void main(String[] args) throws Exception { //在IOUtils类下面 // public static int copy(InputStream in, OutputStream out) 文件复制(适合文件大小为2GB以下) // public static long copyLarge(InputStream in, OutputStream out) 文件复制(适合文件大小为2GB以上) //输入流 FileInputStream fis = new FileInputStream("3.jpg"); //输出流 FileOutputStream fos = new FileOutputStream("4.jpg"); //复制 IOUtils.copy(fis,fos); //关流 fos.close(); fis.close(); } }
-
FileUtils类中
-
public static void copyFileToDirectory(File srcFile, File destFile) //复制文件到另外一个目录下。
-
public static void copyDirectoryToDirectory(File srcDir,File destDir);文件夹复制,将srcDir复制到destDir
需求1:将D:\1.mp3复制到F盘
需求2:D:\test文件夹复制到F盘下
public class Test01 { public static void main(String[] args) throws Exception { // public static void copyFileToDirectory(File srcFile, File destFile) //复制文件到另外一个目录下。 // public static void copyDirectoryToDirectory(File srcDir,File destDir);文件夹复制,将srcDir复制到destDir //需求:将D:\1.mp3复制到F盘 //文件 // File f1 = new File("D:\\1.mp3"); // //文件夹 // File f2 = new File("F:\\"); // // //调用方法 // FileUtils.copyFileToDirectory(f1,f2); //copyDirectoryToDirectory() :把文件夹复制到一个文件夹下 //文件夹 File f3 = new File("D:\\test"); ////文件夹 File f4 = new File("F:\\"); //调用方法 FileUtils.copyDirectoryToDirectory(f3,f4); } }
-