zoukankan      html  css  js  c++  java
  • Java核心(一)——集合/ArrayList/HashMap

    iwehdio的博客园:https://www.cnblogs.com/iwehdio/

    学习自:

    1、Collection

    image-20210107204637501

    • 最上层的 Collection里定义了很多方法,这些方法也都会继承到各个子接口和实现类里。

    • 操作集合,无非就是「增删改查」四大类,也叫 CRUD:

      Create, Read, Update, and Delete。

    • 把这些 API 分为这四大类:

      功能 方法
      add()/addAll()
      remove()/ removeAll()
      Collection Interface 里没有
      contains()/ containsAll()
      其他 isEmpty()/size()/toArray()
    • 增:

      boolean add(E e);
      
      • add() 方法传入的数据类型必须是 Object,所以当写入基本数据类型的时候,会做自动装箱 auto-boxing 和自动拆箱 unboxing。
      • 还有另外一个方法 addAll(),可以把另一个集合里的元素加到此集合中。
      boolean addAll(Collection<? extends E> c);
      
    • 删:

      boolean remove(Object o);
      
      • remove()是删除的指定元素。
      • 那和 addAll() 对应的,自然就有removeAll(),就是把集合中的所有元素都删掉。
      boolean removeAll(Collection<?> c);
      
    • 改:Collection Interface 里并没有直接改元素的操作。

    • 查:

      • 查下集合中有没有某个特定的元素:
      boolean contains(Object o);
      
      • 查集合 A 是否包含了集合 B:
      boolean containsAll(Collection<?> c);
      
    • 集合整体操作:

      • 判断集合是否为空:
      boolean isEmpty();
      
      • 集合的大小:
      int size();
      
      • 把集合转成数组:
      Object[] toArray();
      

    List

    • List 最大的特点就是:有序可重复

    • List 的实现方式有 LinkedList 和 ArrayList 两种。

    • API时间复杂度:

      功能 方法 ArrayList LinkedList
      add(E e) O(1) O(1)
      add(int index, E e) O(n) O(n)
      remove(int index) O(n) O(n)
      remove(E e) O(n) O(n)
      set(int index, E e) O(1) O(n)
      get(int index) O(1) O(n)
      • add(E e) 是在尾巴上加元素,虽然 ArrayList 可能会有扩容的情况出现,但是均摊复杂度(amortized time complexity)还是 O(1) 的。
      • add(int index, E e)是在特定的位置上加元素,LinkedList 需要先找到这个位置,再加上这个元素,虽然单纯的「加」这个动作是 O(1) 的,但是要找到这个位置还是 O(n) 的。
      • 二者的选择:
        1. 改查选择 ArrayList;
        2. 增删在尾部的选择 ArrayList;
        3. 其他情况下,如果时间复杂度一样,推荐选择 ArrayList,因为 overhead 更小,或者说内存使用更有效率。
    • 其他API:

      • void trimToSize():将此 ArrayList 实例的容量调整为列表的当前大小。
      • int indexOf(Object o):返回此列表中首次出现的指定元素的索引,或如果此列表不包含元素,则返回 -1。
      • int lastIndexOf(Object o):返回此列表中最后一次出现的指定元素的索引,或如果此列表不包含索引,则返回 -1。
      • boolean contains(Object o):如果此列表中包含指定的元素,则返回 true。
    • ArrayList是什么?

      • ArrayList就是数组列表,主要用来装载数据,当我们装载的是基本类型的数据int,long,boolean,short,byte…的时候我们只能存储他们对应的包装类,它的主要底层实现是数组Object[] elementData。是线程不安全的。

      image-20210112192359810

      image-20210112192245764

    • 为啥线程 不安全还使用他呢?

      • 因为正常使用的场景中,都是用来查询,不会涉及太频繁的增删,如果涉及频繁的增删,可以使用LinkedList,如果你需要线程安全就使用Vector。
    • ArrayList的初始化:

      • ArrayList可以通过构造方法在初始化的时候指定底层数组的大小。
      • 通过无参构造方法的方式ArrayList()初始化,则赋值底层数Object[] elementData为一个默认空数组Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {}所以数组容量为0,只有真正对数据进行添加add时,才分配默认DEFAULT_CAPACITY = 10的初始容量。

      image-20210112192230918

      • new ArrayList()会将elementData 赋值为 DEFAULTCAPACITY_EMPTY_ELEMENTDATA,new ArrayList(0)会将elementData 赋值为 EMPTY_ELEMENTDATA,EMPTY_ELEMENTDATA添加元素会扩容到容量为1,而DEFAULTCAPACITY_EMPTY_ELEMENTDATA扩容之后容量为10
    • ArrayList的扩容:

      • 数组的长度是有限制的,而ArrayList是可以存放任意数量对象,长度不受限制。是通过扩容实现的。
      • 比如有一个长度为10的数组,现在要新增一个元素,发现已经满了。
      • 第一步会重新定义一个长度为10+10/2的数组也就是新增一个长度为15的数组。
      • 然后把原数组的数据,原封不动的复制到新数组中,这个时候再把指向原数的地址换到新数组。
    • ArrayList的默认数组大小为什么是10?

      • 据说是因为sun的程序员对一系列广泛使用的程序代码进行了调研,结果就是10这个长度的数组是最常用的最有效率的。
    • ArrayList在增删的时候是怎么做的?

      • 有指定index新增,也有直接新增的,在这之前他会有一步校验长度的判断ensureCapacityInternal**,就是说如果长度不够,是需要扩容的。

      image-20210112193532939

      • 调用grow扩容时,扩容为原来的1.5倍。如果原容量的1.5倍比minCapacity小,那么就扩容到minCapacity。

      image-20210112193642423

      • 校验之后,就是数组的copy。具体的原理是,如果要插入的位置是index,那就把index后的数组元素都复制一份,并且放在index+1处。然后再将设置index处的元素。

      image-20210112193906986

      • 效率低的原因也就是在此,即新增元素需要复制后边所有的元素。
    • ArrayList(int initialCapacity)会不会初始化数组大小size?

      • 不会初始化大小。因为这里只是初始化了底层数组的大小,但是并没有关联数组的大小size。
      • 而且此时并没有往ArrayList中添加数据,size也应该是0。只有添加元素,size才会变化。
    • ArrayList插入删除一定慢么?

      • 取决于你删除的元素离数组末端有多远,ArrayList拿来作为堆栈来用还是挺合适的,push和pop操作完全不涉及数据移动操作。
    • 删除怎么实现的呢?

      • 删除其实跟新增是一样的,不过叫是叫删除,但是在代码里面我们发现,他还是在copy一个数组。
      • 即如果要删除index处的元素,则复制index+1开始的数组,放到index处,相当于覆盖了index。

      image-20210112194818511

    • ArrayList是线程安全的么?

      • 不是,线程安全版本的数组容器是Vector。
      • Vector的实现很简单,就是把所有的方法统统加上synchronized。
      • 也可以不使用Vector,用Collections.synchronizedList把一个普通ArrayList包装成一个线程安全版本的数组容器也可以,原理同Vector是一样的,就是给所有的方法套上一层synchronized。
    • 不用Vector,线程安全的List还有什么?

      • 在java.util.concurrent包下还有一个类,叫做CopyOnWriteArrayList。是一个线程安全的List,底层是通过复制数组的方式来实现的。
      • 写入时复制CopyOnWrite机制:
        • 如果有多个调用者(callers)同时请求相同资源(如内存或磁盘上的数据存储),他们会共同获取相同的指针指向相同的资源。
        • 直到某个调用者试图修改资源的内容时,系统才会真正复制一份专用副本(private copy)给该调用者,而其他调用者所见到的最初的资源仍然保持不变。
        • 优点是如果调用者没有修改该资源,就不会有副本(private copy)被建立,因此多个调用者只是读取操作时可以共享同一份资源。
      • add()方法的实现:加lock锁(ReentrantLock),然后会复制出一个新的数组,往新的数组里边add真正的元素,最后把array的指向改变为新的数组。
      • get()方法又或是size()方法只是获取array所指向的数组的元素或者大小。读不加锁,写加锁。
      • CopyOnWriteArrayList很耗费内存的,每次set()/add()都会复制一个数组出来。
      • CopyOnWriteArrayList只能保证数据的最终一致性,不能保证数据的实时一致性。假设两个线程,线程A去读取CopyOnWriteArrayList的数据,还没读完,现在线程B把这个List给清空了,线程A此时还是可以把剩余的数据给读出来。
    • ArrayList用来做队列合适么?

      • 队列一般是FIFO(先入先出)的,如果用ArrayList做队列,就需要在数组尾部追加数据,数组头部删除数组,反过来也可以。
      • 但是无论如何总会有一个操作会涉及到数组的数据搬迁,这个是比较耗费性能的。所以ArrayList不适合做队列。
    • 那数组适合用来做队列么?

      • 数组是非常合适的。
      • 比如ArrayBlockingQueue内部实现就是一个环形队列,它是一个定长队列,内部是用一个定长数组来实现的。
      • 简单点说就是使用两个偏移量来标记数组的读位置和写位置,如果超过长度就折回到数组开头,前提是它们是定长数组。
    • ArrayList和LinkedList性能比较如何?

      • 论遍历ArrayList要比LinkedList快得多,ArrayList遍历最大的优势在于内存的连续性,CPU的内部缓存结构会缓存连续的内存片段,可以大幅降低读取内存的性能开销。
      • ArrayList的增删底层调用的copyOf()被优化过,现代CPU对内存可以块操作,ArrayList的增删一点儿也不会比LinkedList慢。
    • 为什么elementData没有用private修饰呢?

      • private修饰会影响内部类的访问。
    • Vector:

      • Vector 和 ArrayList 一样,也是继承自 java.util.AbstractList,底层也是用数组来实现的。但是现在已经被弃用了,因为它加了太多的 synchronized,导致效率降低。
      • ArrayList 的扩容实现是将新容量变为原容量的1.5倍。
      • Vector 的扩容实现是,先看有没有定义扩容量 capacityIncrement,没定义则扩容为原来的2倍。

    Queue & Deque

    • Queue有两组 API,基本功能是一样的,但是呢:

      • 一组是会抛异常的;
      • 另一组会返回一个特殊值。
      功能 抛异常 返回值
      add(e) offer(e)
      remove() poll()
      element() peek()
    • 为什么会抛异常呢?

      • 比如队列空了,那 remove() 就会抛异常,但是 poll() 就返回 null;element() 就会抛异常,而 peek() 就返回 null 。
    • Deque 是两端都可以进出的,那自然是有针对 First 端的操作和对 Last 端的操作,那每端都有两组,一组抛异常,一组返回特殊值:

      功能 抛异常 返回值
      addFirst(e)/ addLast(e) offerFirst(e)/ offerLast(e)
      removeFirst()/ removeLast() pollFirst()/ pollLast()
      getFirst()/ getLast() peekFirst()/ peekLast()
    • 实现类有三个:LinkedListArrayDequePriorityQueue

      • 如果想实现「普通队列 - 先进先出」的语义,就使用 LinkedList 或者 ArrayDeque 来实现;
      • 如果想实现「优先队列」的语义,就使用 PriorityQueue;
      • 如果想实现「栈」的语义,就使用 ArrayDeque。
    • 在实现普通队列时,如何选择用 LinkedList 还是 ArrayDeque 呢?

      • 推荐使用 ArrayDeque,因为效率高,而 LinkedList 还会有其他的额外开销(overhead)。
    • ArrayDeque 和 LinkedList 的区别有哪些呢?

      1. ArrayDeque 是一个可扩容的数组,LinkedList 是双向链表结构;
      2. ArrayDeque 里不可以存 null 值,但是 LinkedList 可以;
      3. ArrayDeque 在操作头尾端的增删操作时更高效,但是 LinkedList 只有在当要移除中间某个元素且已经找到了这个元素后的移除才是 O(1) 的;
      4. ArrayDeque 在内存使用方面更高效。
    • 什么情况下要选择用 LinkedList 呢?

      • Java 6 以前。因为 ArrayDeque 在 Java 6 之后才有的。
    • Stack:

      • Java 中有 Stack 这个类,但是呢,官方文档都说不让用了。因为 Vector 已经过被弃用了,而 Stack 是继承 Vector 的。
      • 想实现 Stack 的语义,就用 ArrayDeque 。

    Set

    • Set 的特点是无序不重复的。

    • Set 的常用实现类有三个:

      • HashSet: 采用 Hashmap 的 key 来储存元素,主要特点是无序的,基本操作都是 O(1) 的时间复杂度,很快。
      • LinkedHashSet: 这个是一个 HashSet + LinkedList 的结构,特点就是既拥有了 O(1) 的时间复杂度,又能够保留插入的顺序。
      • TreeSet: 采用红黑树结构,特点是可以有序,可以用自然排序或者自定义比较器来排序;缺点就是查询速度没有 HashSet 快。
    • 每个 Set 的底层实现其实就是对应的 Map:

      • 数值放在 map 中的 key 上,value 上放了个 PRESENT,是一个静态的 Object,相当于 place holder,每个 key 都指向这个 object。

    2、Map

    • Map 也有这三个实现类:

      • HashMap: 与 HashSet 对应,也是无序的,O(1)。
      • LinkedHashMap: 这是一个「HashMap + 双向链表」的结构,落脚点是 HashMap,所以既拥有 HashMap 的所有特性还能有顺序。
      • TreeMap: 是有序的,本质是用二叉搜索树来实现的。
    • HashMap基本操作:

      • 增:put(K key, V value)
      • 删:remove(Object key)
      • 改:还是用的 put(K key, V value)
      • 查:get(Object key) / containsKey(Object key)
      • 为什么有些 key 的类型是 Object,有些是 K 呢?
        • 这是因为如果重写了equals()和HashCode(),在 get/remove 的时候,不一定是用的同一个 object。
    • HashMap的实现原理:

      • 对于 HashMap 中的每个 key,首先通过hashCode函数计算出哈希值,然后通过hash函数(最基本的是模于数组的长度),将哈希值转换为数组的地址。
      • 数组里面每个地方都存了Key-Value这样的实例,在Java7叫Entry在Java8中叫Node。是HashMap中的一个静态内部类,实现了Map.Entry接口。

      image-20210107220831982

    • HashMap 中是如何保证元素的唯一性?即相同的元素会不会算出不同的哈希值呢?

      • 通过 hashCode() 和 equals() 方法来保证元素的唯一性。
      • 如果 key 的 hashCode() 值相同,那么有可能是要发生 hash collision 了,也有可能是真的遇到了另一个自己。那么如何判断呢?继续用 equals() 来比较。
        • hashCode() 决定了 key 放在这个桶里的编号,也就是在数组里的 index;
        • equals() 是用来比较两个 object 是否相同的。
    • 为什么重写 equals() 方法,一定要重写 hashCode() 呢?

      • 对于hashCode()而言,不同的对象会返回不同的哈希值。
      • 那么在这个条件下,有两个对象是equals的,那如果不重写 hashCode(),算出来的哈希值都不一样,就会去到不同的 buckets 了。而get的时候,也找不到对应bucket中的equals的对象。
      • hashCode() 和 equals() 方法都是在 Object类中定义的。
        • 在Object中,equals() 方法就是比较这两个 references 是否指向了同一个 object,即与==一样。
        • 在Object中声明,是为了让继承Object的类重写。
        • hashCode() 返回的并不一定是对象的(虚拟)内存地址,具体取决于运行时库和JVM的具体实现。
      • 所以,HashMap中的key如果是Object类型,需要重写hashCode和equals方法。
        • hashCode用于确定存储数据的位置,实现不当会导致严重的哈希冲突。
        • equals用于比较存储位置上是否存在这个key。
    • 如果不同的元素算出了相同的哈希值,即多个 key 对应了同一个桶。就是发生了哈希碰撞。

      • 哈希碰撞一般有两类解决方式:

        • Separate chaining独立链和Open addressing开放寻址。
        • Java中使用的是第一种 Separate chaining,即在发生碰撞的那个桶后面再加一条“链”来存储。

        image-20210108193422690

      • 对于数组中存放的每一个节点,都会保存自身的hash、key、value、以及下个节点:

        image-20210108192243749

    • 在多线程环境下,HashMap有可能会有数据丢失和获取不了最新数据的问题。

    • 新的Entry节点在插入链表的时候,是怎么插入的?

      • java8之前是头插法,就是说新来的值会取代原有的值,原有的值就顺推到链表中去。因为写这个代码的作者认为后来的值被查找的可能性更大一点,提升查找的效率。

      • 但是,在java8之后,都是用尾部插入了。因为头插法在多线程并发扩容时,会导致死循环(如果获取哈希值相同但是不存在的键)。

      • 在java7中为什么出现死循环?

        • 场景:多线程操作一个HashMap,并且并发扩容。
        • 问题主要出现在将旧Entry数组中的元素移到新Entry数组的transfer()方法中。
        • 如果线程A在do循环的第四句时停住了,等到线程B扩容完成才开始执行。本来应该从链表的第一个元素开始移动,但是线程B扩容时使用了头插法,导致原本的第一个元素变为了最后一个。
        • 注意根据Java内存模型,在线程B完成后,线程A获取到达都是最新的newTable,但是e和next还是之前的。
        void transfer(Entry[] newTable)
        {
            Entry[] src = table;
            int newCapacity = newTable.length;
            //  从OldTable里摘一个元素出来,然后放到NewTable中
            for (int j = 0; j < src.length; j++) {
                Entry<K,V> e = src[j];
                if (e != null) {
                    src[j] = null;
                    do {
                        Entry<K,V> next = e.next;	
                        int i = indexFor(e.hash, newCapacity);
                        e.next = newTable[i];
                        newTable[i] = e;	//如果线程A在这里停住了
                        e = next;
                    } while (e != null);
                }
            }
        }
        
        • 这样,线程A就会把原本的第一个元素插入两次,导致死循环。在有些情况下还会导致数据丢失。

        image-20210108202226270

      • 总之,是因为头插法改变了链表的顺序,如果其他线程获取的是扩容前的顺序关系,就会出现死循环。

      • 在Java 8中使用尾插法,不会出现循环链表,但是在put时可能会出现数据覆盖,还是线程不安全的。

    • HashMap的扩容机制:

      • 如果 pairs 太多,buckets 太少怎么破?

        • 扩容。 也就是碰撞太多的时候,会把数组扩容至两倍(默认)。所以这样虽然 hash 值没有变,但是因为数组的长度变了,所以算出来的 index 就变了,就会被分配到不同的位置上了。
      • 什么时候会 resize 呢?也就是怎么衡量桶里是不是足够拥挤要扩容了呢?

        • 有两个因素:

          • Capacity:HashMap当前长度。
          • LoadFactor:负载因子,默认值0.75f。

          image-20210108193044981

        • 即用 pair 的数量除以 buckets 的数量,也就是平均每个桶里装几对。Java 中默认值是 0.75f,如果超过了这个值就会 rehashing。

      • 怎么扩容的呢?

        • 扩容:创建一个新的Entry空数组,长度是原数组的2倍。
        • Rehash:遍历原Entry数组,把所有的Entry重新Hash到新数组。
      • 为什么要重新Hash呢,而不是直接复制过去?

        • 是因为长度扩大以后,Hash的规则也随之改变。Hash的公式---> index = HashCode(Key) & (Length - 1)
        • 原来长度(Length)是8你位运算出来的值是2 ,新的长度是16你位运算出来的值明显不一样了。

        image-20210108193947075

      • java 7 是先扩容再插入,java 8 是先插入再扩容。

    • 为什么HashMap的默认初始化长度是16?

      image-20210108202906869

      • 在创建HashMap的时候,阿里巴巴规范插件会提醒我们最好赋初值,而且最好是2的幂。16可能是考量了扩容可能和空间的经验值。

      • 首先,这样是为了位运算的方便,位与运算比算数计算的效率高了很多。

      • 其次,是为了服务将Key均匀的映射到index的算法(即hash & (length-1))。因为对于2的幂,length-1是全为1,这样数组的索引就是hash值的低位,如果哈希值是均匀的,则数组也是均匀分布的。

        image-20210108204331828

      • 最后,在扩容时,新容量默认是原容量的二倍,这样就只有最高位不同,原数组中的一些元素的索引位置不变。

        image-20210108204318619
      • 为什么默认加载因子是0.75?

        • 首先,0.75 * 16=12是一个整数,大于16的就更是了。
        • 其次,是一种查询效率和空闲效率的折中。
          • 加载因子过高,虽然减少了空间开销,提高了空间利用率,但同时哈希冲突的概率增加,增加了查询时间成本。
          • 加载因子过低,虽然可以减少查询时间成本,但是空间利用率很低,同时提高了rehash操作的次数。
    • HashMap指定容量初始化:

      • 当我们通过HashMap(int initialCapacity)设置初始容量的时候,HashMap并不一定会直接采用我们传入的数值,而是经过计算,得到一个新值,目的是提高hash的效率。(1->1、3->4、7->8、9->16)。
      • 在JDK 1.7和JDK 1.8中,HashMap初始化这个容量的时机不同。JDK 1.8中,在调用HashMap的构造函数定义HashMap的时候,就会进行容量的设定。而在JDK 1.7中,要等到第一次put操作时才进行这一操作。
      • HashMap根据用户传入的初始化容量,利用无符号右移和按位或运算等方式计算出第一个大于该数的2的幂。
    • HashMap中的hash()方法:

      • 在同一个版本的Jdk中,HashMap、HashTable以及ConcurrentHashMap里面的hash方法的实现是不同的。不同的版本的JDK中(Java7 和 Java8)中也是有区别的。

      • hash方法的功能是根据Key来定位这个K-V在链表数组中的位置的。也就是hash方法的输入应该是个Object类型的Key,输出应该是个int类型的数组下标。

      • 基本原理:只要调用Object对象的hashCode()方法,该方法会返回一个整数,然后用这个数对HashMap或者HashTable的容量进行取模就行了。

      • HashMap In Java 7:

        final int hash(Object k) {
           int h = hashSeed;
           if (0 != h && k instanceof String) {
               return sun.misc.Hashing.stringHash32((String) k);
           }
        	//扰动
           h ^= k.hashCode();
           h ^= (h >>> 20) ^ (h >>> 12);
           return h ^ (h >>> 7) ^ (h >>> 4);
        }
        
        static int indexFor(int h, int length) {
           return h & (length-1);
        }
        
        • 由于HashMap使用位运算代替了取模运算,这就带来了另外一个问题,那就是有可能发生冲突。比如:CA11 10000001 1000在对0000 1111进行按位与运算后的值是相等的。
        • 对key的hashCode进行扰动计算,防止不同hashCode的高位不同但低位相同导致的hash冲突。简单点说,就是为了把高位的特征和低位的特征组合起来,降低哈希冲突的概率,也就是说,尽量做到任何一位的变化都能对最终得到的结果产生影响。
      • HashTable In Java 7:

        private int hash(Object k) {
           // hashSeed will be zero if alternative hashing is disabled.
           return hashSeed ^ k.hashCode();
        }
        //起了indexFor方法的作用
        int index = (hash & 0x7FFFFFFF) % tab.length;
        
        • 把hash值和0x7FFFFFFF做一次按位与操作呢,主要是为了保证得到的index的的二进制的第一位为0(一个32位的有符号数和0x7FFFFFFF做按位与操作,其实就是在取绝对值。),也就是为了得到一个正数。因为有符号数第一位0代表正数,1代表负数。
        • HashTable简单的取模是有一定的考虑在的。涉及到HashTable的构造函数和扩容函数:
          • HashTable默认的初始大小为11,之后每次扩充为原来的2n+1。
          • 也就是说,HashTable的链表数组的默认大小是一个素数、奇数。之后的每次扩充结果也都是奇数。
          • 由于HashTable会尽量使用素数、奇数作为容量的大小。当哈希表的大小为素数时,简单的取模哈希的结果会更加均匀(可以证明)。
      • 比较:

        • 当哈希表的大小为素数时,简单的取模哈希的结果会更加均匀,所以单从这一点上看,HashTable的哈希表大小选择,似乎更高明些。因为hash结果越分散效果越好。
        • 在取模计算时,如果模数是2的幂,那么我们可以直接使用位运算来得到结果,效率要大大高于做除法。所以从hash计算的效率上,又是HashMap更胜一筹。
        • 但是,HashMap为了提高效率使用位运算代替哈希,这又引入了哈希分布不均匀的问题,所以HashMap为解决这问题,又对hash算法做了一些改进,进行了扰动计算。
      • ConcurrentHashMap In Java 7:

        private int hash(Object k) {
           int h = hashSeed;
        
           if ((0 != h) && (k instanceof String)) {
               return sun.misc.Hashing.stringHash32((String) k);
           }
        
           h ^= k.hashCode();
        
           // Spread bits to regularize both segment and index locations,
           // using variant of single-word Wang/Jenkins hash.
           h += (h <<  15) ^ 0xffffcd7d;
           h ^= (h >>> 10);
           h += (h <<   3);
           h ^= (h >>>  6);
           h += (h <<   2) + (h << 14);
           return h ^ (h >>> 16);
        }
        
        int j = (hash >>> segmentShift) & segmentMask;
        
        • 上面这段关于ConcurrentHashMap的hash实现其实和HashMap如出一辙。都是通过位运算代替取模,然后再对hashcode进行扰动。区别在于,ConcurrentHashMap 使用了一种变种的Wang/Jenkins 哈希算法,其主要目的也是为了把高位和低位组合在一起,避免发生冲突。
      • HashMap In Java 8:

        static final int hash(Object key) {
           int h;
           return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
        }
        
        • 原理和Java 7中基本类似。Java 8中这一步做了优化,只做一次16位右位移异或混合,而不是四次,但原理是不变的。
        • 优化了高位运算的算法,通过hashCode()的高16位异或低16位实现的。主要是从速度、功效、质量来考虑的。
        • 认为引入红黑树后,即使哈希冲突比较严重,寻址效率也足够高,所以并未在哈希值的计算上做过多设计。
      • HashTable In Java 8:和Java 7中的实现几乎无差别。

      • ConcurrentHashMap In Java 8:将Key的hashCode值与其高16位作异或并保证最高位为0(从而保证最终结果为正整数)。

    • 为什么java8中使用了红黑树:

      • 如果只使用链表,在哈希碰撞很严重的情况下,这个链表可能会很长,那么put/get操作都可能需要遍历这个链表。也就是说时间复杂度在最差情况下会退化到O(n)。
      • 红黑树可以将这种极端情况下的查询复杂度降低到O(log n)。
    • 为什么Hashmap中的链表大小超过八个时会自动转化为红黑树,当删除小于六时重新变为链表?

      • 首先,桶中元素的个数符合泊松分布。根据泊松分布,在负载因子默认为0.75的时候,单个hash槽内元素个数大于8的概率小于百万分之一。这就是说红黑树只是面对极端情况下的。
      • 其次,由于树节点的大小大约是常规节点的两倍,因此我们仅在容器包含足够的节点以保证使用时才使用它们,当它们变得太小(由于移除或调整大小)时,它们会被转换回普通的链表。
      • 最后,如果元素小于等于6树还原转为链表,大于等于8转为树),中间有个差值7可以有效防止链表和树频繁转换。
    • 此外,链表转为红黑树还需要散列表底层数组长度大于64才行。

    • 为什么 HashMap 中 String、Integer 这样的包装类适合作为 key 键:

      • 是final类型,具有不可变性。保证了key的不可更改,不会出现放入和获取时哈希值不同。
      • 内部重写了equals和hashCode方法,不容易出现哈希碰撞。
    • HashMap的k-v都允许为null:

      • key为null是唯一的,因为key为1,哈希值默认为0。
      • 值可以有多个null。
    • HashMap不保证有序,而且存储顺序可能变化:

      • 存储顺序是根据hash值得到的数组下标,讲求随机和均匀性。
      • 存在扩容操作,可能会导致存储位置变化。

    iwehdio的博客园:https://www.cnblogs.com/iwehdio/
  • 相关阅读:
    Python for Data Science
    Python for Data Science
    Python for Data Science
    Python for Data Science
    Python for Data Science
    Python for Data Science
    Python for Data Science
    Python for Data Science
    Python for Data Science
    软件工程实践总结
  • 原文地址:https://www.cnblogs.com/iwehdio/p/14269208.html
Copyright © 2011-2022 走看看