【万文全解】Java集合源码解析【HashMap】【ArrayList】【JDK源码集合部分】

Java集合源码解析

本文主体部分是作者跟着B站韩顺平老师的课程总结而来,中间穿插自己的理解还有网上各类优质解答

第一节:集合介绍与集合体系图

集合与数组对比(引入集合的目的)

数组:

  1. 长度必须指定,一旦指定不能更改(无法动态扩容)
  2. 保存的是同一类元素

集合:

  1. 可以动态保存任意多个对象,比较方便
  2. 提供了方便操作对象的方法:add、remove、set、get等

主要是三类 List Set Map

  • List :存储有序、可重复
  • Set :存储无序、不可重复
  • Map : 使用k-v键值对进行存储

集合体系图1
集合体系图2

List

  • ArraylistObject[] 数组
  • VectorObject[] 数组
  • LinkedList : 双向链表(JDK1.6 之前为循环链表,JDK1.7 取消了循环)

Set

  • HashSet (⽆序,唯⼀): 基于 HashMap 实现的,底层采⽤ HashMap 来保存元素

  • LinkedHashSet : 是 HashSet 的⼦类,并且其内部是通过LinkedHashMap 来实现的。有点类似于我们之前说的 LinkedHashMap 其内部是基于HashMap 实现⼀样,不过还是有⼀点点区别的

  • TreeSet(有序,唯⼀): 红⿊树(⾃平衡的排序⼆叉树)

Map

  • HashMapJDK1.8 之前 HashMap 由数组+链表组成的,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突⽽存在的(“拉链法”解决冲突)。JDK1.8 以后在解决哈希冲突时有了较大的变化,当链表⻓度大于阈值(默认为 8)(将链表转换成红⿊树前会判断,如果当前数组的⻓度小于 64,那么会选择先进⾏数组扩容,⽽不是转换为红⿊树)时,将链表转化为红⿊树,以减少搜索时间
  • LinkedHashMapLinkedHashMap 继承⾃ HashMap ,所以它的底层仍然是基于拉链式散列结构即由数组和链表或红⿊树组成。另外, LinkedHashMap 在上⾯结构的基础上,增加了⼀条双向链表,使得上⾯的结构可以保持键值对的插⼊顺序。同时通过对链表进⾏相应的操作,实现了访问顺序相关逻辑。
  • Hashtable : 数组+链表组成的
  • TreeMap : 红⿊树(⾃平衡的⼆叉排序树)

补充

二叉树 -> 二叉搜索(排序)树 BST-> 平衡二叉树AVL -> 红黑树 RBT

如果想学习或复习红黑树的小伙伴,可以沿着这条线来学习,在这里不展开了

第二节:Collection接口常用方法

通过ArrayList子类来演示

add 添加
remove 删除
contains 判断是否存在
size 获取元素个数
isEmpty  是否为空
clear  清空
addAll 添加多个元素
containsAll 判断多个元素是否存在
removeAll  删除多个元素
    public static void main(String[] args) {
        ArrayList list1 = new ArrayList();
        list1.add("Tom");
        list1.add(10); //等同于 list1.add(new Integer(10));
        list1.add(true);
        System.out.println("list="+list1);//list=[Tom, 10, true]
        list1.add(3,"cat"); // 按照索引添加指定位置
        //list1.add(5,"json"); // 报错,数组越界 java.lang.IndexOutOfBoundsException

        list1.remove("cat"); // 删除指定元素
        list1.remove(0);  // 删除指定位置的元素
        System.out.println("list="+list1);

        ArrayList list2 = new ArrayList();
        list2.add("java");
        list2.add("spring");
        list2.add("netty");
        list1.addAll(list2);
        System.out.println("list="+list1); // 添加整个集合  list=[10, true, java, spring, netty]

    }

第三节:迭代器遍历 + 增强for

public class Collection2 {

    public static void main(String[] args) {
        ArrayList list = new ArrayList();
        list.add(new Subject("java","nb"));
        list.add(new Subject("spring","framworks"));
        list.add(new Subject("spring-boot","yyds"));

        Iterator iterator = list.iterator();
        while (iterator.hasNext()) { // 快捷键 itit
            Object next =  iterator.next();
            System.out.println(next.toString());
        }
        //iterator.next();  // 越界报错 NoSuchElementException
        //此时迭代器的指针 指向最后一个元素,如果想要将指针指向首部
        iterator = list.iterator();
    }
}
class  Subject{
    public String name;
    public String desc;

