Java集合框架(黄图是思路)

先看红线

 

Vector 和 Stack

Stack:继承Vector,基于动态数组实现的一个线程安全的栈;

     有同步          

public synchronized E peek();

返回栈顶的值;

public E push(E item);

入栈操作;

public synchronized E pop();

出栈操作;

Vector:随机访问速度快,插入和移除性能较差(数组的特点);支持null元素;有顺序;元素可以重复;线程安全;

 

Vector 和 ArrayList 

Vector与ArrayList基本是一致的,不同的是Vector是线程安全的,会在可能出现线程安全的方法前面加上synchronized关键字;

 

Vector 和 AbstractList

Vector 继承于 AbstractList

 

AbstractList 和 AbstractCollection 和 List

 

 

AbstractCollection  和 Collection

 

 

 

ArrayList 和 LinkedList的区别?

常考点:

ArrayList :底层是基于动态数组(可以动态的扩容),根据下标随机访问数组的元素的效率高,向数组尾部添加元素的效率高

但是 删除数组中的元素以及向数组中间添加数据效率低,因为需要移动数组

ArrayList的扩容机制:

default ca'pacity 默认容量是10

可以自己定义 刚开始的ArrayList的容量大小

直接调用ArrayList的构造方法创建的是空的集合

扩容的新的容量是原来的1.5倍

 

LinkedList 

LinkedList的底层数据结构是双向链表

LinkedList继承于AbstractSequentialList,实现List接口,因此也可以对其进行队列操作,它也实现了Deque接口,所以LinkedList也可当做双端队列使用,还有LinkedList是非同步的。

 

从源码上可以非常清楚的了解LinkedList加入元素是直接放在链表尾的,主要点构成双向链表

由于双向链表,顺序访问效率高,而随机访问效率较低。

 

 

HashSet和TreeSet

HashSet继承于  AbstractSet

Set的概念:

Set可以理解为集合,非常类似数据概念中的集合,集合三大特征:1、确定性;2、互异性;3、无序性,因此Set实现类也有类似的特征。

HashSet和HashMap的关系?

HashSet内部使用HashMap来存储数据,数据存储在HashMap的key中,value都是同一个默认值:

 

LinkedHashSet和HashSet的关系?

HashSet和HashMap都不保证顺序,LinkedHashSet能保证顺序(引出下一个问题)。

LinkedHashSet继承于HashSet

 

为什么LinkedHashSet是有序的?

LinkedHashSet是一个哈希表和链表的结合,而且还是一个双向链表。LinedHashSet内部维护了个LinkedList来维护元素的顺序

 

HashSet和TreeSet的关系?(一个无序,一个有序)

当向HashSet集合中存入一个元素时,HashSet会调用该对象的hashCode()方法来得到该对象的hashCode值,然后根据 hashCode值来决定该对象在HashSet中存储位置。所以存储位置是随机的(跟HashMap的存储原理一样)

当再次向HashSet集合中插入数据时,先根据hashcode计算出存储的位置,然后根据equals判断两个对象是否是相等的

HashSet可以存储null值,由于两个null值的hashcode(hashcode根据内容求值)一致,调用equals也一样,判定是同一个数据,所以只能存储一个null值

 

TreeSet的两个特点:Tree 表示有序,Set表示唯一

TreeSet在使用二叉树存储对象,对象必须要实现compareTo方法,判断对象是不是同一个就是看该方法返回的值是否为0

 

SortedSet 和 TreeSet

SortedSet 继承于Set,同时又提供了一些功能增强的方法,比如 comparator 从而实现了元素的有序性。

SortedSet 插入到有序集中的所有元素必须实现Comparable接口(或者被指定的Comparator接受),并且所有这些元素必须是可相互比较的,比如:e1.compareTo(e2)

TreeSet是SortedSet的唯一实现类,红黑树实现,树形结构,它的本质可以理解为是有序,无重复的元素的集合。

