一、clear()
和 ListUtils.newArrayListWithExpectedSize()区别
1、关于 clear()
和 ListUtils.newArrayListWithExpectedSize()
:
-
clear()
方法的优点:clear()
方法确实会清空列表的内容,但它不会更改列表的容量。也就是说,列表的内部数组仍然保持原来的大小,这样在后续的操作中,ArrayList
不需要重新分配内存,因此可以提高性能,尤其是在数据量较大的情况下。clear()
的操作是 O(n) 时间复杂度,但一般来说,clear()
被频繁调用时,它会更高效,因为它仅仅清空了引用,并没有重新分配内存。
-
ListUtils.newArrayListWithExpectedSize()
的作用:- 如果我们使用
ListUtils.newArrayListWithExpectedSize(BATCH_COUNT)
重新创建一个新的列表,它会创建一个新的ArrayList
对象,并根据期望的容量分配内存。这个操作的缺点是,每次批量保存后,都需要分配一个新的对象,造成额外的内存分配。 - 如果
BATCH_COUNT
较大,或者列表频繁被清空,重新创建新的列表会导致更多的内存分配,可能会比使用clear()
更低效。
- 如果我们使用
- 如果您的目的是减少内存分配,复用对象并提高性能,那么
clear()
是一个更合适的选择。它不会增加对象创建的开销,而且不会释放列表的内存,因此避免了频繁的内存分配和垃圾回收。 ListUtils.newArrayListWithExpectedSize()
适用于需要在每次处理完批次后完全清空并重新分配内存的场景。如果您需要保证每次处理时都有一个全新的、空的列表并且不关心内存复用,它是一个不错的选择。
2、什么时候使用 clear()
,什么时候使用 newArrayListWithExpectedSize()
?
-
使用
clear()
:在大多数情况下,尤其是当您希望复用同一个ArrayList
对象时,clear()
是推荐的做法。它避免了重新创建对象,并且通过保持内存容量,减少了内存分配的成本。 -
使用
newArrayListWithExpectedSize()
:如果您担心某些场景下clear()
不够高效,或者您希望确保每个批次都有一个干净的列表,并且不关心内存复用,可以选择重新创建新的ArrayList
。不过,这会带来一些额外的内存分配和性能开销。
所以,基于您的情况,如果目标是提高性能并避免不必要的内存分配,clear()
方法是更好的选择,您无需额外使用 ListUtils.newArrayListWithExpectedSize()
。clear()
会清空列表内容,但不重新分配内存,这在内存复用和性能方面是更优的做法。
二、ListUtils.newArrayListWithExpectedSize(BATCH_COUNT) 的底层实现
1. newArrayListWithExpectedSize
方法:
public static <E> ArrayList<E> newArrayListWithExpectedSize(int estimatedSize) { return new ArrayList(computeArrayListCapacity(estimatedSize)); }
-
这个方法接收一个
estimatedSize
参数,表示预期的数组大小(或列表元素数量)。它的目的是根据预期的大小来创建一个ArrayList
,并优化其初始容量,以避免动态扩容带来的性能开销。 -
方法首先调用了
computeArrayListCapacity(estimatedSize)
,计算出ArrayList
应该使用的初始容量,随后使用这个容量来初始化ArrayList
。
2. computeArrayListCapacity
方法:
static int computeArrayListCapacity(int arraySize) { checkNonnegative(arraySize, "arraySize"); return IntUtils.saturatedCast(5L + (long)arraySize + (long)(arraySize / 10)); }
-
参数验证:
checkNonnegative(arraySize, "arraySize")
是一个参数验证方法,确保arraySize
不为负数。如果参数无效(负数),通常会抛出异常。 -
计算初始容量:计算初始容量的核心逻辑如下:
return IntUtils.saturatedCast(5L + (long)arraySize + (long)(arraySize / 10));
-
5L + (long)arraySize + (long)(arraySize / 10)
计算出了一个优化的初始容量。 -
5L
:这是一种常见的“最小”初始容量偏移量(通常是为了保证容量不会太小),可以理解为提供一个起始容量,确保ArrayList
在创建时不会因为元素很少而分配一个过小的数组。 -
(long)arraySize
:这个值即传入的estimatedSize
,即预期的数组大小。 -
(long)(arraySize / 10)
:额外分配 10% 的空间,以允许一定的增长,这样可以避免当数据量增长时,每次添加元素时都需要扩容。 -
为什么这样计算? 计算方式基于两个方面:
- 初始容量:除了直接基于预期大小分配外,还加上一个固定的 5 和 10% 的附加空间,避免频繁的扩容。增加的这部分空间是为了适应未来可能的扩展,使
ArrayList
在添加元素时不容易触发扩容操作,从而减少了内存重分配的频率。 - 优化动态扩容:如果仅根据
estimatedSize
来设置容量,可能会导致频繁的内存扩容,而额外的 10% 容量则允许列表在初期就有额外的空间,用于应对稍微高于估计大小的情况。
- 初始容量:除了直接基于预期大小分配外,还加上一个固定的 5 和 10% 的附加空间,避免频繁的扩容。增加的这部分空间是为了适应未来可能的扩展,使
-
3. IntUtils.saturatedCast
方法:
return IntUtils.saturatedCast(5L + (long)arraySize + (long)(arraySize / 10));
-
saturatedCast
是一个常见的做法,用来确保计算出的结果不会超出int
类型的范围。如果结果超出了int
范围,它会返回Integer.MAX_VALUE
。具体地:
-
saturatedCast(long value)
会将long
类型的值转化为int
类型。如果计算结果大于Integer.MAX_VALUE
,它会将结果设置为Integer.MAX_VALUE
,以防止溢出。 -
为什么需要
saturatedCast
?ArrayList
的容量最终会使用int
类型来表示,而long
类型计算的容量可能会超出int
的最大值(Integer.MAX_VALUE
)。使用saturatedCast
可以确保即使在容量计算超出int
范围时,也不会导致溢出,保证程序的稳定性。
-
4. 总结:EasyExcel 内部实现的优化思路:
-
预分配适当的内存容量:通过
computeArrayListCapacity
方法,EasyExcel 会根据预期的数据大小计算出一个合理的初始容量,并且通过加上 10% 的扩展空间,减少了内存的频繁重新分配。 -
避免频繁的扩容:通过在初始容量时就考虑 10% 的附加空间,避免了当数据量略超预期时,
ArrayList
的容量需要多次扩展。这样可以减少ArrayList
动态扩容的次数,提高性能,特别是在处理大量数据时。 -
避免溢出:通过
saturatedCast
保证计算结果不会超出int
的范围,避免了容量计算过程中可能导致的溢出问题。
5. 实践中的优点:
- 性能优化:通过预分配合适容量并避免动态扩容,EasyExcel 可以显著提高大数据量处理时的性能,减少内存重分配的开销。
- 内存管理:通过合理的容量计算(加上 10% 的附加空间),EasyExcel 可以在不浪费过多内存的前提下,确保列表有足够的空间容纳即将添加的元素。
总结:
这段代码的主要目的是通过预分配适当的内存容量和扩容空间来优化 ArrayList
的性能,避免频繁扩容和内存重分配,从而提高数据处理效率。这是 EasyExcel 在处理大量数据时对性能的优化措施之一。