    public Subject(String name,String desc){
        this.name = name;
        this.desc = desc;
    }

    public void setDesc(String desc) {
        this.desc = desc;
    }

    public String getDesc() {
        return desc;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    @Override
    public String toString() {
        return "Subject is "+this.getName() + "--" +this.getDesc();
    }
}
    public static void main(String[] args) {
        ArrayList list = new ArrayList();
        list.add(new Subject("java","nb"));
        list.add(new Subject("spring","framworks"));
        list.add(new Subject("spring-boot","yyds"));
		// 快捷键 I

        for (Object subject :list) {
            System.out.println(subject.toString());
        }

    }

第四节:ArrayList 底层源码

扩展:List接口方法
void add(int index,Object obj) 指定位置添加元素
Object get(int index) 获取指定位置元素
int indexOf(Object obj) 返回元素首次出现位置
int lastIndexOf(Object obj)
Object remove(int index)  删除指定位置元素
Object set(int index,Object obj) 替换
List subList(int fromIndex,int toIndex) 取出子集(前闭后开,>= from   &&  < toIndex )
ArrayList 扩容机制

线程不安全,执行效率高,多线程下不建议使用

  1. ArrayList维护了一个Object类型的数组elementData
  2. 当创建ArrayList对象时,如果使用的是无参构造器,则初始容量为0,第一次添加,扩容到10,如果需要再次扩容,则扩大1.5倍
  3. 如果使用指定大小构造器,则初始为指定大小,如果要扩容,也按照1.5倍扩容
    成员属性
    在这里插入图片描述
ArrayList 底层源码

1 首先,创建一个ArrayList对象

ArrayList arrayList = new ArrayList();

底层原理:调用无参构造器

 public ArrayList() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }
//常量DEFAULTCAPACITY_EMPTY_ELEMENTDATA 的值为“{}”,为空数组

即使用ArrayList()创建ArrayList对象时,并没有初始化底层数组elementData,等到调用add(E e) 方法的时候再初始化,这种"懒加载"模式可以节省内存。

2 调用add(E e) 方法

 for (int i = 0; i < 10; i++) {
            arrayList.add(i);
        }

底层原理:

public boolean add(E e) {
        // 确认elementData容量是否足够
        ensureCapacityInternal(size + 1);  // 第一次调用add()方法时,size=0
        elementData[size++] = e;
        return true;
    }

先调用ensureCapacityInternal(int minCapacity)方法,对数组容量进行检查,不够时则进行扩容。

private void ensureCapacityInternal(int minCapacity) {
    // 如果elementData为"{}"即第一次调用add(E e),重新定义minCapacity的值,赋值为DEFAULT_CAPACITY=10
    // 即第一次调用add(E e)方法时,定义底层数组elementData的长度为10
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
        }
	// 判断是否需要扩容
        ensureExplicitCapacity(minCapacity);
    }

ensureExplicitCapacity(minCapacity) 判断是否需要扩容

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

// 第一次进入时,minCapacity=10,elementData.length=0,对数组进行扩容
// 之后再进入时,minCapacity=size+1,elementData.length=10(每次扩容后会改变),
// 需要minCapacity>elementData.length成立,才能扩容
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
    }

grow(minCapacity) 对数组进行扩容

private void grow(int minCapacity) {
        // 将数组长度赋值给oldCapacity
        int oldCapacity = elementData.length;
    	// 将oldCapacity右移一位再加上oldCapacity,即相当于newCapacity=1.5oldCapacity(不考虑精度损失)
        int newCapacity = oldCapacity + (oldCapacity >> 1);
    	// 如果newCapacity还是小于minCapacity,直接将minCapacity赋值给newCapacity
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;
    	// 特殊情况:newCapacity的值过大,直接将整型最大值赋给newCapacity,
	// 即newCapacity=Integer.MAX_VALUE
        if (newCapacity - MAX_ARRAY_SIZE > 0)
            newCapacity = hugeCapacity(minCapacity);
        // 将elementData的数据拷贝到扩容后的数组
        elementData = Arrays.copyOf(elementData, newCapacity);
    }

// 如果大于临界值,进行整型最大值的分配
private static int hugeCapacity(int minCapacity) {
        if (minCapacity < 0) // overflow
            throw new OutOfMemoryError();
        return (minCapacity > MAX_ARRAY_SIZE) ?
            Integer.MAX_VALUE :
            MAX_ARRAY_SIZE;
    }

