Java集合类的主要目的是创建一个简单的、高性能的数据结构操作框架。

总览

Java的集合框架均是围绕一组标准的接口而设计的,根接口是数据集合(Collection)与键值映射表(Map),并基于这两个接口派生了多个子接口,进而实现了包括数组、哈希表、集合等数据结构,以Collection接口为例,其派生的主要集合类如下:
Java集合类剖析 - 图1
在该框架中,主要有三部分内容,其一,容器的接口,如Collection、Map、Set等等;其二,实现类,是对容器的具体实现,也就是实际所用的数据结构,例如HashSet、ArrayList等等;其三,相应的算法,这些算法主要是配套数据集结构进行使用,完成一些常用的功能,如排序、搜索等等。

Collection

Collection接口主要定义了一些通用的方法,留待后续数据结构进行实现,详情如下:

  1. public interface Collection<E> extends Iterable<E> {
  2. int size(); // 集合内元素个数
  3. boolean isEmpty();//该集合是否为空
  4. boolean contains(Object o);//是否包含某个元素,需要进行搜索
  5. Iterator<E> iterator();//返回迭代器
  6. Object[] toArray();//转换为数组
  7. <T> T[] toArray(T[] a);
  8. boolean add(E e);
  9. boolean remove(Object o);
  10. boolean containsAll(Collection<?> c);//相当于判断某个集合是不是另外集合的子集
  11. boolean addAll(Collection<? extends E> c);
  12. boolean removeAll(Collection<?> c);
  13. default boolean removeIf(Predicate<? super E> filter){
  14. …… //当满足条件时,对元素进行删除
  15. }
  16. boolean retainAll(Collection<?> c);//需要保留的元素,其他的删除
  17. void clear();
  18. boolean equals(Object o);
  19. int hashCode();
  20. default Spliterator<E> spliterator() { //分割为多个迭代器
  21. return Spliterators.spliterator(this, 0);
  22. }
  23. default Stream<E> stream() {//流处理
  24. return StreamSupport.stream(spliterator(), false);
  25. }
  26. default Stream<E> parallelStream() {//并行处理
  27. return StreamSupport.stream(spliterator(), true);
  28. }
  29. }

Iterable与Iterator

另外,Collections与Map都继承了Iterable接口,Iterable主要用作遍历:

  1. public interface Iterable<T> {
  2. Iterator<T> iterator();
  3. default void forEach(Consumer<? super T> action) {
  4. Objects.requireNonNull(action);
  5. for (T t : this) {
  6. action.accept(t);
  7. }
  8. }
  9. default Spliterator<T> spliterator() {
  10. return Spliterators.spliteratorUnknownSize(iterator(), 0);
  11. }
  12. }

其中最重要的时迭代器Iterator接口:

  1. public interface Iterator<E> {
  2. boolean hasNext();
  3. E next();
  4. default void remove() {
  5. throw new UnsupportedOperationException("remove");
  6. }
  7. default void forEachRemaining(Consumer<? super E> action) {
  8. Objects.requireNonNull(action);
  9. while (hasNext())
  10. action.accept(next());
  11. }
  12. }

迭代器Iterator有两个主要方法:hasNext与next,前者查询是否还有下一个迭代器,后者则是取下一个迭代器。不同数据结构对这两个方法的实现不一致,后文再表。
image.png
接下来先介绍Collection接口的三个子接口:List、Set、Queue。

List

根据Java Doc所言,List集合是一个元素可重复的有序集合,同广义上的数组类似,可以用过下标直接操作集合内元素。除了Collections接口原本的方法之外,List增加了一些自定义的方法:

  1. public interface List<E> extends Collection<E> {
  2. ……//Collection继承的
  3. default void sort(Comparator<? super E> c) {
  4. //根据comparator进行元素排序
  5. }
  6. E get(int index); //获取指定下标的元素
  7. E set(int index, E element); // 替换指定下标的元素,返回原来的元素值
  8. int indexOf(Object o); //返回第一次出现的下标,否则为-1
  9. int lastIndexOf(Object o);//最后一次出现,否则为-1
  10. List<E> subList(int fromIndex, int toIndex); //根据下标提取子List
  11. //iterator迭代器
  12. ListIterator<E> listIterator(); //返回迭代器
  13. ListIterator<E> listIterator(int index); //指定位置的迭代器
  14. }

