Java:性能优化细节11-20
11. 尽量合理的创建HashMap
当你要创建一个比较大的hashMap时,充分利用这个构造函数
public HashMap(int initialCapacity, float loadFactor);
合理初始化HashMap
的容量和负载因子是提高HashMap
性能的关键因素之一。HashMap
在Java中是一种广泛使用的数据结构,用于存储键值对。正确地设置其初始容量(initialCapacity
)和负载因子(loadFactor
)可以显著减少哈希表重构的次数,从而优化性能。
初始容量(initialCapacity
)
- 初始容量是哈希表在创建时的容量。
- 如果预先知道将要存储的键值对数量,指定一个接近或稍微大于预计数量的初始容量可以减少或避免后续的扩容操作。
- 扩容操作(即重新哈希)涉及到创建一个新的哈希表并重新计算每个元素的位置,这是一个相对昂贵的操作,特别是对于大型
HashMap
。
负载因子(loadFactor
)
- 负载因子是哈希表在其容量自动增加之前可以达到多满的一种度量。它是一个介于0和1之间的浮点数。
- 默认的负载因子(0.75)提供了时间和空间成本之间的良好折衷。增加负载因子可以减少空间开销,但增加了查找成本(反映在大多数
HashMap
操作中,包括get
和put
)。 - 在设置负载因子时,需要权衡空间利用率和查询性能。如果预期
HashMap
中的元素会非常密集地填充,可以考虑使用稍高的负载因子;反之,如果性能是首要考虑,而空间不是问题,则可以使用较低的负载因子。
实践建议
- 在创建
HashMap
时,如果有关于将要存储的元素数量的估计,应使用带有初始容量和负载因子的构造函数,以避免或减少扩容操作。 - 对于极大或极小的数据集,可以适当调整负载因子以优化性能或空间使用率。
- 记住,过大的初始容量可能会浪费内存,而过小的初始容量可能会导致频繁的扩容。找到合适的平衡点是关键。
通过这种方式,可以确保HashMap
在性能和空间使用上都得到优化,特别是在处理大量数据时。这同样适用于Hashtable
和Vector
等其他Java集合框架中的类,它们也提供了类似的构造函数来调整初始容量和负载因子。
12. 尽量减少对变量的重复计算
减少在循环中对变量或方法的重复计算是优化代码性能的一个有效手段。这种优化可以减少计算开销,特别是当循环体中的表达式涉及到复杂计算或调用成本较高的方法时。
示例优化分析
原始代码:
for(int i=0; i < list.size(); i++)
每次循环迭代都会调用list.size()
,如果list.size()
的调用成本较高,或者list
是一个同步集合,这可能会导致性能瓶颈。
优化后的代码:
for(int i=0, len=list.size(); i < len; i++)
在循环开始之前计算了list.size()
并将其存储在len
变量中,这样每次循环迭代时就避免了对list.size()
的重复调用。这种方式特别适用于循环次数较多的情况,可以显著减少总体的计算开销。
为什么这种优化有效
- 减少调用开销:避免在每次迭代中重复调用方法,尤其是那些在循环执行期间不会改变返回值的方法。
- 减少计算量:对于复杂的表达式,将其结果预先计算并存储可以避免每次迭代时的重复计算。
更多优化策略
- 预先计算:如果循环体内有可以预先计算的表达式,应该将它们移出循环。
- 简化表达式:简化循环条件和循环体内的表达式。例如,如果循环体内的某些计算不依赖于循环变量,可以将它们提前计算。
-
使用增强的for循环:当遍历集合或数组时,考虑使用Java的增强for循环(
for-each
循环),它可以减少代码量并避免一些性能问题,尽管在某些情况下它可能不如传统for循环灵活。
13. 尽量避免不必要的创建
原始代码:
A a = new A();
if(i==1){
list.add(a);
}
优化建议
优化后的代码示例将对象a
的创建移入了条件判断内部,这样对象a
只有在条件满足时才会被创建,从而避免了不必要的对象创建:
if(i == 1) {
A a = new A();
list.add(a);
}
注意点
-
作用域限制:将对象
a
的创建移到条件语句内部会限制a
的作用域仅在条件语句内,如果后续代码需要使用a
,这种方式可能会引入作用域相关的问题。 -
条件外引用:如果需要在条件外部引用
a
,则需要确保a
在条件外部有定义,并且在使用前已经正确初始化。例如:A a = null; if(i == 1) { a = new A(); } if(a != null) { list.add(a); }
更广泛的应用
- 懒加载:这种优化思路不仅适用于简单的条件判断,还可以应用于更复杂的场景,如对象的懒加载(按需创建)。
- 资源管理:在处理资源密集型对象时(如文件流、数据库连接等),确保仅在需要时才创建和打开资源,使用完毕后及时关闭,可以显著降低资源消耗和提高应用性能。
14. 尽量在finally块中释放资源
程序中使用到的资源应当被释放,以避免资源泄漏,这最好在finally块中去做。不管程序执行的结果如何,finally块总是会执行的,以确保资源的正确关闭。
在Java中,使用资源如文件流、数据库连接或网络连接时,必须确保这些资源在使用完成后被正确关闭,以避免资源泄漏,这可能导致性能下降和资源耗尽。
使用finally
块释放资源
在try-catch
语句中,finally
块提供了一个确保执行清理工作的机制,不管是否发生异常,finally
块中的代码都会执行。因此,将资源释放放在finally
块中是一个好习惯,例如:
FileInputStream in = null;
try {
in = new FileInputStream("file.txt");
// 使用资源
} catch (IOException e) {
e.printStackTrace();
} finally {
if (in != null) {
try {
in.close(); // 确保资源被关闭
} catch (IOException e) {
e.printStackTrace();
}
}
}
使用try-with-resources
语句
Java 7引入了try-with-resources
语句,进一步简化了资源管理。此语法确保实现了AutoCloseable
接口的资源会在语句结束时自动关闭,即使出现异常也是如此。使用try-with-resources
可以使代码更简洁,减少出错的可能:
try (FileInputStream in = new FileInputStream("file.txt")) {
// 使用资源
} catch (IOException e) {
e.printStackTrace();
}
在这个示例中,FileInputStream
资源在try
块结束时会自动关闭,无需显式地在finally
块中关闭它。这种方式不仅减少了代码量,还提高了代码的可读性和可维护性。
最佳实践
- 对于Java 7及以上版本,优先使用
try-with-resources
语句自动管理资源。 - 如果因为某些原因不能使用
try-with-resources
(如使用的Java版本较低),确保在finally
块中释放资源。 - 在释放资源时,考虑到可能的
NullPointerException
和其他异常,确保资源释放代码的健壮性。
15. 尽量使用移位来代替’a/b’的操作
在编程中,特别是在性能敏感的应用中,使用位移操作代替除法是一种优化性能的常见手段。位移操作在底层硬件上通常比除法操作要快得多,因为它们可以直接通过移动数字的位来实现,而不涉及复杂的算术运算。
示例说明
使用位移操作来优化除以2的幂次的操作:
- 除以4等价于向右位移2位(
a >> 2
)。 - 除以8等价于向右位移3位(
a >> 3
)。
注意事项
- 适用范围:这种优化只适用于除数是2的幂次(如2、4、8、16等)的情况。对于非2的幂次的除法操作,不能直接用位移操作来替代。
-
有符号和无符号移位:Java提供了两种右位移操作。
>>
是有符号右移,它会保留符号位(即负数在移位后仍然是负数),而>>>
是无符号右移,不考虑符号位,空位都以0填充。根据具体情况选择适当的操作。 - 精度问题:位移操作与除法操作在某些情况下的结果可能不完全相同,特别是涉及到负数时。位移操作是直接对二进制位进行操作,不会四舍五入或进行任何形式的调整。
注释的重要性
由于位移操作对于不熟悉二进制运算的人可能不太直观,因此,在使用位移操作时添加清晰的注释是一个好习惯。注释应解释位移操作的目的,以及它是如何等价于特定的除法操作的。例如:
// 使用位移操作来实现除以8,等价于 a / 8
int num = a >> 3;
这样做可以帮助维护代码的其他人员更容易理解代码的意图,保证代码的可读性和可维护性。
16.尽量使用移位来代替’a*b’的操作
使用位移操作来代替乘法操作,尤其是乘以2的幂次数时,确实可以提高计算效率。位移操作直接在二进制级别上移动位,比传统的乘法操作要快,因为它避免了复杂的算术运算过程。这种优化手段在性能敏感的应用中特别有价值,如图形处理、游戏开发和低级系统编程等领域。
示例说明
使用位移操作来优化乘以2的幂次的操作:
- 乘以4等价于向左位移2位(
a << 2
)。 - 乘以8等价于向左位移3位(
a << 3
)。
注意事项
- 适用范围:这种优化只适用于乘数是2的幂次(2、4、8、16等)。对于其他乘数,不能使用位移操作来替代。
-
结果的范围:需要注意的是,位移操作可能导致整数溢出,尤其是当操作的数值很大,接近其类型(如
int
或long
)的最大值时。因此,使用位移操作时要考虑结果是否会超出预期的数值范围。 - 有符号数:对于有符号数,左位移操作会影响符号位。在进行位移操作时,应确保不会不小心改变了数值的符号。
17. 尽量确定StringBuffer的容量
StringBuffer 的构造器会创建一个默认大小(通常是16)的字符数组。在使用中,如果超出这个大小,就会重新分配内存,创建一个更大的数组,并将原先的数组复制过来,再丢弃旧的数组。在大多数情况下,你可以在创建 StringBuffer的时候指定大小,这样就避免了在容量不够的时候自动增长,以提高性能。
如:
StringBuffer buffer = new StringBuffer(1000);
正确初始化StringBuffer
(或其后继者StringBuilder
)的容量可以显著提高字符串处理的性能,尤其是在处理大量字符串拼接操作时。这是因为每次StringBuffer
(或StringBuilder
)容量不足以容纳更多的字符时,它都需要执行一个扩容操作,这包括分配一个新的更大的数组,复制旧数组中的内容到新数组,然后丢弃旧数组。这个过程不仅消耗时间,还会增加临时内存的使用,从而可能影响垃圾收集器的效率。
为什么指定容量是个好主意
-
减少扩容次数:预先估计所需容量并在
StringBuffer
初始化时指定,可以最小化或完全避免扩容操作,从而提高性能。 - 减少内存开销:通过避免不必要的数组复制和临时对象创建,可以减少内存使用,特别是在大量字符串操作的上下文中。
StringBuffer
vs StringBuilder
虽然这里讨论的是StringBuffer
,但对于单线程环境,StringBuilder
通常是更好的选择,因为它不是线程安全的,因此不会因为同步操作而产生额外的性能开销。和StringBuffer
一样,你也可以在创建StringBuilder
实例时指定初始容量。
使用建议
-
预估容量:在使用
StringBuffer
或StringBuilder
之前,尽量预估最终字符串的长度,并据此指定一个初始容量。这可能需要根据使用场景进行一些经验估计。 -
单线程使用
StringBuilder
:如果你的代码是在单线程环境下运行,考虑使用StringBuilder
代替StringBuffer
,因为前者在没有线程安全需求时提供了更好的性能。 - 动态估计:在某些情况下,如果你事先不知道需要多少容量,可以考虑根据动态数据来估计容量。例如,如果你在循环中添加字符串,可以根据循环次数和平均字符串大小来计算一个大致的容量需求。
18. 尽量早释放无用对象的引用
合理管理对象引用对于优化Java程序的内存使用和性能非常重要。虽然Java虚拟机(JVM)的垃圾收集器(GC)会自动回收无用对象占用的内存,但是合理地控制对象的生命周期可以更早地释放内存,特别是在处理大对象或内存敏感的应用中。
局部变量的引用释放
对于方法中的局部变量,一旦变量超出了其作用域,就会自然地失去引用,其指向的对象也就变成了垃圾回收的候选对象。因此,通常情况下,无需显式地将局部引用变量设置为null
。下面的做法是多余的:
public void test() {
Object obj = new Object();
// ...其他操作...
obj = null; // 没有必要
}
长生命周期的对象引用释放
然而,在某些情况下,尤其是在对象的引用在长时间执行的方法中仍然保持活跃,但实际上已经不再需要时,显式地释放这些引用(即将引用设置为null
)可能有助于垃圾收集器更早地回收这些对象,从而减少内存占用,尤其是在如下情况:
public void test() {
Object obj = new Object();
// ...其他操作...
obj = null; // 在执行耗时或耗内存的操作前显式释放引用
// 执行耗时,耗内存操作;或调用耗时,耗内存的方法
}
最佳实践
-
对于短生命周期的局部变量,无需显式设为
null
:方法执行完毕后,局部变量会自动失去作用域,因此它们引用的对象会自然成为垃圾回收的候选。 -
在长时间运行的方法中及时释放不再需要的对象引用:如果方法执行时间很长,且某些对象在方法中早期就已经不再需要,那么将这些对象引用设为
null
可以帮助垃圾收集器更早回收这些对象。 -
谨慎使用
null
:虽然在某些情况下设为null
有助于内存管理,但过度使用可能会使代码难以理解和维护,甚至引入空指针异常的风险。因此,仅在必要时才这样做,并确保这种做法的目的和理由是清晰的。
19. 尽量避免使用二维数组
在Java中,数组是存储在堆内存上的对象。一个二维数组,从本质上讲,是一个数组的数组,即外层数组的每个元素都是另一个数组的引用。这种结构确实比一维数组使用更多的内存,主要原因包括:
- 引用开销:在二维数组中,外层数组包含了对内层数组的引用。每个引用本身就是一个额外的内存开销。
- 对象头开销:Java中的每个对象都有一个对象头(包含类指针、垃圾收集信息等),因此每个内层数组都会有这样的开销。
- 内存对齐:Java虚拟机可能会对对象数据进行内存对齐,增加额外的填充以优化性能,这也可能导致额外的内存使用。
使用场景和替代方案
- 使用场景:在需要表达多维数据结构时,二维数组是一种直观的选择。例如,在处理矩阵、网格或任何需要行和列结构的数据时。
- 性能考虑:如果性能和内存效率是首要考虑的话,可以考虑其他数据结构作为替代。例如,可以使用一维数组来模拟二维数组,通过计算索引来访问元素,这样可以减少内存的开销,尤其是在处理非常大的数组时。
-
替代方案:对于复杂的数据结构,还可以考虑使用集合类(如
ArrayList
),或者创建自定义类来更有效地管理数据和内存。
20. 尽量避免使用split
避免使用split
方法的建议主要基于性能考虑。String.split()
方法在Java中确实是基于正则表达式来分割字符串的,这意味着即使是简单的分割操作也涉及到正则表达式的编译和匹配过程,这在处理大量数据时可能会成为性能瓶颈。
性能影响
-
正则表达式编译:
split
方法内部使用正则表达式,每次调用时都需要编译正则表达式,这是一个相对昂贵的操作。 - 复杂度:正则表达式的复杂度可能会对性能产生显著影响,特别是当表达式复杂或输入字符串较长时。
替代方案
-
使用
StringUtils.split
:Apache Commons Lang库提供的StringUtils.split
方法是一个不基于正则表达式的替代方案,它对于简单的分隔符分割操作来说,性能通常优于String.split()
。 -
手动实现分割逻辑:对于非常特定的分割需求,手动实现分割逻辑(如使用循环和
String.substring()
)可能会提供更好的性能。这种方法允许细粒度控制和优化,但可能会增加实现的复杂性。文章来源:https://www.toymoban.com/news/detail-833303.html -
结果缓存:如果对相同的字符串执行相同的分割操作多次,考虑将结果缓存起来重用,以避免重复的分割操作。文章来源地址https://www.toymoban.com/news/detail-833303.html
使用建议
-
需求分析:在选择使用
split
还是其他方法之前,应该根据实际需求进行分析。如果分割操作简单且不频繁,使用split
可能是可接受的。对于高性能需求,考虑使用更高效的方法。 - 性能测试:在决定最终实现时,进行性能测试比理论分析更为重要。不同的方法在不同的上下文和数据集上可能表现不同,实际测试可以帮助确定最合适的选择。
到了这里,关于Java:性能优化细节11-20的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!