Java 集合

java集合

fail-fast机制

在java集合类中,使用modCount来检查数组的状态.
当在迭代集合的时候,(通常会实现 iterator()方法来获取迭代对象,或者 foreach),
集合里面的数据,在其他地方被修改了,这个时候 modCount就会被修改,与迭代过程的modCount不一致.将抛出ConcurrentModificationException异常,这个过程就叫 fail-fast

List 篇

主要介绍 ArrayList,VectorLinkedList,CopyOnWriteArrayList


ArrayList

是一个数组长度可自增长的线程不安全的列表.内部使用数组实现. Object[] elementData.

  1. 默认的长度为 10.
  2. fail-fast
  3. 自动扩容(每次扩容为原来的1.5倍)
  4. 线程不安全
  5. 使用连续控件存储
  6. 继承自 List,RandomAccess等.
  7. 允许添加 null

时间复杂度 :

根据索引查询,复杂度为 O(1).
根据元素查询,复杂度为 O(N).
插入和删除,如果在列首复杂度为 O(N);在列尾复杂度为 O(1);平均复杂为 O(N)

插入和删除非列尾数据, 将会导致数组移动数据.

RandomAccess 接口

RandomAccess 是一个标记接口(没有任何方法).
如果实现了 RandomAccess 接口,表示该集合可以进行随机快速访问(通常底层是 数组形式的集合),如 ArrayList.
可以快速访问的集合,使用

for (int i=0, n<size; i++)
    list.get(i);

未实现 RandomAccess接口的集合,如LinkedList,使用迭代器效率更高.

for (Iterator i=list.iterator(); i.hasNext(); )
    i.next();

使用 instanceof RandomAccess来判断是否 支持随机快速访问.

Vector

是一个数组长度可自增长的线程安全的列表,内部使用数组实现.

  1. 默认的长度为 10.
  2. fail-fast
  3. 自动扩容(可设置每次扩容的增量值,默认扩容为原来的2倍.)
  4. 线程安全(synchronized)
  5. 使用连续控件存储
  6. 继承自 RandomAccess,List
  7. 允许添加 null

线程安全的vector,照样可以触发 fail-fast.
时间复杂度与 arraylist 一样.

在不涉及线程安全的前提下,arraylist效率更高.

fun main(args: Array<String>) {
    val vector = Vector(listOf(1, 2, 3, 4, 5))

    singleThread(vector)
    multiThread(vector)
    multiThreadSynchronized(vector)
}

/**
 * 单线程下,迭代抛出 ConcurrentModificationException
 */
fun singleThread(vector: Vector<Int>) {
    val it = vector.iterator()
    while (it.hasNext()) {
        println(it.next())
        vector.add(1)
    }
}

/**
 * 多线程下,迭代抛出 ConcurrentModificationException
 */
fun multiThread(vector: Vector<Int>) {
    thread {
        repeat(10000) {
            vector.add(it)
        }
    }

    thread {
        vector.forEach { println(it) }
    }
}

/**
 * 多线程下,修改vector是添加同步锁,避免同时迭代同时修改
 */
fun multiThreadSynchronized(vector: Vector<Int>) {
    thread {
        synchronized(vector) {
            repeat(10000) {
                vector.add(it)
            }
        }
    }

    thread {
        vector.forEach { println(it) }
    }
}

Stack

继承自 vector,线程安全,栈结构,先进后出.

LinkedList

是以双向链表方式实现的非线程安全的集合.

  1. 一个元素节点包含 上一个节点和下一个节点的引用,以及自身的值.
  2. 内存空间可不连续
  3. 线程不安全
  4. fail-fast
  5. 继承自 List,Deque等.
  6. 允许添加 null

时间复杂度:

删除和插入元素,复杂度为 O(1).
查询时,由于采用双向链表,判断离哪头近,从哪头开始找.
最糟糕的情况是O(N/2),所以它的复杂度为 O(N).

CopyOnWriteArrayList

CopyOnWriteArrayList是并发包中的实现.使用 ReenterLockSystem.arraycopy来实现数组读写的线程安全.

在读取的时不加锁,所以可以支持多线程同时读取数据,并且同时读取数不受限制.

在写入(add,set,remove等操作)的时候,使用 ReenterLock锁住方法,然后操作数据时先将原数组拷贝一份,对拷贝数据进行操作,操作完后将拷贝的原来的数据引用指向拷贝的数组引用,实现数据的更新操作,更新完后释放锁. 和其名字操作一致(写时拷贝).

没有设置初始长度的方法和扩容的策略. 因为该数组在每次 添加数据是, 都会使用 System.arraycopy拷贝一份比当前数据 +1 的数组.

    public boolean add(E e) {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            Object[] elements = getArray();
            int len = elements.length;
            Object[] newElements = Arrays.copyOf(elements, len + 1);
            newElements[len] = e;
            setArray(newElements);
            return true;
        } finally {
            lock.unlock();
        }
    }

