一文帮你掌握集合类库常见工具方法

前言

原创不易,禁止转载。

本文旨在帮助读者建立 Java 集合类库相关工具方法的印象,有需要的读者可以收藏,待有具体需要时查阅。本文将侧重于容易被忽略的静态方法,同时会给出一些良好实践。本文讨论的集合类库指的是标准库和 Guava。由于 Apache Collections4 的设计水平一般且繁杂,不推荐使用。

原则

  1. 标准库优先
  2. Stream 操作优先
  3. Guava Immutable 类型优先,不可变对象可以高效利用内存,天然线程安全
  4. 可读性优先
  5. 性能好的优先
  6. 选择相关的方法时,应该确保已经选择了正确的类型或者接口
  7. 工具方法应该面向接口实现
  8. 集合类型优先于底层数组,当有性能要求时再使用底层数组
  9. 尽量不使用迭代器,迭代器可能有性能问题,常常需要维护状态信息,容易出 bug
  10. 原则有冲突时的选择看团队规范或者个人喜好

1. 创建

这里涉及的类型为最常见的 List、Map、Set、Queue、Deque、SortedMap、SortedSet、NavigableMap、NavigableSet

构造器规约:标准库支持空参数构造器创建,拷贝构造器创建:

java 复制代码
List<Integer> list = new ArrayList<>();
List<Integer> list2 = new ArrayList<>(list2);
Map<String, String> map = new HashMap<>();
Map<String, String> map2 = new HashMap<>(map);

可读性更好的静态工厂(Guava):

java 复制代码
List<Type> exactly100 = Lists.newArrayListWithCapacity(100);
List<Type> approx100 = Lists.newArrayListWithExpectedSize(100);
Set<Type> approx100Set = Sets.newHashSetWithExpectedSize(100);
// 尽量确保100条 entry 不会发生底层数组扩容
Map<String, String> approxMap = Maps.newHashMapWithExpectedSize(100);
// new HashMap(n) 中的 n 指的是预期底层数组的长度(创建时会适配成2的幂次方)

// Guava 引入的集合类型不支持通过构造器创建对象,需要使用静态工程
Multiset<String> multiset = HashMultiset.create();

空集合、单一原则集合创建:

ini 复制代码
Collections.emptyList();
Collections.singletonList(1);

重复元素集合创建:

arduino 复制代码
Collections.nCopies("element", 42);

不可修改对象(视图)创建:

scss 复制代码
// 注意需要自行防止对象共享修改
Collections.unmodifiableList(list);

从数组视图创建:

ini 复制代码
List<Integer> list = Arrays.asList(1, 2, 3);

Guava 不可变对象创建:

java 复制代码
// 1. 工厂方法(of)
List<Integer> list = ImmutableList.of(1, 2, 3);
// 2. 拷贝复制
List<Integer> list2 = ImmutabeList.copyOf(list);
// 3. 收集器创建, 相关方法在 ImmutableXXX 类中,请使用静态引入 import static
List<Integer> list3 = list.stream().collect(toImmutableList());
// 4. builder 创建
List<Integer> list4 = ImmutableList.<Integer>builder()
	.add(111)
	.addAll(list3)
	.build();

流式创建,使用 Collector:

java 复制代码
Stream.of(1, 2, 3).filter(x -> x > 1).collect(toList());

同步集合类型创建:

java 复制代码
// 返回线程安全的列表
Collections.synchronizedList(List<T> list)
// 返回线程安全的映射
Collections.synchronizedMap(Map<K, V> m)
// 返回线程安全的集合
Collections.synchronizedSet(Set<T> s)

2. 运算与查询

  • topK

可以使用 Guava Comparators 提供的 Collector,时间复杂度为o(k logk)。实现使用了快排中的partition 技巧。

java 复制代码
// Guava Comparators.least / largest
Stream.of("foo", "quux", "banana", "elephant")
    .collect(least(2, comparingInt(String::length)))
  • 根据属性去重(dictinctBy)。Stream#distinct + 借助Guava提供的 Equivalence 实现自定义相等 equals
java 复制代码
// record User(String name, int age) {}
Stream.of(new User("a", 1), new User("b", 1))
        .map(Equivalence.equals().onResultOf(User::age)::wrap)
        .distinct()
        .map(Equivalence.Wrapper::get)
        .collect(toList());
  • 无限循环

无限循环指的是迭代器无限重复,Guava Iterables 提供了cycle 方法,Stream 也可以实现,Guava的实现可以弃用了。

java 复制代码
Stream.generate(() -> collection).flatMap(Collection::stream)
  • 去重 Stream#distinct
  • 二分查找 binarySearch

