【java】对象流

对象流

使用DataInputStream或者DataOutputStream可以读写对象数据,但是操作比较繁琐

//从文件中按照id值查找对应的对象
int id=dis.readInt(); //用户id--用户标识
int len=dis.readInt(); //用户名称的字符数
StringBuilder username=new StringBuilder(); //用户名称
for(int i=0;i<len;i++) //一次读取一个字符,然后拼接成完整的字符串
username.append(dis.readChar());
len=dis.readInt();
StringBuilder password=new StringBuilder(); //用户口令
for(int i=0;i<len;i++)
password.append(dis.readChar());
double balance=dis.readDouble(); //用户余额
if(dis==id){
res=new Account(); //Account是一个自定义类型,用于封装账户信息
res.setUsername(username.toString());
res.setPassword(password.toString());
res.setBalance(balance);
break;
}

SUN提供了ObjectInputStream/ObjectOutputStream可以直接将Object写入或读出

这里实际上还有针对8种简单类型及其包装类的操作方法,以及针对String类型的操作方法

  • readObject():Object
  • writeObject(Object):void
//简单写法,应该使用try/finally结构或者使用try/resource的写法
Date now=new Date();
ObjectOutputStream oos=new ObjectOutputStream(
new BufferedOutputStream(new FileOutputStream("data3.txt")));
oos.writeObject(now);
oos.close();
ObjectInputStream ois=new ObjectInputStream(
new BufferedInputStream(new FileInputStream("data3.txt")));
Object obj=ois.readObject();
if(obj!=null && obj instanceof Date) {
Date dd=(Date)obj;
System.out.println(dd);
}
ois.close();

读写一个对象的前提是这个类型的对象是可以被序列化的;

  • NotSerializableException

对象序列化【简单来说就是将对象可以直接转换为二进制数据流】/对象的反序列化【可以将二进制数据流转换为对象】,这一般依靠JVM实现,编程中只做声明对象序列化的目标是将对象保存到磁盘中,或允许在网络中直接传输对象。对象序列化机制允许把内存中的Java对象转换成平台无关的二进制流,从而允许把这种二进制流持久地保存在磁盘上,通过网络将这种二进制流传输到另一个网络节点,其他程序一旦获取到这种二进制流,都可以将这种二进制流恢复成原来的Java对象

1、如何声明对象所属于的类可以进行序列化和反序列化Serializable/Externalizable接口其中的接口没有任何定义,仅仅只起到了说明的作用,这种接口叫做标志接口或者旗标接口

2、可以通过ObjectInputStream【readObject():Object】和
ObjectOutputStream【writeObject(Object):void】提供的方法直接操作对象

3、输出对象

User user = new User();
user.setId(100L);
user.setUsername("zhangsan");
user.setPassword("123456");
ObjectOutputStream oos = new ObjectOutputStream(new
FileOutputStream("users.data"));
oos.writeObject(user);
oos.close();

4、读取对象

ObjectInputStream ois = new ObjectInputStream(new
FileInputStream("users.data"));
Object temp = ois.readObject();
if (temp != null && temp instanceof User) {
User user = (User) temp;
System.out.println(user);
}
ois.close();
编码细节

1、需要通过对象流读写的对象必须实现了序列化接口,否则java.io.NotSerializableException

class User implements Serializable

2、Serializable接口是标志接口,没有需要实现的方法,所有的序列化和反序列化操作由VM负责实现。

Externalizable接口定义为public interface Externalizable extends java.io.Serializable,这个接口中包含两个方法需要实现writeExternal自定义实现对象的序列化,readExternal自定义实现对象的反序列化。除非特殊需求一般不使用Externalizable接口,因为没有必要自定义

class User implements Externalizable {
@Override
public void writeExternal(ObjectOutput out) throws IOException {
// 写出对象的操作
System.out.println("现在需要写出对象:" + this);
out.writeLong(this.id);
out.writeUTF(this.username);
out.writeUTF(this.password);
}
@Override
public void readExternal(ObjectInput in) throws IOException,
ClassNotFoundException {
// 读取对象的操作
this.id = in.readLong();
this.username = in.readUTF();
this.password = in.readUTF();
}
public User(){}

3、类型转换问题:

Object temp = ois.readObject();
if (temp != null && temp instanceof User) {
User user = (User) temp;
System.out.println(user);
}

4、private static final long serialVersionUID = 6889840055394511246L

如果不添加序列号,则会有警告信息,但是不是错误信息

一般选择Add generated serial version ID会生成一个在项目中永不重复的的序列版本编号

序列版本号可以不用添加,这个序列版本号是一种序列化和反序列化中快速识别类型的简单方法,比不加序列号的识别效率高。引入的功能是如果版本号不对应,不会进行类型识别,而是直接报异常
InvalidClassException

5、一般针对敏感数据不应该进行序列化操作,针对不需要进行序列操作的属性可以添加一个关键字transient,表示该属性不参与序列化和反序列化操作

class User implements Serializable {
private transient String password; //transient用于声明该属性不支持序列化操作
class User implements Serializable {
private String username;
private transient String password;
private Role role;//因为Role没有实现序列化接口,所以写出user对象时会有报错
NotSerializableException。处理报错的方法有:1、可以给Role类定义添加序列接口。2、在role
属性上添加transient表示这个属性不序列化处理

6、读文件的判断:读取文件时可以通过EOFException异常来判断文件读取结束

try (
ObjectInputStream ois = new ObjectInputStream(new
FileInputStream("users.data"));
) {
while (true) {
try {
Object temp = ois.readObject();
if (temp != null && temp instanceof User) {
User user = (User) temp;
System.out.println(user);
}
} catch (EOFException ex) { // 这个异常是用于判断文件结尾,所以不需要进行处break;
}
}
}

已经向文件中写入数据后,继续追加存储,则读取数据会出现StreamCorruptedException

