深入了解Java数据结构(集合及底层实现)

一、集合介绍

    Collection(单列集合)

        List(有序,可重复)

            ArrayList
                底层数据结构是数组,查询快,增删慢(因为:增删后涉及到其他数据的位移
                线程不安全,效率高
            Vector
                底层数据结构是数组,查询快,增删慢(因为:增删后涉及到其他数据的位移
                线程安全,效率低
            LinkedList
                底层数据结构是双向链表,查询慢,增删快
                线程不安全,效率高


        Set(无序,唯一)

            HashSet
                底层数据结构是哈希表
                哈希表依赖两个方法:hashCode()和equals()
                执行顺序:
                    首先判断hashCode()值是否相同
                        是:继续执行equals(),看其返回值
                            是true:说明元素重复,不添加
                            是false:就直接添加到集合
                        否:就直接添加到集合
                最终:
                    自动生成hashCode()和equals()即可
                    
                LinkedHashSet
                    底层数据结构由链表和哈希表组成。
                    由链表保证元素有序。
                    由哈希表保证元素唯一。
            TreeSet
                底层数据结构是红黑树。(是一种自平衡的二叉树)
                如何保证元素唯一性呢?
                    根据比较的返回值是否是0来决定
                如何保证元素的排序呢?
                    两种方式
                        自然排序(元素具备比较性)
                            让元素所属的类实现Comparable接口
                        比较器排序(集合具备比较性)
                            让集合接收一个Comparator的实现类对象


    Map(双列集合)

        A:Map集合的数据结构仅仅针对键有效,与值无关。
        B:存储的是键值对形式的元素,键唯一,值可重复。

        HashMap

            底层数据结构是:

            jdk1.8以下:数组+单向链表)哈希表

            jdk1.8+:数组+[单向链表 / 红黑树])哈希表,根据情况会选择链表和红黑树之间进行转换

            线程不安全,效率高
                哈希表依赖两个方法:hashCode()和equals()
                执行顺序:
                    首先判断hashCode()值是否相同
                        是:继续执行equals(),看其返回值
                            是true:说明元素重复,不添加
                            是false:就直接添加到集合
                        否:就直接添加到集合
                最终:
                    自动生成hashCode()和equals()即可
                LinkedHashMap
                底层数据结构由链表和哈希表组成。
                    由链表保证元素有序。
                    由哈希表保证元素唯一。

        Hashtable

            底层数据结构是哈希表。线程安全,效率低
                哈希表依赖两个方法:hashCode()和equals()
                执行顺序:
                    首先判断hashCode()值是否相同
                        是:继续执行equals(),看其返回值
                            是true:说明元素重复,不添加
                            是false:就直接添加到集合
                        否:就直接添加到集合
                最终:
                    自动生成hashCode()和equals()即可

        TreeMap

            底层数据结构是红黑树。(是一种自平衡的二叉树)
                如何保证元素唯一性呢?
                    根据比较的返回值是否是0来决定
                如何保证元素的排序呢?
                    两种方式
                        自然排序(元素具备比较性)
                            让元素所属的类实现Comparable接口
                        比较器排序(集合具备比较性)
                            让集合接收一个Comparator的实现类对象
                            

二、常见数据结构

1、数组结构

数组结构我想不必多说了

数组是最最基本的数据结构,很多语言都内置支持数组。数组是使用一块连续的内存空间保存数据,保存的数据的个数在分配内存的时候就是确定的

2、链表结构

单向链表 

双向链表

 

3、二叉树结构

4、散列表结构(哈希表)

 

 

三、关于集合选取原则

    是否是键值对象形式:
        是:Map
            键是否需要排序:
                是:TreeMap
                否:HashMap
            不知道,就使用HashMap。

        否:Collection
            元素是否唯一:
                是:Set
                    元素是否需要排序:
                        是:TreeSet
                        否:HashSet
                    不知道,就使用HashSet
                    
                否:List
                    要安全吗:
                        是:Vector
                        否:ArrayList或者LinkedList
                            增删多:LinkedList
                            查询多:ArrayList
                        不知道,就使用ArrayList
            不知道,就使用ArrayList


四、集合的常见方法及遍历方式

    Collection:
        add()
        remove()
        contains()
        iterator()
        size()
        
        遍历:
            增强for
            迭代器
            
        |--List
            get()
            
            遍历:
                普通for
        |--Set
    
    Map:
        put()
        remove()
        containskey(),containsValue()
        keySet()
        get()
        value()
        entrySet()
        size()
        
        遍历:
            根据键找值
            根据键值对对象分别找键和值

 

