序列化总结
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。浅克隆和深克隆的主要区别在于是否支持引用类型的成员变量的复制
浅克隆
-
被复制的类需要实现Clonenable接口(不实现的话在调用clone方法会抛出CloneNotSupportedException异常), 该接口为标记接口,不含任何方法
-
覆盖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());
}
}
特殊RandomAccessFile
RandomAccessFile不属于IO流,支持对文件的读取和写入随机访问
public class RandomAccessFile implements DataOutput, DataInput, Closeable
- DataInput接口中定义了基本数据类型的读操作,例如readInt/readDouble等
- DataOutput接口定义了基本数据类型的写操作,例如writeInt/writeDouble等
RandomAccessFile是Java输入/输出流体系中功能最丰富的文件内容访问类,既可以读取文件内容,也可以向文件输出数据。
RandomAccessFile构造器
RandomAccessFile类在创建对象时,除了指定文件本身,还需要指定一个mode参数指定
RandomAccessFile的访问模式,该参数有如下四个值:
- r以只读方式打开指定文件。如果试图对该RandomAccessFile指定的文件执行写入方法则会抛出IOException
- rw以读取、写入方式打开指定文件。如果该文件不存在,则尝试创建文件
- rws以读取、写入方式打开指定文件。相对于rw模式,还要求对文件的内容或元数据的每个更新都同步写入到底层存储设备,默认情形下(rw模式下),是使用buffer的,只有cache满的或者使用RandomAccessFile.close()关闭流的时候儿才真正的写到文件
- rwd与rws类似,只是仅对文件的内容同步更新到磁盘,而不修改文件的元数据
RandomAccessFile写方法
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SIq7UkZD-1665384097475)(C:\Users\dfxy\AppData\Roaming\Typora\typora-user-images\image-20221010143850999.png)]
RandomAccessFile raf=new RandomAccessFile("d:\\abc.data","rw");
raf.writeInt(1);
raf.writeChars("yanjun");
raf.writeDouble(1234.0/567);
raf.close();
RandomAccessFile读方法
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NL7A1GcR-1665384097477)(C:\Users\dfxy\AppData\Roaming\Typora\typora-user-images\image-20221010143941384.png)]
RandomAccessFile raf=new RandomAccessFile("d:/abc.data","r");
int id=raf.readInt();
//String name=raf.readLine();
//byte[] bb=new byte[4];
//raf.readFully(bb);
//String name=new String(bb);
int len=raf.readInt();
StringBuilder sb=new StringBuilder();
for(int i=0;i<len;i++)
sb.append(raf.readChar());
String name=sb.toString();
double salary=raf.readDouble();
读取文件时如果到文件结尾是抛出异常EOFException,所以这里采用的是异常用于判断文件结束
典型应用:多线程下载和断点续传
记录指针的特殊方法
与普通的输入/输出流不同的是: RandomAccessFile支持跳到文件任意位置读写数据,RandomAccessFile对象包含一个记录指针,用以标识当前读写处的位置,当程序创建一个新的RandomAccessFile对象时,该对象的文件记录指针对于文件头(也就是0处),当读写n个字节后,文件记录指针将会向后移动n个字节。除此之外,RandomAccessFile可以自由移动该记录指针
RandomAccessFile包含两个方法来操作文件记录指针:文件指针是按照字节数进行统计,取值范围为[0,file.length()]
-
long getFilePointer():返回文件记录指针的当前位置
-
void seek(long pos):将文件记录指针定位到pos位置
-
skipByte(int step); 相对当前位置跳过step个字节
如果文件中间的内容需要进行修改,注意新内容中的字串和原始文件内容的长度应该一致,否则可能会导致修改数据后面的内容无法正常读取
常见方法
- void close() 关闭操作
- int read(byte[] b)将内容读取到一个byte数组之中
- byte readByte()读取一个字节
- int readInt()从文件中读取整型数据… readDouble()等8种简单类型
- String readLine()读取一行数据
- void writeBytes(String s)将一个字符串写入到文件之中,按字节的方式处理。writeChars
- void writeInt(int v)将一个int型数据写入文件,长度为4位。…writeDouble等8种类型