一、学习本章你将学习到连接到的新方法:
1、HashSet中的成员变量:map、hash
private transient HashMap map;
2、HashSet中的无参构造方法
3、HashMap中put()方法详解
4、put()方法中putval的作用以及Hash()方法的作用
5、putval方法中resize()方法的作用与返回值
6、putval()方法中构造节点的方法newCode()方法
7、putVal()方法中第二个if针对三种情形的处理方式
二、详解add的底层代码
1、创建HashSet集合时:
public HashSet() {
map = new HashMap<>();
}
上面提到,执行创建对象,相当于调用无参构造方法HashSet,之后创建一个HashMap对象为map(HashSet中的成员变量)赋值;
2、使用HashSet中的add方法:
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
可以看出,返回值是boolean型,并且执行了map中的put()方法,其中传入的e即是集合中的泛型,而PRESENT则是一个常量,下面分析put方法:
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
一、put方法返回的是putVal方法的返回值,可以看到其中的形参中调用了hash方法,下面详解hash()方法的作用:
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
该方法的返回值由key.hashCode()方法返回值的不同决定的,既可以这样理解:只要key.hashCode()不同,返回值就不相同:(思考:hashCode()方法的作用?)
下面引入toString()方法:可以返回一个引用型对象的地址(默认条件下可以不写):注意:String类型的变量已将改写该方法,所以输出的是字符串
public String toString() {
return getClass().getName() + "@" + Integer.toHexString(hashCode());
}
此时,我们理解到hashCode就是地址的十进制表达方式,而我们正常看到的地址是Integer.toHexString(hashCode())方法为我们转换成了十六进制的数:
public class Test {
public static void main(String[] args) {
Test test=new Test();
System.out.println(test.toString());
System.out.println(Integer.toHexString(test.hashCode()));
}
}
执行结果如下:moon.Test@52e922
52e922
注:对于正常的String方法,其中已经改写了hashCode方法,此时结论是:只要是对象中的字符串相同,那么hashCode的返回值相同:
public int hashCode() {
int h = hash;
if (h == 0 && value.length > 0) {
char val[] = value;
for (int i = 0; i < value.length; i++) {
h = 31 * h + val[i];
}
hash = h;
}
return h;
}
证明如下:
public class Test {
public static void main(String[] args) {
String str="Tom";
String str2=new String("Tom");
System.out.println(str==str2);
System.out.println(str.hashCode());
System.out.println(str2.hashCode());
}
}
执行结果为:false
84274
84274
执行结果说明:hashCode结果相同,而地址不同
二、putVal方法:
先看一下的底层代码分析:
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node[] tab; Node p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
else if (p instanceof TreeNode)
e = ((TreeNode)p).putTreeVal(this, tab, hash, key, value);
else {
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
第一个if()处:
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
1、resize()方法: 第一次赋值时为成员变量:table 和局部变量 tab赋予相同的地址(这两个属性为一个节点(Node)的集合)
final Node[] resize() {
Node[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
if (oldCap > 0) {
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
}
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
else { // zero initial threshold signifies using defaults
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
threshold = newThr;
@SuppressWarnings({"rawtypes","unchecked"})
Node[] newTab = (Node[])new Node[newCap];
table = newTab;
if (oldTab != null) {
for (int j = 0; j < oldCap; ++j) {
Node e;
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;
else if (e instanceof TreeNode)
((TreeNode)e).split(this, newTab, j, oldCap);
else { // preserve order
Node loHead = null, loTail = null;
Node hiHead = null, hiTail = null;
Node next;
do {
next = e.next;
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
我们在代码中找到这几行分析:
newCap = DEFAULT_INITIAL_CAPACITY;
Node
table = newTab
return newTab;
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
可以看出resize的作用就是把table 和局部变量 tab赋予相同的地址(这两个属性为一个节点(Node)的集合),并且第一次确定数组的长度:n=16;
第二个if()语句:
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
括号中判断这次输入的数据是否是已经存在过:我们知道,通过hash的值根据hashCode()返回值不同而不同,这也就是说,每添加一个新的地址key,就会执行if()语句后面的方法,再到最后执行:
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
这样就在成功添加了一个数据:并且在put方法中输出true
此时不成立的条件下,会自动将相同上一次输出的集合元素赋值给p这个变量,所以此处的p实则找到与本次hash相同的变量
下面探究当数据的hash相同时,else执行的操作:
else {
Node e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
}
下面分为三种情况讨论:
一、当直接添加字符串:
public class Test {
public static void main(String[] args) {
HashSet set =new HashSet<>();
set.add("Tom");
set.add("Tom");
}
}
此时由于直接添加字符串,添加的key地址相同,hash也相同,即else后面if(表达式为真),此时执行将p赋值给e,然后执行下一步:、
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
此时的两个if()均为真;所以用新的key替代旧的key,且返回非null ,put()返回false;
二、当Stringl类采用对象赋值时;
import java.util.HashSet;
public class Test {
public static void main(String[] args) {
HashSet set =new HashSet<>();
set.add("Tom");
String name =new String("Tom");
System.out.println(set.add(name));
}
}
上面提到过,String类已经修改过hashCode()方法了,所以此时的hash还是 相同的,此时我们直接进行这一段代码的比较:
else {
Node e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
}
前后的Hash虽然相同,但是——地址不相同:在进行两者equals的对比;由于String中的equals也经过重写,所以此时也是:true,之后又是最后的覆盖内容:
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
此时为false;
三、输出的是非String类新建对象时
import java.util.HashSet;
public class Test {
public static void main(String[] args) {
HashSet set =new HashSet<>();
set.add(new Test());
set.add(new Test());
}
}
此时由于地址不同,会存在直接录入;
三、在创建项目时:
此时可以将hashCode( ) 与equals ( )进行重写方法,使项目变得简单,这也是我们学习底层代码的原因:
public class Student {
public String id;
public String name;
public Student(String id, String name) {
this.id = id;
this.name = name;
}
@Override
public int hashCode() {
return id.hashCode();
}
@Override
public boolean equals(Object obj) {
if(obj instanceof Student) {
Student student =(Student)obj;
return this.id.equals(student.id);
}
return false;
}
}
如图所示:执行顺序,有可能存在泛型过大而导致不同对象ID重复并且录入的现象:
import java.util.HashSet;
public class Test {
public static void main(String[] args) {
HashSet
此时执行代码:
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
else if (p instanceof TreeNode)
e = ((TreeNode)p).putTreeVal(this, tab, hash, key, value);
else {
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
此时由于Student类中的改写,会出现以下情况:在判断第if时输出的是false,之后执行到下面的for循环中,将集合中的每个元素进行遍历,当出现先集合中出现空位时,将这个新值添加进去;
本节的内容到此结束:谢谢