五、HashMap底层实现原理(jdk1.7\jdk1.8+)

面试常问特点:

1、底层结构

  • jdk1.8以下:HashMap的底层是:数组+链表(单向链表)
  • jdk1.8+:HashMap的底层是:数组+[链表(单向链表) / 红黑树 ]

2、线程不安全(put方法没有加锁) 

3、初始化默认大小:16 【1 << 4】

4、扩容 

      扩容触发机制:

  •       当前存储过的键值对的数量【即HashMap中的一个size属性】必须大于等于阈值(threshold)【注意:阈值=数组length*加载因子】;
  •       当前加入的数据是否发生hash冲突

      加载因子:0.75 当元素存储(使用)到达现有数组的75%的时候进行扩容(例如:下标100的数组,任意75个下标存储后就会扩容)

      扩大容量:每次在原基础乘以2,扩容后为原来的2倍

     /**
     * The default initial capacity - MUST be a power of two.
     */
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

     /**
     * The load factor used when none specified in constructor.
     */
    static final float DEFAULT_LOAD_FACTOR = 0.75f;

     /**
     * The next size value at which to resize (capacity * load factor).
     * @serial
     */
    // If table == EMPTY_TABLE then this is the initial capacity at which the
    // table will be created when inflated.
    int threshold;

     /**
     * Constructs an empty <tt>HashMap</tt> with the default initial capacity
     * (16) and the default load factor (0.75).
     */
    public HashMap() {
        this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
    }

 

准确点来说是 Entry[]数组 (根据Entry的内部结构不同,来断定具体是什么数据结构)

  • 数组的特点是:寻址容易,插入和删除困难(因为添加删除会涉及到其他元素的位移);
  • 链表的特点是:寻址困难,插入和删除容易(因为寻址需要一个一个的遍历过去,无法直接定位);

那么我们能不能综合两者的特性,做出一种寻址容易,插入删除也容易的数据结构?答案是肯定的,这就是我们要提起的哈希表,哈希表有多种不同的实现方法,我接下来解释的是最常用的一种方法——拉链法,我们可以理解为“链表的数组”
 

问:数组我们都知道,但什么是Entry数组呢?

答:就是一个数组里放的都是Entry

问:那这里的Entry又是什么呢?

答:

