Java基础知识——集合类

工具类:Collection和Map,两者是同一级别的在这里插入图片描述
在这里插入图片描述
知识点:
expectedModCount
ArrayList:懒加载,transient,System.arraycopy()
LinkedList:双向链表
Vector:锁机制
Stack:继承Vector,
知识点:
初始化:tableForSize
put:计算hash/如何转树扩容
扩容:如何定位
遍历:迭代器
为什么是2
1.7/1.8d的区别
对比区别
LinkedListHashMap:结构,LRU
TreeMap:比较器,一致性哈希
Queue:队列
Set:内置HashMap

List

arraylist源码阅读:
知识点一:

private transient Object[] elementData;

elementData,假如现在实际有了5个元素,而elementData的大小可能是10,那么在序列化时只需要储存5个元素,数组中的最后五个元素是没有实际意义的,不需要储存。所以ArrayList的设计者将elementData设计为transient,然后在writeObject方法中手动将其序列化,并且只序列化了实际存储的那些元素,而不是整个数组

知识点二:
初始化时,如果指定Capacity则生成Capacity大小的数组,如果没有指定则为空,等到添加元素的时候再扩容,节约内存懒加载机制?

参考资料

	// 空的数组
    private static final Object[] EMPTY_ELEMENTDATA = {};
    // 默认容量的空数组
    private static final Object[]  DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

这里有两个空数组的定义,注意到是static,final不可改变的常量。所以说我们可以推断,当定义很多个空的ArrayList,他们都指向这两个数组节约内存

知识点三:
所以indexOf(Object o),remove等都可以穿入null
o==null
null既不是对象也不是一种类型,它仅是一种特殊的值,你可以将其赋予任何引用类型,它还仅仅是一个特殊值,并不属于任何类型,用instanceof永远返回false

知识点四:

参考资料

操作中实现可拓展数组,根本在于System.arraycopy()
是native方法,一般是借助C/C++实现的

复制方法有四种:
1、for循环,手动复制
2、System.arraycopy()方法
3、Arrays.copyOf()方法
4、clone()方法
由于System.arraycopy()是最贴近底层的,其使用的是内存复制,省去了大量的数组寻址访问等时间,故效率最高。
对于Arrays.copyOf()方法查看源码可以看到:
public static int[] copyOf(int[] original, int newLength) {
int[] copy = new int[newLength];
System.arraycopy(original, 0, copy, 0,
Math.min(original.length, newLength));
return copy;
}
它是借助System.arraycopy()方法实现的,故效率次于System.arraycopy()
clone()方法效率是最低的,一般需要重写

LinkedList源码阅读
知识点一:可以看到LinkedList基于链表,并且是双向循环链表,每一个节点是一个Entry,element,next,previous,构造方法和entry(int index) ,entry(int index) 找index位置的元素并返回。其他的操作比如add,remove都是符合双向循环链表基本操作

private static class Entry<E> {
E element;
Entry<E> next;
Entry<E> previous;
Entry(E element, Entry<E> next, Entry<E> previous) {
    this.element = element;
    this.next = next;
    this.previous = previous;
}
private Entry<E> entry(int index) {
   if (index < 0 || index >= size)
          throw new IndexOutOfBoundsException("Index: "+index+     ", Size: "+size);
   Entry<E> e = header;
   // 根据这个判断决定从哪个方向遍历这个链表
   if (index < (size >> 1)) {
   for (int i = 0; i <= index; i++)
   e = e.next;
   } else {
   // 可以通过header节点向前遍历,说明这个一个循环双向链表,header的previous指向链表的最后一个节点,这也验证了构造方法中对于header节点的前后节点均指向自己的解释
   for (int i = size; i > index; i--)
   e = e.previous;
   }
   return e;
   }
   }

知识点二“:
expectedModCount字段,
modCount,用于记录对象的修改次数,比如增、删、改
可以理解成version,在特定的操作下需要对version进行检查,适用于Fail-Fast机制。
Fail-Fast 机制
比如当A通过iterator去遍历某集合的过程中,因为iterator长时间拥有对象,而且是线程安全即其他线程也可以操作对象,所以为了防止便利的时候读取脏数据,通过checkForComodification()方法,判断modCount==expectedModCount,若其他线程修改了此集合,此时会抛出ConcurrentModificationException异常。

知识点三:重写clone方法,利用clone.add(e.element);