ListIterator继承于Iterator,在原本的基础上进行方法的增加,从而便于List的遍历和使用:

  1. public interface ListIterator<E> extends Iterator<E> {
  2. boolean hasNext();
  3. E next();
  4. //之前是否还有迭代器,与hasNext方向相反
  5. boolean hasPrevious();
  6. E previous();
  7. //迭代器下次访问的下标,如果已到末尾,则返回列表尺寸
  8. int nextIndex();
  9. //迭代器下次访问的下标(往前),如果已到开头,则返回-1
  10. int previousIndex();
  11. //从迭代器位置开始进行删除
  12. void remove();
  13. //替换迭代器位置的元素
  14. void set(E e);
  15. //其实是insert,再迭代器位置进行元素插入
  16. void add(E e);
  17. }

Vector

Vector的底层其实是元素数组,有三个重要属性:

  1. //元素数组,使用Object类型进行初始化
  2. protected Object[] elementData;
  3. //当前数组中的元素个数
  4. protected int elementCount;
  5. //扩容时容量增加数量,默认为0,表示容量翻倍,否则为old+capacityIncrement
  6. protected int capacityIncrement;

在往数组中进行插入时,需要先判定当前空间是否足够:

  1. public synchronized boolean add(E e) {
  2. modCount++;
  3. ensureCapacityHelper(elementCount + 1);
  4. elementData[elementCount++] = e;
  5. return true;
  6. }

其中的ensureCapacityHelper即是判断当前的容量是否够用,如果不够需要调用grow()方法进行扩容:

  1. private void grow(int minCapacity) {
  2. // overflow-conscious code
  3. int oldCapacity = elementData.length;
  4. int newCapacity = oldCapacity + ((capacityIncrement > 0) ?
  5. capacityIncrement : oldCapacity);
  6. if (newCapacity - minCapacity < 0)
  7. newCapacity = minCapacity;
  8. if (newCapacity - MAX_ARRAY_SIZE > 0)
  9. newCapacity = hugeCapacity(minCapacity);
  10. elementData = Arrays.copyOf(elementData, newCapacity);
  11. }

值得注意的是,Vector是线程安全的,采用synchronized对成员函数进行加锁;

Stack

Stack继承Vector,在基类的基础上实现了push、pop、peek等方法,从而实现了栈的功能,也是线程安全的。

其实就是在Vector的末尾进行插入、删除等操作。

ArrayList

与Vector一样,ArrayList的底层也是一个自动扩容数组,且扩容为2倍(无法修改)
另外,ArrayList是线程不安全的,不涉及synchronized,因此其性能相较于Vector更好,因为少去了加锁等操作。

LinkedList

LinkedList类是List接口的实现类——这意味着它是一个List集合,可以根据索引来随机访问集合中的元素。除此之外,LinkedList还实现了Deque接口,可以被当作成双端队列来使用,因此既可以被当成“栈”来使用,也可以当成队列来使用。
LinkedList的实现机制与ArrayList完全不同。ArrayList内部是以数组的形式来保存集合中的元素的,因此随机访问集合元素时有较好的性能;而LinkedList内部以链表的形式来保存集合中的元素,因此随机访问集合元素时性能较差,但在插入、删除元素时性能比较出色。

ArrayList与LinkedList性能对比

ArrayList 是一个数组队列,相当于动态数组。它由数组实现,随机访问效率高,随机插入、随机删除效率低。ArrayList应使用随机访问(即,通过索引序号访问)遍历集合元素。
LinkedList 是一个双向链表。它也可以被当作堆栈、队列或双端队列进行操作。LinkedList随机访问效率低,但随机插入、随机删除效率高。LinkedList应使用采用逐个遍历的方式遍历集合元素。
如果涉及到“动态数组”、“栈”、“队列”、“链表”等结构,应该考虑用List,具体的选择哪个List,根据下面的标准来取舍。
(01) 对于需要快速插入,删除元素,应该使用LinkedList。
(02) 对于需要快速随机访问元素,应该使用ArrayList
(03) 对于“单线程环境” 或者 “多线程环境,但List仅仅只会被单个线程操作”,此时应该使用非同步的类(如ArrayList)。对于“多线程环境,且List可能同时被多个线程操作”,此时,应该使用同步的类(如Vector)。