  • ObjectOutputStream oos=new ObjectOutputStream(new BufferedOutputStream(new FileOutputStream(“users.data”,true)));
// 向文件中追加新数据 true
// 首先读取数据,然后再统一写入
Object[] arr = new Object[100];
int counter = 0;
File ff = new File("user.data");
if (ff.exists()) {
ObjectInputStream ois = new ObjectInputStream(new
FileInputStream("user.data"));
while (true) {
try {
Object temp = ois.readObject();
arr[counter++] = temp;
} catch (EOFException e) {
break;
}
}
ois.close();
}
// 追加数据
User user = new User();
user.setId(299L); user.setUsername("name299");
user.setPassword("pwd299");
arr[counter++] = user;
// 然后统一写出到文件中
ObjectOutputStream oos = new ObjectOutputStream(new
FileOutputStream("user.data"));
for (int i = 0; i < arr.length; i++) {
Object temp = arr[i];
if (temp != null)
oos.writeObject(temp);
}
oos.close();

问题:如果某个类的属性类型不是基本类型或者String类型,且没有实现可序列化接口,则该类型属性类是不可序列化

针对于对象中的InputStream/OutputStream之类的资源类型的属性,不仅不能进行序列化操作,而且在序列化之前应该释放资源,在反序列化后应该重新创建资源链接。Externalizable

class User implements Externalizable {
private Long id; // 要求自增长
private String username;
private String password;
private InputStream is; //输入流不能被序列
@Override
public void writeExternal(ObjectOutput out) throws IOException {
//writeObject方法时调用
is.close(); //释放资源
out.writeLong(this.id);
out.writeUTF(this.username);
}
@Override
public void readExternal(ObjectInput in) throws IOException,
ClassNotFoundException { //readObject方法时执行
this.id=in.readLong();
this.username=in.readUTF();
is=new FileInputStream("ddd.txt");//重新获取资源
}
}

序列化总结
Java序列化就是将一个对象转化为一个二进制表示的字节数组,通过保存或则转移这些二进制数组达到持久化的目的。要实现序列化,需要实现java.io.Serializable接口。反序列化是和序列化相反的过程,就是把二进制数组转化为对象的过程。在反序列化的时候,必须有原始类的模板才能将对象还原。

  • 当父类实现了Serializable接口的时候,所有的子类都能序列化
  • 子类实现了Serializable接口,父类没有,父类中的属性不能被序列化(不报错,但是数据会丢失)
  • 如果序列化的属性是对象,对象必须也能序列化,否则会报错
    反序列化的时候,如果对象的属性有修改或则删减,修改的部分属性会丢失,但是不会报错
  • 在反序列化的时候serialVersionUID被修改的话,会反序列化失败
  • 在存Java环境下使用Java的序列化机制会支持的很好,但是在多语言环境下需要考虑别的序列化机制,比如xml、json或protobuf等

serialVersionUID值是用于确保类序列化与反序列化的兼容性问题的,如果序列化和反序列化过程中这两个值不一样,那么将导致序列化失败

可以看到编译器推荐两种方式,一种是生成默认的versionID,这个值为1L,还有一种方式是根据类名、接口名、成员方法及属性等来生成一个 64 位的哈希字段,只要类名、方法名、变量有修改或者有空格、注释、换行等操作,计算出来的哈希字段都会不同,当然这里需要注意,每次有以上的操作的时候尽量都要重新生成一次serialVerionUID,编译器并不会自动修改

