轩辕李的博客 轩辕李的博客
首页
  • Java
  • Spring
  • 其他语言
  • 工具
  • HTML&CSS
  • JavaScript
  • 分布式
  • 代码质量管理
  • 基础
  • 操作系统
  • 计算机网络
  • 编程范式
  • 安全
  • 中间件
  • 心得
关于
  • 分类
  • 标签
  • 归档
GitHub (opens new window)

轩辕李

勇猛精进,星辰大海
首页
  • Java
  • Spring
  • 其他语言
  • 工具
  • HTML&CSS
  • JavaScript
  • 分布式
  • 代码质量管理
  • 基础
  • 操作系统
  • 计算机网络
  • 编程范式
  • 安全
  • 中间件
  • 心得
关于
  • 分类
  • 标签
  • 归档
GitHub (opens new window)
  • Java

    • 核心

      • Java8--Lambda 表达式、Stream 和时间 API
      • Java集合
        • 一、JDK内部集合
          • 1、数组
          • 2、List
          • 2.1、主要实现类对比
          • 2.2、ArrayList详解
          • 2.3、LinkedList详解
          • 2.4、vs LinkedList选择指南
          • 3、Set
          • 3.1、Set实现类对比
          • 3.2、HashSet实现原理
          • 3.3、LinkedHashSet特点
          • 3.4、TreeSet特点
          • 4、Map
          • 4.1、Map实现类对比
          • 4.2、HashMap深度解析
          • 4.3、LinkedHashMap特性
          • 4.4、TreeMap特性
          • 5、其他
        • 二、Commons Collection集合
          • 1、工具类
          • 2、容器类
          • 3、辅助类
        • 三、Guava集合
          • 1、不可变系列对象
          • 2、新的集合类型
          • 3、实用工具类
        • 四、并发集合
        • 五、Redission分布式集合
        • 六、集合选择指南
          • 1、如何选择合适的集合
          • 2、性能对比总结
          • 2.1、时间复杂度对比
        • 七、最佳实践
          • 1、初始化容量
          • 2、使用合适的遍历方式
          • 3、正确实现equals和hashCode
          • 4、避免在循环中修改集合
          • 5、选择合适的并发集合
        • 八、常见面试题
          • 1、ArrayList和LinkedList的区别?
          • 2、HashMap的工作原理?
          • 3、如何保证集合线程安全?
          • 4、fail-fast和fail-safe的区别?
          • 5、为什么HashMap的容量是2的幂次方?
        • 九、总结
      • Java IO
      • Java 文件操作
      • Java 网络编程
      • Java运行期动态能力
      • Java可插入注解处理器
      • Java基准测试(JMH)
      • Java性能分析(Profiler)
      • Java调试(JDI与JDWP)
      • Java管理与监控(JMX)
      • Java加密体系(JCA)
      • Java服务发现(SPI)
      • Java随机数生成研究
      • Java数据库连接(JDBC)
      • Java历代版本新特性
      • 写好Java Doc
      • 聊聊classpath及其资源获取
    • 并发

    • 经验

    • JVM

    • 企业应用

  • Spring

  • 其他语言

  • 工具

  • 后端
  • Java
  • 核心
轩辕李
2021-08-06
目录

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类似,不过指定值为自定义Function
  • sort:升序排序
  • 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式的forEach
  • spliterator:拆分器

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);
}

去重原理:

  1. 当添加元素时,先计算元素的hashCode()确定存储位置
  2. 如果该位置已有元素,调用equals()方法判断是否相同
  3. 相同则不添加(去重),不同则通过链表或红黑树解决冲突

