Java的四个标记接口:Serializable、Cloneable、RandomAccess和Remote接口
一、概述
标记接口是一些没有属性和方法的接口,也是一种设计思想。Java中的一个标记接口表示的的是一种类的特性,实现了该标记接口的类则具有该特性。如实现了Serializable接口的类,表示这个类的对象是可以进行序列化和反序列化的。Java中常见的标记接口还有Cloneable接口、RandomAccess接口和Remote接口。可以用 if(对象名 instanceof 标记接口名)检测一个类是否实现某个标记接口。
二、四个标记接口的浅析
1、java.io.Serializable标记接口
该接口用来标记类的对象是否能够可以进行序列化,或者说串行化。将对象序列化之后,可以进行持久化的储存以及在网络中进行传输。如把对象变成字节流写入到一个文件中,就是一个序列化的过程,实现了对象的持久化储存,然后你的程序可以从这个文件中读取序列化的对象并且把它还原成原来的对象,进行反序列化。如果进行序列化的类的对象没有实现Serializable接口,则会抛出NotSerializableException。
1)对象序列化有哪些特点?在对象序列化时,该对象引用的实例变量也会被序列化,如果这个实例变量是一个对象,这个对象也会被序列化。被transient修饰的实例变量也不参与序列化。在对象反序列化时transient修饰的变量将重新初始化,对象初始为null,基本数据类型被初始化为0、false等值。而static修饰的静态变量也不参与序列化的过程,在反序列化时,它的值是运行时虚拟机中该变量的值,并不是对象序列化时"储存"的值。
2)serialVersionUID是什么,有什么用?在对象序列化时,这个对象都会有一个的serialVersionUID,它是根据类的结构信息计算出来的。在反序列化时,如果对象有了不同的serialVersionUID,则虚拟机将会抛出异常,还原操作将会失败。如在ArrayList类的源码(JDK8)中就可以发现如下属性,所以JDK8中ArrayList类的对象都有一个一致的serialVersionUID值。
private static final long serialVersionUID = 8683452581122892189L;
3)代码测试
- 先创建一个Baby类,实现Serializable接口
package mytest; import java.io.Serializable; public class Baby implements Serializable{ private int age; private String name; public Baby(int age,String name) { this.age=age; this.name=name; } @Override public String toString() { return "Baby [age=" + age + ", name=" + name + "]"; } }
- 再创建一个Mother类,实现了Serializable接口。包含有一个int类型实例变量age,Baby类型的实例变量mybaby,一个String类型的静态变量name,一个修饰符为transient的String实例变量,用于序列化测试
package mytest; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.io.Serializable; public class Mother implements Serializable { private int i; private Baby mybaby;// 一个实例变量,也是一个实现了Serializable的类的对象 private static String name; private transient String nickname; public Mother(int i, Baby mybaby, String nickname) { this.i = i; this.mybaby = mybaby; this.nickname = nickname; } public void setName(String name) { this.name = name; } public String toString() { return "Mother [i=" + i + ", mybaby=" + mybaby +", name="+name+", nickname=" + nickname + "]"; } public static void main(String[] args) { Baby mybaby = new Baby(2, "小官"); Mother mother = new Mother(25, mybaby, "官妈"); mother.setName("小红");//此时静态变量name引用的字符串为"小红" ObjectOutputStream oos = null; try { FileOutputStream fos = new FileOutputStream("MyMom.ser"); //"MyMom.ser"如果不存在会自动创建 oos = new ObjectOutputStream(fos); oos.writeObject(mother);//将变量引用的对象序列化并写入MyMom.ser这个文件 } catch (Exception e) { e.printStackTrace(); } finally { if (oos != null) { try { oos.close(); } catch (IOException e) { e.printStackTrace(); } } } ObjectInputStream ois = null; Mother reMother=null; mother.setName("不是小红");//静态变量name引用的字符串修改为"不是小红" try { FileInputStream fis = new FileInputStream("MyMom.ser"); ois = new ObjectInputStream(fis); reMother = (Mother) ois.readObject(); //将写入MyMom.ser的对象反序列化,由于返回的是Object对象,所以需要进行强转 } catch (Exception e) { e.printStackTrace(); } finally { if (ois != null) { try { ois.close(); } catch (IOException e) { e.printStackTrace(); } } } System.out.println(reMother);//将反序列化得到的对象reMother打印出来 } }
- 输出结果如下
Mother [i=25, mybaby=Baby [age=2, name=小官], name=不是小红, nickname=null]
可以发现mother对象中,Baby类型的实例变量mybaby所引用的对象也进行了序列化,String类型静态变量name未参与序列化,以及transient修饰的String实例变量nickname也被初始化为null。
2、java.lang.Cloneable标记接口
该接口标记一个类的对象是否有安全的clone方法。在java.lang.Object类中有一个clone()方法如下
protected native Object clone() throws CloneNotSupportedException;//native修饰符表示方法的实现不在此,这个方法是异地用C/C++实现的
由于Object类又是所有类的父类,所以所有的类都有一个继承自Object的clone()方法。如果一个类的对象调用了从Object继承来的clone方法,但是没有implements Cloneable接口,虚拟机将抛出CloneNotSupportedException。
1)Object类clone()的修饰符为protected产生的影响。由于Object的clone()方法是protected的,所以一个类从Object继承的clone方法默认只有java.lang包可见,以及子类可以继承,但子类也只能调用受保护的clone()方法来克隆它自己的对象(注意调用super.clone()不会克隆父类的对象,还是克隆的这个类的对象)。一个类必须重新定义clone()为public方法才能允许"所有方法"调用这个类的clone()方法来克隆这个类的实例。
2)克隆对象和原对象有什么联系?克隆出来的对象与原来的对象属性是相同的,但不是同一个对象,他们的内存地址是不一样的,所以改变克隆出来的对象的属性一般不会影响到原来的对象。但是从Object继承的clone方法是一个浅拷贝,如果原对象包含子对象的引用,拷贝对象和原对象会拥有对同一个子对象的引用,这个情况下克隆对象对这个子对象的更改会改变原对象中这个子对象的属性。所以在原对象引用的子对象不都是String这种不可变的对象的话,必须重新定义clone方法来建立一个深拷贝。
3)代码检验
- 先创建一个People类,实现Cloneable接口,只有people_id和people_name属性
package mytest2; public class People implements Cloneable { private int people_age; private String people_name; public People(int people_age, String people_name) { super(); this.people_age = people_age; this. people_name = people_name; } @Override public String toString() { return " [people_age=" +people_age + ", people_name=" + people_name + "]"; } }
- 在创建一个继承自People的Adult类,实现了Cloneable接口,有adult_age和adult_name属性,和一个Date类型的testDay属性。在main方法中进行有关测试
package mytest2; import java.util.Date; public class Adult extends People implements Cloneable { int adult_age; String adult_name; Date testDay; public Adult(int adult_age, String adult_name, int people_age, String people_name) { super(people_age, people_name); this.adult_age = adult_age; this.adult_name = adult_name; this.testDay= new Date(); } public People superClone() throws CloneNotSupportedException { return (People) super.clone();// 这里克隆得到的依然会是一个Adult类的对象 } @Override public String toString() { return "[adult_age=" + adult_age + ", adult_name=" + adult_name + ", testDay=" + testDay + super.toString()+"]"; } public static void main(String[] args) { People people = new People(26, "小官"); // people.clone();//父类的clone方法这里不可见,报错误 Adult adult = new Adult(18, "成年官",10,"小官"); Adult adultclone = null; try { adultclone = (Adult) adult.clone();// Object的clone方法返回值为Object对象,这里需要进行强转 people = (People) adult.superClone();// 这里得到的应该依然是一个Adult对象 } catch (CloneNotSupportedException e) { e.printStackTrace(); } System.out.println(adultclone); System.out.println(people); adultclone.testDay.setTime(System.currentTimeMillis()+1000000000); /* System.currentTimeMillis()得到的是从1970年1月1日0时到当前时间的毫秒数 这里改变下testDay的值来测试原对象的testday属性会不会发生改变,以及发生了怎样的改变?*/ System.out.println("--------------------"); System.out.println(adultclone); System.out.println(adult); } }
- 输出结果为
[adult_age=18, adult_name=成年官, testDay=Wed Apr 17 13:05:17 CST 2019 [people_age=10, people_name=小官]]
[adult_age=18, adult_name=成年官, testDay=Wed Apr 17 13:05:17 CST 2019 [people_age=10, people_name=小官]]
--------------------
[adult_age=18, adult_name=成年官, testDay=Mon Apr 29 02:51:57 CST 2019 [people_age=10, people_name=小官]]
[adult_age=18, adult_name=成年官, testDay=Mon Apr 29 02:51:57 CST 2019 [people_age=10, people_name=小官]]
第一行的输出结果和第二行一致,可以看出super.clone()并不是克隆父类,克隆的Adult类的对象。第三行和第四行的输出与前两行的输出相比发生了改变,可以验证改变克隆对象引用的子对象,原对象引用的子对象也发生了同样的更改。
4)建立深克隆
- 由于Date类型已经将重新覆盖了clone方法,并将clone()的修饰符改为public,所以在外部可以用Date的实例直接调用这个clone方法来克隆Date的实例。Date相关源码(JDK8)如下:
public Object clone() { Date d = null; try { d = (Date)super.clone(); if (cdate != null) { d.cdate = (BaseCalendar.Date) cdate.clone(); } } catch (CloneNotSupportedException e) {} // Won't happen return d; }
- 在Adult类中覆盖clone方法,并实现深克隆,有关代码如下
public Object clone() { Adult adultClone = null; try { adultClone = (Adult) super.clone(); adultClone.testDay = (Date) this.testDay.clone();//将克隆对象的testDay域变为原对象test域的一个拷贝,实现深克隆 } catch (CloneNotSupportedException e) {} return adultClone; }
- 覆盖clone()后,测试结果为
[adult_age=18, adult_name=成年官, testDay=Wed Apr 17 13:28:43 CST 2019 [people_age=10, people_name=小官]] [adult_age=18, adult_name=成年官, testDay=Wed Apr 17 13:28:43 CST 2019 [people_age=10, people_name=小官]] -------------------- [adult_age=18, adult_name=成年官, testDay=Mon Apr 29 03:15:23 CST 2019 [people_age=10, people_name=小官]] [adult_age=18, adult_name=成年官, testDay=Wed Apr 17 13:28:43 CST 2019 [people_age=10, people_name=小官]]//原对象引用的子对象testDay这次没有发生改变!
3、java.util.RandomAccess标记接口
若一个容器类对象具有能够快速随机访问对象内元素的特性,就可以实现RandomAccess标记接口。比如ArrayList类就实现了这个接口,因为ArrayList是一个动态数组,底层是由数组实现的,所以ArrayList对象能够根据索引对对象内包含的元素进行快速随机的访问,其时间复杂度为O(1)。
4、java.rmi.Remote标记接口
这个标记接口用于远程方法调用,即RMI(Remote Method Invocation)技术。若一个类要实现远程方法调用,需要先实现这个接口。RMI技术也是EJB技术的原理。
(小官原创,若有谬误,望各位大佬批评指正)
(转载请注明出处)