硬核干货Java集合详解

目录

一、问题是最好的老师

二、集合的由来

三、数组存在的问题

四、数组和集合的区别?

五、集合是什么?

六、集合整体架构图

七、集合架构图详解

1、Collection

2、List

ArrayList

LinkedList

3、Set

HashSet

TreeSet

LinkedHashSet

4、Map

HashMap

TreeMap


集合框架整体思维模式:

一、问题是最好的老师

思考:

首先要明白集合是怎么来的?为什么要有集合?它来给我们解决了什么问题?集合不是无缘无故就来了,它一定是来给我们解决了什么现实的问题?很显然,在他之前我们明明在很多情况下用数组就很好啊,那么为什么我们还要用集合呢?数组到底有什么问题呢?

其次是整个集合框架的概况,整个集合框架都包含写什么?为什么要这么分?

再次才是深入的具体的每个集合中去,每个集合的底层数据结构是什么?他们各自的优缺点是什么?都使用在了 什么样的场景之下?

最后才是他们的源码和特别功能是什么?

二、集合的由来

通常,我们的程序需要根据程序运行时才知道创建多少个对象。但若非程序运行,程序开发阶段,我们根本不知道到底需要多少个数量的对象,甚至不知道它的准确类型。为了满足这些常规的编程需要,我们要求能在任何时候,任何地点创建任意数量的对象,而这些对象用什么来容纳呢?我们首先想到了数组,但是数组只能放统一类型的数据,而且其长度是固定的,那怎么办呢?集合便应运而生了!

首先要知道我们所学习的Java语言是一个完全面向对象的语言,而这种语言对事物的描述是通过对象体现的,为了方便对多个对象进行操作,我们就必须把这多个对象进行存储。一个基本类型的变量显然是无法满足存储多个对象的,所以应该是一个容器类型的变量,我们知道数组和StringBuffe、StringBuilder均属于容器类型。但是呢? StringBuffer的结果是一个字符串,不一定满足我们的要求,所以我们只能选择数组,这就是对象数组。可是问题又来了,对象数组又不能适应变化的需求,因为数组的长度是固定的,这个时候,为了适应变化的需求,Java就提供了集合类供我们使用。

三、数组存在的问题

(1)数组一旦指定了长度,那么长度就被确定了,不可以更改。

(2)删除,增加元素 效率低。

(3)数组中实际元素的数量是没有办法获取的,没有提供对应的方法或者属性来获取

(4)数组存储:有序,可重复 ,对于无序的,不可重复的数组不能满足要求。

四、数组和集合的区别?

在Java中,数组是一种效率最高的存储和随机访问对象的引用序列的方式。数组就是一个简单的线性序列,这使得元素访问非常快速。但是为这种速度所付出的代价是数组对象的大小被固定,并且在其生命周期中不可改变。你可能会建议使用ArrayList,它可以通过创建一个新实例,然后把旧实例中所有的引用到移到新实例中,从而实现更多空间的自动分配。尽管通常应该首选ArrayList而不是数组、但是这种弹性需要开销,因此,ArrayList的效率比数组低很多

名称

长度

内容

类型

数组

固定

存同种类型元素

基本数据类型+引用类型

集合

可变

存不同类型元素

引用类型

五、集合是什么?

集合是一个用来存储对象的容器,集合只能存放对象。比如你存一个 int 型数据 1放入集合中,其实它是自动转换成 Integer 类后存入的,Java中每一种基本类型都有对应的引用类型。集合存放的是多个对象的引用,对象本身还是放在堆内存中。集合可以存放不同类型,不限数量的数据类型。

六、集合整体架构图

该图来自:史上最全Java集合关系图_zxiaofan.com-CSDN博客_java集合图

七、集合架构图详解

通过上图不难看出,整个集合框架最顶层的接口就是Collection,然后才是Queue、List、Set和Map,其中Queue、List、Set实现了Collection顶层接口。因此我们学习集合就从顶层接口Collection开始,但凡顶层接口Collection有的方法,下面实现了该接口的都能够实现顶层接口的方法。

1、Collection

(1)常用方法