Set

Set就是常规意义上的集合(Set),其接口定义与Collection几乎一样,特点在于该集合内部不能存在相同的元素,在第二次加入相同的元素时会提示错误。

HashSet

HashSet实质上是一个HashMap,并且在插入某一个对象时,以对象作为键,并“虚构”一个对象作为值:

  1. public boolean add(E e) {
  2. return map.put(e, PRESENT)==null;
  3. }

当HashMap已存在该对象时,返回false,且不会改变HashMap,否则返回True,表示值已插入。
注意,HashSet是线程不安全的。
HashSet中藏着一个包私有的方法:

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

这是给其子类LinkedHashSet提供的实现函数。

EnumSet

枚举EnumSet是一个虚基类,无法直接New出。那么如何使用?
它提供了一系列的静态接口,因此可以通过静态工厂来获取EnumSet对象:

  1. EnumSet<E> noneOf(Class<E> elementType):构造一个空的集合
  2. EnumSet<E> allOf(Class<E> elementType):构造一个包含枚举类中所有枚举项的集合
  3. EnumSet<E> of(E e):构造包含1个元素的集合
  4. EnumSet<E> of(E e1, E e2):构造包含2个元素的集合
  5. EnumSet<E> of(E e1, E e2, E e3):构造包含3个元素的集合
  6. EnumSet<E> of(E e1, E e2, E e3, E e4):构造包含4个元素的集合
  7. EnumSet<E> of(E e1, E e2, E e3, E e4, E e5):构造包含5个元素的集合
  8. EnumSet<E> of(E first, E... rest):构造包含多个元素的集合(使用可变参数)
  9. EnumSet<E> copyOf(EnumSet<E> s):构造包含参数中所有元素的集合
  10. EnumSet<E> copyOf(Collection<E> c):构造包含参数中所有元素的集合

例如使用of获取EnumSet:

  1. public static <E extends Enum<E>> EnumSet<E> of(E e) {
  2. EnumSet<E> result = noneOf(e.getDeclaringClass());
  3. result.add(e);
  4. return result;
  5. }

其中国noneOf是EnumSet的关键函数,它会根据Set中的元素数目返回指定的枚举集合:

  1. public static <E extends Enum<E>> EnumSet<E> noneOf(Class<E> elementType) {
  2. Enum<?>[] universe = getUniverse(elementType);
  3. if (universe == null)
  4. throw new ClassCastException(elementType + " not an enum");
  5. if (universe.length <= 64)
  6. return new RegularEnumSet<>(elementType, universe);//仅有64位,以long为底层
  7. else
  8. return new JumboEnumSet<>(elementType, universe);
  9. }

作个补充,什么是枚举类型(Enum Type)?
简单来说就是定义一个枚举类型,该枚举类型中的取值范围已经做了限定,比如彩虹七色

  1. enum RainbowColor { RED, ORANGE, YELLOW, GREEN, CYAN, BLUE, PURPLE }

那么EnumSet是怎么实现集合的功能的呢?比如add、contain?
位运算!如下是RegularEnumSet中add的部分代码:

  1. public boolean add(E e) {
  2. typeCheck(e);
  3. long oldElements = elements;
  4. elements |= (1L << ((Enum<?>)e).ordinal());
  5. return elements != oldElements;
  6. }

其中typeCheck(e)是为了确保插入元素确实属于枚举类型,之后的三行操作简单理解就是通过位运算查看当前Set中是否已经插入了某个枚举类型,如果已经存在,则elements与oldElements相等,返回false,否则返回True。
contains的代代码如下:

  1. public boolean contains(Object e) {
  2. if (e == null)
  3. return false;
  4. Class<?> eClass = e.getClass();
  5. if (eClass != elementType && eClass.getSuperclass() != elementType)
  6. return false;
  7. return (elements & (1L << ((Enum<?>)e).ordinal())) != 0;
  8. }