public Object clone() {
	LinkedList<E> clone = null;
	try {
    	clone = (LinkedList<E>) super.clone();
	} catch (CloneNotSupportedException e) {
    	throw new InternalError();
	}
	clone.header = new Entry<E>(null, null, null);
	clone.header.next = clone.header.previous = clone.header;
	clone.size = 0;
	clone.modCount = 0;
	for (Entry<E> e = header.next; e != header; e = e.next)
    	clone.add(e.element);
	return clone;
}

Iterator:

参考资料


参考资料

知识点一:
接口中的default方法,能够在借口中直接写方法体和抽象类的区别又缩小了
但是如果实现A,B两个接口都有相同函数签名的default的方法,必须重写因为不知道应该继承哪个
如果继承父类A,和实现接口B都有default方法,则会实际为父类A的方法

知识点二:
首先Java提供两个迭代器接口Iterator和ListIterator
Iterator方法比较少:next,hasNext,remove
ListIterator方法比较多:add,previous等等

参考资料

AbstractList:是ArrayList和LinkedList的父类
实现了两个内部类Itr和ListItr
Itr:实现Iterator接口
ListItr:实现ListIterator并且继承自Itr

ArrayList:直接使用AbstractList的Itr
Itr:cursor lastRet(remove时移除此元素)利用数组特性遍历

checkForComodification();
try {  
    Object next = get(cursor);//先取当前光标所在位置后面的元素  
    lastRet = cursor++;//然后把最后一次操作所在光标设置成当前光标位置,再把当前光标后移一   
    return next;  
} catch (IndexOutOfBoundsException e) {  
    checkForComodification();  
    throw new NoSuchElementException();  
}   

LinkedList:
单独定义内部类ListItr,它实现了ListIterator接口,用来对LinkedList进行专门迭代,因为LinkedList与ArrayList还不同,它是使用链表结构实现的,所以需专门的迭代器。
ListItr:
lastReturned:最近一次操作返回的元素
Entry next:即将返回的元素
nextIndex:即将返回的元素的编号

 public Object next() {  
        //检查外部是否修改了集合结构,即modCount是否与expectedModCount相等    
        checkForComodification();  
        if (nextIndex == size)  
            throw new NoSuchElementException();  
        lastReturned = next;//先记录next所在位置,并把它赋给lastReturned  
        next = next.next;//然后再把next指向下一个元素  
        nextIndex++;//nextIndex与next操作需同步,所以也要增一  
        return lastReturned.element;  
    }  

Vector
参考资料
1:底层实现与ArrayList类似,基于数组
2:线程安全,add,remove方法都是synchronized方法。但是其实并不是真正的意义安全,对方法加锁实际上是没有太大意义的。因为如果你申请遍历Vector,同样需要锁来禁止修改Vector遍历没加锁而修改加锁。注意add,remov仍然是不能同时执行的,因为synchronized非静态方法是对对象实例加锁,并且效率低下
3: Vector扩容由oldCapacity 与 capacityIncrement共同决定,

int newCapacity = oldCapacity + ((capacityIncrement > 0) ?capacityIncrement : oldCapacity);

而ArrayList为

int newCapacity = (oldCapacity * 3)/2 + 1;

参考资料
stack:
public class Stack extends Vector
Vector:
public class Vector extends AbstractList implements List, RandomAccess, Cloneable, Serializable
继承Vector 并实现pop,push等方法
问题一:为什么说Java的Stack类实现List接口是个笑话?
因为Stack是FILO,但是LIst是RandomAccess,从设计角度上讲不合理。接口的实现,不取决于类的应用场景,而是接口的契约的应用场景,固然栈有些时候是需要随机访问,但是他本质还是FILO
就像牙刷是用来刷牙的,但是有些时候我们的确也可以用来洗衣服
问题二:为什么stack是一个类?
因为collection体系是在jdk1.2被设计出来的,而vector,stack,hashtable这些是随java的第一个版本就发布了的。简而言之,在最初的java版本中,提供了基本的容器实现,但未经良好设计,因此在革命性的1.2版本中,重新设计了后来获得广泛好评的Collection体系

HashMap


参考资料


参考资料

前沿:首先HashMap具体实现是由链表+数组的实现方式,并且采用了动态扩容技术
数据结构:

特点:
1:动态扩充
数组是否需要扩充是通过负载因子判断的,如果当前元素个数为数组容量的0.75时,就会扩充数组。这个0.75就是默认的负载因子,可由构造传入。我们也可以设置大于1的负载因子,这样数组就不会扩充,牺牲性能,节省内存。
2:链表和红黑树转化:
为了解决碰撞,数组中的元素是单向链表类型。当1:链表长度到达一个阈值时(7或8)TREEIFY_THRESHOLD并且2:总数量是否到达一个阈值(64),会将链表转换成红黑树提高性能。而当链表长度缩小到另一个阈值时(6)UNTREEIFY_THRESHOLD,又会将红黑树转换回单向链表提高性能
TreeNodes占用空间是普通Nodes的两倍(两个指针和一个指针)所以刚开始用Nodes节省空间,后面当链表长度大泊松分布几率小时再用红黑树

