集合

集合

  • 1)可以动态保存任意多个对象,使用比较方便!!!
  • 2)提供了一系列方便的操作对象的方法:add,remove,set,get等
  • 3)使用集合添加,删除新元素的示意代码 --- 简洁明了

集合框架体系

  • 单列

image-20221110175919759

  • 双列

image-20221110180603414

注意:在 AbstractCollection () 中重写了 toString() 方法

image-20221212161849278

public String toString() {
    Iterator<E> it = iterator();
    if (! it.hasNext())
        return "[]";

    StringBuilder sb = new StringBuilder();
    sb.append('[');
    for (;;) {
        E e = it.next();
        sb.append(e == this ? "(this Collection)" : e);
        if (! it.hasNext())
            return sb.append(']').toString();
        sb.append(',').append(' ');
    }
}

一、Collection(单列集合)

Collection接口实现类的特点

  1. collection 实现子类可以存放多个元素,每个元素可以是 Object
  2. 有些Collection 的实现类,可以存放重复的元素,有些不可以
  3. 有些Collection 的实现类,有些是有序的(List),有些不是有序(Set)
  4. Collection 接口没有直接的实现子类,是通过它的子接口 Set 和 List来实现的

常用方法:

方法名 作用
add 添加单个元素
remove 删除
contains 查找元素是否存在
size 获取元素个数
isEmpty 判断元素是否为空
clear 清空
addAll 添加多个元素
containsAll 查找多个元素是否存在
reomveAll 删除多个元素
List list = new ArrayList();
list.add(10);	//list.add(new Integer(10));

remove方法有2种情况:

  • remove(Object o):返回 boolean
  • remove(Object index):返回被删除的对象
  • 由于整数类型既可以看作int,又可以视为Object,当我们从 List 中移除一个整数时候,是有歧义的,正确方法如下
int value = 250;
//	移除List 中下标为 250 的元素
list.remove(value);
//	移除List 中首个数值等于 250 的元素
list.remove((Integer)value);
//	我们想要从 List 中移除一个整数,一定要先将其转换为 Integer
//	而且每次只能移除一个元素,如果有多个值相同的,需要通过以下代码判断是否已经完全删除
boolean finish = !list.contains((Integer)value);

迭代器遍历

通过 iterator() 方法,可以返回 Iterator 对象

public interface Iterable<T> {
    /**
     * Returns an iterator over elements of type {@code T}.
     *
     * @return an Iterator.
     */
    Iterator<T> iterator();
  1. Iterator对象称为迭代器,主要用于遍历 Collection集合中的元素
  2. 所有实现了Collection接口的集合类都有一个 iterator()方法,用以返回一个实现了Iterator接口的对象,即可以返回一个迭代器
  3. Iterator的结构
  4. Iterator 仅用于遍历集合,Iterator本身并不存放对象

配合 hasNext() 和 next() 方法一起使用

public interface Iterator<E> {
    /**
     * Returns {@code true} if the iteration has more elements.
     * (In other words, returns {@code true} if {@link #next} would
     * return an element rather than throwing an exception.)
     *
     * @return {@code true} if the iteration has more elements
     */
    boolean hasNext();

    /**
     * Returns the next element in the iteration.
     *
     * @return the next element in the iteration
     * @throws NoSuchElementException if the iteration has no more elements
     */
    E next();
//	next() 方法有2个作用
//	1)下移
//	2)将下移以后集合位置上的元素返回
System.out.println(iterator.next());

while快捷键:itit

while (iterator.hasNext()) {
    Object next =  iterator.next();
}
//	当退出while循环后,这时 iterator迭代器,指向最后的元素
//	iterator.next():NoSuchElementException
//	如果希望再次遍历,需要重置我们的迭代器
//	iterator = col.iterator();
System.out.println("========第二次遍历===========");
while (iterator.hasNext()) {
    Object next =  iterator.next();
}

使用 Ctrl + j:显示所有的快捷键

集合增强 for

可以代替iterator迭代器,特点:增强for就是简化版的 iterator,本质一样,只能用于遍历j集合或数组。

package com.collection_;

import java.util.ArrayList;
import java.util.Collection;

/**
 * @Author: Ronnie LEE
 * @Date: 2022/1/9 - 01 - 09 - 23:46
 * @Description: com.collection_
 * @version: 1.0
 */
public class CollectionFor {
    public static void main(String[] args) {
        Collection col = new ArrayList();
        col.add(new Book("三国演义", "罗贯中", 10.1));
        col.add(new Book("小李飞刀", "古龙", 5.1));
        col.add(new Book("红楼梦", "曹雪芹", 34.6));
        //  老韩解读
        //  1. 使用增强 for 循环,在Collection集合
        //  2. 增强for,底层仍然是迭代器
        //  3. 增强for可以理解成就是简化版本的 迭代器遍历
        //  4. 快捷方式 I
       for (Object book : col){
           System.out.println("book=" + book);
       }
//        //  增强 for, 也可以直接在数组使用
//        int[] nums = {1, 8, 10, 90};
//        for (int i : nums){
//            System.out.println("i = " + i);
//        }
    }
}

image-20221110202231138

1. List

List接口是 Collection 接口的子接口

  1. List集合中元素有序(即:添加顺序和取出顺序一致),且可重复
  2. List容器中的每一个元素都有其对应的顺序索引,即支持索引
  3. List容器中的元素都对应一个整数型的序号记载其在容器中的位置,可以根据序号存取容器中的元素
  4. JDK API中 List接口的实现类有:AbstractList , AbstractSequentialList , ArrayList , AttributeList , CopyOnWriteArrayList(并发) , LinkedList , RoleList , RoleUnresolvedList , Stack , Vector

注意:这里的 List 指的是:util 包下的 util

java.util.List

常用方法

方法名 作用
void add(int index, Object ele) 在index位置,插入ele元素
boolean addAll(int index, Collection eles) 从index位置开始,将eles中的所有元素添加进来
Object get(int index) 获取指定index位置的元素
int indexOf(Object obj) 返回 obj 在集合中首次出现的位置
int lastIndexOf(Object obj) 返回 obj 在集合中末次出现的位置
Object remove(int index) 移除指定 index 位置的元素,并返回此元素
Object set(int index, Object ele) 设置指定 index 位置的元素为 ele,相当于是替换
List subList(int fromIndex, int toIndex) 返回从 fromIndex 到 toIndex 位置的子集合

三种遍历方式

  1. 使用 iterator迭代器

    Iterator iter = list.iterator();
    while(iter.hasNext()){
        Object o = iter.next();
    }
    
  2. 使用增强for

    for(Object o : list){
        
    }
    
  3. 使用普通for

    for(int i = 0; i < list.size(); i++){
        object o = list.get(i);
    }
    

List 练习:

普通冒泡排序

public static int[] bubble(int[] arr){
    for (int e = arr.length - 1; e > 0; e--) {
        for (int i = 0; i < e; i++) {	//	每次确定一个数
            if (arr[i] > arr[i + 1]){
                swap(arr, i, i + 1);
            }
        }
    }
    return arr;
}
//	交换
public static void swap(int[] arr, int i, int j) {
    arr[i] = arr[i] ^ arr[j];
    arr[j] = arr[i] ^ arr[j];
    arr[i] = arr[i] ^ arr[j];
}
//	打印
public static void printArray(int[] arr) {
    if (arr == null) {
        return;
    }
    for (int i = 0; i < arr.length; i++) {
        System.out.print(arr[i] + " ");
    }
    System.out.println();
}

如何对集合进行排序:

public static void sort(List list){
    for (int e = list.size() - 1; e > 0; e--) {
        for (int i = 0; i < e; i++) {
            //  取出对象 Book
            Book book1 = (Book)(list.get(i));
            Book book2 = (Book)(list.get(i + 1));
            if (book1.getPrice() > book2.getPrice()){   //  交换
                list.set(i, book2);
                list.set(i + 1, book1);
            }
        }
    }
}

实现子类

1.1 ArrayList
Arraylist基本定义
  1. 允许所有元素,包括 null,可以放置多个 null
  2. 底层是数组
  3. ArrayList基本等同于 Vector,但是 ArrayList是线程不安全的(执行效率高),多线程下不建议使用
//	ArrayList 是线程不安全的,可以看源码,没有 synchronized 关键字
public boolean add(E e) {
        modCount++;
        add(e, elementData, size);
        return true;
}
ArrayList扩容机制
  1. ArrayList中维护了一个Object 类型的数组 elementData 【Debug看源码】

  2. transient Object[] elementData; // transient 表示 瞬间,短暂的,表示该属性不会被序列化

  3. 当创建ArrayList对象时

    • 如果使用的是无参构造器,则初始elementData容量为 0,第1次添加,则扩容elementData 为10,如需要再次扩容,则扩容elementData为1.5倍。【0 ---> 10 ---> 15 ---> 15 + (15 / 2) = 22】

    • 如果使用的是指定大小的构造器,则初始elementData容量为指定大小,如果需要扩容,则直接扩容 elementData为1.5倍

      image-20221111151418536

   /**
     * The array buffer into which the elements of the ArrayList are stored.
     * The capacity of the ArrayList is the length of this array buffer. Any
     * empty ArrayList with elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA
     * will be expanded to DEFAULT_CAPACITY when the first element is added.
     */
    transient Object[] elementData; // non-private to simplify nested class access
ArrayList底层源码

无参构造器,初始为一个 null 数组

/**
     * Shared empty array instance used for default sized empty instances. We
     * distinguish this from EMPTY_ELEMENTDATA to know how much to inflate when
     * first element is added.
     */
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

//	无参构造器
public ArrayList() {
    this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;	//	初始化为一个 null 数组
}

使用 for 循环添加数据,int 会先装箱成 Integer ,再进行添加

//  使用for循环添加 1 -10 数据
for (int i = 1; i < 10; i++) {
    list.add(i);	//	会将 int 进行装箱成 Integer
}

@HotSpotIntrinsicCandidate
public static Integer valueOf(int i) {
    if (i >= IntegerCache.low && i <= IntegerCache.high)
        return IntegerCache.cache[i + (-IntegerCache.low)];
    return new Integer(i);
}

开始添加,调用 boolean add(E e)方法 ----> add(E e,Object[] elementData, int s)

//	开始添加,调用 boolean add(E e)
 public boolean add(E e) {
        modCount++;	//	记录当前集合被修改的次数,防止有多个线程同时去修改,如果同时修改会抛出异常
        add(e, elementData, size);
        return true;
  }

private void add(E e, Object[] elementData, int s) {
    if (s == elementData.length)	//	第二次添加2的时候,由于 1 != 10,就不执行 grow() 方法了
        elementData = grow();
    elementData[s] = e;
    size = s + 1;
}

调用 grow() 方法 ---> grow(int minCapacity)

当添加11时,调用 grow()---> grow(11),minCapacity:当前需要的最小空间