3. 视图运算

  • 分区 Lists.partition
java 复制代码
List<Integer> list = ImmutableList.of(1, 2, 3, 4, 5);
List<List<Integer>> parts = Lists.partition(list, 3);
// [1, 2, 3], [4, 5]
  • 翻转

来自Guava Lists, reverse(List<T> list):List<T> ,返回结果为视图。对于不可变实现,其可以复用底层数组,时间复杂度为o(1);

  • 全排列、排序全排列 Collections2.permutations、orderedPermutations
  • 笛卡尔积 Sets.cartesianProduct Lists.
  • 集合转换(视图)
java 复制代码
// Deque -> Queue
Queue<String> q = Collections.asLifoQueue(deque);
// Map<Key, Boolean> -> Set
Set<String> set = Collections.newSetFromMap(new HashMap<String, Boolean>());
// Map -> keySet
Set<String> keySet = map.keySet();
// Multimap -> Map<K, Collection<V>>
Map<K, List<V>> m = Multimaps.asMap(multimap);

4. 其他集合

引入新的集合的目的是鼓励开发者使用这些抽象,面向接口编程,这些接口提供一些新的API,方便使用,可以减少 bug。举个例子,Go 语言竟然没有 Set 类型,开发者需要使用 map 辅助实现。

  • BiMap 双向Map,是 Map 的子接口,也是 Map
  • Multimap,多值Map,不是 Map 的子接口,不是 Map
  • Multiset,多值集合,不是 Set 的子接口,不是 Set,可以理解成 Map<XXX, Integer>
  • RangeMap 区间到值的映射
  • RangeSet 区间集合
  • Table,表格,可以理解成 Map<R,Map<C, V>>

有些集合可能被开发者忽略,如:

  • MinMaxPriorityQueue

Guava 提供了双端优先队列,可以双向多次获取当前的最大值、最小值。

  • ConcurrentHashSet

很多人不知道标准库其实提供了并发安全 Set。

java 复制代码
Set<String> mySet = ConcurrentHashMap.newKeySet(); // 视图

5. 数学意义上集合的运算

  • 交集

Sets.intersection(Set set1, Set set2)

{1, 2} ∩ {2, 3} = {2}

  • 并集

Sets.union(Set set1, Set set2)

{1, 2} ∪ {2, 3} = {1, 2, 3}

  • 差集

Sets.difference(Set set1, Set set2)

{1, 2} - {2, 3} = {1}

  • 对称差集

Sets.symmetricDifference(Set set1, Set set2)

{1, 2} △ {2, 3} = {1, 3}

  • 子集

NavigableSet#subSet,Guava 也可以支持计算 Range 内的子集

java 复制代码
// Guava Sets 方法签名
public static <**K** extends Comparable<? super **K**>> NavigableSet<**K**> subSet(NavigableSet<**K**> set**,** Range<**K**> range)
  • 补集

Sets.complementOf(Collection<E > collection**,** Class<E> type)

需要有全集,对于枚举类型适用

java 复制代码
// 方法签名 class Sets
public static <E extends Enum<E>> EnumSet<E> complementOf(Collection<E> collection, Class<E> type)
  • 幂集,即集合的所有子集 Sets.powerSet
vbnet 复制代码
Set<String> animals = ImmutableSet.of("gerbil", "hamster");
Set<String> fruits = ImmutableSet.of("apple", "orange", "banana");

Set<List<String>> product = Sets.cartesianProduct(animals, fruits);
// {{"gerbil", "apple"}, {"gerbil", "orange"}, {"gerbil", "banana"},
//  {"hamster", "apple"}, {"hamster", "orange"}, {"hamster", "banana"}}

Set<Set<String>> animalSets = Sets.powerSet(animals);
// {{}, {"gerbil"}, {"hamster"}, {"gerbil", "hamster"}}
  • 笛卡尔积

多个集合的笛卡尔积集合依然是集合。

java 复制代码
Sets.cartesianProduct(ImmutableList.of(
    ImmutableSet.of(1, 2),
    ImmutableSet.of("A", "B", "C")))
// ImmutableList.of(1, "A")
// ImmutableList.of(1, "B")
// ImmutableList.of(1, "C")
// ImmutableList.of(2, "A")
// ImmutableList.of(2, "B")
// ImmutableList.of(2, "C")
  • Map Entries

Guava 支持对 Map 条目(entry)进行类似的操作,如 Maps.difference(map1, map2) 返回 MapDifference。

