文章目录
0. 前言
- 通过这篇文章你可以了解Serializable、Cloneable、RandomAccess这些标记性接口的作用。
- 你可以了解到什么是浅拷贝、什么是深拷贝。
- 什么是随机访问、什么是顺序访问
1. Serializable序列化接口
-
序列化接口没有方法或字段,仅仅用于标识可串行化的语义。
-
类的序列化由实现Serializable接口的类启动。不实现此接口的类将不会使用任何状态序列化或者反序列化。
-
序列化:将对象的数据写入到文件(写对象)
-
反序列化:将文件中对象的数据读取出来(读对象)
-
代码例子:
import java.io.*; /** * @author JIN * @description 序列化 * @createTime 2021-03-03 18:27 **/ public class Test1 { public static void main(String[] args) throws IOException, ClassNotFoundException { Person p = new Person("小明",18); // 序列化 写对象 ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("src/data1.txt")); out.writeObject(p); // 反序列化 读对象 ObjectInputStream in = new ObjectInputStream(new FileInputStream("src/data1.txt")); Person object = (Person) in.readObject(); System.out.println(object); } } // 一定要实现Serializable接口 class Person implements Serializable { private String name; private int age; public Person(String name, int age) { this.name = name; this.age = age; } @Override public String toString() { return "Person{" + "name='" + name + '\'' + ", age=" + age + '}'; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } public String getName() { return name; } public void setName(String name) { this.name = name; } }
2. Cloneable克隆标记性接口: 用于克隆
-
一个类实现Cloneable接口来指示Object.clone()方法,该方法对于该类的实例进行字段的复制是合法的
-
在不现实Cloneable接口的实例上调用对象的克隆方法会导致异常CloneNotSupportedException被抛出.
-
克隆就是依据有的数据,创造一份新的完全一样的数据拷贝。
-
克隆的前提条件:
- 被克隆对象所在的类必须实现Cloneable接口
- 必须重写clone方法
-
代码例子:
public class CloneableTest { public static void main(String[] args) { ArrayList<String> list = new ArrayList<>(); list.add("a"); list.add("b"); list.add("c"); Object clone = list.clone(); System.out.println(clone); // [a, b, c] // 说明只是复制了数据内容。 System.out.println(clone == list); // false } }
3. 传统方法拷贝
-
题目:现有一个User对象[name=“小明”,age=18],需要得到一个与之数据相同的对象User2.
-
传统代码:
/** * @author JIN * @description * @createTime 2021-03-03 18:46 **/ public class User { private String name; private int age; public User() { } public User(String name, int age) { this.name = name; this.age = age; } @Override public String toString() { return "User{" + "name='" + name + '\'' + ", age=" + age + '}'; } public String getName() { return name; } public void setName(String name) { this.name = name; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } }
public class Test2 { public static void main(String[] args) { User user1 = new User("小明",18); // 直接将user1的值赋给user2,但如果属性很多,十几个的时候,我们就觉得很繁琐 User user2 = new User(); user2.setName(user1.getName()); user2.setAge(user1.getAge()); System.out.println(user2); // 修改user1的属性,查看user2是否会改变---不会改变 user1.setAge(2000); System.out.println(user);// User{name='小明', age=2000} System.out.println(cloneUser); // User{name='小明', age=18} } }
4. 浅拷贝与深拷贝
-
克隆的前提条件:
- 被克隆对象所在的类必须实现Cloneable接口
- 必须重写clone方法
-
浅拷贝的实现方法:
// 修改User类 public class User implements Cloneable{ // 重写方法的时候,把方法的权限修饰符更改为public @Override public Object clone() throws CloneNotSupportedException { return super.clone(); } // 其他代码不变 }
public class Test2 { public static void main(String[] args) throws CloneNotSupportedException { User user1 = new User("小明",18); // 代码明显比传统方法简单很多 User cloneUser = (User) user1.clone(); System.out.println(cloneUser); //User{name='小明', age=18} // 修改user1的属性,查看user2是否会改变---不会改变 user1.setAge(2000); System.out.println(user);// User{name='小明', age=2000} System.out.println(cloneUser); // User{name='小明', age=18} } }
但是浅拷贝也有自己的局限性:基本数据类型可以达到完全复制,但是引用数据类型则不可以,对于引用数据类型来说仅仅是拷贝了一份引用。即若对引用数据类型进行修改,拷贝哪一方也会一起修改。
代码演示:
public class School { private String name; public School(String name) { this.name = name; } @Override public String toString() { return "School{" + "name='" + name + '\'' + '}'; } public String getName() { return name; } public void setName(String name) { this.name = name; } } public class User implements Cloneable{ private String name; private int age; // 添加一个引用数据类型 private School school; public User(String name, int age, School school) { this.name = name; this.age = age; this.school = school; } @Override public Object clone() throws CloneNotSupportedException { return super.clone(); } @Override public String toString() { return "User{" + "name='" + name + '\'' + ", age=" + age + ", school=" + school + '}'; } public User() { } public School getSchool() { return school; } public void setSchool(School school) { this.school = school; } public String getName() { return name; } public void setName(String name) { this.name = name; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } }
public class Test2 { public static void main(String[] args) throws CloneNotSupportedException { User user1 = new User("小明",18,new School("北京")); User cloneUser = (User) user1.clone(); System.out.println(cloneUser); System.out.println("---------------"); // 修改user1的学校,查看cloneUser是否会改变----会一起变 user1.getSchool().setName("上海"); System.out.println("user1: " + user1); System.out.println("cloneUser: " + cloneUser); } }
User{name='小明', age=18, school=School{name='北京'}} --------------- user1: User{name='小明', age=18, school=School{name='上海'}} cloneUser: User{name='小明', age=18, school=School{name='上海'}}
从上面结果,我们看到了当user1修改了学校的名字,cloneuser中学校的名字也一起改变了,显然这不是我们想要的结果,我们只是想要数据的内容的复制就好了,不需要有关系、关联。
原因:在学生对象user被克隆的时候,其属性School(引用类型)仅仅是拷贝了一份引用,因此在School的值发生改变的时候,被克隆对象cloneuser的属性也会改变。
解决方案:使用深拷贝。
-
深拷贝代码:
针对上面浅拷贝的问题,我们并不能简单的调用父类的clone,需要自己重写clone中代码。
// 对于school类,修改如下: public class School implements Cloneable{ @Override public Object clone() throws CloneNotSupportedException { return super.clone(); } // 其他不变 }
// 对于user类,修改如下: public class User implements Cloneable{ @Override public Object clone() throws CloneNotSupportedException { // 1. 先克隆出一个user对象 User cloneUser = (User) super.clone(); // 2. 再克隆出一个引用类型的对象 School cloneSchool = (School) this.school.clone(); // 3. 将引用类型的新对象赋值给克隆出来的cloneUser cloneUser.setSchool(cloneSchool); // 克隆出来的cloneSchool相当于新的一个对象,与原来的school对象并没用关联。 // 4. 返回克隆对象 return cloneUser; } // 其他不变 }
public class Test2 { public static void main(String[] args) throws CloneNotSupportedException { User user1 = new User("小明",18,new School("北京")); User cloneUser = (User) user1.clone(); System.out.println(cloneUser); System.out.println("---------------"); // 修改user1的学校,查看cloneUser是否会改变----不会一起变 user1.getSchool().setName("上海"); System.out.println("user1: " + user1); System.out.println("cloneUser: " + cloneUser); } } 重新测试结果: User{name='小明', age=18, school=School{name='北京'}} ------------------------------------------------ user1: User{name='小明', age=18, school=School{name='上海'}} cloneUser: User{name='小明', age=18, school=School{name='北京'}} // 没改变
5. RandomAccess标记接口:支持快速的随机访问
-
作用:是允许通用算法更改其行为,以便在应用于随机访问列表或者顺序访问列表时提供良好的性能。
-
此接口由list实现使用,以表明它们支持快速的随机访问。 实现了这个接口的集合是支持快速随机访问策略的。
-
通俗来说: 如果是实现了这个接口的 List,那么使用for循环的方式(随机访问)获取数据会优于用**迭代器(顺序访问)**获取数据。
-
什么是随机访问:
for(int i = 0; i < 10; i++) list.get(i);
-
什么是顺序访问:
while (iterator.hasNext()){ Object next = iterator.next(); }
-
验证代码:
public class Test3 { public static void main(String[] args) { // 实现了RandomAccess接口----for方法更加快 ArrayList<Integer> list = new ArrayList<>(); for (int i = 0; i < 100000; i++) { list.add(i); } // for方法 随机访问 t1(list); // 迭代器 顺序访问 t2(list); } private static void t1(ArrayList<Integer> list) { long start = System.currentTimeMillis(); for (int i = 0; i < list.size(); i++) { Object object = list.get(i); } long end = System.currentTimeMillis(); System.out.println("for随机访问消耗的时间: " + (end - start)); } private static void t2(ArrayList<Integer> list) { long start = System.currentTimeMillis(); Iterator<Integer> iterator = list.iterator(); while (iterator.hasNext()){ Object object = iterator.next(); } long end = System.currentTimeMillis(); System.out.println("iterator顺序访问消耗的时间: " + (end - start)); } }
运行结果: for随机访问消耗的时间: 6 iterator顺序访问消耗的时间: 7
-
我们使用LinkedList来测试一下,LinkedList并没实现RandomAccess接口。
public class Test3 { public static void main(String[] args) { // LinkedList 没有实现了RandomAccess接口----iterator方法更加快 LinkedList<Integer> list = new LinkedList<>(); for (int i = 0; i < 100000; i++) { list.add(i); } // for方法 随机访问 t1(list); // 迭代器 顺序访问 t2(list); } private static void t1(LinkedList<Integer> list) { long start = System.currentTimeMillis(); for (int i = 0; i < list.size(); i++) { Object object = list.get(i); } long end = System.currentTimeMillis(); System.out.println("for随机访问消耗的时间: " + (end - start)); } private static void t2(LinkedList<Integer> list) { long start = System.currentTimeMillis(); Iterator<Integer> iterator = list.iterator(); while (iterator.hasNext()){ Object object = iterator.next(); } long end = System.currentTimeMillis(); System.out.println("iterator顺序访问消耗的时间: " + (end - start)); } }
运行结果: for随机访问消耗的时间: 6542 iterator顺序访问消耗的时间: 7
小结:
- 如果实现了RandomAccess接口,则for方法遍历比iterator迭代器方法遍历好,如:Arraylist。
- 如果没实现,如上面例子LinkedList,则迭代器方法远优于for方法遍历。