  • oldCapacity = elementData.length【当前集合大小】
  • newCapacity = ArraySupport.newLength(oldCapacity, minCapacity - oldCapacity, oldCapacity >> 1)
  • newCapacity = ArraySupport.newLength(10,11 - 10,10 >> 1) = ArraySupport.newLength(10,1,5);
  • newCapacity = 10 + 5 =15
private Object[] grow() {
    return grow(size + 1);
}

private Object[] grow(int minCapacity) {
    int oldCapacity = elementData.length;
    if (oldCapacity > 0 || elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {	//	不是初始化时,判定新的大小 newCapacity
        int newCapacity = ArraysSupport.newLength(oldCapacity,
                                                  minCapacity - oldCapacity, /* minimum growth */
                                                  oldCapacity >> 1           /* preferred growth */);
        return elementData = Arrays.copyOf(elementData, newCapacity);
    } else {//	如果 elementData 是一个 null 数组,直接扩容为大小为 10 的数组
        return elementData = new Object[Math.max(DEFAULT_CAPACITY, minCapacity)];
    }
}

private static final int DEFAULT_CAPACITY = 10;	//	默认大小为:10

newLength()方法,来判定是否扩容

扩容使用的是:Arrays.copyOf(elementData, newCapacity) 方法

public static final int MAX_ARRAY_LENGTH = Integer.MAX_VALUE - 8;

public static int newLength(int oldLength, int minGrowth, int prefGrowth) {
        // assert oldLength >= 0
        // assert minGrowth > 0

        int newLength = Math.max(minGrowth, prefGrowth) + oldLength;	//	nweLenth = Math.max() + 10;
        if (newLength - MAX_ARRAY_LENGTH <= 0) {	//	不越界的情况
            return newLength;
        }
        return hugeLength(oldLength, minGrowth);
    }
 private static int hugeLength(int oldLength, int minGrowth) {
        int minLength = oldLength + minGrowth;
        if (minLength < 0) { // overflow
            throw new OutOfMemoryError("Required array length too large");
        }
        if (minLength <= MAX_ARRAY_LENGTH) {
            return MAX_ARRAY_LENGTH;
        }
        return Integer.MAX_VALUE;
    }

注意:IDEA工具,默认情况下,Debug显示的数据是简化后的,所以,你有些数据看不到。如果想要看到完整的数据,需要做设置

将 Data Views ---> Java 中的 Enable alternative view for Collections classed 拿掉

image-20221111164709268


使用有参构造器的情况

除了初始大小不是10,其余完全一样

ArrayList list = new ArrayList(8);
 public ArrayList(int initialCapacity) {
     if (initialCapacity > 0) {
         this.elementData = new Object[initialCapacity];
     } else if (initialCapacity == 0) {
         this.elementData = EMPTY_ELEMENTDATA;
     } else {
         throw new IllegalArgumentException("Illegal Capacity: "+
                                            initialCapacity);
     }
 }
private void add(E e, Object[] elementData, int s) {
    if (s == elementData.length)	8 == 8
        elementData = grow();
    elementData[s] = e;
    size = s + 1;
}
private Object[] grow() {
    return grow(size + 1);	//	grow(9)
}

grow(9):

  • oldCapacity = elementData.length = 8【当前集合大小】
  • newCapacity = ArraySupport.newLength(oldCapacity, minCapacity - oldCapacity, oldCapacity >> 1)
  • newCapacity = ArraySupport.newLength(8,9 - 1,8 >> 1) = ArraySupport.newLength(8,1,4);
  • newCapacity = 10 + 4 =12
    private Object[] grow(int minCapacity) {
        int oldCapacity = elementData.length;
        if (oldCapacity > 0 || elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            int newCapacity = ArraysSupport.newLength(oldCapacity,
                    minCapacity - oldCapacity, /* minimum growth */
                    oldCapacity >> 1           /* preferred growth */);
            return elementData = Arrays.copyOf(elementData, newCapacity);
        } else {
            return elementData = new Object[Math.max(DEFAULT_CAPACITY, minCapacity)];
        }
    }
1.2 LinkedList

image-20221213104310903

LinkedList说明
  1. LinkedList实现了双向链表和双端队列特点
  2. 可以添加任意元素(元素可以重复),包括 null
  3. 线程不安全,没有实现同步
LinkedList底层操作机制
  1. LinkedList底层维护了一个 双向链表
  2. LinkedList中维护了两个属性 first 和 last 分别指向 首节点和尾节点
  3. 每个节点(Node对象),里面又维护了pre、next、item三个属性,其中通过 prev 指向前一个,通过 next 指向后一个节点。最终实现双向链表
  4. 由于LinkedList元素添加和删除,不是通过数组完成的,效率高(因为不涉及数组的扩容)
  5. 模拟模拟一个简单的双向链表

image-20221112150030185

public class LinkedList01 {
    public static void main(String[] args) {
        //  模拟一个简单的双向链表
        Node jack = new Node("jack");
        Node tom = new Node("tom");
        Node hsp = new Node("老韩");

        //  连接3个结点,形成双向链表
        //  jack --> tom --> hsp
        jack.next = tom;
        tom.next = hsp;
        //  hsp --> tom --> jack
        hsp.pre = tom;
        tom.pre = jack;

        Node first = jack; //   让first引用指向 jack,就是双向链表的头结点
        Node last = hsp; //   让last引用指向 hsp,就是双向链表的尾结点


        //  演示:从头到尾进行遍历
        System.out.println("=====从头到尾进行遍历======");
        while (true) {
            if (first == null) {
                break;
            }
            //  输出 first 信息
            System.out.println(first);
            first = first.next;
        }

        //  演示:从尾到头进行遍历
        System.out.println("=====从尾到头进行遍历======");
        while (true) {
            if (last == null) {
                break;
            }
            //  输出 last 信息
            System.out.println(last);
            last = last.pre;
        }

        //  演示:链表的添加对象 / 数据,是多么的方便
        //  要求:是在 tom --- 老韩 之间插入一个对象 “smith”

        //  1. 先创建一个 Node 结点,name 就是 smith
        Node smith = new Node("smith");
        //  下面就把smith 加入到双向链表了	[4条线,从自己开始]
        smith.next = hsp;
        smith.pre = tom;
        hsp.pre = smith;
        tom.next = smith;

        //  由于之前在遍历的时候,first 已经走到最后了,所以现在要让 first 重新指向 开头的 jack
        first = jack;
        //  演示:从头到尾进行遍历
        System.out.println("=====从头到尾进行遍历======");
        while (true) {
            if (first == null) {
                break;
            }
            //  输出 first 信息
            System.out.println(first);
            first = first.next;
        }

        //  同理,如果要重新倒序排列的时候,要将 last 重新指向 最后一个元素 "老韩"
        last = hsp;
        //  演示:从尾到头进行遍历
        System.out.println("=====从尾到头进行遍历======");
        while (true) {
            if (last == null) {
                break;
            }
            //  输出 last 信息
            System.out.println(last);
            last = last.pre;
        }
    }
}

//  定义一个Node 类, Node 对象表示双向链表的一个结点。
class Node{
    public Object item; //  真正存放数据
    public Node next;   //  指向后一个结点
    public Node pre;    //  指向前一个结点
    public Node(Object name) {
        this.item = name;
    }

    public String toString() {
        return "Node name=" + item;
    }
}
LinkedList CRUD案例

增加 add

LinkedList linkedList = new LinkedList();
linkedList.add(1);
linkedList.add(2);
linkedList.add(3);
System.out.println("linkedList=" + linkedList);

add()方法

public boolean add(E e) {
    linkLast(e);
    return true;
}

linkLast(e) 方法

void linkLast(E e) {
    final Node<E> l = last;	//	l 指向未添加前的尾节点
    final Node<E> newNode = new Node<>(l, e, null);	//	new Node(pre, element, next)
    last = newNode;
    if (l == null)	//	假如当前节点为 null,则 first 指向新节点
        first = newNode;
    else
        l.next = newNode;
    size++;	//	链表长度
    modCount++;	//	链表操作次数
}

调用构造器创建新的节点 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;
    }
}

删除 remove() <===> removeFirst() 删除头结点

删除一个结点的源码

linkedList.remove();
public E remove() {
    return removeFirst();
}

调用 removeFirst() 方法

public E removeFirst() {
    final Node<E> f = first;
    if (f == null)
        throw new NoSuchElementException();
    return unlinkFirst(f);
}

调用 unlinkFirst() 方法,将 f 指向的双向链表的第一个结点 拿掉

private E unlinkFirst(Node<E> f) {	//	f 为 不为 null 的头指针
    // assert f == first && f != null;
    final E element = f.item;	//	该方法返回头指针指向结点的 item
    final Node<E> next = f.next;	//	next:头指针的下一结点
    
    //	1)头指针 item 置空,next置空,指针右移,当前结点的 pre 置空
    f.item = null;
    f.next = null; // help GC ---> 将 item 也置空的原因
    first = next;
    if (next == null)
        last = null;
    else
        next.prev = null;
    size--;
    modCount++;
    return element;	//	返回被删除的内容
}

修改 set

linkedList.set(1, 999);

调用 E set(int index, E element)

public E set(int index, E element) {
    checkElementIndex(index);
    Node<E> x = node(index);
    E oldVal = x.item;	
    x.item = element;	//	更新旧值为 element
    
    return oldVal;	//	返回的是旧值
}
private void checkElementIndex(int index) {
    if (!isElementIndex(index)){
        throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
    }
}
private boolean isElementIndex(int index) {	//	判断该索引确实存在元素
	return index >= 0 && index < size;
}

返回指定索引的 非null 结点

Node<E> node(int index) {
    // assert isElementIndex(index);
    if (index < (size >> 1)) {	//	折半查找
        Node<E> x = first;
        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;	//	后半段:从后往前找
    }
}

查询 get

得到某个结点的对象

//	get(1):得到双向链表的第 2 个对象
Object o = linkedList.get(1);
public E get(int index) {
    checkElementIndex(index);	//	确认该索引处,确实存在元素
    return node(index).item;	//	利用二分法,得到该结点,最后返回结点的 item
}
LinkedList和ArrayList比较
底层结构 增删的效率 改查的效率
ArrayList 可变数组 较低(数组扩容) 较高(通过索引)
LinkedList 双向链表 较高(通过链表追加) 较低

一般来说:

实际开发中,很大部分都是查询,因此大部分会选择 ArrayList

在一个项目中,可以灵活选择,一个模块使用的是 ArrayList,另一个模块使用 LinkedList

1.3 Vector
Vector基本定义
  1. Vector类的定义说明
public class Vector<E>
    extends AbstractList<E>
    implements List<E>, RandomAccess, Cloneable, java.io.Serializable
  1. 底层也是一个对象数组
 protected Object[] elementData;
  1. 线程同步,即线程安全,Vector类的常用方法带有 synchronized
public synchronized E get(int index) {
    if (index >= elementCount)
        throw new ArrayIndexOutOfBoundsException(index);

    return elementData(index);
}
  1. 在开发中,需要线程同步安全时,考虑使用 Vector

image-20221111193021299

Vector和ArrayList比较
底层结构 版本 线程安全(同步)效率 扩容倍数
ArrayList 可变数组 jdk1.2 不安全,效率高 有参:1.5(无参:10)
Vector 可变数组 jdk1.0 安全,效率不高(线程同步,每个方法都会做安全校验) 有参:2(无参:10)
Vector扩容机制
public class Vector_ {
    public static void main(String[] args) {
        //  无参构造器
        Vector vector = new Vector();
        for (int i = 0; i < 10; i++) {
            vector.add(i);
        }
        vector.add(100);
        System.out.println("vector=" + vector);
    }
}
public Vector() {
    this(10);	//	initialCapacity:10
}

public Vector(int initialCapacity) {
    this(initialCapacity, 0);
}

public Vector(int initialCapacity, int capacityIncrement) {
    super();
    if (initialCapacity < 0)
        throw new IllegalArgumentException("Illegal Capacity: "+
                                           initialCapacity);
    this.elementData = new Object[initialCapacity];
    this.capacityIncrement = capacityIncrement;
}

调用 add()方法

public synchronized boolean add(E e) {
    modCount++;
    add(e, elementData, elementCount);
    return true;
}

调用 add(E e, Object[] elements, int s) 方法

private void add(E e, Object[] elementData, int s) {
    if (s == elementData.length)	//	0 != 10,不扩容
        elementData = grow();
    elementData[s] = e;
    elementCount = s + 1;
}

当添加第 11个元素时:

(s == elementData.length) = 10;	//	调用 grow() 方法

调用grow(element + 1) ---> grow(11)

private Object[] grow() {
    return grow(elementCount + 1);
}

调用 grow(11)

  • oldCapacity = 10
  • newCapacity = ArraySupport.newLength(10,1,10) = 20 = oldCapacity + oldCapacity = 2 * oldCapacity
//	由于 capacityIncrement 默认是0,所以 三元运算符 运算后,只能取到 oldCapacity
private Object[] grow(int minCapacity) {
    int oldCapacity = elementData.length;
    int newCapacity = ArraysSupport.newLength(oldCapacity,
                                              minCapacity - oldCapacity, /* minimum growth */
                                              capacityIncrement > 0 ? capacityIncrement : oldCapacity
                                              /* preferred growth */);
    return elementData = Arrays.copyOf(elementData, newCapacity);
}
public static int newLength(int oldLength, int minGrowth, int prefGrowth) {
    // assert oldLength >= 0
    // assert minGrowth > 0

    int newLength = Math.max(minGrowth, prefGrowth) + oldLength;
    if (newLength - MAX_ARRAY_LENGTH <= 0) {
        return newLength;
    }
    return hugeLength(oldLength, minGrowth);
}

有参构造器类似

2. Set

Set接口是 Collection 接口的子接口

  1. 无序(添加和取出的顺序不一致),没有索引
  2. 不允许重复元素,所以最多包含一个 null
  3. JDK API 中Set 接口的实现类有:AbstractSet , ConcurrentSkipListSet , CopyOnWriteArraySet , EnumSet , HashSet , JobStateReasons , LinkedHashSet , TreeSet

常用方法

和List接口一样,Set接口也是 Collection的子接口,因此,常用方法和 Collection 接口一样

两种遍历方式

  1. 迭代器
  2. 增强 for

注意:不能用普通索引的方式获取,因为 Set 没有索引

实现子类

2.1 HashSet [只有当 hashCode() 和 equals() 都相同时才加不进去]
  • HashSet实现了 Set 接口
  • HashSet实际上是 HashMap
  • 可以存放 null 值,但是只能有一个 null
  • HashSet不保证元素是有序的,取决于 hash后,再确定索引的结果(即:不保证存放元素的顺序和取出的顺序一致)
  • 不能有重复的元素 / 对象,在前面 Set 接口使用已经说过
Set set = new HashSet();
set.add("lucy");	//	T	都指向常量池(都是字符串常量)
set.add("lucy");	//	F	
set.add(new Dog("tom"));	//	T
set.add(new Dog("tom"));	//	T

再加深一下,非常经典的面试题

//	看源码,即:add() 方法到底发生了什么
set.add(new String("hsp"));
set.add(new String("hsp"));	//	False