public class Test01 {
    //这是main方法,程序的入口
    public static void main(String[] args) {
        /*
        Collection接口的常用方法:
        增加:add(E e) addAll(Collection<? extends E> c)
        删除:clear() remove(Object o)
        修改:
        查看:iterator() size()
        判断:contains(Object o)  equals(Object o) isEmpty()
         */
        //创建对象:接口不能创建对象,利用实现类创建对象:
        Collection col = new ArrayList();
        //调用方法:
        //集合有一个特点:只能存放引用数据类型的数据,不能是基本数据类型
        //基本数据类型自动装箱,对应包装类。int--->Integer
        col.add(18);
        col.add(12);
        col.add(11);
        col.add(17);
        System.out.println(col/*.toString()*/);
        List list = Arrays.asList(new Integer[]{11, 15, 3, 7, 1});
        col.addAll(list);//将另一个集合添加入col中
        System.out.println(col);
        //col.clear();清空集合
        System.out.println(col);
        System.out.println("集合中元素的数量为:"+col.size());
        System.out.println("集合是否为空:"+col.isEmpty());
        boolean isRemove = col.remove(15);
        System.out.println(col);
        System.out.println("集合中数据是否被删除:"+isRemove);
        Collection col2 = new ArrayList();
        col2.add(18);
        col2.add(12);
        col2.add(11);
        col2.add(17);
        Collection col3 = new ArrayList();
        col3.add(18);
        col3.add(12);
        col3.add(11);
        col3.add(17);
        System.out.println(col2.equals(col3));
        System.out.println(col2==col3);//地址一定不相等  false
        System.out.println("是否包含元素:"+col3.contains(117));
    }
}

(2)迭代器执行原理图

2、List

(1)list的常用方法和遍历方法

public class Test03 {
    //这是main方法,程序的入口
    public static void main(String[] args) {
        /*
        List接口中常用方法:
        增加:add(int index, E element)
        删除:remove(int index)  remove(Object o)
        修改:set(int index, E element)
        查看:get(int index)
        判断:
         */
        List list = new ArrayList();
        list.add(13);
        list.add(17);
        list.add(6);
        list.add(-1);
        list.add(2);
        list.add("abc");
        System.out.println(list);
        list.add(3,66);
        System.out.println(list);
        list.set(3,77);
        System.out.println(list);
        list.remove(2);//在集合中存入的是Integer类型数据的时候,调用remove方法调用的是:remove(int index)
        System.out.println(list);
        list.remove("abc");
        System.out.println(list);
        Object o = list.get(0);
        System.out.println(o);
        //List集合 遍历:
        //方式1:普通for循环:
        System.out.println("---------------------");
        for(int i = 0;i<list.size();i++){
            System.out.println(list.get(i));
        }
        //方式2:增强for循环:
        System.out.println("---------------------");
        for(Object obj:list){
            System.out.println(obj);
        }
        //方式3:迭代器:
        System.out.println("---------------------");
        Iterator it = list.iterator();
        while(it.hasNext()){
            System.out.println(it.next());
        }
    }
}

ArrayList

  • 实现结构图

    • 实现了RandomAccess接口,可以随机访问
    • 实现了Cloneable接口,可以克隆
    • 实现了Serializable接口,可以序列化、反序列化
    • 实现了List接口,是List的实现类之一
    • 实现了Collection接口,是Java Collections Framework成员之一
    • 实现了Iterable接口,可以使用for-each迭代
  • 底层数据结构

——数组,就是一个动态数组,且有下标,因此查询快,增删慢。

  • 缺点:

——增加和删除元素需要移动所以很慢

——需要设计数组的扩容或者拷贝,效率很低

  • 底层源码

通过下面的源码可以非常清楚的看到,jdk11在调用空构造器的时候底层的elementData数组的初始化为{},那么它是在什么时候给赋的初始长度呢?

public ArrayList() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }

private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

