理解Java中的三个标记接口,原来如此!!
Java技术迷 | 出品
标识接口是没有任何方法和属性的接口。标识接口不对实现它的类有任何语义上的要求,它仅仅表明实现它的类属于一个特定的类型。
Java中的标记接口有很多,这里介绍其中三个比较经典的标记接口:
1.Serializable
2.Cloneable3.RandomAccess
Serializable
这个接口相信大家都不陌生,该接口用于实现序列化,实现了该接口的类就是可序列化的(序列化指的是将对象的数据写入文件),来看看Serializable接口的源代码:
public interface Serializable {
}
Serializable内部其实没有任何内容,所以它实质上是一个标记型的接口,用于表示某些类可以被序列化。下面就来简单地使用一下Serializable接口,首先创建一个普通的Java类:
@Data
@NoArgsConstructor
@AllArgsConstructor
@ToString
public class User {
private String name;
private Integer age;
}
然后编写一段序列化代码:
public static void writeObject() throws Exception {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("E:\\Desktop\\test.txt"));
User user = new User("张三", 20);
oos.writeObject(user);
oos.close();
}
执行该方法,程序报错:
Exception in thread "main" java.io.NotSerializableException: com.wwj.collection_.User
这是因为User类没有实现序列化接口,看看底层源码是如何对其进行检验的。首先查看writeObject方法:
public final void writeObject(Object obj) throws IOException {
if (enableOverride) {
writeObjectOverride(obj);
return;
}
try {
writeObject0(obj, false);
} catch (IOException ex) {
if (depth == 0) {
writeFatalException(ex);
}
throw ex;
}
}
enableOverride是类的一个成员变量,初始值为false,所以接着执行writeObject0方法:
private void writeObject0(Object obj, boolean unshared)
throws IOException{
......
// remaining cases
if (obj instanceof String) {
writeString((String) obj, unshared);
} else if (cl.isArray()) {
writeArray(obj, desc, unshared);
} else if (obj instanceof Enum) {
writeEnum((Enum<?>) obj, desc, unshared);
} else if (obj instanceof Serializable) {
writeOrdinaryObject(obj, desc, unshared);
} else {
if (extendedDebugInfo) {
throw new NotSerializableException(
cl.getName() + "\n" + debugInfoStack.toString());
} else {
throw new NotSerializableException(cl.getName());
}
}
}
方法很长,我们挑重要的看,首先判断当前的object是否为String类型,若为String,则执行writeString;其次判断是否为Array类型,接着判断是否为Enum,最后看看object是否为Serializable类型,如果该object实现了Serializable接口,那么就可以正确执行之后的方法,若是没有实现,则抛出NotSerializableException异常。明白其原理后,我们就需要让User类实现Serializable接口:
@Data
@NoArgsConstructor
@AllArgsConstructor
@ToString
public class User implements Serializable {
private String name;
private Integer age;
}
此时再次执行序列化方法即可将对象内容存入文件:
$ cat test.txt
▒▒srcom.wwj.collection_.User▒I]a▒▒4LagetLjava/lang/Integer;LnametLjava/lang/String;xpsrjava.lang.Integer⠤▒▒▒8Ivaluexrjava.lang.Number▒▒▒
▒▒▒xpt张三
也可以将文件中的内容反序列化成一个对象,代码如下:
public static void readObject() throws Exception {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("E:\\Desktop\\test.txt"));
User user = (User) ois.readObject();
System.out.println(user);
ois.close();
}
运行结果:
User(name=张三, age=20)
Cloneable
该接口与Serializable接口一样,都是标记型接口:
public interface Cloneable {
}
当一个类实现了Cloneable接口时,就可以使用该类的clone方法对其进行克隆,否则,就会抛出CloneNotSupportedException异常。克隆指的是根据已有的数据,创建一份完全一样的副本,一个类要想能够被克隆,则必须实现Cloneable接口并重写clone方法,比如:
@Data
@NoArgsConstructor
@AllArgsConstructor
@ToString
public class User implements Cloneable {
private String name;
private Integer age;
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
此时即可对User对象进行克隆:
public static void main(String[] args) throws Exception {
User user = new User("张三", 20);
User user2 = (User) user.clone();
System.out.println(user);
System.out.println(user2);
}
运行结果:
User(name=张三, age=20)
User(name=张三, age=20)
然而这样的克隆方式是有局限性的,比如我们修改一下User类:
@Data
@NoArgsConstructor
@AllArgsConstructor
public class User implements Cloneable {
private String name;
private Integer age;
private Address address;
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
@Data @NoArgsConstructor @AllArgsConstructor public class Address { private String province; private String city; }
此时我们再去克隆一下试试:
```java
public static void main(String[] args) throws Exception {
Address address = new Address("浙江", "杭州");
User user = new User("张三", 20,address);
User user2 = (User) user.clone();
System.out.println(user);
System.out.println(user2);
}
运行结果:
User(name=张三, age=20, address=Address(province=浙江, city=杭州))
User(name=张三, age=20, address=Address(province=浙江, city=杭州))
看着运行结果好像并没有产生什么问题,接下来我们尝试修改其中一个对象的属性值:
public static void main(String[] args) throws Exception {
Address address = new Address("浙江", "杭州");
User user = new User("张三", 20,address);
User user2 = (User) user.clone();
user.setName("李四");
address.setCity("金华");
System.out.println(user);
System.out.println(user2);
}
运行结果:
User(name=李四, age=20, address=Address(province=浙江, city=金华))
User(name=张三, age=20, address=Address(province=浙江, city=金华))
通过运行结果能够发现两点,首先name属性值的修改并没有影响到user2,但是Address中city属性值的修改却影响到了它,这种现象就叫做 浅拷贝
。
浅拷贝是按位拷贝对象,它会创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝。如果属性是基本类型,拷贝的就是基本类型的值;如果属性是内存地址(引用类型),拷贝的就是内存地址 ,因此如果其中一个对象改变了这个地址,就会影响到另一个对象。
通过一张图来理解一下:对于基本类型,user2会将原值拷贝一份,放入自己的对象中,而对于引用类型,它只会拷贝内存地址,也就是说,user2中的Address属性实际上只是引用了user中的内容,现在修改user中的值:由于name值是一份真的拷贝,所以user中对name的修改并不会影响到user2,但修改了user中的city,此时读取user2的city时,它读取的仍然是user中的city,所以就出现了刚才的现象。
要想解决这一问题,我们可以使用深拷贝,若想实现深拷贝,我们就需要改造刚才的程序,首先要让Address类也实现Cloneable接口:
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Address implements Cloneable{
private String province;
private String city;
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
接着需要改造一下User类的clone方法:
Data
@NoArgsConstructor
@AllArgsConstructor
public class User implements Cloneable {
private String name;
private Integer age;
private Address address;
@Override
protected Object clone() throws CloneNotSupportedException {
User user = (User) super.clone();
Address address = (Address) user.address.clone();
user.setAddress(address);
return user;
}
}
首先我们仍然通过父类的clone方法克隆出一个新的User对象,但我们知道,这个User对象仅仅是拷贝了基本数据类型,即:name和age的值,对于引用类型Address,只是拷贝了它的内存地址,若想让引用类型也能够完全拷贝值,就需要手动调用User对象中Address的clone方法,并将克隆得到的Address对象赋值给属性address;需要注意的是,如果Address类中仍然有引用类型变量,那么就需要改造Address的clone方法,并手动克隆出引用类型变量,将其赋值。现在重新运行之前的代码:
public static void main(String[] args) throws Exception {
Address address = new Address("浙江", "杭州");
User user = new User("张三", 20,address);
User user2 = (User) user.clone();
user.setName("李四");
address.setCity("金华");
System.out.println(user);
System.out.println(user2);
}
结果如下:
User(name=李四, age=20, address=Address(province=浙江, city=金华))
User(name=张三, age=20, address=Address(province=浙江, city=杭州))
其原理图如下:经过深拷贝克隆出来的对象是完全相互独立的,无论user对象如何变化,user2都不会受到影响。
RandomAccess
该接口也是一个标记接口:
public interface RandomAccess {
}
若某个类实现了RandomAccess,则表明该类支持快速的随机访问,其目的是允许通用算法更改其行为,以便在应用于随机访问列表(访问索引)或顺序访问列表(迭代器)时提供良好的性能。比如Java集合中的ArrayList就实现了该接口,而且对于实现了该接口的List,其随机访问列表的效率要高于顺序访问列表,我们可以尝试着验证一下。先来看看随机访问列表的耗时:
public static void main(String[] args) throws Exception {
List<Integer> list = new ArrayList<>();
for (int i = 0; i < 100000; i++) {
list.add(i);
}
long start = System.currentTimeMillis();
for (int i = 0; i < list.size(); i++) {
Integer num = list.get(i);
}
long end = System.currentTimeMillis();
System.out.println(end - start);
}
运行结果:
8
再看看顺序访问列表的耗时:
public static void main(String[] args) throws Exception {
List<Integer> list = new ArrayList<>();
for (int i = 0; i < 100000; i++) {
list.add(i);
}
Iterator<Integer> iterator = list.iterator();
long start = System.currentTimeMillis();
while(iterator.hasNext()){
Integer num = iterator.next();
}
long end = System.currentTimeMillis();
System.out.println(end - start);
}
运行结果:
17
而对于没有实现RandomAccess接口的LinkedList,它的效率又会是如何呢?首先测试随机访问列表:
public static void main(String[] args) throws Exception {
List<Integer> list = new LinkedList<>();
for (int i = 0; i < 100000; i++) {
list.add(i);
}
long start = System.currentTimeMillis();
for (int i = 0; i < list.size(); i++) {
Integer num = list.get(i);
}
long end = System.currentTimeMillis();
System.out.println(end - start);
}
运行结果:
11076
然而测试顺序访问列表:
public static void main(String[] args) throws Exception {
List<Integer> list = new LinkedList<>();
for (int i = 0; i < 100000; i++) {
list.add(i);
}
Iterator<Integer> iterator = list.iterator();
long start = System.currentTimeMillis();
while(iterator.hasNext()){
Integer num = iterator.next();
}
long end = System.currentTimeMillis();
System.out.println(end - start);
}
运行结果:
36
由此可以看出,LinkedeList的随机和顺序访问效率都很低,其中随机访问效率低的离谱。
本文作者:汪伟俊 为Java技术迷专栏作者 投稿,未经允许请勿转载。