一、Object对象
1.2 java创建对象的四种方式
java程序中对象的创建有四种方式:
● 调用new语句创建对象,最常见的一种
● 运用反射手段创建对象,调用java.lang.Class 或者 java.lang.reflect.Constructor 类的newInstance()实例方法
● 调用对象的clone()方法
● 运用序列化手段,调用java.io.ObjectInputStream 对象的 readObject()方法,其实就是一种深拷贝
@see https://www.cnblogs.com/avivahe/p/5702132.html
二、hashcode与equals
2.1 hashcode定义
hashCode() 的作用是获取哈希码,也称为散列码;它实际上是返回一个int整数。
这个哈希码的作用是确定该对象在哈希表中的索引位置。
hashCode() 定义在JDK的Object.java中,Java中的任何类都包含有
hashCode() 函数。
散列表存储的是键值对(key-value),它的特点是:能根据“键”快速的检索出对应的“值”。
这其中就利用到了散列码!(可以快速找到所需要的对象)
让同一个类的对象按照自己不同的特征尽量的有不同的哈希码,但不表示不同的对象哈希码完全不同。在Java中,哈希码代表对象的特征。
注意:有些朋友误以为默认情况下,hashCode返回的就是对象的存储地址,事实上这种看法是不全面的,确实有些JVM在实现时是直接返回对象的存储地址,但是大多时候并不是这样,只能说可能存储地址有一定关联。
hashCode方法的作用:
增加查询速度。快速判断对象是否不相等。
2.2 equals方法实现原理
先比较两个对象的hashCode,然后比较两个对象所指向的值
可以对照散列表的数据结构理解,hashcode值相当于桶的索引值,equals方法主要是判断在相同索引值下,遍历链表的值是否相同。
Object.equals方法实质就是判断对象的存储地址。
源码如下:
public boolean equals(Object obj) {
return (this == obj);
}
String中的equals方法其实重写了Object.equals方法
源码如下:
public boolean equals(Object anObject) {
if (this == anObject) {//先进行地址比较
return true;
}
if (anObject instanceof String) {//接着判断是否为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;
}
执行逻辑为:先判断对象存储地址是否相等,如果不等,判断对应的值,如果值相等则返回true。
故可以的得出如下结论:
如果调用equals方法得到的结果为true,则两个对象的hashcode值必定相等;
如果equals方法得到的结果为false,则两个对象的hashcode值不一定不同;
如果两个对象的hashcode值不等,则equals方法得到的结果必定为false;
如果两个对象的hashcode值相等,则equals方法得到的结果未知。
或者:
1、如果两个对象相同,那么它们的hashCode值一定要相同;
2、如果两个对象的hashCode相同,它们并不一定相同
3.两个对象不相同,他们的hashCode值不一定不相同。
4.两个对象的hashCode值不相同,他们一定是两个不同的对象
在什么场景下需要重新实现hashcode和equals这两个方法。
1、加入到hashset中的自定义类的对象,为确保他们不重复,需要对他们的类重写equals()和hashcode()的方法。
如果不重写equals,相同内容不同引用的对象会被当做不同的对象被加入到hashset中。
2.在重写equals方法的同时,必须重写hashCode方法
两个对象相等,首先要满足其hashcode值相等,因为两个不同的对象很有可能共用一个hashcode值。然后equals方法来判断是否相等。只有两种同时满足才能确定他们两个对象是相等的。
注意:equals方法最初是在所有类的基类Object中进行定义的,源码是
在这里插入代码片
public boolean equals(Object obj) {
return (this == obj);
}
由equals的源码可以看出这里定义的equals与是等效的(Object类中的equals没什么区别),不同的原因就在于有些类(像String、Integer等类)对equals进行了重写,但是没有对equals进行重写的类(比如我们自己写的类)就只能从Object类中继承equals方法,其equals方法与就也是等效的,除非我们在此类中重写equals。
@see https://www.cnblogs.com/zjc950516/p/7877511.html
思考
1.java 比较大小的坑和总结
- “== ”用于判断的是对象的内存地址
public class ArrayTest {
public static void main(String[] args){
String a = new String("aw");
String b = new String("aw");
System.out.println(a==b);//false
System.out.println(a.equals(b));//true
}
}
显然,尽管 a 与 b 对象的值相同,但是在内存中的地址是不同的,即hashcode不同,所以两个对象是不一样的。但用equals是返回的true。
再看一个例子:
public class ArrayTest {
public static void main(String[] args){
String a = new String("aw");
String b = new String("aw");
String c= "aa";
String d= "aa";
System.out.println(a.hashCode());
System.out.println(b.hashCode());
System.out.println(c.hashCode());
System.out.println(d.hashCode());
System.out.println(a==b);//false
System.out.println(c==d);//true
}
}
运行结果为
3126
3126
3104
3104
false
true
可见两个对象不相同,他们的hashCode值不一定不相同。
2.数字大小的比较最好用compareTo而不是equals
Long num = 1L;
Integer num2 = 1;
boolean b = num.compareTo(num2.longValue()) == 0;//true
boolean obj = Objects.equals(num2.intValue(),num.longValue());//false
System.out.println(Objects.equals(num,num2));//false
boolean equals = num2.intValue() == num.longValue();//true
2.解决hash冲突的几种方法
1.链表法
链表法就是将相同hash值的对象组织成一个链表放在hash值对应的槽位,如Java中的HashMap
2,不易探测到整个散列表的所有空间(线性探测法除外,但线性探测会出现堆积)
2.开放地址法
开放地址法有个非常关键的特征,就是所有输入的元素全部存放在哈希表里,也就是说,位桶的实现是不需要任何的链表来实现的,换句话说,也就是这个哈希表的装载因子不会超过1。它的实现是在插入一个元素的时候,先通过哈希函数进行判断,若是发生哈希冲突,就以当前地址为基准,根据再寻址的方法(探查序列),去寻找下一个地址,若发生冲突再去寻找,直至找到一个为空的地址为止。所以这种方法又称为再散列法。
有几种常用的探查序列的方法:
①线性探查
dii=1,2,3,…,m-1;这种方法的特点是:冲突发生时,顺序查看表中下一单元,直到找出一个空单元或查遍全表。
(使用例子:ThreadLocal里面的ThreadLocalMap)
②二次探查
di=12,-12,22,-22,…,k2,-k2 ( k<=m/2 );这种方法的特点是:冲突发生时,在表的左右进行跳跃式探测,比较灵活。
③ 伪随机探测
di=伪随机数序列;具体实现时,应建立一个伪随机数发生器,(如i=(i+p) % m),生成一个位随机序列,并给定一个随机数做起点,每次去加上这个伪随机数++就可以了。
缺点:1,删除工作很困难,假如要从哈希表 HT 中删除一个记录,应将这个记录所在位置置为空,但我们只能标上已被删除的标记,否则,将会影响以后的查找。
2,不易探测到整个散列表的所有空间(线性探测法除外,但线性探测会出现堆积)
3.再散列法
再散列法其实很简单,就是再使用哈希函数去散列一个输入的时候,输出是同一个位置就再次散列,直至不发生冲突位置
缺点:每次冲突都要重新散列,计算时间增加。
4.公共溢出区法
散列表由两个一维数组组成,一个称为基本表,它实际上就是一个散列表。另外一个称为溢出表。插入首先在基本表上进行,假如发生冲突,则将同义词存入溢出表。这样,可以保证基本表不会发生“堆积”
PS:基本表是不会发生堆积了,那溢出表呢?当进行查找时,查找到溢出表,这是不是又开启了新一轮的冲突解决?
@see 【java基础 10】hash算法冲突解决方法 https://www.cnblogs.com/hhx626/p/7534618.html
3.为什么这些操作线程的方法要定义在object类中呢?
答:因为加锁和解锁都是基于对象来的,而锁的信息是存在对象中的一个markword信息里面的,所以要定义在Object类里。
3 静态变量、实例变量、局部变量与线程安全
1.静态变量:线程非安全。
静态变量表示所有实例共享的一个属性,位于方法区,共享一份内存,而成员变量是对象的特殊描述,不同对象的实例变量被分配在不同的内存空间,一旦静态变量被修改,其他对象均对修改可见,故线程非安全。
2.实例变量:单例模式(只有一个对象实例存在)线程非安全,非单例线程安全。
实例变量为对象实例私有,在虚拟机的堆中分配,若在系统中只存在一个此对象的实例,在多线程环境下,被某个线程修改后,其他线程对修改均可见,故线程非安全;
如果每个线程执行都是在不同的对象中,那对象与对象之间的实例变 量的修改将互不影响,故线程安全。
3.局部变量:线程安全。
局部变量存在于栈内存中,作用的范围结束,变量空间会自动释放。由于每个线程执行时将会把局部变量放在各自栈帧的工作内存中,线程间不共享,故不存在线程安全问题。
@see 静态变量、实例变量、局部变量与线程安全 https://www.cnblogs.com/tonyluis/p/5549149.html
参考资料
1.浅谈Java中的hashcode方法 https://www.cnblogs.com/dolphin0520/p/3681042.html
2.[转]Java 的强引用、弱引用、软引用、虚引用 http://www.cnblogs.com/gudi/p/6403953.html