前言
原创不易,禁止转载。
本文旨在帮助读者建立 Java 集合类库相关工具方法的印象,有需要的读者可以收藏,待有具体需要时查阅。本文将侧重于容易被忽略的静态方法,同时会给出一些良好实践。本文讨论的集合类库指的是标准库和 Guava。由于 Apache Collections4 的设计水平一般且繁杂,不推荐使用。
原则
- 标准库优先
- Stream 操作优先
- Guava Immutable 类型优先,不可变对象可以高效利用内存,天然线程安全
- 可读性优先
- 性能好的优先
- 选择相关的方法时,应该确保已经选择了正确的类型或者接口
- 工具方法应该面向接口实现
- 集合类型优先于底层数组,当有性能要求时再使用底层数组
- 尽量不使用迭代器,迭代器可能有性能问题,常常需要维护状态信息,容易出 bug
- 原则有冲突时的选择看团队规范或者个人喜好
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。