当我们通过add方法进入,你就会发现ArrayList是在什么时候进行初始化长度赋值的。他是在我们进行调用add方法的时候才给数组赋初始最大长度10,那么当这个数组满了又是怎么扩容的呢?每次加入元素的时候会进行判断,当前数组大小是否小于默认的10,如果不是则将原来的数组扩容到1.5倍,然后将原来的数组copy进新数组。

 public boolean add(E e) {
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        elementData[size++] = e;
        return true;
    }


    private void ensureCapacityInternal(int minCapacity) {
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
        }

        ensureExplicitCapacity(minCapacity);
    }

    private static final int DEFAULT_CAPACITY = 10;

    private void ensureExplicitCapacity(int minCapacity) {
        modCount++;

        // overflow-conscious code
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
    }

    private void grow(int minCapacity) {
        // overflow-conscious code
        int oldCapacity = elementData.length;
        int newCapacity = oldCapacity + (oldCapacity >> 1);
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;
        if (newCapacity - MAX_ARRAY_SIZE > 0)
            newCapacity = hugeCapacity(minCapacity);
        // minCapacity is usually close to size, so this is a win:
        elementData = Arrays.copyOf(elementData, newCapacity);
    }

LinkedList

  • 结构图

    • LinkedList 是一个继承于AbstractSequentialList的双向链表。
    • LinkedList 可以被当作堆栈、队列或双端队列进行操作。
    • LinkedList 实现 List 接口,所以能对它进行队列操作。
    • LinkedList 实现 Deque 接口,能将LinkedList当作双端队列使用。
    • LinkedList 实现了Cloneable接口,即覆盖了函数clone(),能克隆。
    • LinkedList 实现java.io.Serializable接口,所以LinkedList支持序列化,能通过序列化去传输。
    • LinkedList 是非同步的。
  • 底层数据结构

——双向链表每个结点都有一个头指针head和尾指针tail,双链表相比单链表更容易操作,双链表结点的首结点的head指向为null,tail指向下一个节点的tail;尾结点的head指向前一个结点的head,tail 指向为null,是双向的关系

——插入删除不需要移动元素,链表的前后都可以插入数据,可以双向遍历

——查找慢,增删快

  • 缺点:

——内存地址不连续,查找效率低

  • 源码
public class LinkedList<E>{//E是一个泛型,具体的类型要在实例化的时候才会最终确定
        transient int size = 0;//集合中元素的数量
        //Node的内部类
        private static class Node<E> {
        E item;//当前元素
        Node<E> next;//指向下一个元素地址
        Node<E> prev;//上一个元素地址
        Node(Node<E> prev, E element, Node<E> next) {
            this.item = element;
            this.next = next;
            this.prev = prev;
        }
    }
        transient Node<E> first;//链表的首节点
        transient Node<E> last;//链表的尾节点
        //空构造器:
        public LinkedList() {
    }
        //添加元素操作:
        public boolean add(E e) {
        linkLast(e);
        return true;
    }
        void linkLast(E e) {//添加的元素e
        final Node<E> l = last;//将链表中的last节点给l 如果是第一个元素的话 l为null
                //将元素封装为一个Node具体的对象:
        final Node<E> newNode = new Node<>(l, e, null);
                //将链表的last节点指向新的创建的对象:
        last = newNode;
                
        if (l == null)//如果添加的是第一个节点
            first = newNode;//将链表的first节点指向为新节点
        else//如果添加的不是第一个节点 
            l.next = newNode;//将l的下一个指向为新的节点
        size++;//集合中元素数量加1操作
        modCount++;
    }
        //获取集合中元素数量
        public int size() {
        return size;
    }
        //通过索引得到元素:
        public E get(int index) {
        checkElementIndex(index);//健壮性考虑
        return node(index).item;
    }
        
    Node<E> node(int index) {
        //如果index在链表的前半段,那么从前往后找
        if (index < (size >> 1)) {
            Node<E> x = first;
            for (int i = 0; i < index; i++)
                x = x.next;
            return x;
        } else {//如果index在链表的后半段,那么从后往前找
            Node<E> x = last;
            for (int i = size - 1; i > index; i--)
                x = x.prev;
            return x;
        }
    }
}

3、Set

Set集合类似于一个罐子,程序可以依次把多个对象“丢进”Set集合,而Set集合通常不能记住元素的添加顺序。实际上Set就是Collection只是行为略有不同(Set不允许包含重复元素)。Set集合不允许包含相同的元素,如果试图把两个相同元素加入同一个Set集合中,则添加操作失败,add()方法返回false,且新元素不会被加入。

  • 插入无序
  • 元素不能重复
  • 底层均为Map集合实现

