重生之Collectors.toMap()的坑,我真的踩了又踩

431 阅读3分钟

第一章:噩梦的开始

jdk8 引入了让人欲罢不能的 stream 流处理 -- 楔子

每当遇到 List<Object> 对象需要各种花样的处理时,你就会想到你的梦中情人Stream了吧。

image.png

先造点数据,下面都要用 考试重点

List<User> list = new ArrayList<>();
list.add(new User(1, "a"));
list.add(new User(1, "b"));
list.add(new User(2, "c"));
list.add(new User(2, null));

@Data
class User{
    private int id;
    private String name;
    public User(int id, String name) {
        this.id = id;
        this.name = name;
    }
}

比如过滤 + 筛选属性:Collectors.toList()

List<String> nameList = list.stream().filter(item -> StringUtils.hasText(item.getName())).collect(Collectors.toList());

而当你需要将List转为Map的时候,兄dei,你可要小心了!!!

路人甲:这有什么难的,我直接:Collectors.toMap()

Map<Integer, String> nameMap = list.stream().collect(Collectors.toMap(User::getId, User::getName));

自信,直接启动~~

woc 报错了

第二章:重生之我是技术博主

image.png

噢噢噢 java.lang.IllegalStateException: Duplicate key a key重复了

怎么解决呢?点进toMap()方法,看看源码

public static <T, K, U>
Collector<T, ?, Map<K,U>> toMap(Function<? super T, ? extends K> keyMapper,
                                Function<? super T, ? extends U> valueMapper) {
    return toMap(keyMapper, valueMapper, throwingMerger(), HashMap::new);
}

噢噢,这里调用了另外一个方法,传了一个 throwingMerger() 参数 点过去看看

/**
返回一个合并函数,适合在 Map. merge() 中使用 或 toMap(),该函数总是抛出 IllegalStateException。这可用于强制执行所收集的元素是不同的假设。
返回:
一个总是抛出的合并函数 IllegalStateException
 */
private static <T> BinaryOperator<T> throwingMerger() {
    return (u,v) -> { throw new IllegalStateException(String.format("Duplicate key %s", u)); };
}

哦豁,gg了?

这时候你又发现(google,baidu),刚好有另外一个方法可以解决这个重复key的问题

手动提供key重复解决策略

public static <T, K, U>
Collector<T, ?, Map<K,U>> toMap(Function<? super T, ? extends K> keyMapper,
                                Function<? super T, ? extends U> valueMapper,
                                BinaryOperator<U> mergeFunction) {
    return toMap(keyMapper, valueMapper, mergeFunction, HashMap::new);
}

于是你修改了下

Map<Integer, String> nameMap = list.stream().collect(Collectors.toMap(User::getId, User::getName, (k1, k2) -> k1));

这次不太自信了,debug启动~~

啊?又又报错了-翻车

image.png

还是老朋友NullPointerException 空指针,哪里受过这种委屈。

image.png

第三章:已老实,求放过!!!

打两个断点,debug debug

image.png

断点进入这个merge方法

image.png

WTF? HashMap 不是支持null的value值吗?--隐约记得八股文是如此啊

这里作者也去google了一下,放一下stackoverflow的链接 stackoverflow.com/questions/2…

第四章:道法自然

好的,没办法,那就的处理一下valuenull

这里借鉴一下其他大佬的博文(小声bb:实在好笑)

image.png

这里作者我就不这么优雅了。我们filter处理一波

Map<Integer, String> nameMap = list.stream()
        .filter(user -> StringUtils.hasText(user.getName()))
        .collect(Collectors.toMap(User::getId, User::getName, (k1, k2) -> k1));

嗯?又是stream,又是filter,又是Collectors.toMap()...我丢,看起来似乎有点复杂

要是不装这个stream的逼呢?

Map<Integer, String> nameMap = new HashMap<>();
list.forEach(user -> nameMap.put(user.getId(), user.getName()));

// 亦或者
for (User user : list) {
  nameMap.put(user.getId(), user.getName());
}

嗯~~~ 舒服了

image.png

完结篇:小声BB + 个人揣测

再多看看这个merge()方法 这是Map接口的default方法,那么其他子Map如果没有重写,都会调用这个default的merge方法。 image.png

这里联想到既然是default的方法,是不是jdk作者是为了兼容各种类型的Map(例如HashMap,ConcurrentHashMap)呢?

JDK作者: 哎你小子,可真会琢磨 image.png

然后打脸就来了,HashMapConcurrentHashMap都重写了这个merge()方法

image.png

image.png

备注:google了很多文章,也没有找到官方一点的 value判空的说明。

那我就只能继续揣测了

  1. HashMapget 方法,就算 key 不存在,拿到的 value 也会是 null
  2. 那么如果允许 valuenull 存储,多个相同的 key ,其 value 都为 null
  3. 在使用的时候,某些场景下,跟直接不存入HashMap的结果是一样的。

一般我们使用 Hash 表,大多数情况是为了使用 get(key) 方法,避免 for 循环,if 判断key 执行业务,那么这种场景下,将 value 值为 nullkey 存进来,就是单纯的浪费空间了。

路人乙:我要抬杠,我要用mapkeys(去重的key),或者 values(为去重的value)

如果是keys 可以直接用

Set<Integer> ids = list.stream().map(User::getId).collect(Collectors.toSet());

如果是values 可以直接用

List<String> nameList = list.stream().map(User::getName).collect(Collectors.toList());

这二者都比先转map,再拿keys或者values性能好。且这种非空的value 也能兼容其他map,就算是强转类型,也能使用其merge方法。