SCX Projects

SCX Collection

SCX Collection 是一个轻量集合工具库。

它提供了两个常用集合结构:

MultiMap    一个 key 对应多个 value 的映射
CountMap    一个 key 对应一个计数值的映射

同时提供 ScxCollection 工具类,用于快速把普通数组或 Iterable 转换成 MultiMapCountMap

SCX Collection 本身不是 JDK Collection Framework 的替代品,也不是 Stream API 的替代品。它更像是对 Java 集合的一组补充,用来处理“按 key 分组”和“按 key 计数”这两类常见场景。

当前版本为 0.1.0

GitHub

安装

Maven

<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

手动使用 MultiMap

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 的数量。

手动使用 CountMap

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

ScxCollection 是工具类。

它目前提供两类方法:

groupingBy    按 key 分组,返回 MultiMap
countingBy    按 key 计数,返回 CountMap

支持的输入包括:

Iterable<T>
T[]

也就是说,可以处理:

List<T>
Set<T>
Collection<T>
T[]

groupingBy

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

指定 valueFn

默认情况下,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 返回值

groupingBy(...) 返回的是:

MultiMap<K, V>

默认实现是:

DefaultMultiMap<K, V>

内部使用:

HashMap<K, List<V>>
ArrayList<V>

countingBy

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

如果希望按元素中的某个字段计数,可以传入 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

指定 countFn

如果每个元素贡献的数量不是固定的 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

如果 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

MultiMap<K, V> 表示一个 key 可以对应多个 value 的映射。

它和普通 Map<K, V> 的区别是:

Map<K, V>          一个 key 对应一个 value
MultiMap<K, V>     一个 key 对应多个 value

例如:

dev -> [Tom, Jerry]
ops -> [Alice]

这种结构适合下面这些场景:

  1. 按部门分组用户。
  2. 按类型分组配置项。
  3. 按标签分组文章。
  4. 按请求头名称保存多个请求头值。
  5. 按字段名保存多个错误信息。
  6. 按 key 聚合多个匹配结果。

创建 MultiMap

默认实现是 DefaultMultiMap

import dev.scx.collection.multi_map.DefaultMultiMap;
import dev.scx.collection.multi_map.MultiMap;

MultiMap<String, String> map = new DefaultMultiMap<>();

默认情况下,内部使用:

HashMap
ArrayList

也可以指定内部 MapList 的实现。

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

这适合下面这些场景:

  1. 希望 key 按插入顺序保存。
  2. 希望 value 使用指定 List 实现。
  3. 希望控制底层集合类型。
  4. 希望在测试中得到更稳定的遍历顺序。

add

add(...) 用于给某个 key 追加 value。

添加单个 value

var map = new DefaultMultiMap<String, String>();

map.add("dev", "Tom");
map.add("dev", "Jerry");

结果:

dev -> [Tom, Jerry]

添加多个 value

map.add("dev", "Tom", "Jerry", "Alice");

结果:

dev -> [Tom, Jerry, Alice]

添加 Collection

map.add("dev", List.of("Tom", "Jerry"));

结果:

dev -> [Tom, Jerry]

添加 Map

如果有普通 Map<K, V>,可以把每个 entry 添加到 MultiMap

var source = Map.of(
    "dev", "Tom",
    "ops", "Alice"
);

map.add(source);

等价于:

source.forEach(map::add);

添加另一个 MultiMap

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

set(...) 用于覆盖某个 key 对应的 values。

覆盖为单个 value

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

覆盖为多个 value

map.set("dev", "Tom", "Jerry");

结果:

dev -> [Tom, Jerry]

覆盖为 Collection

map.set("dev", List.of("Tom", "Jerry"));

结果:

dev -> [Tom, Jerry]

使用 Map 覆盖

var source = Map.of(
    "dev", "Tom",
    "ops", "Alice"
);

map.set(source);

等价于:

source.forEach(map::set);

使用 MultiMap 覆盖

map.set(otherMultiMap);

这会遍历另一个 MultiMap 的每个 entry,然后用对应 values 覆盖当前 key。

get

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

getAll(key) 用于获取某个 key 对应的所有 values。

var values = map.getAll("dev");

结果:

[Tom, Jerry]

如果 key 不存在,返回一个新的空 List。

var values = map.getAll("unknown");

结果:

[]

需要注意:

  1. 如果 key 存在,返回的是内部 List。
  2. 如果 key 不存在,返回的是一个新的空 List,但不会自动放入 map。
  3. 修改已存在 key 的返回 List,会影响 MultiMap 内部数据。
  4. 修改不存在 key 的返回空 List,不会影响 MultiMap。

示例:

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

containsKey(key) 用于判断是否存在某个 key。

map.add("dev", "Tom");

boolean b1 = map.containsKey("dev");
boolean b2 = map.containsKey("ops");

结果:

true
false

containsValue

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

remove(...) 用于删除某个 key 下的指定 value。

删除单个 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

删除多个 value

map.remove("dev", "Tom", "Jerry");

或者:

map.remove("dev", List.of("Tom", "Jerry"));

这些方法会从指定 key 对应的 List 中删除对应 values。

删除后自动移除空 key

如果删除后某个 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

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

keys() 返回所有 key。

var keys = map.keys();

结果类型是:

Set<K>

需要注意,keys() 返回的是内部 map 的 keySet() 视图。

这意味着:

  1. 它不是副本。
  2. 当前 MultiMap 改变后,keys 视图也会变化。
  3. 修改 keys 视图可能影响 MultiMap 内部数据。

如果需要安全副本,可以自己复制:

var keysCopy = new HashSet<>(map.keys());

values

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

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

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

clear() 用于清空 MultiMap

map.clear();

它会:

  1. 清空每个 key 对应的 List。
  2. 清空内部 map。

调用后:

map.size()

结果为:

0

toMultiValueMap

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

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 实现的场景。

MultiMap 遍历

forEach

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

forEachEntry(...) 会按 entry 遍历。

map.forEachEntry((key, values) -> {
    System.out.println(key + " -> " + values);
});

输出:

dev -> [Tom, Jerry]
ops -> [Alice]

也就是说,forEachEntry(...) 是按 key 遍历,每次拿到这个 key 对应的完整 values。

Iterator

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

CountMap<K> 表示一个 key 对应一个计数值的映射。

它和普通 Map<K, Long> 很像,但提供了更适合计数场景的 add(...) 方法。

例如:

apple  -> 3
orange -> 1
banana -> 6

这种结构适合下面这些场景:

  1. 统计词频。
  2. 统计标签出现次数。
  3. 统计分组数量。
  4. 统计商品数量。
  5. 统计事件次数。
  6. 对某个 key 累加权重。

创建 CountMap

默认实现是 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);

这适合下面这些场景:

  1. 希望 key 按插入顺序保存。
  2. 希望使用指定 Map 实现。
  3. 希望测试输出顺序更稳定。

add

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

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

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

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

remove(key) 用于删除某个 key,并返回删除前的数量。

countMap.add("apple", 5);

Long oldValue = countMap.remove("apple");

结果:

5

删除后:

countMap.get("apple")

结果:

null

如果 key 不存在,返回:

null

keys

keys() 返回所有 key。

var keys = countMap.keys();

结果类型是:

Set<K>

需要注意,keys() 返回的是内部 map 的 keySet() 视图。

这意味着:

  1. 它不是副本。
  2. 当前 CountMap 改变后,keys 视图也会变化。
  3. 修改 keys 视图可能影响 CountMap 内部数据。

如果需要安全副本,可以自己复制:

var keysCopy = new HashSet<>(countMap.keys());

size

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

isEmpty() 用于判断是否没有任何 key。

var countMap = new DefaultCountMap<String>();

System.out.println(countMap.isEmpty());

countMap.add("apple", 1);

System.out.println(countMap.isEmpty());

结果:

true
false

clear

clear() 用于清空所有计数。

countMap.clear();

调用后:

countMap.isEmpty()

结果为:

true

toMap

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 不会因此被清空。

CountMap 遍历

forEach

forEach(...) 会遍历每个 key-count 组合。

countMap.forEach((key, count) -> {
    System.out.println(key + " -> " + count);
});

示例输出:

apple -> 5
orange -> 3

Iterator

CountMap 实现了 Iterable<CountMapEntry<K>>

因此可以使用增强 for 循环:

for (var entry : countMap) {
    System.out.println(entry.key());
    System.out.println(entry.count());
}

CountMapEntry 提供:

K key();

long count();

方法总览

ScxCollection

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
)

MultiMap

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

CountMap

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

完整示例:MultiMap 和 CountMap 配合

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}

设计说明

1. MultiMap 是 Map<K, List> 的封装

DefaultMultiMap 内部使用:

Map<K, List<V>>

它的目的不是发明一套复杂集合模型,而是把下面这种重复代码封装起来:

map.computeIfAbsent(key, k -> new ArrayList<>()).add(value);

使用 MultiMap 后,可以直接写:

multiMap.add(key, value);

2. CountMap 是 Map<K, Long> 的封装

DefaultCountMap 内部使用:

Map<K, Long>

它主要封装了累计逻辑。

countMap.add(key, count);

等价于:

map.merge(key, count, Long::sum);

3. groupingBy 返回 MultiMap

ScxCollection.groupingBy(...) 的结果是 MultiMap

它适合处理“一对多”的分组结果。

department -> users
tag        -> articles
type       -> configs

4. countingBy 返回 CountMap

ScxCollection.countingBy(...) 的结果是 CountMap

它适合处理“一个 key 对应一个累计数量”的结果。

word      -> count
productId -> quantity
status    -> event count

5. 默认实现不是线程安全的

DefaultMultiMap 默认使用 HashMapArrayList

DefaultCountMap 默认使用 HashMap

因此它们默认都不是线程安全集合。

如果需要在多线程中共享修改,应由调用方负责同步,或者提供线程安全的底层集合实现。

6. 默认遍历顺序不稳定

默认内部 map 是 HashMap

因此 key 的遍历顺序不应被依赖。

如果需要稳定顺序,可以使用 LinkedHashMap

var multiMap = new DefaultMultiMap<String, String>(
    LinkedHashMap::new,
    ArrayList::new
);

var countMap = new DefaultCountMap<String>(
    LinkedHashMap::new
);

7. 有些方法返回内部视图

需要注意下面这些方法返回的不是纯副本:

MultiMap.keys()
MultiMap.getAll(key)    当 key 存在时
MultiMap.toMultiValueMap()

CountMap.keys()

修改这些返回值,可能影响原始集合。

下面这些方法返回的是新集合:

MultiMap.values()
MultiMap.toSingleValueMap()
CountMap.toMap()

8. MultiMap 的 size 是 value 总数

MultiMap#size() 返回的是所有 values 的扁平总数。

dev -> [Tom, Jerry]
ops -> [Alice]

size() 是:

3

而不是:

2

如果需要 key 的数量,请使用:

multiMap.keys().size()

9. CountMap 的 size 是 key 数量

CountMap#size() 返回的是 key 的数量。

apple  -> 5
orange -> 3

size() 是:

2

而不是:

8

如果需要所有 count 的总和,需要自己遍历累加。

10. null 的处理遵循底层集合

默认实现基于 HashMapArrayList

因此默认情况下,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 的集合实现,那么对应行为也会变化。

11. equals 只比较相同默认实现

DefaultMultiMap#equals(...) 会和另一个 DefaultMultiMap 比较内部 map。

DefaultCountMap#equals(...) 会和另一个 DefaultCountMap 比较内部 map。

也就是说,它们主要用于默认实现之间的相等判断。

不要假定不同 MultiMap 实现之间一定可以通过 equals(...) 比较为相等。

常见问题

SCX Collection 是不是替代 Java Stream?

不是。

Java Stream 已经提供了很多强大的集合处理能力。

SCX Collection 只是提供更直接的 MultiMapCountMap 结构,以及对应的简单构造工具。

groupingBy 和 Stream Collectors.groupingBy 有什么区别?

Collectors.groupingBy(...) 返回的是普通 Map<K, List<T>>

ScxCollection.groupingBy(...) 返回的是 MultiMap<K, V>

MultiMap 提供了更直接的 add(...)get(...)getAll(...)values()toSingleValueMap() 等方法。

countingBy 和 Stream Collectors.counting 有什么区别?

Collectors.counting() 通常配合 groupingBy(...) 使用,返回普通 Map<K, Long>

ScxCollection.countingBy(...) 返回的是 CountMap<K>

CountMap 提供了更直接的 add(...)set(...)get(...)toMap(...) 等方法。

MultiMap 的 get 返回什么?

返回某个 key 对应的第一个 value。

如果 key 不存在,返回 null

MultiMap 的 getAll 返回什么?

返回某个 key 对应的所有 values。

如果 key 不存在,返回空 List。

getAll 返回的是副本吗?

如果 key 存在,返回的是内部 List。

如果 key 不存在,返回的是新的空 List。

MultiMap 的 size 是 key 数量吗?

不是。

MultiMap#size() 是所有 values 的总数。

如果需要 key 数量,使用:

multiMap.keys().size()

CountMap 的 size 是所有 count 的总和吗?

不是。

CountMap#size() 是 key 的数量。

如果需要所有 count 的总和,需要自己遍历累加。

CountMap 的 add 是覆盖吗?

不是。

add(...) 是累加。

如果需要覆盖,使用:

set(...)

CountMap 可以添加负数吗?

可以。

add(...) 只是做加法累计,不限制正负。

CountMap 中不存在的 key 返回 0 吗?

不会。

get(...) 返回的是 Long

如果 key 不存在,返回 null

toMultiValueMap 返回副本吗?

不是。

toMultiValueMap() 返回内部 map。

修改返回的 map 会影响原始 MultiMap

toSingleValueMap 返回副本吗?

是。

它会创建一个新的 map,并把每个 key 的第一个 value 放进去。

CountMap 的 toMap 返回副本吗?

是。

toMap() 会创建一个新的 map。

默认实现是否线程安全?

不是。

默认使用 HashMapArrayList

多线程共享修改时,需要调用方自己同步。

默认遍历顺序固定吗?

不固定。

默认使用 HashMap

如果需要固定顺序,可以使用 LinkedHashMap 作为底层 map。

可以自定义底层集合吗?

可以。

DefaultMultiMap 可以指定 map 和 list 的 supplier。

new DefaultMultiMap<>(LinkedHashMap::new, LinkedList::new)

DefaultCountMap 可以指定 map 的 supplier。

new DefaultCountMap<>(LinkedHashMap::new)

MultiMap 支持 null key 或 null value 吗?

默认实现支持,因为默认底层是 HashMapArrayList

但如果你换成其它不支持 null 的集合实现,行为取决于对应集合。

CountMap 支持 null key 吗?

默认实现支持,因为默认底层是 HashMap

remove 后如果 values 为空,key 还会保留吗?

不会。

DefaultMultiMap 在删除 value 后,如果该 key 对应的 List 为空,会把 key 从 map 中移除。

groupingBy 会跳过 null 吗?

不会主动跳过。

groupingBy(...) 会直接使用 keyFnvalueFn 的结果调用 multiMap.add(...)

如果 key 或 value 是 null,是否允许取决于底层 MultiMap 实现。

countingBy 会跳过 null 吗?

countingBy(...) 只会在 countFn 返回 null 时跳过该元素。

如果 keyFn 返回 null,默认 DefaultCountMap 可以接受这个 null key。

什么时候用 MultiMap?

适合一个 key 对应多个 value 的场景。

例如:

部门 -> 用户列表
标签 -> 文章列表
请求头名 -> 请求头值列表
字段名 -> 错误信息列表

什么时候用 CountMap?

适合一个 key 对应一个累计数量的场景。

例如:

单词 -> 出现次数
商品 -> 购买数量
状态 -> 事件次数
标签 -> 使用次数