Java基础(3)|Collection

Java基础(3)|Collection

目录

1、Collection接口继承树

Java基础(3)|Collection

2、基本操作

  • add(Object o):增加元素
  • addAll(Collection c):...
  • clear():...
  • contains(Object o):是否包含指定元素
  • containsAll(Collection c):是否包含集合c中的所有元素
  • iterator():返回Iterator对象,用于遍历集合中的元素
  • remove(Object o):移除元素
  • removeAll(Collection c):相当于减集合c
  • retainAll(Collection c):相当于求与c的交集
  • size():返回元素个数
  • toArray():把集合转换为一个数组

3、Collection的遍历

Collection的遍历可以使用Iterator接口或者是foreach循环来实现

4、Set

Set集合不允许包含相同的元素,而判断两个对象是否相同则是根据对象的hashcode和equals方法。

4.1、HashSet

带着问题去学习

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;

4.2、LinkedHashSet

LinkedHashSet类也是根据元素的hashCode值来决定元素的存储位置,但它同时使用链表维护元素的次序。与HashSet相比,特点:

  1. 对集合迭代时,按增加顺序返回元素。
  2. 性能略低于HashSet,因为需要维护元素的插入顺序。但迭代访问元素时会有好性能,因为它采用链表维护内部顺序。
上一篇:GBase 8a Mpp Cluster集群产品性能优化篇之关联顺序


下一篇:剑指 Offer 59 - II. 队列的最大值