  • remove() 指定删除某个对象
//	看下构造器,发现 HashSet的底层是 HashMap
Set set = new HashSet();
/**
     * Constructs a new, empty set; the backing {@code HashMap} instance has
     * default initial capacity (16) and load factor (0.75).
     */
/**
	* 构造一个新的空集
	* 后备 {@code HashMap} 实例具有
	* 默认的初始容量 (16)
	* 负载因子 (0.75)
	*/
public HashSet() {
    map = new HashMap<>();
}

public HashMap() {
    this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
//  说明
//  1. 在执行add 方法后,会返回一个 boolean值
//  2. 如果成功了,返回 true;否则返回 false
//  3. 可以通过 remove 指定删除具体某个对象
System.out.println(set.add("john"));    //  T	
System.out.println(set.add("lucy"));    //  T
System.out.println(set.add("john"));    //  F
System.out.println(set.add("jack"));    //  T
System.out.println(set.add("Rose"));    //  T
set.remove("john");
System.out.println("set" + set);    //  3个 顺序不保证哦
1. HashSet底层机制

分析 HashSet底层是 HashMap,而 HashMap底层是(数组 + 链表 + 红黑树)

当它的链表到达一定量的时候,而且满足数组的大小,在某一范围的时候,就会把链表进行树化,变成一个红黑树

模拟简单的 数组 + 链表 的结构

public class HashSetStructure {
    public static void main(String[] args) {
        //  模拟一个HashSet的底层 (HashMap 的底层结构)

        //  1. 创建一个数组,数组的类型就是 Node
        //  2. 有些人,直接把 Node[] 数组称为表
        Node[] table = new Node[16];
        System.out.println("table=" + table);
        //  3. 创建结点
        Node john = new Node("john", null);
        table[2] = john;
        Node jack = new Node("jack", null);
        john.next = jack;   //  将 jack 结点挂载到 john
        Node rose = new Node("Rose", null);
        jack.next = rose;   //  将 rose 结点挂载到 jack

        Node lucy = new Node("lucy", null);
        table[3] = lucy;    //  把lucy 放到 table 表的索引为3的位置
        System.out.println("table" + table);
    }
}

class Node {    //  结点,存储数据,可以指向下一个结点,从而形成链表。
    Object item;    //  存放数据
    Node next;      //  指向下一个结点

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

12

2. HashSet 扩容机制(Node<K, V>[] resize() )
  • HashSet 底层是 HashMap

  • 添加一个元素时,先得到 hash 值 ---> 会转成 索引值

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

    计算 hash 值

    static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }
    

    再执行 putVal(int hash, K key, V value, boolean onlyIfAbsenet, boolean evict)

    得到索引值:

     if ((p = tab[i = (n - 1) & hash]) == null)	
    
    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 ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;	//	n:table 数组大小
        if ((p = tab[i = (n - 1) & hash]) == null)		//	索引 i = (n - 1) & hash = 15 & 3254803 = 3
            tab[i] = newNode(hash, key, value, null);
    
  • 找到存储数据表 table,看这个索引位置是否已经存放有元素?

    • 没有:直接加入
    • 有:调用 equals 比较 【程序员控制 equals 方法具体比较标准】
      • 如果相同,就放弃添加
      • 如果不相同,则以链表的方式添加
3. 初始化(第一次 add)
public class HashSetSource {
    public static void main(String[] args) {
        HashSet hashSet = new HashSet();
        hashSet.add("java");    //  到此为止,第一次add分析完毕
        hashSet.add("php");     //  到此为止,第二次add分析完毕
        hashSet.add("java");    //  第三次故意写了一个相同的,我们来看看它怎么完成不添加的流程的。
        System.out.println("hashset=" + hashSet);
        //  老韩对 HashSet的源码解读
    }
}
//	key="java", key.hashCode() = 3254818
//	^:异或运算 XOR
//	hash("key")  = 3254818 ^ (3254818 >>> 16) = 3254803

//	计算 key.hashCode() 并将哈希的较高位传播(XOR)到较低位。由于该表使用二次幂掩码,因此仅在当前掩码之上位变化的散列集将始终发生冲突。 (已知的例子是在小表中保存连续整数的 Float 键集。)因此,我们应用了一种变换,将高位的影响向下传播。在位扩展的速度、实用性和质量之间存在折衷。因为许多常见的散列集已经合理分布(所以不要从传播中受益),并且因为我们使用树来处理 bin 中的大量冲突,我们只是以最便宜的方式对一些移位的位进行异或,以减少系统损失,以及合并最高位的影响,否则由于表边界,这些最高位将永远不会用于索引计算。

//右移16位是为了让高16位也参与运算,可以更好的均匀散列,减少碰撞,进一步降低hash冲突的几率
//异或运算是为了更好保留两组32位二进制数中各自的特征

static final int hash(Object key) {
    int h;	//	保存 key 的 哈希值
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

if ((p = tab[i = (n - 1) & hash]) == null)	//	索引 i = (n - 1) & hash = 15 & 3254803 = 3

image-20221114111653797

看下 add() 方法

// Dummy value to associate with an Object in the backing Map
//	起到占位的作用
//	为了让 HashSet 使用到 HashMap,它的 value 统一放了一个值 PRESENT
//	将来不管你传进来多少次,key 是变化的,但是 value 始终都是 PRESENT【static:共享的】
private static final Object PRESENT = new Object();

public boolean add(E e) {	//	e = "java"
        return map.put(e, PRESENT)==null;	//	map.put方法,返回 null,就是添加成功了---> true
}

调用 V put(K key, V value) 方法

public V put(K key, V value) {	//	K: 哈希值,V:对象
    return putVal(hash(key), key, value, false, true);
}

调用 int hash(key) 方法

  • 得到 key 对应的 哈希值,以便后面确认在 table 中的位置
  • 并不完全等价 hashCode,因为要防止冲突
static final int hash(Object key) {
    int h;	//	保存 key 的 哈希值
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

看到 HashSet 中有 map HashMap<E, Object>字段

image-20221114004454058

再看下 HashMap中有 table 属性(放 Node 结点的一个数组)

image-20221114004730886

调用 V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) 方法 (false、true)

/**
     * The table, initialized on first use, and resized as
     * necessary. When allocated, length is always a power of two.
     * (We also tolerate length zero in some operations to allow
     * bootstrapping mechanics that are currently not needed.)
     */
transient Node<K,V>[] table;	//	HashMap的一个属性 【放 Node 结点的一个数组(刚才模拟的那个数组)】
/**
     * Implements Map.put and related methods.
     *
     * @param hash hash for key
     * @param key the key
     * @param value the value to put
     * @param onlyIfAbsent if true, don't change existing value
     * @param evict if false, the table is in creation mode.
     * @return previous value, or null if none
     */
/**
     * 实现 Map.put 和相关方法
     *
     * @param hash:key 的 哈希值
     * @param key :key
     * @param value:要放入的值(对象)
     * @param onlyIfAbsent:如果为真,不要改变现有的值
     * @param evict:如果为假,表处于创建模式
     * @return previous value(返回旧对象), or null if none(如果没有则返回 null)
     */
final V 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 ((tab = table) == null || (n = tab.length) == 0)	//	初始化
        n = (tab = resize()).length;	//	n = 16(初始化容量大小为: 16)
    if ((p = tab[i = (n - 1) & hash]) == null)	//	计算 key 对应的 hash值,去计算该 key 应该在 table 表的哪一个索引位置去存放,并把这个位置的对象 赋给 辅助变量 p
        //	再判断是否为 null
        //	1)如果 p 为 null,表示还没有存放过元素,就创建一个 Node(key="java", value = PRESENT)放在该位置
        //	为什么把 key 对应的 hash 也存进去了,因为将来会去比较,如果再有的话,它会去看,如果相等会往后怼
        tab[i] = newNode(hash, key, value, null);
    else {
        //	辅助变量
        Node<K,V> e; K k;
        if (p.hash == hash &&	//	不能添加
            ((k = p.key) == key || (key != null && key.equals(k))))
            e = p;
        else if (p instanceof TreeNode)	//	树
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        else {	//	链表【新加入的 Node,与链表中的 Node依次比较】
            for (int binCount = 0; ; ++binCount) {	
                if ((e = p.next) == null) {	//	能加的情况,还要看是否要树化,e = null
                    p.next = newNode(hash, key, value, null);
                    if (binCount >= TREEIFY_THRESHOLD - 1) // TREEIFY-THRESHOLD = 8【binCount 从0开始计数,当binCount = 0 时候,表示将要插入到第 2个位置;依次类推,binCount = 1 时,表示要插入到第 3个位置;所以当 binCount = 7时,表要要插入到第 9个位置。此时进行树化
                        treeifyBin(tab, hash);
                    break;
                }
                if (e.hash == hash &&	//	如果在循环过程中,发现有相同,就不添加了 break【e != null】只是替换 value
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                p = e;
            }
        }
        if (e != null) { // existing mapping for key
            V oldValue = e.value;	//e = p (链表的首位置)
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;	// 更新 e.value = 新传进来的 value
            afterNodeAccess(e);
            return oldValue;
        }
    }
    ++modCount;	//	添加成功的次数
    if (++size > threshold)	//	 1 > threshold = 12 false
        resize();
    afterNodeInsertion(evict);	//	evict 默认是 true,在结点后插入 【对于 HashMap来说是空方法,目的是为了让它的子类去是实现这个方法,再做一些动作的】
    return null;
}
Node<K, V>[] resize() 方法

调用 Node<K, V>[] resize() 方法,初始化或加倍表大小

  • 如果为 null,则按照字段阈值中保存的初始容量目标进行分配
  • 否则,因为我们使用二次幂展开,每个 bin 中的元素必须保持相同的索引,或者在新表中以二次幂的偏移量移动
/**
     * Initializes or doubles table size.  If null, allocates in
     * accord with initial capacity target held in field threshold.
     * Otherwise, because we are using power-of-two expansion, the
     * elements from each bin must either stay at same index, or move
     * with a power of two offset in the new table.
     *
     * @return the table
     */
//	allocates:分配
//	in accord with :符合
//	power of two:二次幂

final Node<K,V>[] resize() {
    Node<K,V>[] oldTab = table;
    int oldCap = (oldTab == null) ? 0 : oldTab.length;	//	旧容量
    int oldThr = threshold;	//	旧阈值(初始为:0)
    int newCap, newThr = 0;
    if (oldCap > 0) {
        if (oldCap >= MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return oldTab;
        }
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                 oldCap >= DEFAULT_INITIAL_CAPACITY)
            newThr = oldThr << 1; // double threshold
    }
    else if (oldThr > 0) // initial capacity was placed in threshold
        newCap = oldThr;
    else {               // zero initial threshold signifies using defaults(初始化)
        newCap = DEFAULT_INITIAL_CAPACITY;	//	16
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    }
    if (newThr == 0) { //	新阈值:16 * 0.75 = 12 != 0【table大小有16,但是只要用了12个,就要开始扩容了】
        float ft = (float)newCap * loadFactor;
        newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                  (int)ft : Integer.MAX_VALUE);
    }
    threshold = newThr;		//	更新 阈值 threshold = 12
    @SuppressWarnings({"rawtypes","unchecked"})
    Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];	//	创建初始默认容量大小数组(16)
    table = newTab;
    if (oldTab != null) {	//	初始化时,oldTab == null,后面的代码都不执行,直接走到最后的 return
        for (int j = 0; j < oldCap; ++j) {
            Node<K,V> e;
            if ((e = oldTab[j]) != null) {
                oldTab[j] = null;
                if (e.next == null)
                    newTab[e.hash & (newCap - 1)] = e;	//	扩容后 索引位置也会同步更新
                else if (e instanceof TreeNode)
                    ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                else { // preserve order
                    Node<K,V> loHead = null, loTail = null;
                    Node<K,V> hiHead = null, hiTail = null;
                    Node<K,V> next;
                    do {
                        next = e.next;
                        if ((e.hash & oldCap) == 0) {
                            if (loTail == null)
                                loHead = e;
                            else
                                loTail.next = e;
                            loTail = e;
                        }
                        else {
                            if (hiTail == null)
                                hiHead = e;
                            else
                                hiTail.next = e;
                            hiTail = e;
                        }
                    } while ((e = next) != null);
                    if (loTail != null) {
                        loTail.next = null;
                        newTab[j] = loHead;
                    }
                    if (hiTail != null) {
                        hiTail.next = null;
                        newTab[j + oldCap] = hiHead;
                    }
                }
            }
        }
    }
    return newTab;	//	返回初始容量大小数组
}

上方 代码 42 行,将 table 容量更新为:16【table = null】

默认初始容量:16

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

默认的加载因子:0.75

/**
     * The load factor used when none specified in constructor.
     */
static final float DEFAULT_LOAD_FACTOR = 0.75f;
4. 添加不同的元素(第二次 add)
//	hash("php")= 110968 ^ (110968 >>> 16) = 110968 ^ 1 = 110969
//	i = (n - 1) & hash = 15 & 110969 = 9
static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
 }
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 ((tab = table) == null || (n = tab.length) == 0)	
        n = (tab = resize()).length;	
    if ((p = tab[i = (n - 1) & hash]) == null)	//	计算 key 对应的 hash值,去计算该 key 应该在 table 表的哪一个索引位置去存放,并把这个位置的对象 赋给 辅助变量 p
        tab[i] = newNode(hash, key, value, null);
    else {
        Node<K,V> e; K k;
        if (p.hash == hash &&	//	只有满足 2个条件,才不添加
            ((k = p.key) == key || (key != null && key.equals(k))))
            e = p;
        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;
                }
                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;	//	修改的次数:2
    if (++size > threshold)	//	 2 > threshold = 12 false
        resize();
    afterNodeInsertion(evict);	//	evict 默认是 true,在结点后插入
    return null;
}

image-20221114114722520

5. 添加已有元素

再加入一次 "java"

//	key="java", key.hashCode() = 3254818
//	^:异或运算 XOR
//	hash("key")  = 3254818 ^ (3254818 >>> 16) = 3254803