3:数据结构是数组+链表+红黑树
拉链法进行冲突处理
在这里插入图片描述

内部类:
继承关系如下:Node是单向链表节点,Entry是双向链表节点,TreeNode是红黑树节点。
Node->Entry->TreeNode
注意Entry是JDK1.7之前的数据机构,1.8后主要是用Node和TreeNode不用Entry可能认为双向循环链表意义不大,效率不如Node

Node
static class Node<K,V> implements Map.Entry<K,V> {
        // hash是经过hash()方法处理过的hashCode,为了使hashCode分布更加随机,
        // 待会会深入这个hash()方法。
        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 int hashCode() {
            // key的hashCode异或value的哈希Code
            return Objects.hashCode(key) ^ Objects.hashCode(value);
        }

        // 其余略
    }

HashMap方法:

成员变量

/**
  * 数组的默认初始长度,java规定hashMap的数组长度必须是2的次方
  * 扩展长度时也是当前长度 << 1。
  */
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

// 数组的最大长度
static final int MAXIMUM_CAPACITY = 1 << 30;

// 默认负载因子,当元素个数超过这个比例则会执行数组扩充操作。
static final float DEFAULT_LOAD_FACTOR = 0.75f;

// 树形化阈值,当链表节点个大于等于TREEIFY_THRESHOLD - 1时,
// 会将该链表换成红黑树。
static final int TREEIFY_THRESHOLD = 8;

// 解除树形化阈值,当链表节点小于等于这个值时,会将红黑树转换成普通的链表。
static final int UNTREEIFY_THRESHOLD = 6;

// 最小树形化的容量,即:当内部数组长度小于64时,不会将链表转化成红黑树,而是优先扩充数组。
static final int MIN_TREEIFY_CAPACITY = 64;

// 这个就是hashMap的内部数组了,而Node则是链表节点对象。
transient Node<K,V>[] table;

// 下面三个容器类成员,作用相同,实际类型为HashMap的内部类KeySet、Values、EntrySet。
// 他们的作用并不是缓存所有的key或者所有的value,内部并没有持有任何元素。
// 而是通过他们内部定义的方法,从三个角度(视图)操作HashMap,更加方便的迭代。
// 关注点分别是键,值,映射。
transient Set<K>        keySet;  // AbstractMap的成员
transient Collection<V> values; // AbstractMap的成员
transient Set<Map.Entry<K,V>> entrySet;

// 元素个数,注意和内部数组长度区分开来。
transient int size;

// 再上一篇文章中说过,是容器结构的修改次数,fail-fast机制。
transient int modCount;

// 阈值,超过这个值时扩充数组。 threshold = capacity * load factor,具体看上面的静态常量。
int threshold;

// 装在因子,具体看上面的静态常量。
final float loadFactor;

其中,Node等为transient变量
1:有很多空的元素,不需要序列化
2:HashCode依赖于不同的虚拟机,常规序列化可能导致错误

1:构造方法
//主要是对传入的initialCapacity和loadFactor进行参数检验,没有为数组table分配内存空间而是在执行put操作的时候才真正构建table数组

public HashMap(int initialCapacity, float loadFactor) {
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal initial capacity: " +
                                               initialCapacity);
        if (initialCapacity > MAXIMUM_CAPACITY)
            initialCapacity = MAXIMUM_CAPACITY;
        if (loadFactor <= 0 || Float.isNaN(loadFactor))
            throw new IllegalArgumentException("Illegal load factor: " +
                                               loadFactor);
        this.loadFactor = loadFactor;
        this.threshold = tableSizeFor(initialCapacity);    
        init();//init方法在HashMap中没有实际实现,不过在其子类如 linkedHashMap中就会有对应实现
    }

public HashMap(int initialCapacity, float loadFactor)这个构造可以由我们指定数组的初始容量和负载因子。
但是前面说过,数组容量必须是2的次方。所以就需要通过某个算法将我们给的数值转换成2的次方。
tableSizeFor(int cap)就是这个作用。