第五节:Vector源码解析

  • Vector底层也是一个对象数组,Object[] elementData
  • Vector是线程同步的,即线程安全,是有synchronized修饰
  • 开发中如果需要线程安全,考虑使用Vector

VectorArrayList比较

底层结构版本安全/效率扩容倍数
ArrayList可变数组jdk1.2不安全、效率高如果有参构造1.5倍
如果无参,第一次10,第二次按照1.5倍
Vector可变数组jdk1.0安全、效率不高如果无参,默认10,第二次按照2倍
如果指定大小,则每次按照2倍

第六节:LinkedList源码解析

  1. LinkedList底层是一个双向链表
  2. LinkedList中有两个属性first last 分别指向头结点和尾结点
  3. 每个结点(Node对象)中又有三个属性 prev next item
  4. 添加和删除时间复杂度低,效率高
  5. 线程不安全的

在这里插入图片描述

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;
    }
}

1 查找

LinkedList 查找过程要稍麻烦一些,需要从链表头结点(或尾节点)向后查找,时间复杂度为 O(N)

public E get(int index) {
    checkElementIndex(index);
    return node(index).item;
}

Node<E> node(int index) {
    /*
     * 查找位置 index 如果小于节点数量的一半,
     * 从头节点开始查找,否则从尾节点查找
     * >>  位运算符,相当于/2   << 相当于 *2
     */    
    if (index < (size >> 1)) {
        Node<E> x = first;
        // 循环向后查找,直至 i == index
        for (int i = 0; i < index; i++)
            x = x.next;
        return x;
    } else {
        Node<E> x = last;
        for (int i = size - 1; i > index; i--)
            x = x.prev;
        return x;
    }
}

2 遍历

从头节点往后遍历就行了。通常情况下,我们会使用 foreach 遍历 LinkedList,而 增强for 最终转换成迭代器形式。

public ListIterator<E> listIterator(int index) {
    checkPositionIndex(index);
    return new ListItr(index);
}

private class ListItr implements ListIterator<E> {
    private Node<E> lastReturned;
    private Node<E> next;
    private int nextIndex;
    private int expectedModCount = modCount;

    /** 构造方法将 next 引用指向指定位置的节点 */
    ListItr(int index) {
        // assert isPositionIndex(index);
        next = (index == size) ? null : node(index);
        nextIndex = index;
    }

    public boolean hasNext() {
        return nextIndex < size;
    }

    public E next() {
        checkForComodification();
        if (!hasNext())
            throw new NoSuchElementException();

        lastReturned = next;
        next = next.next;    // 调用 next 方法后,next 引用都会指向他的后继节点
        nextIndex++;
        return lastReturned.item;
    }

    // 省略部分方法
}

3 插入

链表的优势所在

/** 在链表尾部插入元素 */
public boolean add(E e) {
    linkLast(e);
    return true;
}

/** 在链表指定位置插入元素 */
public void add(int index, E element) {
    checkPositionIndex(index);

    // 判断 index 是不是链表尾部位置,如果是,直接将元素节点插入链表尾部即可
    if (index == size)
        linkLast(element);
    else
        linkBefore(element, node(index));
}

/** 将元素节点插入到链表尾部 */
void linkLast(E e) {
    // 将尾指针保存
    final Node<E> l = last;
    // 创建节点,并指定节点前驱为链表尾节点 last,后继引用为空
    final Node<E> newNode = new Node<>(l, e, null);
    // 将 last 引用指向新节点
    last = newNode;
    // 判断尾节点是否为空,为空表示当前链表还没有节点
    if (l == null)
        first = newNode;
    else
        l.next = newNode;    // 让原尾节点后继引用 next 指向新的尾节点
    size++;
    modCount++;
}

/** 将元素节点插入到 succ 之前的位置 */
void linkBefore(E e, Node<E> succ) {
    // assert succ != null;
    final Node<E> pred = succ.prev;
    // 1. 初始化节点,并指明前驱和后继节点
    final Node<E> newNode = new Node<>(pred, e, succ);
    // 2. 将 succ 节点前驱引用 prev 指向新节点
    succ.prev = newNode;
    // 判断尾节点是否为空,为空表示当前链表还没有节点    
    if (pred == null)
        first = newNode;
    else
        pred.next = newNode;   // 3. succ 节点前驱的后继引用指向新节点
    size++;
    modCount++;
}