关键就在于最后一行代码,通过相与操作来判定当前Set中是否已存在该对象。

可阅读:https://www.cnblogs.com/yuexiaoyun/p/12078048.html

注意,EnumSet也不是线程安全的。

TreeSet

基于TreeMap进行实现。

Queue

Queue队列,提供出队、入队方法:

  1. public interface Queue<E> extends Collection<E> {
  2. boolean add(E e);//如果空间足够将指定元素加入队列,否则报出异常
  3. boolean offer(E e);//当用于有空间限制的队列时,优于add方法
  4. E remove();//获取并移除队列头,为空则报出异常
  5. E poll();//获取并移除队列头,为空则返回null
  6. E element();//获取队列头,为空则报出异常
  7. E peek();//获取队列头,为空则返回null
  8. }

ArrayDeque

https://www.jianshu.com/p/35760d7bac0d

ArrayDeque是基于数组实现的双端队列,ArrayDeque为了满足可以同时在数组两端插入或删除元素的需求,其内部的动态数组还必须是循环的,即循环数组(circular array),也就是说数组的任何一点都可能被看作起点或者终点。
下面再说说扩容函数doubleCapacity(),其逻辑是申请一个更大的数组(原数组的两倍),然后将原数组复制过去。过程如下图所示:
image.png
图中我们看到,复制分两次进行,第一次复制head右边的元素,第二次复制head左边的元素

  1. private void doubleCapacity() {
  2. assert head == tail;
  3. int p = head;
  4. int n = elements.length;
  5. int r = n - p; // number of elements to the right of p
  6. int newCapacity = n << 1;
  7. if (newCapacity < 0)
  8. throw new IllegalStateException("Sorry, deque too big");
  9. Object[] a = new Object[newCapacity];
  10. System.arraycopy(elements, p, a, 0, r);
  11. System.arraycopy(elements, 0, a, r, p);
  12. elements = a;
  13. head = 0;
  14. tail = n;
  15. }

PriorityQueue

PriorityQueue会根据队列中元素大小进行排序,因此当调用Peek或Pool取出队列元素时,并不是取出最先进入队列的元素,而是最大(最小,或自定义的方法)的元素。
怎么实现的?
其实本质也是一个动态数组,并通过二分法来进行元素的插入:

  1. private void siftUpComparable(int k, E x) {
  2. Comparable<? super E> key = (Comparable<? super E>) x;
  3. while (k > 0) {
  4. int parent = (k - 1) >>> 1;
  5. Object e = queue[parent];
  6. if (key.compareTo((E) e) >= 0)
  7. break;
  8. queue[k] = e;
  9. k = parent;
  10. }
  11. queue[k] = key;
  12. }

PriorityQueue实现插入方法(offer、poll、remove() 和 add 方法) 的时间复杂度是O(log(n)) ;
实现 remove(Object) 和 contains(Object) 方法的时间复杂度是O(n) ;
实现检索方法(peek、element 和 size)的时间复杂度是O(1)。
所以在遍历时,若不需要删除元素,则以peek的方式遍历每个元素。

Map