// 这个方法可以将任意一个整数转换成2的次方。
static final int tableSizeFor(int cap) {
int n = cap - 1;
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
0000 0100 0000 0000
0000 0110 0000 0000
0000 0111 1000 0000
0000 0111 1111 1000
0000 0111 1111 1111
0000 1000 0000 0000 可以看到变成2的次方
为什么要 int n = cap - 1?
我的理解是,因为给定的MAXIMUM_CAPACITY = 1 << 30,相当于在说明文档中说,最大值可以取1<<30。如果不减1,那么将1<<30带入后结果为0,与要求不符,所以-1后计算。

当然,如果未指定容量,则同样也是懒加载机制

public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
    }

特点

2:put
检查数组是否为空,执行resize()扩充;
通过hash值计算数组索引,获取该索引位的首节点。
如果首节点为null,直接添加节点到该索引位。
如果首节点不为null,那么有3种情况
① key和首节点的key相同,覆盖value;否则执行②或③
② 如果首节点是红黑树节点(TreeNode),将键值对添加到红黑树。
③ 如果首节点是链表,将键值对添加到链表。添加之后会判断链表长度是否到达TREEIFY_THRESHOLD - 1这个阈值,“尝试”将链表转换成红黑树。
最后判断当前元素个数是否大于threshold,扩充数组。

public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
    //tab存放当前的哈希桶,p用作临时链表节点  
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    //如果当前哈希表是空的,代表是初始化
    //table为Entry,tab为Node,Entry是Node的子类,上转型对象
    if ((tab = table) == null || (n = tab.length) == 0)
    //那么直接去扩容哈希表,并且将扩容后的哈希桶长度赋值给n
    n = (tab = resize()).length;
    //如果当前index的节点是空的,表示没有发生哈希碰撞。直接构建一个新节点Node,挂载在index处即可。
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    else {//否则 发生了哈希冲突。
        Node<K,V> e; K k;
        //如果哈希值相等,key也相等,则是覆盖value操作
        if (p.hash == hash &&((k = p.key) == key || (key != null && key.equals(k))))
            e = p;//将当前节点引用赋值给e
        else if (p instance of TreeNode)//如果是红黑树节点
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        else {//不是红黑树节点
            //遍历链表
            for (int binCount = 0; ; ++binCount) {
                if ((e = p.next) == null) {//遍历到尾部,,仍没有key与其相等,说明是新节点,追加新节点到尾部
                    p.next = newNode(hash, key, value, null);
                    //如果追加节点后,链表数量>=8,则转化为红黑树
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                    treeifyBin(tab, hash);
                    break;
                }
                //如果找到key与其相同,说明是曾经插入过的节点
                if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                p = e;
            }
        }
        //如果e不是null,说明有需要覆盖的节点,由上面的break;弹出
        if (e != null) { // existing mapping for key
            //则覆盖节点值,并返回原oldValue
            V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            //这是一个空实现的函数,用作LinkedHashMap重写使用。
            afterNodeAccess(e);
            return oldValue;
        }
    }
    //如果执行到了这里,说明插入了一个新的节点,所以会修改modCount,以及返回null。
    ++modCount;
    //更新size,并判断是否需要扩容。
    if (++size > threshold)
    resize();
    //这是一个空实现的函数,用作LinkedHashMap重写使用。
    afterNodeInsertion(evict);
    return null;
}

计算实际存储位置:
1:利用Java重写的HashCode计算哈希值
2:对哈希值进行扰动处理 (h = key.hashCode()) ^ (h >>> 16)
3:与运算代替模计算(n - 1) & hash

hash():转为hash值

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

hashCode的高16位与低16位进行异或运算注意h进行h >>> 16已经被赋值,因为内部数组的容量一般都不会很大,基本分布在16~256之间。所以一个32位的hashCode,一直都用最低的4到8位进行与运算,而高位几乎没有参与。“扰动函数”
获得索引值:

(p = tab[i = (n - 1) & hash]) == null

(n - 1) & hash实际上=hash%(n)n是长度,为一个2的次方数
利用与运算代替模运算可以极大增加运算速度

Hash表扩容:
1:计算新数组的容量和阈值
2:将原数组的元素拷贝到新数组中
这里JDK1.7之前利用的是重新计算Hash索引,即rehash操作
1.8后,由于数组容量是2的次方且扩充后翻倍,则只需要判断原来的hash值新增的那个bit是1还是0就好了,是0的话索引没变,是1的话索引变成“原索引+oldCap”