下面为linkBefore 插入到指定位置的图示

在这里插入图片描述

4 删除

public boolean remove(Object o) {
    if (o == null) {
        for (Node<E> x = first; x != null; x = x.next) {
            if (x.item == null) {
                unlink(x);
                return true;
            }
        }
    } else {
        // 遍历链表,找到要删除的节点
        for (Node<E> x = first; x != null; x = x.next) {
            if (o.equals(x.item)) {
                unlink(x);    // 将节点从链表中移除
                return true;
            }
        }
    }
    return false;
}

public E remove(int index) {
    checkElementIndex(index);
    // 通过 node 方法定位节点,并调用 unlink 将节点从链表中移除
    return unlink(node(index));
}

/** 将某个节点从链表中移除 */
E unlink(Node<E> x) {
    // assert x != null;
    final E element = x.item;
    final Node<E> next = x.next;
    final Node<E> prev = x.prev;

    // prev 为空,表明删除的是头节点
    if (prev == null) {
        first = next;
    } else {
        // 将 x 的前驱的后继指向 x 的后继
        prev.next = next;
        // 将 x 的前驱引用置空,断开与前驱的链接
        x.prev = null;
    }

    // next 为空,表明删除的是尾节点
    if (next == null) {
        last = prev;
    } else {
        // 将 x 的后继的前驱指向 x 的前驱
        next.prev = prev;
        // 将 x 的后继引用置空,断开与后继的链接
        x.next = null;
    }

    // 将 item 置空,方便 GC 回收
    x.item = null;
    size--;
    modCount++;
    return element;
}

简单来说:构建2个指针,删除4个指针

在这里插入图片描述

第 七节:HashSetHashMap 源码

  1. 无序(添加顺序和取出顺序不一致),没有索引
  2. 不允许重复元素,最多只有一个null
  3. 虽然取出顺序和添加顺序不一致,但是取出顺序是固定的,每次都一样
  4. 遍历方式:迭代器、增强for,但是不能通过索引

HashSet底层其实是 HashMapHashMap底层是数组+链表+红黑树

1 引入案例

为什么有些能重复,有些不能重复

public class HashSet1 {
    public static void main(String[] args) {


        HashSet set = new HashSet();
        System.out.println(set.add("java"));// true,相当于 new String("java")
        System.out.println(set.add("java"));// false,都是加在常量池中,所以判断重复

        System.out.println(set.add(new Lesson("spring")));// true
        System.out.println(set.add(new Lesson("spring")));// true
        // 打印结果: hashSet = [java, com.liangchen.Lesson@4554617c, com.liangchen.Lesson@1b6d3586] 可以看到地址是不同的
        System.out.println("hashSet = " + set);

        System.out.println(set.add(new String("redis")));// true
        System.out.println(set.add(new String("redis")));// false
        System.out.println("hashSet = " + set);
    }
}
class Lesson{
    private String name;

    public Lesson(String name) {
        this.name = name;
    }
}

需要先看后面的源码解析部分

解析如下:

  1. 判断是否重复原理

    if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
    
    • 首先判断 hash值 是否相同 ,是 hashcode 位运算得到的,结合上面例子 两个Lesson()是不同的对象,hash值 肯定不同,如图
    • 通过 == 判断 两个对象的 内存地址是否一样,也就是说是否为同一个对象
    • 最后通过 equals方法来比较,默认跟 == 一样,也是比较内存地址,但是该方法是可以由程序员重写的,自定义判断方法,比如:
    class Lesson{
        private String name;
    
        public Lesson(String name) {
            this.name = name;
        }
    	
        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (o == null || getClass() != o.getClass()) return false;
            Lesson lesson = (Lesson) o;
            // 只要 name 相同,就认为是 同一个对象
            return Objects.equals(name, lesson.name);
        }
        // 只要 name 相同就产生一样的hashcode  (多个成员属性也是同样道理)
        @Override
        public int hashCode() {
            return Objects.hash(name);
        }
    }
    

在这里插入图片描述

2 使用数组+链表 模拟HashMap