注意:每个集合类里面的存放实现都有可能是不一样的(比如 TreeSet 里面也有 Entry,但是和 HashMap Entry 内部结构实现是不同的

HashMap 的 Entry 就是我们说的链表结构 (单向链表)

下图中我们可以看到,在HashMap里面有个静态类 Entry<K,V> 也是 key value 形式的;

里面有 next 属性,类型也是Entry<K,V>

这就是一个 Entry 里面嵌套了另一个 Entry(典型的链表结构)

注意:我们可以看到这个 Entry类里面只有一个 Entry<key,value> next 属性,只有下一个,所以这是一个 单向链表

HashMap 结构 ,里面有个Entry(这是用来存放数据的)

内部类 Entry 结构 

下面是HashMap的结构图,结合上面的代码就很好理解了吧

 

1、HashMap中有一个table的全局属性属性,该属性是Enrty数组

数据都在这里面存放

首先执行初始化 :初始化会加载一些配置信息

初始化完成,如果在调用put方法时,发现table里面没有任何数据,那么会调用 inflateTable(int toSize)

 

2、【重点】HashMap 是如何添加数据的(我们主要看红色标注区域)

(1)首先拿到我们的 key 算出对应的 HashCode【line:492】

(2)根据 key 和 table.length 调用 indexFor() 来计算出一个数值 i,这个数值是 table 数组的下标【line:493】

(3)重点来了,我们需要比较这个 key 是否已经存在,如果存在则给对应的 value 重新赋值【line:494~502】

  1. 根据计算出来的下标 i 拿到该 table数组 在该下标内的 Entry
  2. 判断 entry 是不是 null ,如果不是则表示该值已存在,拿到值(for遍历过程)
  3. 判断数组中存在的 entry hash 和我们计算出的 hash 是否一致、 key 是否一致
  4. 如果一致,则说明 HashMap 中该 key 已经存在,我们进入 if() 内进行赋值操作,并返回旧的值

(4)如果该数组下标内没有找到对应的 key,我们则调用 addEntry() 方法进行添加【line:505】

addEntry()方法如下:

  1. 先进行扩容机制的判断【line:878~882
  2. 调用createEntry()方法进行添加键值对
  3. 进行链表的添加(将旧节点加入新节点的 next 中
  4. 添加完成后为全局变量size++(扩容时会用size进行判断)

大概看一下我们就可以理解,有兴趣的同学可以看一下 (红色区域为添加,蓝色区域后面会讲到)

链表的添加方式每次都是在最外层创建一个新的链表点,然后把旧的链表插入新的点所以最外层的(顶层的)永远是最新的

(5)get(key)方法如何实现的呢?

如果你认真的跟着我上面的步骤理解了 put(key,value)方法,我想不用看源码也能大概说出来是如何实现的

  1. 我们根据 key 算出 hashCode
  2. 用这个 HashCode 再算出,数组中的一个下标
  3. 如果这个下标是null 那么我们返回null
  4. 如果不是,那我们就遍历对比链表里面的每一个Entry,找到就返回,找不到就返回null

亮点,我们也附带学习了遍历链表的方法 

 

3、【重点】扩容机制源码 

扩容必须同时满足两个条件:

  1.  存放新值的时候,发现当前已有键值对(key-value)元素的个数(size)必须大于等于阈值(阈值=加载因子*当前数组长度)
  2.  存放新值的时候,当前新值数据key发生hash碰撞(当前key计算的hash值换算出来的数组下标位置已经存在值)

在put()方法中有调用addEntry()方法,这个方法里面是具体的存值,在存值之前还有判断是否需要扩容

判断扩容条件:(size >= threshold) && (null != table[bucketIndex])【line:878】

如果下面两个条件都发生,那么就扩容

  1. 判断当前个数是否大于等于阈值(size >= threshold)
  2. 当前存放是否发生哈希碰撞(null != table[bucketIndex])

扩容调用方法:resize(int) 【line:879】

扩容大小为原先数组大小的两倍 

阈值:threshold  

threshold 就是所说的阈值,它是一个全局变量,决定了数组是否进行扩容的判断条件之一【上图中 line:878】 

阈值根据加载因子数组大小决定的 

默认情况下: 阈值=加载因子 * 当前数组大小

HashMap的构造函数有4个:

下图中的3个构造函数可以看到都是调用了同一个构造函数 public HashMap (int initialCapacity, float loadFactor)

代码中选中的是加载因子 

public HashMap (int initialCapacity, float loadFactor)

 

如果需要扩容,调用扩容的方法:resize(int)

 

总结:

HashMap的扩容需要同时满足两个条件:

  1.  存放新值的时候,发现当前已有键值对(key-value)元素的个数(size)必须大于等于阈值(阈值=加载因子*当前数组长度)
  2.  存放新值的时候,当前新值数据key发生hash碰撞(当前key计算的hash值换算出来的数组下标位置已经存在值)

因为上面这两个条件,所以存在下面这些情况

  1. 就是hashmap在存值的时候(默认大小为16,负载因子0.75,阈值12),可能达到最后存满16个值的时候,再存入第17个值才会发生扩容现象,因为前16个值,每个值在底层数组中分别占据一个位置,并没有发生hash碰撞。【key不触发hash碰撞】
  2. 当然也有可能存储更多值(超多16个值,最多可以存26个值)都还没有扩容。原理:前11个值全部hash碰撞,存到数组的同一个位置(这时元素个数小于阈值12,不会扩容),后面所有存入的15个值全部分散到数组剩下的15个位置(这时元素个数大于等于阈值,但是每次存入的元素并没有发生hash碰撞,所以不会扩容),前面11+15=26,所以在存入第27个值的时候才同时满足上面两个条件,这时候才会发生扩容现象。

 

HashMap(jdk1.8+)的变化

变化如下:

  1. 数据存储结构进行改变
  2. 对数组下标的定位生成,做了改动,使之更具有分散性(减少经常向同一个下标存储的情况)
  3. 加入了链表和红黑树相互转变的机制(减少链的深度)

1、存储key value的内部存储类 Entry 变为 Node 或 TreeNode

transient Node<K,V>[] table;
static class Node<K,V> implements Map.Entry<K,V> {
        final int hash;
        final K key;
        V value;
        Node<K,V> next;

        Node(int hash, K key, V value, Node<K,V> next) {
            this.hash = hash;
            this.key = key;
            this.value = value;
            this.next = next;
        }

        public final K getKey()        { return key; }
        public final V getValue()      { return value; }
        public final String toString() { return key + "=" + value; }

        public final int hashCode() {
            return Objects.hashCode(key) ^ Objects.hashCode(value);
        }

        public final V setValue(V newValue) {
            V oldValue = value;
            value = newValue;
            return oldValue;
        }

        public final boolean equals(Object o) {
            if (o == this)
                return true;
            if (o instanceof Map.Entry) {
                Map.Entry<?,?> e = (Map.Entry<?,?>)o;
                if (Objects.equals(key, e.getKey()) &&
                    Objects.equals(value, e.getValue()))
                    return true;
            }
            return false;
        }
    }

 TreeNode

     /**
     * Entry for Tree bins. Extends LinkedHashMap.Entry (which in turn
     * extends Node) so can be used as extension of either regular or
     * linked node.
     */
    static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
        TreeNode<K,V> parent;  // red-black tree links
        TreeNode<K,V> left;
        TreeNode<K,V> right;
        TreeNode<K,V> prev;    // needed to unlink next upon deletion
        boolean red;
        TreeNode(int hash, K key, V val, Node<K,V> next) {
            super(hash, key, val, next);
        }

     ......

     }

2、put方法的改变(加入了红黑树的转变机制)

使用树型结构,而不去使用单向链表

当节点超过8时,从链表转变为红黑树结构,从而减小链的深度 

当节点小于6时,从红黑树转变为链表结构

     /**
     * The bin count threshold for using a tree rather than list for a
     * bin.  Bins are converted to trees when adding an element to a
     * bin with at least this many nodes. The value must be greater
     * than 2 and should be at least 8 to mesh with assumptions in
     * tree removal about conversion back to plain bins upon
     * shrinkage.
     */
    static final int TREEIFY_THRESHOLD = 8;

    /**
     * The bin count threshold for untreeifying a (split) bin during a
     * resize operation. Should be less than TREEIFY_THRESHOLD, and at
     * most 6 to mesh with shrinkage detection under removal.
     */
    static final int UNTREEIFY_THRESHOLD = 6;

put方法的改变

 

(1)根据情况判断是否进行初始化【line:628~629】

(2)计算出数组的下标,取出数据存入变量p,判断是否为null,如果为null则直接生成新节点存入,跳至661【line:630~631】

(3)如果该下标中存在数据则进入else【line:632~660】

         (1、下标中先取链表的 顶层 Node节点,判断key是否相同。相同则直接进入line:653进行数据替换【line:634~636】

         (2、判断该下标Node节点类型是否为TreeNode(树结构),如果是则使用内部类TreeNodeputTreeVal进行存储【line:637~638】

         (3、如果传入的key不是 顶层 Node节点,数组下标节点也不是TreeNode(树结构),那么就对该下标下的Node节点进行循环遍历【line:639~652】

         (4、对value值进行替换处理

 

 

 

LinkedHashMap 底层实现原理

 LinkedHashMap 继承了 HashMap 所以很多方法都是继承来的,但是 LinkedHashMap 又是有序的,我们可以看到构造器中有个 accessOrder 参数,这个就是来控制有序的

 

六、HashTable底层实现原理

面试常问特点:

1、底层结构

jdk1.8以下:HashMap的底层是:数组+链表(单向链表)

jdk1.8+:HashMap的底层是:数组+[链表(单向链表) / 红黑树 ]

2、线程安全(put方法加了synchronized修饰) 

3、初始化默认大小:11 

4、扩容 

      扩容触发机制:数据大小【size()】大于等于阈值【count >= threshold】

      加载因子:0.75

      扩大容量:每次原大小乘以2再加1【(old.length << 1)+1

     /**
     * Constructs a new, empty hashtable with a default initial capacity (11)
     * and load factor (0.75).
     */
    public Hashtable() {
        this(11, 0.75f);
    }

关于2n+1的扩展,在hashtable选择用取模的方式来进行,那么尽量使用素数、奇数会让结果更加均匀一些,具体证明,可以看看已经证明这点的技术文章
关于hash,hashmap用2的幂,主要是其还有一个hash过程即二次hash,不是直接用key的hashcode,这个过程打散了数据
总体就是一个减少hash冲突,并且找索引效率还要高,实现都是要考量这两因素的 

1、添加方法和HashMap几乎是一样的 

(1)用 key 算出对应的 HashCode【line:465】

(2)根据 HashCodetable.length 计算出 table 数组的下标【line:466】

(3)循环遍历table下标中的entry链表比较这个 key 是否已经存在,如果存在则给对应的 value 重新赋值【line:468~475】

(4)如果该数组下标内没有对应的key,我们则调用 addEntry() 方法进行添加【line:477】

 

2、扩容方法 rehash()

 

七、TreeMap 底层实现原理

1、了解结构 

TreeMap 有一个 Entry<k,v> root 属性 (用来存放数据的)

Entry 类型又有三个 Entry 类型的 left、right、parent 属性,和自己的 key、value属性;(用来存放 树结构 左、右、父的对象数据,还有自己的值;color 属性是当前树节点的颜色)

由此可见 Entry 类型是个 红黑树 的结构,而 TreeMap 里面存储的是 Entry 结果自然就是红黑树

 

2、内部类 Entry 结构

 

八、ArrayList 底层实现原理

面试常问特点:

1、底层结构:数组 

2、线程不安全(add方法没有加锁)  

3、初始化默认大小:10 

4、扩容

      扩容触发机制:当存储第11个数据时,11超过了默认的10,就会触发扩容

      扩大容量:每次在原基础上增加0.5倍,扩容后为原来的1.5倍

     /**
     * Default initial capacity.
     */
    private static final int DEFAULT_CAPACITY = 10;

    /**
     * Shared empty array instance used for empty instances.
     */
    private static final Object[] EMPTY_ELEMENTDATA = {};

     /**
     * Constructs an empty list with an initial capacity of ten.
     */
    public ArrayList() {
        super();
        this.elementData = EMPTY_ELEMENTDATA;
    }

 

扩容机制源码

ensureCapacityInternal() 方法

(1)在调用Add方法时,会先通过 ensureCapacityInternal() 方法确保当前ArrayList维护的数组具有存储新元素的能力【Line:440】

(2)ensureCapacityInternal() 判断ArrayList默认的元素存储数据是否为空,为空则设置最小要求的存储能力为必要存储的元素和默认存储元素个数的两个数据之间的最大值,然后调用ensureExplicitCapacity方法实现这种最低要求的存储能力【Line:208】

(3)如果最低要求的存储能力 > ArrayList 已有的存储能力,这就表示ArrayList的存储能力不足,因此需要调用 grow();方法进行扩容 【Line:215】

 

 

扩容方法源码(扩容后为原来的1.5倍)

grow()方法 

(1)数字转换为二进制,使用向右位移符 >> 移动所有二进制数,实现除法(源码中 oldCapacity >> 1表示所有二进制向右移动1位,表示除以2,移出去的数直接忽略)

(2)新size = 旧的size + 扩大的size(Line:236)

(3)创建一个新的数组

(4)通过Arrays.copyOf方法,将原数组的数据复制到新数组

总结:

举例说明:添加20个元素到ArrayList中 

当第一次插入元素时才分配10(默认)个对象空间。之后扩容会按照1.5倍增长。

也就是当添加第11个数据的时候,Arraylist继续扩容变为10*1.5=15;

当添加第16个数据时,继续扩容变为15 * 1.5 =22个;

 

 

九、Vector 底层实现原理

面试常问特点:

1、底层结构:数组 

2、线程安全(add方法添加synchronized锁) 

3、初始化默认大小:10 

      扩容触发机制:当存储第N+1个数据时,N+1超过了先前的数组最大个数N,就会触发扩容

      扩大容量:每次在原基础上增加1倍,也就是总大小为原来的2倍

    /**
     * Constructs an empty vector so that its internal data array
     * has size {@code 10} and its standard capacity increment is
     * zero.
     */
    public Vector() {
        this(10);
    }

 

扩容机制源码

ensureCapacityHelper() 方法 

基本上ArrayList一样

扩容方法源码(扩容后为原来的2倍)

grow()方法 

(1)(capacityIncrement > 0) ? capacityIncrement : oldCapacity(大多情况会返回 oldCapacity

(2)新size = 旧的size + 旧的size(Line:256)

(3)创建一个新的数组

(4)通过Arrays.copyOf方法,将原数组的数据复制到新数组

 

总结:

举例说明:添加40个元素到 Vector 中 

当第一次插入元素时才分配10(默认)个对象空间。之后扩容会按照2倍增长。

也就是当添加第11个数据的时候,Vector 继续扩容变为10*2=20;

当添加第21个数据时,继续扩容变为20 * 2 =40个;

 

十、LinkedList 底层实现原理

1、了解结构。我们进入类中,看到它的变量

LinkedList 有两个 Node 类型的 first、last属性,和自己的size;(用来存放第一个和最后一个,还有总大小的值)

Node 类型又有两个 Node 类型的 next、prev属性,和自己的 item 属性;(用来存放 前一个和后一个对象数据,还有自己的值)

由此可见 Node 类型是个 双向链表 的结构,而 LinkedList 里面存储的是 Node 结果自然就是 双向链表

2、内部类 Node 结构

  • 22
    点赞
  • 83
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值