final HashMap.Node<K,V>[] resize() {
    HashMap.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;
        }
        // 否则数组容量扩充一倍。(2的N次方)
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                oldCap >= DEFAULT_INITIAL_CAPACITY)
            newThr = oldThr << 1; // double threshold
    }
    // 如果数组还没创建,但是已经指定了threshold(这种情况是带参构造创建的对象),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);
    }
    // 可能是上面newThr = oldThr << 1时,最高位被移除了,变为0。
    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"})
    HashMap.Node<K,V>[] newTab = (HashMap.Node<K,V>[])new HashMap.Node[newCap];
    table = newTab;
    
    // 下面代码是将原来数组的元素转移到新数组中。问题在于,数组长度发生变化。 
    // 那么通过hash%数组长度计算的索引也将和原来的不同。
    // jdk 1.7中是通过重新计算每个元素的索引,重新存入新的数组,称为rehash操作。
    // 这也是hashMap无序性的原因之一。而现在jdk 1.8对此做了优化,非常的巧妙。
    if (oldTab != null) {
        
        // 遍历原数组
        for (int j = 0; j < oldCap; ++j) {
            // 取出首节点
            HashMap.Node<K,V> e;
            if ((e = oldTab[j]) != null) {
                oldTab[j] = null;
                // 如果链表只有一个节点,那么直接重新计算索引存入新数组。
                if (e.next == null)
                    newTab[e.hash & (newCap - 1)] = e;
                // 如果该节点是红黑树,执行split方法,和链表类似的处理。
                else if (e instanceof HashMap.TreeNode)
                    ((HashMap.TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                
                // 此时节点是链表
                else { // preserve order
                    // loHead,loTail为原链表的节点,索引不变。
                    HashMap.Node<K,V> loHead = null, loTail = null;
                    // hiHeadm, hiTail为新链表节点,原索引 + 原数组长度。
                    HashMap.Node<K,V> hiHead = null, hiTail = null;
                    HashMap.Node<K,V> next;
                    
                   // 遍历链表
                    do {
                        next = e.next;
                        // 新增bit为0的节点,存入原链表。
                        if ((e.hash & oldCap) == 0) {
                            if (loTail == null)
                                loHead = e;
                            else
                                loTail.next = e;
                            loTail = e;
                        }
                        // 新增bit为1的节点,存入新链表。
                        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;
}

4:遍历
参考资料
KeySet,values,EntrySet
Set< String>,Collections< String>,Set< Entry< String,String>>

在HashMap构造时,只有一个KeySet的引用,而只有当用户调用Map.KeySet()方法时,才实例化一个最新的KeySet懒加载机制
public Set keySet() {
Set ks = keySet;
if (ks == null) {
ks = new KeySet();
keySet = ks;
}
return ks;
}
而在KeySet是一个内部类
拥有
一:iterator,即可以通过遍历器遍历
public final Iterator iterator() { return new KeyIterator(); }
返回一个KeyIterator,而KeyIterator有nextNode方法进行遍历
final Node<K,V> nextNode() {
Node<K,V>[] t;
Node<K,V> e = next;
// fast-fail 机制的实现 即在迭代器往后遍历时,每次都检测expectedModCount是否和modCount相等
// 不相等则抛出ConcurrentModificationException异常
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
//如果遍历越界,则抛出NoSuchElementException异常
if (e == null)
throw new NoSuchElementException();
if ((next = (current = e).next) == null && (t = table) != null) {
//如果遍历到末尾,则跳到table中下一个不为null的节点处
do {} while (index < t.length && (next = t[index++]) == null);
}
return e;
}
二:调用forEach方法
public final void forEach(Consumer<? super V> action) {—}思想和上述类似,都是利用tab[index]进行遍历访问,并没有涉及Set元素的增添

五:化树

static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
        TreeNode<K,V> parent;  // 红黑树父节点
        TreeNode<K,V> left;
        TreeNode<K,V> right;
        TreeNode<K,V> prev;    // 删除后需要取消链接
        boolean red;
        TreeNode(int hash, K key, V val, Node<K,V> next) {
            super(hash, key, val, next);
        }

我们知道当一个桶中节点数量超过8时就会转化为红黑树,过程如下:
步骤一:treeifyBin()先将所有节点替换为TreeNode,然后再将单链表转为双链表
步骤二:treeify(tab);依次比较节点,分别比较Hash值(HashCode异或16位的值),是否有比较器,两者的类名(),对象的HashCode
步骤三:进行平衡调整
d = a.getClass().getName().
compareTo(b.getClass().getName())) == 0

关键点:为什么实际大小总是2的幂?
我的理解:

原因一:
因为index=hash%length-1
比如对于:
length=16 则length-1 =15 即 00001111
length=32 则length-1=31 即 00011111
可以看到,对于一个相同的hash,分别&两个不同的length-1,得到的结果result1,result2 差别就只在右数第5位,相当于扩容后只要修改一位就可以改变索引

原因二:
因为length=2^n,则length-1的位都是保持00001111111的形状,而hash&length-1中,高位不会产生影响,而低位任意一个变化变化都会产生影响,减少冲突的概率。
在h<length的情况下:
对于长度:23 length-1则比特为 10110,则对于结果result=10110
既可以是h=11110 10110 11111 10111 都对应相同的结果冲突增加
而对于长度32 length-1 则比特位11111 ,则对于结果10110
只有h=10110才可以与其对应

原因三:可以用与运算代替模运算,增加了效率

JDK1.7和JDK1.8中HashMap的区别
底层:
最重要的一点是底层结构不一样,1.7是数组+链表,1.8则是数组+链表+红黑树结构;并且内部类的实现也有区别
1.7:Entry
1.8:Node-Entry-TreeNode 两个Entry是不同的

put:
1:1.8中会将节点插入到链表尾部,而1.7中是采用头插
头插法:
1:扩容时颠倒链表顺序
2:扩容时形成闭环
线程1插入节点A,线程2插入节点B。顺序
1A 2B 2A 此时成环
而尾插法会遍历链表,如果发现已经在则停止
为什么头插法形成闭环
2:jdk1.7中的hash函数对哈希值的计算直接使用key的hashCode值,而1.8中则是采用key的hashCode异或上key的hashCode进行无符号右移16位的结果,避免了只靠低位数据来计算哈希时导致的冲突,计算结果由高低位结合决定,使元素分布更均匀;

扩容:
1:扩容时1.8会保持原链表的顺序,而1.7会颠倒链表的顺序;
2:jdk1.8是扩容时通过hash&cap是否等于0将链表分散,无需改变hash值,而1.7是通过更新hashSeed来修改hash值达到分散的目的;
3:扩容策略:1.7中是只要不小于阈值就直接扩容2倍;而1.8的扩容策略会更优化,当数组容量未达到64时,以2倍进行扩容,超过64之后若桶中元素个数不小于7就将链表转换为红黑树,但如果红黑树中的元素个数小于6就会还原为链表,当红黑树中元素大于32的时候才会再次扩容。

HashTable:
Hashtable 是遗留类
很多映射的常用功能与 HashMap 类似,不同的是它承自 Dictionary 类,
线程安全的,并发性不如 ConcurrentHashMap,
HashTable和HashMap的区别:
1:HashTable线程安全,HashMap线程不安全
2:HashTable不允许键和值为Null,而HashMap允许。
两点原因:
1:源码角度
HashTable:可以看到会直接调用hash=key.hashCode();当key为null时报错在这里插入图片描述
而HashMap会进行一个判断,如果key为null则设为0
在这里插入图片描述
2:设计角度,hashtable为早期版本,可能认为null不可以作为分类依据。而后面的hashmap可能认为null也可以作为分类依据

HashTable、ConcurrentHashMap以及Collections中的静态方法SynchronizedMap比较:
HashTable:对set/get方法都使用synchronized
SynchronizedMap:内部封装HashMap,加了synchronized
ConcurrentHashMap:synchronized+CAS

红黑树:
参考资料
参考资料
性质:
1:根节点和叶子结点是黑色
2:不能出现连续的两个红色结点
3:从根到任意叶子结点上的黑色结点数相同
根据定义:
若节点只有一棵子树,那么一定是黑根+红子

查找:O(log2(N))的时间复杂度
插入:插入节点都是红色结点
判断父亲节点
1:若是黑色,直接插入红色节点
2:若是红色,判断叔叔节点。插入红色节点后
若叔叔节点是红色:交换色(父叔节点和祖父节点换色)
若叔叔节点是黑色(或者不存在):交换色+旋转 四种情况
其实不可能是黑色,只有不存在的情形。因为父亲节点是单子树或者子节点,但是如果是单子树只能是黑色(这样却不满足性质3),所以父亲节点是叶子结点,那么叔叔只能是红色或者无
删除:
步骤1:确定实际的删除位置
步骤2:当进入情形1时,进行树的平衡调整

步骤1:
情形1:叶子节点:如果是红色直接删除,如果是黑色那么需要进行平衡
情形2:有一个叶子:子节点代替(变为删除子节点)——转化为情形1或2或3
情形3:有两个叶子:最邻近代替(一般是右子树,变为删除替换节点)——直接转化为情形1
叶子代替的时候,将代替节点转化为被代替的颜色

树的平衡调整
被删除结点:
若被删除结点兄弟节点在右子树
被删除结点节点红色:直接删除
被删除结点节点黑色:
1 被删除结点兄弟节点为红色:父亲兄弟改色+旋转——2.3
2 被删除结点兄弟节点是黑色:
2.1兄弟节点的右子节点为红色:兄弟黑,父亲兄右红+旋转(唯一终态)
2.2兄弟节点的右子为黑色,但是左子为红色:换色后旋转——变为2.1
2.3兄弟节点双子都是黑色:改色+父节点作为替换节点
优势:
平衡二叉树的插入/删除操作带来的旋转操作可能会达到logn次,而红黑树的插入/删除操作带来的旋转操作最多为2/3次。

平衡二叉树的删除:

参考资料

左子树上节点的删除相当于我们在右子树上插入了一个新节点,右子树上节点的删除相当于在左子树上插入了一个新节点
三种情况:
1:被删节点为叶节点
2:被删节点只一个叶节点
3:被删节点有两个叶子结点

在这里插入图片描述

LInkedHashMap:
HashMap和LinkedList的结合体,可以看到节点含有三个指针(链表),四个指针(树)

static class Entry<K,V> extends Node<K,V> {
        Entry<K,V> before, after;
        Entry(int hash, K key, V value, Node<K,V> next) {
            super(hash, key, value, next);
        }
    }

在这里插入图片描述

accessOrder两种顺序进行存储
一种是元素的插入顺序,另一种便是元素的访问顺序(LRU缓存实现)
LRU的实现:最久未访问元素
addBefore(lm.header)是把当前访问的元素挪到head的前面,即最近访问的元素被放到了链表头,如此要实现LRU算法只需要从链表末尾往前删除就可以
而删除如何实现的呢?

void addEntry(int hash, K key, V value, int bucketIndex) {
        super.addEntry(hash, key, value, bucketIndex);
 
        // Remove eldest entry if instructed
        Entry<K,V> eldest = header.after;
        if (removeEldestEntry(eldest)) {
            removeEntryForKey(eldest.key);
        }
    }

可以看到判断removeEldestEntry(eldest)即是否需要删除最晚元素
LinkedHashMap默认的removeEldestEntry方法如下

protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
        return false;
    }

所以开发者需要实现LRU算法只需要继承LinkedHashMap并重写removeEldestEntry方法

内部比较器和外部比较器:
参考资料
内部:实现Comparable接口并重写compareTo
外部:单独定义一个类实现Comparator接口并重写compare

TreeMap:底层架构是红黑树
数据结构是Entry,并没有复用HashMap的TreeNode节省内存?
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;
}
比较是先进行外部比较器的比较,再进行内部比较器的比较
如果没有定义外部比较器,key=null则报错
如果没有定义外部比较器,key也没有实现Comparable接口,报
问题一:
基本数据结构都实现了Comparable,但是如果是某个类,需要实现Comparable接口?
因为其会调用compareTo的方法
问题二:如何实现一致性哈希
tailMap(K fromKey)方法,支持从红黑树中查找比fromKey大的值的集合,但并不需要遍历整个数据结构
第一个顺时针元素Integer i = subMap.firstKey();即是我们的存储节点

我们只要把服务器的节点名字取hashCode(需要加虚拟节点,简便实现方式为+i)put进TreeMap中,之后对于查找的点调用tailMap/subMap.firstKey即可

LinkedListHashMap:结构,实现LRU
TreeMap:比较器,实现一致性哈希

Queue

超级接口:
Queue 接口
Deque 双端接口

ArrayQueue:普通队列
参考资料
1:不可扩容
2:循环队列(数组)
一般来说,队空队满head==tail的区分法有三种常用方法,但是ArrayQueue并没有使用,而是利用this.capacity = capacity + 1;
当add后if (newtail == head)表示此时已经有 capacity + 1个元素,直接报错 不具有容错性,感觉不咋样

public boolean add(T o) {
        queue[tail] = o;
        // 通过除余来实现下标的循环;
        int newtail = (tail + 1) % capacity;
        if (newtail == head)
            throw new IndexOutOfBoundsException("Queue full");
        tail = newtail;
        return true; // we did add something
    }

public T remove(int i) {
        if (i != 0)
            throw new IllegalArgumentException("Can only remove head of queue");
        if (head == tail)
            throw new IndexOutOfBoundsException("Queue empty");
        T removed = queue[head];
        queue[head] = null;
        // 通过除余来实现下标的循环;
        head = (head + 1) % capacity;
        return removed;
    }

ArrayDeque:双端队列,实现了Deque超级接口
参考资料
1:可以扩充,方式为2的次方容积和HashMap一致
区别在于如果输入的大于1<<30,则返回1<<30 理解为10000–为-0,小于0

if (initialCapacity < 0) 
    initialCapacity >>>= 1;

2:利用循环数组,且是双端循环数组理解为两个栈在这里插入图片描述
开始的时候head=0,tail=0。当要在head插入数值时,是先移动位置再赋值
步骤一:head = (head - 1),此时head=-1,而负数是以伪码形似存储1111 1111 1111 1111 1111 1111 1111 1111
步骤二:1111 1111 1111 1111 1111 1111 1111 1111 & (elements.length - 1)
其实本质就是对elements[elements.length - 1]=e
步骤三:当再插入时,-2伪码1111 1111 1111 1111 1111 1111 1111 1110
与之后相当于elements[elements.length - 2]=e
可以看到实际就是上图循环数组的龙尾部分

同理,我们可以对tail进行分析,先赋值再移动位置,实际上就是上图的龙首部分

public void addFirst(E e) {
if (e == null)
throw new NullPointerException();
elements[head = (head - 1) & (elements.length - 1)] = e;
if (head == tail)
doubleCapacity();
}
public void addLast(E e) {
if (e == null)
throw new NullPointerException();
elements[tail] = e;
if ( (tail = (tail + 1) & (elements.length - 1)) == head)
doubleCapacity();

而当 if (head == tail)时,扩充容newCapacity = n << 1
并将head之后的元素复制到新数组的开头,把剩余的元素复制到新数组之后
原数组长度为n,(tail=head)——0为tail栈,(tail=head)——n-1为head栈。记head=tail=flag

新数组相当于交叉原数组
新数组长度为2n,head=0——flag为head栈,tail——flag为tail栈

3:删除元素。同样分为pollFirst()和pollLast() 。并且实现了如栈操作:pop,push,peek,队列操作:add,offer,remove,poll,peek,element,双端队列操作:addFirst,addLast,getFirst,getLast等方法,目的是模拟其他数据结构

4:遍历:可以看到从head开始遍历到tail。没有使用fast-fail机制,而是利用
if (tail != fence || result == null),即判断head和tail和记录的有无更改和modCount一个思想

private class DeqIterator implements Iterator<E> {
        private int cursor = head;

        private int fence = tail;

        private int lastRet = -1;

        public boolean hasNext() {
            return cursor != fence;
        }
        public E next() {
            if (cursor == fence)
                throw new NoSuchElementException();
            @SuppressWarnings("unchecked")
            E result = (E) elements[cursor];
            if (tail != fence || result == null)
                throw new ConcurrentModificationException();
            lastRet = cursor;

PriorityQueue:
1:PriorityQueue是优先级队列,取出元素时会根据元素的优先级进行排序。其内部内部是一个用数组实现的小顶堆
2:使用场景:N取K,动态插入含有优先级的数组

  private void grow(int minCapacity) {
        int oldCapacity = queue.length;
        // 如果当前容量比较小(小于64)的话进行双倍扩容,否则扩容50%
        int newCapacity = oldCapacity + ((oldCapacity < 64) ?
                                         (oldCapacity +2) :
                                         (oldCapacity >> 1));
        // 如果发现扩容后溢出了,则进行调整
        if (newCapacity - MAX_ARRAY_SIZE > 0)
            newCapacity = hugeCapacity(minCapacity);
        queue = Arrays.copyOf(queue, newCapacity);
    }

Set

基本类
AbstractSet:提供 Set 接口的骨干实现,从而最大限度地减少了实现此接口所需的工作。实现了HashCode,equals,removeAll
SortedSet:按照对象的比较函数对元素排序在这里插入图片描述
NavigableSet:接口继承自SortedSet接口在这里插入图片描述

引申类:
HashSet
通过内部成员变量HashMap实现其功能
public Iterator iterator() {
return map.keySet().iterator();
}
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
其中PRESENT是一个Object 对象

LinkedHashSet:拥有HashSet的功能,且能实现插入有序或者访问有序。依靠成员变量LinkedHashMap实现
TreeSet:可自定义排序的Set
拥有成员变量NavigableMap
private transient NavigableMap<E,Object> m;
// map中共用的一个value
private static final Object PRESENT = new Object();

KeySet:HashMap的内部类,继承AbstractSet

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值