HashSet

  • 结构图

  • 底层数据结构

——哈希表=数组+链表

  • 不能保证元素的排列顺序,顺序可能与添加顺序不同,顺序也可能发生变化;
  • HashSet不是同步的;
  • 集合元素值可以是null;

——内部存储机制

当向HashSet集合中存入一个元素时,HashSet会调用该对象的hashCode方法来得到该对象的hashCode值,然后根据该hashCode值决定该对象在HashSet中的存储位置。如果有两个元素通过equals方法比较true,但它们的hashCode方法返回的值不相等,HashSet将会把它们存储在不同位置,依然可以添加成功。也就是说。HashSet集合判断两个元素的标准是两个对象通过equals方法比较相等,并且两个对象的hashCode方法返回值也相等。

——为啥不直接用数组,而选择用HashSet?

因为数组的索引是连续的而且数组的长度是固定的,无法自由增加数组的长度。而HashSet就不一样了,HashCode表用每个元素的hashCode值来计算其存储位置,从而可以自由增加HashCode的长度,并根据元素的hashCode值来访问元素。而不用一个个遍历索引去访问,这就是它比数组快的原因。

  • 源码——底层是HashMap

TreeSet

  • 结构图 

  • 底层数据结构

——红黑树:自平衡二叉树

硬核图解面试最怕的红黑树【建议反复摩擦】_敖丙-CSDN博客

  • 源码——底层是TreeMap

LinkedHashSet

  • 结构图

    • 它扩展了HashSet类,扩展了AbstractSet类。
    • 它实现了Set接口。
    • LinkedHashSet中不允许重复的值
    • LinkedHashSet中允许一个NULL元素。
    • 这是一个有序集合,这是元素插入到集合中的顺序(插入顺序)。
    • LinkedHashSet未同步。 如果多个线程同时访问哈希集合,并且至少有一个线程修改了哈希集合,则必须在外部对其进行同步。
    • LinkedHashSet还实现了SearlizableCloneable接口。
    • 此类的迭代器方法返回的迭代器为快速失败,并且如果在创建迭代器后的任何时间修改了集合,则可能会抛出ConcurrentModificationException,除了通过迭代器自己的remove()方法之外 。
    • 使用Collections.synchronizedSet(new LinkedHashSet())方法来获取同步的LinkedHashSet
    • HashSet一样,此类为基本操作(添加,删除,包含和调整大小)提供恒定时间性能

  • 底层数据结构

——链表+哈希表

它只是在HashSet的基础上多了一个总的链表,这个链表将放入的元素串在一起,方便有序的便利

  • 源码

——构造函数调用的是同一个父类的构造函数。这个构造函数是一个包内私有构造函数(见下面的代码,HashSet的构造函数没有使用public公开),它只能被LinkedHashSet使用。LinkedHashSet并没有自己的方法,所有的方法都继承自它的父类HashSet,因此,对LinkedHashSet的所有操作方式就好像对HashSet操作一样。唯一的不同是内部使用不同的对象去存储元素。在HashSet中,插入的元素是被当做HashMap的键来保存的,而在LinkedHashSet中被看作是LinkedHashMap的键。

public LinkedHashSet() {
        super(16, .75f, true);
    }


    HashSet(int initialCapacity, float loadFactor, boolean dummy) {
        map = new LinkedHashMap<>(initialCapacity, loadFactor);
    }

——LinkedHashSet是如何维护插入顺序的?从下面的代码可以看出,是通过成员变量before和after负责维护LinkedHashSet的插入顺序

private static class Entry<K,V> extends HashMap.Entry<K,V>
{
        // These fields comprise the doubly linked list used for iteration.
        Entry<K,V> before, after;
 
        Entry(int hash, K key, V value, HashMap.Entry<K,V> next) {
            super(hash, key, value, next);
        }
}

——初始容量是指创建LinkedHashSet时的存储桶数(在支持HashMap中)。 如果当前大小已满,则存储桶数将自动增加。默认初始容量为 16 。 我们可以通过在构造器 LinkedHashSet(int initialCapacity)中传递默认容量来覆盖此默认容量。