ArrayList,Vector和LinkedList的区别

ArrayListVector内部都是使用数组实现的.它们的主要区别在于

  1. ArrayList 是线程不安全的,Vector是线程安全的.
  2. ArrayList 自动扩容时固定增长为原来的1.5倍,不可指定增量值. Vector可指定每次的增量值,如果没指定,则扩容为原来的2倍.

在不考虑线程安全的情况下,优先使用 ArrayList,如果在使用前,能够预估大概有元素,创建时指定个数,效果更好.

LinkedList 使用双向链表实现,也是线程不安全的.与ArrayList的区别在于

  1. 实现方式不同.ArrayList使用数组,Vector使用双向链表.
  2. 内存空间管理不同,LinkedList可以是不连续的空间,ArrayList需要连续的内存空间.
  3. LinkedList 在删除和插入上的性能较好, ArrayList在查询上的效率更好.
  4. ArrayList 一个item只存自己的值,比较省空间, LinkedList一个item除了自己的值,还需要存放上一个和下一个元素的引用,比较费空间.

如果主要需要对列表进行遍历,以及增删操作,使用LinkedList其他情况使用ArrayList.

如果需要用到线程安全的列表,请使用 CopyOnWriteArrayList

Map篇

Map 是以键值对形式存储的集合.

主要介绍 HashMap,TreeMapHashtable,ConcurrentHashMap


HashMap

HashMap 内部混合使用数组,链表,红黑树的结构来构建的键值对集合.

HashMap的本质基础是基于数组的. 数组的各个元素,使用单向链表的结构.(Node(int hash, K key, V value, Node<K,V> next)).

HashMap通过哈希函数(hash())来完成keyindex的映射.

当出现hash后index冲突(index位置已经存在不同的key的键值对)时,数据将在index位置,使用单向链表或红黑树(当链表长度大于等8个时,链表中的所有元素改用红黑树存放)来存放元素.

当使用掉的索引 占用了 数组长度的一定比例时(填装因子),HashMap将进行扩容(扩容为原来的两倍,key与index重新映射).

HashMap的性能瓶颈:

  1. 优良的 哈希函数.

理想的状态是,每一个键值对的存放,能够比较均匀适当的分布在数组的索引上,尽量避免过多的hash值冲突.

    // java 8 中的实现
    static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }
// index = (n - 1) & hash , 2的次方来说就是取余操作.
// 由于 (n -1)的二进制 高位都是0,地位都是1.
// hashcode 与 它进行 & 操作的话, hashcode的高位变化低位不变,则计算出来的index值讲不变.
// 为了消除高位变化 不影响 index 的值, 于是将 hashcode 的 高16位 移动到低16位,再和 hashcode进行 ^ 操作.
// 这样高位的变化,也能影响index值, 降低了冲突的概率.
  1. 填装因子

过低的填装因子,如填装因子=0.5,容量剩余一半的时候就扩容,可能导致空间上的浪费.
过高的填装因子,可能导致为了命中未使用的索引,而频繁出现 冲突.
HashMap实现中,填装因子默认使用 0.75的值.

特点 :

  1. 默认的长度为 16,默认填装因子为 0.75.
  2. fail-fast
  3. 自动扩容(默认扩容为原来的2倍.)
  4. 线程不安全
  5. 混合使用数组,链表和红黑树
  6. 继承自Map
  7. keyvalue都可以添加 null
  8. 插入和遍历顺序不一致

时间复杂度 :

查询,插入和删除操作的平均时间复杂度为 O(1).
极端情况下(全部发生冲突),若长度小于8,使用单项链表,复杂度为 O(n),如果长度大于等于8,使用红黑树,复杂度为O(logn)

LinkedHashMap

继承自 HashMap, 底层使用哈希表与双向链表来保存元素,与HashMap的差异就在于 访问顺序上.
构造中,accessOrder 默认为 false,代表按照插入顺序进行迭代;为 true,代表以访问顺序进行迭代(最常访问的在前,最不经常访问的在后LRU)。

TreeMap

TreeMap内部使用 红黑树来实现的键值对集合.与HashMap相比,TreeMap是一个能比较元素大小的Map集合,会对传入的key进行了大小排序。其中,可以使用元素的自然顺序,也可以使用集合中自定义的比较器来进行排序.

特点 :

  1. fail-fast
  2. 线程不安全
  3. 使用红黑树实现
  4. keyvalue都可以添加 null
  5. 插入和遍历顺序不一致
  6. 支持对key排序
  7. 继承自NavigableMap,拥有强大的搜索功能.

时间复杂度 :

红黑树是自平衡的二叉搜索树, 查询,添加删除 效率都是 O(logn).

HashTable

HashTable 内部使用数组和单项链表实现的键值对集合.