if ((p = tab[i = (n - 1) & hash]) == null)	//	索引 i = (n - 1) & hash = 15 & 3254803 = 3
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 ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    if ((p = tab[i = (n - 1) & hash]) == null)	//	P指向当前索引对应的链表的第一个元素。【不为 null,走到下方 else 语句】
        tab[i] = newNode(hash, key, value, null);
    else {
        //	开发技巧提示:在需要局部变量(辅助变量)时候,再创建
        Node<K,V> e; K k;
        // [看能否添加]
        if (p.hash == hash && //  如果当前索引位置对应的链表的第一个元素 和 准备添加的 key 的哈希值一样
            ((k = p.key) == key || (key != null && key.equals(k))))	//	并且满足 下面2个条件之一:
        //	1)准备加入的 key 和 p 指向的 Node结点的 key 是同一个对象 
        //	2)p 指向的 Node 结点的 key 【k = p.key】和 准备加入的 key 经过 equals()比较后相同 (动态绑定)
            e = p;
        //	[能添加,看是否是一棵树]
        else if (p instanceof TreeNode)		//	再判断 p 是不是一棵红黑树
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        //	[能添加,且不是树 ===> 索引位置是一个链表]
        else {	//	如果 table 对应索引位置,已经是一个链表,就用 for 循环比较
            //	1)依次和该链表的每一个元素比较后,都不相同,则加入到该链表的最后
            //	注意在把元素添加到链表后,立即判断 该链表是否已经达到 8 个结点:
            //		如果达到了:就调用 treeifyBin() 对当前这个链表进行树化(转成红黑树)
		   //       注意:在转成红黑树时,还进行判断, 如果该 table 数组的大小 < 64,则先调用 resize() 方法对 table表进行扩容
            //	2)依次和该链表的每一个元素比较过程中,如果有相同的情况,就直接break
            	
            for (int binCount = 0; ; ++binCount) {	//	循环比较(死循环)
                if ((e = p.next) == null) {	//	e 指向下一个
                    p.next = newNode(hash, key, value, null);
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st ---> TREEIFY_THRESHOLD - 1 = 7(从 0 开始,当等于7 时,就说明有 8 个元素了)
                        treeifyBin(tab, hash);
                    break;
                }
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                p = e;	//	e = p.next (重新循环)
            }
        }
        if (e != null) { // existing mapping for key
            V oldValue = e.value;	//	其实就是我们的 PRESENT
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            afterNodeAccess(e);
            return oldValue;	//	返回 oldValue,不为 null,所以最后 add() 方法返回失败
        }
    }
    /*
    该 HashMap 被结构修改的次数 结构修改是那些改变 HashMap 中的映射数量或以其他方式修改其内部结构(例如,rehash)的修改。该字段用于使 HashMap 的集合视图上的迭代器快速失败。 (请参阅并发修改异常)。
    */
    ++modCount;	
    if (++size > threshold)
        resize();
    afterNodeInsertion(evict);
    return null;
}

只有2中情况会 break

  • e = p.next == null:将新结点挂在后面【++modCount】
  • key 和链表中相同,直接 break,return oldValue,添加失败

image-20221114144809276

   /**
     * 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.
     */

	/**
	  * 使用树而不是列表的 bin 计数阈值
	  * 将元素添加到至少具有这么多节点的 bin 时,bin 将转换为树。
	  * 该值必须大于 2 并且应该至少为 8,以便与树移除中关于在收缩时转换回普通 bin 的假设相吻合。
	  */

static final int TREEIFY_THRESHOLD = 8;
/**
     * Replaces all linked nodes in bin at index for given hash unless
     * table is too small, in which case resizes instead.
     */
/**
	替换给定哈希索引处的 bin 中的所有链接节点,除非表太小,在这种情况下调整大小。
*/
final void treeifyBin(Node<K,V>[] tab, int hash) {
    int n, index; Node<K,V> e;
    if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)	//	64
        resize();
    else if ((e = tab[index = (n - 1) & hash]) != null) {
        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);
    }
}
/**
     * The smallest table capacity for which bins may be treeified.
     * (Otherwise the table is resized if too many nodes in a bin.)
     * Should be at least 4 * TREEIFY_THRESHOLD to avoid conflicts
     * between resizing and treeification thresholds.
     */
/**
  * 可对其进行树化的 bin 的最小表容量。 (否则,如果 bin 中有太多节点,则调整表的大小。)应至少为 4       * TREEIFY_THRESHOLD 以避免调整大小和树化阈值之间的冲突。
  */

static final int MIN_TREEIFY_CAPACITY = 64;	//	4 * 16 = 64

未达到 64,则先通过 resize() 方法进行扩容

/**
     * Initializes or doubles table size.  If null, allocates in
     * accord with initial capacity target held in field threshold.
     * Otherwise, because we are using power-of-two expansion, the
     * elements from each bin must either stay at same index, or move
     * with a power of two offset in the new table.
     *
     * @return the table
     */
final Node<K,V>[] resize() {
    Node<K,V>[] oldTab = table;
    int oldCap = (oldTab == null) ? 0 : oldTab.length;
    int oldThr = threshold;
    int newCap, newThr = 0;
    if (oldCap > 0) {
        if (oldCap >= MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return oldTab;
        }
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                 oldCap >= DEFAULT_INITIAL_CAPACITY)
            newThr = oldThr << 1; // double threshold
    }
    else if (oldThr > 0) // initial capacity was placed in threshold
        newCap = oldThr;
    else {               // zero initial threshold signifies using defaults
        newCap = DEFAULT_INITIAL_CAPACITY;
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    }
    if (newThr == 0) {
        float ft = (float)newCap * loadFactor;
        newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                  (int)ft : Integer.MAX_VALUE);
    }
    threshold = newThr;
    @SuppressWarnings({"rawtypes","unchecked"})
    Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
    table = newTab;
    if (oldTab != null) {
        for (int j = 0; j < oldCap; ++j) {
            Node<K,V> e;
            if ((e = oldTab[j]) != null) {
                oldTab[j] = null;
                if (e.next == null)
                    newTab[e.hash & (newCap - 1)] = e;
                else if (e instanceof TreeNode)
                    ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                else { // preserve order
                    Node<K,V> loHead = null, loTail = null;
                    Node<K,V> hiHead = null, hiTail = null;
                    Node<K,V> next;
                    do {
                        next = e.next;
                        if ((e.hash & oldCap) == 0) {
                            if (loTail == null)
                                loHead = e;
                            else
                                loTail.next = e;
                            loTail = e;
                        }
                        else {
                            if (hiTail == null)
                                hiHead = e;
                            else
                                hiTail.next = e;
                            hiTail = e;
                        }
                    } while ((e = next) != null);
                    if (loTail != null) {
                        loTail.next = null;
                        newTab[j] = loHead;
                    }
                    if (hiTail != null) {
                        hiTail.next = null;
                        newTab[j + oldCap] = hiHead;
                    }
                }
            }
        }
    }
    return newTab;
}
6. HashSet转成红黑树机制
  1. HashSet底层是 HashMap,第一次添加时,table 数组扩容到 16,临界值(threshold)是 16 * 加载因子(loadFactor = 0.75) = 12
  2. 如果table 数组使用到了 临界值 12,就会扩容到 16 * 2 = 32,新的临界值就是 32 * 0.75 = 24,依此类推
  3. 在 Java8 中,如果一条链表的
    • 元素个数到达 TREEIFY_THRESHOLD(默认是 8)
    • 并且 table 的大小 >= MIN_TREEIFY_CAPACITY(默认是 64),就会进行树化(红黑树),否则仍然采用数组扩容机制

image-20221114154007790

public class HashSetIncrement {
    public static void main(String[] args) {
        /*
        HashSet底层是HashMap,第一次添加时,table 数组扩容到 16,
        临界值(threshold)是 16 * 加载因子(loadFactor)是0.75 = 12
        如果table 数组使用到了临界值 12,就会扩容到 16 * 2 = 32,
        新的临界值就是 32 * 0.75 = 24,依次类推
        */
        HashSet hashSet = new HashSet();
        for (int i = 1; i < 100; i++) {
            hashSet.add(i); //  1,2,3,4,5 ... 100 【数字不同,hash之后得到的索引可能相同】
        }
    }
}

image-20221114160742223

添加第13个元素时

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 ((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);	//	添加第 13个元素
    else {
        Node<K,V> e; K k;
        if (p.hash == hash &&	//	hash 相同【首先确保索引相同了】
            ((k = p.key) == key || (key != null && key.equals(k))))	// 链表首位的 k 和要加入的 key 是一个,或者经过人工设定的 equal方法判定相等了 ===> 就舍弃要假如的节点
            e = p;
        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;
                }
                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;	//	13(该表操作了13次)
    if (++size > threshold)	//	++12 > 12
        resize();	//	调用 resize()方法扩容
    afterNodeInsertion(evict);
    return null;
}

调用 resize() 方法扩容

final Node<K,V>[] resize() {
        Node<K,V>[] oldTab = table;
        int oldCap = (oldTab == null) ? 0 : oldTab.length;	//	16
        int oldThr = threshold;	// 12
        int newCap, newThr = 0;
        if (oldCap > 0) {
            if (oldCap >= MAXIMUM_CAPACITY) {	// 12 >= 1<<20 false
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&	//	容量翻倍
                     oldCap >= DEFAULT_INITIAL_CAPACITY)	
                newThr = oldThr << 1; // double threshold  门限翻倍
        }
        else if (oldThr > 0) // initial capacity was placed in threshold
            newCap = oldThr;
        else {               // zero initial threshold signifies using defaults
            newCap = DEFAULT_INITIAL_CAPACITY;
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        }
        if (newThr == 0) {
            float ft = (float)newCap * loadFactor;
            newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                      (int)ft : Integer.MAX_VALUE);
        }
        threshold = newThr;	//	24
        @SuppressWarnings({"rawtypes","unchecked"})
        Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];	//	新建一个容量 为 32 的 table
        table = newTab;
        if (oldTab != null) {
            for (int j = 0; j < oldCap; ++j) {	// oldCap = 16
                Node<K,V> e;
                if ((e = oldTab[j]) != null) {
                    oldTab[j] = null;
                    if (e.next == null)
                        newTab[e.hash & (newCap - 1)] = e;	//	将 oldTab中的值拷贝到 newTab 中去【扩容之后,索引位置重新计算】
                    else if (e instanceof TreeNode)
                        ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                    else { // preserve order
                        Node<K,V> loHead = null, loTail = null;
                        Node<K,V> hiHead = null, hiTail = null;
                        Node<K,V> next;
                        do {
                            next = e.next;
                            if ((e.hash & oldCap) == 0) {
                                if (loTail == null)
                                    loHead = e;
                                else
                                    loTail.next = e;
                                loTail = e;
                            }
                            else {
                                if (hiTail == null)
                                    hiHead = e;
                                else
                                    hiTail.next = e;
                                hiTail = e;
                            }
                        } while ((e = next) != null);
                        if (loTail != null) {
                            loTail.next = null;
                            newTab[j] = loHead;
                        }
                        if (hiTail != null) {
                            hiTail.next = null;
                            newTab[j + oldCap] = hiHead;
                        }
                    }
                }
            }
        }
        return newTab;	//	返回 newTab
}

以此类推,当添加第 25个元素时候:table 的大小变为:64

image-20221114161512536

人为构造 hash 一样,但是 值不同的场景