java 复制代码
public interface MapDifference<K extends @Nullable Object, V extends @Nullable Object> {
  /**
   * Returns {@code true} if there are no differences between the two maps; that is, if the maps are
   * equal.
   */
  boolean areEqual();

  /**
   * Returns an unmodifiable map containing the entries from the left map whose keys are not present
   * in the right map.
   */
  Map<K, V> entriesOnlyOnLeft();

  /**
   * Returns an unmodifiable map containing the entries from the right map whose keys are not
   * present in the left map.
   */
  Map<K, V> entriesOnlyOnRight();

  /**
   * Returns an unmodifiable map containing the entries that appear in both maps; that is, the
   * intersection of the two maps.
   */
  Map<K, V> entriesInCommon();

  /**
   * Returns an unmodifiable map describing keys that appear in both maps, but with different
   * values.
   */
  Map<K, ValueDifference<V>> entriesDiffering();
  • Multiset 也支持类似的运算,很多所谓的 Collection 集合运算都应该使用Multiset相关方法,不再赘述。(apache collections4 的 intersection 入参为两个 Iterable,性能极差)。

6. 很少用到的方法

  • 受检集合,保证集合中元素为指定类型,很少使用,有一些老代码需要用到:
arduino 复制代码
// 视图
Collection<String> c = Collections.checkedCollection(
         new HashSet<>(), String.class);
  • 判断子列表的位置
java 复制代码
public static int indexOfSubList(List<?> source, List<?> target)
public static int lastIndexOfSubList(List<?> source, List<?> target)

7. 修改方法

这一节故意放在了"很少用到的方法"之后。如果你对于Stream流式编程和之前提到的内容已经熟练使用,这一节基本可以不用看了。

  • addAll,注意这里是添加数组。相比于使用 Arrays.asList 作为中介性能更好一点
java 复制代码
// Collections.addAll(Collection c, T... elements);
Collections.addAll(list, 1, 2, 3);
// 少许性能损耗
list.addAll(Arrays.asList(1, 2, 3));
  • 翻转:Collections.revese(list)
  • 打乱:Collections.shuffle(list)
  • 交换:Collections.swap(list, i, j)
  • 填充所有:Collections.fill(list, obj)
  • 填充集合:Collections.copy(dest, src)
  • 替换所有:replaceAll(List list, T oldVal, T newVal)
  • 旋转:List 的旋转指定是下标偏移, rotate(List<?> list, int distance), 可以使用 Stream循环操作 + skip 实现

8. 没啥用的方法,不推荐使用

  • Collections.max/min

和 Stream 操作重复了,可使用 Stream.max,min

arduino 复制代码
int max = Stream.of(3, 1, 2).max(naturalOrder()).orElseThrow();
  • Collections.reverseOrder

和 Comparator 中方法重复了,请使用 Comparator.naturalOrder() Comparator#reversed。话说集合工具类中有比较器相关方法有点奇怪。

  • Collections.sort

请使用 Stream#sorted 或者 List#sort

  • Collections.disjoint

签名如下:

java 复制代码
// 如果没有交集,返回true
public static boolean disjoint(Collection<?> c1, Collection<?> c2)

这个方法取得是反逻辑,反逻辑是常见的反模式。不如使用集合交集运算 + 判断交集是否为空;如果需要处理非Set类型的集合,先转成Set即可。

  • Guava 无特殊意义的静态工厂
scss 复制代码
Lists.newArrayList(1, 2, 3); // 不如直接使用 new
  • Guava 中过时的工具类

Iterables,Iterators, FluentIterable 都可以使用 Stream 替代,官方已经停止其继续支持,同时也鼓励开发者使用 Stream。

相关推荐
C4程序员11 分钟前
北京JAVA基础面试30天打卡06
java·开发语言·面试
Mike_小新17 分钟前
【Mike随想】未来更看重架构能力和业务经验,而非单纯编码能力
后端·程序员
Abadbeginning20 分钟前
FastSoyAdmin导出excel报错‘latin-1‘ codec can‘t encode characters in position 41-54
前端·javascript·后端
很小心的小新25 分钟前
五、SpringBoot工程打包与运行
java·spring boot·后端
ACGkaka_28 分钟前
SpringBoot 集成 MapStruct
java·spring boot·后端
anthem3728 分钟前
12、Python项目实战
后端
anthem3728 分钟前
7、Python高级特性 - 提升代码质量与效率
后端
anthem3728 分钟前
6、Python文件操作与异常处理
后端
anthem3732 分钟前
3、Python控制流与函数 - 从Java到Python的转变
后端
pe7er1 小时前
Mac 上使用 Homebrew 安装 MySQL 8.4 和 MySQL 5.7 共存
前端·后端