注意事项:

  • 自定义对象必须重写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:列表级别的indexOf
  • unmodifiableCollection:变为不可修改集合。还有系列方法:unmodifiableSet、unmodifiableSortedSet、unmodifiableNavigableSet、unmodifiableMap等
  • synchronizedCollection:变为现场安全的集合。系统方法同上。Java1.5之后新增了同步容器ConcurrentMap等,所以不再推荐使用此系列方法
  • checkedCollection:返回指定集合的动态类型安全视图,只允许插入指定的类型。系列方法同上
  • emptyIterator:返回一个没有元素的迭代器。还有系列方法:emptySet、emptyList、emptyMap等
  • singleton:返回一个只包含指定对象的不可变集合。还有系列方法:singletonList、singletonMap
  • nCopies:n个对象副本组成的不可变列表
  • reverseOrder:排序辅助方法,返回逆序的Comparator
  • enumeration:返回集合的Enumeration对象
  • list:Enumeration对象转换为List对象
  • frequency:返回指定集合中等于指定对象的元素数,其实可以叫getCount
  • disjoint:两集合没有共同的元素,则返回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操作流程:

  1. 计算key的hash值:(key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16)
  2. 确定数组索引:(n - 1) & hash
  3. 如果该位置为空,直接插入
  4. 如果不为空,判断是否key相同(hash相同且equals相同)
  5. 相同则替换value,不同则:
    • 链表长度 < 8:插入链表尾部
    • 链表长度 >= 8 且数组长度 >= 64:转为红黑树
  6. 检查是否需要扩容(size > threshold)

扩容机制:

// 扩容条件
if (++size > threshold)
    resize();

// threshold = capacity * loadFactor
// 默认:16 * 0.75 = 12

性能优化建议:

  1. 预估容量,避免频繁扩容:new HashMap<>((int)(expectedSize / 0.75f + 1))
  2. 自定义对象作为key时,必须重写hashCode()和equals()
  3. 尽量使用不可变对象作为key(如String、Integer)
  4. 合理设置负载因子:默认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:获得键Set
  • values:获得值集合
  • entrySet:获得键值对Set,泛型为Map.Entry
  • getOrDefault:安全的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操作造成索引超出异常时无缝的增加列表长度,可以避免大多数的IndexOutOfBoundsException
  • LazyList:可以自动增长,但只发生在get时--允许指定一个Factory获得超出索引位置的值
  • PredicatedList:添加元素时进行校验
  • SetUniqueList:不允许重复元素的列表
  • TransformedList:在set和add时对元素进行转换后再添加
  • TreeList:实现了一个快速插入、删除,同时查询性能也可观的List

看完之后,怎么说呢?这些类用的频率都不太高,但又弃之可惜。


Set的扩展有如下类:

  • CompositeSet:多个Set的组合视图
  • 剩余的Predicated、Transformed、Unmodifiable和List一样,不赘言

Map的扩展有如下类:

  • CaseInsensitiveMap:大小写不敏感的Map,会把键都转换为小写
  • CompositeMap:多个Map的组合视图
  • DefaultedMap:键不存在,返回默认对象
  • FixedSizeMap:固定大小的Map
  • IdentityMap:匹配键值不是通过==,而是equals
  • MultiKeyMap:允许多个键关联到一个值上
  • MultiValueMap:允许多个值关联到一个键上
  • BidiMap:双向Map,可以通过值查找键。主要实现类是DualHashBidiMap、TreeBidiMap
  • LRUMap:固定大小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开发至关重要。

关键要点:

  1. 理解各集合的底层实现原理和适用场景
  2. 根据实际需求选择合适的集合类型
  3. 注意性能优化和线程安全问题
  4. 遵循最佳实践,编写高质量代码

学习集合不仅要了解API使用,更要理解其设计思想和实现原理。通过源码阅读和实践应用,逐步提升对集合框架的掌握程度。

祝你变得更强!

编辑 (opens new window)
#Java集合
上次更新: 2025/08/15
Java8--Lambda 表达式、Stream 和时间 API
Java IO

← Java8--Lambda 表达式、Stream 和时间 API Java IO→

最近更新
01
AI时代的编程心得
09-11
02
Claude Code与Codex的协同工作
09-01
03
Claude Code实战之供应商切换工具
08-18
更多文章>
Theme by Vdoing | Copyright © 2018-2025 京ICP备2021021832号-2 | MIT License
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式