Java Collectors.toMap的内存优化

时间:2018-07-13 22:52:19

标签: java java-8 java-stream

我有一个将列表转换为地图的函数。调用此函数后,地图的大小不会改变。我正在尝试在以下两种实现之间做出决定:

Map<Long, Object> listToMap(List<Object> objs) {
        /* Implementation One: */

        Map<Long, Object> map = new HashMap<>(objs.size(), 1);
        for (Object obj : objs) {
            map.put(obj.getKey(), obj);
        }
        return map;

        /* Implementation Two: */

        return objs.stream().collect(Collectors.toMap(Object::getKey, obj -> obj));

    }

在第一个实现中,我通过使用1的加载因子和列表的大小为所有元素分配了足够的内存。这样可以确保不会执行调整大小操作。然后,我遍历该列表并一个接一个地添加元素。

在第二种实现中,我使用Java 8流来提高可读性。

我的问题是:第二个实现是否会涉及HashMap的多个调整大小,或者是否已对其进行优化以分配足够的内存?

3 个答案:

答案 0 :(得分:8)

第二种实现将涉及HashMap的多个调整大小。

我只是通过在调试器中运行它,并在每次调整哈希映射的大小时都将其破坏来确定这一点。首先,我调整了您发布的代码以使其在我的系统上编译:

import java.util.*;
import java.util.stream.*;

class Test {
  public static void main(String[] args) {
    List<Object> list = new ArrayList<Object>();
    for(int i=0; i<100000; i++) {
      list.add(new Integer(i));
    }
    new Test().listToMap(list);
  }

    Map<Integer, Object> listToMap(List<Object> objs) {
        return objs.stream().collect(Collectors.toMap(Object::hashCode, obj -> obj));
    }
}

然后我编译它并在调试器中运行它,直到命中listToMap

$ javac Test.java && jdb Test
Initializing jdb ...
> stop in Test.listToMap
Deferring breakpoint Test.listToMap.
It will be set after the class is loaded.
> run
run Test
Set uncaught java.lang.Throwable
Set deferred uncaught java.lang.Throwable
>
VM Started: Set deferred breakpoint Test.listToMap

Breakpoint hit: "thread=main", Test.listToMap(), line=14 bci=0
14            return objs.stream().collect(Collectors.toMap(Object::hashCode, obj -> obj));

main[1]

然后我在java.util.HashMap.resize中设置一个断点并继续:

main[1] stop in java.util.HashMap.resize
Set breakpoint java.util.HashMap.resize
main[1] cont
>
Breakpoint hit: "thread=main", java.util.HashMap.resize(), line=678 bci=0

main[1]

cont投入了更多,直到我无聊为止:

main[1] cont
>
Breakpoint hit: "thread=main", java.util.HashMap.resize(), line=678 bci=0

main[1] cont
>
Breakpoint hit: "thread=main", java.util.HashMap.resize(), line=678 bci=0

main[1] cont
>
Breakpoint hit: "thread=main", java.util.HashMap.resize(), line=678 bci=0

main[1] cont
>
Breakpoint hit: "thread=main", java.util.HashMap.resize(), line=678 bci=0

main[1] cont
>
Breakpoint hit: "thread=main", java.util.HashMap.resize(), 
line=678 bci=0

main[1] print size
 size = 3073
main[1] cont
>
Breakpoint hit: "thread=main", java.util.HashMap.resize(), line=678 bci=0

main[1] print size
 size = 6145
main[1] cont
>
Breakpoint hit: "thread=main", java.util.HashMap.resize(), line=678 bci=0

main[1] print size
 size = 12289

所以是的:它绝对会不断地调整大小。

答案 1 :(得分:7)

  

第二种实现将涉及HashMap的多个调整大小,还是已经过优化以仅分配足够的内存?

在您的代码中,前者。参见https://stackoverflow.com/a/51333961/139985

值得注意的是您当前的实现方式:

  1. 调整大小消耗的许多额外内存将在下一次GC运行时回收。
  2. collect完成后,您仍然可以得到一个最大为最大2倍的主哈希数组。表中每个条目的“浪费”内存最多为8个字节,但平均每个条目为4个字节。
  3. 即使如此,哈希条目节点仍将是HashMap中最大的内存使用者。除了用于表示键和值的空间之外,每个条目大约消耗32个字节。

(以上数字假定为64位引用。)


或者,如果您使用toMap()中的4 argument overload,则可以提供Supplier来创建要填充的Map。这样您就可以执行以下操作:

  • 分配一个HashMap,其初始容量应足以避免调整大小,但又不能太大。
  • 使用Map的(假设)替代实现,该实现比HashMap少使用每个条目。
  • 为您的Map<K,V>K类型创建一个包装器,以填充未实现V ...的类似地图的对象。 (例如,您可能会使用GNU Trove库中的TLongObjectHashMap。)

(在后两种情况下,目标是找到使用更少内存(针对您的MapK类型的V或“类似地图”的类)具有适当的查询性能。)

答案 2 :(得分:1)

总结其他人说的话,并添加一些内容,这是使用自定义Collector的一种方法。但是,您应该记住两件事:

  1. 延续Stephen Chis answer的思想,直到发现这确实是应用程序中的性能瓶颈,您才真正担心优化这种情况。正如Donald Knuth所说,“过早的优化是万恶之源”。

  2. 正如shmosel在评论中指出的那样,如果在Collector中使用了HashMap,则分配了预定义大小的Collector的{​​{1}}并行模式。因此,我建议的Collector不支持并行收集。

话虽如此,您可以编写以下通用Collector

public class ExtraCollectors {

    public static <T, K, V> Collector<T, ?, HashMap<K, V>> toSizedMap(
            Function<? super T, ? extends K> keyMapper, Function<? super T, ? extends V> valueMapper, int size) {
        return toSequentialMap(
                () -> com.google.common.collect.Maps.newHashMapWithExpectedSize(size),
                keyMapper, valueMapper, Collector.Characteristics.UNORDERED
        );
    }

    public static <T, K, V, M extends Map<K, V>> Collector<T, ?, M> toSequentialMap(
            Supplier<M> mapSupplier, Function<? super T, ? extends K> keyMapper,
            Function<? super T, ? extends V> valueMapper, Collector.Characteristics... characteristics) {
        return Collector.of(
                mapSupplier,
                (map, element) -> map.merge(
                        keyMapper.apply(element), valueMapper.apply(element), ExtraCollectors::mergeUnsupported
                ),
                ExtraCollectors::combineUnsupported,
                characteristics
        );
    }

    private static <T> T mergeUnsupported(T valueA, T valueB) {
        throw new UnsupportedOperationException("This Collector does not support merging.");
    }

    private static <A> A combineUnsupported(A accumulatorA, A accumulatorB) {
        throw new UnsupportedOperationException("This Collector does not support parallel streams.");
    }
}

请注意,我使用了番石榴的Maps.newHashMapWithExpectedSize,因此您得到的HashMap恰好是您需要的大小(它的大小大致与Andreas在他对问题的评论中解释的一样)。如果您不依赖Guava(并且不想拥有),则可以将Maps.capacity方法复制到您的代码库中。

使用上面定义的ExtraCollectors.toSizedMap()方法,您的转换方法将如下所示:

Map<Long, KeyedObject> listToMap(List<? extends KeyedObject> objs) {
    return objs.stream().collect(ExtraCollectors.toSizedMap(KeyedObject::getKey, obj -> obj, objs.size()));

}

不过,如果您确实想要最大的性能(以可重用性为代价),则可以完全跳过Stream API,并应用解决方案1但使用Maps.newHashMapWithExpectedSize来获得{{1} }的大小正确。