了解常见集合类
一、集合类框架
1、集合类框架结构图
首先我们要对集合类结构有一个大体的认识,所有集合都继承于迭代器,分为单列集合和映射集合,单列集合分为有序可重复和有序不可重复,大概结构如下图所示
2、主要集合类的介绍
集合类 | 介绍 |
---|---|
Vector | Vector 类实现了一个动态数组 |
ArrayList | 一个可以动态修改的数组,与普通数组的区别就是它是没有固定大小的限制,我们可以添加或删除元素 |
LinkedList | 一种常见的基础数据结构,是一种线性表,但是并不会按线性的顺序存储数据,而是在每一个节点里存到下一个节点的地址。链表可分为单向链表和双向链表。 |
HashSet | HashSet 基于 HashMap 来实现的,是一个不允许有重复元素的集合。HashSet 允许有 null 值。HashSet 是无序的,即不会记录插入的顺序。HashSet 不是线程安全的, 如果多个线程尝试同时修改 HashSet,则最终结果是不确定的。 您必须在多线程访问时显式同步对 HashSet 的并发访问。 |
HashMap | HashMap 是一个散列表,它存储的内容是键值对(key-value)映射。HashMap 实现了 Map 接口,根据键的 HashCode 值存储数据,具有很快的访问速度,最多允许一条记录的键为 null,不支持线程同步。HashMap 是无序的,即不会记录插入的顺序。 |
3、各个集合类之间的区别
该部分内容摘自java成神之路
Set和List的区别
List特点:元素有放入顺序,元素可重复 。
有顺序,即先放入的元素排在前面。
Set特点:元素无放入顺序,元素不可重复。
无顺序,即先放入的元素不一定排在前面。 不可重复,即相同元素在set中只会保留一份。所以,有些场景下,set可以用来去重。 不过需要注意的是,set在元素插入时是要有一定的方法来判断元素是否重复的。这个方法很重要,决定了set中可以保存哪些元素。
ArrayList、LinkedList与Vector之间的区别
ArrayList 是一个可改变大小的数组.当更多的元素加入到ArrayList中时,其大小将会动态地增长.内部的元素可以直接通过get与set方法进行访问,因为ArrayList本质上就是一个数组.
LinkedList 是一个双链表,在添加和删除元素时具有比ArrayList更好的性能.但在get与set方面弱于ArrayList.
Vector 和ArrayList类似,但属于强同步类。如果你的程序本身是线程安全的(thread-safe,没有在多个线程之间共享同一个集合/对象),那么使用ArrayList是更好的选择。
Vector和ArrayList在更多元素添加进来时会请求更大的空间。Vector每次请求其大小的双倍空间,而ArrayList每次对size增长50%.
而 LinkedList 还实现了 Queue 接口,该接口比List提供了更多的方法,包括 offer(),peek(),poll()等.
HashMap、HashTable、ConcurrentHashMap区别
HashMap和HashTable有何不同?
线程安全: HashTable 中的方法是同步的,而HashMap中的方法在默认情况下是非同步的。在多线程并发的环境下,可以直接使用HashTable,但是要使用HashMap的话就要自己增加同步处理了。
继承关系: HashTable是基于陈旧的Dictionary类继承来的。 HashMap继承的抽象类AbstractMap实现了Map接口。
允不允许null值: HashTable中,key和value都不允许出现null值,否则会抛出NullPointerException异常。 HashMap中,null可以作为键,这样的键只有一个;可以有一个或多个键所对应的值为null。
默认初始容量和扩容机制: HashTable中的hash数组初始大小是11,增加的方式是 old*2+1。HashMap中hash数组的默认大小是16,而且一定是2的指数。原因参考全网把Map中的hash()分析的最透彻的文章,别无二家。-HollisChuang’s Blog
哈希值的使用不同 : HashTable直接使用对象的hashCode。 HashMap重新计算hash值。
遍历方式的内部实现上不同 : Hashtable、HashMap都使用了 Iterator。而由于历史原因,Hashtable还使用了Enumeration的方式 。 HashMap 实现 Iterator,支持fast-fail,Hashtable的 Iterator 遍历支持fast-fail,用 Enumeration 不支持 fast-fail
HashMap 和 ConcurrentHashMap 的区别?
ConcurrentHashMap和HashMap的实现方式不一样,虽然都是使用桶数组实现的,但是还是有区别,ConcurrentHashMap对桶数组进行了分段,而HashMap并没有。
ConcurrentHashMap在每一个分段上都用锁进行了保护。HashMap没有锁机制。所以,前者线程安全的,后者不是线程安全的。
PS:以上区别基于jdk1.8以前的版本。
二、常见集合类的数据结构
1、List
- ArrayList:Object数组
- Vector:Object数组
- LinkedList:双向循环链表
2、Set
- HashSet(无序,唯一):基于 HashMap 实现的,底层采用 HashMap 来保存元素
- LinkedHashSet: 继承与 HashSet,并且其内部是通过 LinkedHashMap 来实现的。
- TreeSet(有序,唯一): 红黑树(自平衡的排序二叉树)
3、Map
- HashMap: JDK1.8之前HashMap由数组+链表组成的,数组是HashMap的主体,链表则是主要为了解决哈希冲突而存在的(“拉链法”解决冲突).JDK1.8以后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)时,将链表转化为红黑树,以减少搜索时间
- LinkedHashMap:继承自 HashMap,所以它的底层仍然是基于拉链式散列结构即由数组和链表或红黑树组成。另外,LinkedHashMap 在上面结构的基础上,增加了一条双向链表,使得上面的结构可以保持键值对的插入顺序。同时通过对链表进行相应的操作,实现了访问顺序相关逻辑。
- HashTable: 数组+链表组成的,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的
- TreeMap: 红黑树(自平衡的排序二叉树)
三、多线程环境下的常见集合类
1、多线程下的常见集合类介绍
-
CopyOnWriteArrayList:CopyOnWriteArrayList采用的是一种写时复制的思想,也就是读写分离。
CopyOnWriteArrayList容器即写时复制的容器,往一个容器添加元素的时候,不直接往当前容器Object[]添加,而是先将当前容器Object[]进行copy,复制出一个新的容器Object[] newElements,然后新的容器newElements中添加元素,添加完元素之后,在将原容器的引用指向新的容器setArray(newElements);。这样做的好处是可以对CopyOnWriteArrayList容器进行并发的读,而不需要加锁,因为当前容器不会添加任何元素,所以CopyOnWriteArrayList容器也是有一种读写分离的思想,读和写不同的容器。 -
CopyOnWriteArraySet:由 CopyOnWriteArrayList实现
-
ConcurrentHashMap:
java1.8以前,它由Segment数组结构和HashEntry数组结构组成。Segment数组在ConcurrentHashMap里扮演锁的角色,HashEntry则用于存储键-值对数据。一个ConcurrentHashMap里包含一个Segment数组,Segment的结构和HashMap类似,是一种数组和链表结构;一个Segment里包含一个HashEntry数组,每个HashEntry是一个链表结构的元素;每个Segment守护着一个HashEntry数组里的元素,当对HashEntry数组的数据进行修改时,必须首先获得它对应的Segment锁。
java1.8以后,乐观锁+Sysnchronized,多线程并发向同一个散列桶添加元素时若散列桶为空,则触发乐观锁机制,线程获取散列桶中的版本号,在添加元素之前判断线程中的版本号与桶中的版本号是否一致,添加成功不一致,自旋若散列桶不为空,,则使用synchroinized.先访问到的线程给头结点解锁,形成链表或红黑树,JDK1.8中ConcrruentHashMap在保证线程安全的同时允许最大程序的多线程并发执行
2、ArrayList, HashMap,HashSet的线程不安全演示
1、ArrayList
import java.util.ArrayList;
import java.util.List;
public class ArrayListInThread {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
for (int i = 0; i < 10; i++) {
new Thread(() -> {
list.add("aaa");
System.out.println(list);
}).start();
}
}
}
查看三次结果
当线程过多时
2、HashMap
import java.util.HashMap;
import java.util.Map;
import java.util.Random;
public class HashMapInThread {
public static void main(String[] args) {
Map<Integer, Integer> map = new HashMap<>();
for (int i = 0; i < 5; i++) {
new Thread(() -> {
Random random = new Random();
int anInt = random.nextInt();
map.put(anInt,anInt);
System.out.println(map);
}).start();
}
}
}
查看三次结果
3、HashSet
import java.util.HashSet;
import java.util.Random;
import java.util.Set;
public class HashSetInThread {
public static void main(String[] args) {
Set<Integer> set = new HashSet<>();
for (int i = 0; i < 5; i++) {
new Thread(() -> {
Random random = new Random();
int anInt = random.nextInt();
set.add(anInt);
System.out.println(set);
}).start();
}
}
}
查看三次结果
四、操作集合类的注意事项
1、如何安全的删除集合中的元素
主要有三种方式
- 倒序删除
/**
* 使用迭代器删除元素
* @param list
*/
public static void removeElementByIterator(List<Integer> list, int number){
Iterator<Integer> iterator = list.iterator();
while (iterator.hasNext()){
if (iterator.next() == number){
iterator.remove();
}
}
}
- 迭代器删除
/**
* 倒序删除
* @param list
*/
public static void removeElementByReverse(List<Integer> list, int number){
for (int i = list.size() - 1; i > 0; i--) {
Integer element = list.get(i);
if (element == number) {
list.remove(element);
}
}
}
- lambda表达式删除
/**
* lambda表达式删除元素
* @param list
*/
public static void removeElementByLambda(List<Integer> list, int number){
list.removeIf( num -> num == number );
}