上面提到了Comparable和Comparator

Comparable和Comparator的区别?

Comaarable是类可以实现的接口,实现了该接口,就必须实现compareTo方法   

如果类没有实现Comparable接口,又想对两个类进行比较。或者对实现类实现了Comparble接口,但是对compareTo方法里的算法不满意,那么可以实现Comparator接口,自定义一个比较器,写比较算法

实现Comparable接口的方式比实现Comparator接口的耦合性 要强一些,如果要修改比较算法,要修改Comparable接口的实现类,而实现Comparator的类是在外部进行比较的,不需要对实现类有任何修 改。

 

 

LinkedHashMap 与 HashMap

LinkedHashMap 继承于HashMap

 

 

LinkedHashMap的构造方法有三种:

       

 

查看LinedHashMap源码之前,建议先查看HashMap的源码

HashMap的基本原理:

HashMap 使用了拉链式的散列算法,并在jdk1.8中引入了红黑树优化过长的链表

ps:  为什么叫做拉链式的算法,仔细看下面这张图,认真想

发现里面定义了很多的常量

 : 当前 HashMap 所能容纳键值对数量的最大值,超过这个值,则需扩容

 :默认初始容量 16 

 :最大的容量

  默认的加载因子 0.75

 链表转化成二叉树的阈值 : 8

:二叉树转成链表的阈值 :6

 :转化成二叉树的最小容量(桶)为 64

默认情况下,HashMap 初始容量是16,负载因子为 0.75。这里并没有默认阈值,原因是阈值可由容量乘上负载因子计算而来(注释中有说明),即threshold = capacity * loadFactor

HashMap的查找源码:

final Node<K,V> getNode(int hash, Object key) {
        Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
        // 1. 定位键值对所在桶的位置
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (first = tab[(n - 1) & hash]) != null) {
            if (first.hash == hash && // always check first node
                ((k = first.key) == key || (key != null && key.equals(k))))
                return first;
            if ((e = first.next) != null) {
                if (first instanceof TreeNode)
                // 2. 如果 first 是 TreeNode 类型,则调用黑红树查找方法    
                    return ((TreeNode<K,V>)first).getTreeNode(hash, key);
                // 3. 对链表进行查找
                do {
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        return e;
                } while ((e = e.next) != null);
            }
        }
        return null;
    }

查找的核心逻辑是封装在 getNode 方法中的,getNode 方法源码有一些注释,应该不难看懂。我们先来看看查找过程的第一步 - 确定桶位置,其实现代码如下:

n -1 与 hash 的运算的结果等价于 对 length取余,这样就可以找到桶的位置

举个例子说明一下吧,假设 hash = 185,n = 16。计算过程示意图如下:

还有一个计算 hash 的方法。这个方法源码如下:

看这个方法的逻辑好像是通过位运算重新计算 hash,那么这里为什么要这样做呢?为什么不直接用键的 hashCode 方法产生的 hash 呢?

这样做有两个好处,我来简单解释一下。我们再看一下上面求余的计算图,图中的 hash 是由键的 hashCode 产生。计算余数时,由于 n 比较小,hash 只有低4位参与了计算,高位的计算可以认为是无效的。这样导致了计算结果只与低位信息有关,高位数据没发挥作用。为了处理这个缺陷,我们可以上图中的 hash 高4位数据与低4位数据进行异或运算,即 hash ^ (hash >>> 4)。通过这种方式,让高位数据与低位数据进行异或,以此加大低位信息的随机性,变相的让高位数据参与到计算中。此时的计算过程如下:

在 Java 中,hashCode 方法产生的 hash 是 int 类型,32 位宽。前16位为高位,后16位为低位,所以要右移16位。上面所说的是重新计算 hash 的一个好处,除此之外,重新计算 hash 的另一个好处是可以增加 hash 的复杂度。当我们覆写 hashCode 方法时,可能会写出分布性不佳的 hashCode 方法,进而导致 hash 的冲突率比较高。通过移位和异或运算,可以让 hash 变得更复杂,进而影响 hash 的分布性。这也就是为什么 HashMap 不直接使用键对象原始 hash 的原因了。

