0804(024天 输入输出流04 对象数据流+对象拷贝)
每日一狗(田园犬西瓜瓜)
对象数据流+拷贝
文章目录
1. 对象数据流
1.1 对象数据流的文件读写
- ObjectInputStream
- ObjectOutputStream
读写对象数据:
要求对象类必须实现序列化接口(但是没有时间代价,只需要一个声明实现就行,接口中并没有任何抽象方法)
package com.yang1;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
public class 对象的dafa读写 {
public static void main(String[] args) throws FileNotFoundException, IOException, ClassNotFoundException {
User user = new User();
user.setId(20);
user.setUserName("guofeiyang");
try (ObjectOutputStream dos = new ObjectOutputStream(
new BufferedOutputStream(new FileOutputStream("data/a.data")))) {
dos.writeObject(user);
dos.flush();
}
try (ObjectInputStream ois = new ObjectInputStream(
new BufferedInputStream(new FileInputStream("data/a.data")))) {
Object newUser = ois.readObject();
System.out.println(newUser);
}
}
}
class User implements Serializable {
/**
*
*/
private static final long serialVersionUID = 1L;
private int id;
private String userName;
@Override
public String toString() {
return "User [id=" + id + ", userName=" + userName + "]";
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getUserName() {
return userName;
}
public void setUserName(String userName) {
this.userName = userName;
}
}
1.2 对象的序列化
序列化接口 Serializable
序列化:通过某种方法将一个对象的存储区域转换成二进制流方便存储;
反序列化:将存储的二进制转成对象类型
这个序列化和反序列化方法是由虚拟机来进行实现的,我们只需要告诉虚拟机,我这个对象可以被序列化,我在存储的时候虚拟机会自动实现序列化的操作。
Serializable接口中没有任何抽象方法,所以就不用自定义序列化方法
- 对象属性必须都能直接或者间接的可以被序列化
- 如果有属性不能被序列化的话而且我又懒得自定义序列化方法可以使用 transient 修饰该属性,那么这个属性就不会参与这个对象的序列化
public interface Serializable {
}
自定义序列化接口 Externalizable
那那些不能被序列化的对象属性就不能被序列化了吗?不能够呀!
Externalizable接口支持自定义序列化接口
public interface Externalizable extends java.io.Serializable {
// 对象序列化的方法
void writeExternal(ObjectOutput out) throws IOException;
// 对象的反序列化的放法
void readExternal(ObjectInput in) throws IOException, ClassNotFoundException;
}
案例:实现自定义序列化方法的类
package com.yang1;
import java.io.Externalizable;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInput;
import java.io.ObjectOutput;
public class User2 implements Externalizable {
private long id;
private String username;
private InputStream memo;
public User2() throws Exception {
memo = new FileInputStream("data/data1.txt");
}
@Override
public void writeExternal(ObjectOutput out) throws IOException {
out.writeLong(id);
out.writeUTF(username);
memo.close();
}
@Override
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
this.id = in.readLong();
this.username = in.readUTF();
this.memo = new FileInputStream("data/data1.txt");
System.out.println("User2 [id=" + id + ", username=" + username + "]");
}
///
public long getId() {
return id;
}
@Override
public String toString() {
return "User2 [id=" + id + ", username=" + username + ", memo=" + memo + "]";
}
public void setId(long id) {
this.id = id;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public InputStream getMemo() {
return memo;
}
public void setMemo(InputStream memo) {
this.memo = memo;
}
}
序列化总结
Java序列化就是将一个对象转化为一个二进制表示的字节数组,通过保存或则转移这些二进制数组达到 持久化的目的。要实现序列化,需要实现java.io.Serializable接口。反序列化是和序列化相反的过程,就 是把二进制数组转化为对象的过程。在反序列化的时候,必须有原始类的模板才能将对象还原。
- 当父类实现了Serializable接口的时候,所有的子类都能序列化
- 子类实现了Serializable接口,父类没有,父类中的属性不能被序列化(不报错,但是数据会丢失,父类有的子类没有的数据会丢失)
- 如果序列化的属性是对象,对象必须也能序列化,否则会报错
- 反序列化的时候,如果对象的属性有修改或则删减,修改的部分属性会丢失,但是不会报错
- 在反序列化的时候serialVersionUID被修改的话,会反序列化失败
- 在存Java环境下使用Java的序列化机制会支持的很好,但是在多语言环境下需要考虑别的序列化机 制,比如xml、json或protobuf等
serialVersionUID值是用于确保类序列化与反序列化的兼容性问题的,如果序列化和反序列化过程中这 两个值不一样,那么将导致序列化失败
可以看到编译器推荐两种方式,一种是生成默认的versionID,这个值为1L,还有一种方式是根据类名、 接口名、成员方法及属性等来生成一个 64 位的哈希字段,只要类名、方法名、变量有修改或者有空 格、注释、换行等操作,计算出来的哈希字段都会不同,当然这里需要注意,每次有以上的操作的时候 尽量都要重新生成一次serialVerionUID,编译器并不会自动修改
- Java 序列化只是针对对象的属性的传递,至于方法和序列化过程无关
- 当一个父类实现了序列化,那么子类会自动实现序列化,不需要显示实现序列化接口,反过来,子 类实现序列化,而父类没有实现序列化则序列化会失败—即序列化具有传递性
- 当一个对象的实例变量引用了其他对象,序列化这个对象的时候会自动把引用的对象也进行序列化 (实现深度克隆)
- 当某个字段被申明为 transient 后,默认的序列化机制会忽略这个字段
- 被申明为 transient 的字段,如果需要序列化,可以添加两个私有方法writeObject和readObject或 者实现Externalizable接口
2、 Object对象数据存储机制(面试重灾区)
2.1 构造方法的调用问题
他在读取user对象数据的时候不会调用user类的构造器,从机制上也很容易想,他返回的是Object对象,怎么可能调用user类的构造器,他撑死用一下Object
2.2 他是把对象数据存储到文件中,
2.3 存储的玩意
- 对象类型
- 属性
- 方法签名(没有方法的具体实现)
2.4 进进出出
序列化出去时候的类和序列化进来的类必须要一毛一样(不能添加或减少方法),反正就是写入对象后给添加一个方法,然后在读出来后,在调用方法时会出错。
有一个序列号可以解决这个报错,可以使用警告信息中的第二个来生成一个当前项目中绝对不会重复的一个序列号
2.5 敏感信息的处理
2.6 对象克隆 对象要实现Cloneable接口
- 克隆:生成一个对象不同数据相同的另一个对象。
- 对象的克隆不会调用构造器
Object对象有一个方法clone是一个native方法,在底层实现的。
深克隆
- 使用序列化和反序列化
/// 被克隆对象
package com.yang3;
import java.io.Serializable;
import java.util.Date;
public class MyUser implements Cloneable, Serializable {
private static final long serialVersionUID = -5323453670967927322L;
private int i;
private Date time;
@Override
public String toString() {
return "MyUser [i=" + i + ", time=" + time + "]";
}
public int getI() {
return i;
}
public void setI(int i) {
this.i = i;
}
public Date getTime() {
return time;
}
public void setTime(Date time) {
this.time = time;
}
}
/克隆地调用着
package com.yang3;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.util.Date;
public class Test01 {
public static void main(String[] args) throws IOException, ClassNotFoundException {
// 深克隆
MyUser u1 = new MyUser();
u1.setI(20);
u1.setTime(new Date());
// 创建可变长数组数据流
ByteArrayOutputStream bais = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bais);
// 创建可变长数组数据流
oos.writeObject(u1);
oos.close();
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(bais.toByteArray()));
MyUser u2 = (MyUser) ois.readObject();
u2.getTime().setYear(9000); // 改一下深克隆出来的引用类型的数据
System.out.println(u1); // MyUser [i=20, time=Thu Aug 04 15:18:40 CST 2022]
System.out.println(u2); // MyUser [i=20, time=Wed Aug 04 15:18:40 CST 10900]
}
}
浅克隆
只进行一层克隆,针对简单值类型他会进行数据拷贝,但是针对引用类型他只会克隆引用,不会对引用类型的数据进行拷贝。
克隆对象和源对象公用一个引用类型的地址,特殊(字符串的修改都会引发字符串的新建,所以字符串属性的修改不会)
实现:覆盖clone()方法,访问修饰符设为public。方法中调用super.clone()方法得到需要的复制对象。 native为本地方法
public class T01 {
public static void main(String[] args) {
A1 a1 = new A1();
a1.setAge(20);
a1.setUserName("guofeiyang");
A1 a2 = a1.clone();
a2.setAge(90);
System.out.println(a1); // A1 [age=20, userName=guofeiyang]
System.out.println(a2); // A1 [age=90, userName=guofeiyang]
}
}
public class A1 implements Cloneable {
private int age;
private String userName;
// 需要重写 clone,声明为公共,内部调用父类的克隆方法,并处理其内部的 CloneNotSupportedException(不支持克隆 异常 )
@Override
public A1 clone() {
A1 res = null;
try {
res = (A1) super.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
return res;
}
@Override
public String toString() {
return "A1 [age=" + age + ", userName=" + userName + "]";
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public String getUserName() {
return userName;
}
public void setUserName(String userName) {
this.userName = userName;
}
}
2.7 原型模式(结构型模式)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YRdJXrIx-1659616584770)(https://my-img-1302704997.cos.ap-chengdu.myqcloud.com/study-java/image-20220804201916224.png)]
你来问我要对象,我不new对象,我把我内部预先存储的对象克隆一份给你。(很多资源连接池就是用这个的)
原型模式Prototype Pattern是用于创建重复的对象,同时又能保证性能。这种类型的设计模式属于创建 型模式,它提供了一种创建对象的最佳方式。
这种模式是实现了一个原型接口,该接口用于创建当前对象的克隆。
当直接创建对象的代价比较大时, 则采用这种模式
意图:用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象。
优点:1、性能提高。 2、逃避构造函数的约束。
缺点:
1、配备克隆方法需要对类的功能进行通盘考虑,这对于全新的类不是很难,但对于已有的类不一 定很容易,特别当一个类引用不支持串行化的间接对象,或者引用含有循环结构的时候。
2、必须实现 Cloneable接口。
注意:与通过对一个类进行实例化来构造新对象不同的是,原型模式是通过拷贝一个现有对象生成新对 象的。浅拷贝实现 Cloneable,重写,深拷贝是通过实现 Serializable 读取二进制流。
具体实现
抽象类定义,资源池中的对象都直接或间接的继承于他
package com.test.a;
public abstract class Shape implements Cloneable {
private String id;
protected String type;
abstract void araw();
@Override
public Object clone() {
Object res = null;
try {
res = super.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
return res;
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
}
具体形状类
package com.test.a;
public class Rectangle extends Shape {
public Rectangle() {
type = "矩形";
}
@Override
void draw() {
System.out.println("这里是矩形的 draw()方法");
}
}
资源池管理类
package com.test.a;
import java.util.Hashtable;
public class ShapeCache {
// 声明一个资源池哈希表
private static Hashtable<String, Shape> shapeMap = new Hashtable<String, Shape>();
// 获取传入形状的字符串ID值
public static Shape getShape(String shapeID) {
Shape cachedShape = shapeMap.get(shapeID);
return (Shape) cachedShape.clone();
}
// 对每种形状都运行操作创建该形状shapeMap.put(shapeKey, shape);
public static void loadCache() {
Rectangle rectangle = new Rectangle();
rectangle.setId("666");
shapeMap.put(rectangle.getId(), rectangle);
}
}
测试类
package com.test.a;
public class PrototypePatternDemo {
/*
* 这里是矩形的 draw()方法
*
* com.test.a.Rectangle@73a28541
*
* com.test.a.Rectangle@6f75e721
*
* com.test.a.Rectangle@69222c14
*
* com.test.a.Rectangle@606d8acf
*
* com.test.a.Rectangle@782830e
*/
public static void main(String[] args) {
ShapeCache.loadCache();
Shape clonShape = (Shape) ShapeCache.getShape("666");
clonShape.draw(); // 这里是矩形的 draw()方法
for (int i = 0; i < 5; i++) {
Shape tmp = (Shape) ShapeCache.getShape("666");
System.out.println(tmp);
}
}
}
3. 一些细节
6、文件结尾判定
使用EOFException来进行文件结尾的判定
// 读出时报错
ObjectInputStream ois = new ObjectInputStream(
new FileInputStream("data/03.data")
);
while (true) {
try {
Object tmp = ois.readObject();
System.out.println(tmp);
} catch (EOFException e) {
}
}
7、对象数据文件的追加写入对象数据
不能直接使用追加写
package com.yang1;
import java.io.EOFException;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
public class Test03 {
/*
* 对象类型追加写
*/
public static void main(String[] args) throws Exception {
// // 写入
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("data/03.data", true));
User u1 = new User();
u1.setId(21);
u1.setUserName("guofeiyang");
oos.writeObject(u1);
oos.close();
// 追加写
oos = new ObjectOutputStream(new FileOutputStream("data/03.data"));
User u2 = new User();
u1.setId(21);
u1.setUserName("guofeiyang");
oos.writeObject(u2);
oos.close();
// 读出时报错
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("data/03.data"));
while (true) {
try {
Object tmp = ois.readObject(); // java.io.StreamCorruptedException: invalid type code: AC
System.out.println(tmp);
} catch (EOFException e) {
}
}
}
}
只能新建一个文件将数据读取出来写入新的文件,再将要追加的数据写到新文件末尾,在用新文件替换原来的文件
public static void test1() throws Exception {
// 写入
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("data/03.data"));
User u1 = new User();
u1.setId(21);
u1.setUserName("guofeiyang");
oos.writeObject(u1);
oos.close();
// 读出并拷贝到新文件中
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("data/03.data"));
oos = new ObjectOutputStream(new FileOutputStream("data/03tmp.data"));
while (true) {
try {
Object tmp = ois.readObject();
if (tmp != null) {
oos.writeObject(tmp);
}
} catch (EOFException e) {
break;
}
}
ois.close();
// 写入新的数据
User u3 = new User();
u3.setId(90);
u3.setUserName("追加数据");
oos.writeObject(u3);
oos.close();
File f1 = new File("data/03.data");
f1.delete();
File f2 = new File("data/03tmp.data");
f2.renameTo(f1);
// 读出时报错
ois = new ObjectInputStream(new FileInputStream("data/03.data"));
while (true) {
try {
Object tmp = ois.readObject(); // java.io.StreamCorruptedException: invalid type code: AC
System.out.println(tmp);
} catch (EOFException e) {
break;
}
}
}