Java集合框架总结

结合JDK1.8源码对其进行总结分析。理解其底层实现原理才是关键。


先上总结结果:

一表胜千言
这里写图片描述
Tips:
除ConcurrentHashMap,均实现 Cloneable, Serializable接口
实现接口 List,能对它进行队列操作
实现 Deque 接口,能当作双端队列使用。
实现接口RandomAccess提供随机访问功能


Java 集合框架

这里写图片描述
Java集合是java提供的工具包,包含了常用的数据结构
Java集合主要可以划分为4个部分:

  • List列表
  • Set集合
  • Map映射
  • 工具类(Iterator迭代器、Enumeration枚举类、Arrays和Collections)

1.Collection 接口

  • List 接口
    实现类有LinkedList, ArrayList, Vector, Stack。
  • Set接口
    实现类有HastSet和TreeSet
    HashSet依赖于HashMap;TreeSet依赖于TreeMap

  • Queue接口
    子类Deque双端队列, LinkedList


Iterator遍历集合的工具

  • ListIterator是专门为遍历List而存在的
  • Enumeration只能在Hashtable, Vector, Stack使用
Arrays和Collections

它们是操作数组、集合的两个工具类


AbstractCollection

  • 定义
    Collection 接口 的一个直接实现类, Collection 下的大多数子类都继承 AbstractCollection ,比如 List 的实现类, Set的实现类
  • 子类
    AbstractList, AbstractQueue, AbstractSet,
    ArrayDeque, ConcurrentLinkedDeque

AbstractList

  • 继承自 AbstractCollection 抽象类
  • 实现了Iterable, Collection, List
  • 子类:AbstractSequentialList, ArrayList, Vector

AbstractSequentialList

  • 继承自 AbstractList,是 LinkedList 的父类
  • 是 List 接口 的简化版实现,只支持按次序访问

Vector

线程安全

  • Vector继承自AbstractList实现接口 List,能对它进行队列操作

  • Vector实现接口Cloneable、RandomAccess、Serializable

重要对象

  • elementData :动态数组,默认容量大小是10
  • elementCount:动态数组的实际大小
  • capacityIncrement:动态数组的增长系数
    若容量增加系数 >0,则将容量的值增加“容量增加系数”;
    否则,将容量大小增加一倍
private void grow(int minCapacity) {
        // overflow-conscious code
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + ((capacityIncrement > 0) ?capacityIncrement : oldCapacity);
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;
        if (newCapacity - MAX_ARRAY_SIZE > 0)
            newCapacity = hugeCapacity(minCapacity);
        elementData = Arrays.copyOf(elementData, newCapacity);
    }

遍历方式

  • 通过迭代器Iterator遍历
  • 随机访问,通过索引值去遍历(实现RandomAccess接口)
  • for循环遍历:for (Integer integer:list)
  • Enumeration遍历

Tips:使用索引的随机访问方式最快,使用迭代器最慢


Stack

线程安全

  • Stack继承于Vector(矢量队列),即通过数组实现的,而非链表
  • 执行peek时(取出栈顶元素,不执行删除),返回数组末尾的元素。

ArrayList

非线程安全

  • ArrayList 继承了AbstractList
  • 实现接口RandomAccess提供随机访问功能
  • 实现接口Cloneable、Serializable
  • 实现接口 Iterable, Collection, List

重要对象

  • elementData:动态数组,容量默认是10
  • size:动态数组的实际大小
  • 容量不足ArrayList会重新设置容量:新的容量=原始容量*1.5
  • JDK1.8 为1.5倍 !!!
  • int newCapacity = oldCapacity + (oldCapacity >> 1);

遍历方式

  • 通过迭代器Iterator遍历
  • 随机访问,通过索引值去遍历(实现RandomAccess接口)
  • for循环遍历

初始化

1.新建默认ArrayList对象
调用默认构造函数,将默认的空数组传给elementData 数组

private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
public ArrayList() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }

2.添加一个元素
此时size=0,调用add函数

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

add函数调用ensureCapacityInternal函数判断是否要扩容,传入参数size+1

private void ensureCapacityInternal(int minCapacity) {
        ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
    }

此时elementData 为默认空数组,默认容量10
不为空直接返回minCapacity
(minCapacity为size+1,返回10)

private static int calculateCapacity(Object[] elementData, int minCapacity) {
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            return Math.max(DEFAULT_CAPACITY, minCapacity);
        }
        return minCapacity;
    }

modCount自增,表示修改次数加一
(传入参数10)
如果minCapacity 大于数组长度,调用grow,参数为minCapacity
(此时minCapacity =10,length=0 )

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

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

