List, Set, Queue, Map 四者的区别
-
List
(对付顺序的好帮手): 存储的元素是有序的、可重复的。 -
Set
(注重独一无二的性质): 存储的元素是无序的、不可重复的。 -
Queue
(实现排队功能的叫号机): 按特定的排队规则来确定先后顺序,存储的元素是有序的、可重复的。 -
Map
(用 key 来搜索的专家): 使用键值对(key-value)存储,类似于数学上的函数 y=f(x),“x” 代表 key,“y” 代表 value,key 是无序的、不可重复的,value 是无序的、可重复的,每个键最多映射到一个值。
集合底层数据结构
List
Arraylist
:Object[]
数组Vector
:Object[]
数组LinkedList
: 双向链表(JDK1.6 之前为循环链表,JDK1.7 取消了循环,现在是双向链表)
Set
-
HashSet
(无序,唯一): 基于HashMap
实现的,底层采用HashMap
来保存元素 -
LinkedHashSet
:LinkedHashSet
是HashSet
的子类,并且其内部是通过LinkedHashMap
来实现的。有点类似于我们之前说的LinkedHashMap
其内部是基于HashMap
实现一样,不过还是有一点点区别的 -
TreeSet
(有序,唯一): 红黑树(自平衡的排序二叉树)
Queue
PriorityQueue
:Object[]
数组来实现二叉堆ArrayQueue
:Object[]
数组 + 双指针
Map
HashMap
: JDK1.8 之前HashMap
由数组+链表组成的,数组是HashMap
的主体,链表则是主要为了解决哈希冲突而存在的(“拉链法”解决冲突)。JDK1.8以后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为 8)(将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树)时,将链表转化为红黑树,以减少搜索时间LinkedHashMap
:LinkedHashMap
继承自HashMap
,所以它的底层仍然是基于拉链式散列结构即由数组和链表或红黑树组成。另外,LinkedHashMap
在上面结构的基础上,增加了一条双向链表,使得上面的结构可以保持键值对的插入顺序。同时通过对链表进行相应的操作,实现了访问顺序相关逻辑。详细可以查看:Hashtable
: 数组+链表组成的,数组是Hashtable
的主体,链表则是主要为了解决哈希冲突而存在的TreeMap
: 红黑树(自平衡的排序二叉树)
选用集合
-
根据键值获取元素值使用
Map
接口下的集合- 排序:
TreeSet
- 不排序:
HashMap
- 线程安全:
ConcurrentHashMap
- 排序:
-
存取单个元素使用
Collection
接口下的集合- 保证元素唯一:
set
下的集合 - 不需要保证元素唯一:
List
下的集合
- 保证元素唯一:
为什么要使用集合来存储对象
- 使用数组的话,数组的长度一旦声明后,就固定了。同时,数组中保存的元素类型也就固定了。
- 使用集合的话,可以根据需要自动扩容或手动扩容;并且,集合中可以存储的对象类型是多种多样的。另外,有各种各样不同类型的集合满足生产需求。
Collection 子接口List
Arraylist
和 Vector
的区别?
Arraylist
使用Object[ ]数组存储元素,适用于频繁的查找工作。非线程安全Vector
使用Object[ ]数组存储元素,线程安全
Arraylist
和 LinkedList
的区别?
-
Arraylist
和LinkedList
都是不同步的,线程不安全的。 -
Arraylist
底层使用Object[ ]数组存储元素;LinkedList
底层在1.7之前使用循环链表,1.7开始取消循环链表使用双向链表 -
随机插入和删除元素是否受元素位置的影响:
Arraylist
采用数组实现,随机插入和删除元素会导致这个位置之后的每个元素都移动。所以随机插入和删除元素是受元素位置影响的,在首尾添加时间复杂度都为O(1)。LinkedList
采用链表实现,随机插入和删除元素是受元素位置影响的,需要先将指针移动到指定位置才可以插入删除元素。在首尾添加时间复杂度都为O(1)。
-
快速随机访问元素:
Arraylist
采用数组实现,可以根据索引快速定位到元素。LinkedList
采用链表实现,无法实现快速随机访问元素。
-
内存占用:
Arraylist
的内存占用主要是数组需要预留一定的空间来存储新的元素;LinkedList
的内存占用是每一个元素占用的空间都比Arraylist
更多,因为每一个元素需要存储直接前驱、直接后继和数据。
-
补充:RandomAccess 接口
RandomAccess
接口中并没有任何语句,根据注释可以发现,这个借口是一个标志借口。实现了RandomAccess
这个接口的集合,具有随即快速访问的功能。
/**Marker interface used by List implementations to indicate that they support fast (generally constant time) random access.
列表实现所使用的标记接口,用于指示它们支持快速(通常为常数时间)随机访问。
public interface RandomAccess {
}
Arraylist
实现了RandomAccess
接口,所以是支持快速随机访问的。当然主要是因为它的数据结构决定了它适用于快速访问。
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
而LinkedList
并没有实现RandomAccess
接口,底层的数据结构也不支持LinkedList
快速访问。
public class LinkedList<E>
extends AbstractSequentialList<E>
implements List<E>, Deque<E>, Cloneable, java.io.Serializable
{
ArrayList 的扩容机制
-
默认初始化容量为10
JDK1.7之前无参构造创建的
ArrayList
是直接创建一个容量为10的Object [ ]
数组JDK1.7开始无参构造创建的
ArrayList
是创建一个空Object [ ]
数组,当插入第一个元素时,会将数组容量扩容到10。
/**
* Default initial capacity.
*/
private static final int DEFAULT_CAPACITY = 10;
-
使用add()方法添加元素超出当前数组容量时,数组会进行自动扩容。在调用add()方法后,会先调用
ensureCapacityInternal
方法获取当前容量和插入元素需要的最小容量;/** * 将指定的元素追加到此列表的末尾。 */ public boolean add(E e) { //添加元素之前,先调用ensureCapacityInternal方法 ensureCapacityInternal(size + 1); // Increments modCount!! //这里看到ArrayList添加元素的实质就相当于为数组赋值 elementData[size++] = e; return true; }
//得到最小扩容量 private void ensureCapacityInternal(int minCapacity) { if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) { // 获取默认的容量和传入参数的较大值 minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity); } ensureExplicitCapacity(minCapacity); }
再调用
ensureExplicitCapacity
方法判断当前数组应该有的最小容量是否大于默认容量;//判断是否需要扩容 private void ensureExplicitCapacity(int minCapacity) { modCount++; // overflow-conscious code if (minCapacity - elementData.length > 0) //调用grow方法进行扩容,调用此方法代表已经开始扩容了 grow(minCapacity); }
如果大于,则会调用==
grow
方法进行扩容。使用位运算将旧容量右移一位,也就是扩大了1.5倍==。
/**
* 要分配的最大数组大小
*/
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
/**
* ArrayList扩容的核心方法。
*/
private void grow(int minCapacity) {
// oldCapacity为旧容量,newCapacity为新容量
int oldCapacity = elementData.length;
//将oldCapacity 右移一位,其效果相当于oldCapacity /2,
//我们知道位运算的速度远远快于整除运算,整句运算式的结果就是将新容量更新为旧容量的1.5倍,
int newCapacity = oldCapacity + (oldCapacity >> 1);
//然后检查新容量是否大于最小需要容量,若还是小于最小需要容量,那么就把最小需要容量当作数组的新容量,
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
// 如果新容量大于 MAX_ARRAY_SIZE,进入(执行) `hugeCapacity()` 方法来比较 minCapacity 和 MAX_ARRAY_SIZE,
//如果minCapacity大于最大容量,则新容量则为`Integer.MAX_VALUE`,否则,新容量大小则为 MAX_ARRAY_SIZE 即为 `Integer.MAX_VALUE - 8`。
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// minCapacity is usually close to size, so this is a win:
elementData = Arrays.copyOf(elementData, newCapacity);
}
其中调用了Arrays.copyOf()
方法对原有数组进行扩容
- 在 add 大量元素之前用
ensureCapacity
方法,以减少增量重新分配的次数
Collection 子接口Set
无序性和不可重复性
- 无序性:是指存储的数据并非按照索引的顺序来添加,而是使用哈希值确定添加位置
- 不可重复性:是指添加元素时,需要调用
equals()
方法进行判断集合中是否已经添加了和这个元素值相等的元素。必须要同时重写equals()
方法和hashCode()
方法。
比较 HashSet、LinkedHashSet 和 TreeSet
-
都是
Set
接口下的实现类,都可以保证元素唯一,并且都是线程不安全的。 -
HashSet
的底层是基于HashMap
实现的哈希表。LinkedHashSet
的底层数据结构是链表和哈希表。TreeSet
底层数据结构是红黑树,元素是有序的,排序的方式有自然排序和定制排序。 -
应用场景:
HashSet
用于不需要保证元素插入和取出顺序的场景,LinkedHashSet
用于保证元素的插入和取出顺序满足 FIFO 的场景,TreeSet
用于支持对元素自定义排序规则的场景。
comparable 和 Comparator 的区别
Comparable
接口:
public interface Comparable<T> {
public int compareTo(T o);
}
Comparator
接口:
public interface Comparator<T> {
int compare(T o1, T o2);
- 如果需要对一个集合实现两种排序方式,就可以重写
compareTo
方法并且自定义Comparator
重写排序规则。
Collection 子接口Queue
Queue 与 Deque 的区别
-
Queue
是单端队列,只能从一端插入元素,另一端删除元素,实现上一般遵循 先进先出(FIFO) 规则。Queue
因为自身容量问题导致操作失败有两种处理方式:抛出异常和返回特殊值 -
Deque
是双端队列,在队列的两端均可以插入或删除元素。Deque
继承自Queue
,在队首和队尾都增加了插入和删除的方法。也根据操作失败后处理方式的不同分为两类。Deque
中还提供了push
和pop
方法,可以模拟栈。
ArrayDeque 与 LinkedList 的区别
-
ArrayDeque
与LinkedList
都实现了Deque
接口,都具有双端队列的功能。 -
ArrayDeque
是基于Object[]
数组和双指针实现的,而LinkedList
是双向链表 -
ArrayDeque
不支持存储NULL
数据,但LinkedList
支持。 -
ArrayDeque
是在 JDK1.6 才被引入的,而LinkedList
早在 JDK1.2 时就已经存在。 -
ArrayDeque
数组的初始化容量是8,插入值可能会存在扩容。LinkedList
无需扩容,但每一次插入数据都需要新的堆内存,均摊性能较低。
从性能的角度上,选用 ArrayDeque
来实现队列要比 LinkedList
更好。此外,ArrayDeque
也可以用于实现栈。
PriorityQueue
PriorityQueue
是在 JDK1.5 中被引入的, 其与 Queue
的区别在于元素出队顺序是与优先级相关的,即总是优先级最高的元素先出队。
PriorityQueue
利用了二叉堆的数据结构来实现的,底层使用可变长的数组来存储数据PriorityQueue
通过堆元素的上浮和下沉,实现了在 O(logn) 的时间复杂度内插入元素和删除堆顶元素。PriorityQueue
是非线程安全的,且不支持存储NULL
和non-comparable
的对象。PriorityQueue
默认是小顶堆,但可以接收一个Comparator
作为构造参数,从而来自定义元素优先级的先后。
Map接口
HashMap 和 Hashtable 的区别
-
是否线程安全:
Hashtable
中的方法都是有synchronized关键字修饰的,是线程安全的;HashMap
中的方法没有synchronized关键字修饰,是线程不安全的;public synchronized boolean isEmpty() { return count == 0; } public synchronized Enumeration<K> keys() { return this.<K>getEnumeration(KEYS); } public synchronized Enumeration<V> elements() { return this.<V>getEnumeration(VALUES); }
-
**效率:**因为
HashMap
是非线程安全的,不存在上锁,所以效率会比Hashtable
更高一些。 -
对 Null key 和 Null value 的支持:
HashMap
是支持Null key 和 Null value 的,但作为key只允许存在一个键位null,value可以有多个。而Hashtable
中是不允许存储null键和null值的。
-
初始容量大小和每次扩充容量大小的不同 :
HashMap
的初始容量是16,每次扩容到之前容量的2倍。创建时如果指定了初始化容量大小,HashMap
会将其扩充为2的幂次方大小。Hashtable
的初始容量是11,每次扩容为2n+1倍。创建时如果指定了初始化容量大小,Hashtable
就会使用指定的容量初始化集合。
注:
Hashtable
基本被淘汰,在实际的业务中,很少使用。
HashMap 和 HashSet 区别
HashSet
底层就是使用HashMap
实现的,除去重写了少量方法,大部分方法都是直接使用HashMap
中的方法。
HashMap | HashSet |
---|---|
实现了Map接口 | 实现了Set接口 |
存储键值对 | 存储的是对象 |
put方法添加元素 | add方法添加元素 |
HashMap 是使用key来计算hashCode 值 | HashSet 使用对象来计算hashCode 值 |
HashMap 和 TreeMap 区别
NavigableMap
接口:实现该接口的集合具有对集合内元素的搜索能力
SortedMap
接口:实现该接口的集合具有根据key值排序的能力,默认按照升序排序,但也可以自定义排序规则。
所以,TreeMap
相比于 HashMap
主要多了对集合中的元素根据键排序的能力以及对集合内元素的搜索的能力。
HashSet 如何检查重复
-
插入对象时,会先计算对象对应的
hashCode
来判断对象加入的位置,并且检查集合中是否有相同hashCode
值的元素,如果有,会使用equals
方法来判断两个对象是否真的相同。如果返回true
,就无法插入该元素;如果返回false
,就 会重新生成hashCode
值确定插入元素。 -
通过查看源码。无论
HashSet
中是否存在该元素,都会先插入,然后通过返回值来告诉我们插入前是否存在相同元素。
/**
* Implements Map.put and related methods
*
* @param hash hash for key
* @param key the key
* @param value the value to put
* @param onlyIfAbsent if true, don't change existing value
* @param evict if false, the table is in creation mode.
* @return previous value, or null if none
如果插入位置没有元素返回null,否则返回上一个元素
*/
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
-
hashCode()
与equals()
的相关规定:-
hashCode()
是对堆上的对象产生独特值。 -
hashCode()
值相同,不一定是相同对象。存在哈希碰撞。 -
hashCode()
值不相同,一定是不相同对象。 -
相同对象的
equals()
方法返回 true。总结:必须同时重写
hashCode()
和equals()
-
HashMap
JDK1.8 之前的底层实现
JDK1.8 之前 HashMap
底层是 数组和链表 结合在一起使用也就是 链表散列。HashMap 通过 key 的 hashCode 经过扰动函数处理过后得到 hash 值,然后通过 (n - 1) & hash 判断当前元素存放的位置(这里的 n 指的是数组的长度),如果当前位置存在元素的话,就判断该元素与要存入的元素的 hash 值以及 key 是否相同,如果相同的话,直接覆盖,不相同就通过拉链法解决冲突。
扰动函数:指的就是 HashMap 的 hash 方法。使用 hash 方法也就是扰动函数是为了防止一些实现比较差的 hashCode() 方法 换句话说使用扰动函数之后可以减少碰撞。
“拉链法” 就是:将链表和数组相结合。也就是说创建一个链表数组,数组中每一格就是一个链表。若遇到哈希冲突,则将冲突的值加到链表中即可。
JDK1.8 之后的底层实现
-
解决哈希冲突的方法变化:当链表长度大于阈值(默认为8),并且当此时数组长度是大于64的(如果小于64,会先进行数组扩容),就会将单向链表转换为红黑树,减少搜索时间。
-
使用红黑树的原因:解决二叉查找树存在的缺陷:如果因为算法问题,大量的元素都存放在了二叉树的一个分支上,这样就影响了效率。
HashMap 的长度为什么是 2 的幂次方
为了能让 HashMap 存取高效,尽量较少碰撞,也就是要尽量把数据分配均匀。
在Hash 值的范围值下,大概 40 亿的映射空间,只要哈希函数映射得比较均匀松散,一般应用是很难出现碰撞的。但问题是一个 40 亿长度的数组,内存是放不下的。所以这个散列值是不能直接拿来用的。用之前还要先做对数组的长度取模运算,得到的余数才能用来要存放的位置也就是对应的数组下标。
这个数组下标的计算方法是“ (n - 1) & hash
”。(n 代表数组长度)。
使用取余运算来实现:hash%length==hash&(length-1)的前提是 length 是 2 的 n 次方,并且使用&运算相比于%提高了运算效率,这就解释了为什么HashMap 的长度为什么是 2 的幂次方。
多线程操作HashMap 导致死循环问题
- 并发下的 Rehash 会造成元素之间会形成一个循环链表。
- 不过,jdk 1.8 后解决了这个问题,但是还是不建议在多线程下使用 HashMap,因为多线程下使用 HashMap 还是会存在其他问题比如数据丢失。
- 并发环境下推荐使用 ConcurrentHashMap
HashMap常见的遍历方式
-
迭代器(Iterator)方式遍历;
-
使用迭代器(Iterator)EntrySet 的方式进行遍历;
long l = System.currentTimeMillis(); Iterator<Map.Entry<Integer, String>> iterator = hashMap.entrySet().iterator(); while (iterator.hasNext()) { Map.Entry<Integer, String> next = iterator.next(); System.out.println("key: " +next.getKey() + " | value: " + next.getValue()); } long l1 = System.currentTimeMillis() - l; System.out.println("遍历用时:" + l1);
-
使用迭代器(Iterator)KeySet 的方式进行遍历;
long l = System.currentTimeMillis(); Iterator<Integer> iterator = hashMap.keySet().iterator(); while (iterator.hasNext()) { Integer next = iterator.next(); System.out.println("key: " + next + " | value: " + hashMap.get(next)); } long l1 = System.currentTimeMillis() - l; System.out.println("遍历用时:" + l1);//遍历用时:0
-
-
For Each 方式遍历;
-
使用 For Each EntrySet 的方式进行遍历;
long l = System.currentTimeMillis(); for (Map.Entry<Integer, String> entry: hashMap.entrySet()) { System.out.println("key: " + entry.getKey() + " | value: " + entry.getValue()); } long l1 = System.currentTimeMillis() - l; System.out.println("遍历用时:" + l1);//遍历用时:0
-
使用 For Each KeySet 的方式进行遍历;
long l = System.currentTimeMillis(); for (Integer key : hashMap.keySet()) { System.out.println("key: " + key + " | value: " + hashMap.get(key)); } long l1 = System.currentTimeMillis() - l; System.out.println("遍历用时:" + l1);//遍历用时:0
-
-
Lambda 表达式遍历(JDK 1.8+);
-
使用 Lambda 表达式的方式进行遍历;
long l = System.currentTimeMillis(); // 遍历 hashMap.forEach((key, value) -> { System.out.println("key: " + key + " | value: " + value); }); long l1 = System.currentTimeMillis() - l; System.out.println("遍历用时:" + l1);//遍历用时:38
-
-
Streams API 遍历(JDK 1.8+)。
-
使用 Streams API 单线程的方式进行遍历;
long l = System.currentTimeMillis(); hashMap.entrySet().stream().forEach((entry) -> { System.out.println("key: " + entry.getKey() + " | value: " + entry.getValue()); }); long l1 = System.currentTimeMillis() - l; System.out.println("遍历用时:" + l1);//遍历用时:37
-
使用 Streams API 多线程的方式进行遍历。
long l = System.currentTimeMillis(); hashMap.entrySet().parallelStream().forEach((entry) -> { System.out.println("key: " + entry.getKey() + " | value: " + entry.getValue()); }); long l1 = System.currentTimeMillis() - l; System.out.println("遍历用时:" + l1);//遍历用时:38
-
性能对比:
EntrySet
比 KeySet
的性能高
原因:
KeySet
在循环时使用了 map.get(key)
,在使用迭代器或者 for 循环时,其实已经遍历了一遍 Map 集合了,因此再使用 map.get(key)
查询时,相当于遍历了两遍。
而 EntrySet
只遍历了一遍 Map 集合,之后通过代码“Entry<Integer, String> entry = iterator.next()”把对象的 key
和 value
值都放入到了 Entry
对象中,因此再获取 key
和 value
值时就无需再遍历 Map 集合,只需要从 Entry
对象中取值就可以了。
所以,EntrySet
的性能比 KeySet
的性能高出了一倍,因为 KeySet
相当于循环了两遍 Map 集合,而 EntrySet
只循环了一遍。
ConcurrentHashMap
底层具体实现
- JDK1.7
ConcurrentHashMap
是由 Segment
数组结构 + HashEntry
数组结构组成。
将数据分段存储,然后为每一段数据分配一把锁,当操作某一段数据时候,不会影响对其它段的数据。
Segment
的个数一旦初始化就不能改变,默认Segment
的个数是16个,也可以认为ConcurrentHashMap
最多支持16个线程并发。
Segment
实现了 ReentrantLock
,所以 Segment
是一种可重入锁,扮演锁的角色。HashEntry
用于存储键值对数据。
据。
static class Segment<K,V> extends ReentrantLock implements Serializable {
}
一个 ConcurrentHashMap
里包含一个 Segment
数组。Segment
的结构和 HashMap
类似,是一种数组和链表结构,一个 Segment
包含一个 HashEntry
数组,每个 HashEntry
是一个链表结构的元素,每个 Segment
守护着一个 HashEntry
数组里的元素,当对 HashEntry
数组的数据进行修改时,必须首先获得对应的 Segment
的锁。
- JDK1.8
ConcurrentHashMap
取消了 Segment
分段锁,采用 CAS 和 synchronized
来保证并发安全。数据结构跟 HashMap1.8 的结构类似,数组+链表/红黑二叉树。Java 8 在链表长度超过一定阈值(8)时将链表(寻址时间复杂度为 O(N))转换为红黑树(寻址时间复杂度为 O(log(N)))
synchronized
只锁定当前链表或红黑二叉树的首节点,这样只要 hash 不冲突,就不会产生并发,效率又提升 N 倍。
ConcurrentHashMap 和 Hashtable 的区别
区别主要体现在实现线程安全的方式上不同。
-
底层数据结构:
- JDK1.7 的
ConcurrentHashMap
底层采用 分段的数组+链表 实现,JDK1.8 采用的数据结构跟HashMap1.8
的结构一样,数组+链表/红黑二叉树。 Hashtable
和 JDK1.8 之前的HashMap
的底层数据结构类似都是采用 数组+链表 的形式,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的;
- JDK1.7 的
-
实现线程安全的方式(重要):
- ① 在 JDK1.7 的时候,
ConcurrentHashMap
(分段锁) 对整个桶数组进行了分割分段(Segment
),每一把锁只锁容器其中一部分数据,多线程访问容器里不同数据段的数据,就不会存在锁竞争,提高并发访问率。 到了 JDK1.8 的时候已经摒弃了Segment
的概念,而是直接用Node
数组+链表+红黑树的数据结构来实现,并发控制使用synchronized
和 CAS 来操作。(JDK1.6 以后 对synchronized
锁做了很多优化) 整个看起来就像是优化过且线程安全的HashMap
,虽然在 JDK1.8 中还能看到Segment
的数据结构,但是已经简化了属性,只是为了兼容旧版本; - ②
Hashtable
(同一把锁) :使用synchronized
来保证线程安全,效率非常低下。当一个线程访问同步方法时,其他线程也访问同步方法,可能会进入阻塞或轮询状态,如使用 put 添加元素,另一个线程不能使用 put 添加元素,也不能使用 get,竞争会越来越激烈效率越低。
- ① 在 JDK1.7 的时候,
Collections 工具类
-
排序
List<Integer> list = new ArrayList<>(); list.add(16); list.add(25); list.add(14); list.add(43); Collections.reverse(list);//反转 Collections.shuffle(list);//随机排序 Collections.sort(list);//按照自然升序排 Collections.swap(list,1,2);//交换对应索引的元素 Collections.rotate(list,1);//旋转。当distance为正数时,将list后distance个元素整体移到前面。当distance为负数时,将 list的前distance个元素整体移到后面 for (Integer integer : list) { System.out.println(integer); }
-
查找,替换操作
Collections.binarySearch(list,3);//二分查找,前提是有序数组 Collections.max(list);//按照自然顺序返回最大的元素 Collections.frequency(list,25);//统计指定元素的出现次数 Collections.indexOfSubList(list,list1);//统计list1在list中第一次出现的索引,找不到的话 Collections.replaceAll(list,25,56);//用新元素替代在集合中出现的旧元素 Collections.fill(list,25);//用指定元素替代在集合中所有的元素
-
同步控制(不推荐,需要线程安全的集合类型时请考虑使用 JUC 包下的并发集合)
Collections
提供了多个synchronizedXxx()
方法·,该方法可以将指定集合包装成线程同步的集合,从而解决多线程并发访问集合时的线程安全问题。HashSet
,TreeSet
,ArrayList
,LinkedList
,HashMap
,TreeMap
都是线程不安全的。Collections
提供了多个静态方法可以把他们包装成线程同步的集合。注:尽量不使用这些方法,效率非常低,需要线程安全的集合类型时请考虑使用 JUC 包下的并发集合。
Collections.synchronizedList()
Collections.synchronizedMap()
Collections.synchronizedSet()
使用集合的注意事项
集合判空
判断所有集合内部的元素是否为空,使用
isEmpty()
方法,而不是size()==0
的方式。
- 因为
isEmpty()
方法的可读性更好,并且时间复杂度为 O(1)。 - 绝大多数集合的
size()
的时间复杂度也为 O(1),但java.util.concurrent
包下的某些集合(ConcurrentLinkedQueue
、ConcurrentHashMap
…)时间复杂度就不是O(1)。
集合转 Map
在使用
java.util.stream.Collectors
类的toMap()
方法转为Map
集合时,一定要注意当 value 为 null 时会抛 空指针异常。
toMap()
调用了Map
接口下的merge()
方法
Collector<T, ?, M> toMap(Function<? super T, ? extends K> keyMapper,
Function<? super T, ? extends U> valueMapper,
BinaryOperator<U> mergeFunction,
Supplier<M> mapSupplier) {
BiConsumer<M, T> accumulator
= (map, element) -> map.merge(keyMapper.apply(element),
valueMapper.apply(element), mergeFunction);
return new CollectorImpl<>(mapSupplier, accumulator, mapMerger(mergeFunction), CH_ID);
}
这个方法是Map
接口的默认实现,会先判断值是否为NULL
。
default V merge(K key, V value,
BiFunction<? super V, ? super V, ? extends V> remappingFunction) {
Objects.requireNonNull(remappingFunction);
Objects.requireNonNull(value);
如果value
为null
,会抛出NPE异常
public static <T> T requireNonNull(T obj) {
if (obj == null)
throw new NullPointerException();
return obj;
}
集合遍历
不要在 foreach 循环里进行元素的
remove/add
操作。remove 元素请使用Iterator
方式,如果并发操作,需要对Iterator
对象加锁。
foreach 语法糖底层其实还是依赖 Iterator
。不过, remove/add
操作直接调用的是集合自己的方法,而不是 Iterator
的 remove/add
方法
这就导致 Iterator
莫名其妙地发现自己有元素被 remove/add
,然后,它就会抛出一个 ConcurrentModificationException
来提示用户发生了并发修改异常。这就是单线程状态下产生的 fail-fast 机制。
fail-fast 机制 :多个线程对 fail-fast 集合进行修改的时候,可能会抛出
ConcurrentModificationException
。 即使是单线程下也有可能会出现这种情况,上面已经提到过。
集合去重
可以利用
Set
元素唯一的特性,可以快速对一个集合进行去重操作,避免使用List
的contains()
进行遍历去重或者判断包含操作。
以 HashSet
和 ArrayList
为例
HashSet
的 contains()
方法依赖于HashMap
的 containsKey()
方法,时间复杂度接近于 O(1)
public boolean contains(Object o) {
return map.containsKey(o);
}
ArrayList
的 contains()
方法调用了indexOf()
方法,相当于遍历集合,时间复杂度接近于 O(n)
public boolean contains(Object o) {
return indexOf(o) >= 0;
}
/**
* Returns the index of the first occurrence of the specified element
* in this list, or -1 if this list does not contain the element.
* More formally, returns the lowest index <tt>i</tt> such that
* <tt>(o==null ? get(i)==null : o.equals(get(i)))</tt>,
* or -1 if there is no such index.
*/
public int indexOf(Object o) {
if (o == null) {
for (int i = 0; i < size; i++)
if (elementData[i]==null)
return i;
} else {
for (int i = 0; i < size; i++)
if (o.equals(elementData[i]))
return i;
}
return -1;
}
集合转数组
使用集合转数组的方法,必须使用集合的
toArray(T[] array)
,传入的是类型完全一致、长度为 0 的空数组。
toArray(T[] array)
方法的参数是一个泛型数组,如果 toArray
方法中没有传递任何参数的话返回的是 Object
类 型数组。
String[] strings = new String[]{"hello","spring","redis","linux"};
for (String string : strings) {
System.out.println(string);
}
List<String> list2 = Arrays.asList(strings);
String[] strings1 = list2.toArray(new String[0]);
for (String s : strings1) {
System.out.println(s);
}
由于 JVM 优化,new String[0]
作为Collection.toArray()
方法的参数现在使用更好,new String[0]
就是起一个模板的作用,指定了返回数组的类型,0 是为了节省空间,因为它只是为了说明返回的类型。
数组转集合
使用工具类
Arrays.asList()
把数组转换成集合时,不能使用其修改集合相关的方法, 它的add/remove/clear
方法会抛出UnsupportedOperationException
异常。
1、Arrays.asList()
是泛型方法,传递的数组必须是对象数组,而不是基本类型。
当传入一个原生数据类型数组时,Arrays.asList()
的真正得到的参数就不是数组中的元素,而是数组对象本身!此时 List
的唯一元素就是这个数组,这也就解释了上面的代码。
使用包装类型数组就可以解决这个问题。
2、使用集合的修改方法: add()
、remove()
、clear()
会抛出异常。
Arrays.asList()
方法返回的并不是 java.util.ArrayList
,而是 java.util.Arrays
的一个内部类,这个内部类并没有实现集合的修改方法或者说并没有重写这些方法。
如何正确的将数组转换为 ArrayList
?
(1)最简便的方法
List list = new ArrayList<>(Arrays.asList("a", "b", "c"))
(2)使用 Java8 的 Stream
(推荐)
Integer [] myArray = { 1, 2, 3 };
List myList = Arrays.stream(myArray).collect(Collectors.toList());
//基本类型也可以实现转换(依赖boxed的装箱操作)
int [] myArray2 = { 1, 2, 3 };
List myList = Arrays.stream(myArray2).boxed().collect(Collectors.toList());
(3)使用 Apache Commons Collections
List<String> list = new ArrayList<String>();
CollectionUtils.addAll(list, str);
(4) 使用 Java9 的 List.of()
方法
Integer[] array = {1, 2, 3};
List<Integer> list = List.of(array);