一.基础知识:
-
如果一个变量指向的数据是对象类型,这时候涉及两块内存,对象本身占用一块内存(堆内存),变量也占用一块内存。例如:Object o = new Object(); 变量o占一个内存,new Object()是占另一个内存。通俗来讲:new Object()创造了一个对象,类型是Object,而Object o则创造了一个可以存储Object类型对象引用的“引用存储器”,而等号=则把new Object()这个对象的引用存到了o之中,这样就可以用o来使用这个对象数据和方法了。
-
举一个案例(图示):
//构造方法创建 ---new
String s1 = new String("aaa");//这里把"aaa"代表的对象的引用存到了s1中
s1= new String("bbb");//这里又把另一个对象"bbb"存到了s1中,所以s1就不是指向原来的"aaa"了,而是指向"bbb"了,新的空间, aaa等着回收
二.equals和==的区别
- 测试的代码有两个类,Test类和User类,可以自己粘贴测试一下
- Test类:
import java.util.HashSet;
public class Test {
public static void main(String[] args) {
test_eq();
// test_hashcode();
}
public static void test_eq() {
int a = 1;
int b = 1;
System.out.println(a == b); //true 比较的是值
User u1 = new User("tom",12,1001);
User u2 = new User("tom",12,1001);
System.out.println(u1+","+u2); //User@1b6d3586,User@4554617c
//这里==比较的是变量(栈)内存中存放的对象的(堆)内存地址,用来判断两个对象的地址是否相同,即是否是指相同一个对象
System.out.println(u1 == u2); //false 比较的是地址,不是值
System.out.println(u1.equals(u2));
//String类中对equals方法进行了重写,字符串与字符串比较的是值
String s1 = "jack"; //字符常量,放在常量池
String s2 = "jack"; //创建流程:先判断常量池是否存在jack,如果有,就直接指向,如果没有则再创建一个对象
System.out.println(s1.equals(s2)); //true 字符串 实际调用的是String类中重写的方法,比较的是值
}
private static void test_hashcode() {
User u1 = new User("tom",12,1001);
User u2 = new User("tom",12,1001);
HashSet<Object> set = new HashSet<>();
set.add(u1);
set.add(u2);
System.out.println(u1.hashCode()); //110571150
System.out.println(u2.hashCode()); //110571150
System.out.println(u1.hashCode()==u2.hashCode()); //true
System.out.println(Integer.toHexString(u1.hashCode())); //6972e8e
System.out.println(u1); // User@6972e8e 包名+类名 + @ + 16进制的hashCode值
System.out.println(u1.equals(u2));
}
}
- User类:
import java.util.Objects;
public class User {
private String name;
private int age;
private int id;
public User(String name, int age, int id) {
this.name = name;
this.age = age;
this.id = id;
}
public User() {
}
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 int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
//按快捷键自动生成equals和hashcode方法
// @Override
// public boolean equals(Object o) {
// if (this == o) return true;
// if (o == null || getClass() != o.getClass()) return false;
// User user = (User) o;
// return age == user.age &&
// id == user.id &&
// Objects.equals(name, user.name);
// }
//
// @Override
// public int hashCode() {
// return Objects.hash(name, age, id);
// }
//这个equals()方法主要测试equals和==的
@Override
public boolean equals(Object obj) {
//instanceof运算符是用来在运行时指出对象是否是特定类的一个实例,instanceof通过返回一个布尔值指出true或者false
//在这里是Test类创建的User类的u1和u2是否是User的实例,所以要判断,通俗来讲:你不能在大街上随便拉一个人做手术,必须要有医师执照
if(obj instanceof User) {
//属于引用类型的强制转换,将父类类型转为子类,向下转后,可以访问子类特有的属性和方法
User u = (User)obj;
//这里equals和==是一样的,都是比较的是值
if(this.name.equals(u.getName()) && this.age == u.getAge() && this.id==u.getId()) {
return true;
}
}
return false;
}
}
- ==判断的是是否引用同一个对象(判断基本类型比较的是值,判断引用类型比较的是地址)
int a = 1;
int b = 1;
System.out.println(a == b); //true 比较的是值
- 基本类型有8个(byte,short,int,long,float,double,boolean,char),基本形式:数据类型 变量名 = 值,输出时显示的是具体的值
User u1 = new User("tom",12,1001);
User u2 = new User("tom",12,1001);
System.out.println(u1+","+u2); //User@1b6d3586,User@4554617c
System.out.println(u1 == u2); //false 比较的是地址,不是值
- 这里==比较的是变量(栈)内存中存放的对象的(堆)内存地址,用来判断两个对象的地址是否相同,即是否是指相同一个对象
- 引用类型:除了基本类型以外都是引用类型,如String 数组 类 接口 枚举 基本形式:数据类型 名称 = new 数据类型(),输出的显示的是内存地址
User u1 = new User("tom",12,1001);
User u2 = new User("tom",12,1001);
System.out.println(u1+","+u2); //User@1b6d3586,User@4554617c
System.out.println(u1 == u2); //false 比较的是地址,不是值
System.out.println(u1.equals(u2));//false
- 上述代码增加了一行equals的比较
1. 没有在User类重写equals()方法
- 如果equals没有被重写,默认和“==”一样。也就是User类中没有重写equals方法,只写了用户类的属性,有参和无参构造,还有get和set方法,其余没有。
- 这时候运行出来的结果是false,在User类中没有重写equals方法之前,比较的是地址。
2.在User类重写equals()方法
- User类:
// @Override
// public boolean equals(Object o) {
// if (this == o) return true;
// if (o == null || getClass() != o.getClass()) return false;
// User user = (User) o;
// return age == user.age &&
// id == user.id &&
// Objects.equals(name, user.name);
// }
//
// @Override
// public int hashCode() {
// return Objects.hash(name, age, id);
// }
@Override
public boolean equals(Object obj) {
//instanceof运算符是用来在运行时指出对象是否是特定类的一个实例,instanceof通过返回一个布尔值指出true或者false
//在这里是Test类创建的User类的u1和u2是否是User的实例,所以要判断,通俗来讲:你不能在大街上随便拉一个人做手术,必须要有医师执照
if(obj instanceof User) {
//属于引用类型的强制转换,将父类类型转为子类,向下转后,可以访问子类特有的属性和方法
User u = (User)obj;
//这里equals和==是一样的,都是比较的是值
if(this.name.equals(u.getName()) && this.age == u.getAge() && this.id==u.getId()) {
return true;
}
}
return false;
}
- Test类:
User u1 = new User("tom",12,1001);
User u2 = new User("tom",12,1001);
System.out.println(u1+","+u2); //User@1b6d3586,User@4554617c
System.out.println(u1 == u2); //false 比较的是地址,不是值
System.out.println(u1.equals(u2));//true
- 上述代码是User类中重写equals方法后(在重写equals方法时,系统会自动将equals和hashCode两个方法都重写了),这时如果想用系统自动生成的equals方法进行测试,可以先将hashCode方法给注释掉。这里我用的是自己写的equals方法进行的测试,再运行程序时,equals比较出来的是true。
提醒:equals方法的重写,用系统的或者用自己写的equals方法,性质都是一样的,只能使用其中一个equals()方法。
- 源码equals()方法:
public boolean equals(Object anObject) {
if (this == anObject) {
return true;
}
if (anObject instanceof String) {
String anotherString = (String)anObject;
int n = value.length;
if (n == anotherString.value.length) {
char v1[] = value;
char v2[] = anotherString.value;
int i = 0;
while (n-- != 0) {
if (v1[i] != v2[i])
return false;
i++;
}
return true;
}
}
return false;
}
- 因为这时候是按照重写后的规则进行比较,而源码equals方法返回的是“==”的值,比较的是值或者内容是否相等。如果将u1中的name值改变,equals返回的就是false,因为u1和u2实例内容不一样呢。
String s1 = "jack"; //字符常量,放在常量池
String s2 = "jack"; //创建流程:先判断常量池是否存在jack,如果有,就直接指向,如果没有则再创建一个对象
System.out.println(s1.equals(s2)); //true 字符串 实际调用的是String类中重写的方法,比较的是值
- 看一个知识:因为String类经常会被用到,所以String类中对equals方法就已经进行了重写,字符串与字符串比较的是值
- String常量:使用双引号直接创建的字符串,称为字符常量,字符常量放在内存中的常量池,在我的下一篇博客中已经写了关于常量池的介绍。
- 代码常量池示意图
总结:
- == 判断的是是否引用同一个对象( 判断基本类型比较的是值,判断引用类型比较的是地址 )
- 如果equals 没有被重写,默认和 == 一样
String类中对equals 方法进行了重写,字符串与字符串比较的是值- 如果重写了equals 方法,则按照重写后的规则进行比较
- 自定义类可以重写equals方法,实现对特定属性的比较
三.equals和hashcode的使用:
- 为什么要使用hashCode():
java.lang.Object 类 ,所有类的根,所有类都直接或者间接的继承了Object类。其中hashcode()和equals()方法,经常会用到。
集合Set中的元素是无序且不可重复,那判断两个元素是否重复的依据是什么呢?
有小伙伴说:比较对象是否相等当然用Object类中的equals()方法啊。但是,如果Set集合中存在大量对象,后添加到集合中Set的对象元素比较次数会逐渐增多,大大的降低了程序运行效率。Java中采用哈希算法(也叫散列算法)来解决这个问题,将对象(或数据)依特定算法直接映射到一个地址上,对象的存取效率大大提高。
User u1 = new User("tom",12,1001);
User u2 = new User("tom",12,1001);
HashSet<Object> set = new HashSet<>();
set.add(u1);
set.add(u2);
System.out.println(u1.hashCode()); //110571150
System.out.println(u2.hashCode()); //110571150
System.out.println(u1.hashCode()==u2.hashCode()); //true
System.out.println(Integer.toHexString(u1.hashCode())); //6972e8e
System.out.println(u1); // User@6972e8e 包名+类名 + @ + 16进制的hashCode值
System.out.println(u1.equals(u2));
1.equals()方法注释掉,使用hashCode()方法
// @Override
// public boolean equals(Object o) {
// if (this == o) return true;
// if (o == null || getClass() != o.getClass()) return false;
// User user = (User) o;
// return age == user.age &&
// id == user.id &&
// Objects.equals(name, user.name);
// }
//
@Override
public int hashCode() {
return Objects.hash(name, age, id);
}
- 以上这个示例,我们只是重写了hashCode方法,u1和u2通过equals()方法比较返回的是false
- 从上面的结果可以看出:虽然两个对象的hashCode相等,但是实际上两个对象并不是相等;,我们没有重写equals方法,那么就会调用object默认的equals方法,是比较两个对象的引用是不是相同,显示这是两个不同的对象(堆内存),两个对象的引用肯定是不定的。这里我们将生成的对象放到了HashSet中,而HashSet中只能够存放唯一的对象,也就是相同的(适用于equals方法)的对象只会存放一个,但是这里实际上是两个对象a,b都被放到了HashSet中,这样HashSet就失去了他本身的意义了。
2.User类同时使用equals()方法和hashCode()方法
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
User user = (User) o;
return age == user.age &&
id == user.id &&
Objects.equals(name, user.name);
}
@Override
public int hashCode() {
return Objects.hash(name, age, id);
}
-
以上这个示例,我们同时重写了equals和hashCode方法,这时u1和u2通过equals()方法比较返回的是true
-
从上面的结果可以看出:把User类中的equals方法写上,比较的就是两个对象的内容是否相等(也就是比较的是值,不是地址)
-
那集合(Set)如何判重呢?
- 添加元素时,调用该对象的hashCode方法,获取一个哈希值
- 根据哈希值,使用哈希算法(有好几种算法,一般用除留取余法)通过余数确定一个哈希表中的位置
- 如果该位置没有元素,说明此对象是第一次存储到集合Set中,则直接将此对象存储在此位置中
- 如果该位置有元素了,则继续调用两个对象的equals()方法进行比较
- true:则认为两个对象为同一个对象,无法添加,舍弃当前对象
- false:则认为两个对象不是同一个对象,以链表结构向当前位置进行追加,这属于哈希表中解决冲突的方法称为:链地址法。还有一个方法叫线性探查法,这属于数据结构中哈希表中的知识。
-
通俗来讲:有两个人名字一样(hashCode()值一样),你不能说他们是同一个人。名字相同,还要通过equals()判断两个人是否相等。
-
下面是我学习数据结构中看视频截的图,哈希表解决冲突的线性探查法和链地址法讲解图示:
-
这里首先明白一个问题:
如果两个对啊ing的equals方法比较为true时,则两个对象的hashCode值也一定相同。先判断hashCode(),然后再判断equals()。如果两个对象的equals方法比较为false,却不能证明两个对象的hashCode值可以不相同,也可以相同。
总结:
- 判断重复的依据,两个对象hashCode值一致,并且equals也返回true— 同一个对象–集合中只添加一个进入
- 在set结合中防止相同对象被添加,需要hashCode 和equals都重写
如果对你有帮助,点个赞吧0.0
若有不正之处,请多多谅解并欢迎批评指正,不甚感激
- 参考资料
Java中hashCode的作用