Object类总结
介绍
- java.lang.Object是类层次结构的根(父)类。
- 每个类(Person,Student…)都使用 Object 作为超(父)类。
- 所有对象(包括数组)都实现这个类的方法。
常用方法
Person实体类
package com.itheima.demo01Object;
import java.util.Objects;
public class Person {
private String name;
private int age;
public Person() {
}
public Person(String name, int age) {
this.name = name;
this.age = age;
}
/*
直接打印对象的地址值没有意义,需要重写Object类中的toString方法
打印对象的属性(name,age)
*/
/*@Override
public String toString() {
//return "abc";
return " Person { name = " + name + " , age = " + age + " } ";
}*/
/*@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}*/
/*
Object类的equals方法,默认比较的是两个对象的地址值,没有意义
所以我们要重写equals方法,比较两个对象的属性(name,age)
问题:
隐含着一个多态
多态的弊端:无法使用子类特有的内容(属性和方法)
Object obj = p2 = new Person("古力娜扎",19);
解决:可以使用向下转型(强转)把obj类型转换为Person
*/
/*@Override
public boolean equals(Object obj) {
//增加一个判断,传递的参数obj如果是this本身,直接返回true,提高程序的效率
if(obj==this){
return true;
}
//增加一个判断,传递的参数obj如果是null,直接返回false,提高程序的效率
if(obj==null){
return false;
}
//增加一个判断,防止类型转换一次ClassCastException
if(obj instanceof Person){
//使用向下转型,把obj转换为Person类型
Person p = (Person)obj;
//比较两个对象的属性,一个对象是this(p1),一个对象是p(obj->p2)
boolean b = this.name.equals(p.name) && this.age==p.age;
return b;
}
//不是Person类型直接返回false
return false;
}*/
@Override
public boolean equals(Object o) {
if (this == o) return true;
//getClass() != o.getClass() 使用反射技术,判断o是否是Person类型 等效于 obj instanceof Person
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);
}
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;
}
}
1、String toString()
返回该对象的字符串表示。实体类未重写则打印的是地址值
源码
public String toString() {
return getClass().getName() + "@" + Integer.toHexString(hashCode());
}
测试
package com.itheima.demo01Object;
import java.util.ArrayList;
import java.util.Random;
import java.util.Scanner;
/*
java.lang.Object
类 Object 是类层次结构的根(父)类。
每个类(Person,Student...)都使用 Object 作为超(父)类。
所有对象(包括数组)都实现这个类的方法。
*/
public class Demo01ToString{
public static void main(String[] args) {
/*
Person类默认继承了Object类,所以可以使用Object类中的toString方法
String toString() 返回该对象的字符串表示。
实体类未重写则打印的是地址值
*/
Person p = new Person("张三",18);
String s = p.toString();
System.out.println(s);//com.itheima.demo01.Object.Person@75412c2f | abc | Person{name=张三 ,age=18}
//直接打印对象的名字,其实就是调用对象的toString p=p.toString();
System.out.println(p);//com.itheima.demo01.Object.Person@5f150435 | abc | Person{name=张三 ,age=18}
//看一个类是否重写了toString,直接打印这个类的对象即可,如果没有重写toString方法那么打印的是对象的地址值
Random r = new Random();
System.out.println(r);//java.util.Random@3f3afe78 没有重写toString方法
Scanner sc = new Scanner(System.in);
System.out.println(sc);//java.util.Scanner[delimiters=\p{javaWhitespace}+.. 重写toString方法
ArrayList<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);
list.add(3);
System.out.println(list);//[1, 2, 3] 重写toString方法
}
}
2、boolean equals(Object obj)
Person类默认继承了Object类,所以可以使用Object类的equals方法
源码
public boolean equals(Object obj) {
return (this == obj);
}
boolean equals(Object obj) 指示其他某个对象是否与此对象“相等”
参数:
Object obj:可以传递任意对象
== 比较运算符:返回的是一个布尔值 true false
引用数据类型:比较的是值地址值
this是谁?哪个对象调用的方法,方法中的this就是哪个对象:
p1调用了equals方法所以this就是p1
obj是谁?传递过来的参数p2
引用数据类型:比较的是两个对象的
this == obj --> p1 == p2
测试类
package com.itheima.demo01Object;
import java.util.ArrayList;
public class Demo02Equals {
public static void main(String[] args) {
/*
* Person类默认继承了Object类,所以可以使用Object类的equals方法
* boolean equals(Object obj) 指示其他某个对象是否与此对象“相等”
* equals方法的源码:
* public boolean equals(Object obj) {
* return (this == obj);
* }
* 参数:
* Object obj:可以传递任意对象
* == 比较运算符:返回的是一个布尔值 true false
* 引用数据类型:比较的是值地址值
* this是谁?哪个对象调用的方法,方法中的this就是哪个对象:
* p1调用了equals方法所以this就是p1
* obj是谁?传递过来的参数p2
* 引用数据类型:比较的是两个对象的
* this == obj --> p1 == p2
* */
Person p1 = new Person("迪丽热巴",18);
//Person p2 = new Person("古力娜扎",19);
Person p2 = new Person("迪丽热巴",18);
System.out.println("p1:" + p1); //地址值
System.out.println("p2:" + p2); //地址值
// p1 = p2; //把p2的地址值赋值给p1
ArrayList<String> list = new ArrayList<>();
boolean b = p1.equals(list);
boolean c = p1.equals(p2);
System.out.println(b);
System.out.println(c);
}
}
3、public int hashCode()
源码
public native int hashCode();
官方文档
public int hashCode() 返回对象的哈希码值。 支持这种方法是为了散列表,如HashMap提供的那样 。
hashCode的总合同是:
只要在执行Java应用程序时多次在同一个对象上调用该方法, hashCode方法必须始终返回相同的整数,前提是修改了对象中equals比较中的信息。 该整数不需要从一个应用程序的执行到相同应用程序的另一个执行保持一致。
如果根据equals(Object)方法两个对象相等,则在两个对象中的每个对象上调用hashCode方法必须产生相同的整数结果。
不要求如果两个对象根据equals(java.lang.Object)方法不相等,那么在两个对象中的每个对象上调用hashCode方法必须产生不同的整数结果。 但是,程序员应该意识到,为不等对象生成不同的整数结果可能会提高哈希表的性能。
尽可能多的合理实用,由类别Object定义的hashCode方法确实为不同对象返回不同的整数。 (这通常通过将对象的内部地址转换为整数来实现,但Java的编程语言不需要此实现技术。)
结果
该对象的哈希码值。
以上这段官方文档的定义,我们可以抽出成以下几个关键点:
1、hashCode的存在主要是用于查找的快捷性,如Hashtable,HashMap等,hashCode是用来在散列存储结构中确定对象的存储地址的;
2、如果两个对象相同,就是适用于equals(java.lang.Object) 方法,那么这两个对象的hashCode一定要相同;
3、如果对象的equals方法被重写,那么对象的hashCode也尽量重写,并且产生hashCode使用的对象,一定要和equals方法中使用的一致,否则就会违反上面提到的第2点;
4、两个对象的hashCode相同,并不一定表示两个对象就相同,也就是不一定适用于equals(java.lang.Object) 方法,只能够说明这两个对象在散列存储结构中,如Hashtable,他们“存放在同一个篮子里”。
native关键字
使用native关键字说明这个方法是原生方法,也就是这个方法是用C/C++语言实现的,并且被编译成了DLL,由java去调用。JDK的源代码中并不包含,是看不到的。对于不同的平台它们也是不同的。这也是java的底层机制,实际上java就是在不同的平台上调用不同的native方法实现对操作系统的访问的。native的意思就是通知操作系统,这个函数你必须给我实现,因为我要使用。所以native关键字的函数都是操作系统实现的,java只能调用。当对象调用Object类中的hashCode()方法时,JVM通过调用原生函数计算得出该对象的hashcode值
hash(哈希) 、hash表(哈希表)、hashcode(哈希值)
-
哈希是指一个过程,这个过程就是把任意长度的输入,通过哈希算法,变换成固定长度的输出,所输出的称为哈希值。这种变换是一种压缩映射,也即哈希值所占的空间一般来说远小于输入值的空间,不同的输入可能会哈希出相同的输出(概率很小)。这就体现了其优势:查询的快捷性 (用hashcode来代表对象在hash表中的位置)
以HashSet为例,根据对象的hashCode方法计算出该对象的hashcode,可以快速地找到要进行操作的对象位置。。
-
哈希函数、算法
哈希算法将任意长度的二进制值映射为较短的固定长度的二进制值,这个小的二进制值称为哈希值。哈希值是一段数据唯一且极其紧凑的数值表示形式。如果散列一段明文而且哪怕只更改该段落的一个字母,随后的哈希都将产生不同的值。要找到散列为同一个值的两个不同的输入,在计算上是不可能的,所以数据的哈希值可以检验数据的完整性。一般用于快速查找和加密算法 —《数据结构与算法分析》
-
哈希表
散列表(Hash table,也叫哈希表),是根据关键码值(Key value)而直接进行访问的数据结构。也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数,存放记录的数组叫做散列表。在JAVA中,每个对象的hashcode值决定了在哈希表中的存储位置。
-
哈希碰撞(也叫哈希冲突)
当添加元素时,在判断hashCode相等的情况下的情况下,再判断equals,如果返回值为false,则这个时候会以链表的形式在同一个位置上存放两个元素,这会使得HashSet的性能降低,因为不能快速定位了。
-
特点
如果两个哈希值是不相同的(根据同一函数),那么这两个散列值的原始输入一定是不相同的。
如果两个哈希值相同,两个输入值很可能(极大概率)是相同的,但也可能不同,这种情况称为“哈希碰撞”
抗篡改能力:对于一个数据块,哪怕只改动其一个比特位,其hash值的改动也会非常大。
它是一种单向函数是“非对称”的,即它是一个从明文到密文的不可逆的映射,只有加密过程,没有解密过程 -
举例
我们先复习数据结构里的一个知识点:在一个长度为n(假设是10000)的线性表(假设是ArrayList)里,存放着无序的数字;如果 我们要找一个指定的数字,就不得不通过从头到尾依次遍历来查找,这样的平均查找次数是n除以2(这里是5000)。
我们再来观察Hash表(这里的Hash表纯粹是数据结构上的概念,和Java无关)。它的平均查找次数接近于1,代价相当小,关键是在Hash表里,存放在其中的数据和它的存储位置是用Hash函数关联的。
我们假设一个Hash函数是x*x%5。当然实际情况里不可能用这么简单的Hash函数,我们这里纯粹为了说明方便,而Hash表是一个长度是11的线性表。如果我们要把6放入其中,那么我们首先会对6用Hash函数计算一下,结果是1,所以我们就把6放入到索引号是1这个位置。同样如果我们要放数字7,经过Hash函数计算,7的结果是4,那么它将被放入索引是4的这个位置。这个效果如下图所示。
这样做的好处非常明显。比如我们要从中找6这个元素,我们可以先通过Hash函数计算6的索引位置,然后直接从1号索引里找到它了。
不过我们会遇到“Hash值冲突”这个问题,即哈希碰撞。比如经过Hash函数计算后,7和8会有相同的Hash值,对此Java的HashMap对象采用的是”链地址法“的解决方案。效果如下图所示。
具体的做法是,为所有Hash值是i的对象建立一个同义词链表。假设我们在放入8的时候,发现4号位置已经被占,那么就会新建一个链表结点放入8。同样,如果我们要找8,那么发现4号索引里不是8,那会沿着链表依次查找。
虽然我们还是无法彻底避免Hash值冲突的问题,但是Hash函数设计合理,仍能保证同义词链表的长度被控制在一个合理的范围里。
Set接口的特征及其子类HashSet
Set接口存储特点:——储存引用类型
- 不允许元素重复
- 不会记录元素的添加先后顺序
HashSet为Set接口的实现类
HashSet底层采用哈希表实现,元素对象的hashCode值决定了在哈希表中的存储位置
其基本原理是:使用一个下标范围很大的数组来存储对象。通过一个函数,根据每个对象的信息计算得到一个函数值(即数组下标,hashcode),然后该单元数组来存储这个对象
每一个存储到哈希表中的对象,都得覆盖hashCode和equals方法用来判断是否是同一个对象
对象的hash算法是怎么计算,怎么判断的?
-
如果成员变量是基本数据类型,则根据变量数据参加计算后判断
-
如果成员变量是引用数据类型,则获得成员变量的哈希码值(原生函数)后,再参数计算后判断
当往HashSet集合中添加新的元素对象时,先会判断该对象和集合对象中的hashCode值:
-
不等: 直接把该新的对象存储到hashCode指定的位置
-
相等: 再继续判断新对象和集合对象中的equals做比较
- 若equals为true:则视为是同一个对象,则不保存。
- 若equals为false:存储在之前对象同槽位的链表上 —— 哈希碰撞
为什么重写equals()方法后也要重写hashCode()方法
首先回到hashCode()官方文档的关键点:
1、如果两个对象相同,就是适用于equals(java.lang.Object) 方法,那么这两个对象的hashCode一定要相同;
2、两个对象的hashCode相同,并不一定表示两个对象就相同,也就是不一定适用于equals(java.lang.Object) 方法,只能够说明这两个对象在散列存储结构中,如Hashtable,他们“存放在同一个篮子里”。
情景一:只重写了equals()方法
package com.itheima.demo01Object;
import java.util.HashSet;
import java.util.Set;
/**
* @ClassName: HashTest
* @Description:
* @Author: keke
* @Date: 2021/4/18
*/
public class HashTest {
private int i;
public HashTest(int i) {
this.i = i;
}
public int getI() {
return i;
}
public void setI(int i) {
this.i = i;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
HashTest hashTest = (HashTest) o;
return i == hashTest.i;
}
/*@Override
public int hashCode() {
return Objects.hash(i);
}*/
public final static void main(String[] args) {
HashTest a = new HashTest(1);
HashTest b = new HashTest(1);
Set<HashTest> set = new HashSet<HashTest>();
boolean add1 = set.add(a);
boolean add2 = set.add(b);
System.out.println(add1); //true,添加成功
System.out.println(add2); //true,添加成功
System.out.println(a.hashCode() == b.hashCode()); //false
System.out.println(a.equals(b)); //true
System.out.println(set); //[com.itheima.demo01Object.HashTest@39a054a5, com.itheima.demo01Object.HashTest@7a7b0070]
}
}
结果分析
从结果可以看出,重写后的equals()方法,在判断a,b的内容是否相等时返回true
在一般情况下,我们会希望a,b的成员变量值相同时就认为他们相同,我们就会重写equals()方法
我们期望的是当a,b对象内容相等时,不进行add添加操作,而往Set集合里添加b时,会先判断a和b中的hashCode值,这里hashCode值不等,直接把 b对象 存储到hashCode指定的位置,这就不符合我们的期望
情景二:只重写了hashCode()方法
package com.itheima.demo01Object;
import java.util.HashSet;
import java.util.Objects;
import java.util.Set;
/**
* @ClassName: HashTest
* @Description:
* @Author: keke
* @Date: 2021/4/18
*/
public class HashTest {
private int i;
public HashTest(int i) {
this.i = i;
}
public int getI() {
return i;
}
public void setI(int i) {
this.i = i;
}
/*@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
HashTest hashTest = (HashTest) o;
return i == hashTest.i;
}*/
@Override
public int hashCode() {
return Objects.hash(i);
}
public final static void main(String[] args) {
HashTest a = new HashTest(1);
HashTest b = new HashTest(1);
Set<HashTest> set = new HashSet<HashTest>();
boolean add1 = set.add(a);
boolean add2 = set.add(b);
System.out.println(add1); //true,添加成功
System.out.println(add2); //true,添加成功
System.out.println(a.hashCode() == b.hashCode()); //true
System.out.println(a.equals(b)); //false
System.out.println(set); //[com.itheima.demo01Object.HashTest@20, com.itheima.demo01Object.HashTest@20]
}
}
结果分析
我们只是重写了hashCode方法,从上面的结果可以看出,虽然两个对象的hashCode相等,但是实际上两个对象并不是相等;我们没有重写equals方法,那么就会调用object默认的equals方法,是比较两个对象的引用是不是相同,显示这是两个不同的对象,两个对象的引用肯定是不定的。这里我们将生成的对象放到了HashSet中,而HashSet中只能够存放唯一的对象,也就是相同的(适用于equals方法)的对象只会存放一个,但是这里实际上是两个对象a,b都被放到了HashSet中,这样HashSet就失去了他本身的意义了。
情景三:重写了hashCode()和equals()方法
package com.itheima.demo01Object;
import java.util.HashSet;
import java.util.Objects;
import java.util.Set;
/**
* @ClassName: HashTest
* @Description:
* @Author: keke
* @Date: 2021/4/18
*/
public class HashTest {
private int i;
public HashTest(int i) {
this.i = i;
}
public int getI() {
return i;
}
public void setI(int i) {
this.i = i;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
HashTest hashTest = (HashTest) o;
return i == hashTest.i;
}
@Override
public int hashCode() {
return Objects.hash(i);
}
public final static void main(String[] args) {
HashTest a = new HashTest(1);
HashTest b = new HashTest(1);
Set<HashTest> set = new HashSet<HashTest>();
boolean add1 = set.add(a);
boolean add2 = set.add(b);
System.out.println(add1); //true,添加成功
System.out.println(add2); //false,添加失败
System.out.println(a.hashCode() == b.hashCode()); //true
System.out.println(a.equals(b)); //true
System.out.println(set); //[com.itheima.demo01Object.HashTest@20]
}
}
结果分析
从结果我们可以看出,现在两个对象就完全相等了,HashSet中也只存放了一份对象。
总结
- hashCode主要用于提升查询效率,来确定在散列结构中对象的存储地址;
- 重写equals()必须重写hashCode(),二者参与计算的自身属性字段应该相同;
- hash类型的存储结构,添加元素重复性校验的标准就是先取hashCode值,后判断equals();
- equals()相等的两个对象,hashcode()一定相等;反过来:hashcode()不等,一定能推出equals()也不等;hashcode()相等,equals()可能相等,也可能不等
- 其实,哈希表就相当于一堆桶,当桶中存放自定义对象时,通过hash函数计算hashcode,放入对应的桶中;这样出现哈希碰撞的对象就会以链表的形式存在同一个桶中;在查找时,通过hashcode()方法可以找到桶,再通过equals()方法来在这个桶里找到我们要的对象。