// 当加入第 9 个元素时,table扩容至 32 【因为 table 的 size 还未达到 64,所以先数组扩容】
添加元素个数 table大小
1 - 8 16
9 32
10 64
11 🌳树化
public class HashSetIncrement_1 {
    public static void main(String[] args) {
        HashSet hashSet = new HashSet();

        /*  java8中,如果一条链表的元素个数到达 TREEIFY_THRESHOLD(默认是8),
            并且table的大小 >= MIN_TREEIFY_CAPACITY(默认是64),就会进行树化(红黑树),
            否则仍然采用数组扩容机制
         */
        for (int i = 1; i < 12; i++) {
             hashSet.add(new A(i));
        }
        System.out.println("hashset" + hashSet);

class A {
    private int n;

    public A(int n) {
        this.n = n;
    }

    @Override
    public int hashCode() {
        return 100;
    }
}

可以看到把 1 和 2放到同一条链表上去了

image-20221114163445943

当加入第 9 个元素时,table扩容至 32 【因为 table 的 size 还未达到 64,所以先数组扩容 】

image-20221114194919499

当加入第 10个元素时,还是会触发扩容机制,table 扩容至:64

image-20221114194948712

newTab[e.hash & (newCap - 1)] = e

扩容后,索引位置会重新计算

image-20221114195156653

注意:当加入第11个元素时,会进行树化

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 ((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 {	//	p != null 
        Node<K,V> e; K k;
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            e = p;
        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	bincount = 9 时,执行
                        treeifyBin(tab, hash);
                    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;
    if (++size > threshold)
        resize();
    afterNodeInsertion(evict);
    return null;
}
final void treeifyBin(Node<K,V>[] tab, int hash) {
    int n, index; Node<K,V> e;
    if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)	//	n = tab.length = 64 【MIN_TREEIFY_CAPACITY=64】
        resize();
    else if ((e = tab[index = (n - 1) & hash]) != null) {
        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);
    }
}
// For treeifyBin
TreeNode<K,V> replacementTreeNode(Node<K,V> p, Node<K,V> next) {
    return new TreeNode<>(p.hash, p.key, p.value, next);
}
/**
         * Forms tree of the nodes linked from this node.
         * 从此节点链接的节点的表单树
         */
final void treeify(Node<K,V>[] tab) {
    TreeNode<K,V> root = null;
    for (TreeNode<K,V> x = this, next; x != null; x = next) {
        next = (TreeNode<K,V>)x.next;
        x.left = x.right = null;
        if (root == null) {
            x.parent = null;
            x.red = false;
            root = x;
        }
        else {
            K k = x.key;
            int h = x.hash;
            Class<?> kc = null;
            for (TreeNode<K,V> p = root;;) {
                int dir, ph;
                K pk = p.key;
                if ((ph = p.hash) > h)
                    dir = -1;
                else if (ph < h)
                    dir = 1;
                else if ((kc == null &&
                          (kc = comparableClassFor(k)) == null) ||
                         (dir = compareComparables(kc, k, pk)) == 0)
                    dir = tieBreakOrder(k, pk);

                TreeNode<K,V> xp = p;
                if ((p = (dir <= 0) ? p.left : p.right) == null) {
                    x.parent = xp;
                    if (dir <= 0)
                        xp.left = x;
                    else
                        xp.right = x;
                    root = balanceInsertion(root, x);
                    break;
                }
            }
        }
    }
    moveRootToFront(tab, root);
}

可以看到:类型已经变成 TreeNode

image-20221114201600374

7. 临界值 12
//	size:就是我们每加入一个结点 Node(k, v, hash, next),size ++
//	1)不管你是加在 tab 表的第一个位置
//	2)还是加载 tab 表的某一条链表上
//	都算是一个 size	
++modCount;	
if (++size > threshold)	
    resize();	//	扩容
afterNodeInsertion(evict);
return null;

例子如下:

public class HashSetIncrement_1 {
    public static void main(String[] args) {
        HashSet hashSet = new HashSet();
        /*
             当我们向 hashset增加一个元素, --> Node --> 加入table,就算是增加了一个,size++
         */
        for (int i = 1; i <= 7 ; i++) { //  在table的某一条链条上添加了 7个A对象
            hashSet.add(new A(i));  //
        }

        for (int i = 1; i <= 7 ; i++) { //  在table的另外一条链表上添加了 7个B对象
            hashSet.add(new B(i));  //
        }
    }
}

class B{
    private int n;

    public B(int n) {
        this.n = n;
    }

    @Override
    public int hashCode() {
        return 200;
    }
}

class A {
    private int n;

    public A(int n) {
        this.n = n;
    }

    @Override
    public int hashCode() {
        return 100;
    }
}

8. 最佳实践
  1. 定义一个Employee类,该类包含:private 成员属性 name, age,要求:
    • 创建 3个 Employee 放入 HashSet 中
    • 当 name 和 age 值相同时,认为是相同员工,不能添加到 HashSet 集合中
public class GGBoy {
    public static void main(String[] args) {
        HashSet hashSet = new HashSet();
        hashSet.add(new Employee("东七七", 11));
        hashSet.add(new Employee("东七七", 11));
        hashSet.add(new Employee("东七七", 22));
        hashSet.add(new Employee("东66", 11));	//	退100步来说,假设 hashcode 一样,经过 equals 比较后为 false,还是可以添加进去
        System.out.println(hashSet);
    }
}

class Employee{
    private String name;
    private int age;

    public Employee(String name, int age) {
        this.name = name;
        this.age = age;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Employee employee = (Employee) o;
        return age == employee.age &&
            Objects.equals(name, employee.name);
    }

    @Override
    public int hashCode() {
        return Objects.hash(name, age); //  如果 name 和 age 相同,那么 hashCode 就相同 ===> 确定索引
    }

    @Override
    public String toString() {
        return "Employee{" +
            "name='" + name + '\'' +
            ", age=" + age +
            '}';
    }
}
  1. 定义一个 Employee 类,该类包含:private成员属性 name、sal、birthday(MyDate类型)其中 birthday 为MyDate 类型(属性包含:year,month,day)
  • 创建3个 Employee 放入 HashSet 中
  • 当 name 和 birthday 的值相同时,认为是相同员工,不能添加到 HashSet 集合中
package collections_;

import java.util.HashSet;
import java.util.Objects;

/**
 * @Author: Ronnie LEE
 * @Date: 2022/11/14 - 11 - 14 - 20:31
 * @Description: collections_
 * @version: 1.0
 */
public class GGBoy {
    public static void main(String[] args) {

        HashSet hashSet = new HashSet();
        hashSet.add(new Employee("faker", 25000, new MyDate("1996", "5", "12")));
        hashSet.add(new Employee("faker", 30000, new MyDate("1996", "5", "12")));
        hashSet.add(new Employee("Bang", 88888, new MyDate("1994", "11", "3")));
        hashSet.add(new Employee("faker", 88888, new MyDate("1994", "11", "22")));
        for (Object o : hashSet) {
            if (o instanceof Employee) {
                Employee employee = (Employee) o;
                System.out.println(employee);
            }
        }
    }
}

class Employee {
    private String name;
    private double sal;
    MyDate birthday;

    public Employee(String name, double sal, MyDate birthday) {
        this.name = name;
        this.sal = sal;
        this.birthday = birthday;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Employee employee = (Employee) o;
        return Objects.equals(name, employee.name) &&
                Objects.equals(birthday, employee.birthday);
    }

    @Override
    public int hashCode() {
        return Objects.hash(name, birthday);
    }

    @Override
    public String toString() {
        return "Employee{" +
                "name='" + name + '\'' +
                ", sal=" + sal +
                ", birthday=" + birthday +
                '}';
    }
}


class MyDate{
    private String year;
    private String month;
    private String day;

    public MyDate(String year, String month, String day) {
        this.year = year;
        this.month = month;
        this.day = day;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        MyDate myDate = (MyDate) o;
        return Objects.equals(year, myDate.year) &&
                Objects.equals(month, myDate.month) &&
                Objects.equals(day, myDate.day);
    }

    @Override
    public int hashCode() {
        return Objects.hash(year, month, day);
    }

    @Override
    public String toString() {
        return "MyDate{" +
                "year='" + year + '\'' +
                ", month='" + month + '\'' +
                ", day='" + day + '\'' +
                '}';
    }
}

2.2 LinkedHashSet
  1. LinkedHashSet 是 HashSet 的子类
  2. LinkedHashSet 底层是一个 LinkedHashMap,底层维护了一个 数组 + 双向链表
  3. LinkedHashSet 根据元素的 hashCode 值来决定元素的位置,同时使用双向链表维护元素的 次序,这使得元素看起来是以插入顺序保存的
  4. LinkedHastSet 不允许添重复元素

image-20221114234412324

1. LinkedHashSet 底层机制示意图
  • 在LinkedHashSet 中维护了一个 hash 表和双向链表(LinkedHashSet 有 head 和 tail)

  • 每一个节点有 pre 和 next 属性,这样就可以形成双向链表

  • 在添加一个元素时,先求 hash值,再求索引,确定该元素在 hashtable 的位置,然后将添加的元素加入到双向链表(如果已经存在,不添加【原则和 hashset 一样】

    tail.next = newElement;	//	简单指定 【2条线】
    newElement.pre = tail;
    
    tail = newElement;	//	移动尾指针
    
  • 这样的话,我们遍历 LinkedHashSet 也能确保插入顺序和遍历顺序一致

image-20221115003419261

public class LinkedHashSetSource {
    public static void main(String[] args) {
        //  分析一下LinkedHashSet的底层机制
        Set set = new LinkedHashSet();
        set.add(new String("AA"));
        set.add(456);
        set.add(456);
        set.add(new Customer("刘", 1001));
        set.add(123);
        set.add("HSP");

        System.out.println("set" + set);
        //  老韩解读
        //  1.  LinkedHashSet 加入顺序和取出元素/数据的顺序一致
        //  2.  LinkedHashSet 底层维护的是一个LinkedHashMap(是HashMap的子类)
        //  3.  LinkedHashSet 底层结构(数组table + 双向链表)
        //  4.  添加第一次时,直接将 数组table 扩容到16,存放的结点类型是 LinkedHashMap$Entry
        //  5.  数组是 HashMap&Node[]  存放的元素/数据是 LinkedHashMap$Entry类型

        /*
             // 继承关系是在内部类完成
             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);
                }
             }
         */
    }
}

class Customer{

    private String name;
    private int no;

    public Customer(String name, int no) {
        this.name = name;
        this.no = no;
    }
}
  • 数组是 HashMap$Node[] 类型的,
  • 存放的元素 / 数据是:LinkedHashMap$Entry 类型

image-20221209114539829

//	在 LinkedHashMap.java 中
//	继承关系是在内部类完成
/**
     * HashMap.Node subclass for normal LinkedHashMap entries.
     */
static class Entry<K,V> extends HashMap.Node<K,V> {	//	这个 Node<K, V> 也是静态内部类,为什么做成静态的,因为这个Node<K, V>类是专门为我们 HashMap 这条线使用的
    Entry<K,V> before, after;
    Entry(int hash, K key, V value, Node<K,V> next) {
        super(hash, key, value, next);
    }
}

由于 LinkedHashSet 是 HashSet 的子类,所以走的还是之前的那一套

public boolean add(E e) {
    return map.put(e, PRESENT)==null;
}
2. 课后练习

Car 类(属性:name,price),如果 name 和 price 一样,则认为是相同元素,就不能添加

//	重写 equals 方法 和 hashCode
//	当 name 和 price 相同时,就返回相同的 hashCode 值,equals 返回 true
public class ho123 {
    public static void main(String[] args) {
        Set linkedHashSet = new LinkedHashSet();
        linkedHashSet.add(new Car1("奥拓", 1000));
        linkedHashSet.add(new Car1("奥迪", 300000));
        linkedHashSet.add(new Car1("法拉利", 10000000));
        linkedHashSet.add(new Car1("奥迪", 300000));  //  默认情况下是可以加进去的,因为 hash 不同
        linkedHashSet.add(new Car1("保时捷", 70000000));
        linkedHashSet.add(new Car1("奥迪", 300000));
        for (Object o : linkedHashSet) {
            System.out.println((Car1)o);
        }
    }
}

class Car1{
    private String name;
    private double price;

    public Car1(String name, double price) {
        this.name = name;
        this.price = price;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Car1 car1 = (Car1) o;
        return Double.compare(car1.price, price) == 0 &&
                Objects.equals(name, car1.name);
    }

    @Override
    public int hashCode() {
        return Objects.hash(name, price);
    }

    @Override
    public String toString() {
        return "Car1{" +
                "name='" + name + '\'' +
                ", price=" + price +
                '}';
    }
}

询问:如果只保留hashCode() 方法【舍弃 equals() 方法】会出现什么问题:

  • 会造成所有的 Car对象都可以添加
    • 因为首先 通过 hashCode方法判定要放入同一位置
    • 再通过equals() 方法,如果不重写的话,默认是比较地址,所以肯定不同,所以所有对象都可以添加。
    • 总结:只有当 hashCode 和 equals()方法都相同时,才加不进去
2. 3 TreeSet

底层就是 TreeMap

  • 在调用 add() 方法添加数据时,会调用 TreeMap的 put() 方法
public boolean add(E e) {
    //	Value 还是用 PRESENT 来代替
    return m.put(e, PRESENT)==null;
}
//	PRESENT 是在 TreeSet 中创建的一个静态的 final 类型的 Object
// Dummy value to associate with an Object in the backing Map
private static final Object PRESENT = new Object();

HashMap中内部类

/**
     * Node in the Tree.  Doubles as a means to pass key-value pairs back to
     * user (see Map.Entry).
     */

static final class Entry<K,V> implements Map.Entry<K,V> {
    K key;
    V value;
    Entry<K,V> left;
    Entry<K,V> right;
    Entry<K,V> parent;
    boolean color = BLACK;

    /**
         * Make a new cell with given key, value, and parent, and with
         * {@code null} child links, and BLACK color.
         */
    Entry(K key, V value, Entry<K,V> parent) {
        this.key = key;
        this.value = value;
        this.parent = parent;
    }

    /**
         * Returns the key.
         *
         * @return the key
         */
    public K getKey() {
        return key;
    }

    /**
         * Returns the value associated with the key.
         *
         * @return the value associated with the key
         */
    public V getValue() {
        return value;
    }

    /**
         * Replaces the value currently associated with the key with the given
         * value.
         *
         * @return the value associated with the key before this method was
         *         called
         */
    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;
    }
}

可以排序

  1. 当我们使用无参构造器,创建 TreeSet时,仍然是无序的

  2. 现在希望按照字符串大小来排序

  3. 使用 TreeSet 提供的一个构造器,可以传入一个比较器(comparator 接口 ---> 有一个 compare 方法:用到 匿名内部类),指定排序规则。

    TreeSet treeSet = new TreeSet(new Comparator() {
        @Override
        public int compare(Object o1, Object o2) {
            //  下面 调用String 的 compareTo 方法进行字符串大小比较
            return ((String) o1).compareTo((String) o2);    //  这里返回一个数字,然后赋值给cmp再去进行比较
        }
    });
    
    
    public static int compareTo(byte[] value, byte[] other, int len1, int len2) {
        int lim = Math.min(len1, len2);
        for (int k = 0; k < lim; k++) {
            if (value[k] != other[k]) {
                return getChar(value, k) - getChar(other, k);
            }
        }
        return len1 - len2;
    }
    

    执行过程:会将匿名内部类传给 TreeSet

    image-20221212113308597

    可以 TreeSet的底层是 TreeMap

    image-20221212113920732

    image-20221212114031249

    image-20221212111910367

    put V
public V put(K key, V value) {
    Entry<K,V> t = root;
    if (t == null) {
        //	第一次添加,将k - v 封装到 Entry 对象,放入 root
        compare(key, key); // type (and possibly null) check

        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;	//	cpr:实现了Comparator接口的匿名内部类(对象)
    if (cpr != null) {
        //	遍历所有的 key,给当前 key 找到适当的位置
        do {
            parent = t;
            cmp = cpr.compare(key, t.key);	//	调用 compare 方法 【key:新加的】
            if (cmp < 0)
                t = t.left;
            else if (cmp > 0)
                t = t.right;
            else	//	如果相等,即返回0,这个数据就没有加入(替换)
                return t.setValue(value);
        } while (t != null);	//	循环比较
    }
    else {
        if (key == null)
            throw new NullPointerException();
        @SuppressWarnings("unchecked")
        Comparable<? super K> k = (Comparable<? super K>) key;
        do {
            parent = t;
            cmp = k.compareTo(t.key);
            if (cmp < 0)
                t = t.left;
            else if (cmp > 0)
                t = t.right;
            else
                return t.setValue(value);
        } while (t != null);
    }
    Entry<K,V> e = new Entry<>(key, value, parent);	//	可以加入,放入新节点【parent 指向的是t(将要插入的位置是这个位置的左或者右,根据 cmp大小来判断)】
    if (cmp < 0)
        parent.left = e;
    else
        parent.right = e;
    fixAfterInsertion(e);
    size++;
    modCount++;
    return null;
}

二、 Map(双列集合)

image-20221209133711048

image-20221209164002148

之前讲的Set,底层也是 Map,只不过

  • K:要加入的对象
  • V:PRESENT 常量对象

1. Map接口实现类的特点 【很实用】

  1. Map【双列集合】 与 Collection【单列集合】 并列存在,用于保存具有映射关系的数据:Key - Value

  2. Map 中的 key 和 value 可以是任何引用类型的数据,会封装到 HashMap$Node 对象中

  3. Map 中的 key 不允许重复 (原因和 HashSet 一样,前面分析过源码)

    • 当有相同的 key 时,就等价于 替换
  4. Map 中的 value 可以重复

  5. Map 中 key 可以为 null,value 也可以为 null,注意 key为 null,只能有一个,value 为 null,可以有多个

  6. 常用 String 类作为 Map 的 key

  7. key 和 value 之间存在单向一对一关系,即通过指定的 key 总能找到对应的 value

  8. Map 存放数据的 key - value 示意图:

    image-20221209171245420

一对 k - v 是放在一个 HashMap$Node【Node是 HashMap的一个静态内部类】中的

image-20221209171924306

if ((p = tab[i = (n - 1) & hash]) == null)	//	计算 key 对应的 hash值,去计算该 key 应该在 table 表的哪一个索引位置去存放,并把这个位置的对象 赋给 辅助变量 p
        //	再判断是否为 null
        //	1)如果 p 为 null,表示还没有存放过元素,就创建一个 Node(key="java", value = PRESENT)放在该位置
        //	为什么把 key 对应的 hash 也存进去了,因为将来会去比较,如果再有的话,它会去看,如果相等会往后怼
        tab[i] = newNode(hash, key, value, null);

这里面返回的 Node 就是 HashMap中的 Node

// Create a regular (non-tree) node
Node<K,V> newNode(int hash, K key, V value, Node<K,V> next) {
    return new Node<>(hash, key, value, next);
}
//	HashMap.java 中
/**
     * Basic hash bin node, used for most entries.  (See below for
     * TreeNode subclass, and in LinkedHashMap for its Entry subclass.)
     */
static class Node<K,V> implements Map.Entry<K,V> {	//	静态内部类 Node<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) {	//	Node 构造器
        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;
    }
}

Entry接口

又因为 Node 实现了 Map.Entry 接口,有些书上也说 一对 k - v 就是一个 Entry

image-20221209171536490

//	同时提供了 2个重要的方法
//	1. 
// Views
	
    /**
     * Returns a {@link Set} view of the keys contained in this map.
     * The set is backed by the map, so changes to the map are
     * reflected in the set, and vice-versa.  If the map is modified
     * while an iteration over the set is in progress (except through
     * the iterator's own {@code remove} operation), the results of
     * the iteration are undefined.  The set supports element removal,
     * which removes the corresponding mapping from the map, via the
     * {@code Iterator.remove}, {@code Set.remove},
     * {@code removeAll}, {@code retainAll}, and {@code clear}
     * operations.  It does not support the {@code add} or {@code addAll}
     * operations.
     *
     * @return a set view of the keys contained in this map
     */
    Set<K> keySet();

    /**
     * Returns a {@link Collection} view of the values contained in this map.
     * The collection is backed by the map, so changes to the map are
     * reflected in the collection, and vice-versa.  If the map is
     * modified while an iteration over the collection is in progress
     * (except through the iterator's own {@code remove} operation),
     * the results of the iteration are undefined.  The collection
     * supports element removal, which removes the corresponding
     * mapping from the map, via the {@code Iterator.remove},
     * {@code Collection.remove}, {@code removeAll},
     * {@code retainAll} and {@code clear} operations.  It does not
     * support the {@code add} or {@code addAll} operations.
     *
     * @return a collection view of the values contained in this map
     */
    Collection<V> values();

HashMap中方法实现:

Set keySet() 方法

//	keySet() 方法实现
/**
     * Returns a {@link Set} view of the keys contained in this map.
     * The set is backed by the map, so changes to the map are
     * reflected in the set, and vice-versa.  If the map is modified
     * while an iteration over the set is in progress (except through
     * the iterator's own {@code remove} operation), the results of
     * the iteration are undefined.  The set supports element removal,
     * which removes the corresponding mapping from the map, via the
     * {@code Iterator.remove}, {@code Set.remove},
     * {@code removeAll}, {@code retainAll}, and {@code clear}
     * operations.  It does not support the {@code add} or {@code addAll}
     * operations.
     *
     * @return a set view of the keys contained in this map
     */
/**
返回此映射中包含的键的 {@link Set} 视图。该集合由map支持,因此对map的更改会反映在集合中,反之亦然。如果在对集合进行迭代时修改映射(除了通过迭代器自己的 {@code remove} 操作),迭代的结果是未定义的。 set支持元素移除,通过{@code Iterator.remove}、{@code Set.remove}、{@code removeAll}、{@code retainAll}和{@code clear从map中移除对应的映射} 操作。它不支持 {@code add} 或 {@code addAll} 操作。 @return 映射中包含的键的集合视图
*/
public Set<K> keySet() {
    Set<K> ks = keySet;
    if (ks == null) {
        ks = new KeySet();
        keySet = ks;
    }
    return ks;
}

Collection values() 方法

// 注解同上
public Collection<V> values() {
    Collection<V> vs = values;
    if (vs == null) {
        vs = new Values();
        values = vs;
    }
    return vs;
}

2. 源码分析

HashMap 的内部类(有很多)

image-20221209185231496

  1. k - v 最后是 HashMap$Node node = new Node(hash, key, value, null)

  2. k - v 为了方便程序员去遍历,还会创建 EntrySet 集合,该集合存放的元素类型为 Entry

    • 而一个 Entry 对象就有 k, v,即:EntrySet<Entry<k, v>>

    • 证明:在 HashMap中,有 entrySet 字段,它的类型是 Set<Map.Entry<K, V>>

    • 只是简单地一个指向 【为了遍历方便】

      • K ---> key
      • V ---> value

HashMap的 entrySet 字段

transient Set<Map.Entry<K,V>> entrySet;

image-20221209183406861

HashMap的 entrySet() 方法

  1. 通过 map.entrySet() 方法来验证

    public Set<Map.Entry<K,V>> entrySet() {
        Set<Map.Entry<K,V>> es;	//	中间变量
        return (es = entrySet) == null ? (entrySet = new EntrySet()) : es;	//	初始化 entrySet = new EntrySet();
    }
    
    Set set = map.entrySet();
    System.out.println(set.getClass())	//  class java.util.HashMap$EntrySet
    

HashMap的 EntrySet 类

//	HashMap 内部类 EntrySet
final class EntrySet extends AbstractSet<Map.Entry<K,V>> {
    public final int size()                 { return size; }
    public final void clear()               { HashMap.this.clear(); }
    public final Iterator<Map.Entry<K,V>> iterator() {
        return new EntryIterator();
    }
    public final boolean contains(Object o) {
        if (!(o instanceof Map.Entry))
            return false;
        Map.Entry<?,?> e = (Map.Entry<?,?>) o;
        Object key = e.getKey();
        Node<K,V> candidate = getNode(hash(key), key);
        return candidate != null && candidate.equals(e);
    }
    public final boolean remove(Object o) {
        if (o instanceof Map.Entry) {
            Map.Entry<?,?> e = (Map.Entry<?,?>) o;
            Object key = e.getKey();
            Object value = e.getValue();
            return removeNode(hash(key), key, value, true, true) != null;
        }
        return false;
    }
    public final Spliterator<Map.Entry<K,V>> spliterator() {
        return new EntrySpliterator<>(HashMap.this, 0, -1, 0, 0);
    }
    public final void forEach(Consumer<? super Map.Entry<K,V>> action) {
        Node<K,V>[] tab;
        if (action == null)
            throw new NullPointerException();
        if (size > 0 && (tab = table) != null) {
            int mc = modCount;
            for (Node<K,V> e : tab) {
                for (; e != null; e = e.next)
                    action.accept(e);
            }
            if (modCount != mc)
                throw new ConcurrentModificationException();
        }
    }
}

注意:在 entrySet 中,定义的类型是:Map.Entry,但是实际上存放的还是 HashMap$Node,这是因为:

  • Node 实现了 Map.Entry 接口
  • 接口类型可以指向 实现了这个接口的子类(多态)
  1. 当把 HashMap$Node 对象存放到 entrySet,就方便我们遍历,因为 Map.Entry 提供了重要方法

    K getKey()

    V getValue()

    image-20221209192756933

3. 总结

  • 首先,你添加的每个数据都会成为一个 HashMap$Node

  • 然后由于 HashMap$Node 实现了 Map.Entry 接口【同时 Map.Entry 接口还提供了2个好方法:getKey,getValue】

  • 所以为了 方便遍历 ,我们要向上转型成:Map.Entry

    • 一个 Node(k - v) 对应一个 Entry(k - v)

    • 为了方便管理,HashMap 中定义一个 entrySet 字段,来管理 entry

      transient Set<Map.Entry<K,V>> entrySet;
      
    • 通过 hashMap.entryset() 进行初始化

      public Set<Map.Entry<K,V>> entrySet() {
          Set<Map.Entry<K,V>> es;	//	中间变量
          return (es = entrySet) == null ? (entrySet = new EntrySet()) : es;	//	初始化 entrySet = new EntrySet();
      }
      
    • 看下 entrySet 运行类型 EntrySet

      final class EntrySet extends AbstractSet<Map.Entry<K,V>> {
          public final int size()                 { return size; }
          public final void clear()               { HashMap.this.clear(); }
          public final Iterator<Map.Entry<K,V>> iterator() {
              return new EntryIterator();
          }
          public final boolean contains(Object o) {
              if (!(o instanceof Map.Entry))
                  return false;
              Map.Entry<?,?> e = (Map.Entry<?,?>) o;
              Object key = e.getKey();
              Node<K,V> candidate = getNode(hash(key), key);
              return candidate != null && candidate.equals(e);
          }
          public final boolean remove(Object o) {
              if (o instanceof Map.Entry) {
                  Map.Entry<?,?> e = (Map.Entry<?,?>) o;
                  Object key = e.getKey();
                  Object value = e.getValue();
                  return removeNode(hash(key), key, value, true, true) != null;
              }
              return false;
          }
          public final Spliterator<Map.Entry<K,V>> spliterator() {
              return new EntrySpliterator<>(HashMap.this, 0, -1, 0, 0);
          }
          public final void forEach(Consumer<? super Map.Entry<K,V>> action) {
              Node<K,V>[] tab;
              if (action == null)
                  throw new NullPointerException();
              if (size > 0 && (tab = table) != null) {
                  int mc = modCount;
                  for (Node<K,V> e : tab) {
                      for (; e != null; e = e.next)
                          action.accept(e);
                  }
                  if (modCount != mc)
                      throw new ConcurrentModificationException();
              }
          }
      }
      
    • HashMap 对 2个方法的的实现:

      image-20221210002045602

      static class Node<K,V> implements Map.Entry<K,V> {
          final int hash;
          final K key;	//	Node 节点的属性
          V value;	//	Node 节点的属性
          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; }	//	返回 Node 节点的 key
          public final V getValue()      { return value; }	//	返回 Node 节点的 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;
          }
      }
      