public class HashMapDemo {
    public static void main(String[] args) {
        // 使用 数组 + 链表 模拟 HashMap

        Node[] table = new Node[16];
        // 创建一个 结点
        Node nodeFrameWork = new Node("framework", null);
        table[2] = nodeFrameWork; // 保存到 数组 索引为 2 的位置
        Node nodeSpring = new Node("spring", null);
        Node nodeSpringBoot = new Node("springBoot", null);
        nodeFrameWork.next = nodeSpring; // 将 [spring] 结点 挂在到 [framework] 结点上,组成链表
        nodeSpring.next = nodeSpringBoot; // 将 [springBoot] 结点 挂在到 [spring] 结点上,组成链表

        Node nodeMQ = new Node("MQ", null);
        table[4] = nodeMQ; // 保存到 数组 索引为 4 的位置
        Node nodeRabbitMQ = new Node("rabbitMQ", null);
        Node nodeKafka = new Node("kafka", null);
        nodeMQ.next = nodeRabbitMQ; // 将 [rabbitMQ] 结点 挂在到 [MQ] 结点上,组成链表
        nodeRabbitMQ.next = nodeKafka; // 将 [kafka] 结点 挂在到 [rabbitMQ] 结点上,组成链表
    }
}

class Node{
    Object name;
    Node   next;

    public Node(Object name, Node next) {
        this.name = name;
        this.next = next;
    }
}

使用Debugger 查看内存结构如下:

在这里插入图片描述

3 源码分析

  1. HashSet 底层是 HashMap

  2. 添加一个元素时,先得到 hash值,会转换成 索引值

  3. 找到存储数据表的 table ,检查这个索引位置是否已存放数据元素

  4. 如果没有,直接加入

  5. 如果有,调用 equals() 比较,相同舍弃,不同则添加到最后

  6. Java8中,如果一条链表的元素个数到达默认值 8 个,并且table的大小大于 64,就会将链表 树化 转换成 红黑树,否则仍然采用数组扩容机制,扩大2倍

  7. 构造函数,底层使用的是HashMap

注意几个参数

initialCapacity 初始容量,默认16

laodFactor 加载/负载因子,默认0.75

threshold 阈值,默认 16*0.75 = 12

public HashSet() {
     map = new HashMap<>();
}
  1. 添加元素的add方法,会调用HashMap的put方法
public boolean add(E e) {
    return map.put(e, PRESENT)==null;
}
  1. 执行put方法,key是传入参数(存储的值,比如”张三“”hello world“),value是一个Object对象,占位,不放东西,始终不变
public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}
  1. 计算hash值,得到key对应的hash值,并不等同于hashcode,是 hashcode进行了位运算之后的数值