——负载系数负载因子是在自动增加LinkedHashSet的容量之前允许其充满的度量。 默认负载系数为 0.75 。这称为阈值,等于(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY)。 当LinkedHashSet元素计数超过此阈值时,将调整LinkedHashSet的大小,并且新容量是先前容量的两倍。使用默认的LinkedHashSet时,内部容量为 16,负载系数为 0.75。 当表格中有 12 个元素时,存储桶数将自动增加。

4、Map

哈希表(hash table)也叫散列表,是一种非常重要的数据结构,应用场景及其丰富,许多缓存技术(比如memcached)的核心其实就是在内存中维护一张大的哈希表,本文会对java集合框架中HashMap的实现原理进行讲解,并对JDK7的HashMap源码进行分析。

HashMap

  • 结构图

  • 底层数据结构

HashMap的主干是一个Entry数组。Entry是HashMap的基本组成单元,每一个Entry包含一个key-value键值对。(其实所谓Map其实就是保存了两个对象之间的映射关系的一种集合)

——数组+链表+红黑树

数组是HashMap的主体,链表则是主要为了解决哈希冲突而存在的,如果定位到的数组位置不含链表(当前entry的next指向null),那么查找,添加等操作很快,仅需一次寻址即可;如果定位到的数组包含链表,对于添加操作,其时间复杂度为O(n),首先遍历链表,存在即覆盖,否则新增;对于查找操作来讲,仍需遍历链表,然后通过key对象的equals方法逐一比对查找。所以,性能考虑,HashMap中的链表出现越少,性能才会越好。

  • 源码JDK1.7
