详述HashSet add方法
- 调用HashSet无参构造方法创建HashSet对象底层是为HashSet中的全局变量map赋值了一个HashMap对象的引用,HashSet无参构造方法源码如下:
public HashSet() { map = new HashMap<>(); }
- HashSet添加元素的add()方法底层调用了HashMap的put()方法,实质是添加在了HashMap的key,而value位置是一个全局常量,add()方法源码如下:
public boolean add(E e) { return map.put(e, PRESENT)==null; }
- HashMap的put()方法内部又调用了putVal()方法,put()方法源码如下:
public V put(K key, V value) { return putVal(hash(key), key, value, false, true); }
- putVal()方法源码分以下两种情况进行分析(部分注释为本人在此处添加,并非源码原有)
1、如果HashSet中存放的是String类型或者是基本数据类型的包装类
- 例:
public class Test { public static void main(String[] args) { HashSet<String> set = new HashSet<String>(); set.add("Tom"); set.add(new String("Tom")); System.out.println(set.size()); //1 } }
- 则执行“set.add(“Tom”);”流程如下:
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { //定义了几个局部变量 Node<K,V>[] tab; Node<K,V> p; int n, i; if ((tab = table) == null || (n = tab.length) == 0) /** * 这里的table只进行了声明而没有赋值“transient Node<K,V>[] table;” * 因此这里是null,因此第一个逻辑表达式为真,第二个逻辑表达式不执行 * 整个逻辑表达式为真,所以执行if语句内部的代码 * resize()方法内给table赋值了一个Node<K,V>[]对象的引用,该语句且将该对象引用返回给了tab * Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap]; * table = newTab; * ... * return newTab; * 该语句又将赋值过的tab数组的长度赋值给了n,默认是16 */ n = (tab = resize()).length; if ((p = tab[i = (n - 1) & hash]) == null) /** * 将传进来的字符串计算出来的hash值乘以15并赋值给i * 将tab数组中第i个位置的对象引用赋值给p * 判断p是否为空,此时返回真,所以执行if语句内部的代码 * 创建调用newNode方法创建新的节点放在tab数组i的位置 */ tab[i] = newNode(hash, key, value, null); else { //if执行了所以这里跳过 Node<K,V> 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<K,V>)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; } } ++modCount; if (++size > threshold) resize(); afterNodeInsertion(evict); //返回null,add()方法收到null,判断是否为null,是则返回true,表示添加成功 return null; }
- 执行“set.add(new String(“Tom”));”流程如下:
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { //定义了几个局部变量 Node<K,V>[] tab; Node<K,V> p; int n, i; if ((tab = table) == null || (n = tab.length) == 0) /** * 这里的table执行“set.add("Tom");”的时候已经赋值,所以不为null * 因此第一个逻辑表达式为假同时将上次的table赋值给了tab * 第二个逻辑表达式执行为假同时将tab数组的长度赋值给了n * 整个逻辑表达式为假,所以不执行if语句内部的代码 */ n = (tab = resize()).length; if ((p = tab[i = (n - 1) & hash]) == null) /** * 将传进来的字符串计算出来的hash值乘以15并赋值给i * 将tab数组中第i个位置的对象引用赋值给p * 判断p是否为空,由于上次传入的字符串与这次传入的字符串内容相同 * String类重写了hashCode()方法,内容相同则哈希值也相等,哈希值相同则计算出来的i也相等 * 由于上次该位置已经添加了一个对象,所以此位置此时不为null,返回假,所以不执行if语句内部的代码 */ tab[i] = newNode(hash, key, value, null); else { //if没执行所以这里执行 Node<K,V> e; K k; if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) /** * 内容相同的字符串的哈希值相同,但由于这次传入的字符串是新创建的 * 所以两者地址不同,这次传入的字符串不为null,所以此逻辑表达式返回true * 将p,也就是tab数组中i位置的对象引用赋值给e */ e = p; else if (p instanceof TreeNode) //if执行此处不执行 e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); else { //if执行此处不执行 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 /** * 由于此时e指向的是tab数组中i位置的对象,所以不为空,if中的代码将被执行 * 将e的value赋值给oldValue */ V oldValue = e.value; if (!onlyIfAbsent || oldValue == null) e.value = value; afterNodeAccess(e); /** * 返回oldValue,oldValue不为空,所以返回给putVal(hash(key), key, value, false, true)的值不为空 * 进而返回给map.put(e, PRESENT)的值也不为空,此时判断是否为空,结果为false,表明添加失败 * 此句结束该方法 */ return oldValue; } } ++modCount; if (++size > threshold) resize(); afterNodeInsertion(evict); return null; }
2、如果HashSet中存放的是自定义数据类型
- 例1,自定义类没有重写hashCode()方法和equals()方法:
import java.util.HashSet; class Student{ private String id; public Student(String id) { this.id = id; } } public class Test { public static void main(String[] args) { HashSet<Student> set = new HashSet<Student>(); set.add(new Student("110")); set.add(new Student("110")); System.out.println(set.size()); } }
- 执行第一个“set.add(new Student(“110”));”语句时:与上面第一次添加字符串时流程相似
- 执行第二个“set.add(new Student(“110”));”语句时:
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { //定义了几个局部变量 Node<K,V>[] tab; Node<K,V> p; int n, i; if ((tab = table) == null || (n = tab.length) == 0) /** * 这里的table执行“set.add("Tom");”的时候已经赋值,所以不为null * 因此第一个逻辑表达式为假同时将上次的table赋值给了tab * 第二个逻辑表达式执行为假同时将tab数组的长度赋值给了n * 整个逻辑表达式为假,所以不执行if语句内部的代码 */ n = (tab = resize()).length; if ((p = tab[i = (n - 1) & hash]) == null) /** * 将传进来的字符串计算出来的hash值乘以15并赋值给i * 将tab数组中第i个位置的对象引用赋值给p * 判断p是否为空,由于上次传入的字符串与这次传入的字符串内容相同 * Student类没有重写hashCode()方法,所以地址不同相同则哈希值也不相等,哈希值不相同则计算出来的i也不相等 * 所以此位置此时为null,返回真,所以执行if语句内部的代码 * 创建一个新的对象放在tab数组的i位置上 */ tab[i] = newNode(hash, key, value, null); else { //if执行了所以这里不执行 Node<K,V> 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<K,V>)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; } } ++modCount; if (++size > threshold) resize(); afterNodeInsertion(evict); //返回null,在add()方法中判断是null,并返回true表示添加成功 return null; }
- 例2,自定义类重写了hashCode()方法和equals()方法:
import java.util.HashSet; class Student{ private String id; public Student(String id) { this.id = id; } @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; } } public class Test { public static void main(String[] args) { HashSet<Student> set = new HashSet<Student>(); set.add(new Student("110")); set.add(new Student("110")); System.out.println(set.size()); } }
- 执行第一个“set.add(new Student(“110”));”语句时:与上面第一次添加字符串时流程相似
- 执行第二个“set.add(new Student(“110”));”语句时:
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { //定义了几个局部变量 Node<K,V>[] tab; Node<K,V> p; int n, i; if ((tab = table) == null || (n = tab.length) == 0) /** * 这里的table执行“set.add("Tom");”的时候已经赋值,所以不为null * 因此第一个逻辑表达式为假同时将上次的table赋值给了tab * 第二个逻辑表达式为假同时将tab数组的长度赋值给了n * 整个逻辑表达式为假,所以不执行if语句内部的代码 */ n = (tab = resize()).length; if ((p = tab[i = (n - 1) & hash]) == null) /** * 将传进来的字符串计算出来的hash值乘以15并赋值给i * 将tab数组中第i个位置的对象引用赋值给p * 判断p是否为空,由于上次传入的字符串与这次传入的字符串内容相同 * Student类没有重写了hashCode()方法,所以上次的对象和这次的对象哈希值相等,哈希值相同则计算出来的i也相等 * 所以此位置此时不为null,返回假,所以不执行if语句内部的代码 */ tab[i] = newNode(hash, key, value, null); else { //if没执行所以这里执行 Node<K,V> e; K k; if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) /** * 上次传入的对象和这次传入的对象哈希值相同,并且Student类重写了equals()方法 * 两次传入的对象equals()比较后为真,所以整个表达式为真 * 将p,也就是tab数组第i个位置上的对象引用赋值给e */ e = p; else if (p instanceof TreeNode) //if执行此处不执行 e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); else { //if执行此处不执行 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 /** * 经判断e确实不为null,所以返回真,执行下面的代码 * 将e的value赋值给oldValue,此时oldValue不为null */ V oldValue = e.value; if (!onlyIfAbsent || oldValue == null) e.value = value; afterNodeAccess(e); /** * 返回的oldValue不为null因此add()方法返回"oldValue == null",即false,表示添加失败 * 此句执行完毕结束当前方法 */ return oldValue; } } ++modCount; if (++size > threshold) resize(); afterNodeInsertion(evict); return null; }
- 注:由于HashSet的值是存储在HashMap的key位置上的,HashSet的add()方法底层调用的是HashMap()方法的put()方法,因此其值不允许重复。判断是否重复的两个重要的标准就是hashCode()和equals()两个方法。
- 如果HashSet存储的是String类型:如果内容相同,则哈希值也相同;equals()比较两个内容相同的字符串返回true
- 如果HashSet存储的是基本数据类型的包装类:如果值相同,则哈希值也相同;equals()比较两个相同的数值返回true
- 如果HashSet存储的是自定义数据类型:如果没有重写hashCode()方法,则地址不同,哈希值也不同,可以重写hashCode()方法来规定自定义数据类型的哈希值;equals()比较两个自定义数据类型默认比较的是地址,而不同的对象有不同的地址,因此可以重写equals()方法来规定两个对象在满足什么条件时认定它们是同一个对象