static final int hash(Object key) {
     int h;
     return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
  1. 进入putVal
    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;  //定义辅助变量
        // if语句表示,如果当前table是null,或者大小为0,进行第一次扩容,到16个空间
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        // 根据key,得到的hash值,计算key应该存放到table表的哪个索引位置,并把这个位置的对象赋值给 p
        // 判断 p 是否为空,如果为空表示还没放置过元素,就创建一个Node
        // p 就是计算出的 索引位置
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {  //如果p 不为空,证明已经放置过元素
            Node<K,V> e; K k;
            // 如果当前索引位置对应的链表的第一个元素和准备添加的key的hash值相同
            // 并且满足准备加入的 key 和 p 指向的Node节点的key是同一个对象  或者 内容相同
            // equals方法由程序员重写来决定,比如比较Person对象,1-名字相同 2-名字+性别相同 3-名字+身份证号相同,都是不固定的
            // 不能加入
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            // 再判断 p 是不是红黑树
            else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);  //如果是红黑树,调用putTreeVal添加
            else {
                //如果table对应的索引位置是一个链表,遍历链表进行判断
                for (int binCount = 0; ; ++binCount) {
                    // 这里判断是否为 链表 的最后一个结点
                    if ((e = p.next) == null) { // 依次和该链表的每一个元素比较后都不相同,则加入到该链表的最后
                        p.next = newNode(hash, key, value, null);
                        //添加到链表之后,立即判断该链表是否已经达到8个结点,达到了8个节点进行树化
                        // 是否真的树化,还需要判断表的长度是否超过64,小于64只是进行数组的扩容
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
                    // 依次比较,如果有相同的直接break
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            // 如果存在该值,就用新值覆盖旧值,同时会返回旧值
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;
        //size是每加入一个结点,size就会加一(并不是table中的索引被使用了12个才会扩容,而是加入的元素个数到达了12就要扩容,此时可能有4个元素在索引为1的地方,8个元素在索引为4的地方)
        if (++size > threshold)   //判断是否扩容,第一次添加的时候,table数组扩容到16,临界值是(16*加载因子)(默认是0.75)(第一次就是16*0.75=12),到达临界值进行扩容
            resize();
        afterNodeInsertion(evict);  //子类使用,HashMap是一个空方法
        return null;
    }

HashMap作用和遍历方式

  1. 保存key-value 映射关系数据
  2. Mapkey 不能重复(如果相同key,则会被后者覆盖),value可以重复

遍历的主要使用方式

  • keySet 获取所有的键

  • entrySet 获取所有映射关系 EntrySet<Map.Entry<K,V>>

  • values 获取所有的值

下图主要想说明HashMap的基础数据单元Entry<K,V> 和整体的图解(数组+链表)(未树化)

在这里插入图片描述

4 Debugger 调试查看HashSet扩容机制

public static void main(String[] args) {

        HashSet set = new HashSet();
        for (int i = 0; i < 100; i++) {
            set.add(i);
        }
    }
  1. 当第一次初始化HashSet时,默认大小为16,阈值为12

在这里插入图片描述

  1. 当添加元素个数超过阈值时,自动扩容两倍

在这里插入图片描述

  1. 同理最后扩大到64->128,因为不满足树化条件(1-如果一条链表的元素个数到达默认值 8 个 2-table的大小大于 64),所以继续采用数组扩容机制扩大2倍

5 Debugger 调试查看HashSet树化扩容机制

public class HashSet2 {
    public static void main(String[] args) {

        HashSet set = new HashSet();

        for (int i = 0; i < 12; i++) {
            set.add(new Dog("dog"+i));
        }

    }
}
class Dog{
    private String name;

    public Dog(String name) {
        this.name = name;
    }

    //重写方法,返回相同的hashcode
    @Override
    public int hashCode() {
        return 100;
    }
}
  1. 取个中间状态,比如添加了4个元素,数据结构如下:

在这里插入图片描述

  1. 当添加了8个元素,只满足了 树化 的一个条件(某条链表到达8个),但是不满足整体table 大于64,所以进行数组扩容,由 16 -> 32 -> 64
  2. 当满足两个条件时,底层进行树化,将链表 转化为 红黑树

在这里插入图片描述

扩容之后,要重新计算键值对的位置,并把它们移动到合适的位置上去
如上所示:原本在索引4的位置,扩容之后迁移到索引为36的位置

在这里插入图片描述

第八节:LinkedHashSet LinkedHashMap 源码解析

  1. LinkedHashSet底层是LinkedHashMap,底层是 数组 + 双向链表
  2. LinkedHashSet加入顺序和取出元素的顺序是一致的

因为底层是LinkedHashMap,我们可以看这张图

在这里插入图片描述

简单看下源码,重点是中间的 entry这数据对象

static class Entry<K,V> extends HashMap.Node<K,V> {
    // 可以看到 它继承了 HashMap中的 Node结点(有一个next指针)
    // 同时又新增了两个指针,前驱指针before,后继指针after
    Entry<K,V> before, after;
    Entry(int hash, K key, V value, Node<K,V> next) {
        super(hash, key, value, next);
    }
}

在这里插入图片描述

第九节:TreeSet TreeMap 源码解析

进行源码分析前,需要说明个容易误解的地方,底层使用的是红黑树,我们暂时可以不用分析,重点要注意存储的对象是Entry<K,V>是进行保存键值对的

但是TreeSet仅是单列数据,不是键值对,所以规定 K表示所要保存的数据,而V用一个new object固定占位符替代;跟上面分析HashSetHashMap异曲同工

public class TreeSet01 {
    public static void main(String[] args) {

//        TreeSet treeSet = new TreeSet();

        TreeSet treeSet = new TreeSet(new Comparator() {
            @Override
            public int compare(Object o1, Object o2) {
                // 通过字符串的ASCII比较大小  打印结果  [c, c++, java, php, python],此处请看源码分析
                return ((String) o1).compareTo ((String) o2);

                // 通过比较字符串长度, 打印结果  [c, c++, java, python]j,此时已插入长度为3的字符串c++,所以最后插入php时就插入不了
                // 通过源码分析,同理。此处比较的是字符串长度,当插入php时,已经存在了【c,c++,java,python】,进入循环体比较时,
                // 已经存在了长度为3的字符串,则仅仅只替换value,但此时的数据结构是Entry<K,V>  K->数据,V表示一个默认对象的占位符
                
                //return (((String) o1).length() - ((String) o2).length());
            }
        });

        /**
         *   如果使用默认的构造器,是按照ASCII排序
         *   如果想要排序,则需要自定义比较器,通过匿名内部类传入
         */

        treeSet.add("java");
        treeSet.add("python");
        treeSet.add("c");
        treeSet.add("java");
        treeSet.add("c++");
        treeSet.add("php");

        System.out.println(treeSet);
    }
}
// 此处是 TreeMap的底层添加方法
// 依此添加  java->python->c->java->c++ 我们重点比较插入相同元素的比较过程
// 如果是TreeSet调用,则值是key, value可以理解为一个固定的占位符;如果是TreeMap调用,则是键值对
public V put(K key, V value) {
        Entry<K,V> t = root;
    	// 添加首个元素时,root == null,后面添加就不进入判断
        if (t == null) {
            //添加第一个元素,只能自己与自己比较
            compare(key, key);

            root = new Entry<>(key, value, null);
            size = 1;
            modCount++;
            return null;
        }
        int cmp;
        Entry<K,V> parent;
        // split comparator and comparable paths
    	// 调用我们自定义的比较器
        Comparator<? super K> cpr = comparator;
    	// 此时TreeMap中存在 【java,python,c】三个元素,循环依此比较(使用比较器的自定义方法)
        if (cpr != null) {
            do {
                parent = t;
                cmp = cpr.compare(key, t.key);
                if (cmp < 0)
                    t = t.left;
                else if (cmp > 0)
                    t = t.right;
                else
                    // 此时比较得到 0 ,表示插入数据,已经存在,则覆盖value值
                    // 注意上面提到的,TreeSet中,key是值,value是一个对象占位符,无意义的,此时setVaule,并没有改变key的值,所以还是原来的值
                    return t.setValue(value);
            } while (t != null);
        }
        else {
        	//省略
        }
        Entry<K,V> e = new Entry<>(key, value, parent);
        if (cmp < 0)
            parent.left = e;
        else
            parent.right = e;
        fixAfterInsertion(e);
        size++;
        modCount++;
        return null;
    }

第十节:HashTable

哈希表、散列表

  1. 存放的是键值对 k - v
  2. 键和值都不能为null
  3. hashTable是线程安全的,hashMap是线程不安全的

整体扩容机制与HashMap类似

第十一节:Properties

  1. properties主要用于 从 .properties 文件中,加载数据到properties类对象,并读取和修改

第十二节:开发中如何选择集合的实现类

  1. 先判断存储的数据类型(是一组对象、还是一组键值对)
  2. 一组对象,选择Collction接口下的ListSet
    1. 允许数据重复,则选择List
      • 增删频繁,选择LinkedList(底层使用双向链表实现)
      • 修改查询频繁,选择ArrayList(底层使用的是Object类型的可变数组)
    2. 不允许数据重复,则选择Set
      • 允许无序排列,选择HashSet(底层使用HashMap,使用数组+链表+红黑树)
      • 要求排序,选择TreeSet(底层使用TreeMap,使用红黑树)
      • 只要求插入和取出顺序一致,则选择LinkedHashSet(底层使用 数组+双向链表)
  3. 一组键值对,选择Map接口下的实现类
    1. 键无序排列,则选择HashMap(在JDK8中,使用数组+链表+红黑树)
    2. 键要排序,则选择TreeMap(底层使用红黑树)
    3. 键插入顺序和取出顺序一致,选择LinkedHashMap

第十三节:Collections工具类

Collections上一个操作Set List Map 集合的工具列,提供静态方法可以方便的进行排序、查询、修改操作

  • reverse(List) 反转List中元素
  • shuffle(List) 随机取出
  • sort(List) 默认排序ASCII
  • sort(List,Comparator) 自定义比较器排序
  • swap(List,int,int) 交换两个位置数据
  • max
  • min
  • frequency(Collection,Object) 返回指定集合中,某个元素出现的次数
  • copy(List desc,List src) 复制
  • replaceAll(List list,Object old,Object new) 替换

参考
《LinkedHashMap 源码详细分析(JDK1.8)》https://www.imooc.com/article/22931

《详解Java集合框架LinkedHashSet和LinkedHashMap源码剖析(图)》https://www.php.cn/java-article-359154.html

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值