1 equals方法
在 Object 类中,== 运算符和 equals 方法是等价的,都是比较两个对象的引用是否相等。public boolean equals(Object obj) { return (this == obj); }
。
从另一方面来讲,如果两个对象的引用相等,那么这两个对象一定是相等的。对于我们自定义的一个对象,如果不重写 equals 方法,那么在比较对象的时候就是调用Object 类的 equals方法,也就是用==运算符比较两个对象。在 Java 规范中,对equals方法的使用必须遵循以下几个规则:
- 自反性:对于任何非空引用值X,X.equals(X)都应返回true
- 对称性:对于任何非空引用值X和Y,当且仅当Y.equals(X)返回true时,X.equals(Y)也应该返回true
- 传递性:对于任何非空引用值X,Y,Z,如果X.equals(Y)返回true,并且Y.equals(Z)返回true,那么X.equals(Z)应返回true
- 一致性:对于任何非空引用值X和Y,多次调用X.equals(Y)始终返回true或始终返回false
另外,为了避免空指针异常,最好让确定非空的对象调用equals对象。
2 ==与equals()的区别
== 作用是判断两个对象的地址是不是相等。即,判断两个对象是不是同一个对象。(基本数据类型比较的是值,引用数据类型比较的是内存地址);
equals():它的作用也是判断两个对象是否相等。但它一般有两种使用情况:
- 情况1:类没有覆盖equals()方法。则通过equals()比较该类的两个对象时,等价于通过“==”比较这两个对象。
- 情况2:类覆盖了 equals()方法。覆盖equals()方法来判断两个对象的内容是否相等;若它们的内容相等,则返回true(即,认为这两个对象相等)。
对于基本类型,== 判断两个值是否相等,基本类型没有 equals() 方法;对于引用类型,== 判断两个变量是否引用同一个对象,而 equals() 判断引用的对象是否等价。
3 为什么重写 equals 方法就必须重写 hashcode 方法
当不重写 equals 方法时,判断两个对象是否相等,等同于利用 == 判断。当重写 equals 方法时,是根据内容判断两个对象是否相等。
HashSet 或 HashMap在存放元素之前,要根据hashcode方法计算得到的hash值来判断元素是否已经存在。不重写 hashcode 时,两个属性值完全相同的对象产生的hash值不同,但是当把这两个对象加入到 HashSet 或 HashMap 中则会产生异常。
就结果而言,两个内容意义上相等的不同对象加入 HashSet 中,容量应该为1,但是 HashSet 使用 hashcode 判断对象是否已存在或相等,产生了歧义,最差的结果会导致HashSet 的不正常运行。所以重写 equals 方法必须重写 hashcode 方法。
4 hashcode方法
hashcode() 是 Object 类中的函数,所有类都拥有该函数,用于返回该对象的hash值。hash值的通用约定如下:
- 在 java 程序执行过程中,在一个对象没有被改变的前提下,无论这个对象被调用多少次,hashCode 方法都会返回相同的整数值。对象的hash值没有必要在不同的程序中保持相同的值。
- 如果 2 个对象使用 equals 方法进行比较并且相同的话,那么这 2 个对象的 hashCode 方法的值也必须相等
- 根据 equals 方法,得到两个对象不相等,那么这 2 个对象的 hashCode 可以相同或不同。但是,不相等的对象的 hashCode 值不同的话可以提高哈希表的性能
在 JDK6 与 JDK7 中基于随机数作为 hashcode 的来源; JDK8 默认通过和当前线程油管的一个随机数+三个确定值,运用 Marsaglia’s xorshift scheme 随机数算法得到的一个随机数。
static inline intptr_t get_next_hash(Thread * Self, oop obj) {
intptr_t value = 0 ;
if (hashCode == 0) {
// 根据Park-Miller伪随机数生成器生成的随机数
value = os::random() ;
} else
if (hashCode == 1) {
// 此类方案将对象的内存地址,做移位运算后与一个随机数进行异或得到结果
intptr_t addrBits = cast_from_oop<intptr_t>(obj) >> 3 ;
value = addrBits ^ (addrBits >> 5) ^ GVars.stwRandom ;
} else
if (hashCode == 2) {
value = 1 ; // 返回固定的1
} else
if (hashCode == 3) {
value = ++GVars.hcSequence ; // 返回一个自增序列的当前值
} else
if (hashCode == 4) {
value = cast_from_oop<intptr_t>(obj) ; // 对象地址
} else {
// 通过和当前线程有关的一个随机数+三个确定值
unsigned t = Self->_hashStateX ;
t ^= (t << 11) ;
Self->_hashStateX = Self->_hashStateY ;
Self->_hashStateY = Self->_hashStateZ ;
Self->_hashStateZ = Self->_hashStateW ;
unsigned v = Self->_hashStateW ;
v = (v ^ (v >> 19)) ^ (t ^ (t >> 8)) ;
Self->_hashStateW = v ;
value = v ;
}
value &= markOopDesc::hash_mask;
if (value == 0) value = 0xBAD ;
assert (value != markOopDesc::no_hash, "invariant") ;
TEVENT (hashCode: GENERATE) ;
return value;
}
hashCode() 返回hash值,而 equals() 是用来判断两个对象是否等价。等价的两个对象hash值一定相同,但是hash值相同的两个对象不一定等价。
对于一些容器,尤其是 HashMap 来说,为了保持 Key 唯一性且降低比较次数,自然而然地引入了 hash 值。以 HashMap 的 put 为例:如果这个位置上没有元素,它就可以直接存储在这个位置上,不用再进行任何比较了;如果这个位置上已经有元素了,就调用它的 equals 方法与新元素进行比较,相同的话就不存了;不相同的话,也就是发生了 hash冲突的情况,那么就在这个发生hash冲突的地方产生一个链表,将所有产生相同 hash 值的对象放到这个单链表上去,串在一起(很少出现)。
5 clone方法
clone 方法是创建并且返回一个对象的复制之后的结果。复制的含义取决于对象的类定义。
clone 方法首先会判对象是否实现了 Cloneable 接口,若无则抛出 CloneNotSupportedException 异常,最后会调用internalClone 。
intervalClone 是一个 native 方法,一般来说 native 方法的执行效率高于非 native 方法。
6 浅拷贝和深拷贝的区别
在 Java 中,除了基本数据类型(元类型)之外,还存在 类的实例对象 这个引用数据类型。
而一般使用 = 号做赋值操作的时候,对于基本数据类型,实际上是拷贝的它的值,但是对于对象而言,赋值的只是这个对象的引用,将原对象的引用传递过去,他们实际上还是指向的同一个对象。
浅拷贝:对基本数据类型进行了拷贝,对引用数据类型只是进行了引用的传递,而没有真实的创建一个新的对象。
深拷贝:对基本数据类型进行了拷贝,对引用数据类型进行拷贝的时候,创建了一个新的对象,并且复制其内的成员变量。
浅拷贝和深拷贝,只是在拷贝对象的时候,对 类的实例对象 这种引用数据类型的不同操作而已。
浅拷贝是按位拷贝对象,它会创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝。如果属性是基本类型,拷贝的就是基本类型的值;如果属性是内存地址(引用类型,数组或引用),拷贝的就是内存地址。因此如果其中一个对象改变了这个地址,就会影响到另一个对象。Object 的 clone() 方法,提供的是一种浅拷贝的机制。
浅拷贝的实现方式:
- 对于数据类型是基本数据类型的成员变量,浅拷贝会直接进行值传递,也就是将该属性值复制一份给新的对象。因为是两份不同的数据,所以对其中一个对象的该成员变量值进行修改,不会影响另一个对象拷贝得到的数据。
- 对于数据类型是引用数据类型的成员变量,比如说成员变量是某个数组、某个类的对象等,那么浅拷贝会进行引用传递,也就是只是将该成员变量的引用值(内存地址)复制一份给新的对象。因为实际上两个对象的该成员变量都指向同一个实例。在这种情况下,在一个对象中修改该成员变量会影响到另一个对象的该成员变量值。
深拷贝会拷贝所有的属性,并拷贝属性指向的动态分配的内存。当对象和它所引用的对象一起拷贝时即发生深拷贝。深拷贝相比于浅拷贝速度较慢并且花销较大。如果想要实现对对象的深拷贝,在不引入第三方jar包的情况下,可以使用如下办法:
- 先对对象进行序列化,紧接着马上反序列化出。
- 重写 clone 方法。与通过重写 clone 方法实现浅拷贝的基本思路一样,只需要为对象里面的每一层的每一个对象都实现 Cloneable 接口并重写 clone 方法。最后在最顶层的类的重写的 clone 方法中调用所有的 clone 方法即可实现深拷贝。简单的说就是:每一层的每个对象都进行浅拷贝=深拷贝。
- 拷贝构造函数。拷贝构造方法指的是该类的构造方法的参数为该类的对象。 ```java package org.example;
import java.io.*;
// 深拷贝之 序列化 public class CopyTest1 { public static void main(String[] args) throws IOException, ClassNotFoundException { // 创建一个对象 School school = new School(“china”); // 对象作为参数 Student student = new Student(“zhangsan”, 1, school); // 序列化对象 SerializeStudent(student); // 反序列化对象 Student deserializeStudent = DeserializeStudent(); // 修改序列化后的对象 deserializeStudent.setAge(999); // 输出结果 System.out.println(student.getAge()); System.out.println(deserializeStudent.getAge()); }
// 序列化
private static void SerializeStudent(Student student) throws IOException {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream
(new File("G:/javaCode/demo_code/demo/src/org/example/student.txt")));
oos.writeObject(student);
System.out.println("student 序列化成功");
oos.close();
}
// 反序列化
private static Student DeserializeStudent() throws IOException, ClassNotFoundException {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(new File
("G:/javaCode/demo_code/demo/src/org/example/student.txt")));
Student student = (Student) ois.readObject();
System.out.println("student 反序列化成功");
ois.close();
return student;
}
private static class Student implements Serializable {
private String name;
private int age;
private School school;
// 省略了 get set constructor
}
private static class School implements Serializable {
private String address;
// 省略了 get set constructor
}
}
```java
package org.example;
// 深拷贝之 重写clone方法
public class CopyTest2 {
public static void main(String[] args) throws CloneNotSupportedException {
// 创建一个对象
School school = new School("china");
// 对象作为参数
Student student = new Student("zhangsan", 1, school);
// 调用clone方法进行深拷贝
Student copyStudent = student.clone();
// 修改源对象属性值
student.setAge(999);
// 展示结果
System.out.println(student.getAge());
System.out.println(copyStudent.getAge());
}
// 实现Cloneable接口
private static class Student implements Cloneable {
private String name;
private int age;
private School school;
// 省略了 get set constructor
// 重写clone方法
// super.clone()其实是浅拷贝,所以对引用数据类型,需要再次调用其clone方法
@Override
protected Student clone() throws CloneNotSupportedException {
// super.clone()其实是浅拷贝
Student student = (Student) super.clone();
// 其中的school是引用类型,需要再次调用school的clone方法。并且school的clone也必须被重写
student.setSchool(this.school.clone());
return student;
}
}
// 实现Cloneable接口
private static class School implements Cloneable {
private String address;
// 省略了 get set constructor
// 重写clone方法
@Override
protected School clone() throws CloneNotSupportedException {
return (School) super.clone();
}
}
}
package org.example;
// 深拷贝之 拷贝构造函数
public class CopyTest3 {
public static void main(String[] args) {
// 创建一个对象
School school = new School("china");
// 对象作为参数
Student student = new Student("zhangsan", 1, school);
// 调用构造函数进行深拷贝
Student copyStudent = new Student(student.getName(), student.getAge(), new School(school.getAddress()));
// 修改源对象的值
student.setAge(999);
// 输出结果
System.out.println(student.getAge());
System.out.println(copyStudent.getAge());
// 浅拷贝
Student student1 = student;
student1.setName("lisi");
System.out.println(student.getName());
System.out.println(student1.getName());
}
private static class Student {
private String name;
private int age;
private School school;
// 省略了 get set constructor
}
private static class School {
private String address;
// 省略了 get set constructor
}
}
总结来说:
1、浅拷贝:对基本数据类型进行值传递,对引用数据类型进行引用传递般的拷贝,此为浅拷贝。
2、深拷贝:对基本数据类型进行值传递,对引用数据类型,创建一个新的对象,并复制其内容,此为深拷贝。
浅拷贝和深拷贝只是相对的,如果一个对象内部只有基本数据类型,那用 clone() 方法获取到的就是这个对象的深拷贝,而如果其内部还有引用数据类型,那用 clone() 方法就是一次浅拷贝的操作。
参考:深拷贝和浅拷贝的区别
7 数组的复制方法
- for循环。
- System.arraycopy() 和 Arrays.copyOf()。
- Arrays.copyOf 本质上是调用 System.arraycopy 。之所以时间差距比较大,是因为很大一部分开销全花在了 Math.min 函数上了。所以,相比之下,System.arraycopy 效率要高一些。
- 对于基本数据类型来说 System.arraycopy() 方法是深拷贝;对于引用数据类型来说 System.arraycopy() 方法是浅拷贝。
- System.arraycopy 线程不安全。
- 从参数上可以看出,System.arraycopy需要传入源数组、源数组的起始位置、目标数组,目标数组的起始位置、复制长度;而copyOf方法只需要传入源数组和复制长度,其内部会自行创建并返回一个数组。
- clone() 。
- 如果数组存储的类型是对象,它是浅拷贝,复制的是引用(地址值)。
- 如果存储的是基本数据,一维数组是深拷贝,二维及以上数组是浅拷贝。
public static byte[] copyOf(byte[] original, int newLength){
byte[] copy = new byte[newLength];
System.arraycopy(original, 0, copy, 0, Math.min(original.length, newLength));
return copy;
}
public static void arraycopy(
Object src, //源数组
int srcPos, //源数组的起始位置
Object dest, //目标数组
int destPos, //目标数组的起始位置
int length //复制长度
)
package org.example;
// for循环复制数组
public class ArrayCopyTest1 {
public static void main(String[] args) {
int a[] = {1, 2, 3};
int b[] = new int[a.length];
for (int i = 0; i < a.length; i++) {
b[i] = a[i];
}
for (int i = 0; i < b.length; i++) {
System.out.println(a[i]);
System.out.println(b[i]);
System.out.println("-----------");
}
}
}
package org.example;
import java.util.Arrays;
// System.arraycopy、Arrays.copyOf 复制数组
public class ArrayCopyTest2 {
public static void main(String[] args) {
int a[] = {1, 2, 3};
int b[] = new int[a.length];
// System.arraycopy 复制基本类型数组
Long begin1 = System.currentTimeMillis();
System.arraycopy(a, 0, b, 0, a.length);
Long end1 = System.currentTimeMillis();
// Arrays.copyOf 复制基本类型数组
Long begin2 = System.currentTimeMillis();
int[] c = Arrays.copyOf(a, a.length);
Long end2 = System.currentTimeMillis();
// 这里数据较少,看不出时间差距
System.out.println(begin1 - end1);
System.out.println(begin2 - end2);
System.out.println("==============");
for (int i = 0; i < b.length; i++) {
System.out.println(a[i]);
System.out.println(b[i]);
System.out.println(c[i]);
System.out.println("-----------");
}
// 基本数据类型,System.arraycopy是深拷贝
b[1] = 999;
System.out.println(a[1]);
System.out.println(b[1]);
System.out.println(c[1]);
System.out.println("===========");
Student s1 = new Student("zhangsan");
Student s2 = new Student("lisi");
Student s3 = new Student("wangwu");
Student s[] = {s1, s2, s3};
Student copyS[] = new Student[s.length];
// System.arraycopy 复制引用类型数组
System.arraycopy(s, 0, copyS, 0, s.length);
// Arrays.copyOf 复制引用类型数组
Student[] copyS2 = Arrays.copyOf(s, s.length);
// 没有重写toString方法,默认打印的是地址值,发现三个都是相同的地址,说明是浅拷贝
for (int i = 0; i < s.length; i++) {
// 引用类型,System.arraycopy是浅拷贝
System.out.println(s[i]);
System.out.println(copyS[i]);
System.out.println(copyS2[i]);
System.out.println("-----------");
}
}
private static class Student {
private String name;
// 省略了 get set constructor
}
}
package org.example;
// clone 复制数组
public class ArrayCopyTest3 {
public static void main(String[] args) {
int a[] = {1, 2, 3};
int[] copyA = a.clone();
// clone 复制基本类型数组,深拷贝
for (int i = 0; i < a.length; i++) {
System.out.println(a[i]);
System.out.println(copyA[i]);
System.out.println("-----------");
}
a[1] = 999;
System.out.println(a[1]);
System.out.println(copyA[1]);
System.out.println("=========");
Student s1 = new Student("zhangsan");
Student s2 = new Student("lisi");
Student s[] = {s1, s2};
Student[] copyS = s.clone();
// clone 复制引用类型数组是浅拷贝,复制的是引用
for (int i = 0; i< s.length; i++) {
// 打印的地址值相同,说明复制的是引用
System.out.println(s[i]);
System.out.println(copyS[i]);
System.out.println("------------");
}
// 修改源数组内对象的属性,复制数组也发生了变化
s[1].setName("wangwu");
System.out.println(s[1].getName());
System.out.println(copyS[1].getName());
}
private static class Student {
private String name;
// 省略了 get set constructor
}
}