Java Collectors.toMap的内存优化
我有一个将列表转换为映射的函数。调用此函数后,贴图的大小不会更改。我试图在以下两种实现之间做出选择:Java Collectors.toMap的内存优化,java,java-8,java-stream,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 : ob
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));
}
映射列表映射(列表对象){
/*实施一:*/
Map Map=newhashmap(objs.size(),1);
用于(对象对象对象:对象对象对象){
put(obj.getKey(),obj);
}
返回图;
/*实施二:*/
返回objs.stream().collect(Collectors.toMap(Object::getKey,obj->obj));
}
在第一个实现中,我使用1的负载因子和列表的大小为所有元素分配了足够的内存。这可确保不会执行调整大小操作。然后,我遍历列表并逐个添加元素
在第二个实现中,我使用Java8流来提高可读性
我的问题是:第二个实现是否会涉及HashMap的多个大小调整,或者它是否经过优化以分配足够的内存?第二个实现将涉及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));
}
}
然后我在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
所以是的:它肯定会一次又一次地调整大小
第二个实现是涉及HashMap的多个大小调整,还是经过优化以分配足够的内存
在你的代码中,前者。看
值得注意的是,对于您当前的实施:
collect
完成后,您仍然可能会得到一个主散列数组,该数组的大小可能是原来的2倍。表中每个条目的内存“浪费”可能高达8字节,但平均每个条目的内存“浪费”为4字节HashMap
中最大的内存消耗者。每个条目大约消耗32字节。。。除了用于表示键和值的空间之外或者,如果您使用
toMap()
的,您可以提供供应商
来创建要填充的映射。这允许您执行以下操作:
- 分配一个初始容量足够大的
HashMap
,以避免调整大小,但不要太大
- 使用
Map
的(假设的)替代实现,它比HashMap
使用更少的内存
- 创建一个包装器来填充不实现
map
。。。对于您的K
和V
类型。(例如,您可能会使用GNU-Trove库中的TLongObjectHashMap
)
(在后两种情况下,目标是找到一个Map
或“Map-like”类,它使用更少的内存(对于K
和V
类型),但仍然具有适当的查找性能。)总结其他人所说的内容并添加一点,下面是一种使用自定义收集器的方法。但是,您应该记住两件事:
继续考虑从,您不应该真正担心优化这些情况,直到您发现它确实是应用程序中的性能瓶颈。正如所说,“过早优化是万恶之源”
正如注释中指出的,如果在并行模式下使用所述收集器
,则分配具有预定义大小的哈希映射
的收集器将过度分配。因此,我建议的收集器
不支持并行收集
话虽如此,您可以编写以下通用的Collector
s:
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.");
}
}
尽管如此,如果您确实希望获得最大的性能(以可重用性为代价),您可以完全跳过流
API,并使用Maps.newHashMapWithExpectedSize
应用您的解决方案1,以获得正确的HashMap
您可以使用接受地图供应商的。但我不明白为什么你认为1的负载系数是理想的。这会降低查找的效率。这意味着它的bucket数将与条目数相同,这会增加冲突的可能性。有关更多详细信息,请参阅。要回答您的问题,标准实现仅使用HashMap::new
,因此没有针对大小流的优化。这可能也不是一个好主意,因为它在并行执行时会过度分配。加载因子本身不会影响性能,内部哈希表中的桶数也会影响性能。通常,加载因子用于告诉HashMap何时重新调整哈希表的大小。已经发现75%的默认负载因子是大小与性能之间的一个很好的折衷,因此除非您知道得更好,否则请坚持这一点,这意味着您需要创建大小为objs.size()/75%
akaobjs.size()/(3/4)
akaobjs.size()*4/3+1
(+1强制取整)。如果这样做,任何75%或更高的加载因子都不会调整哈希表的大小(如果map.size()只有当您有一个。
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.");
}
}
Map<Long, KeyedObject> listToMap(List<? extends KeyedObject> objs) {
return objs.stream().collect(ExtraCollectors.toSizedMap(KeyedObject::getKey, obj -> obj, objs.size()));
}