image-20221209193644337

通过断点验证:

Set set = map.entrySet();

image-20221209200734434

HashMap 3 个方法

1. keySet() 方法【Set 类型】
Set set1 = map.keySet();
2. values() 方法【Collection 类型】
Collection values = map.values();
3. entrySet() 方法【Set 类型】
getKey() 方法
getValue() 方法

由于是 Set 类型,即实现了 Itarable 接口,可以使用迭代器

  • 应用迭代器遍历时候,每个 entry 向下转型

    Map.Entry entry = (Map.Entry)iterator.next();
    System.out.println(entry.getKey() + "-" + entry.getValue());	//	动态绑定到 HashMap$Node 类的 getKey 和 getValue() 方法,对应着 Node 的 key 和 value
    

4. Map接口常用方法

方法 作用
put 添加
remove 根据键删除映射关系
get 根据键获取值
size 获取元素个数
isEmpty 判断个数是否为0
clear 清空
containKey 查找键是否存在

5. Map接口的 6大遍历方式

image-20221210010729363

方法名 作用
containsKey 查找键是否存在
keySet 获取所有键盘
entrySet 获取所有关系 k - v
values 获取所有的值

image-20221210013115699

遍历有 3种 方式

  1. 先取出 所有的 Key,通过 Key 取出对应的Value
  2. 把所有的values取出来
  3. 通过EntrySet 来获取 k-v

