11、Java中的深克隆和浅克隆
✅ 我们先来思考一下为什么需要克隆?
答案是:
克隆的对象可能包含一些已经修改过的属性,保留着你想克隆对象的值,而new出来的对象的属性全是一个新的对象,对应的属性没有值,所以我们还要重新给这个对象赋值。即当需要一个新的对象来保存当前对象的“状态”就靠clone方法了。那么我把这个对象的临时属性一个一个的赋值给我新new的对象不也行嘛?可以是可以,但是一来麻烦不说,二来,大家通过上面的源码都发现了clone是一个native方法,就是快啊,在底层实现的。
接下来所叙述的深克隆和浅克隆是建立在对象中是否存在嵌套其他引用对象而言的,一定要注意我说的这句话。
✅ 相同点
无论是深克隆还是浅克隆它们都有一个共同的特点,它们所复制后的对象的变量值都具有与原对象的变量值相同。
✅ 不同点
不同点我们先从浅克隆导致的问题说起吧。
在Java中,我们的一个普通类可以通过实现 Cloneable 接口来克隆对象。具体步骤如下:
- 普通类实现 Cloneable 接口
- 覆盖 Object 类中的 Clone 方法,访问修饰符设为public,默认是protected。
- 在覆盖的 Clone 方法中调用 super.clone() 方法。(后边会谈及深克隆时此处略微有变化)
我们先看看下面一段代码:
package cn.wb.day2;
public class Student implements Cloneable {
private int age;
private String name;
public Student(int age, String name) {
this.age = age;
this.name = name;
}
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;
}
@Override
public String toString() {
return "Student [age=" + age + ", name=" + name + "]";
}
@Override
public Object clone() throws CloneNotSupportedException {
// TODO Auto-generated method stub
return super.clone();
}
/**
* @param args
* @throws CloneNotSupportedException
*/
public static void main(String[] args) throws CloneNotSupportedException {
Student student1 = new Student(20, "张三");
Student student2 = (Student) student1.clone();
// 在这里我们修改student2的age值 但是没有影响 student1的值
student2.setAge(22);
System.out.println("student1:" + student1.getName() + "-->"+ student1.getAge());
System.out.println("student2:" + student2.getName() + "-->"+ student2.getAge());
}
}
运行结果:
student1:张三-->20
student2:张三-->22
✅ 由于Student对象中没有嵌套深层对象,所以克隆时复制对象引用中所指向的对象,所以克隆对象变化时原对象没有发生变化。因为它们的内存地址映射是不一样的。
我们此时注释掉 tostring 方法,打印两个对象看看他们的内存地址映射是否一致?
student1:com.cxsw.clone.Student@28d93b30
student2:com.cxsw.clone.Student@1b6d3586
事实证明当没有嵌套深层对象的时候,克隆后的对象和原对象他们的内存地址映射是不一样的,所以当没有嵌套深层次对象的时候,调用 clone 方法得到的对象也是一种深克隆。
当我们在一个对象中的属性中嵌套另一个对象的时候会有怎么样的变化,我们再来看看另一段代码:
package cn.wb.day2;
class Teacher implements Cloneable {
private String name;
private Student student;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Student getStudent() {
return student;
}
public void setStudent(Student student) {
this.student = student;
}
@Override
public String toString() {
return "Teacher [name=" + name + ", student=" + student + "]";
}
@Override
public Object clone() throws CloneNotSupportedException {
// TODO Auto-generated method stub
return super.clone();
}
public static void main(String[] args) throws CloneNotSupportedException {
Student s1 = new Student();
s1.setAge(20);
s1.setName("张三");
Teacher teacher1 = new Teacher();
teacher1.setName("小赵老师");
teacher1.setStudent(s1);
//为什么会出现以下结果, Teacher中的clone方法
Teacher teacher2 = (Teacher)teacher1.clone();
Student s2 = teacher2.getStudent();
s2.setName("李四");
s2.setAge(30);
System.out.println("teacher1:"+teacher1);
System.out.println("teacher2:"+teacher2);
}
}
运行结果如下:
teacher1:Teacher [name=小赵老师, student=Student [age=30, name=李四]]
teacher2:Teacher [name=小赵老师, student=Student [age=30, name=李四]
✅ 由于我们克隆是没有考虑到嵌套的深层对象student,所以导致出现了这种结果。
此时我们还是将Student对象中的 tostring 方法注释掉,看一下嵌套对象的内存地址映射:
**teacher1:Teacher [name=小赵老师, student=com.cxsw.clone.Student@28d93b30]
teacher2:Teacher [name=小赵老师, student=com.cxsw.clone.Student@28d93b30]**
我们清楚的看到嵌套的深层对象的内存地址映射是一样的,所以此时无论你改变 teacher1 还是teacher2 他都会引起两个teacher对象的统一变化,因为它们的引用(内存地址映射)是相同的。这就是浅克隆。
接下来,我们就讲一下如何解决这个问题,那当然需要深克隆了。
🛠 深克隆
老规矩,先看代码:
package cn.wb.day2;
class Teacher implements Cloneable {
private String name;
private Student student;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Student getStudent() {
return student;
}
public void setStudent(Student student) {
this.student = student;
}
@Override
public String toString() {
return "Teacher [name=" + name + ", student=" + student + "]";
}
@Override
public Object clone() throws CloneNotSupportedException {
// TODO Auto-generated method stub
//注意以下代码 实现深克隆的关键
Teacher teacher = (Teacher)super.clone();
teacher.setStudent((Student)teacher.getStudent().clone());
return teacher;
}
public static void main(String[] args) throws CloneNotSupportedException {
Student s1 = new Student();
s1.setAge(20);
s1.setName("张三");
Teacher teacher1 = new Teacher();
teacher1.setName("小杨老师");
teacher1.setStudent(s1);
Teacher teacher2 = (Teacher)teacher1.clone();
teacher2.setName("小魏老师");
Student s2 = teacher2.getStudent();
s2.setName("李四");
s2.setAge(30);
System.out.println("teacher1:"+teacher1);
System.out.println("teacher2:"+teacher2);
}
}
上边的代码的关键是在clone方法中对student对象调用了一次 clone 方法,使要克隆的类和类中所有非基本数据类型的属性对应起来。
运行结果如下
teacher1:Teacher [name=小杨老师, student=Student [age=20, name=张三]]
teacher2:Teacher [name=小魏老师, student=Student [age=30, name=李四]]
此时我们将Student对象中的 tostring 方法注释掉,看一下我们改变了clone方法的实现后嵌套对象的内存地址映射:
teacher1:Teacher [name=小杨老师, student=com.cxsw.clone.Student@28d93b30]
teacher2:Teacher [name=小魏老师, student=com.cxsw.clone.Student@1b6d3586]
我们清楚的看到嵌套的深层对象的内存地址映射是不一样的,所以此时无论你改变 teacher1 还是teacher2 他不会引起两个teacher对象的统一变化,因为它们的引用(内存地址映射)是不相同的。这就是深克隆。
✅ 总的来说,它们的不同点可以用以下两句话概括:
- 浅克隆是指拷贝对象时仅仅拷贝对象本身(包括对象中的基本变量),而不拷贝对象包含的引用指向的对象。
-
深克隆不仅拷贝对象本身,而且拷贝对象包含的引用指向的所有对象
12、Java中的final有哪些用法
如果问到这个问题,我们通常回答出以下五种就可以了:
- 被final修饰的类不能被其他类继承。
- 被final修饰的方法不能被重写。
- 被final修饰的变量不可改变,如果修饰的是引用,表示引用不可变,引用指向的内容可变。
- 被final修饰的变量,JVM会尝试将其内联,以提高运行效率。
- 被 final修饰的常量,在编译阶段会存入常量池中。
❓ 我这里再补充一个问题,被final修饰的变量一定是常量吗?
答案是否定的。这个后边我会陆续讲到静态常量池的一些相关问题。
13、static 都有哪些用法?
- static 修饰的方法被称为静态方法,可以不再初始化该类的情况下直接调用该方法。
- static 修饰的变量被称为静态变量,又称类变量,他是存储在静态区中的,而非静态变量是存储在堆内存中。此外,静态变量实在类加载时加载的,因此他可以直接通过类名调用。
- static可以修饰内部类,被称为静态内部类。
- static 也可以用作静态代码块,用于初始化操作,
- jdk 1.5 之后,static 可以用来指定导入某个类中的静态资源,并且不需要使用类名,可以直接使用资源名,比如:
import static java.lang.Math.*;
public class Test{
public static void main(String[] args){
//System.out.println(Math.sin(20));传统做法
System.out.println(sin(20));
}
}
14、有没有可能两个不相等的对象有相同的hashcode?
答案是有可能的,两个不相等的对象就会有相同的 hashcode 值,当hash冲突产生时,一般
有以下几种方式来处理:
- 拉链法:每个哈希表节点都有一个next指针,多个哈希表节点可以用next指针构成一个单向链 表,被分配到同一个索引上的多个节点可以用这个单向链表进行存储.
- 开放定址法:一旦发生了冲突,就去寻找下一个空的散列地址,只要散列表足够大,空的散列地址总 能找到,并将记录存入
- 再哈希:又叫双哈希法,有多个不同的Hash函数.当发生冲突时,使用第二个,第三个....等哈希函数计算地址,直到无冲突.
15、Java创建对象的方式有哪几种?
- new创建新对象
- 通过反射机制
- 采用clone机制
- 通过序列化机制
📖 这里我在补充一下通过反射机制如何创建一个对象:方法有很多种,这里只举例其中一种:
package com.cxsw.clone;
import java.util.Objects;
public class Test {
public static void main(String[] args) {
// 获取类名
try {
Class<?> className = Class.forName("com.cxsw.clone.Person");
Person person = (Person)className.newInstance();
person.setAge(18);
person.setName("test");
System.out.println(person);;
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}
class Person{
private String name;
private int age;
public String getName() {
return name;
}
public int getAge() {
return age;
}
public void setName(String name) {
this.name = name;
}
public void setAge(int age) {
this.age = age;
}
@Override
public boolean equals(Object o) {
if (this == o) {return true;}
if (o == null || getClass() != o.getClass()) {return false;}
Person person = (Person) o;
return age == person.age && Objects.equals(name, person.name);
}
@Override
public int hashCode() {
return Objects.hash(name, age);
}
@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
✅ 打印结果
Person{name='test', age=18}
16、泛型常用特点
以集合来举例,使用泛型的好处是我们不必因为添加元素类型的不同而定义不同类型的集合,如整 型集合类,浮点型集合类,字符串集合类,我们可以定义一个集合来存放整型、浮点型,字符串型 数据,而这并不是最重要的,因为我们只要把底层存储设置了Object即可,添加的数据全部都可向 上转型为Object。 更重要的是我们可以通过规则按照自己的想法控制存储的数据类型。
17、Java的四种引用,强弱软虚
✅ 强引用:强引用是平常中使用最多的引用,强引用在程序内存不足(OOM)的时候也不会被回收,使用方式:
String str = new String("str");
System.out.println(str);
✅ 软引用:软引用在程序内存不足时,会被回收,使用方式:
// 注意:wrf这个引用也是强引用,它是指向SoftReference这个对象的,
// 这里的软引用指的是指向new String("str")的引用,也就是SoftReference类中T
SoftReference<String> wrf = new SoftReference<String>(new String("str"));
可用场景: 创建缓存的时候,创建的对象放进缓存中,当内存不足时,JVM就会回收早先创建的对象。
✅ 弱引用:弱引用就是只要JVM垃圾回收器发现了它,就会将之回收,使用方式:
WeakReference<String> wrf = new WeakReference<String>(str);
可用场景: Java源码中的 java.util.WeakHashMap 中的 key 就是使用弱引用,我的理解就是,一旦我不需要某个引用,JVM会自动帮我处理它,这样我就不需要做其它操作。
✅ 虚引用:回收机制跟弱引用差不多,但是它被回收之前,会被放入ReferenceQueue 中。
其它引用是被JVM回收后才被传入 ReferenceQueue 中的。由于这个机制,所以虚引用大多被用于引用销毁前的处理工作。还有就是,虚引用创建的时候,必须带有 ReferenceQueue ,使用例子:
PhantomReference<String> prf = new PhantomReference<String>(new String("str"),new ReferenceQueue<>());
可用场景: 对象销毁前的一些操作,比如说资源释放等。 Object.finalize() 虽然也可以做这 类动作,但是这个方式即不安全又低效。
上诉所说的几类引用,都是指对象本身的引用,而不是指Reference的四个子类的引用(SoftReference等)。
18、Collection包结构,与Collections的区别
Collection是集合类的上级接口,子接口有 Set、List、LinkedList、ArrayList、Vector、Stack、 Set;
Collections是集合类的一个帮助类, 它包含有各种有关集合操作的静态多态方法,用于实现对各种集合的搜索、排序、线程安全化等操作。此类不能实例化,就像一个工具类,服务于Java的 Collection框架。
19、HashMap 和 HashTable 有什么不同?
- 两者父类不同
HashMap是继承自AbstractMap类,而Hashtable是继承自Dictionary类。不过它们都实现了同时实现了map、Cloneable(可复制)、Serializable(可序列化)这三个接口。
- 对外提供的接口不同
Hashtable 比 HashMap 多提供了 elments() 和 contains() 两个方法。 elments() 方法继承自
Hashtable 的父类 Dictionnary。elements() 方法用于返回此 Hashtable 中的value的枚举。contains()方法判断该 Hashtable 是否包含传入的 value 。它的作用与 containsValue() 一致。事实上,contansValue() 就只是调用了一下 contains() 方法。
- 对null的支持不同
Hashtable:key和value都不能为null。HashMap:key可以为null,但是这样的key只能有一个,因为必须保证key的唯一性;可以有多个
key值对应的value为null。
- 安全性不同
HashMap是线程不安全的,在多线程并发的环境下,可能会产生死锁等问题,因此需要开发人员自己处理多线程的安全问题。Hashtable是线程安全的,它的每个方法上都有synchronized 关键字,因此可直接用于多线程中。虽然HashMap是线程不安全的,但是它的效率远远高于Hashtable,这样设计是合理的,因为大部分的使用场景都是单线程。当需要多线程操作的时候可以使用线程安全的ConcurrentHashMap。ConcurrentHashMap虽然也是线程安全的,但是它的效率比Hashtable要高好多倍。因为ConcurrentHashMap使用了分段锁,并不对整个数据进行锁定。
- 初始容量大小和每次扩充容量大小不同
- 计算hash值的方法不同
20、ArrayList 和 LinkedList 的区别?
List—是一个有序的集合,可以包含重复的元素,提供了按索引访问的方式,它继承Collection。
List有两个重要的实现类:ArrayList和LinkedList。ArrayList: 可以看作是能够自动增长容量的数组。
ArrayList的toArray方法返回一个数组
ArrayList的asList方法返回一个列表
ArrayList:查找元素的效率比LinkedList高,因为它的底层实现是通过数组来实现的,它使用索引在数组中搜索和读取数据是很快的。
LinkedList:它的底层实现逻辑是链表,因此它插入或删除元素时效率特别高,由于ArrayList。当然,它们的效率高低是针对插入大量元素时而言的。
😁 今天的分享就到此结束了 ✌️