HashTable的哈希函数 (hash & 0x7FFFFFFF) % tab.length,只是对key的hashcod进行取整和取余操作.

HashTable默认的初始大小为11,之后每次扩充为原来的2n+1。
也就是说,HashTable的链表数组的默认大小是一个素数、奇数。之后的每次扩充结果也都是奇数。
由于HashTable会尽量使用素数、奇数作为容量的大小。当哈希表的大小为素数时,简单的取模哈希的结果会更加均匀。(这个是可以证明出来 的,可参考:http://zhaox.github.io/algorithm/2015/06/29/hash

特点 :

  1. 默认的长度为 11,默认填装因子为 0.75.
  2. fail-fast
  3. 自动扩容(扩容为原来的2n+1)
  4. 线程安全
  5. 混合使用数组,链表
  6. 继承自Map
  7. keyvalue都不能使用null值,否则抛出空指针异常.
  8. 插入和遍历顺序不一致

时间复杂度 :

HashMap一致

ConcurrentHashMap (java.util.concurrent)

HashMap一样,是内部使用数组,链表,红黑树的结构来构建的键值对集合,因为需要保证线程安全,所以内部实现更加复杂.

哈希函数 (h ^ (h >>> 16)) & HASH_BITS , 和HashMap类似,与高位进行异或操作的方式,来消除高位数据变化导致索引不变的情况,多加了一步正数的操作.

当冲突的链表中个数超过8个, 如果数组长度小于64,则扩容,否则,将链表数据转化为 红黑树结构存储.

ConcurrentHashMap 采用 CAS(Compare and Swap)原子类型操作,来保证线程的安全性. 较 jdk1.7 的分段锁机制性能更好.

特点 :

  1. 默认长度为16,无默认的装填因子

构造函数为 : ConcurrentHashMap(int initialCapacity, float loadFactor, int concurrencyLevel)
没有默认的装填因子,传入 初始容量和装填因子后,会被算法调整为 2次方,最小容量为 16.
并发级别,表示 可以同时对数据进行写操作的线程数, 最大可达16. 读取不受限制.

  1. 没有 fail-fast, 将保证线程安全.
  2. 自动扩容为原来的 2 倍.
  3. 线程安全
  4. 混合使用数组,链表和红黑树
  5. 继承自ConcurrentMap
  6. keyvalue可以添加 null
  7. 插入和遍历顺序不一致

时间复杂度 :

HashMap一致

HashMapHashTable , ConcurrentHashMap对比

HashMap 线程不安全 , HashTable线程安全,但还是存在fail-fast问题 , ConcurrentHashMap线程安全,不存在fail-fast问题.

HashMapConcurrentHashMap 内部都是用 数组,链表 加红黑树来构建.(较高效) HashTable 使用数组加链表.

HashMapConcurrentHashMap 都是用 高位异或的哈希算法. HashTable 采用 数组长度为素数,直接取余的哈希算法.

HashMap 允许插入 null键值对, HashTable , ConcurrentHashMap 不允许.

结论 : 不考虑线程安全的情况下使用 HashMap, 线程安全则使用 ConcurrentHashMap.
如果知道 大概会存多少数据,指定 初始容量 会更高效, 避免扩容和重新hash映射产生的效率问题.

Set篇

Set 是没有重复元素的单元素集合.

TreeSet 内部使用 TreeMap实现.

HashSet 内部使用 HashMap实现. 当构造参数为三个时 LinkedHashMap实现.

LinkedHashSet 继承自 HashSet,但是都是调用 HashSet中有三个构造参数的LinkedHashMap来实现.

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 218,546评论 6 507
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,224评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,911评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,737评论 1 294
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,753评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,598评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,338评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,249评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,696评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,888评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,013评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,731评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,348评论 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,929评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,048评论 1 270
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,203评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,960评论 2 355

推荐阅读更多精彩内容

  • ArrayList实现原理要点概括 参考文献:http://zhangshixi.iteye.com/blog/6...
    晨光光阅读 1,068评论 0 1
  • 原文地址 Java集合 Java集合框架:是一种工具类,就像是一个容器可以存储任意数量的具有共同属性的对象。 Ja...
    gyl_coder阅读 978评论 0 8
  • 从最基础的数据结构 数组|链表|树 开始,基于这些基础数据结构通过各种设计组合成具备特定功能的数据结构,这些结构是...
    轩居晨风阅读 1,213评论 2 31
  • 一、ArrayList和Vector的区别 共同点: 都实现了List接口继承了AbstractList接口 底层...
    HikariCP阅读 631评论 0 0
  • 今天大哥家办升学宴,我都没早去,为了听林甸的杨志主任讲的课,一个带着有学问的帅哥上场,文质彬彬给大家鞠躬,然后面带...
    波澜起伏阅读 99评论 0 0