Java学习路线之集合(二)

目录

一、集合

       List、Set、Map之间的关系图

二、List

       Voctor、ArrayList、LinkedList 之间的区别

三、Set

       HashSet、TreeSet、LinkedHashSet 之间的区别

四、Map

       3.1 HashTable、HashMap、TreeMap、LinkedHashMap 之间的区别

       3.2 为什么 HashMap 的键值可以为 null,HashTable 不能为 null?

       3.3 HashMap 数据结构

       3.4 描述下 HashMap 的 put() 操作?

       3.5 HashMap 怎么实现线程安全?

       3.6 JDK 1.7,HashMap 是怎么扩容的?

       3.7 HashMap扩容时,为什么时2的冥次方,而且要再做一次 Hash 计算?

       3.8 JDK 1.7,多线程情况下 HashMap扩容会存在什么问题?如何解决?

       3.9 LinkedHashMap

       3.10 ConcurrentHashMap 底层原理?

       3.11 HashMap遍历的三种方法


一、集合

       List、Set、Map之间的关系图

集合关系

二、List

       Voctor、ArrayList、LinkedList 之间的区别

 VectorArrayListLinkedList
底层数据结构数组数组链表
默认初始容量1010 
加载因子11 
扩容后容量2016 
同步机制线程安全线程不安全 
优点 基于下标随机查询插入、删除速度快
缺点 插入、删除速度慢查询速度慢

三、Set

       HashSet、TreeSet、LinkedHashSet 之间的区别

 HashSetTreeSetLinkedHashSet
底层实现HashMapTreeMapLinkedHashMap
默认初始容量161616
加载因子0.750.750.75
扩容后容量323232
同步机制线程不安全线程不安全 

四、Map

       3.1 HashTable、HashMap、TreeMap、LinkedHashMap 之间的区别

 HashTableHashMapTreeMapLinkedHashMap
接口或类Dictionary 类Map 接口Map 接口Map 接口
底层数据结构散列表(数组+链表)散列表(数组+链表)数组+红黑树双向链表
默认初始容量11161616
加载因子0.750.750.750.75
扩容后容量23323232
同步机制线程安全线程不安全线程不安全线程不安全
key/value为null不允许允许允许允许
根据hash值计算数组下标的算法不同的hash值计算得到相同下标值的几率较高对 Key 的 hash 做移位运算和位的与运算,能更广泛地分散到数组的不同位置  
 两者都会重新根据Key的hash值计算其在数组中的新位置,重新放置。算法相似,时间、空间效率相同compareTo()函数实现元素有序和 HashMap 相似,维护循环双向链表

       3.2 为什么 HashMap 的键值可以为 null,HashTable 不能为 null?

          HashTable 通过 get() 获得 value 为 null 时,因为多线程的并发访问会修改值,导致无法判断 null 是值被设置为空还是从该关键字不存在。 而 Hash Map 是非线程安全的,通过 get() 获得的 value 为 null 或者 0 来判断。

 

       3.3 HashMap 数据结构

HashMap

 

       3.4 描述下 HashMap 的 put() 操作?

