Collection的遍历可以使用Iterator接口或者是foreach循环来实现
Set集合不允许包含相同的元素,而判断两个对象是否相同则是根据对象的hashcode和equals方法。
带着问题去学习
1.HashSet底层是什么数据结构?
2.HashSet允许有空值么?
3.HashSet允许有重复值么?
4.如果new两个值一样的字符串,往HashSet集合中添加,是否能添加进去?
5.HashSet是如何保证元素的唯一性的?
6.HashSetadd方法其实是调用的那个方法?
7.HashSet是否是线程安全的呢?
上面的几乎都没有什么难度,使用过集合的大多数人都了解。
那我们来看看哪些硬核的难点吧!
我们都知道HashSet底层其实上是new了一个HashMap集合,那我们就来看看,HashSet调用add方法的时候的一些问题。
1.HashMap的value部分值是否相同?
2.HashMap的初始化容量是多大?是在什么时候进行初化容量?
3.在计算HashMap的key的HashCode值的时候是单纯的时候hashCode方法计算出来的么?
4.HashMap什么时候进行扩容?
5.HashMap数组转红黑树需要满足那些条件?
7.HashSet在添加重复元素的时候,具体是怎么进行判断该元素已经存在的?
8.使用HashSet集合的时候,需要重写HashCode和equlas方法么?
那我们来看看哪些硬核的难点吧!
1.HashMap的value部分值是否相同?
2.HashMap的初始化容量是多大?是在什么时候进行初化容量?
3.在计算HashMap的key的HashCode值的时候是单纯的时候hashCode方法计算出来的么?
4.HashMap什么时候进行扩容?
5.HashMap数组转红黑树需要满足那些条件?
7.HashSet在添加重复元素的时候,具体是怎么进行判断该元素已经存在的?
8.使用HashSet集合的时候,需要重写HashCode和equlas方法么?
1、HashSet底层是什么数据结构?
HashSet底层采用的是数组+链表+红黑树,在new HashSet的时候实际底层是new了一个HashMap,把HashMap的key部分,作为HashSet的Value部分。
2、HashSet允许有空值么?
准确的来说是允许的(也就是代码不会出现异常),但是只能有一个空值,如果有第二个空值,那么第二个空值将加不进HashSet集合。
3.HashSet允许有重复值么?
肯定是不允许的,因为HashSet的value部分是HashMap的key部分,因为HashMap的key本身就是无序不可重复的,所以HashSet也就不可能重复。
4.如果new两个值一样的字符串,往HashSet集合中添加,是否能添加进去?
是不可以加入进去的,因为在进行添加元素的时候会进行判断,通过hashCode方法和equals方法进行比对,String这个类,重写了这两个方法,比较的是字符串的值,而不是使用继承自Object的equlash和hashCode方法去进行比较。
5.HashSet是如何保证元素的唯一性的?
依赖于hashCode()和equals()这两个方法,所有在我们比较两个我们自定义的对象的时候,需要我们重写这两个方法,自定义比较规则,否则就是使用继承自Object的进行比对,比对的是对象的内存地址。
6.HashSet的add方法其实是调用的哪个方法?
其实调用的是HashMap的map.put方法。
7.HashSet是否是线程安全的呢?
HashSet是线程不安全的,所以呢?他的执行效率比较高,因为HashSet和HashMap的源代码中的方法都有没有加synchronized关键字。
那我们来看看哪些硬核的难点吧!
1.HashMap的value部分值是否相同?
都是相同的,因为value部分是使用了一个静态的Object对象进行占位,这个对象只是用于占位操作,并没有多大的实际意义。
private static final Object PRESENT = new Object();
2.HashMap的初始化容量是多大?是在什么时候进行初化容量?
初始化容量是16,是在第一次调用resize()方法的时候进行扩容的,并不是new HashMap方法的时候就进行扩容。
3.在计算HashMap的key的HashCode值的时候是单纯的时候hashCode方法计算出来的么?
不是,而是通过一个表达式进行计算后的结果(
(key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16)
),并不是单纯的hashCode值。
4.HashMap什么时候进行扩容?
底层数组超过临界值12的时候就会进行扩容,那么为什么不是到16才进行扩容呢?试下一下,他是一个线程不安全的集合,万一此时突突来了很多对象,要加入到这个集合,那么这个集合不就炸了么?扩容的机制就是:
当前数组容量乘以2再乘以加载因子0.75
每次添加元素的时候都会++Size,并不是说,这个数组中满了12个单向链表的时候进行扩容。
5.HashMap数组转红黑树需要满足那些条件?
首先判断该链表是否已经到达8个节点,如果满足该条件,再次进行判断这个数组链表的值是否大于64,如果小于64,还不会转化为红黑树,而是进行数组的扩容,大于64再转红黑树。
7.HashSet在添加重复元素的时候,具体是怎么进行判断该元素已经存在的?
进行equlas方法和HashCode方法进行比对,如果比对不出来再进行判断该链表是不是一颗红黑树,是的话进行红黑树的方式进行判断,如果不是,那么就遍历该链表,依次进行比对,如果比对到匹配的值,那么添加失败,如果没有比对到相等的值,那就把该元素添加到该链表的末尾。
8.使用HashSet集合的时候,为什么要重写HashCode和equlas方法?
因为底层添加元素的时候会调用这两个方法进行比对,而这个两个方法就是需要我们自定义比对规则,不然默认继承Object的。
源码分析,证明答案
new HashSet的源码:
//执行构造器 public HashSet() { map = new HashMap<>(); }
1.第一次调用add方法的源码分析:
// 第一次add方法的执行过程: // 2.add方法 :调用map的put方法 PRESENT:静态的一个Object对象 用于占位,每一个map的value都是用一个对象 * public boolean add(E e) { * return map.put(e, PRESENT)==null; //如果return null的时候就代表执行成功了 * } * // 调用hash方法获取到key的hash值 * 3. public V put(K key, V value) { * return putVal(hash(key), key, value, false, true); * } * // 通过hash算法获取的key的hash值 此hash值并不等于key原本的hash值 * static final int hash(Object key) { * int h; * return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); * } * * 4.得出hash值后 然后去putValue方法判断是否应该把这个值添加进去 * 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成立,调用resize() * if ((tab = table) == null || (n = tab.length) == 0) //table 其实就是HashMap里面的那个Node数组[] 存放链表的那个数组 * n = (tab = resize()).length; //resize())执行完后,返回一个初始化容量为16的table[]数组 * * // 通过key的hash值计算出元素应该存放到table数组的那个索引位置 * //并把这个位置的对象赋值给临时变量p,判断p是否为null * //如果p为空,代表这个位置还没有存放过元素,就创建一个node对象,key和value都放进去,next为null,留给第后来添加的元素存放Node对象 * if ((p = tab[i = (n - 1) & hash]) == null) * tab[i] = newNode(hash, key, value, null); * else { * 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) //判断当前这个table数组是否超过了12这个最大容量值,如果超过进行扩容 * resize(); * // 这个方法其实是一个空方法,是留给子类去实现的 * afterNodeInsertion(evict); * return null; //程序走到这儿,就代表我们第一次添加的元素已经成功了 * }
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { Node<K,V>[] tab; Node<K,V> p; int n, i; //只有第一次add的时候才会执行这个 if if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length; // 此时这个 方法为false 因为这次添加的元素是我们上次已经添加过的元素,所以算出来的下标1肯定是和上一次算出的下标一致 // 判断这个数组的下标位置中是否已经有链表元素存在 if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null); else { //添加重复值的时候执行: Node<K,V> e; K k; // 此时的这个p就是指向的上面算出来的数组下标里的那个Node对象 //如果当前索引位置对应的链表的第一个元素和准备添加的这个key的hash值hash值相同 if (p.hash == hash && //如果hash值相同的情况下 当前准备要加入的key和刚刚计算出来的数组下标对应的那个Node对象的key是同一个对象 或者 // 当前的这个key不为null然后在和计算出来的那个数组下标对应的那个Node对象里的key进行equals比较, //如果没有重写那么调用的就是继承自Object的equals方法,如果重写过,那么就调用重写后的,hashcode方法也是一样,所以建议两个方法都重写 ((k = p.key) == key || (key != null && key.equals(k)))) e = p; // 如果上面一个条件为假 再判断 这个p是不是一颗红黑树,如果是红黑树的话再按照红黑树的方式进行比较 // 如果是红黑树 调用:putTreeVal(); 方法进行添加 else if (p instanceof TreeNode) e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); else { // 假如不是红黑树,那就是第三种情况:按照上面第一个情况的方式依次和整个链表进行比较,如果找到条件满足的那就直接break(此元素已经存在); // 结束遍历,return oldValue 那么就代表着添加失败,如果说,比较完后都没有满足条件的(该元素不存在),那就挂载到这个链表的末尾 // 在把元素添加到最后,立即判断 该链表是否已经到达8个节点,如果到达,调用treeifyBin(tab, hash);方法把当前这个链表转化为红黑树 判断条件如下: if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY) resize(); // 上面条件不成立才进行树化 再进行转红黑树时还进行判断这个数组链表的值是否大于64,如果小于64,还不会转化为红黑树,而是进行数组的扩容,大于64再转红黑树 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; } // 如果 比的过程中找到一个值与准备添加的元素的值一致,那么就直接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); return null;
LinkedHashSet类也是根据元素的hashCode值来决定元素的存储位置,但它同时使用链表维护元素的次序。与HashSet相比,特点: