SCX Collection 是一个轻量集合工具库。
它提供了两个常用集合结构:
MultiMap 一个 key 对应多个 value 的映射
CountMap 一个 key 对应一个计数值的映射
同时提供 ScxCollection 工具类,用于快速把普通数组或 Iterable 转换成 MultiMap 或 CountMap。
SCX Collection 本身不是 JDK Collection Framework 的替代品,也不是 Stream API 的替代品。它更像是对 Java 集合的一组补充,用来处理“按 key 分组”和“按 key 计数”这两类常见场景。
当前版本为 0.1.0。
<dependency>
<groupId>dev.scx</groupId>
<artifactId>scx-collection</artifactId>
<version>0.1.0</version>
</dependency>
SCX Collection 中最核心的概念包括:
ScxCollection 集合工具类
MultiMap 多值映射接口
DefaultMultiMap MultiMap 的默认实现
MultiMapEntry MultiMap 的遍历条目
CountMap 计数映射接口
DefaultCountMap CountMap 的默认实现
CountMapEntry CountMap 的遍历条目
它们之间的关系可以简单理解为:
ScxCollection.groupingBy(...) -> DefaultMultiMap
ScxCollection.countingBy(...) -> DefaultCountMap
DefaultMultiMap -> 内部使用 Map<K, List<V>>
DefaultCountMap -> 内部使用 Map<K, Long>
也就是说:
MultiMap 解决一个 key 对应多个 value 的问题
CountMap 解决一个 key 对应一个累计数量的问题
import dev.scx.collection.ScxCollection;
import java.util.List;
var users = List.of(
new User("Tom", "dev"),
new User("Jerry", "dev"),
new User("Alice", "ops")
);
var map = ScxCollection.groupingBy(users, User::department);
System.out.println(map.getAll("dev"));
System.out.println(map.getAll("ops"));
结果类似:
[User[name=Tom, department=dev], User[name=Jerry, department=dev]]
[User[name=Alice, department=ops]]
import dev.scx.collection.ScxCollection;
import java.util.List;
var words = List.of("a", "b", "a", "c", "b", "a");
var countMap = ScxCollection.countingBy(words);
System.out.println(countMap.get("a"));
System.out.println(countMap.get("b"));
System.out.println(countMap.get("c"));
结果:
3
2
1
import dev.scx.collection.multi_map.DefaultMultiMap;
var map = new DefaultMultiMap<String, String>();
map.add("dev", "Tom");
map.add("dev", "Jerry");
map.add("ops", "Alice");
System.out.println(map.get("dev"));
System.out.println(map.getAll("dev"));
System.out.println(map.size());
结果:
Tom
[Tom, Jerry]
3
需要注意,MultiMap#size() 返回的是所有 value 的总数,不是 key 的数量。
import dev.scx.collection.count_map.DefaultCountMap;
var countMap = new DefaultCountMap<String>();
countMap.add("apple", 1);
countMap.add("apple", 2);
countMap.add("orange", 1);
System.out.println(countMap.get("apple"));
System.out.println(countMap.get("orange"));
System.out.println(countMap.size());
结果:
3
1
2
这里 CountMap#size() 返回的是 key 的数量。
ScxCollection 是工具类。
它目前提供两类方法:
groupingBy 按 key 分组,返回 MultiMap
countingBy 按 key 计数,返回 CountMap
支持的输入包括:
Iterable<T>
T[]
也就是说,可以处理:
List<T>
Set<T>
Collection<T>
T[]
groupingBy(...) 用于把一组数据按 key 分组。
最简单的形式是:
var multiMap = ScxCollection.groupingBy(list, keyFn);
其中:
list 原始数据
keyFn 从每个元素中提取 key 的函数
示例:
import dev.scx.collection.ScxCollection;
import java.util.List;
var users = List.of(
new User("Tom", "dev"),
new User("Jerry", "dev"),
new User("Alice", "ops")
);
var map = ScxCollection.groupingBy(users, User::department);
等价逻辑大致是:
var map = new DefaultMultiMap<String, User>();
for (var user : users) {
map.add(user.department(), user);
}
默认情况下,groupingBy(...) 会把原始元素本身作为 value。
如果只想保存元素中的某个字段,可以传入 valueFn。
var map = ScxCollection.groupingBy(
users,
User::department,
User::name
);
结果类似:
dev -> [Tom, Jerry]
ops -> [Alice]
也就是说:
keyFn 决定分组 key
valueFn 决定放入 MultiMap 的 value
完整示例:
import dev.scx.collection.ScxCollection;
import java.util.List;
public class GroupingDemo {
public static void main(String[] args) {
var users = List.of(
new User("Tom", "dev"),
new User("Jerry", "dev"),
new User("Alice", "ops")
);
var map = ScxCollection.groupingBy(
users,
User::department,
User::name
);
System.out.println(map.getAll("dev"));
System.out.println(map.getAll("ops"));
}
public record User(String name, String department) {
}
}
输出:
[Tom, Jerry]
[Alice]
groupingBy(...) 也支持数组。
var users = new User[]{
new User("Tom", "dev"),
new User("Jerry", "dev"),
new User("Alice", "ops")
};
var map = ScxCollection.groupingBy(users, User::department);
指定 valueFn:
var map = ScxCollection.groupingBy(
users,
User::department,
User::name
);
groupingBy(...) 返回的是:
MultiMap<K, V>
默认实现是:
DefaultMultiMap<K, V>
内部使用:
HashMap<K, List<V>>
ArrayList<V>
countingBy(...) 用于把一组数据按 key 计数。
最简单的形式是:
var countMap = ScxCollection.countingBy(list);
示例:
import dev.scx.collection.ScxCollection;
import java.util.List;
var words = List.of("a", "b", "a", "c", "b", "a");
var countMap = ScxCollection.countingBy(words);
System.out.println(countMap);
结果类似:
{a=3, b=2, c=1}
默认情况下:
keyFn 使用元素本身作为 key
countFn 每个元素贡献 1
等价逻辑大致是:
var countMap = new DefaultCountMap<String>();
for (var word : words) {
countMap.add(word, 1L);
}
如果希望按元素中的某个字段计数,可以传入 keyFn。
var countMap = ScxCollection.countingBy(users, User::department);
示例:
import dev.scx.collection.ScxCollection;
import java.util.List;
var users = List.of(
new User("Tom", "dev"),
new User("Jerry", "dev"),
new User("Alice", "ops")
);
var countMap = ScxCollection.countingBy(users, User::department);
System.out.println(countMap.get("dev"));
System.out.println(countMap.get("ops"));
结果:
2
1
如果每个元素贡献的数量不是固定的 1,可以传入 countFn。
var countMap = ScxCollection.countingBy(
orders,
Order::productId,
Order::quantity
);
示例:
import dev.scx.collection.ScxCollection;
import java.util.List;
var orders = List.of(
new Order("apple", 2L),
new Order("orange", 3L),
new Order("apple", 5L)
);
var countMap = ScxCollection.countingBy(
orders,
Order::productId,
Order::quantity
);
System.out.println(countMap.get("apple"));
System.out.println(countMap.get("orange"));
结果:
7
3
如果 countFn 返回 null,该元素会被跳过。
var countMap = ScxCollection.countingBy(
orders,
Order::productId,
order -> order.enabled() ? order.quantity() : null
);
等价逻辑是:
for (var order : orders) {
var key = keyFn.apply(order);
var count = countFn.apply(order);
if (count != null) {
countMap.add(key, count);
}
}
这适合把过滤和计数合在一起的简单场景。
countingBy(...) 也支持数组。
var words = new String[]{"a", "b", "a", "c"};
var countMap = ScxCollection.countingBy(words);
指定 keyFn:
var countMap = ScxCollection.countingBy(users, User::department);
指定 keyFn 和 countFn:
var countMap = ScxCollection.countingBy(
orders,
Order::productId,
Order::quantity
);
MultiMap<K, V> 表示一个 key 可以对应多个 value 的映射。
它和普通 Map<K, V> 的区别是:
Map<K, V> 一个 key 对应一个 value
MultiMap<K, V> 一个 key 对应多个 value
例如:
dev -> [Tom, Jerry]
ops -> [Alice]
这种结构适合下面这些场景:
默认实现是 DefaultMultiMap。
import dev.scx.collection.multi_map.DefaultMultiMap;
import dev.scx.collection.multi_map.MultiMap;
MultiMap<String, String> map = new DefaultMultiMap<>();
默认情况下,内部使用:
HashMap
ArrayList
也可以指定内部 Map 和 List 的实现。
import dev.scx.collection.multi_map.DefaultMultiMap;
import java.util.LinkedHashMap;
import java.util.LinkedList;
var map = new DefaultMultiMap<String, String>(
LinkedHashMap::new,
LinkedList::new
);
这适合下面这些场景:
add(...) 用于给某个 key 追加 value。
var map = new DefaultMultiMap<String, String>();
map.add("dev", "Tom");
map.add("dev", "Jerry");
结果:
dev -> [Tom, Jerry]
map.add("dev", "Tom", "Jerry", "Alice");
结果:
dev -> [Tom, Jerry, Alice]
map.add("dev", List.of("Tom", "Jerry"));
结果:
dev -> [Tom, Jerry]
如果有普通 Map<K, V>,可以把每个 entry 添加到 MultiMap。
var source = Map.of(
"dev", "Tom",
"ops", "Alice"
);
map.add(source);
等价于:
source.forEach(map::add);
var a = new DefaultMultiMap<String, String>();
a.add("dev", "Tom");
var b = new DefaultMultiMap<String, String>();
b.add("dev", "Jerry");
b.add("ops", "Alice");
a.add(b);
结果:
dev -> [Tom, Jerry]
ops -> [Alice]
add(MultiMap) 会把另一个 MultiMap 中的每个 key 对应的所有 values 追加到当前对象中。
set(...) 用于覆盖某个 key 对应的 values。
var map = new DefaultMultiMap<String, String>();
map.add("dev", "Tom");
map.add("dev", "Jerry");
var oldValues = map.set("dev", "Alice");
执行后:
dev -> [Alice]
oldValues 是覆盖前的值:
[Tom, Jerry]
如果 key 原来不存在,返回 null。
map.set("dev", "Tom", "Jerry");
结果:
dev -> [Tom, Jerry]
map.set("dev", List.of("Tom", "Jerry"));
结果:
dev -> [Tom, Jerry]
var source = Map.of(
"dev", "Tom",
"ops", "Alice"
);
map.set(source);
等价于:
source.forEach(map::set);
map.set(otherMultiMap);
这会遍历另一个 MultiMap 的每个 entry,然后用对应 values 覆盖当前 key。
get(key) 用于获取某个 key 对应的第一个 value。
var map = new DefaultMultiMap<String, String>();
map.add("dev", "Tom");
map.add("dev", "Jerry");
var value = map.get("dev");
结果:
Tom
如果 key 不存在,或者 key 对应的 values 为空,返回:
null
需要注意,get(key) 只返回第一个 value。
如果需要所有 value,应该使用:
getAll(key)
getAll(key) 用于获取某个 key 对应的所有 values。
var values = map.getAll("dev");
结果:
[Tom, Jerry]
如果 key 不存在,返回一个新的空 List。
var values = map.getAll("unknown");
结果:
[]
需要注意:
示例:
var map = new DefaultMultiMap<String, String>();
map.add("dev", "Tom");
var values = map.getAll("dev");
values.add("Jerry");
System.out.println(map.getAll("dev"));
结果:
[Tom, Jerry]
因为 values 是内部 List。
containsKey(key) 用于判断是否存在某个 key。
map.add("dev", "Tom");
boolean b1 = map.containsKey("dev");
boolean b2 = map.containsKey("ops");
结果:
true
false
containsValue(value) 用于判断所有 values 中是否存在某个 value。
map.add("dev", "Tom");
map.add("ops", "Alice");
boolean b1 = map.containsValue("Tom");
boolean b2 = map.containsValue("Jerry");
结果:
true
false
它会遍历所有 key 对应的所有 values。
因此它不是只检查某个 key,而是检查整个 MultiMap。
remove(...) 用于删除某个 key 下的指定 value。
var map = new DefaultMultiMap<String, String>();
map.add("dev", "Tom");
map.add("dev", "Jerry");
boolean removed = map.remove("dev", "Tom");
执行后:
dev -> [Jerry]
返回值:
true
如果 value 不存在,返回:
false
map.remove("dev", "Tom", "Jerry");
或者:
map.remove("dev", List.of("Tom", "Jerry"));
这些方法会从指定 key 对应的 List 中删除对应 values。
如果删除后某个 key 对应的 List 为空,DefaultMultiMap 会把这个 key 从内部 map 中移除。
var map = new DefaultMultiMap<String, String>();
map.add("dev", "Tom");
map.remove("dev", "Tom");
System.out.println(map.containsKey("dev"));
结果:
false
removeAll(key) 用于删除某个 key 对应的所有 values。
var map = new DefaultMultiMap<String, String>();
map.add("dev", "Tom");
map.add("dev", "Jerry");
var oldValues = map.removeAll("dev");
执行后:
dev 不再存在
oldValues 是删除前的 values:
[Tom, Jerry]
如果 key 不存在,返回:
null
keys() 返回所有 key。
var keys = map.keys();
结果类型是:
Set<K>
需要注意,keys() 返回的是内部 map 的 keySet() 视图。
这意味着:
如果需要安全副本,可以自己复制:
var keysCopy = new HashSet<>(map.keys());
values() 返回所有 value 的扁平列表。
var map = new DefaultMultiMap<String, String>();
map.add("dev", "Tom");
map.add("dev", "Jerry");
map.add("ops", "Alice");
var values = map.values();
结果:
[Tom, Jerry, Alice]
values() 会创建一个新的 List,然后把所有 key 对应的 values 合并进去。
因此修改返回的 List,不会直接影响 MultiMap 内部结构。
var values = map.values();
values.clear();
System.out.println(map.size());
map 本身不会因此被清空。
size() 返回所有 values 的总数量。
var map = new DefaultMultiMap<String, String>();
map.add("dev", "Tom");
map.add("dev", "Jerry");
map.add("ops", "Alice");
System.out.println(map.size());
结果:
3
需要注意,这不是 key 的数量。
如果要获取 key 的数量,可以使用:
map.keys().size()
示例:
System.out.println(map.keys().size());
结果:
2
isEmpty() 判断 MultiMap 中是否没有任何 value。
var map = new DefaultMultiMap<String, String>();
System.out.println(map.isEmpty());
map.add("dev", "Tom");
System.out.println(map.isEmpty());
结果:
true
false
内部语义等价于:
size() == 0L
clear() 用于清空 MultiMap。
map.clear();
它会:
调用后:
map.size()
结果为:
0
toMultiValueMap() 返回内部的多值 map。
Map<String, List<String>> rawMap = map.toMultiValueMap();
需要特别注意:
toMultiValueMap() 返回的是内部 map,不是副本。
因此修改返回的 map 会影响原始 MultiMap。
var rawMap = map.toMultiValueMap();
rawMap.clear();
System.out.println(map.isEmpty());
结果:
true
如果需要副本,可以自己复制:
var copy = new HashMap<String, List<String>>();
map.forEachEntry((key, values) -> {
copy.put(key, new ArrayList<>(values));
});
toSingleValueMap() 会把 MultiMap 转成普通 Map<K, V>。
转换规则是:
每个 key 只保留第一个 value
示例:
var map = new DefaultMultiMap<String, String>();
map.add("dev", "Tom");
map.add("dev", "Jerry");
map.add("ops", "Alice");
var singleMap = map.toSingleValueMap();
结果:
dev -> Tom
ops -> Alice
也可以指定目标 map 类型:
var singleMap = map.toSingleValueMap(LinkedHashMap::new);
这适合希望保留指定 map 实现的场景。
forEach(...) 会遍历每一个 key-value 组合。
map.forEach((key, value) -> {
System.out.println(key + " -> " + value);
});
如果 map 内容是:
dev -> [Tom, Jerry]
ops -> [Alice]
输出:
dev -> Tom
dev -> Jerry
ops -> Alice
也就是说,forEach(...) 是扁平遍历。
forEachEntry(...) 会按 entry 遍历。
map.forEachEntry((key, values) -> {
System.out.println(key + " -> " + values);
});
输出:
dev -> [Tom, Jerry]
ops -> [Alice]
也就是说,forEachEntry(...) 是按 key 遍历,每次拿到这个 key 对应的完整 values。
MultiMap 实现了 Iterable<MultiMapEntry<K, V>>。
因此可以使用增强 for 循环:
for (var entry : map) {
System.out.println(entry.key());
System.out.println(entry.value());
System.out.println(entry.values());
}
MultiMapEntry 提供:
K key();
V value();
List<V> values();
其中:
key() 当前 key
value() 当前 key 对应的第一个 value
values() 当前 key 对应的所有 values
如果 values 为空,value() 返回 null。
CountMap<K> 表示一个 key 对应一个计数值的映射。
它和普通 Map<K, Long> 很像,但提供了更适合计数场景的 add(...) 方法。
例如:
apple -> 3
orange -> 1
banana -> 6
这种结构适合下面这些场景:
默认实现是 DefaultCountMap。
import dev.scx.collection.count_map.CountMap;
import dev.scx.collection.count_map.DefaultCountMap;
CountMap<String> countMap = new DefaultCountMap<>();
默认情况下,内部使用:
HashMap<K, Long>
也可以指定内部 map 实现。
import dev.scx.collection.count_map.DefaultCountMap;
import java.util.LinkedHashMap;
var countMap = new DefaultCountMap<String>(LinkedHashMap::new);
这适合下面这些场景:
add(key, count) 用于给某个 key 累加数量。
var countMap = new DefaultCountMap<String>();
long count = countMap.add("apple", 5);
执行后:
apple -> 5
返回值是添加后的数量:
5
继续添加:
count = countMap.add("apple", 3);
执行后:
apple -> 8
返回值:
8
也就是说:
add("apple", 3)
不是覆盖,而是累加。
add(...) 使用加法累计。
因此可以添加负数。
countMap.add("apple", 10);
countMap.add("apple", -3);
结果:
apple -> 7
需要注意,CountMap 不会阻止计数变成负数。
如果业务上不允许负数,应由调用方自己限制。
set(key, count) 用于直接设置某个 key 的计数。
var countMap = new DefaultCountMap<String>();
Long oldValue = countMap.set("apple", 10);
如果 key 原来不存在,返回:
null
继续设置:
oldValue = countMap.set("apple", 15);
执行后:
apple -> 15
返回值是覆盖前的数量:
10
set(...) 和 add(...) 的区别是:
add 累加数量
set 覆盖数量
get(key) 用于获取某个 key 的数量。
countMap.add("apple", 5);
Long count = countMap.get("apple");
结果:
5
如果 key 不存在,返回:
null
因此如果你想把不存在的 key 当作 0 处理,可以这样写:
long count = countMap.get("apple") == null ? 0L : countMap.get("apple");
或者:
var count = countMap.get("apple");
long safeCount = count == null ? 0L : count;
containsKey(key) 用于判断是否存在某个 key。
countMap.add("apple", 5);
boolean b1 = countMap.containsKey("apple");
boolean b2 = countMap.containsKey("orange");
结果:
true
false
需要注意,containsKey(...) 判断的是 key 是否存在,而不是数量是否大于 0。
例如:
countMap.set("apple", 0);
此时:
countMap.containsKey("apple")
结果仍然是:
true
remove(key) 用于删除某个 key,并返回删除前的数量。
countMap.add("apple", 5);
Long oldValue = countMap.remove("apple");
结果:
5
删除后:
countMap.get("apple")
结果:
null
如果 key 不存在,返回:
null
keys() 返回所有 key。
var keys = countMap.keys();
结果类型是:
Set<K>
需要注意,keys() 返回的是内部 map 的 keySet() 视图。
这意味着:
如果需要安全副本,可以自己复制:
var keysCopy = new HashSet<>(countMap.keys());
size() 返回 key 的数量。
var countMap = new DefaultCountMap<String>();
countMap.add("apple", 5);
countMap.add("orange", 3);
System.out.println(countMap.size());
结果:
2
需要注意,这和所有 count 的总和不是一回事。
例如:
apple -> 5
orange -> 3
size() 是:
2
总计数是:
8
如果需要总计数,可以自己遍历:
long total = 0;
for (var entry : countMap) {
total = total + entry.count();
}
isEmpty() 用于判断是否没有任何 key。
var countMap = new DefaultCountMap<String>();
System.out.println(countMap.isEmpty());
countMap.add("apple", 1);
System.out.println(countMap.isEmpty());
结果:
true
false
clear() 用于清空所有计数。
countMap.clear();
调用后:
countMap.isEmpty()
结果为:
true
toMap() 用于把 CountMap 转成普通 Map<K, Long>。
var map = countMap.toMap();
默认返回一个新的 HashMap。
也可以指定目标 map 类型:
var map = countMap.toMap(LinkedHashMap::new);
需要注意,toMap(...) 返回的是副本。
修改返回的 map,不会影响原始 CountMap。
var map = countMap.toMap();
map.clear();
System.out.println(countMap.isEmpty());
countMap 不会因此被清空。
forEach(...) 会遍历每个 key-count 组合。
countMap.forEach((key, count) -> {
System.out.println(key + " -> " + count);
});
示例输出:
apple -> 5
orange -> 3
CountMap 实现了 Iterable<CountMapEntry<K>>。
因此可以使用增强 for 循环:
for (var entry : countMap) {
System.out.println(entry.key());
System.out.println(entry.count());
}
CountMapEntry 提供:
K key();
long count();
static <T, K> MultiMap<K, T> groupingBy(
Iterable<T> list,
Function<T, K> keyFn
)
static <T, K, V> MultiMap<K, V> groupingBy(
Iterable<T> list,
Function<T, K> keyFn,
Function<T, V> valueFn
)
static <T, K> MultiMap<K, T> groupingBy(
T[] list,
Function<T, K> keyFn
)
static <T, K, V> MultiMap<K, V> groupingBy(
T[] list,
Function<T, K> keyFn,
Function<T, V> valueFn
)
static <K> CountMap<K> countingBy(
Iterable<K> list
)
static <T, K> CountMap<K> countingBy(
Iterable<T> list,
Function<T, K> keyFn
)
static <T, K> CountMap<K> countingBy(
Iterable<T> list,
Function<T, K> keyFn,
Function<T, Long> countFn
)
static <K> CountMap<K> countingBy(
K[] list
)
static <T, K> CountMap<K> countingBy(
T[] list,
Function<T, K> keyFn
)
static <T, K> CountMap<K> countingBy(
T[] list,
Function<T, K> keyFn,
Function<T, Long> countFn
)
boolean add(K key, V value)
boolean add(K key, V... values)
boolean add(K key, Collection<V> values)
void add(Map<K, V> map)
void add(MultiMap<K, V> map)
List<V> set(K key, V value)
List<V> set(K key, V... values)
List<V> set(K key, Collection<V> values)
void set(Map<K, V> map)
void set(MultiMap<K, V> map)
V get(K key)
List<V> getAll(K key)
boolean containsKey(K key)
boolean containsValue(V value)
boolean remove(K key, V value)
boolean remove(K key, V... values)
boolean remove(K key, Collection<V> values)
List<V> removeAll(K key)
Set<K> keys()
List<V> values()
long size()
boolean isEmpty()
void clear()
Map<K, List<V>> toMultiValueMap()
Map<K, V> toSingleValueMap()
Map<K, V> toSingleValueMap(Supplier<Map<K, V>> mapSupplier)
<X extends Throwable> void forEach(
Function2Void<K, V, X> action
) throws X
<X extends Throwable> void forEachEntry(
Function2Void<K, List<V>, X> action
) throws X
long add(K key, long count)
Long set(K key, long count)
Long get(K key)
boolean containsKey(K key)
Long remove(K key)
Set<K> keys()
long size()
boolean isEmpty()
void clear()
Map<K, Long> toMap()
Map<K, Long> toMap(Supplier<Map<K, Long>> mapSupplier)
<X extends Throwable> void forEach(
Function2Void<K, Long, X> action
) throws X
import dev.scx.collection.ScxCollection;
import java.util.List;
public class GroupingUsersDemo {
public static void main(String[] args) {
var users = List.of(
new User("Tom", "dev"),
new User("Jerry", "dev"),
new User("Alice", "ops"),
new User("Bob", "ops"),
new User("Lucy", "qa")
);
var usersByDepartment = ScxCollection.groupingBy(
users,
User::department
);
usersByDepartment.forEachEntry((department, departmentUsers) -> {
System.out.println(department + " -> " + departmentUsers);
});
}
public record User(String name, String department) {
}
}
输出类似:
dev -> [User[name=Tom, department=dev], User[name=Jerry, department=dev]]
ops -> [User[name=Alice, department=ops], User[name=Bob, department=ops]]
qa -> [User[name=Lucy, department=qa]]
如果只想保留用户名:
var namesByDepartment = ScxCollection.groupingBy(
users,
User::department,
User::name
);
结果类似:
dev -> [Tom, Jerry]
ops -> [Alice, Bob]
qa -> [Lucy]
import dev.scx.collection.ScxCollection;
import java.util.List;
public class WordCountDemo {
public static void main(String[] args) {
var words = List.of(
"java",
"scx",
"java",
"collection",
"scx",
"java"
);
var countMap = ScxCollection.countingBy(words);
countMap.forEach((word, count) -> {
System.out.println(word + " -> " + count);
});
}
}
输出类似:
java -> 3
scx -> 2
collection -> 1
import dev.scx.collection.ScxCollection;
import java.util.List;
public class OrderCountDemo {
public static void main(String[] args) {
var orders = List.of(
new Order("apple", 2L),
new Order("orange", 3L),
new Order("apple", 5L),
new Order("banana", 1L)
);
var countMap = ScxCollection.countingBy(
orders,
Order::productId,
Order::quantity
);
System.out.println(countMap.get("apple"));
System.out.println(countMap.get("orange"));
System.out.println(countMap.get("banana"));
}
public record Order(String productId, Long quantity) {
}
}
输出:
7
3
1
import dev.scx.collection.ScxCollection;
import java.util.List;
public class CollectionDemo {
public static void main(String[] args) {
var users = List.of(
new User("Tom", "dev", List.of("java", "sql")),
new User("Jerry", "dev", List.of("java", "web")),
new User("Alice", "ops", List.of("linux", "sql"))
);
var usersByDepartment = ScxCollection.groupingBy(
users,
User::department,
User::name
);
System.out.println(usersByDepartment);
var allTags = users.stream()
.flatMap(user -> user.tags().stream())
.toList();
var tagCount = ScxCollection.countingBy(allTags);
System.out.println(tagCount);
}
public record User(String name, String department, List<String> tags) {
}
}
输出类似:
{dev=[Tom, Jerry], ops=[Alice]}
{java=2, sql=2, web=1, linux=1}
DefaultMultiMap 内部使用:
Map<K, List<V>>
它的目的不是发明一套复杂集合模型,而是把下面这种重复代码封装起来:
map.computeIfAbsent(key, k -> new ArrayList<>()).add(value);
使用 MultiMap 后,可以直接写:
multiMap.add(key, value);
DefaultCountMap 内部使用:
Map<K, Long>
它主要封装了累计逻辑。
countMap.add(key, count);
等价于:
map.merge(key, count, Long::sum);
ScxCollection.groupingBy(...) 的结果是 MultiMap。
它适合处理“一对多”的分组结果。
department -> users
tag -> articles
type -> configs
ScxCollection.countingBy(...) 的结果是 CountMap。
它适合处理“一个 key 对应一个累计数量”的结果。
word -> count
productId -> quantity
status -> event count
DefaultMultiMap 默认使用 HashMap 和 ArrayList。
DefaultCountMap 默认使用 HashMap。
因此它们默认都不是线程安全集合。
如果需要在多线程中共享修改,应由调用方负责同步,或者提供线程安全的底层集合实现。
默认内部 map 是 HashMap。
因此 key 的遍历顺序不应被依赖。
如果需要稳定顺序,可以使用 LinkedHashMap。
var multiMap = new DefaultMultiMap<String, String>(
LinkedHashMap::new,
ArrayList::new
);
var countMap = new DefaultCountMap<String>(
LinkedHashMap::new
);
需要注意下面这些方法返回的不是纯副本:
MultiMap.keys()
MultiMap.getAll(key) 当 key 存在时
MultiMap.toMultiValueMap()
CountMap.keys()
修改这些返回值,可能影响原始集合。
下面这些方法返回的是新集合:
MultiMap.values()
MultiMap.toSingleValueMap()
CountMap.toMap()
MultiMap#size() 返回的是所有 values 的扁平总数。
dev -> [Tom, Jerry]
ops -> [Alice]
size() 是:
3
而不是:
2
如果需要 key 的数量,请使用:
multiMap.keys().size()
CountMap#size() 返回的是 key 的数量。
apple -> 5
orange -> 3
size() 是:
2
而不是:
8
如果需要所有 count 的总和,需要自己遍历累加。
默认实现基于 HashMap 和 ArrayList。
因此默认情况下,key 和 value 都可以是 null。
例如:
var map = new DefaultMultiMap<String, String>();
map.add(null, "value");
map.add("key", null);
对于 CountMap:
var countMap = new DefaultCountMap<String>();
countMap.add(null, 1);
是否允许 null,最终取决于底层 map/list 实现。
如果你换成不支持 null 的集合实现,那么对应行为也会变化。
DefaultMultiMap#equals(...) 会和另一个 DefaultMultiMap 比较内部 map。
DefaultCountMap#equals(...) 会和另一个 DefaultCountMap 比较内部 map。
也就是说,它们主要用于默认实现之间的相等判断。
不要假定不同 MultiMap 实现之间一定可以通过 equals(...) 比较为相等。
不是。
Java Stream 已经提供了很多强大的集合处理能力。
SCX Collection 只是提供更直接的 MultiMap 和 CountMap 结构,以及对应的简单构造工具。
Collectors.groupingBy(...) 返回的是普通 Map<K, List<T>>。
ScxCollection.groupingBy(...) 返回的是 MultiMap<K, V>。
MultiMap 提供了更直接的 add(...)、get(...)、getAll(...)、values()、toSingleValueMap() 等方法。
Collectors.counting() 通常配合 groupingBy(...) 使用,返回普通 Map<K, Long>。
ScxCollection.countingBy(...) 返回的是 CountMap<K>。
CountMap 提供了更直接的 add(...)、set(...)、get(...)、toMap(...) 等方法。
返回某个 key 对应的第一个 value。
如果 key 不存在,返回 null。
返回某个 key 对应的所有 values。
如果 key 不存在,返回空 List。
如果 key 存在,返回的是内部 List。
如果 key 不存在,返回的是新的空 List。
不是。
MultiMap#size() 是所有 values 的总数。
如果需要 key 数量,使用:
multiMap.keys().size()
不是。
CountMap#size() 是 key 的数量。
如果需要所有 count 的总和,需要自己遍历累加。
不是。
add(...) 是累加。
如果需要覆盖,使用:
set(...)
可以。
add(...) 只是做加法累计,不限制正负。
不会。
get(...) 返回的是 Long。
如果 key 不存在,返回 null。
不是。
toMultiValueMap() 返回内部 map。
修改返回的 map 会影响原始 MultiMap。
是。
它会创建一个新的 map,并把每个 key 的第一个 value 放进去。
是。
toMap() 会创建一个新的 map。
不是。
默认使用 HashMap 和 ArrayList。
多线程共享修改时,需要调用方自己同步。
不固定。
默认使用 HashMap。
如果需要固定顺序,可以使用 LinkedHashMap 作为底层 map。
可以。
DefaultMultiMap 可以指定 map 和 list 的 supplier。
new DefaultMultiMap<>(LinkedHashMap::new, LinkedList::new)
DefaultCountMap 可以指定 map 的 supplier。
new DefaultCountMap<>(LinkedHashMap::new)
默认实现支持,因为默认底层是 HashMap 和 ArrayList。
但如果你换成其它不支持 null 的集合实现,行为取决于对应集合。
默认实现支持,因为默认底层是 HashMap。
不会。
DefaultMultiMap 在删除 value 后,如果该 key 对应的 List 为空,会把 key 从 map 中移除。
不会主动跳过。
groupingBy(...) 会直接使用 keyFn 和 valueFn 的结果调用 multiMap.add(...)。
如果 key 或 value 是 null,是否允许取决于底层 MultiMap 实现。
countingBy(...) 只会在 countFn 返回 null 时跳过该元素。
如果 keyFn 返回 null,默认 DefaultCountMap 可以接受这个 null key。
适合一个 key 对应多个 value 的场景。
例如:
部门 -> 用户列表
标签 -> 文章列表
请求头名 -> 请求头值列表
字段名 -> 错误信息列表
适合一个 key 对应一个累计数量的场景。
例如:
单词 -> 出现次数
商品 -> 购买数量
状态 -> 事件次数
标签 -> 使用次数