java集合类的总结
1.数组、链表和哈希表的区别
数组顺序存储,查找快,插入和删除慢。
链表每个元素都用指针相连,查找慢,插入和删除快。
哈希表是数组合链表的结合体,根据key通过hash函数映射值,查找速度快。
set集合怎么保证元素唯一的呢?
通过hashcode方法和equals方法,添加元素的时候,先判断两个元素的hashcode方法,如果不一致则直接添加,无需在比较equals方法;如果hashcode值一致呢则开始比较equals方法。
如果比较是对象,则需要重写hashcode和equals方法。
2.set以及实现类
set是一个无序且不可重复的数据结构。其实现类有hashSet、treeSet和LinkedHashSet。
2.1.hashSet是由hash table(哈希表)组成的。其中的元素是无序的,内部使用hashmap存储数据。HashSet的add、remove、contains方法 的时间复杂度为常量O(1)。线程不安全,运行速度快;并且允许存储null。
2.2.treeSet是由treeMap构造的,而treeMap是由红黑树(自平衡的排序二叉树)构造的。因此是一个有序的set,如果插入的是自定义对象 需要让类实现 Comparable 接口并且必须要重写compareTo。因为对象里可能定义了多个变量,所以需要自定义比较器来实现这个顺序。线程不安全。
2.3.LinkedHashSet是一个哈希表和链表的结合,且是一个双向链表。和hashSet唯一的区别是LinkedHashSet内部使用的是LinkHashMap。这样做的意义或者好处就是LinkedHashSet中的元素顺序是可以保证的,也就是说遍历序和插入序是一致的。线程不安全。
3.list以及实现类
list是一个有序且可以重复的数据结构。
3.1. ArrayList
ArrayList底层依赖数组来实现,查询效率较高,增删效率较低
ArrayList中的元素有序、可重复、允许null值
ArrayList会进行扩容检测。当超过数组长度的时候,会自动进行扩容1.5倍。初始化时尽量指定初始容量,可避免频繁扩容,影响程序执行效率
线程不安全,适用于单线程环境。
3.2. Vector
Vector底层依赖数组来实现,查询效率较高,增删效率较低
Vector中的元素有序、可重复、允许null值,添加单个元素的话,只能添加到集合末尾
Vector会自动进行扩容。扩容后的容量由增量来决定,(2倍 or 原容量+增量)
大多数方法用关键字synchronized修饰,线程安全,因此效率很低,应用场景很少。
3.3. LinkedList
LinkedList底层依赖双向循环链表实现,增删效率较高,查询效率较低
LinkedList中的元素有序、可重复、允许null值
线程不安全,适用于单线程环境。
4.map以及实现类的总结
map是一种存储key-value的结构。
1、HashMap
底层由数组合链表组成,而JDK1.8中,HashMap采用位桶+链表+红黑树实现,当链表长度超过阈值(8)时,将链表转换为红黑树,这样大大减少了查找时间。线程不安全。当hashmap的容量超过初始容量的0.75时,将链表数组扩大2倍,把原链表数组的搬移到新的数组中,是为扩容。键值对可以设置为null。
put过程:
先通过hashcode方法,找到数组的位置,如果么有值直接添加,否则使用equals方法,有相等的直接覆盖,否则添加(头插法)。
①.判断键值对数组table[i]是否为空或为null,否则执行resize()进行扩容;
②.根据键值key计算hash值得到插入的数组索引i,如果table[i]==null,直接新建节点添加,转向⑥,如果table[i]不为空,转向③;
③.判断table[i]的首个元素是否和key一样,如果相同直接覆盖value,否则转向④,这里的相同指的是hashCode以及equals;
④.判断table[i] 是否为treeNode,即table[i] 是否是红黑树,如果是红黑树,则直接在树中插入键值对,否则转向⑤;
⑤.遍历table[i],判断链表长度是否大于8,大于8的话把链表转换为红黑树,在红黑树中执行插入操作,否则进行链表的插入操作;遍历过程中若发现key已经存在直接覆盖value即可;
⑥.插入成功后,判断实际存在的键值对数量size是否超多了最大容量threshold,如果超过,进行扩容。
get过程:
先通过hashcode找到数组位置,在equals遍历。
通过在put方法上加锁可以使其为线程安全,synchronized里面放的应该是一个static的,volitile的对象。因为锁的是每一次插入的动作,必须多线程共享。
为什么会线程不安全,第一:是因为当两个线程同时对这个hashmap进行扩容(会新建数组,将原先的元素全部复制到新的数据结构里去),或出现循环的结果导致预想不到的结果,链表会形成闭环。
第二:当两个线程同时进行put操作,然后在同一个位置出现指针碰撞,都在链表的头部插入,会丢失一个数据。
2、hashtable
hashTable是线程安全的一个map实现类,它实现线程安全的方法是在各个方法上添加了synchronize关键字。但是现在已经不再推荐使用HashTable了,因为现在有了ConcurrentHashMap这个专门用于多线程场景下的map实现类,其大大优化了多线程下的性能。不可设置键值对为null。
3、ConcurrentHashMap
基本结构和hashmap一样。无序。
主要实现原理是segment段锁,它不再使用和HashTable一样的synchronize一样的关键字对整个方法进行枷锁,而是转而利用segment段落锁来对其进行加锁,以保证Map的多线程安全。各segment之间互不干扰,并发进行啊。每个segment都是一个hashmap。HashEntry 中的 value 变量 和 指向下一个节点的引用变量 next 都是使用 volatile 关键字进行修饰的,以此保证有序性和可见性。get() 方法不加锁,先找 segemnt 再找 桶put() 方法要加锁,插入之前先判断 count 与阈值的关系,有需要的话则进行扩容处理(桶的容量扩容两倍)。put、remove、replace、clear 操作都要加锁。size() 先尝试两次不加锁的统计各个 segment 中的 count,如果结果不一致,则采用对全部 segment 加锁的方式统计。
Java1.8时,采用了cas来实现。
算法思路:V是共享变量,我们拿着自己准备的这个E,去跟V去比较,如果E == V ,说明当前没有其它线程在操作,所以,我们把N 这个值 写入对象的 V 变量中。如果 E != V ,说明我们准备的这个E,已经过时了,所以我们要重新准备一个最新的E ,去跟V 比较,比较成功后才能更新 V的值为N。
使用 synchronized 和 volatile 关键字来保证线程安全
取消 segment ,当桶中 链表 长度大于 8 时,将链表转换为 RBTree
get()方法不加锁
put()方法:
如果桶是空的,则初始化桶,并采用 CAS 操作将元素放在链表的首位
如果桶非空,则通过使用 synchronized 锁住 链表的第一个节点(以此来锁住整个桶),再使用 Unsafe 类的 CAS 操作将元素放在链表的末尾
4、TreeMap
红黑树实现,默认按字典排序。
5、LinkedHashMap
有序,一个哈希表和双向链表的结合。
6、weakHashMap