public class HashMap<K,V>
    extends AbstractMap<K,V> //【1】继承的AbstractMap中,已经实现了Map接口
        //【2】又实现了这个接口,多余,但是设计者觉得没有必要删除,就这么地了
    implements Map<K,V>, Cloneable, Serializable{
                
                
        //【3】后续会用到的重要属性:先粘贴过来:
    static final int DEFAULT_INITIAL_CAPACITY = 16;//哈希表主数组的默认长度
        //定义了一个float类型的变量,以后作为:默认的装填因子,加载因子是表示Hsah表中元素的填满的程度
        //太大容易引起哈西冲突,太小容易浪费  0.75是经过大量运算后得到的最好值
        //这个值其实可以自己改,但是不建议改,因为这个0.75是大量运算得到的
        static final float DEFAULT_LOAD_FACTOR = 0.75f;
        transient Entry<K,V>[] table;//主数组,每个元素为Entry类型
        transient int size;
        int threshold;//数组扩容的界限值,门槛值   16*0.75=12 
        final float loadFactor;//用来接收装填因子的变量
        
        //【4】查看构造器:内部相当于:this(16,0.75f);调用了当前类中的带参构造器
        public HashMap() {
        this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
    }
        //【5】本类中带参数构造器:--》作用给一些数值进行初始化的!
        public HashMap(int initialCapacity, float loadFactor) {
        //【6】给capacity赋值,capacity的值一定是 大于你传进来的initialCapacity 的 最小的 2的倍数
        int capacity = 1;
        while (capacity < initialCapacity)
            capacity <<= 1;
                //【7】给loadFactor赋值,将装填因子0.75赋值给loadFactor
        this.loadFactor = loadFactor;
                //【8】数组扩容的界限值,门槛值
        threshold = (int)Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
                
                //【9】给table数组赋值,初始化数组长度为16
        table = new Entry[capacity];
                   
    }
        //【10】调用put方法:
        public V put(K key, V value) {
                //【11】对空值的判断
        if (key == null)
            return putForNullKey(value);
                //【12】调用hash方法,获取哈希码
        int hash = hash(key);
                //【14】得到key对应在数组中的位置
        int i = indexFor(hash, table.length);
                //【16】如果你放入的元素,在主数组那个位置上没有值,e==null  那么下面这个循环不走
                //当在同一个位置上放入元素的时候
        for (Entry<K,V> e = table[i]; e != null; e = e.next) {
            Object k;
                        //哈希值一样  并且  equals相比一样   
                        //(k = e.key) == key  如果是一个对象就不用比较equals了
            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
        }
        modCount++;
                //【17】走addEntry添加这个节点的方法:
        addEntry(hash, key, value, i);
        return null;
    }
        
        //【13】hash方法返回这个key对应的哈希值,内部进行二次散列,为了尽量保证不同的key得到不同的哈希码!
        final int hash(Object k) {
        int h = 0;
        if (useAltHashing) {
            if (k instanceof String) {
                return sun.misc.Hashing.stringHash32((String) k);
            }
            h = hashSeed;
        }
                //k.hashCode()函数调用的是key键值类型自带的哈希函数,
                //由于不同的对象其hashCode()有可能相同,所以需对hashCode()再次哈希,以降低相同率。
        h ^= k.hashCode();
        // This function ensures that hashCodes that differ only by
        // constant multiples at each bit position have a bounded
        // number of collisions (approximately 8 at default load factor).
                /*
                接下来的一串与运算和异或运算,称之为“扰动函数”,
                扰动的核心思想在于使计算出来的值在保留原有相关特性的基础上,
                增加其值的不确定性,从而降低冲突的概率。
                不同的版本实现的方式不一样,但其根本思想是一致的。
                往右移动的目的,就是为了将h的高位利用起来,减少哈西冲突
                */
        h ^= (h >>> 20) ^ (h >>> 12);
        return h ^ (h >>> 7) ^ (h >>> 4);
    }
        //【15】返回int类型数组的坐标
        static int indexFor(int h, int length) {
                //其实这个算法就是取模运算:h%length,取模效率不如位运算
        return h & (length-1);
    }
        //【18】调用addEntry
        void addEntry(int hash, K key, V value, int bucketIndex) {
                //【25】size的大小  大于 16*0.75=12的时候,比如你放入的是第13个,这第13个你打算放在没有元素的位置上的时候
        if ((size >= threshold) && (null != table[bucketIndex])) {
                        //【26】主数组扩容为2倍
            resize(2 * table.length);
                        //【30】重新调整当前元素的hash码
            hash = (null != key) ? hash(key) : 0;
                        //【31】重新计算元素位置
            bucketIndex = indexFor(hash, table.length);
        }
                //【19】将hash,key,value,bucketIndex位置  封装为一个Entry对象:
        createEntry(hash, key, value, bucketIndex);
    }
        //【20】
        void createEntry(int hash, K key, V value, int bucketIndex) {
                //【21】获取bucketIndex位置上的元素给e
        Entry<K,V> e = table[bucketIndex];
                //【22】然后将hash, key, value封装为一个对象,然后将下一个元素的指向为e (链表的头插法)
                //【23】将新的Entry放在table[bucketIndex]的位置上
        table[bucketIndex] = new Entry<>(hash, key, value, e);
                //【24】集合中加入一个元素 size+1
        size++;
    }
    //【27】
        void resize(int newCapacity) {
        Entry[] oldTable = table;
        int oldCapacity = oldTable.length;
        if (oldCapacity == MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return;
        }
                //【28】创建长度为newCapacity的数组
        Entry[] newTable = new Entry[newCapacity];
        boolean oldAltHashing = useAltHashing;
        useAltHashing |= sun.misc.VM.isBooted() &&
                (newCapacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
        boolean rehash = oldAltHashing ^ useAltHashing;
                //【28.5】转让方法:将老数组中的东西都重新放入新数组中
        transfer(newTable, rehash);
                //【29】老数组替换为新数组
        table = newTable;
                //【29.5】重新计算
        threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
    }
        //【28.6】
        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;
                if (rehash) {
                    e.hash = null == e.key ? 0 : hash(e.key);
                }
                                //【28.7】将哈希值,和新的数组容量传进去,重新计算key在新数组中的位置
                int i = indexFor(e.hash, newCapacity);
                                //【28.8】头插法
                e.next = newTable[i];//获取链表上元素给e.next
                newTable[i] = e;//然后将e放在i位置 
                e = next;//e再指向下一个节点继续遍历
            }
        }
    }
}

TreeMap

  • 结构图

  • 底层数据结构

——红黑树

红黑树规则特点:

  • 节点分为红色或者黑色;
  • 根节点必为黑色;
  • 叶子节点都为黑色,且为null;
  • 连接红色节点的两个子节点都为黑色(红黑树不会出现相邻的红色节点);
  • 从任意节点出发,到其每个叶子节点的路径中包含相同数量的黑色节点;
  • 新加入到红黑树的节点为红色节点;