Map(映射表)给用户提供了一种通过指定关键词来查询对应对象数据的方法。与List(数组)相比,如果要找到List中某个指定员工的薪资,智能通过遍历查找,这种方式过于费时费力。而使用Map,则可以直接根据员工姓名name返回其详细信息,因此Map的主要使用场景就是需要进行快速映射(定位:Key-Value
Java集合类剖析 - 图4
与Collection一样,Map其实是一个接口,预定义了一些通用的方法,主要进行键值对的存储、查询、删除等,之后由后续具体的实现类对其进行实现,相关方法定义和功能如下:

  1. public interface Map<K,V>
  2. {
  3. int size();
  4. boolean isEmpty();
  5. boolean containsKey(Object key);//Map中是否存在某个键
  6. boolean containsValue(Object value);//是否存在某个值
  7. V get(Object key);//根据键获取值
  8. V put(K key, V value);//将一个(K,V)键值对插入Map中
  9. V remove(Object key);//删除某一个键值对
  10. //将两个Map进行合并,但是K与V必须满足泛型要求
  11. void putAll(Map<? extends K, ? extends V> m);
  12. void clear();//清空Map
  13. Set<K> keySet();//获取Map中所有的键,并放入一个Set中
  14. Collection<V> values();//获取Map所有的值,放入Collection中
  15. Set<Map.Entry<K, V>> entrySet();//将Map中的键值对用一个Set来存放,Entry的定义见下
  16. default V putIfAbsent(K key, V value) {...}//如果当前不存在某个Key,进行插入
  17. //当前key所对应的value满足条件进行更换
  18. default boolean replace(K key, V oldValue, V newValue) {
  19. Object curValue = get(key);
  20. if (!Objects.equals(curValue, oldValue) ||
  21. (curValue == null && !containsKey(key))) {
  22. return false;
  23. }
  24. put(key, newValue);
  25. return true;
  26. }
  27. //强制进行value替换
  28. default V replace(K key, V value) {
  29. V curValue;
  30. if (((curValue = get(key)) != null) || containsKey(key)) {
  31. curValue = put(key, value);
  32. }
  33. return curValue;
  34. }
  35. ......
  36. }

值得注意的是,在Map中不能存在相同的Key,可以存在相同的value!当插入相同的Key时,其实是后面的一组将之前的内容给覆盖掉了,比如:

  1. Map<String, Integer> map = new HashMap<>();
  2. map.put("zhangsan", 28000);
  3. map.put("zhangsan", 38000);

上述代码执行完毕之后,通过“zhangsan”进行查询时,获得的Value其实是38000。
另外,之前在Map的方法定义里可以看到put的方法签名为:V put(K key, V value),如果Map中本来不存在这个key,返回的V就是null,如果存在key,返回的V就是之前的value。

在定义的方法中,定义了两个replace方法用来更换键值对的值:boolean replace(K key, V oldValue, V newValue)与V replace(K key, V value) ,两者的区别在于前者仅在oldValue等于当前key所对应的值是进行更换,而后者则是只要当前key存在就进行更换,如何选择使用对应的方法根据实际使用场景来确定即可。

Entry

在Map中其实所有的K-V键值对都是用一类对象来存储的——Entry。
Entry在Key与Value基础上进行了封装,从而实现Key与Value的对应关系,相关接口定义如下:

  1. interface Entry<K,V> {
  2. K getKey();
  3. V getValue();
  4. V setValue(V value);
  5. boolean equals(Object o);
  6. int hashCode();
  7. public static <K extends Comparable<? super K>, V> Comparator<Map.Entry<K,V>>
  8. comparingByKey() {
  9. return (Comparator<Map.Entry<K, V>> & Serializable)
  10. (c1, c2) -> c1.getKey().compareTo(c2.getKey());
  11. }
  12. public static <K, V extends Comparable<? super V>> Comparator<Map.Entry<K,V>>
  13. comparingByValue() {
  14. return (Comparator<Map.Entry<K, V>> & Serializable)
  15. (c1, c2) -> c1.getValue().compareTo(c2.getValue());
  16. }
  17. public static <K, V> Comparator<Map.Entry<K, V>>
  18. comparingByKey(Comparator<? super K> cmp) {
  19. Objects.requireNonNull(cmp);
  20. return (Comparator<Map.Entry<K, V>> & Serializable)
  21. (c1, c2) -> cmp.compare(c1.getKey(), c2.getKey());
  22. }
  23. public static <K, V> Comparator<Map.Entry<K, V>>
  24. comparingByValue(Comparator<? super V> cmp) {
  25. Objects.requireNonNull(cmp);
  26. return (Comparator<Map.Entry<K, V>> & Serializable)
  27. (c1, c2) -> cmp.compare(c1.getValue(), c2.getValue());
  28. }
  29. }

实际上,Entry才是Map中实际存储的对象:
Java集合类剖析 - 图5
有关Map中的Entry如何排列,跟Entry中定义的四种Comparator比较器有关,且不同的Map实现均对Entry类进行了相应的修改,这将在后文具体的Map实现(HashMap, TreeMap, Hashtable, SortedMap等)中进行讲解。

AbstractMap

Abstract是一个虚拟类,实现了Map的大部分接口,减少后续Map实现类的实现复杂度。

HashMap(可以单独拿一章出来了)

怎么学一个集合? 从它的使用流程来学习,即新建,插入、删除等等。。。。。

首先需要对HashMap(哈希表)有一个整体的概念,Java的HashMap采用开链法,从结构看上来看就是数组+链表(红黑树),如下:
Java集合类剖析 - 图6
上图中,1,2,3……等表示数组下标,可以理解为一个一个的桶,每个桶都可以存放键值对(K-V)
怎么放进去?
这就要说到哈希算法了,简单来说就是用来确定某一个键值对存放在数组的哪个小下标!
假设现在只有十个桶,我们需要将一批键值对(K-V)尽可能均匀地找位置放进去,可以设计一个最简单、最常见的哈希算法:hash(x) = x %10,也就是进行一个除10取余操作,余数是多少,就把键值对放进对应的数组空间内。
其中,x是键值对中Key的hashCode值(简单理解就是一个对象的id,类似于身份证,在程序中独一无二)。
哈希算法的目的,是为了将这批键值对放进数组中,而好的hash算法则是尽可能保证它们均匀的放入数组中,不会出现某一些数组下标内有贼多的键值对,但是另外的一个都没有。
为什么有的数组下标里有多个键值对?
因为桶的数量有限,一些K-V键值对经过哈希算法算出来的下标相同:
比如现在有两个键值对:12-‘apple’ 和 22-‘pencil’
通过除10取余,下标都是2,就只能丢到同一个桶里,为了简单可以用链表将其串联起来,这样就好比某些桶里只有一个球,有些桶里是一串球。
那怎么在链表中找到我想要的键值对?
没有别的办法,只能把链表拎出来,一个一个的比对Key,直到找到想要的Key为止,因为哈希表中的Key不允许重复。
可同一个桶里的键值对太多了怎么办?就不能体现哈希表的快速了啊!
问得好,哈希表就是为了能够实现快速的键值对映射,如果一个桶里的链表太长了,严重影响效率。
比如每一个桶里存放几千上万个键值对,怎么利用哈希表快速的找到自己想要的键值对?
对链表进行遍历?O(N)的时间复杂度有点遭不住……
在这种情况下,有两种解决思路
第一,增加桶的数量,桶的数量多了,不同键值对被分配到同一个桶的概率就小,自然而然链表长度短了,查询时间降低;
第二,在不增加桶数量的前提下,为了优化查询时间,可以把链表转换为红黑树,红黑树这种数据结构的查询效率更高,查询时间仅有O(logn)。
哪种解决思路好?
前者的问题在于,空间总是有限的,不可能一味地增加数组上限,后者的问题在于,维护红黑树比维护一个链表要麻烦得多
那么如何权衡时间与空间呢?
来看看JDK1.8的HashMap中有几个与之有关的重要属性:

  1. //默认值
  2. static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // 默认数组大小(哈希表容量)
  3. static final int MAXIMUM_CAPACITY = 1 << 30;//最大数组大小,2^30
  4. static final float DEFAULT_LOAD_FACTOR = 0.75f;//默认负载因子
  5. static final int TREEIFY_THRESHOLD = 8;//链表转换为红黑树的阈值
  6. static final int UNTREEIFY_THRESHOLD = 6;//红黑树转换为链表的阈值
  7. static final int MIN_TREEIFY_CAPACITY = 64;//转换红黑树所要求的哈希表最小容量
  8. //成员遍历
  9. transient int size;//当前哈希表中的键值对数量
  10. int threshold;//当前键值对数量超过某个上限(capacity*loadfactor)时进行空间翻倍
  11. final float loadFactor;//设置的负载因子,如果没有设置,则为默认值

通过上面几个值,可以来模拟一下Java中HashMap是怎么进行空间和时间的平衡的
1、创建一个HashMap,默认的数组大小为8,也就是有8个桶(这个数组其实会等到首次插入键值对时才会进行申请,亦称惰性分配),也可设置初始数组大小;
2、插入第一个键值对(这才真正分配数组空间),利用hash算法将其放入数组中;
3、插入第二个键值对……第三个……第四个键值对计算出的桶与第一个键值对一样,那么将第四个键值对放在第一个键值对的链表末尾(JDK1.8之后均采用插入链表尾部的方法)
4、当插入第6个(80.75)时,由于超过门限值,哈希表进行扩容,容量变为16,并将之前的键值对拷贝至新申请的数组空间内;
5、继续插值……当容量超过12(16
0.75)时再次扩容,容量变为32;
6、继续插值……当容量超过24(320.75)时再次扩容,容量变为64;
7、继续插值,在这个过程中,如果某一个桶中链表数量达到8(TREEIFY_THRESHOLD),则调用treeifyBin()函数将该链表转换为红黑树,注意,如果在之后进行删除操作,使得该红黑树的数量小于等于6(UNTREEIFY _THRESHOLD),则将该红黑树转换为链表;
8、删除操作大致相反,Over!
再次,总结一下上述过程,简单来说就是:
空间快不够用啦,空间加倍啊!
哈希计算冲突啦,用链表串啊!
*链表太特么长啦,用红黑树啊!

为什么要n-1&hash
任何两个数相与,其结果都不会大于最小的那个数。
因此n-1&hash,其实就是为了将最终的结果限定在数组范围内,相比于%,其效率更高。
还有个重点,在JDK1.8之后,hashmap的长度始终保持是2的倍数原来的Key有两种结果一是在原下标,二是在原下标+原数组长度
比如原来的长度为8 :则n-1&hash计算为 0111 & hash
长度变为16时,则为0 1111 &hash,唯一的变数就在于多了一个1,该位相与的结果可能是1也可能是0。

关于计算tableforsize:power of two
image.png
int 是32字节 ,n-1的好处
image.png
https://www.cnblogs.com/xiyixiaodao/p/14483876.html
说到底,就是要将后面的位全部置为1,而后+1,即可得到一个2的整数幂!

modCount是为了实现快速失败机制,当在访问hashmap的过程中进行了修改,就会报异常,主要应用于多线程环境下。

如何遍历hashmap?
利用foreach方法,遍历底层数组+链表(红黑树);
也可以利用keyset、entryset、values函数直接获取值;

与concurrentHashmap的区别?

在进行迭代时,HashTable会锁住整个Map,而ConcurrentHashMap只锁住Map的一部分,所以ConcurrentHashMap在多线程环境下的性能更好。

锁住整个Map怎么理解? hashtable中直接对方法体上修饰synchronized,当一个线程操作时,其他线程都无法访问; 而concurrenthashmap仅锁定方法体中的一部分代码块,其他线程还可以调用相应的线程方法: image.png

另外,concurrentHashmap的kv也不能有null,与hashtable保持一致。

如何让HashMap实现同步功能?
Map m = Collections.synchronizeMap(hashMap);

HashTable

其实现时基于数组+链表实现的,并具有一定的线程安全性。与HashMap相比:
实现接口不同
HashTable基于已经废弃的Dictionary类与Map接口实现的。
而HashMap主要是基于Map实现的,属于Java集合框架。
实现原理不同
Hashtable采用开链法构造哈希表,且hash函数较为简单:
index = (hash & 0x7FFFFFFF) % tab.length;
但Hashmap使用链表与红黑树相结合的方式实现,且hash函数较为复杂。
线程安全性不同
Hashtable利用synchronized实现线程安全。
而HashMap并没有进行相应的线程安全处理。
K-V键值对的要求不同
Hashtable要求K-V中的kv都不能为null,而Hashmap则两者都可以为null,关键性因素在于hashtable中直接对key调用了hashcode,会报错,而hashmap计算hashcode时先进行了判断:

  1. //hashtable
  2. int hash = key.hashCode();//问题出在这里!!!
  3. //hashmap
  4. static final int hash(Object key) {
  5. int h;
  6. //当key等于null的时候,不走hashCode()方法
  7. return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
  8. }

This函数是什么?

在看hashtable的源码时,发现:

  1. public Hashtable() {
  2. this(11, 0.75f);
  3. }

这个this是Java的一个特点,this表示对象自身,可以用于在一个构造函数中调用另一个构造函数

LinkedHashMap

在HashMap的基础上多维护了一个双向链表的,iteration时的遍历顺序是按照插入顺序(insertion-order)来的。
为记录插入的顺序,LinkedHashMap分别使用两个属性值来记录第一个插入值与最后一个插入值:

  1. /**
  2. * The head (eldest) of the doubly linked list.
  3. */
  4. transient LinkedHashMap.Entry<K,V> head;
  5. /**
  6. * The tail (youngest) of the doubly linked list.
  7. */
  8. transient LinkedHashMap.Entry<K,V> tail;

在进行哈希表的迭代时,其按照插入的Key顺序进行迭代,这就是使用它的场景

TreeMap

基于红黑树实现的Map结构,完全按照Key的自然顺序或指定的Comparator进行排序,因此,查询、插入、删除的时间复杂度均为log(n)。

如何判定相等?
类似于TreeSet中判断两个元素相等的标准,TreeMap中判断两个key相等的标准是:两个key通过compareTo()方法返回0,TreeMap即认为这两个key是相等的。
TreeMap中判断两个value相等的标准是:两个value通过equals()方法比较返回true

Map实现类的性能分析及适用场景

HashMap与Hashtable实现机制几乎一样,但是HashMap比Hashtable性能更好些。
LinkedHashMap比HashMap慢一点,因为它需要维护一个双向链表。
TreeMap比HashMap与Hashtable慢(尤其在插入、删除key-value时更慢),因为TreeMap底层采用红黑树来管理键值对。
适用场景:
一般的应用场景,尽可能多考虑使用HashMap,因为其为快速查询设计的。
如果需要特定的排序时,考虑使用TreeMap。
如果仅仅需要插入的顺序时,考虑使用LinkedHashMap。

WeakHashMap

它的功能和实现与HashMap大致相同,其特点就在于其中的Entry继承了WeakReference(弱引用)类,这就让WeakHashMap中的Entry具备了弱引用特性,那何谓弱引用特性?
弱引用对象的生存周期极短,当弱引用对象不在被其他对象引用时,无论内存是否够用,GC都会对其进行回收
从具体表现上看,在WeakHashMap中,如果某个健值已经不再被其他对象引用,则GC会自动删除其键值对,腾出空间。而在HashMap中,除非主动删除Key,否则该Key会一直存在
如下面的这个例子:

  1. //https://www.cnblogs.com/yjf512/p/7413236.html
  2. public class WeakHashMap {
  3. public static void main(String[] args){
  4. String a = new String("a");
  5. String b = new String("b");
  6. Map weakmap = new WeakHashMap();
  7. weakmap.put(a, "aaa");
  8. weakmap.put(b, "bbb");
  9. a=null; //将字符串对象“a”的引用取消了
  10. System.gc(); //a的键值对会被回收
  11. Iterator j = weakmap.entrySet().iterator();
  12. while (j.hasNext()) {
  13. Map.Entry en = (Map.Entry)j.next();
  14. System.out.println("weakmap:"+en.getKey()+":"+en.getValue());
  15. // 打印结果:weakmap:b:bbb
  16. }
  17. }
  18. }

WeakHashMap主要应用于缓存中,可以及时对不需要的缓存键值对进行处理,保存内存使用的高效性。

IdentityHashMap

与HashMap不同的是主要有两个区别:
其一,IdentityHashMap中没有使用红黑树;
其二,IdentityHashMap在比较两个Key是否相同时,采用的是引用相同(reference-equality)而HashMap采用对象相同(object-equality),看代码:

  1. //其实就是==与equals的区别
  2. // identityHashmap
  3. k1 == k2
  4. // HashMap
  5. (k = e.key) == key || (key != null && key.equals(k)))

可以看到,Identify的要求很苛刻:必须是引用相等!
什么是引用相等==?那就是两个Key指向的是同一个对象(同一内存地址)!其实equals原本也是比较内存地址,但是很多类对该equals都进行了重写,变成了比较对象中的值。
==与equals的区别