文章目录
集合框架图
集合和数组的区别:
- 数组是固定长度的;集合可变长度的。
- 数组可以存储基本数据类型,也可以存储引用数据类型;集合只能存储引用数据类型。
- 数组存储的元素必须是同一个数据类型;集合存储的对象可以是不同数据类型。
常用接口介绍以及区别
- List(有序、可重复)
List里存放的对象是有序的,同时也是可以重复的,List关注的是索引,拥有一系列和索引相关的方法,查询速度快。因为往list集合里插入或删除数据时,会伴随着后面数据的移动,所有插入删除数据速度慢。 - Set(无序、不能重复)
Set里存放的对象是无序,不能重复的,集合中的对象不按特定的方式排序,只是简单地把对象加入集合中。 - Map(键值对、键唯一、值不唯一)
Map集合中存储的是键值对,键不能重复,值可以重复。根据键得到值,对map集合遍历时先得到键的set集合,对set集合进行遍历,得到相应的值。 - 一些其它的接口有Queue、Dequeue、SortedSet、SortedMap和ListIterator。
常用接口类介绍
ArrayList
List接口实现类。ArrayList底层是由数组实现的,随机访问速度极快。
- Array可以包含基本数据类型和对象类型,ArrayList只能包含对象类型;
- Array的大小是固定的,ArrayList大小是动态改变的;
- ArrayList提供了许多方法,比如addAll(),removeAll(),iterator()等;
对于基本数据类型,集合使用自动装箱减少代码量,但是如果处理固定大小的基本数据类型时,相对比较慢。
ArrayList扩容机制,使用copyOf浅拷贝进行创建一个新的数组。
优点:数组长度可动态改变
缺点:不适合在中间频繁进行插入和删除操作。因为每次插入和删除都需要移动数组中的元素。
- ArrayList没有指定初始化长度时,默认长度为10
- ArrayList在增加元素的时候超过了原始容量,会采用扩容ensureCapacity方案:原始容量*3/2+1(1.5倍扩容)
- ArrayList是线程不安全的,在多线程的情况下不要使用。
- Vector是线程安全的,被同步关键字修饰。扩容方案是:原来的2倍。
- Vector是ArrayList的多线程的一个替代品
注意: ArrayList会在并发时出现数组越界
问题:ArrayList 内部用什么实现的?
ArrayList 内部是用 Object[]实现的。是线性表(数组)
分析 ArrayList 的构造、add、remove、clear 方法的实现原理得:
- get() 直接读取第几个下标,复杂度 O(1)
- add(E) 添加元素,直接在后面添加,复杂度O(1)
- add(index, E) 添加元素,在第几个元素后面插入,后面的元素需要向后移动,复杂度O(n)
- remove()删除元素,后面的元素需要逐个移动,复杂度O(n)
LinkedList
双向循环链表的链式表,存储结构使用链式表
LinkedList 是链表的操作
- get() 获取第几个元素,依次遍历,复杂度O(n)
- add(E) 添加到末尾,复杂度O(1)
- add(index, E) 添加第几个元素后,需要先查找到第几个元素,直接指针指向操作,复杂度O(n)
- remove()删除元素,直接指针指向操作,复杂度O(1)
特点:适合于在链表中间需要频繁的插入和删除操作。
缺点:随机访问速度较慢,查找一个元素需要从头开始一个一个的找。也不是线程安全的
问题:LinkedList和ArrayList、vector的区别
- LinkedList比ArrayList更占内存,因为LinkedList为每一个节点存储了两个引用,一个指向前一个元素,一个指向下一个元素。
- ArrayList 底层结构是数组,底层查询快,增删慢。
- LinkedList 底层结构是链表型的,增删快,查询慢。
- vector 底层结构是数组 线程安全的,增删慢,查询慢。
HashMap
HashMap可以接受null键值和值,而Hashtable则不能;HashMap是非synchronized;HashMap很快;以及HashMap储存的是键值对
HashMap的工作原理
基于hasing的原理,使用put(key,value)存储对象,使用get(key)获取对象,调用put()方法传递键和值的时候,先对键使用hashCode()方法计算hashCode,返回的hashCode用于找到bucket位置来储存Entry对象。当get()获取对象时,通过键对象的equals()方法找到正确的键值对,然后返回值对象。HashMap使用链表来解决碰撞问题,当发生碰撞了,对象将会储存在链表的下一个节点中。 HashMap在每个链表节点中储存键值对对象。
重要特性为容量、负载因子、扩容极限。
问题:HashMap是线程不安全的,其主要体现:
在jdk1.7中,在多线程环境下,扩容时会造成环形链或数据丢失。
在jdk1.8中,在多线程环境下,会发生数据覆盖的情况。
如果没有hash碰撞则会直接插入元素。如果线程A和线程B同时进行put操作,刚好这两条不同的数据hash值一样,并且该位置数据为null,所以这线程A、B都会进入
线程A进入后还未进行数据插入时挂起,而线程B正常执行,从而正常插入数据,然后线程A获取CPU时间片,此时线程A不用再进行hash判断了,问题出现:线程A会把线程B插入的数据给覆盖,发生线程不安全。
问题:如果两个对象hashCode相同会发生什么?
hashCode相同,bucket位置相同,发生碰撞。HashMap使用链表存储对象,Entry会存储在链表中。
问题:如果两个键的hashCode相同怎么获取值对象?
调用get()方法时,获取hashCode方法找到bucket的位置,然后调用equals()方法找到链表中正确的节点。
问题:如果HashMap大小超过负载因子定义的容量怎么办?
会进行rehasing。
默认负载因子为0.75也就是说当一个map填满了75%的bucket的时候,将大小扩大原来的两倍,重新调整map的大小,将原来的对象放入新的bucket上。
问题:重新调整HashMap大小存在什么问题
当重新调整HashMap大小的时候,存在条件竞争,因为如果两个线程都发现HashMap需要重新调整大小了,它们会同时试着调整大小。在调整大小的过程中,存储在链表中的元素的次序会反过来,因为移动到新的bucket位置的时候,HashMap并不会将元素放在链表的尾部,而是放在头部,这是为了避免尾部遍历(tail traversing)。如果条件竞争发生了,那么就死循环了。
ConcurrentHashMap
详情请看之前文章ConcurrentHashMap的原理分析的原理分析 或者 关注公众号 查看详情
TreeMap
实现SortMap接口,能够把它保存的记录根据键排序,默认是按键值的升序排序,也可以指定排序的比较器,当用Iterator 遍历TreeMap时,得到的记录是排过序的。
LinkedHashMap
是HashMap的一个子类,保存了记录的插入顺序,在用Iterator遍历LinkedHashMap时,先得到的记录肯定是先插入的.也可以在构造时用带参数,按照应用次数排序。在遍历的时候会比HashMap慢,不过有种情况例外,当HashMap容量很大,实际数据较少时,遍历起来可能会比 LinkedHashMap慢,因为LinkedHashMap的遍历速度只和实际数据有关,和容量无关,而HashMap的遍历速度和他的容量有关。
HashMap、HashTable、ConcurrentHashMap、TreeMap的区别
HashMap
1、底层数组+链表实现
2、key与value可以为null
3、线程不安全
4、初始size为16,扩容方式:newSize = oldSize * 2 ;size是2的n次幂
Hashtable
1、底层数组+链表实现
2、key与value 不能为null
3、线程安全:实现线程安全的方式是修改数据时锁住整个HashTable,因此也导致了 Hashtable在写入时会比较慢。
4、初始size为11,扩容方式:newSize = oldSize * 2 + 1
5、扩容针对整个Map,每次扩容时,原来数组中的元素依次重新计算存放位置,并重新插入
6、插入元素后判断是否扩容,如果扩容后没有插入新的元素,判定为无效扩容
7、当Map中元素总数超过Entry数组的75%,触发扩容操作,为了减少链表长度,元素分配更均匀
TreeMap
TreeMap实现SortMap接口,能够把它保存的记录根据键排序,默认是按键值的升序排序(自然顺序),也可以指定排序的比较器,当用Iterator遍历TreeMap时,得到的记录是排过序的。不允许key值为空,非同步的;
ConcurrentHashMap(jdk1.7)
- 底层采用分段的数组+链表实现,线程**安全.**使用了锁分段技术来保证线程安全的
- 通过把整个Map分为N个Segment,可以提供相同的线程安全,但是效率提升N倍,默认提升16倍。(读操作不加锁,由于HashEntry的value变量是 volatile的,也能保证读取到最新的值。)
- Hashtable的synchronized是针对整张Hash表的,即每次锁住整张表让线程独占,ConcurrentHashMap允许多个修改操作并发进行,其关键在于使用了锁分离技术
- 有些方法需要跨段,比如size()和containsValue(),它们可能需要锁定整个表而而不仅仅是某个段,这需要按顺序锁定所有段,操作完毕后,又按顺序释放所有段的锁
- 扩容:段内扩容(段内元素超过该段对应Entry数组长度的75%触发扩容,不会对整个Map进行扩容),插入前检测需不需要扩容,有效避免无效扩容
锁分段技术:首先将数据分成一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问。
ConcurrentHashMap 是线程安全的 HashMap 的实现
put 操作:并没有在此方法上加上 synchronized,首先对 key.hashcode 进行 hash 操作,得到 key 的 hash 值。 hash操作的算法和 map也不同,根据此 hash 值计算并获取其对应的数组中的 Segment对象(继承自ReentrantLock), 接着调用此 Segment 对象的 put 方法来完成当前操作。ConcurrentHashMap 基于 concurrencyLevel 划分出了多个 Segment 来对 key-value 进行存储,从而避免每 次 put 操作都得锁住整个数组。在默认的情况下,最佳情况下可允许 16 个线程并发无阻塞的操作集合对象,
get(key)首先对 key.hashCode 进行 hash 操作,基于其值找到对应的 Segment 对象,调用其 get 方法完成当前操 作。而 Segment 的 get 操作首先通过 hash 值和对象数组大小减 1 的值进行按位与操作来获取数组上对应位置的 HashEntry。
HashSet
HashSet:底层数据结构是哈希表。
特点:
- 不能保证元素的排列顺序。
- 非线程安全
- 集合元素可以使null
哈希表的原理:
- 对对象元素中的关键字(对象中的特有数据),进行哈希算法的运算,并得出一个具体的算法值,这个值 称为哈希值。
- 哈希值就是这个元素的位置。
- 如果哈希值出现冲突,再次判断这个关键字对应的对象是否相同。如果对象相同,就不存储,因为元素重复。如果对象不同,就存储,在原来对象的哈希值基础 +1顺延。
- 存储哈希值的结构,我们称为哈希表。
- 既然哈希表是根据哈希值存储的,为了提高效率,最好保证对象的关键字是唯一的。
这样可以尽量少的判断关键字对应的对象是否相同,提高了哈希表的操作效率。
问题:如何保证元素的唯一性:
通过hashCode和equals两个方法进行确定元素的唯一性,如果两个元素的hashCode值一样,调用equals方法进行判断值是否相等。如果hashCode值不一样,则不会调用equals方法。
hashCode() 方法:
- HashSet 集合判断两个元素相等的标准:两个对象通过 equals() 方法比较相等,并且两个对象的 hashCode () 方法返回值也相等。
- 如果两个对象通过 equals() 方法返回 true ,这两个对象的 hashCode 值也应该相同。
- 重写 hashCode () 方法的基本原则
1、 在程序运行时,同一个对象多次调用 hashCode () 方法应该返回相同的值
2、当两个对象的 equals() 方法比较返回 true 时,这两个对象的 hashCode () 方法的返回值也应相等
3、对象中用作 equals() 方法比较的 Field ,都应该用来计算 hashCode 值
TreeSet
对Set集合中的元素的进行指定顺序的排序,不同步。TreeSet底层的数据结构就是二叉树。
保证元素唯一性
根据compareTo的return返回值。后存入的元素调用compareTo方法并传入前面的元素进行比较,如果compareTo方法return返回正数就表示比前面元素大,就存到了二叉树的右边,取出时是后取出该元素的。如果return返回0,则两个元素一样,则不会存入。如果return返回负数表示比前面元素小,就存在二叉树的左边,取出时会先取出该元素。
排序方式
- TreeSet排序的第一种方式:让元素自身具备比较性。元素需要实现Comparable接口,重写compareTo方法。这种方式也称为元素的自然顺序,也叫元素的默认顺序。
- TreeSet集合第二种排序方式:当元素自身不具备比较性或者具备的比较性不是所需要的,这时需要让集合自身具备比较性。让集合一初始化就有了比较方式。定义比较器类,实现Comparator接口,重写compare()方法。
set集合转List集合
List< String> list= new ArrayList<>(HashSet);
其他问题
问题:fail-fast与fail-safe有什么区别?
Iterator的fail-fast属性与当前的集合共同起作用,因此它不会受到集合中任何改动的影响。Java.util包中的所有集合类都被设计为fail-fast的,而java.util.concurrent中的集合类都为fail-safe的。Fail-fast迭代器抛出ConcurrentModificationException,而fail-safe迭代器从不抛出ConcurrentModificationException。
问题:哪些集合类是线程安全的?
Vector、HashTable、Properties和Stack是同步类,所以它们是线程安全的,可以在多线程环境下使用。Java1.5并发API包括一些集合类,允许迭代时修改,因为它们都工作在集合的克隆上,所以它们在多线程环境中是安全的。
问题:并发集合类是什么?
Java1.5并发包(java.util.concurrent)包含线程安全集合类,允许在迭代时修改集合。迭代器被设计为fail-fast的,会抛出ConcurrentModificationException。一部分类为:CopyOnWriteArrayList、 ConcurrentHashMap、CopyOnWriteArraySet。
问题:队列和栈是什么,列出它们的区别?
栈和队列两者都被用来预存储数据。java.util.Queue是一个接口,它的实现类在Java并发包中。队列允许先进先出(FIFO)检索元素,但并非总是这样。Deque接口允许从两端检索元素。
栈与队列很相似,但它允许对元素进行后进先出(LIFO)进行检索。
Stack是一个扩展自Vector的类,而Queue是一个接口。
更多文章和干货请移驾公众号和个人网站
本文作者:Java技术债务
原文链接:https://www.cuizb.top/myblog/article/1642431648
版权声明: 本博客所有文章除特别声明外,均采用 CC BY 3.0 CN协议进行许可。转载请署名作者且注明文章出处。