赞
踩
温馨提示:下面是以 Java 8 版本进行讲解,除非有特定说明。
Java 集合是一个存储相同类型数据的容器,类似数组,集合可以不指定长度,但是数组必须指定长度。集合类主要从 Collection 和 Map 两个根接口派生出来,比如常用的 ArrayList、LinkedList、HashMap、HashSet、ConcurrentHashMap 等等。
Collection 根接口框架结构图:
Map 根接口框架结构图:
二、List
ArrayList 是基于动态数组实现,容量能自动增长的集合。随机访问效率高,随机插入、随机删除效率低。线程不安全,多线程环境下可以使用 Collections.synchronizedList(list) 函数返回一个线程安全的 ArrayList 类,也可以使用 concurrent 并发包下的 CopyOnWriteArrayList 类。
动态数组,是指当数组容量不足以存放新的元素时,会创建新的数组,然后把原数组中的内容复制到新数组。
主要属性:
//存储实际数据,使用transient修饰,序列化的时不会被保存
transient Object[] elementData;
//元素的数量,即容量。
private int size;
**数据结构:**动态数组
特征:
使用场景:
add(element) 流程:
grow() 流程:
add(index,element) 流程:
set(index,element) 流程:
get(index) 流程:
remove(index) 流程:
注意事项:
LinkedList 是可以在任何位置进行插入和移除操作的有序集合,它是基于双向链表实现的,线程不安全。LinkedList 功能比较强大,可以实现栈、队列或双向队列。
主要属性:
//链表长度 transient int size = 0; //头部节点 transient Node<E> first; //尾部节点 transient Node<E> last; /\*\* \* 静态内部类,存储数据的节点 \*/ private static class Node\<E\> { //自身结点 E item; //下一个节点 Node<E> next; //上一个节点 Node<E> prev; }
**数据结构:**双向链表
特征:
使用场景:
add() 流程:
get(index,element) 流程:
remove() 流程:
Vector 是矢量队列,也是基于动态数组实现,容量可以自动扩容。跟 ArrayList 实现原理一样,但是 Vector 是线程安全,使用 Synchronized 实现线程安全,性能非常差,已被淘汰,使用 CopyOnWriteArrayList 替代 Vector。
主要属性:
//存储实际数据
protected Object[] elementData;
//动态数组的实际大小
protected int elementCount;
//动态数组的扩容系数
protected int capacityIncrement;
**数据结构:**动态数组
特征:
**使用场景:**多线程环境
Stack 是栈,先进后出原则,Stack 继承 Vector,也是通过数组实现,线程安全。因为效率比较低,不推荐使用,可以使用 LinkedList(线程不安全)或者 ConcurrentLinkedDeque(线程安全)来实现先进先出的效果。
**数据结构:**动态数组
**构造函数:**只有一个默认 Stack()
**特征:**先进后出
实现原理:
CopyOnWriteArrayList 是线程安全的 ArrayList,写操作(add、set、remove 等等)时,把原数组拷贝一份出来,然后在新数组进行写操作,操作完后,再将原数组引用指向到新数组。CopyOnWriteArrayList 可以替代 Collections.synchronizedList(List list)。
**数据结构:**动态数组
特征:
缺点:
实现原理:
CopyOnWriteArrayList 写操作加了锁,不然多线程进行写操作时会复制多个副本;读操作没有加锁,所以可以实现并发读,但是可能读到旧的数据,比如正在执行读操作时,同时有多个写操作在进行,遇到这种场景时,就会都到旧数据。
CopyOnWriteArraySet 是线程安全的无序并且不能重复的集合,可以认为是线程安全的 HashSet,底层是通过 CopyOnWriteArrayList 机制实现。
**数据结构:**动态数组(CopyOnWriteArrayList),并不是散列表。
特征:
HashMap 是以key-value 键值对形式存储数据,允许 key 为 null(多个则覆盖),也允许 value 为 null。底层结构是数组 + 链表 + 红黑树。
主要属性:
//存储元素的数组 transient Node<K,V>[] table; //存放元素的个数,非Node数组长度 transient int size; //记录结构性修改次数,用于快速失败 transient int modCount; //阈值 int threshold; //负载因子,默认0.75,用于扩容 final float loadFactor; /\*\* \* 静态内部类,存储数据的节点 \*/ static class Node\<K,V\> implements Map.Entry\<K,V\> { //节点的hash值 final int hash; //节点的key值 final K key; //节点的value值 V value; //下一个节点的引用 Node<K,V> next; }
**数据结构:**数组 + 单链表,Node 结构:hash|key|value|next
**只允许一个 key 为 Null(多个则覆盖),但允许多个 value 为 Null **
使用场景:
哈希冲突的解决方案:
put() 存储的流程(Java 8):
resize() 扩容的流程(Java 8):
扩容过程比较复杂, 迁移算法与 Java 7 不一样,Java 8 不需要每个元素都重新计算 hash,迁移过程中元素的位置要么是在原位置,要么是在原位置再移动 2 次幂的位置。
get() 查询的流程(Java 8):
get() 注意事项:Java 8 没有把 key 为 null 放到数组 table[0] 中。
remove() 删除的流程(Java 8):
remove() 注意事项:删除单个 key,注意返回是的键值对中的 value。
为什么使用位运算(&)来代替取模运算(%):
HashMap 在 Java 7 和 Java 8 中的区别:
和 HashMap 一样,Hashtable 也是一个哈希散列表,Hashtable 继承于 Dictionary,使用重入锁 Synchronized 实现线程安全,key 和 value 都不允许为 Null。HashTable 已被高性能的 ConcurrentHashMap 代替。
主要属性:
//真正存储数据的数组 private transient Entry<?,?>[] table; //存放元素的个数,非Entry数组长度 private transient int count; //阈值 private int threshold; //负载因子,默认0.75 private float loadFactor; //记录结构性修改次数,用于快速失败 private transient int modCount = 0; /\*\* \* 静态内部类,存储数据的节点 \*/ private static class Entry\<K,V\> implements Map.Entry\<K,V\> { //节点的hash值 final int hash; //节点的key值 final K key; //节点的value值 V value; //下一个节点的引用 Entry<K,V> next; }
快速失败原理是在并发场景下进行遍历操作时,如果有另外一个线程对它执行了写操作,此时迭代器可以发现并抛出 ConcurrentModificationException,而不需等到遍历完后才报异常。
**数据结构:**链表的数组,数组 + 链表,Entry 结构:hash|key|value|next
特征:
原理:
与 HashMap 不一样的流程是定位数组下标逻辑,HashTable 是在 key.hashcode() 后使用取模,HashMap 是位运算。HashTable 是 put() 之前进行判断是否扩容 resize(),而 HashMap 是 put() 之后扩容。
ConcurrentHashMap 在 Java 8 版本中丢弃了 Segment(分锁段)、ReentrantLock、HashEntry 数组这些概念,而是采用CAS + Synchronized 实现锁操作,Node 改名为 HashEntry,引入了红黑树保证查询效率,底层数据结构由数组 + 链表 + 红黑树组成,Node 数组默认为 16。数据结构如下图:
数据结构(Java 8):Node[] 数组 + 单链表 + 红黑树
resize() 扩容的流程(Java 8):
扩容的原理是创建新的数组,长度是原来的两倍,然后把旧数组数据迁移到新的数组中,在多线程情况下,需要注意线程安全问题,在解决安全问题的同时,还需要关注其效率。
get() 查询的流程(Java 8):
get() 注意事项:
整个过程都不需要加锁,因为读取数据的属性使用 volatile 修饰,实现线程可见性。
remove() 删除的流程(Java 8):
remove() 注意事项:
remove 函数底层是调用 replaceNode 函数实现结点的删除。
**使用场景:**并发、线程不安全场景
ConcurrentHashMap 在 Java 7 和 Java 8 中的区别:
HashMap、Hashtable、ConccurentHashMap 三者的区别(Java 8):
TreeMap 实现了 SotredMap 接口,意味着可以排序,是一个有序的集合。底层数据结构是红黑树结构,TreeMap 中的每个元素都存放在红黑树的节点上,默认使用自然排序,也可以自定排序,线程不安全。
主要属性:
//排序比较器 private final Comparator<? super K> comparator; //红黑树根节点 private transient Entry<K,V> root; //集合长度 private transient int size = 0; //记录结构性修改次数,用于快速失败 private transient int modCount = 0; //红黑树常量 private static final boolean RED = false; private static final boolean BLACK = true; /\*\* \* 实际存储数据的节点 \*/ static final class Entry\<K,V\> implements Map.Entry\<K,V\> { //节点的key值 K key; //节点的value值 V value; //左子树引用 Entry<K,V> left; //右子树引用 Entry<K,V> right; //父节点引用 Entry<K,V> parent; //节点颜色(默认黑色) boolean color = BLACK; }
**数据结构:**红黑树(高效的检索二叉树),Entry 结构:key|value|left|right|parent|color
特征:
put() 存储的流程:
主要分为两个步骤,构建排序二叉树和构建平衡二叉树。
构建排序二叉树,过程如下:
举个栗子:put(9),步骤如下:
**
** * 从 root 节点 8 开始检索;
remove() 的流程:比 put() 复杂,也是分两个步骤,删除节点和着色旋转。
删除节点,删除时出现以下 3 种情况:
着色旋转,进行颜色的对调和旋转,达到红黑树的特征。
LinkedHashMap 是使用 HashMap 机制实现。LinkedHashMap 在 HashMap 的基础上增加 before 和 after 两个属性来保证了迭代顺序。迭代顺序可以是插入顺序(默认),也可以是访问顺序。线程不安全。
主要属性:
/\*\* \* 静态内部类,存储数据的节点 \*/
static class Entry\<K,V\> extends HashMap.Node\<K,V\> {
Entry<K,V> before, after;
Entry(int hash, K key, V value, Node<K,V> next) {
super(hash, key, value, next);
}
}
//头结点
transient LinkedHashMap.Entry<K,V> head;
//尾结点
transient LinkedHashMap.Entry<K,V> tail;
//排序标识,true访问顺序迭代; false插入顺序迭代(默认)
final boolean accessOrder;
**数据结构:**数组 + 双向链表,Entry 结构:before|hash|key|value|next|after,before 和 after 用于维护整个双向链表。
特征:
原理:
LinkedHashMap 继承了 HashMap,hash 算法也是跟 HashMap 一致。LinkedHashMap 在 Entry 中新增了 before 和 after 两个属性来维护双向链表的迭代顺序。Entry 的 next 属性是维护 Entry 连接顺序,而 after 是维护迭代顺序。LinkedHashMap 使用 LRU 算法实现访问顺序排序,比如 get() 操作后的元素会移动到链表末尾,从而实现按访问顺序迭代。
**使用场景:**保证插入和访问顺序
插入顺序(默认)和访问顺序区别,请看下面的代码:
public static void main(String[] args) { LinkedHashMap<String, String> map1 = new LinkedHashMap<>(); map1.put("name", "joseph"); map1.put("age", "33"); map1.put("job", "敲代码"); map1.put("hobby", "打篮球"); map1.get("job"); System.out.println("----start----按插入顺序遍历----"); Iterator<Map.Entry<String, String>> iterator = map1.entrySet().iterator(); while (iterator.hasNext()) { Map.Entry<String, String> entry = iterator.next(); System.out.println("key=" + entry.getKey() + ", value=" + entry.getValue()); } System.out.println("-----end----按插入顺序遍历-----"); System.out.println(); System.out.println("----start----按访问顺序遍历----"); LinkedHashMap<String, String> map2 = new LinkedHashMap<>(16, 0.75f, true); map2.put("name", "joseph"); map2.put("age", "33"); map2.put("job", "敲代码"); map2.put("hobby", "打篮球"); map2.get("job"); Iterator<Map.Entry<String, String>> iterator2 = map2.entrySet().iterator(); while (iterator2.hasNext()) { Map.Entry<String, String> entry = iterator2.next(); System.out.println("key=" + entry.getKey() + ", value=" + entry.getValue()); } System.out.println("----end----按访问顺序遍历-----"); }
输出结果:
—-start—- 按插入顺序遍历—-
key=name, value=joseph
key=age, value=33
key=job, value = 敲代码
key=hobby, value = 打篮球
—–end—- 按插入顺序遍历—–
—-start—- 按访问顺序遍历—-
key=name, value=joseph
key=age, value=33
key=hobby, value = 打篮球
key=job, value = 敲代码
—-end—- 按访问顺序遍历—–
WeakHashMap 中的 Weak 是“弱”的含义,即弱化版的 HashMap。key 和 value 都允许为 null,线程不安全。
HashSet 是用来存储没有重复元素的集合类,并且是无序的。实现了 Set 接口。底层使用 HashMap 机制实现,所以也是线程不安全。
主要属性:
//定义了一个HashMap类型的成员变量,即拥有HashMap的所有属性
private transient HashMap<E,Object> map;
特征:
**使用场景:**去重、不要求顺序
**原理:**底层使用 HashMap 的 key 不能重复机制来实现没有重复的 HashSet。
TreeSet 实现了 SortedSet 接口,意味着可以排序,它是一个有序并且没有重复的集合类,底层是通过 TreeMap 实现。TreeSet 并不是根据插入的顺序来排序,而是字典自然排序。线程不安全。
TreeSet 支持两种排序方式:自然升序排序和自定义排序。
特征:
原理:
TreeSet 底层是基于 treeMap(红黑树结构)实现的,可以自定义比较器对元素进行排序,或是使用元素的自然顺序。
**使用场景:**去重、要求排序
LinkedHashSet 是使用 HashSet 机制实现,它是一个可以保证插入顺序或是访问顺序,并且没有重复的集合类。线程不安全。
**数据结构:**数组 + 双向链表,Entry 结构:before|hash|key|value|next|after,before 和 after 用于维护整个双向链表。
特征:
原理:
LinkedHashSet 底层使用了 LinkedHashMap 机制(比如 before 和 after),加上又继承了 HashSet,所以可以实现既可以保证迭代顺序,又可以达到不出现重复元素。
**使用场景:**去重、需要保证插入或者访问顺序
HashSet,TreeSet,LinkedHashSet 之间的区别:HashSet 只去重,TreeSet 去重并排序,LinkedHashSet 去重并保证迭代顺序。
Queue 是一个**先入先出(FIFO)**的集合,它有 3 种实现方式:阻塞队列、非阻塞队列、双向队列。Queue 跟 List、Set 一样,也是继承了 Collection 接口。
阻塞队列是一个可以阻塞的先进先出集合,比如某个线程在空队列获取元素时、或者在已存满队列存储元素时,都会被阻塞。BlockingQueue 接口常用的实现类如下:
常用方法:
非阻塞队列是使用**CAS(compare and set)**机制实现,类似 volatile,并发性能好。常用的阻塞队列有 PriorityQueue 和 ConcurrentLinkedQueue。
Deque 是一个既可以在头部操作元素,又可以为尾部操作元素,俗称为双向(双端)队列。Deque 继承自 Queue,Deque 实现类有 LinkedList、 ArrayDeque、ConcurrentLinkedDeque 等等。
PS:如有写错请指正,感谢您阅读。
欢迎关注我的公众号,回复关键字“Java” ,将会有大礼相送!!! 祝各位面试成功!!!
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。