HashMap的插入逻辑

插入操作的源码

public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
               boolean evict) {
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    // 初始化桶数组 table,table 被延迟到插入新数据时再进行初始化
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    // 如果桶中不包含键值对节点引用,则将新键值对节点的引用存入桶中即可
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    else {
        Node<K,V> e; K k;
        // 如果键的值以及节点 hash 等于链表中的第一个键值对节点时,则将 e 指向该键值对
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            e = p;
            
        // 如果桶中的引用类型为 TreeNode,则调用红黑树的插入方法
        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;
                }
                
                // 条件为 true,表示当前链表包含要插入的键值对,终止遍历
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                p = e;
            }
        }
        
        // 判断要插入的键值对是否存在 HashMap 中
        if (e != null) { // existing mapping for key
            V oldValue = e.value;
            // onlyIfAbsent 表示是否仅在 oldValue 为 null 的情况下更新键值对的值
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            afterNodeAccess(e);
            return oldValue;
        }
    }
    ++modCount;
    // 键值对数量超过阈值时,则进行扩容
    if (++size > threshold)
        resize();
    afterNodeInsertion(evict);
    return null;
}

putVal 方法主要做了这么几件事情:

  1. 当桶数组 table 为空时,通过扩容的方式初始化 table
  2. 查找要插入的键值对是否已经存在,存在的话根据条件判断是否用新值替换旧值
  3. 如果不存在,则将键值对链入链表中,并根据链表长度决定是否将链表转为红黑树
  4. 判断键值对数量是否大于阈值,大于的话则进行扩容操作

 

jdk 1.7版本采用的是头插法,在多个线程执行transfer方法的时候,会导致循环的链表,在get数据的时候,就会进入一个死循环

查看1.7的源码

jdk 1.8版本采用的尾插法,然后引入了红黑树这么一个概念

 

 

一会 entry 一会 node 我改用哪个??

在jdk1.7中查看实现Map接口HashMap类可以发现,实现内部接口Map.Entry的类为静态内部类Entry,而在1.8的版本中实现Map.entry的类为Node,如图:

 

 

扩容的操作

 

从上图可以发现,重新映射后,两条链表中的节点顺序并未发生变化,还是保持了扩容前的顺序。以上就是 JDK 1.8 中 HashMap 扩容的代码讲解。另外再补充一下,JDK 1.8 版本下 HashMap 扩容效率要高于之前版本。如果大家看过 JDK 1.7 的源码会发现,JDK 1.7 为了防止因 hash 碰撞引发的拒绝服务攻击,在计算 hash 过程中引入随机种子。以增强 hash 的随机性,使得键值对均匀分布在桶数组中。在扩容过程中,相关方法会根据容量判断是否需要生成新的随机种子,并重新计算所有节点的 hash。而在 JDK 1.8 中,则通过引入红黑树替代了该种方式。从而避免了多次计算 hash 的操作,提高了扩容效率。

 

链表树化、红黑树链化与拆分

 

当桶数组容量小于该值MIN_TREEIFY_CAPACITY 时,优先进行扩容,而不是树化

static final int MIN_TREEIFY_CAPACITY = 64;
 
/**
 * 将普通节点链表转换成树形节点链表
 */
final void treeifyBin(Node<K,V>[] tab, int hash) {
    int n, index; Node<K,V> e;
    // 桶数组容量小于 MIN_TREEIFY_CAPACITY,优先进行扩容而不是树化
    if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
        resize();
    else if ((e = tab[index = (n - 1) & hash]) != null) {
        // hd 为头节点(head),tl 为尾节点(tail)
        TreeNode<K,V> hd = null, tl = null;
        do {
            // 将普通节点替换成树形节点
            TreeNode<K,V> p = replacementTreeNode(e, null);
            if (tl == null)
                hd = p;
            else {
                p.prev = tl;
                tl.next = p;
            }
            tl = p;
        } while ((e = e.next) != null);  // 将普通链表转成由树形节点链表
        if ((tab[index] = hd) != null)
            // 将树形链表转换成红黑树
            hd.treeify(tab);
    }
}