然后由于

  • Set 接口 实现了 Iterable 接口
  • Collection 接口实现了 Iterable 接口
  • Set 接口实现了 Iterable 接口
public class MapFor {
    public static void main(String[] args) {

        Map map = new HashMap();
        map.put("邓超", new Book("", 100));
        map.put("邓超", "孙俪");
        map.put("王宝强", "马蓉");
        map.put("宋喆", "马蓉");
        map.put("刘令博", null);
        map.put(null, "刘亦菲");
        map.put("鹿晗", "关晓彤");


        //  第一组:先取出 所有的 Key,通过 Kwy 取出对应的Value
        Set keySet = map.keySet();
        //  (1)增强 for
        System.out.println("-----第一种方式-----");
        for (Object key : keySet) {
            System.out.println(key + "-" + map.get(key));
        }
        //  (2)迭代器
        System.out.println("----第二种方式-----");
        Iterator iterator = keySet.iterator();
        while (iterator.hasNext()) {
            Object key =  iterator.next();
            System.out.println(key + "-" + map.get(key));
        }


        //  第二组:把所有的values取出来
        Collection values = map.values();
        //  这里可以使用所有的 Collections 使用的遍历方法
        //  (1)增强 for
        System.out.println("---取出所有的value  增强for----");
        for (Object value : values) {
            System.out.println(value);
        }
        //  (2) 迭代器
        System.out.println("---取出所有的value 迭代器----");
        Iterator iterator1 = values.iterator();
        while (iterator1.hasNext()) {
            Object value = iterator1.next();
            System.out.println(value);
        }

        //  第三组:通过EntrySet 来获取 k-v
        Set entrySet = map.entrySet();  //  EntrySet<Map.Entry<K, V>>
        //  (1)增强 for
        System.out.println("----使用EntrySet的 for增强----");
        for (Object entry : entrySet) { //   此时,entry 类型为:entry
            //  将entry 转成 Map.Entry
            Map.Entry m = (Map.Entry) entry;
            System.out.println(m.getKey() + "-" + m.getValue());
        }
        //  (2)迭代器
        System.out.println("----使用EntrySet 的迭代器(第4种)----");
        Iterator iterator2 = entrySet.iterator();
        while (iterator2.hasNext()) {
            Object entry =  iterator2.next();
            System.out.println(entry.getClass());    //  HashMap$Node ---> 实现了 Map.Entry(getKey, getValue) 【多态传递】
            //  向下转型 Map.Entry
            Map.Entry m = (Map.Entry) entry;
            System.out.println(m.getKey() + "-" + m.getValue());
        }
    }
}

6. Map 接口练习

使用 HashMap 添加3个员工对象

  • Key:id
  • Value:员工对象

并显示工资大于 180000的员工

员工类 Emeployee(姓名、工资、员工 id)

7. HashMap 小结

  1. Map 接口的常用实现类:HashMap、Hashtable、和 Properties

  2. HashMap 是 Map 接口使用频率最高的实现类

  3. HashMap 是以 key-val 方式存储数据(HashMap$Node类型)【案例 Entry】

  4. key 不能重复,但是值可以重复,允许使用 null 键和 null 值

  5. 如果添加相同的 key,则会覆盖原来的 key-val,等同于修改

    map.put"no1", "韩顺平");
    //	添加相同的 key
    map.put("no1", "张三丰")
    
    //	传入新的 value
    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) {
    
    if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))){
        //	“no1="韩顺平"
        e = p;	//	p = tab[i = (n - 1) & hash] (类型是 HashMap$Node)
    }
    
    if (e != null) { // existing mapping for key 【是已经存在的 key 的映射】
        V oldValue = e.value;
        if (!onlyIfAbsent || oldValue == null)
            e.value = value;	//	e.value = "张三丰" 【而前面有 e = p】,所以更新value
        afterNodeAccess(e);
        return oldValue;
    }
    
  6. 与 HashSet一样,不保证映射的顺序,因为底层是以 hash 表的方式来存储的

    • JDK 8 的 hashMap 底层:数组 + 链表 + 红黑树 (注意:链表和红黑树可能同时存在)

      table = {HashMap$Node[16]@479}
      
  7. HashMap 没有实现同步,因此线程是不安全的(方法没有 Synchronized)

8. HashMap

HashMap 底层机制

123未命名绘图

  1. HashMap底层维护了 Node 类型的数组 table,默认为 null

    /**
         * Basic hash bin node, used for most entries.  (See below for
         * TreeNode subclass, and in LinkedHashMap for its Entry subclass.)
         */
    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;
        }
    }
    
    /**
         * The table, initialized on first use, and resized as
         * necessary. When allocated, length is always a power of two.
         * (We also tolerate length zero in some operations to allow
         * bootstrapping mechanics that are currently not needed.)
         */
    transient Node<K,V>[] table;
    
  2. 当创建对象时,将加载因子(loadfactor)初始化为:0.75

  3. 当添加 key - val 时,通过 key 的哈希值得到在 table 的索引

    • 判断该索引处是否有元素
      • 没有
        • 直接添加
        • 继续判断该元素(Node)的 key 和 准备加入的 key 是否相等
          • 相等
            • 则直接替换 val
          • 不相等
            • 判断是树结构还是链表结构,做出相应处理
    • 如果在添加时候发现容量不够,则需要扩容 【 Node<K, V>[] resize() 方法 】
  4. 第 1 次添加,则需要扩容 table 容量为:16,临界值(threshold)为 12 【16 * 0.75】

  5. 以后再扩容

    • 需要扩容 table 容量为原来的 2倍:32
    • 临界值 threshold 为原来的2倍:24
  6. 在 Java8 中,如果

    • 一条链表的元素个数超过 TREEIFY_THRESHOLD(默认是 8)

      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	//	因为 binCount 从 0 开始
                      treeifyBin(tab, hash);
                  break;
              }
              if (e.hash == hash &&
                  ((k = e.key) == key || (key != null && key.equals(k))))
                  break;
              p = e;
          }
      }
      

      然后调用方法 treeifyBin(tab, hash);

      • table 大小 < 64 时,调用 resize() 方法扩容
      final void treeifyBin(Node<K,V>[] tab, int hash) {
          int n, index; Node<K,V> e;
          if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)	//	n = tab.length = 64 【MIN_TREEIFY_CAPACITY=64】
              resize();
          else if ((e = tab[index = (n - 1) & hash]) != null) {
              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);
          }
      }
      
    • 并且table.length >= MIN_TREEIFY_CAPACITY(默认 64),就会进行树化(红黑树)

      // For treeifyBin
      TreeNode<K,V> replacementTreeNode(Node<K,V> p, Node<K,V> next) {
          return new TreeNode<>(p.hash, p.key, p.value, next);
      }
      
      /**
               * Forms tree of the nodes linked from this node.
               * 从此节点链接的节点的表单树
               */
      final void treeify(Node<K,V>[] tab) {
          TreeNode<K,V> root = null;
          for (TreeNode<K,V> x = this, next; x != null; x = next) {
              next = (TreeNode<K,V>)x.next;
              x.left = x.right = null;
              if (root == null) {
                  x.parent = null;
                  x.red = false;
                  root = x;
              }
              else {
                  K k = x.key;
                  int h = x.hash;
                  Class<?> kc = null;
                  for (TreeNode<K,V> p = root;;) {
                      int dir, ph;
                      K pk = p.key;
                      if ((ph = p.hash) > h)
                          dir = -1;
                      else if (ph < h)
                          dir = 1;
                      else if ((kc == null &&
                                (kc = comparableClassFor(k)) == null) ||
                               (dir = compareComparables(kc, k, pk)) == 0)
                          dir = tieBreakOrder(k, pk);
      
                      TreeNode<K,V> xp = p;
                      if ((p = (dir <= 0) ? p.left : p.right) == null) {
                          x.parent = xp;
                          if (dir <= 0)
                              xp.left = x;
                          else
                              xp.right = x;
                          root = balanceInsertion(root, x);
                          break;
                      }
                  }
              }
          }
          moveRootToFront(tab, root);
      }
      

HashMap 源码解读

大致类似 HashSet

HashMap 扩容树化触发

treeifyBin(tab, hash)方法
final void treeifyBin(Node<K,V>[] tab, int hash) {
    int n, index; Node<K,V> e;
    if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)	//	n = tab.length = 64 【MIN_TREEIFY_CAPACITY=64】
        resize();
    else if ((e = tab[index = (n - 1) & hash]) != null) {
        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);
    }
}
当 tab.length < 64时,调用resize() 扩容
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)	//	n = tab.length = 64 【MIN_TREEIFY_CAPACITY=64】
    resize();
当 tab.length >= 64时,才调用 treeify(tab) 进行树化
treeifyBin(tab, hash) --> replacementTreeNode(e, null) ---> treeify(tab)
else if ((e = tab[index = (n - 1) & hash]) != null) {
    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);
}
// For treeifyBin
TreeNode<K,V> replacementTreeNode(Node<K,V> p, Node<K,V> next) {
    return new TreeNode<>(p.hash, p.key, p.value, next);
}
/**
         * Forms tree of the nodes linked from this node.
         * 从此节点链接的节点的表单树
         */
final void treeify(Node<K,V>[] tab) {
    TreeNode<K,V> root = null;
    for (TreeNode<K,V> x = this, next; x != null; x = next) {
        next = (TreeNode<K,V>)x.next;
        x.left = x.right = null;
        if (root == null) {
            x.parent = null;
            x.red = false;
            root = x;
        }
        else {
            K k = x.key;
            int h = x.hash;
            Class<?> kc = null;
            for (TreeNode<K,V> p = root;;) {
                int dir, ph;
                K pk = p.key;
                if ((ph = p.hash) > h)
                    dir = -1;
                else if (ph < h)
                    dir = 1;
                else if ((kc == null &&
                          (kc = comparableClassFor(k)) == null) ||
                         (dir = compareComparables(kc, k, pk)) == 0)
                    dir = tieBreakOrder(k, pk);

                TreeNode<K,V> xp = p;
                if ((p = (dir <= 0) ? p.left : p.right) == null) {
                    x.parent = xp;
                    if (dir <= 0)
                        xp.left = x;
                    else
                        xp.right = x;
                    root = balanceInsertion(root, x);
                    break;
                }
            }
        }
    }
    moveRootToFront(tab, root);
}
关于红黑树,还有个剪枝的概念

已经是红黑树了,逐渐删除,越来越少了,到一定量的时候,就会将这棵树重新转成一个链表

  • 认为你的数据量并不大,没有必要用树这种结构了