  • Java 序列化只是针对对象的属性的传递,至于方法和序列化过程无关
  • 当一个父类实现了序列化,那么子类会自动实现序列化,不需要显示实现序列化接口,反过来,子类实现序列化,而父类没有实现序列化则序列化会失败—即序列化具有传递性
  • 当一个对象的实例变量引用了其他对象,序列化这个对象的时候会自动把引用的对象也进行序列化(实现深度克隆)
  • 当某个字段被申明为 transient 后,默认的序列化机制会忽略这个字段
  • 被申明为 transient 的字段,如果需要序列化,可以添加两个私有方法writeObject和readObject或者实现Externalizable接口

对象克隆
在Java中所有的类都是缺省的继承自Java语言包中的Object类的,查看它的源码

native方法是非Java语言实现的代码,供Java程序调用的,因为Java程序是运行在JVM虚拟机上面的,要想访问到比较底层的与操作系统相关的就没办法了,只能由靠近操作系统的语言来实现。

克隆的对象可能包含一些已经修改过的属性,而new出来的对象的属性都还是初始化时候的值,所以当需要一个新的对象来保存当前对象的状态就靠clone方法。那么把这个对象的临时属性一个一个的赋值给新new的对象不也行嘛?可以是可以,但是一来麻烦不说,通过上面的源码都发现了clone是一个native方法,在底层实现的。

常见的Object a=new Object();Object b;b=a;这种形式的代码复制的是引用,即对象在内存中的地址,a和b对象仍然指向了同一个对象。而通过clone方法赋值的对象跟原来的对象时同时独立存在的。

两种不同的克隆方法,浅克隆ShallowClone和深克隆DeepClone。浅克隆和深克隆的主要区别在于是否支持引用类型的成员变量的复制

浅克隆

  1. 被复制的类需要实现Clonenable接口(不实现的话在调用clone方法会抛出CloneNotSupportedException异常), 该接口为标记接口,不含任何方法
  2. 覆盖clone()方法,访问修饰符设为public。方法中调用super.clone()方法得到需要的复制对象。
    native为本地方法
public class Student implements Cloneable{
private int number;
public int getNumber() {
return number;
}
public void setNumber(int number) {
this.number = number;
}
@Override
public Object clone() {
Student stu = null;
try{
stu = (Student)super.clone();
}catch(CloneNotSupportedException e) {
e.printStackTrace();
}
return stu;
}
}

调用

Student stu1 = new Student();
stu1.setNumber(12345);
Student stu2 = (Student)stu1.clone();

在浅克隆中,如果原型对象的成员变量是值类型,将复制一份给克隆对象;如果原型对象的成员变量是引用类型,则将引用对象的地址复制一份给克隆对象,也就是说原型对象和克隆对象的成员变量指向相同的内存地址。

简单来说,在浅克隆中,当对象被复制时只复制它本身和其中包含的值类型的成员变量,而引用类型的成员对象并没有复制。

深克隆

在深克隆中,无论原型对象的成员变量是值类型还是引用类型,都将复制一份给克隆对象,深克隆将原型对象的所有引用对象也复制一份给克隆对象。

简单来说,在深克隆中,除了对象本身被复制外,对象所包含的所有成员变量也将复制。

在Java语言中,如果需要实现深克隆,可以通过覆盖Object类的clone()方法实现,也可以通过序列化Serialization等方式来实现。一般使用序列化的方式实现

序列化就是将对象写到流的过程,写到流中的对象是原有对象的一个拷贝,而原对象仍然存在于内存中。通过序列化实现的拷贝不仅可以复制对象本身,而且可以复制其引用的成员对象,因此通过序列化将对象写到一个流中,再从流里将其读出来,可以实现深克隆。需要注意的是能够实现序列化的对象其类必须实现Serializable接口,否则无法实现序列化操作。

原型模式

原型模式Prototype Pattern是用于创建重复的对象,同时又能保证性能。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。

这种模式是实现了一个原型接口,该接口用于创建当前对象的克隆。当直接创建对象的代价比较大时,则采用这种模式

意图:用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象。

优点:1、性能提高。 2、逃避构造函数的约束。

缺点:1、配备克隆方法需要对类的功能进行通盘考虑,这对于全新的类不是很难,但对于已有的类不一定很容易,特别当一个类引用不支持串行化的间接对象,或者引用含有循环结构的时候。
2、必须实现Cloneable接口。

注意:与通过对一个类进行实例化来构造新对象不同的是,原型模式是通过拷贝一个现有对象生成新对象的。浅拷贝实现 Cloneable,重写,深拷贝是通过实现 Serializable 读取二进制流。

抽象类定义

public abstract class Shape implements Cloneable {
private String id;
protected String type;
abstract void draw();
public String getType(){
return type;
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public Object clone() {
Object clone = null;
try {
clone = super.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
return clone;
}
}

扩展抽象类的实体类

public class Rectangle extends Shape {
public Rectangle(){
type = "Rectangle";
}
@Override
public void draw() {
System.out.println("Inside Rectangle::draw() method.");
}
}

创建一个类获取实体类,并把它们存储在一个 Hashtable 中

public class ShapeCache {
private static Hashtable<String, Shape> shapeMap = new Hashtable<String,
Shape>();
public static Shape getShape(String shapeId) {
Shape cachedShape = shapeMap.get(shapeId);
return (Shape) cachedShape.clone();
}
// 对每种形状都运行操作创建该形状shapeMap.put(shapeKey, shape);
public static void loadCache() {
Circle circle = new Circle();
circle.setId("1");
shapeMap.put(circle.getId(),circle);
}
}

使用ShapeCache类来获取存储在Hashtable中的形状的克隆

public class PrototypePatternDemo {
public static void main(String[] args) {
ShapeCache.loadCache();
Shape clonedShape = (Shape) ShapeCache.getShape("1");
System.out.println("Shape : " + clonedShape.getType());
}
}
  • 3
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值