我有一个将列表转换为地图的函数。调用此函数后,地图的大小不会改变。我正在尝试在以下两种实现之间做出决定:
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的多个调整大小,或者是否已对其进行优化以分配足够的内存?
答案 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
值得注意的是您当前的实现方式:
collect
完成后,您仍然可以得到一个最大为最大2倍的主哈希数组。表中每个条目的“浪费”内存最多为8个字节,但平均每个条目为4个字节。HashMap
中最大的内存使用者。除了用于表示键和值的空间之外,每个条目大约消耗32个字节。(以上数字假定为64位引用。)
或者,如果您使用toMap()
中的4 argument overload,则可以提供Supplier
来创建要填充的Map
。这样您就可以执行以下操作:
HashMap
,其初始容量应足以避免调整大小,但又不能太大。Map
的(假设)替代实现,该实现比HashMap
少使用每个条目。Map<K,V>
和K
类型创建一个包装器,以填充未实现V
...的类似地图的对象。 (例如,您可能会使用GNU Trove库中的TLongObjectHashMap
。)(在后两种情况下,目标是找到使用更少内存(针对您的Map
和K
类型的V
或“类似地图”的类)具有适当的查询性能。)
答案 2 :(得分:1)
总结其他人说的话,并添加一些内容,这是使用自定义Collector
的一种方法。但是,您应该记住两件事:
延续Stephen C中his answer的思想,直到发现这确实是应用程序中的性能瓶颈,您才真正担心优化这种情况。正如Donald Knuth所说,“过早的优化是万恶之源”。
正如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} }的大小正确。