TreeNode<K,V> replacementTreeNode(Node<K,V> p, Node<K,V> next) {
    return new TreeNode<>(p.hash, p.key, p.value, next);
}

在扩容过程中,树化要满足两个条件:

  1. 链表长度大于等于 TREEIFY_THRESHOLD
  2. 桶数组容量大于等于 MIN_TREEIFY_CAPACITY

第一个条件比较好理解,这里就不说了。这里来说说加入第二个条件的原因,个人觉得原因如下:

当桶数组容量比较小时,键值对节点 hash 的碰撞率可能会比较高,进而导致链表长度较长。这个时候应该优先扩容,而不是立马树化。毕竟高碰撞率是因为桶数组容量较小引起的,这个是主因。容量小时,优先扩容可以避免一些列的不必要的树化过程。同时,桶容量较小时,扩容会比较频繁,扩容时需要拆分红黑树并重新映射。所以在桶容量比较小的情况下,将长链表转成红黑树是一件吃力不讨好的事。

 

再来查看HashMap的构造方法,一共有四个:

只说最常用的一个,我们都直接new HashMap(),它会默认设置加载因子为0.75

 

其他HashMap的细节

 

transient 所修饰 table 变量,HashMap 不序列化 table 的原因?

transient 表示易变的意思,在 Java 中,被该关键字修饰的变量不会被默认的序列化机制序列化。我们再回到源码中,考虑一个问题:桶数组 table 是 HashMap 底层重要的数据结构,不序列化的话,别人还怎么还原呢?

这里简单说明一下吧,HashMap 并没有使用默认的序列化机制,而是通过实现readObject/writeObject两个方法自定义了序列化的内容。这样做是有原因的,试问一句,HashMap 中存储的内容是什么?不用说,大家也知道是键值对所以只要我们把键值对序列化了,我们就可以根据键值对数据重建 HashMap。有的朋友可能会想,序列化 table 不是可以一步到位,后面直接还原不就行了吗?这样一想,倒也是合理。但序列化 talbe 存在着两个问题:

  1. table 多数情况下是无法被存满的,序列化未使用的部分,浪费空间
  2. 同一个键值对在不同 JVM 下,所处的桶位置可能是不同的,在不同的 JVM 下反序列化 table 可能会发生错误。

以上两个问题中,第一个问题比较好理解,第二个问题解释一下。HashMap 的get/put/remove等方法第一步就是根据 hash 找到键所在的桶位置,但如果键没有覆写 hashCode 方法,计算 hash 时最终调用 Object 中的 hashCode 方法。但 Object 中的 hashCode 方法是 native 型的,不同的 JVM 下,可能会有不同的实现,产生的 hash 可能也是不一样的。也就是说同一个键在不同平台下可能会产生不同的 hash,此时再对在同一个 table 继续操作,就会出现问题。

综上所述,大家应该能明白 HashMap 不序列化 table 的原因了。

 

hashcode为什么和jvm有关?

hashcode的值是对象在内存的地址算出来的,   不同的程序运行同一个对象,因为内存地址不一样,生成的hashcode当然不一样。

 

 

TreeMap和SortedMap

 TreeMap 是 SortedMap 接口的基于红黑树的实现。此类保证了映射按照升序顺序排列关键字, 根据使用的构造方法不同,可能会按照键的类的自然顺序进行排序(参见 Comparable), 或者按照创建时所提供的比较器进行排序。

 

 

 

 

 

 

 

 

 

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

你在狗叫什么、

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值