9. HashTable

  1. 底层有数组 Hashtable$Entry[] ,初始化大小为:11

  2. 临界值 threshold 8 = 11 * 0.75

  3. 扩容:按照自己的扩容机制来进行 【2 * oldCapacity + 1】

  4. 执行 方法 addEntry(hash, key, value, index); 添加 K - V 封装到 Entry

  5. 按照 int newCapacity = (oldCapacity << 1) + 1; 的大小扩容

    if (count >= threshold) {   满足时,就进行扩容
    //            // Rehash the table if the threshold is exceeded
    //            rehash();
    

image-20221211010305459

基本介绍

  1. 存放的元素是键值对:即K-V
  2. hashtable的键和值都 不能为null ,否则会抛出 NullPointerException
  3. hashTable使用方法基本上和HashMap-样
  4. hashTable是线程安全的,hashMap是线程不安全的
  5. 简单看下底层结构
synchronized V put(K key, V value) 方法
public synchronized V put(K key, V value) {
    // Make sure the value is not null
    if (value == null) {
        throw new NullPointerException();
    }

    // Makes sure the key is not already in the hashtable.
    Entry<?,?> tab[] = table;
    int hash = key.hashCode();	//	key 为 null 的话,会报空指针异常
    int index = (hash & 0x7FFFFFFF) % tab.length;
    @SuppressWarnings("unchecked")
    Entry<K,V> entry = (Entry<K,V>)tab[index];
    for(; entry != null ; entry = entry.next) {
        if ((entry.hash == hash) && entry.key.equals(key)) {
            V old = entry.value;
            entry.value = value;
            return old;
        }
    }

    addEntry(hash, key, value, index);
    return null;
}
addEntry(int hash, K key, V value, int index)
private void addEntry(int hash, K key, V value, int index) {
    Entry<?,?> tab[] = table;
    if (count >= threshold) {
        // Rehash the table if the threshold is exceeded
        rehash();

        tab = table;
        hash = key.hashCode();
        index = (hash & 0x7FFFFFFF) % tab.length;
    }

    // Creates the new entry.
    @SuppressWarnings("unchecked")
    Entry<K,V> e = (Entry<K,V>) tab[index];
    tab[index] = new Entry<>(hash, key, value, e);
    count++;
    modCount++;
}
rehash() 方法
protected void rehash() {
    int oldCapacity = table.length;	//	记录数组旧容量
    Entry<?,?>[] oldMap = table;

    // overflow-conscious code
    int newCapacity = (oldCapacity << 1) + 1;	//	扩容 = 就容量 * 2 + 1
    if (newCapacity - MAX_ARRAY_SIZE > 0) {
        if (oldCapacity == MAX_ARRAY_SIZE)
            // Keep running with MAX_ARRAY_SIZE buckets
            return;
        newCapacity = MAX_ARRAY_SIZE;
    }
    Entry<?,?>[] newMap = new Entry<?,?>[newCapacity];

    modCount++;
    threshold = (int)Math.min(newCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
    table = newMap;

    for (int i = oldCapacity ; i-- > 0 ;) {	//	将旧表内容,经过计算后,拷贝到 新表
        for (Entry<K,V> old = (Entry<K,V>)oldMap[i] ; old != null ; ) {
            Entry<K,V> e = old;
            old = old.next;

            int index = (e.hash & 0x7FFFFFFF) % newCapacity;
            e.next = (Entry<K,V>)newMap[index];
            newMap[index] = e;
        }
    }
}

Hashtable 和 HashMap 对比

版本 线程安全(同步) 效率 允许null和null 值
HashMap 1.2 不安全 高(没有锁,红黑树,取模获取槽位) 可以
Hashtable 1.0 安全 较低(位运算获取槽位) 不可以

10. LinkedHashMap

11. TreeMap

红黑树

/** From CLR */
private void fixAfterInsertion(Entry<K,V> x) {
    x.color = RED;

    while (x != null && x != root && x.parent.color == RED) {
        if (parentOf(x) == leftOf(parentOf(parentOf(x)))) {
            Entry<K,V> y = rightOf(parentOf(parentOf(x)));
            if (colorOf(y) == RED) {
                setColor(parentOf(x), BLACK);
                setColor(y, BLACK);
                setColor(parentOf(parentOf(x)), RED);
                x = parentOf(parentOf(x));
            } else {
                if (x == rightOf(parentOf(x))) {
                    x = parentOf(x);
                    rotateLeft(x);
                }
                setColor(parentOf(x), BLACK);
                setColor(parentOf(parentOf(x)), RED);
                rotateRight(parentOf(parentOf(x)));
            }
        } else {
            Entry<K,V> y = leftOf(parentOf(parentOf(x)));
            if (colorOf(y) == RED) {
                setColor(parentOf(x), BLACK);
                setColor(y, BLACK);
                setColor(parentOf(parentOf(x)), RED);
                x = parentOf(parentOf(x));
            } else {
                if (x == leftOf(parentOf(x))) {
                    x = parentOf(x);
                    rotateRight(x);
                }
                setColor(parentOf(x), BLACK);
                setColor(parentOf(parentOf(x)), RED);
                rotateLeft(parentOf(parentOf(x)));
            }
        }
    }
    root.color = BLACK;
}

12. Properties

  1. 继承自 Hastable 类并且实现了 Map 接口,也是使用一种键值对的形式来保存数据
  2. 特点与Hashtaable 类似
  3. Properties 还可以用于 从 ×××.properties 文件中,加载数据到 Properties 类对象,并进行读取和修改
  4. 说明:工作后 ×××.properties 文件通常作为 配置文件(IO流)
public class Properties_ {
    public static void main(String[] args) {

        //  老韩解读
        //  1. Properties 继承了 Hashtable
        //  2. 可以通过 k - v 存放数据,当然 key 和 value 不能为 null

        Properties properties = new Properties();
        properties.put("john", 100);    //  k - v
        properties.put("lucy", 100);    //  k - v
        properties.put("lic", 100);    //  k - v
        properties.put("lic", 88);    //  如果有相同的 key,value也会被替换

        System.out.println("properties=" + properties);


        //  通过k 获取对应 值
        System.out.println(properties.get("lic"));

        //  删除
        properties.remove("lic");
        System.out.println("properties=" + properties);

        //  修改
        properties.put("john", "约翰");
        System.out.println("properties=" + properties);

        //  查
        System.out.println(properties.get("john"));
        System.out.println(properties.getProperty("john"));

    }
}

Java 读写 Properties 配置文件

​ Properties类继承自Hashtable类并且实现了Map接口,也是使用一种键值对的形式来保存属性集。不过Properties有特殊的地方,就是它的键和值都是字符串类型。

Properties 中主要方法

(1) load(InputStream inStream)

​ 这个方法可以从.properties属性文件对应的文件输入流中,加载属性列表到 Properties类对象。如下面的代码:

Properties pro = new Properties();
FileInputStream in = new FileInputStream("a.properties");
pro.load(in);
in.close();
(2) store(OutputStream out, String comments)

 这个方法将Properties类对象的属性列表保存到输出流中。如下面的代码:

FileOutputStream oFile = new FileOutputStream(file, "a.properties");
pro.store(oFile, "Comment");
oFile.close();

 如果comments不为空,保存后的属性文件第一行会是#comments,表示注释信息;如果为空则没有注释信息。

注释信息后面是属性文件的当前保存时间信息。

(3) getProperty/setProperty

实际案例:

import java.io.BufferedInputStream;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.util.Iterator;
import java.util.Properties;

public class PropertyTest {
    public static void main(String[] args) {
        Properties prop = new Properties();
        try{
            //读取属性文件a.properties
            InputStream in = new BufferedInputStream (new FileInputStream("a.properties"));
            prop.load(in);     ///加载属性列表
            Iterator<String> it=prop.stringPropertyNames().iterator();
            while(it.hasNext()){
                String key=it.next();
                System.out.println(key+":"+prop.getProperty(key));
            }
            in.close();

            ///保存属性到b.properties文件
            FileOutputStream oFile = new FileOutputStream("b.properties", true);//true表示追加打开
            prop.setProperty("phone", "10086");
            prop.store(oFile, "The New properties file");
            oFile.close();
        }
        catch(Exception e){
            System.out.println(e);
        }
    }
}

三、开发中如何选择集合实现类

取决于 业务操作特点,然后根据集合实现类特性进行选择

  1. 先判断存储的类型

    • 一组对象【单列】
    • 一组键值对【双列】
  2. 一组对象【单列】:Collection接口

    • 允许重复:List
      • 增删多:LinkedList【底层维护了一个双向链表】
      • 改查多:ArrayList【底层维护 Object 类型的可变数组】
    • 不允许重复:Set
      • 无序:HashSet【底层是 HashMap,维护了一个哈希表,即(数组 + 链表 + 红黑树)】
      • 排序:TreeSet
      • 插入he1取出顺序一致:LinkedHashSet【底层LinkedHashMap的底层HashMap】,维护数组 + 双向链表
  3. 一组键值对:Map

    • 键无序:HashMap【底层是:哈希表 jdk7:数组 + 链表,jdk8:数组+链表+红黑树】
    • 键无序:TreeMap
    • 键插入和取出顺序一致:LinkedHashMap
    • 读取文件 Properties

四、注意事项

1. 分析 HashSet 和 TreeSet 如何实现去重的

  1. HashSet
    • haasCode() + equals()
    • 底层先通过存入对象,进行运算得到一个 hash值,得到对应索引
      • 如果发现table 表索引所在的位置,没有数据,就直接存放
      • 如果有数据,就进行 equals 比较【遍历比较】
        • 比较后不相同,就加入
        • 否则不加入
  2. TreeSet
    • 如果你传入了一个 Comparator匿名对象,就使用实现的 compare方法去重
      • 方法返回0,就认为是相同的元素 / 数据,就不添加了
    • 如果你没有传入Comparator匿名对象,则以你添加的对象实现的 Comparable 接口的 compareTo 去重
public V put(K key, V value) {
    Entry<K,V> t = root;
    if (t == null) {
        compare(key, key); // type (and possibly null) check

        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;
    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
                return t.setValue(value);
        } while (t != null);
    }
    else {	//	现在没有传入 Comparactor 对象,代码走的是这部分了
        if (key == null)
            throw new NullPointerException();
        @SuppressWarnings("unchecked")
        Comparable<? super K> k = (Comparable<? super K>) key;	//	接口类型 ---> 实现这个接口的对象【向上转型】
        do {
            parent = t;
            cmp = k.compareTo(t.key);	//	调用了字符串本身的 compareTo() 方法 【动态绑定】
            if (cmp < 0)
                t = t.left;
            else if (cmp > 0)
                t = t.right;
            else
                return t.setValue(value);
        } while (t != null);
    }
    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;
}

分析下面代码是否会有异常:

//	因为:现在没有传入 Comparactor接口的匿名内部类(对象)
//	所以,在底层会调用 添加的对象实现的 Comparable 接口的 compareTo 去重
//	走到了上述代码
//	即:把 Person 转成 Comparable 类型
//	由于这个 Person() 类不知道有没有实现 Comparable 接口,所以可能报错!!!---> 类型转换失败 ClassCastException
TreeSet treeSet = new TreeSet();
treeSet.add(new Person());	//	报错

注意:第一次添加的时候,也会进行类型转换

//	第一次添加的时候,就会报错,类型转换异常
@SuppressWarnings("unchecked")
final int compare(Object k1, Object k2) {
    return comparator==null ? ((Comparable<? super K>)k1).compareTo((K)k2)
        : comparator.compare((K)k1, (K)k2);
}

2. remove() 方法

2.1 HashSet的 remove() 返回类型为:boolean
//	HashSet 调用 boolean remove(Object o) 方法,底层调用的是 hashmap的 V remove(Object key)
//	根据 key 的 hash 值,会先算出在 table 中的位置,所过table表中该位置为null,则删除失败
//	删除成功的话,会返回 对应的 value 值
set.remove(obj);
public boolean remove(Object o) {
    return map.remove(o)==PRESENT;
}
2.2 HashSet的 remove() 返回类型为:V
public V remove(Object key) {
    Node<K,V> e;
    return (e = removeNode(hash(key), key, null, false, true)) == null ?
        null : e.value;	//	删除成功时,会返回被删掉的 key 对应的 value
}
final Node<K,V> removeNode(int hash, Object key, Object value,
                           boolean matchValue, boolean movable) {
    Node<K,V>[] tab; Node<K,V> p; int n, index;
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (p = tab[index = (n - 1) & hash]) != null) {	//	要删除的位置不为 null
        Node<K,V> node = null, e; K k; V v;
        if (p.hash == hash &&	//	要删除位置的 hash
            ((k = p.key) == key || (key != null && key.equals(k))))
            node = p;
        else if ((e = p.next) != null) {	//	链表的情况
            if (p instanceof TreeNode)
                node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
            else {
                do {
                    if (e.hash == hash &&
                        ((k = e.key) == key ||
                         (key != null && key.equals(k)))) {
                        node = e;
                        break;
                    }
                    p = e;
                } while ((e = e.next) != null);	//	do-while  循环
            }
        }
        if (node != null && (!matchValue || (v = node.value) == value ||
                             (value != null && value.equals(v)))) {
            if (node instanceof TreeNode)
                ((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
            else if (node == p)
                tab[index] = node.next;
            else
                p.next = node.next;
            ++modCount;
            --size;
            afterNodeRemoval(node);
            return node;
        }
    }
    return null;	//	要删除的位置为 null
}

Collections (工具类)

java.util.Collections

  1. Collections 是一个操作 Set、List 和 Map 等集合的工具类
  2. 提供了一些系列静态放法对集合元素进行排序 查询 修改 等操作
//	对象在集合中出现的次数
public static int frequency(Collection<?> c, Object o) {
    int result = 0;
    if (o == null) {
        for (Object e : c)
            if (e == null)
                result++;
    } else {
        for (Object e : c)
            if (o.equals(e))
                result++;
    }
    return result;
}
  • 排序操作:(均为 static 方法)

    方法名 作用
    reverse(List) 反转 List中的元素
    shuffle(List) 【扑克牌洗牌】 对 List 集合元素进行随机排序
    sort(List) 根据元素的自然顺序对指定 List 集合元素按升序排序
    sort(List, Comparator) 根据指定的 Comparator 产生的顺序对 List 集合元素进行排序
    swap(List, int, int) 将指定 list 集合中的 i 处元素和 j 处元素进行交换
    Object max(Collection) 根据元素的自然顺序,返回给定集合中的最大元素
    Object max(Collection, Comparator) 根据 Comparator 指定的顺序,返回给定集合中的最大元素
    Object min(Collection)
    Object min(Collection, Comparator)
    int frequency(Collection, Object) 返回指定集合中指定元素的出现次数
    void copy(List dest, List src) 将 src 中的内容复制到 dest中
    boolean replaceAll(List list, Object oldVal, Object newVal) 使用 新值替换 List 对象的所有旧值
Collections.max(list, new Comparator() {

    @Override
    public int compare(Object o1, Object o2) {
        return ((String)o1).length() - ((String)o2).length();
    }
});
//	为了完成一个完整拷贝,我们需要先给 dest 赋值,大小和 list.size() 一样
for(int i = 0; i < list.size(); i++){
    dest.add("");
}
posted @ 2023-03-04 10:33  爱新觉罗LQ  阅读(21)  评论(0编辑  收藏  举报