把旧数组扩容1.5
(oldCapacity =0)
若扩容后仍小于minCapacity,取minCapacity
(仍为0,直接把10赋给newCapacity )
若扩容后大于最大容量,取最大容量Integer.MAX_VALUE - 8;
(此处小于最大容量)

//一些JVM实现时,会在数组的前面放一些额外的数据
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

调用Arrays.copyOf 把elementData 的元素拷贝到新的数组
内部是System的arraycopy方法

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

最后在add里把elementData[0] = e

elementData[size++] = e;

此时ArrayList容量为10;size为1



Tips:随机访问效率最高,迭代器效率最低


LinkedList

  • LinkedList 继承了AbstractSequentialList的双向链表
  • LinkedList 实现 Deque 接口,即能将LinkedList当作双端队列使用。
  • 实现接口Cloneable、Serializable
  • 实现接口 List,能对它进行队列操作
  • LinkedList 是非同步的

重要对象

  • header :双向链表的表头
  • size:双向链表中节点的个数

遍历方式

  • 通过迭代器Iterator遍历
  • 快速随机访问,通过索引值去遍历(实现List接口)
  • for循环遍历:for (Integer integer:list)

Tips:不要通过随机访问去遍历LinkedList


2.Map接口

  • AbstractMap抽象类,实现了map接口,它实现了Map中的大部分API
    HashMap,TreeMap,WeakHashMap都是继承于AbstractMap
    Hashtable虽然继承于Dictionary,但它实现了Map接口
  • ConcurrentMap接口
  • SortedMap接口
  • Dictionary
  • Map.Entry

HashMap

非线程安全

  • 继承于AbstractMap
  • 实现了Map、Cloneable、java.io.Serializable接口
  • key、value都可以为null。映射不是有序的。
  • Entry对象的hashCode()由关键字key的hashCode()与值value的hashCode()做异或运算
  • HashMap计算hash对key的hashcode进行了二次hash
  • hashMap求索引时用的是indexFor(int hash,int length)
//JDK7
static int hash(int h) {
        h ^= (h >>> 20) ^ (h >>> 12);
        return h ^ (h >>> 7) ^ (h >>> 4);
    }
int hash = hash(key.hashCode());
int i = indexFor(hash, table.length); 

//JDK8
//Java 8中的散列值优化函数 
static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

通过 h & (table.length -1) 来得到该对象的保存位,而HashMap底层数组的长度总是 2 的 n 次方,这是HashMap在速度上的优化
因为使用的掩码是2的次幂,高于掩码的位组成的哈希集合总是冲突,所以把高位移到低位。
混合原始哈希码的高位和低位,以此来加大低位的随机性。
(数组长度-1)正好相当于一个“低位掩码”

重要对象

  • DEFAULT_INITIAL_CAPACITY :默认的初始容量是16,必须是2的幂
  • MAXIMUM_CAPACITY :最大容量,1<<30
  • table:Entry[]数组类型,每一个Entry本质上是一个单向链表
  • size:HashMap的大小,键值对的数量
  • threshold:HashMap的阈值,threshold=容量*加载因子
  • loadFactor:实际加载因子,默认值为0.75
  • modCount:HashMap被改变的次数,用来实现fail-fast机制
  • 负载因子越大表示散列表的装填程度越高
  • HashMap扩容时,将容量变为原来的2倍
  • 指定为16是从性能考虑。避免重复计算
  • TREEIFY_THRESHOLD :树形和列表的阈值,默认8
  • UNTREEIFY_THRESHOLD :树形转换回链式的阈值,默认6
  • MIN_TREEIFY_CAPACITY :哈希表的最小树形化容量,默认64

JDK1.8中HashMap的数据结构
HashMap是数组+链表+红黑树

在 Java8 中,当链表中的元素超过了 8 个以后,会将链表转换为红黑树,在这些位置进行查找的时候可以降低时间复杂度为 O(logN)。

static class Node<K,V> implements Map.Entry<K,V> {
        final int hash;
        final K key;
        V value;
        Node<K,V> next;
}
public final int hashCode() {
            return Objects.hashCode(key) ^ Objects.hashCode(value);
        }

根据哈希表中元素个数确定是扩容还是树形化

桶的树形化treeifyBin()

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

Fail-Fast机制
在迭代过程中,判断modCount跟expectedModCount是否相等,如果不相等就表示已经有其他线程修改了Map

if (modCount != expectedModCount) {
            throw new ConcurrentModificationException();
        }

