List 列表,有序,可重复;
Queue 队列,有序,可重复;
Set 集合,不可重复;
Map 映射,无序,键唯一,值不唯一;
每种集合类型下都包含多个具体的实现类。
List和Set是存储单列数据的集合,Map是存储键值对这样的双列数据的集合;
List中存储的数据是有顺序的,并且值允许重复;Map中存储的数据是无序的,它的键是不允许重复的,但是值是允许重复的;Set中存储的数据是无顺序的,并且不允许重复,但元素在集合中的位置是由元素的hashcode决定,即位置是固定的(Set集合是根据hashcode来进行数据存储的,所以位置是固定的,但是这个位置不是用户可以控制的,所以对于用户来说set中的元素还是无序的)。
Java 集合框架拥有两大接口 Collection 和 Map,其中,Collection 麾下三生子 List、Set 和 Queue。ArrayList 就实现了 List 接口,其实就是一个数组列表,不过作为 Java 的集合框架,它只能存储对象引用类型,也就是说当我们需要装载的数据是诸如 int、float 等基本数据类型的时候,必须把它们转换成对应的包装类。
扩容机制
JDK1.8
底层使用数组实现,默认初始容量为10. 当超出后,会自动扩容为原来的1.5倍,即自动扩容机制。 数组的扩容是新建一个大容量(原始数组大小+扩充容量)的数组,然后将原始数组 数据拷贝到新数组,然后将新数组作为扩容之后的数组。数组扩容的操作代价很高,我们应该尽量减少这种操作。
开放定址法就是解决hash冲突的一种方式。它是使用一种 探测方式在整个数组中找到另一个可以存 储值的地方。
链地址法(拉链法)HashMap,HashSet其实都是采用的拉链法来解决哈希冲突的,就是在每个位桶 实现的时候,我们采用链表(jdk1.8之后采用链表+红黑树)的数据结构来去存取发生哈希冲突的输 入域的关键字
再散列法再散列法其实很简单,就是再使用哈希函数去散列一个输入的时候,输出是同一个位置就 再次散列,直至不发生冲突位置
缺点:每次冲突都要重新散列,计算时间增加
JDK 1.7 中整个扩容过程就是一个取出数组元素(实际数组索引位置上的每个元素是每个独立单向链表的头部,也就是发生 Hash 冲突后最后放入的冲突元素)然后遍历以该元素为头的单向链表元素,依据每个被遍历元素的 hash 值计算其在新数组中的下标然后进行交换(即原来 hash 冲突的单向链表尾部变成了扩容后单向链表的头部)。
在 JDK 1.8 中 HashMap 的扩容操作就显得更加的骚气了, 由于扩容数组的长度是 2 倍关系,所以对于假设初始 tableSize = 4 要扩容到 8 来说就是 0100 到 1000 的变化 (左移一位就是 2 倍),在扩容中只用判断原来的 hash 值 与左移动的一位(newtable 的值)按位与操作是 0 或 1 就 行,0 的话索引就不变,1 的话索引变成原索引加上扩容前数组的长度。
红黑树的这些特性保证了它的平衡性,使得红黑树的查找、插入和删除操作的时间复杂度都是 O(log n),是一种高效的数据结构。
AVL树和红黑树有几点比较和区别:
(1)AVL树是更加严格的平衡,因此可以提供更快的查找速度,一般读取查找密 集型任务,适用AVL树。
(2)红黑树更适合于插入修改密集型任务。
(3)通常,AVL树的旋转比红黑树的旋转更加难以平衡和调试
我们都知道,链表的时间复杂度是O(n),红黑树的时间复杂度 O(logn),很显然,红黑树的复杂度是优于链表的。因为树节点所占空间是普通节点的两倍,所以只 有当节点足够多的时候,才会使用树节点。也就是说,节点少的时候,尽管时间复杂度上,红黑树 比链表好一点,但是红黑树所占空间比较大,综合考虑,认为只能在节点太多的时候,红黑树占空 间大这一劣势不太明显的时候,才会舍弃链表,使用红黑树,这也是为什么不直接全部使用红黑树
的原因。
只有当数组长度为2的幂次方时,h&(length-1)才等价于h%length,即实现了key的定位, 2的幂次方也可以减少冲突次数,提高HashMap的查询效率。
默认加载因子是0.75
负载因子表示一个散列表的空间的使用程度,有这样一个公式: initailCapacity*loadFactor=HashMap的容量。
由加载因子的定义,可以知道它的取值范围是(0, 1]。
String类型作为Key
String 类型的对象对这个条件有着很好的支持,因为 String 对象的 hashCode() 值是根据 String 对象的内容计算的,并不是根据对象的地址计算。下面是 String 类源码中的 hashCode() 方法:String 对象底层是一个 final 修饰的 char 类型的数组,hashCode() 的计算是根据字符数组的每个元素进行计算的,所以内容相同的 String 对象会产生相同的散列码。
HashMap 内部实现是通过 key 的 hashcode 来确定 value 的存储位置
第一个原因:天生复写了hashCode方 法,根据String对象的内容来计算的 hashCode。
第二个原因:为字符串是不可变的,所以当创建字 符串时,它的 hashcode 被缓存下来, 不需要再次计算,所以相比于其他对象更快。
第三个原因:equals方法 string自己就有
1.在JDK1.7中,当并发执行扩容操作时会造成环形链和数据丢失的情况。
在JDK1.7中,扩容数据时要进行把原数据迁移到新的位置,使用的方法transfer重新定位每个桶的下标,并采用头插法将元素迁移到新数组中。头插法会将链表的顺序翻转,这也是形成死循环的关键点。
2.在JDK1.8中,在并发执行put操作时会发生数据覆盖的情况。
**1、继承的父类不同 **
Hashtable继承自Dictionary类,而HashMap继承自AbstractMap类。但二者都实现了Map接口
2、线程安全性不同
javadoc中关于hashmap的一段描述如下:此实现不是同步的。如果多个线程同时访问一个哈希映射, 而其中至少一个线程从结构上修改了该映射,则它必须保持外部同步。 Hashtable 中的方法是Synchronize的,而HashMap中的方法在缺省情况下是非Synchronize的。
在多线程并发的环境下,可以直接使用Hashtable,不需要自己为它的方法实现同步,但使用 HashMap时就必须要自己增加同步处理。(结构上的修改是指添加或删除一个或多个映射关系的任 何操作;仅改变与实例已经包含的键关联的值不是结构上的修改。)这一般通过对自然封装该映射 的对象进行同步操作来完成。如果不存在这样的对象,则应该使用 Collections.synchronizedMap方 法来“包装”该映射。最好在创建时完成这一操作,以防止对映射进行意外的非同步访问
3、是否提供contains方法
HashMap把Hashtable的contains方法去掉了,改成containsValue和containsKey,因为contains方
法容易让人引起误解。 Hashtable则保留了contains,containsValue和containsKey三个方法,其中contains和
containsValue功能相同。
4、key和value是否允许null值
其中key和value都是对象,并且不能包含重复key,但可以包含重复的value。 Hashtable中,key和value都不允许出现null值。但是如果在Hashtable中有类似put(null,null)的操作,
编译同样可以通过,因为key和value都是Object类型,但运行时会抛出NullPointerException异常,这是 JDK的规范规定的。 HashMap中,null可以作为键,这样的键只有一个;可以有一个或多个键所对应的值为null。当get()方法返 回null值时,可能是 HashMap中没有该键,也可能使该键所对应的值为null。因此,在HashMap中不能由 get()方法来判断HashMap中是否存在某个键, 而应该用containsKey()方法来判断。
5、两个遍历方式的内部实现上不同
Hashtable、HashMap都使用了 Iterator。而由于历史原因,Hashtable还使用了Enumeration的方
式 。
**6、hash值不同 **
哈希值的使用不同,HashTable直接使用对象的hashCode。而HashMap重新计算hash值。
7、内部实现使用的数组初始化和扩容方式不同
HashTable在不指定容量的情况下的默认容量为11,而HashMap为16,Hashtable不要求底层数组的容量一定要为2的整数次幂,而HashMap则要求一定为2的整数次幂。 Hashtable扩容时,将容量变为原来的2倍加1,而HashMap扩容时,将容量变为原来的2倍。 Hashtable和HashMap它们两个内部实现方式的数组的初始大小和扩容的方式。HashTable中hash数组默认大小是11,增加的方式是 old*2+1。
LinkedHashMap是Java中的一种Map实现,它通过链表维护了插入顺序或者访问顺序。在插入元素时,LinkedHashMap会将新元素插入到链表的末尾,从而保证了插入顺序;在访问元素时,LinkedHashMap会将被访问的元素移到链表的末尾,从而保证了访问顺序。
LinkedHashMap底层的数据结构是一个哈希表,其中每个节点是一个链表节点。每个链表节点中包含了键、值和前后指针。在Java8之前,LinkedHashMap内部实现使用的是双向链表,Java8中则使用了一种更加高效的红黑树,用于维护键的顺序。这种树的时间复杂度为O(log n),而链表的时间复杂度为O(n)。
LinkedHashMap还有一个重要的属性accessOrder,它用于控制LinkedHashMap是按照插入顺序还是访问顺序维护元素顺序。当accessOrder为true时,LinkedHashMap会按照访问顺序维护元素顺序,也就是说,每次访问一个元素,该元素会被移到链表的末尾;当accessOrder为false时,LinkedHashMap会按照插入顺序维护元素顺序。
在插入、删除和查找元素时,LinkedHashMap会调用哈希表的相关操作。在需要维护插入或访问顺序时,LinkedHashMap还会通过修改链表节点的前后指针来维护链表的顺序。
TreeMap是Java中的一种基于红黑树实现的有序映射表。TreeMap实现了SortedMap接口,可以保证其键值对的顺序是按照键的自然顺序或者比较器顺序进行排序的。
TreeMap的内部实现是通过一颗红黑树来维护其键值对的顺序。红黑树是一种自平衡二叉查找树,其插入、删除和查找操作的时间复杂度都是O(log n)级别的。因此,TreeMap可以快速地查找和插入键值对,并且它的键值对是有序的。
在TreeMap中,键必须是可比较的,因为它们需要被排序。如果键没有实现Comparable接口,那么在创建TreeMap时必须指定一个比较器(Comparator),以便TreeMap可以使用该比较器来对键进行排序。
TreeMap并不是线程安全的,如果需要在多线程环境中使用,可以考虑使用ConcurrentHashMap等线程安全的集合类
HashSet 的实现依赖于 HashMap
HashSet是Java中基于哈希表实现的集合类,它是一个不允许元素重复的集合。HashSet的底层原理主要涉及哈希表、哈希函数和链表。
具体来说,HashSet内部使用了一个哈希表来存储元素。哈希表是一种基于哈希算法实现的数据结构,它能够实现快速的插入、查找和删除操作。HashSet的哈希表是由一个数组和若干条链表组成的,每个数组元素被称为一个桶,每个桶可以存储多个元素。
当向HashSet中添加元素时,首先会根据元素的哈希值确定它所属的桶,然后将元素加入到该桶中。如果该桶中已经存在一个或多个元素,则会使用链表来解决冲突。具体来说,新元素会被加入到该桶对应的链表的末尾,这样就形成了一个单向链表。当需要查找元素时,先根据元素的哈希值找到对应的桶,然后遍历该桶对应的链表,直到找到目标元素或者遍历完整个链表。
为了提高HashSet的性能,Java中提供了两种方法来调整哈希表的大小。当元素个数超过哈希表大小的75%时,就会触发扩容操作,即创建一个新的哈希表,将原有的元素重新分布到新表中。相反,当元素被删除导致元素个数低于哈希表大小的25%时,就会触发收缩操作,即创建一个新的哈希表,将原有的元素重新分布到新表中,并且将原有的哈希表销毁。
总体来说,HashSet的底层原理是通过哈希表、哈希函数和链表来实现的。它能够快速地实现元素的添加、查找和删除,并且不允许元素重复。需要注意的是,为了保证哈希表的性能,哈希函数的设计十分重要,它应该能够均匀地将元素映射到不同的桶中。
HashSet、LinkedHashSet 和 TreeSet 是 Java 集合框架中常用的三种 Set 集合类型,它们的主要区别在于以下几点:
综上所述,HashSet 适合存储大量元素,不需要保证顺序,并且对性能要求较高的场景;LinkedHashSet 适合需要保留元素插入顺序,并且对遍历性能有要求的场景;而 TreeSet 则适合需要保证元素顺序,并且需要支持高效地遍历元素的场景。
fail-fast,即快速失败机制,它是java集合中的一种错误检测机制,当多个线程(当个线程也是可以滴),在结构上对集合进行改变时,就有可能会产生fail-fast机制。
“fail-safe” 迭代器会在迭代期间创建集合的副本,这样即使集合在迭代期间被修改,迭代器也不会抛出 ConcurrentModificationException 异常。这种方式确保了迭代器的安全性,但是也可能导致迭代器和集合的状态不同步。
ArrayDeque是Java集合框架中的一种双端队列实现。它是一个基于数组的动态数据结构,支持在队列两端进行元素的插入和删除操作。
ArrayDeque的实现方式与ArrayList相似,都是通过数组来存储元素。但是与ArrayList不同的是,ArrayDeque支持在队列两端进行操作,并且不需要像ArrayList一样需要进行数组的复制和移动操作。
在ArrayDeque中,队列两端都可以进行元素的添加和删除。在队列头部添加元素可以使用addFirst()方法,在队列尾部添加元素可以使用addLast()方法。在队列头部删除元素可以使用removeFirst()方法,在队列尾部删除元素可以使用removeLast()方法。
ArrayDeque还提供了一些其他的方法,比如getFirst()和getLast()方法可以获取队列的头部和尾部元素,peekFirst()和peekLast()方法可以获取队列头部和尾部元素但不会将其删除。size()方法可以获取队列中元素的数量,isEmpty()方法可以判断队列是否为空。
由于ArrayDeque是基于数组实现的,因此它的访问速度比较快。另外,它也是线程不安全的,因此在多线程环境中需要进行同步处理。
线程安全的集合类:
线程不安全的集合类:
迭代器(Iterator)是Java集合框架中的一个接口,它用于遍历集合中的元素,提供了统一的访问集合元素的方式,不同的集合类可以通过实现Iterator接口来提供不同的遍历方式。
需要注意的是,在使用迭代器遍历集合时,如果在遍历的过程中对集合进行了修改(比如增加、删除元素),可能会导致ConcurrentModificationException异常,因此在修改集合时应该避免使用迭代器遍历。
List<String> list = new ArrayList<>();
list.add("apple");
list.add("banana");
list.add("orange");
// 使用for-each循环遍历集合
for (String str : list) {
System.out.println(str);
}
// 使用while循环和迭代器遍历集合
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
String str = iterator.next();
System.out.println(str);
}
JDK1.7版本的 ReentrantLock+Segment+HashEntry。写 操作的时候可以只对元素所在的Segment 进行加锁即可,不会影响到其他的 Segment,这样,在最理想的情况下, ConcurrentHashMap可以最高同时支持Segment数量大小的写操作。
JDK8中ConcurrentHashMap参考了JDK8 HashMap的实现,采用了数组+链表+红黑树的实现方式来设计,内部大量采用CAS操作,这里我简要介绍下CAS。 CAS是compare and swap的缩写,即我们所说的比较交换。cas是一种基于锁的操作,而且是乐观 锁。在java中锁分为乐观锁和悲观锁。悲观锁是将资源锁住,等一个之前获得锁的线程释放锁之后, 下一个线程才可以访问。而乐观锁采取了一种宽泛的态度,通过某种方式不加锁来处理资源,比 如通过给记录加version来获取数据,性能较悲观锁有很大的提高。 JDK8中彻底放弃了Segment转而采用的是Node,其设计思想也不再是JDK1.7中的分段锁思想。 Node:保存key,value及key的hash值的数据结构。其中value和next都用volatile修饰,保证并发的 可见性。
Java8 ConcurrentHashMap结构基本上和Java8的HashMap一样,不过保证线程安全性.
其实可以看出JDK1.8版本的ConcurrentHashMap的数据结构已经接近HashMap,相对而言, ConcurrentHashMap只是增加了同步的操作来控制并发,从JDK1.7版本的ReentrantLock+Segment+HashEntry,到JDK1.8版本中synchronized+CAS+HashEntry+红黑树。
1.数据结构:取消了Segment分段锁的数据结构,取而代之的是数组+链表+红黑树的结构。 2.保证线程安全机制:JDK1.7采用segment的分段锁机制实现线程安全,其中segment继承自 ReentrantLock。JDK1.8采用CAS+Synchronized保证线程安全。
3.锁的粒度:原来是对需要进行数据操作的Segment加锁,现调整为对每个数组元素加锁 (Node)。
4.链表转化为红黑树:定位结点的hash算法简化会带来弊端,Hash冲突加剧,因此在链表节点数 量大于8时,会将链表转化为红黑树进行存储。
5.查询时间复杂度:从原来的遍历链表O(n),变成遍历红黑树O(logN)。
Hashtable 线程安全 concurrentHashMap 线程安全的,在多线程下效率更高。
注:hashtable:使用一把锁处理并发问题,当有多个线程访问时,需要多个线程竞争一把锁,导致阻塞。
1.7concurrentHashMap则使用分段锁,相当于把一个 hashmap分成多个,然后每个部分分配一把锁,这样就可 以支持多线程访问。(默认情况下,理论上讲,能同时支 持 16条线程并发)
1.8锁粒度细到了元素本身。理论上讲,是最高级别的并发。
ConcurrentLinkedQueue 是 Java 中实现 Queue 接口的一个线程安全类。
ConcurrentLinkedQueue 底层基于链表数据结构实现,它使用无锁的并发算法来保证多线程并发访问的正确性和性能。它采用了一种称为 “wait-free” 的并发算法,这种算法保证了每个线程在任何情况下都能够取得进展,不会被其他线程阻塞或死锁。
相比于其他的线程安全队列,ConcurrentLinkedQueue 具有更好的可伸缩性和性能表现,因为它能够充分利用多核处理器的并行性能,减少了锁的争用和线程阻塞的情况。同时,由于它是基于链表实现的,因此支持高效的队列操作,如添加元素、移除元素、检索队头元素等,时间复杂度都为 O(1)。
需要注意的是,ConcurrentLinkedQueue 不支持队列中的元素进行排序、随机访问和遍历,因为它只提供了基本的队列操作。如果需要对队列中的元素进行排序或者遍历操作,可以考虑使用其他的数据结构,如 PriorityQueue 或 LinkedList。
阻塞队列是一种特殊的队列,它可以在队列为空时阻塞取元素的线程,也可以在队列满时阻塞添加元素的线程。阻塞队列在多线程编程中非常有用,可以很方便地实现线程间的同步和通信。