红黑树自平衡基本操作:

  • 变色:在不违反上述红黑树规则特点情况下,将红黑树某个node节点颜色由红变黑,或者由黑变红;
  • 左旋:逆时针旋转两个节点,让一个节点被其右子节点取代,而该节点成为右子节点的左子节点
  • 右旋:顺时针旋转两个节点,让一个节点被其左子节点取代,而该节点成为左子节点的右子节点
  • 源码
/**
 * 我们前面提到TreeMap是可以自动排序的,默认情况下comparator为null,这个时候按照key的自然顺序进行排
 * 序,然而并不是所有情况下都可以直接使用key的自然顺序,有时候我们想让Map的自动排序按照我们自己的规则,
 * 这个时候你就需要传递Comparator的实现类
 */
private final Comparator<? super K> comparator;
 
/**
 * TreeMap的存储结构既然是红黑树,那么必然会有唯一的根节点。
 */
private transient Entry<K,V> root;
 
/**
 * Map中key-val对的数量,也即是红黑树中节点Entry的数量
 */
private transient int size = 0;
 
/**
 * 红黑树结构的调整次数
 */
private transient int modCount = 0;

static final class Entry<K,V> implements Map.Entry<K,V> {
    //key,val是存储的原始数据
    K key;
    V value;
    //定义了节点的左孩子
    Entry<K,V> left;
    //定义了节点的右孩子
    Entry<K,V> right;
    //通过该节点可以反过来往上找到自己的父亲
    Entry<K,V> parent;
    //默认情况下为黑色节点,可调整
    boolean color = BLACK;
 
    /**
     * 构造器
     */
    Entry(K key, V value, Entry<K,V> parent) {
        this.key = key;
        this.value = value;
        this.parent = parent;
    }
 
    /**
     * 获取节点的key值
     */
    public K getKey() {return key;}
 
    /**
     * 获取节点的value值
     */
    public V getValue() {return value;}
 
    /**
     * 用新值替换当前值,并返回当前值
     */
    public V setValue(V value) {
        V oldValue = this.value;
        this.value = value;
        return oldValue;
    }
 
    public boolean equals(Object o) {
        if (!(o instanceof Map.Entry))
            return false;
        Map.Entry<?,?> e = (Map.Entry<?,?>)o;
        return valEquals(key,e.getKey()) && valEquals(value,e.getValue());
    }
 
    public int hashCode() {
        int keyHash = (key==null ? 0 : key.hashCode());
        int valueHash = (value==null ? 0 : value.hashCode());
        return keyHash ^ valueHash;
    }
 
    public String toString() {
        return key + "=" + value;
    }
}


//默认构造函数,按照key的自然顺序排列
public TreeMap() {comparator = null;}
//传递Comparator具体实现,按照该实现规则进行排序
public TreeMap(Comparator<? super K> comparator) {this.comparator = comparator;}
//传递一个map实体构建TreeMap,按照默认规则排序
public TreeMap(Map<? extends K, ? extends V> m) {
    comparator = null;
    putAll(m);
}
//传递一个map实体构建TreeMap,按照传递的map的排序规则进行排序
public TreeMap(SortedMap<K, ? extends V> m) {
    comparator = m.comparator();
    try {
        buildFromSorted(m.size(), m.entrySet().iterator(), null, null);
    } catch (java.io.IOException cannotHappen) {
    } catch (ClassNotFoundException cannotHappen) {
    }
}

——内部基本存储原理

内部包含一个 comparator 比较器(或值被置为Key的比较器,或是被置为外部传入的比较器),根结点 root (指向红黑树的根节点),记录修改次数 modCount (用于对集合结构性的检查),还有一个静态内部类(其实可以理解为一个树结点),其中有存储键和值的key和value,还有指向左孩子右孩子的“指针”,还有指向父结点的“指针”,最后还包括一个标志 color。也就是说,一个root指向树的根节点,而这个根结点又链接为一棵树,最后通过这个root可以遍历整个树。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

技术蜗牛-阿春

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

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

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

打赏作者

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

抵扣说明:

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

余额充值