put过程分析

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) {
        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 {
            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) {
                    // 插入到链表的最后面(Java7 是插入到链表的最前面)
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
                    // 如果在该链表中找到了"相等"的 key(== 或 equals)
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            //e!=null 说明存在旧值的key与要插入的key"相等"
            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;
    }

第一次 put 值的时候,触发的 resize()
数组从 null 初始化到默认的 16 或自定义的初始容量

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

(n - 1) & hash找到具体的数组下标,

如果要存放的这个位置刚好没有元素,那么直接初始化 Node 并放置在这个位置

如果此位置有值

  • 首先,判断该位置的第一个数据和我们要插入的数据,key 是不是”相等”,如果是,取出这个节点赋值给e
  • 如果该节点是代表红黑树的节点,调用红黑树的putTreeVal插值方法
  • 否则说明数组该位置上是一个链表,插入到链表的最后面

如果插入后超过8个,会触发 treeifyBin将链表转换为红黑树break
如果在该链表中找到了”相等”的 key(== 或 equals) break

如果e!=null 说明存在旧值的key与要插入的key”相等”
进行 “值覆盖”,然后返回旧值

判断阈值,决定是否扩容


get 过程分析

  • 计算 key 的 hash 值,根据 hash 值找到对应数组下标: hash & (length-1)

  • 判断数组该位置处的元素是否刚好就是我们要找的,如果不是,走第三步

  • 判断该元素类型是否是 TreeNode,如果是,用红黑树的方法取数据,如果不是,走第四步

  • 遍历链表,直到找到相等(==或equals)的 key


遍历方式

  • 遍历HashMap的键值对
    1.根据entrySet()获取HashMap的“键值对”的Set集合
    2.通过Iterator迭代器遍历得到的集合

Hashtable

线程安全

  • 继承于Dictionary
  • 实现了Map、Cloneable、java.io.Serializable接口
  • key、value不可以为null。映射不是有序的。
  • 直接使用key的hashcode对table数组的长度直接进行取模
int hash = key.hashCode();
int index = (hash & 0x7FFFFFFF) % tab.length;
  • 先用hash&0x7FFFFFFF后(转化为正值),再对length取模(防止数据越界)

Hashtable 是通过”拉链法”实现的哈希表
Hashtable 底层就是一个Entry[] 数组结构,数组中的每一项又是一个链表。

重要对象

  • table:Entry[]数组类型,每一个Entry本质上是一个单向链表
  • count:Hashtable的大小,键值对的数量,
  • threshold:Hashtable的阈值,threshold=容量*加载因子
  • loadFactor:实际加载因子,默认值为0.75
  • modCount:Hashtable被改变的次数,用来实现fail-fast机制
  • 负载因子越大表示散列表的装填程度越高
  • 默认容量为11,增加方式为 old*2+1

Fail-Fast机制

遍历
支持Iterator(迭代器)和Enumeration(枚举器)两种方式遍历


 TreeMap

是有序的散列表,它是通过红黑树实现


3.Set接口

AbstractSet

继承于AbstractCollection,实现了Set中的绝大部分函数
HastSet 和 TreeSet 是Set的两个实现类。

  • HashSet依赖于HashMap,它实际上是通过HashMap实现的。HashSet中的元素是无序的.
  • TreeSet依赖于TreeMap,它实际上是通过TreeMap实现的。TreeSet中的元素是有序的。

HashSet

非线程安全

  • 继承于AbstractSet
  • 实现了Set、Cloneable、java.io.Serializable接口
  • 允许使用 null 元素。元素不是有序的。

重要对象

  • map:HashMap对象
  • PRESENT:向map中添加键值对时,键值对的值固定是PRESENT

将存放的对象当做了 HashMap 的健,value 都是相同的 PRESENT 。由于 HashMap 的 key 是不能重复的,所以每当有重复的值写入到 HashSet 时,value 会被覆盖,但 key 不会收到影响,这样就保证了 HashSet 中只能存放不重复的元素。

HashSet通过iterator()返回的迭代器是fail-fast

遍历方式

  • 通过Iterator遍历HashSet
public Iterator<E> iterator() {
          // 实际上返回的是HashMap的“key集合的迭代器”
          return map.keySet().iterator();
      }

Iterator和Enumeration区别

  • 函数接口不同
    Enumeration只有2个函数接口。只能读取集合的数据,不能修改。
    Iterator只有3个函数接口。Iterator能读取也能数据进行删除操作。
  • Iterator支持fail-fast机制,而Enumeration不支持
  • Enumeration使用到它的函数包括Vector、Hashtable等类
  • Iterator是为了HashMap、ArrayList等集合提供遍历接口
  • Enumeration本身并没有支持同步,而在Vector、Hashtable实现Enumeration时,添加了同步。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值