HashMap的put过程

               1、根据关键字 key 获得 Entry 对象的 hash 值判断当前 Entry 对象是否为 null

               2、为 null,直接插入数据,不为 null,遍历 Entry 对象所在的链表

               3、关键字 key 存在,覆盖已有数据;关键字 key 不存在,插入数据(头插法);

               4、链表长度大于8,改为红黑树插入。

               说明:HashMap 1.7 版本 resize()、put() 方法采用头插法, HashMap 1.8 版本的方法采用尾插法。

    // JDK 1.8 版本
    public V put(K key, V value) {
        if (table == EMPTY_TABLE) {
            inflateTable(threshold);
        }
        if (key == null)
            return putForNullKey(value);
        int hash = hash(key);
        int i = indexFor(hash, table.length);
        for (Entry<K,V> e = table[i]; e != null; e = e.next) {
            Object k;
            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
        }

        modCount++;
        addEntry(hash, key, value, i);
        return null;
    }

 

       3.5 HashMap 怎么实现线程安全?

               1、Collections.synchronizedMap()函数实现;  

               2、使用 HashTable;

               3、分段锁:CurrentHashMap。

 

       3.6 JDK 1.7,HashMap 是怎么扩容的?

          HashMap 的初始大小默认为16,加载因子为0.75,容量扩展为原来的2倍;HashMap 的容量必须是2的幂,因为性能,通过限制 HashMap 的容量是一个2的幂数,定位 Entry 在新 Hash Map 的位置时,h & (length-1) 和 h % length 结果是一致的,Java的%、/操作比&慢10倍左右。

          扩容的时候,当复制旧的 HashMap 到新的 HashMap 时,遇到哈希冲突时,采用头插法将冲突的 Entry 插入到链表中。

 

       3.7 HashMap扩容时,为什么时2的冥次方,而且要再做一次 Hash 计算?

          两者的目的都是为了减少哈希冲突

          1、假设扩容后为20时(非2的冥次方),length - 1 = 19,二进制为:10011;那么,h & (length-1) 的结果的二进制倒数第三位和第四位只能是0;会增加哈希冲突。

          2、不同的键的的 hash 值仅仅只能通过低位来区分,高位的信息没有被充分利用。极端情况就是:所有的 hash 值低位全相等,而高位不相等,通过一个再 Hash 来减少哈希冲突。

 

       3.8 JDK 1.7,多线程情况下 HashMap扩容会存在什么问题?如何解决?

          采用队头插入的方式,导致了HashMap在“多线程环境下”的死循环问题,JDK 1.8 采用尾插法,消除了 HashMap 多线程死循环的问题。

    void resize(int newCapacity) {
        Entry[] oldTable = table;
        int oldCapacity = oldTable.length;
        if (oldCapacity == MAXIMUM_CAPACITY) {//最大容量为 1 << 30
            threshold = Integer.MAX_VALUE;
            return;
        }

        Entry[] newTable = new Entry[newCapacity];//新建一个新表
        boolean oldAltHashing = useAltHashing;
        useAltHashing |= sun.misc.VM.isBooted() &&
                (newCapacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
        boolean rehash = oldAltHashing ^ useAltHashing;//是否再hash
        transfer(newTable, rehash);//完成旧表到新表的转移
        table = newTable;
        threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
    }

    void transfer(Entry[] newTable, boolean rehash) {
        int newCapacity = newTable.length;
        for (Entry<K,V> e : table) {//遍历同桶数组中的每一个桶
            while(null != e) {//顺序遍历某个桶的外挂链表
                Entry<K,V> next = e.next;//引用next
                if (rehash) {
                    e.hash = null == e.key ? 0 : hash(e.key);
                }
                int i = indexFor(e.hash, newCapacity);//找到新表的桶位置;原桶数组中的某个桶上的同一链表中的Entry此刻可能被分散到不同的桶中去了,有效的缓解了哈希冲突。
                e.next = newTable[i];//头插法插入新表中
                newTable[i] = e;
                e = next;
            }
        }
    }

       3.9 LinkedHashMap

              LinkedHashMap 和 HashMap 的操作基本一致,只是,LinkedHashMap 维护一个双向链表;

              LinkedHashMap 内部维护了一个双向链表,解决了 HashMap 不能随时保持遍历顺序和插入顺序一致的问题,LinkedHashMap 元素的访问顺序也提供了相关支持,也就是我们常说的 LRU(最近最少使用)原则。

    // LinkedHashMap 数据结构
    static class Entry<K,V> extends HashMap.Node<K,V> {
        Entry<K,V> before, after;
        Entry(int hash, K key, V value, Node<K,V> next) {
            super(hash, key, value, next);
        }
    }

 

       3.10 ConcurrentHashMap 底层原理?

               Java 1.7版本是通过分段锁机制来实现的,ConcurrentHashMap 初始有16个分段锁Segment,后期不能再进行扩容的。每个分段锁Segment都类似于一个HashMap,继承了重入锁ReentrantLock,有了锁的功能,由数组和链表组成,当链表的长度大于8时,转化为红黑树。

              Java 1.8 版本时通过数组 + 红黑树 + CAS (compare and swap) + sychronized 机制 组成,数组可以扩容。

    /** Implementation for put and putIfAbsent */
    final V putVal(K key, V value, boolean onlyIfAbsent) {
        if (key == null || value == null) throw new NullPointerException();
        int hash = spread(key.hashCode());
        int binCount = 0;
        for (Node<K,V>[] tab = table;;) {
            Node<K,V> f; int n, i, fh;
            if (tab == null || (n = tab.length) == 0)
                tab = initTable();
            else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
                if (casTabAt(tab, i, null,
                             new Node<K,V>(hash, key, value, null)))
                    break;                   // no lock when adding to empty bin
            }
            else if ((fh = f.hash) == MOVED)
                tab = helpTransfer(tab, f);
            else {
                V oldVal = null;
                synchronized (f) {
                    if (tabAt(tab, i) == f) {
                        if (fh >= 0) {
                            binCount = 1;
                            for (Node<K,V> e = f;; ++binCount) {
                                K ek;
                                if (e.hash == hash &&
                                    ((ek = e.key) == key ||
                                     (ek != null && key.equals(ek)))) {
                                    oldVal = e.val;
                                    if (!onlyIfAbsent)
                                        e.val = value;
                                    break;
                                }
                                Node<K,V> pred = e;
                                if ((e = e.next) == null) {
                                    pred.next = new Node<K,V>(hash, key,
                                                              value, null);
                                    break;
                                }
                            }
                        }
                        else if (f instanceof TreeBin) {
                            Node<K,V> p;
                            binCount = 2;
                            if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
                                                           value)) != null) {
                                oldVal = p.val;
                                if (!onlyIfAbsent)
                                    p.val = value;
                            }
                        }
                    }
                }
                if (binCount != 0) {
                    if (binCount >= TREEIFY_THRESHOLD)
                        treeifyBin(tab, i);
                    if (oldVal != null)
                        return oldVal;
                    break;
                }
            }
        }
        addCount(1L, binCount);
        return null;
    }

 

       3.11 HashMap遍历的三种方法

               方法1:使用 For-Each 迭代 entries;

Map<Integer, Integer> map = new HashMap<Integer, Integer>();
for(Map.Entry<Integer, Integer> entry : map.entrySet()){
	System.out.println("key = " + entry.getKey() + ", value = " + entry.getValue())
}

               方法2 使用For-Each迭代keys和values(具体代码未给出)

               方法3 使用Iterator迭代(具体代码未给出)

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值