Java集合
本文主要讲解JDK内部集合类、Apache Commons Collection中的集合类、Guava中的集合类、并发集合类、Redission分布式集合类。
# 一、JDK内部集合
# 1、数组
两种初始化方法:
// 方式1
String[] arr = new String[2];
// 方式1
String[] arr = new String[]{"", ""};
多维数组:
String[][] arr = new String[2][3];
用法:
// 访问元素
arr[0]
// 循环-方式1
for (String s : arr) {
...
}
// 循环-方式2
for (int i = 0; i < arr.length; i++) {
String s = arr[i];
...
}
// 循环-方式3
Arrays.stream(arr).forEach(s -> {});
// 循环-倒序
for (int i = arr.length - 1; i >= 0; i--) {
String s = arr[i];
...
}
Arrays工具类中的方法:
stream
:数组转为Stream流copyOfRange
:将指定数组的指定范围复制到新数组中copyOf
:复制指定的数组,截断或填充空值(如有必要),使副本具有指定的长度binarySearch
:二分搜索fill
:将指定的值分配给数组的每个元素setAll
:和fill类似,不过指定值为自定义Functionsort
:升序排序spliterator
:拆分器。大多数情况下,你都不需要使用到这个方法,他主要是跟StreamSupport配合使用。参考java8 Stream之Spliterator (opens new window)parallelPrefix
:使用提供的函数并行累积给定数组的每个元素。相当于并行的reduce操作parallelSetAll
:setAll的并行版本parallelSort
:sort的并行版本toString、equals、hashCode
:共用基础方法,不多说。此外这三个方法还有对应的deep系列,分别是deepToString、deepEquals、deepHashCode
这其中要重点说一下数组拷贝,其实Java中数组拷贝最终都是调用System.arraycopy()
方法,这个方法的性能是非常高的,因为它是直接对内存进行复制。所以关于数组复制尽量调用此方法。
需要注意的是System.arraycopy()
属于浅复制,也就是复制对象和二维数组的时候复制的是引用,修改复制后的对象会影响到原始对象。
数组特点:
- 访问速度快。插入和查询时间复杂度是O(1)
- 初始化之后长度不可变,不支持动态扩容
# 2、List
List
是有序的集合,允许重复元素,可以通过索引访问元素。为了解决数组长度固定不能动态扩容的问题,Java提供了List
接口及其实现类。
# 2.1、主要实现类对比
实现类 | 底层结构 | 随机访问 | 插入/删除 | 内存占用 | 线程安全 |
---|---|---|---|---|---|
ArrayList | 动态数组 | O(1) | O(n) | 低 | 否 |
LinkedList | 双向链表 | O(n) | O(1) | 高 | 否 |
Vector | 动态数组 | O(1) | O(n) | 低 | 是 |
CopyOnWriteArrayList | 动态数组 | O(1) | O(n) | 高 | 是 |
# 2.2、ArrayList详解
ArrayList
是最常用的List
实现类,底层使用动态数组实现:
// 源码核心字段
transient Object[] elementData; // 存储元素的数组
private int size; // 实际元素个数
// 默认初始容量
private static final int DEFAULT_CAPACITY = 10;
扩容机制:
- 默认初始容量为10
- 扩容时增长为原来的1.5倍:
newCapacity = oldCapacity + (oldCapacity >> 1)
- 最大容量为
Integer.MAX_VALUE - 8
性能特点:
- 随机访问快:通过索引直接访问,时间复杂度
O(1)
- 尾部插入快:时间复杂度
O(1)
(不考虑扩容) - 中间插入慢:需要移动元素,时间复杂度
O(n)
- 内存局部性好:元素连续存储,缓存友好
# 2.3、LinkedList详解
LinkedList
使用双向链表实现,同时实现了List
和Deque
接口:
// 源码核心结构
private static class Node<E> {
E item;
Node<E> next;
Node<E> prev;
}
transient int size = 0;
transient Node<E> first; // 头节点
transient Node<E> last; // 尾节点
性能特点:
- 头尾插入快:时间复杂度
O(1)
- 中间插入快:找到位置后
O(1)
,但查找位置需要O(n)
- 随机访问慢:需要遍历,时间复杂度
O(n)
- 内存占用大:每个元素需要额外的前后指针
# 2.4、vs LinkedList选择指南
使用ArrayList的场景:
- 需要频繁随机访问元素
- 数据量可预估,避免频繁扩容
- 需要遍历整个集合
- 对内存占用敏感
使用LinkedList的场景:
- 频繁在头部或尾部插入/删除
- 需要实现队列或栈的功能
- 很少需要随机访问元素
- 数据量动态变化较大
初始化:
List<Integer> list = new ArrayList<>();
list.add(2);
list.add(3);
// java9之后。of方法返回的是不可变List
List<Integer> list = List.of(2,3)
List继承自Collection接口,Collection继承自Iterable接口。
Iterable中的方法:
iterator
:迭代器。有了他我们就可以使用foreach循环了forEach
:java8之后新增方法,lambda式的forEachspliterator
:拆分器
Collection中的方法:
size
:获得集合中的元素数isEmpty
:此集合是否不包含任何元素contains
:是否包含指定元素toArray
:转换为array数组。toArray默认返回时Object数组,但他有两个重载方法,可以返回指定类型的数组
String[] arr = list.toArray(new String[]{});
String[] arr2 = list.toArray(i->new String[]{});
add
:添加元素remove
:删除元素containsAll
:是否包含指定的所有元素addAll
:添加指定的所有元素removeAll
:删除指定的所有元素removeIf
:删除满足条件的元素retainAll
:保留指定的所有元素,其他元素则删除clear
:清空集合stream
:获得stream流parallelStream
:获得并行stream流
List中的方法:
addAll(i,col)
:指定位置添加元素replaceAll
:按照指定规则替换所有元素sort
:根据Comparator进行排序。在java8中使用lambda表达式特别方便
// 对于基本类型
list.sort(String::compareTo);
//对于Comparator<String> compare = String::compareTo;你可能会比较迷惑,其实他的转换过程是这样的
BiFunction<String, String, Integer> bf = String::compareTo;
Comparator<String> compare = (Comparator<String>) bf; // 可以看到Comparator函数接口的结果和BiFunction是相似的,java做了简化处理
// 对于Bean
list.sort(Comparator.comparing(TUser::getId))
get
:获得指定位置元素set
:设置指定位置元素add(i,e)
:添加指定位置元素。会导致原先此位置的元素后移remove(i)
:删除指定位置元素。如果List中泛型为Integer,这里会和Collection中的remove有一个冲突,具体这么做
// 调用的是Collection.remove
intList.remove(Integer.valueOf(2));
// 调用的是List.remove
intList.remove(5);
indexOf
:指定元素第一次出现时的索引lastIndexOf
:指定元素最后一次出现时的索引listIterator
:获得ListIterator,支持在迭代期间修改列表subList
:返回指定位置范围的视图
List还有两个静态方法:
of
:java9之后支持。获得不可修改的列表copyOf
:java11之后支持。拷贝一个不可修改的列表
# 3、Set
Set
是不包含重复元素的集合,主要用于去重和判断元素是否存在。继承自Collection
接口。
# 3.1、Set实现类对比
实现类 | 底层结构 | 是否有序 | 查找性能 | 插入性能 | 空值支持 |
---|---|---|---|---|---|
HashSet | HashMap | 无序 | O(1) | O(1) | 支持一个null |
LinkedHashSet | LinkedHashMap | 插入顺序 | O(1) | O(1) | 支持一个null |
TreeSet | 红黑树 | 自然顺序/比较器顺序 | O(log n) | O(log n) | 不支持null |
EnumSet | 位向量 | 枚举定义顺序 | O(1) | O(1) | 不支持null |
# 3.2、HashSet实现原理
HashSet
底层使用HashMap
实现,元素存储在HashMap
的key中,value使用一个固定的PRESENT
对象:
// HashSet源码片段
private transient HashMap<E,Object> map;
private static final Object PRESENT = new Object();
public boolean add(E e) {
return map.put(e, PRESENT) == null;
}
public boolean contains(Object o) {
return map.containsKey(o);
}
去重原理:
- 当添加元素时,先计算元素的
hashCode()
确定存储位置 - 如果该位置已有元素,调用
equals()
方法判断是否相同 - 相同则不添加(去重),不同则通过链表或红黑树解决冲突
注意事项:
- 自定义对象必须重写
hashCode()
和equals()
方法 hashCode()
相同,equals()
不一定相同equals()
相同,hashCode()
必须相同
# 3.3、LinkedHashSet特点
LinkedHashSet
继承自HashSet
,底层使用LinkedHashMap
实现:
- 维护插入顺序:通过双向链表记录元素插入顺序
- 性能略低于
HashSet
:需要维护链表指针 - 适用场景:需要去重且保持插入顺序
# 3.4、TreeSet特点
TreeSet
基于红黑树实现,元素自动排序:
// 自然排序(元素需实现Comparable接口)
TreeSet<Integer> set1 = new TreeSet<>();
// 自定义排序(提供Comparator)
TreeSet<Student> set2 = new TreeSet<>((s1, s2) ->
s1.getScore() - s2.getScore()
);
特有方法:
first()
/last()
:获取第一个/最后一个元素ceiling(E e)
/floor(E e)
:获取大于等于/小于等于给定元素的最小/最大元素higher(E e)
/lower(E e)
:获取严格大于/小于给定元素的最小/最大元素subSet(E from, E to)
:获取子集视图
初始化:
// 方式1
Set<String> set = new HashSet<>();
set.add("a");
set.add("b");
// 方式2。返回不可变Set
Set<Integer> integers = Set.of(1, 2);
HashSet和LinkedHashSet没有自己独立的方法,参考Collection接口即可。
Set接口有of和copyOf方法,功能同List。
TreeSet继承自NavigableSet,NavigableSet继承自SortedSet。我们来看看他们其中的方法。
SortedSet提供了对元素排序的功能,其中的方法有:
subSet
:返回此集合部分的视图,其元素范围从fromElement (包括)到toElement (不包括)headSet
:返回此集合中元素严格小于toElement的部分的视图tailSet
:返回此集合中元素大于或等于fromElement的部分的视图first
:第一个元素last
:最后一个元素
NavigableSet是对SortedSet的导航扩展,报告给定搜索目标的最接近匹配。参考NavigableSet Api (opens new window)
Set和List都继承自Collection接口,JDK提供了Collections工具类,其中有一些常用的方法:
sort
:自然排序binarySearch
:二分搜索reverse
:反转集合顺序shuffle
:打乱顺序(洗牌)swap
:根据下标交换指定元素的位置copy
:列表拷贝min
:根据排序规则,取得最小值max
:根据排序规则,取得最大值rotate
:旋转,元素的位置将要被重新调整为(i - distance) mod list.size()。用于快速元素移动很有效
List<Integer> list = Lists.newArrayList(1, 2, 3, 4);
// 将元素整体后移一位
Collections.rotate(list, 1); // [4, 1, 2, 3]
// 将元素整体前移一位
Collections.rotate(list, -1); // [2, 3, 4, 1]
// 仅移动部分区间数据
Collections.rotate(list.subList(1,3),1); // [1, 3, 2, 4]
replaceAll
:替换集合中旧值为新值indexOfSubList
:列表级别的indexOfunmodifiableCollection
:变为不可修改集合。还有系列方法:unmodifiableSet、unmodifiableSortedSet、unmodifiableNavigableSet、unmodifiableMap等synchronizedCollection
:变为现场安全的集合。系统方法同上。Java1.5之后新增了同步容器ConcurrentMap等,所以不再推荐使用此系列方法checkedCollection
:返回指定集合的动态类型安全视图,只允许插入指定的类型。系列方法同上emptyIterator
:返回一个没有元素的迭代器。还有系列方法:emptySet、emptyList、emptyMap等singleton
:返回一个只包含指定对象的不可变集合。还有系列方法:singletonList、singletonMapnCopies
:n个对象副本组成的不可变列表reverseOrder
:排序辅助方法,返回逆序的Comparatorenumeration
:返回集合的Enumeration对象list
:Enumeration对象转换为List对象frequency
:返回指定集合中等于指定对象的元素数,其实可以叫getCountdisjoint
:两集合没有共同的元素,则返回true
# 4、Map
Map
是键值对集合,每个键最多映射到一个值。它不是Collection
接口的子接口,是一个独立的接口体系。
# 4.1、Map实现类对比
实现类 | 底层结构 | 是否有序 | 查找性能 | null支持 | 线程安全 | 扩容机制 |
---|---|---|---|---|---|---|
HashMap | 数组+链表+红黑树 | 无序 | O(1) | K和V都支持 | 否 | 2倍扩容 |
LinkedHashMap | HashMap+双向链表 | 插入/访问顺序 | O(1) | K和V都支持 | 否 | 2倍扩容 |
TreeMap | 红黑树 | 自然/比较器顺序 | O(log n) | K不支持,V支持 | 否 | 无需扩容 |
Hashtable | 数组+链表 | 无序 | O(1) | K和V都不支持 | 是 | 2倍+1 |
ConcurrentHashMap | 数组+链表+红黑树 | 无序 | O(1) | K和V都不支持 | 是 | 2倍扩容 |
# 4.2、HashMap深度解析
核心参数:
// 默认初始容量:16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
// 最大容量:2^30
static final int MAXIMUM_CAPACITY = 1 << 30;
// 默认负载因子:0.75
static final float DEFAULT_LOAD_FACTOR = 0.75f;
// 链表转红黑树阈值:8
static final int TREEIFY_THRESHOLD = 8;
// 红黑树转链表阈值:6
static final int UNTREEIFY_THRESHOLD = 6;
// 最小树化容量:64
static final int MIN_TREEIFY_CAPACITY = 64;
数据结构演进:
- JDK 1.7:数组 + 链表
- JDK 1.8+:数组 + 链表 + 红黑树
put操作流程:
- 计算key的hash值:
(key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16)
- 确定数组索引:
(n - 1) & hash
- 如果该位置为空,直接插入
- 如果不为空,判断是否key相同(hash相同且equals相同)
- 相同则替换value,不同则:
- 链表长度 < 8:插入链表尾部
- 链表长度 >= 8 且数组长度 >= 64:转为红黑树
- 检查是否需要扩容(size > threshold)
扩容机制:
// 扩容条件
if (++size > threshold)
resize();
// threshold = capacity * loadFactor
// 默认:16 * 0.75 = 12
性能优化建议:
- 预估容量,避免频繁扩容:
new HashMap<>((int)(expectedSize / 0.75f + 1))
- 自定义对象作为key时,必须重写
hashCode()
和equals()
- 尽量使用不可变对象作为key(如String、Integer)
- 合理设置负载因子:默认0.75是时间和空间的平衡
# 4.3、LinkedHashMap特性
LinkedHashMap
继承自HashMap
,维护插入顺序或访问顺序:
// 默认维护插入顺序
LinkedHashMap<String, Integer> insertOrder = new LinkedHashMap<>();
// 维护访问顺序(LRU实现基础)
LinkedHashMap<String, Integer> accessOrder = new LinkedHashMap<>(16, 0.75f, true);
// 实现简单的LRU缓存
public class LRUCache<K, V> extends LinkedHashMap<K, V> {
private final int maxSize;
public LRUCache(int maxSize) {
super(16, 0.75f, true);
this.maxSize = maxSize;
}
@Override
protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
return size() > maxSize;
}
}
# 4.4、TreeMap特性
TreeMap
基于红黑树实现,保证key有序:
// 自然排序
TreeMap<Integer, String> map1 = new TreeMap<>();
// 自定义排序
TreeMap<Student, Integer> map2 = new TreeMap<>((s1, s2) ->
s1.getName().compareTo(s2.getName())
);
// 特有的导航方法
map1.firstEntry(); // 第一个键值对
map1.lastEntry(); // 最后一个键值对
map1.ceilingEntry(5); // >= 5的最小键值对
map1.floorEntry(5); // <= 5的最大键值对
map1.subMap(1, 10); // 获取子Map [1, 10)
Map接口有of和copyOf方法,功能同List。此外还有两个静态方法:entry和ofEntries。
初始化:
// 方式1
Map<String, Integer> map = new HashMap<>();
map.put("a",1);
map.put("b",2);
// 方式2。返回不可变Map
Map<Integer, Integer> integerMap = Map.of(1, 2);
// 方式3。返回不可变Map
Map<String, Integer> stringIntegerMap = Map.ofEntries(Map.entry("a", 1), Map.entry("b", 2));
Map接口的方法:
size
:键值数量isEmpty
:是否为空字典containsKey
:是否包含指定键containsValue
:是否包含指定值get
:根据键获得值put
:添加键值remove
:删除键值putAll
:批量添加clear
:清空键值字典keySet
:获得键Setvalues
:获得值集合entrySet
:获得键值对Set,泛型为Map.EntrygetOrDefault
:安全的get,没有值则返回默认值forEach
:foreach的lambda版本replaceAll
:替换匹配的所有元素putIfAbsent
:仅当键不存在时,才会执行给定的函数来计算新的值,并用这个新值插入到 Map 中replace
:替换computeIfAbsent
:putIfAbsent的Function版本computeIfPresent
:仅当键存在时,才会执行给定的函数来计算新的值,并用这个新值替换原来的值。compute
:无论键是否存在,都会执行给定的函数来计算新的值,并用这个新值替换原来的值。merge
:如果键存在,则使用提供的 BiFunction 函数来合并旧值和新值;如果键不存在,则直接将新值插入到 Map 中。
后四个方法为Java 8中新增的,有必要演示一下:
// 基础数据
Map<String, Object> map = new HashMap<>();
map.put("name", "Jack");
map.put("age", 25);
// computeIfAbsent:name不会被插入成功。因为已经存在了name键
System.out.println(map.computeIfAbsent("name", k -> k + " random")); // 返回值为"Jack"
System.out.println(map); // map:{name=Jack, age=25}
// 插入一个新的键值对
System.out.println(map.computeIfAbsent("score", k -> 90)); // 返回值为90
System.out.println(map); // map:{name=Jack, age=25, score=90}
// computeIfPresent:name会被插入成功,会替换掉旧值
System.out.println(map.computeIfPresent("name", (k, v) -> k + " random")); // 返回值为"name random"
System.out.println(map); // map:{name=name random, age=25}
// 按照你想要的规则进行替换
System.out.println(map.computeIfPresent("name", (k, v) -> v + " random")); // {name=Jack random, age=25}
System.out.println(map);
System.out.println(map.computeIfPresent("name", (k, v) -> k + v)); // {name=name Jack random, age=25}
System.out.println(map);
// 替换新值为null的话键值会被删除
System.out.println(map.computeIfPresent("name", (k, v) -> null)); // 返回值为null
System.out.println(map); // map:{age=25}
// 插入一个不存在的键值会失败
System.out.println(map.computeIfPresent("score", (k, v) -> 90)); // 返回值为null
System.out.println(map); // map:{age=25, score=90}
// compute:可定制的计算,computeIfPresent的增强版,可以获得现有的k,v对齐进行操作
System.out.println(map.compute("age", (k, v) -> (Integer) v + 1)); // 返回值为26
System.out.println(map); // map:{age=26, score=90}
// merge:合并。如果存在旧值,则使用BiFunction进行替换;否则赋予新值
System.out.println(map.merge("score", 80, (oldValue, newValue) -> (Integer) oldValue + newValue)); // 返回值为170
System.out.println(map); // map:{age=26, score=170}
// 合并一个不存在的键值对
System.out.println(map.merge("height", 180, (oldValue, newValue) -> (Integer) oldValue + newValue)); // 返回值为180
System.out.println(map); // map:{age=26, score=170, height=180}
HashMap和LinkedHashMap没有自己独有的方法,只是实现了Map接口。
TreeMap还实现了NavigableMap->SortedMap,参考NavigableMap Api (opens new window)
# 5、其他
Queue接口定义了队列,有offer、poll、peek等队列专有方法。主要实现类有PriorityQueue(优先级队列)。
Deque继承自Queue,定义了双端队列,有offer/poll/peek-First/Last等系列方法,主要实现类是ArrayDeque、LinkedList。
Stack是Java中栈的实现类,有push、pop、peek等专有方法。
# 二、Commons Collection集合
Apache Commons系列工具包是Java开发中常用的工具包,其中关于集合方面的扩展要使用到commons-collections4:
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-collections4</artifactId>
<version>4.4</version>
</dependency>
此工具工具包主要提供了三方面的扩展
# 1、工具类
主要是SetUtils、ListUtils、CollectionUtils、MapUtils。这些工具类有一些方法在Java8之后有点过时了,比如:
List<Integer> list = List.of(1, 2, 3, 4);
// select方法
List<Integer> select = ListUtils.select(list, e -> e > 2);
// java8之后这么做
select = list.stream().filter(e->e>2).collect(Collectors.toList());
其他诸如filter、transform、getCardinalityMap等方法,用Stream操作会更简单一些。
下面列出一些提高效率的便利方法:
subtract
:取得差集disjunction
:获得彼此差集之后的并集intersection
:取得交集union
:取得并集containsAny
:包含任何permutations
:返回列表的所有排列组合,大小是列表大小的阶乘级别,注意性能和内存问题
# 2、容器类
Bag 会记录对象在集合中出现的次数,用法:
Bag bag = new HashBag();
bag.add("ONE", 6); // 添加6次
bag.remove("ONE", 2); // 删除2次
bag.getCount("ONE"); // 返回4
主要实现类有HashBag和TreeBag(自然排序)
List的扩展,主要有如下类:
CursorableLinkedList
:提供了可修改的迭代器。在JDK的List.listIterator中提供了类似功能FixedSizeList
:固定大小的List,add、remove、clear和retain操作是不被支持的,set方法是允许的但是不会影响列表大小GrowthList
:可以使其在因set或add操作造成索引超出异常时无缝的增加列表长度,可以避免大多数的IndexOutOfBoundsExceptionLazyList
:可以自动增长,但只发生在get时--允许指定一个Factory获得超出索引位置的值PredicatedList
:添加元素时进行校验SetUniqueList
:不允许重复元素的列表TransformedList
:在set和add时对元素进行转换后再添加TreeList
:实现了一个快速插入、删除,同时查询性能也可观的List
看完之后,怎么说呢?这些类用的频率都不太高,但又弃之可惜。
Set的扩展有如下类:
CompositeSet
:多个Set的组合视图- 剩余的Predicated、Transformed、Unmodifiable和List一样,不赘言
Map的扩展有如下类:
CaseInsensitiveMap
:大小写不敏感的Map,会把键都转换为小写CompositeMap
:多个Map的组合视图DefaultedMap
:键不存在,返回默认对象FixedSizeMap
:固定大小的MapIdentityMap
:匹配键值不是通过==,而是equalsMultiKeyMap
:允许多个键关联到一个值上MultiValueMap
:允许多个值关联到一个键上BidiMap
:双向Map,可以通过值查找键。主要实现类是DualHashBidiMap、TreeBidiMapLRUMap
:固定大小Map,容器满了之后删除最近最少使用(Least Recently Used)的元素- LazyMap/PredicatedMap/TransformedMap/TypedMap/UnmodifiableMap,逻辑同List扩展一样,不赘言
# 3、辅助类
主要是Comparator、Predicate、Transformer等辅助类,这些辅助类在Java8 Stream面前都显得有些过时了,不赘言
# 三、Guava集合
如果说commons-collections4是质朴村花的话,那么Guava就是靓丽女神了。
Guava中最受欢迎的就是他的集合类。
# 1、不可变系列对象
比如ImmutableList、ImmutableSet,比如JDK内部的Collections.unmodifiable系列方法来说,具有如下优点:
- 线程安全
- 更加节省空间
需要注意的是,所有Immutable都拒绝空值。
使用:
ImmutableSet.of("red", "orange", "yellow")
Set<Bar> bars = ...
ImmutableSet.copyOf(bars)
ImmutableSet.<Color>builder().addAll(WEBSAFE_COLORS).add(new Color(0, 191, 255)).build();
# 2、新的集合类型
Multiset,定义一个集合,该集合计算对象在集合中出现的次数。
主要方法有add、remove、getCount,使用起来比较简单:
MultiSet<Object> multiSet = new HashMultiSet<>();
multiSet.add("name");
multiSet.add("name");
multiSet.add("name", 3);
multiSet.getCount("name"); // 5
Multimap,一个键可以关联多个值。
初始化:
ListMultimap<String, Integer> multimap1 = MultimapBuilder.hashKeys().arrayListValues().build();
SetMultimap<String, Integer> multimap2 = MultimapBuilder.treeKeys().hashSetValues().build();
使用:
multimap.put("type",1);
multimap.putAll("type", Lists.newArrayList(2,3));
List<Integer> types = multimap.get("type");
Map<String, Collection<Integer>> map = multimap.asMap();
BiMap,双向Map。在commons-collections4有类似实现
BiMap<String, Integer> userId = HashBiMap.create();
...
String userForId = userId.inverse().get(id);
Table,一个索引映射多个值,类似Map<FirstName, Map<LastName, Person>>
这样的结果
用法:
Table<String, String, Double> table = HashBasedTable.create();
table.put("cny", "usd", 0.235);
table.put("cny", "jpy", 8.523);
table.put("hkd", "jpy", 2.053);
table.get("cny", "usd"); // 0.235
table.row("cny"); //{usd=0.235, jpy=8.523}
table.column("jpy"); //{cny=8.523, hkd=2.053}
table.rowKeySet(); //cny, hkd
RangeSet,包含范围的集合。
用法:
RangeSet<Integer> rangeSet = TreeRangeSet.create();
rangeSet.add(Range.closed(1, 10)); // {[1, 10]}
rangeSet.add(Range.closedOpen(11, 15)); // disconnected range: {[1, 10], [11, 15)}
rangeSet.add(Range.closedOpen(15, 20)); // connected range; {[1, 10], [11, 20)}
rangeSet.add(Range.openClosed(0, 0)); // empty range; {[1, 10], [11, 20)}
rangeSet.remove(Range.open(5, 10)); // splits [1, 10]; {[1, 5], [10, 10], [11, 20)}
Set<Range<Integer>> ranges = rangeSet.asRanges();
RangeMap,包含范围的Map。与RangeSet不同的是它不合并相邻映射。
用法:
RangeMap<Integer, String> rangeMap = TreeRangeMap.create();
rangeMap.put(Range.closed(1, 10), "foo"); // {[1, 10] => "foo"}
rangeMap.put(Range.open(3, 6), "bar"); // {[1, 3] => "foo", (3, 6) => "bar", [6, 10] => "foo"}
rangeMap.put(Range.open(10, 20), "foo"); // {[1, 3] => "foo", (3, 6) => "bar", [6, 10] => "foo", (10, 20) => "foo"}
rangeMap.remove(Range.closed(5, 11)); // {[1, 3] => "foo", (3, 5) => "bar", (11, 20) => "foo"}
# 3、实用工具类
比如Lists、Sets等,你可能想象不到,Guava中使用频率最高的是如下代码:
List<Integer> integers = Lists.newArrayList(2, 3, 4);
不由得令人感慨,偷懒是最高生产力。
这些工具类中方法众多,具体参考:Guava 集合工具扩展 (opens new window)
这里列出一些使用频率较高,令人眼前一亮的工具扩展:
Lists.partition
:分割列表。返回连续子列表,每个子列表大小系统(最后一个可能更小)Sets.subSet
:提取范围内的子视图Maps.subMap
:提取范围内的子视图
# 四、并发集合
前面说道不推荐使用Collections.synchronized系列方法,原因是它内部只是提供了一个简单的互斥操作,在高并发情况下,并不十分高效。
Java 5之后利用CAS指令(乐观锁)提供了更高效的并发容器,它们分别是:
ConcurrentLinkedQueue
:高并发环境下性能最好的队列CopyOnWriteArrayList
:只在写时加锁的ArrayList,采用写时复制的思想,读性能大幅提升BlockingQueue
:一个阻塞式的数据共享通道,参考:阻塞队列BlockingQueue。他有三个主要实现:ArrayBlockingQueue
:数组支持的有界队列LinkedBlockingQueue
:链表支持的可选有界队列LinkedBlockingDeque
:链表支持的双向可选有界队列
ConcurrentMap
:线程安全的map,主要实现有:ConcurrentHashMap
:高并发哈希表ConcurrentSkipListMap
:跳表,类似LinkedHashMap,提供有序的遍历。类似于平衡树,特点是插入和删除只需要对局部数据进行操作即可。平衡树的话需要全局锁,而跳表只需要部分锁。
# 五、Redission分布式集合
缓存作为提升性能和体验的必要手段,使用非常广泛。
在同一个JVM内部,使用ConcurrentMap作为缓存是常见手段。
在分布式环境中,可能需要集中式缓存服务,目前比较流行的是Redis,它提供了多种数据结果供我们使用。
Redission作为Redis客户端库,更进一步的把原生Redis数据结构封装成了Java大家熟悉的集合容器。
以Map举例:
RMap<String, SomeObject> map = redisson.getMap("anyMap");
SomeObject prevObject = map.put("123", new SomeObject());
SomeObject currentObject = map.putIfAbsent("323", new SomeObject());
SomeObject obj = map.remove("123");
RFuture<SomeObject> putAsyncFuture = map.putAsync("321");
它提供了同步和异步的添加和删除方法。
Redission还对分布式环境下常用操作做了一些封装,这已经超出了本章要讨论的内容,参考:Redisson项目介绍 (opens new window)
# 六、集合选择指南
# 1、如何选择合适的集合
根据不同的使用场景,选择合适的集合类型:
场景 | 推荐集合 | 原因 |
---|---|---|
需要有序且允许重复 | ArrayList | 索引访问快,内存占用小 |
频繁插入/删除 | LinkedList | 插入删除效率高 |
需要去重 | HashSet | 基于hash,查找快 |
去重且保持顺序 | LinkedHashSet | 维护插入顺序 |
需要排序 | TreeSet /TreeMap | 自动排序 |
键值对存储 | HashMap | 最常用,性能好 |
线程安全 | ConcurrentHashMap | 高并发性能好 |
缓存实现 | LinkedHashMap | 可实现LRU |
# 2、性能对比总结
# 2.1、时间复杂度对比
操作 | ArrayList | LinkedList | HashSet | TreeSet | HashMap | TreeMap |
---|---|---|---|---|---|---|
添加 | O(1) * | O(1) | O(1) | O(log n) | O(1) | O(log n) |
删除 | O(n) | O(1) | O(1) | O(log n) | O(1) | O(log n) |
查找 | O(n) | O(n) | O(1) | O(log n) | O(1) | O(log n) |
获取(索引) | O(1) | O(n) | - | - | - | - |
*注:ArrayList添加可能触发扩容,最坏情况O(n)
# 七、最佳实践
# 1、初始化容量
预估集合大小,避免频繁扩容:
// ArrayList
List<String> list = new ArrayList<>(100);
// HashMap - 考虑负载因子
Map<String, Object> map = new HashMap<>((int)(expectedSize / 0.75f + 1));
// 不要这样做 - 会导致多次扩容
List<String> badList = new ArrayList<>(); // 默认容量10
for(int i = 0; i < 1000; i++) {
badList.add("item" + i); // 扩容多次
}
# 2、使用合适的遍历方式
// ArrayList - 使用索引遍历最快
for (int i = 0; i < list.size(); i++) {
String item = list.get(i);
}
// LinkedList - 使用迭代器避免O(n²)
for (String item : linkedList) { // 使用迭代器
// 处理item
}
// Map - 遍历entrySet而不是keySet
for (Map.Entry<String, Object> entry : map.entrySet()) {
String key = entry.getKey();
Object value = entry.getValue();
}
# 3、正确实现equals和hashCode
public class Person {
private String id;
private String name;
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Person person = (Person) o;
return Objects.equals(id, person.id);
}
@Override
public int hashCode() {
return Objects.hash(id);
}
}
# 4、避免在循环中修改集合
// 错误方式 - 会抛出ConcurrentModificationException
for (String item : list) {
if (item.equals("remove")) {
list.remove(item); // 错误!
}
}
// 正确方式1 - 使用迭代器
Iterator<String> it = list.iterator();
while (it.hasNext()) {
if (it.next().equals("remove")) {
it.remove();
}
}
// 正确方式2 - 使用removeIf (Java 8+)
list.removeIf(item -> item.equals("remove"));
# 5、选择合适的并发集合
// 低并发场景
Collections.synchronizedList(new ArrayList<>());
// 高并发场景
ConcurrentHashMap<String, Object> map = new ConcurrentHashMap<>();
CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();
// 生产者-消费者模式
BlockingQueue<Task> queue = new LinkedBlockingQueue<>();
# 八、常见面试题
# 1、ArrayList和LinkedList的区别?
- 底层结构:ArrayList基于动态数组,LinkedList基于双向链表
- 随机访问:ArrayList支持
O(1)
,LinkedList需要O(n)
- 插入删除:ArrayList中间位置
O(n)
,LinkedList头尾O(1)
- 内存占用:LinkedList每个节点需要额外的前后指针
- 缓存局部性:ArrayList更好,元素连续存储
# 2、HashMap的工作原理?
- 基于数组+链表+红黑树(JDK 1.8+)
- 通过key的hashCode()计算hash值
- 通过
(n-1) & hash
确定数组位置 - 解决冲突:链表法,链表长度>=8且数组长度>=64时转红黑树
- 扩容:当
size > threshold
时,容量翻倍,重新计算位置
# 3、如何保证集合线程安全?
- 使用
Collections.synchronizedXXX()
方法 - 使用并发集合:
ConcurrentHashMap
、CopyOnWriteArrayList
等 - 使用读写锁:
ReentrantReadWriteLock
- 使用不可变集合:
ImmutableList
、ImmutableMap
等
# 4、fail-fast和fail-safe的区别?
- fail-fast:快速失败,检测到并发修改立即抛出
ConcurrentModificationException
- 例如:
ArrayList
、HashMap
- 例如:
- fail-safe:安全失败,在副本上操作,不会抛出异常
- 例如:
CopyOnWriteArrayList
、ConcurrentHashMap
- 例如:
# 5、为什么HashMap的容量是2的幂次方?
- 位运算效率高:
(n-1) & hash
比取模运算快 - 均匀分布:2的幂次方-1的二进制全是1,与运算后分布更均匀
- 扩容优化:扩容时元素要么在原位置,要么在原位置+旧容量位置
# 九、总结
本文全面深入地讲解了Java集合框架,包括JDK内部集合、Apache Commons Collection、Guava集合、并发集合和Redisson分布式集合。掌握这些知识点对于Java开发至关重要。
关键要点:
- 理解各集合的底层实现原理和适用场景
- 根据实际需求选择合适的集合类型
- 注意性能优化和线程安全问题
- 遵循最佳实践,编写高质量代码
学习集合不仅要了解API使用,更要理解其设计思想和实现原理。通过源码阅读和实践应用,逐步提升对集合框架的掌握程度。
祝你变得更强!