代码优化的重要性:如何避免未知错误并提升项目稳定性
来源:五月的仓颉
链接:cnblogs.com/xrq730/p/4865416.html在我看来,代码优化最重要的作用应该是避免未知的错误。线上运行代码的过程中,经常会出现很多意想不到的错误,因为线上环境和开发环境有很大的不同,而错误最终往往定位到一个很小的原因。
不过,为了解决这个错误,我们需要先进行自我验证,然后打包要替换的class文件,暂停业务并重新启动。对于一个成熟的项目来说,最后一个其实有非常大的影响,这意味着用户在这段时间无法访问应用程序。因此,在编写代码时,从源头上关注各种细节,权衡并使用最佳选项,这将在很大程度上避免未知的错误,从长远来看也会大大减少工作量。
代码优化的目标是:
本文内容有的来自网络,有的来自日常工作学习。当然,这并不重要。重要的是这些代码优化的细节是否真的有用。这篇文章会长期更新。只要有值得分享的代码优化细节,本文就会不定期更新。
代码优化细节
1、尽量指定类和方法的final修饰符。
带有final修饰符的类不会被派生。在Java核心API中,有很多final应用的例子,比如java.lang.String,整个类都是final的。为类指定final修饰符可防止该类被继承,为方法指定final修饰符可防止该方法被重写。如果一个类被指定为final,则该类的所有方法都是final的。 Java 编译器将寻找机会内联所有最终方法。内联对于提高Java运行效率有着显着的作用。有关详细信息,请参阅 Java 运行时优化。这可以平均提高 50% 的性能。
2. 尽可能重复使用对象
特别是对于String对象的使用,在发生字符串连接时应该使用StringBuilder/StringBuffer来代替。由于Java虚拟机不仅花时间生成对象,将来可能还需要花时间垃圾收集和处理这些对象。因此,生成过多的对象会对程序的性能产生很大的影响。
3.尽可能使用局部变量
调用方法时传递的参数以及调用过程中创建的临时变量都保存在栈中,速度更快。其他变量,如静态变量、实例变量等都是在堆中创建的,速度较慢。此外,在堆栈上创建的变量的内容随着方法结束而消失,并且不需要额外的垃圾收集。
4、及时关闭流量
在Java编程中,进行数据库连接和I/O流操作时必须小心。使用完毕后及时关闭,释放资源。因为操作这些大对象会造成很大的系统开销,稍有不慎就会导致严重的后果。
5. 尽量减少变量的双重计算
澄清一个概念,调用一个方法,即使方法中只有一条语句,其代价也是高昂的,包括创建栈帧、调用方法时保护场景、调用方法时恢复场景等。所以对于例如以下操作:
for (int i = 0; i < list.size(); i++)
{...}建议将其替换为:
for (int i = 0, length = list.size(); i < length; i++)
{...}这样当list.size()很大的时候,就减少了很多消耗。
6、尽量采用懒加载策略,即只在需要的时候创建。
例如:
String str = "aaa";
if (i == 1)
{
list.add(str);
}建议将其替换为:
if (i == 1)
{
String str = "aaa";
list.add(str);
}7. 谨慎使用异常
异常不利于性能。要抛出异常,必须首先创建一个新对象。 Throwable 接口的构造函数调用名为 fillInStackTrace() 的本地同步方法。 fillInStackTrace() 方法检查堆栈并收集调用跟踪信息。每当抛出异常时,Java虚拟机必须调整调用堆栈,因为在处理过程中会创建一个新对象。异常应该只用于错误处理,而不应该用于控制程序流程。
8.不要在循环中使用try...catch...。它应该放在最外层。
根据网友提出的意见,我认为这一点值得讨论。欢迎大家提出您的意见!
9. 如果要添加的内容的长度可以估计,请为底层集合和以数组方式实现的工具类指定初始长度。
例如ArrayList、LinkedLlist、StringBuilder、StringBuffer、HashSet等,以StringBuilder为例:
StringBuilder() // 默认分配16个字符的空间
StringBuilder(int size) // 默认分配size个字符的空间
StringBuilder(String str) // 默认分配16个字符+str.length()个字符空间可以通过类的构造函数(这里指的不仅仅是上面的StringBuilder)来设置其初始化能力,这样可以显着提高性能。例如StringBuilder,length表示当前StringBuilder可以容纳的字符数。
因为当StringBuilder达到其最大容量时,它会将其容量增加到当前容量的2倍加上2。每当StringBuilder达到其最大容量时,它必须创建一个新的字符数组,然后用它替换旧的字符。数组的内容被复制到一个新的字符数组 - 这是一个非常消耗性能的操作。试想一下,如果在不指定长度的情况下,可以估计字符数组中会存储大约5000个字符,最接近的2到5000的幂是4096,每次扩展都加2,那么:
这样既浪费了内存空间,又降低了代码运行效率。因此,为底层集合和用数组实现的工具类设置合理的初始化容量是没有错的,会带来立竿见影的效果。
但请注意,对于像HashMap这样以数组+链表实现的集合,不要将初始大小设置为与你估计的大小相同,因为一张表只连接一个对象的可能性几乎为0。建议初始大小设置为2的N次方。如果可以估计有2000个元素,可以设置为new HashMap(128)或者new HashMap(256)。
10、复制大量数据时,使用System.arraycopy()命令
11.乘法和除法使用移位运算
例如:
for (val = 0; val < 100000; val += 5)
{
a = val * 8;
b = val / 2;
}使用移位操作可以大大提高性能,因为在计算机底层,位操作是最方便、最快的,所以建议修改为:
for (val = 0; val < 100000; val += 5)
{
a = val << 3;
b = val >> 1;
}移位操作虽然很快,但是可能会导致代码难以理解,所以最好添加相应的注释。
12.不要在循环内不断创建对象引用
例如:
for (int i = 1; i <= count; i++)
{
Object obj = new Object();
}这种方法会导致内存中存在count个Object对象引用。如果计数很大,就会消耗内存。建议改为:
Object obj = null;
for (int i = 0; i <= count; i++)
{
obj = new Object();
}在这种情况下,内存中只有一份 Object 对象引用的副本。每次调用new Object()时,Object对象引用都指向不同的Object,但内存中只有一份,这样就节省了很多。
13、基于效率和类型检查的考虑,尽可能使用数组,只有在无法确定数组大小的情况下才使用ArrayList。
14. 尝试使用HashMap、ArrayList、StringBuilder。除非线程安全需要,否则不建议使用Hashtable、Vector、StringBuffer。后三者由于使用同步机制而导致性能开销。
15. 不要将数组声明为 public static final
因为这是没有意义的。这仅将引用定义为静态最终。数组的内容仍然可以随意改变。将数组声明为公共是一个安全漏洞,这意味着该数组可以被外部类更改。
16.在适当的情况下尝试使用单例。
使用单例可以减轻加载负担,缩短加载时间,提高加载效率,但并不是所有地方都适用。简单来说,单例主要适合以下三个方面:
17.尽量避免随意使用静态变量。
要知道,当一个对象被定义为static的变量引用时,gc通常不会回收这个对象占用的堆内存,如:
public class A
{
private static B b = new B();
}此时静态变量b的生命周期与A类相同,如果A类没有被卸载,那么引用B指向的B对象就会驻留在内存中,直到程序终止。
18.及时清理不再需要的会话
为了清除不再活动的会话,许多应用程序服务器都有默认的会话超时,通常为 30 分钟。当应用服务器需要保存更多会话时,如果内存不足,操作系统会将部分数据转移到磁盘上。应用服务器还可能根据MRU(最近使用的)算法将一些不活动的会话转储到磁盘。甚至可能会引发内存不足异常。如果要将会话转储到磁盘,则必须首先对其进行序列化,而序列化对象的成本很高。因此,当不再需要session时,应及时调用HttpSession的invalidate()方法来清除session。
19. 实现RandomAccess接口的集合,例如ArrayList,应该使用最常见的for循环而不是foreach循环来遍历。
这是JDK向用户推荐的。 JDK API对RandomAccess接口的解释是:实现RandomAccess接口用于表明支持快速随机访问。该接口的主要目的是允许通用算法改变其行为,从而在应用于随机或连续访问列表时提供良好的性能。表现。
实践经验表明,如果随机访问实现RandomAccess接口的类实例,使用普通的for循环会比使用foreach循环更高效;相反,如果顺序访问,使用Iterator会更高效。可以使用类似下面的代码来进行判断:
if (list instanceof RandomAccess)
{
for (int i = 0; i < list.size(); i++){}
}
else
{
Iterator> iterator = list.iterable();
while (iterator.hasNext()){iterator.next()}
}foreach循环的底层实现原理是迭代器Iterator,参见Java语法糖1:变长参数和foreach循环原理。所以后半句“另一方面,如果是顺序访问的话,使用Iterator会更高效”的意思就是那些顺序访问的类实例是使用foreach循环来遍历的。
20.使用同步代码块而不是同步方法
这一点在多线程模块中synchronized锁方法块一文中已经讲得很清楚了。除非确定整个方法需要同步,否则尽量使用同步代码块,避免出现不需要同步的代码。还要进行同步,影响代码执行效率。
21、将常量声明为staticfinal,并以大写字母命名
这样就可以在编译时将这些内容放入常量池中,避免运行时计算常量的值。另外,用大写字母命名常量也可以更容易区分常量和变量。
22.不要创建一些未使用的对象,也不要导入一些未使用的类。
这是没有意义的。如果代码中出现“局部变量i的值没有被使用”和“The import java.util is neverused”,那么请删除这些无用的内容。
23、程序运行时避免使用反射
请参阅反射。反射是Java为用户提供的一个非常强大的功能。功能强大往往意味着效率低下。程序运行过程中不建议使用反射机制,特别是频繁使用反射机制,尤其是方法的invoke方法。如果确实有必要,推荐的做法是对项目启动时需要通过反射加载的类使用反射实例。创建一个对象并将其放入内存中——用户只关心与同伴交互时获得最快的响应,而不关心同伴的项目需要多长时间才能启动。
24、使用数据库连接池和线程池
两个池都用于重用对象。前者可以避免频繁打开和关闭连接,后者可以避免频繁创建和销毁线程。
25.使用缓冲输入和输出流进行IO操作
缓冲输入输出流,即BufferedReader、BufferedWriter、BufferedInputStream、BufferedOutputStream,可以大大提高IO效率
26、顺序插入和随机访问较多的场景使用ArrayList,元素删除和中间插入较多的场景使用LinkedList。
这个可以通过了解LinkedList的原理来理解。
27.公共方法中不要有太多形式参数
公共方法是提供给外界的方法。如果给这些方法赋予太多的形式参数,则有两个主要缺点:
至于“太多”指的是多少,我猜是3个或者4个。比如我们用JDBC写了一个insertStudentInfo方法。有 10 个学生信息字段需要插入到 Student 表中。这10个参数可以封装在一个实体类中作为insert方法的形参。
28、当字符串变量和字符串常量相等时,将字符串常量写在前面。
这是一个比较常见的技巧。如果您有以下代码:
String str = "123";
if (str.equals("123"))
{
...
}建议修改为:
String str = "123";
if ("123".equals(str))
{
...
}这样做主要是为了避免空指针异常
29、请注意,Java中if(i==1)和if(1==i)没有区别,但从阅读习惯来看,建议使用前者
人们通常会问 if (i == 1) 和 if (1== i) 之间有什么区别吗?这要从 C/C++ 开始。
在C/C++中,if(i==1)判断条件成立,是基于0和非0的。 0 表示假,非 0 表示真。假如有这样一段代码:
int i = 2;
if (i == 1)
{
...
}
else
{
...
}C/C++判断i==1不为真,所以表示为0,为假。但如果:
int i = 2;
if (i = 1)
{
...
}
else
{
...
}如果程序员不小心写成了if(i==1)而不是if(i=1),就会出现问题。在 if 中将 i 赋值为 1。如果判断里面的内容不为0,则返回值为true。但是很明显i是2,比较的值是1,所以应该返回false。这种情况在C/C++开发中很可能出现,并且会导致一些难以理解的错误。因此,为了避免开发者在if语句中进行错误的赋值操作,建议将if语句写成:
int i = 2;
if (1 == i)
{
...
}
else
{
...
}这样,即使开发人员不小心写成了 1 = i,C/C++ 编译器也能立即检查出来,因为我们可以将 i 的值赋给变量,但不能将 1 的值赋给常量。
但是,在Java中,C/C++的if(i=1)语法是不可能出现的,因为一旦写出这个语法,Java就会编译并报错Typemismatch:cannotconvertfrominttoboolean。不过,虽然Java的if(i==1)和if(1==i)没有语义上的区别,但从阅读的角度来看,还是使用前者更好。
30. 不要在数组上使用 toString() 方法
看一下在数组上使用 toString() 打印的内容:
public static void main(String[] args)
{
int[] is = new int[]{1, 2, 3};
System.out.println(is.toString());
}结果发现:
[我@18a992f
初衷是打印出数组的内容,但是由于数组引用为空,可能会导致空指针异常。不过,虽然toString()对于数组来说没有意义,但是对于集合来说toString()却可以打印出集合的内容,因为集合的父类AbstractCollections重写了Object的toString()方法。
31、不要对超出范围的基本数据类型进行向下强制转换。
这永远不会给出期望的结果:
public static void main(String[] args)
{
long l = 12345678901234L;
int i = (int)l;
System.out.println(i);
}我们可能期望得到其中一些,但结果是:
1942892530
解释。 Java中的Long是8字节64位,所以12345678901234在计算机中的表示应该是:
0000 0000 0000 0000 0000 1011 0011 1010 0111 0011 1100 1110 0010 1111 1111 0010
int类型数据有4个字节,32位。上述一串二进制数据的前32位取低位为:
0111 0011 1100 1110 0010 1111 1111 0010
该字符串的二进制表示形式为十进制 1942892530,因此这就是上面控制台上的输出内容。从这个例子可以得出两个结论:
32、公共集合类中未使用的数据必须及时移除
如果一个集合类是public的(即它不是方法中的属性),那么集合中的元素将不会被自动释放,因为总有引用指向它们。因此,如果公共集合中的某些数据没有被使用并且没有被移除,那么公共集合将会不断增长,导致系统存在内存泄漏的可能性。
33、将基本数据类型转换为字符串,基本数据类型.toString()是最快的方式,String.valueOf(data)次之,data+""最慢。
基本数据类型的转换通常有三种方法。我有一个整数数据i。我可以使用 i.toString()、String.valueOf(i) 和 i+""。这三种方法的效率如何?参见测试:
public static void main(String[] args)
{
int loopTime = 50000;
Integer i = 0;
long startTime = System.currentTimeMillis();
for (int j = 0; j < loopTime; j++)
{
String str = String.valueOf(i);
}
System.out.println("String.valueOf():" + (System.currentTimeMillis() - startTime) + "ms");
startTime = System.currentTimeMillis();
for (int j = 0; j < loopTime; j++)
{
String str = i.toString();
}
System.out.println("Integer.toString():" + (System.currentTimeMillis() - startTime) + "ms");
startTime = System.currentTimeMillis();
for (int j = 0; j < loopTime; j++)
{
String str = i + "";
}
System.out.println("i + \"\":" + (System.currentTimeMillis() - startTime) + "ms");
}运行结果为:
String.valueOf(): 11ms
Integer.toString(): 5ms
我+“”:25毫秒
因此,以后遇到基本数据类型转换为String时,优先使用toString()方法。至于为什么,也很简单:
比较三者,很明显2最快,1次之,3最慢。
34.用最有效的方式遍历地图
遍历地图的方法有很多种。一般场景下,我们需要的是遍历Map中的Keys和Values。推荐且最有效的方法是:
public static void main(String[] args)
{
HashMap<String, String> hm = new HashMap<String, String>();
hm.put("111", "222");
Set<Map.Entry<String, String>> entrySet = hm.entrySet();
Iterator<Map.Entry<String, String>> iter = entrySet.iterator();
while (iter.hasNext())
{
Map.Entry<String, String> entry = iter.next();
System.out.println(entry.getKey() + "\t" + entry.getValue());
}
}如果只是想遍历这个Map的键值,那么使用 Set keySet = hm.keySet(); 会更合适。
35、建议单独操作资源的close()。
意思是,例如,我有这段代码:
try
{
XXX.close();
YYY.close();
}
catch (Exception e)
{
...
}建议修改为:
try
{
XXX.close();
}
catch (Exception e)
{
...
}
try
{
YYY.close();
}
catch (Exception e)
{
...
}虽然麻烦一点,但是可以避免资源泄漏。我们认为如果没有修改代码的话,如果XXX.close()抛出异常,那么就会进入catch块,YYY.close()不会被执行,YYY资源也不会被回收。像这样占用过多的代码可能会导致资源句柄泄漏。改成下面的写法后,保证XXX和YYY无论如何都会被关闭
36、使用前或使用后一定要删除ThreadLocal。
目前基本上所有的项目都采用了线程池技术,这是非常好的。线程数量可以动态配置,线程可以复用。
但是,如果你的项目中使用了ThreadLocal,请务必在使用之前或之后将其删除。这是因为上面提到的线程池技术是线程复用,也就是说代码运行时,某个线程不会被销毁而是会等待下一次使用。我们看一下Thread类中持有ThreadLocal.ThreadLocalMap的引用:
/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;线程没有被销毁意味着前一个线程设置的ThreadLocal.ThreadLocalMap中的数据仍然存在。那么当下一个线程复用这个Thread时,很可能拿到的是上一个线程设置的数据而不是你想要的内容。
这个问题非常晦涩难懂。一旦因为这个原因出现错误,没有相关经验或者缺乏经验的话,将很难发现这个问题。所以写代码的时候要注意这一点,这样会为你以后减少很多工作量。
37.记住使用常量定义而不是魔鬼数字。魔鬼数的存在会大大降低代码的可读性。字符串常量是否使用常量定义取决于具体情况。
38. 当long或Long初始赋值时,使用大写的L而不是小写的l,因为字母l很容易与数字1混淆。这一点非常详细,值得注意。
39.所有重写的方法必须保留@Override注解
原因有以下三个:
40、建议使用JDK7中新引入的Objects工具类来进行对象的equals比较。直接a.equals(b),存在空指针异常的风险。
41、循环体中不要使用“+”进行字符串拼接,而是直接使用StringBuilder进行连续追加。
下面来说说字符串拼接不使用“+”的原因。如果我有一个方法:
public String appendStr(String oriStr, String... appendStrs) {
if (appendStrs == null || appendStrs.length == 0) {
return oriStr;
}
for (String appendStr : appendStrs) {
oriStr += appendStr;
}
return oriStr;
}这段代码编译后使用javap -c反编译.class文件,截取关键部分:
这意味着虚拟机每次遇到“+”运算符拼接字符串时,都会创建一个新的StringBuilder,然后调用append方法,最后调用toString()方法将字符串转换并赋值给oriStr对象,即循环多少次,就会生成多少个new StringBuilder(),很浪费内存。
42、不要捕获Java类库中定义的继承自RuntimeException的运行时异常类
异常处理效率低。 RuntimeException的大部分运行时异常类都是程序员可以避免的,比如:
43.避免Random实例被多个线程使用。虽然共享此实例是线程安全的,但会因竞争同一种子而导致性能下降。 JDK7之后,可以使用ThreadLocalRandom来获取随机数。
解释同一种子的竞争导致性能下降的原因。例如,看一下 Random 类的 nextInt() 方法的实现:
public int nextInt() {
return next(32);
}调用 next(int bits) 方法,这是一个受保护的方法:
protected int next(int bits) {
long oldseed, nextseed;
AtomicLong seed = this.seed;
do {
oldseed = seed.get();
nextseed = (oldseed * multiplier + addend) & mask;
} while (!seed.compareAndSet(oldseed, nextseed));
return (int)(nextseed >>> (48 - bits));
}这里的种子是一个全局变量:
/**
* The internal state associated with this pseudorandom number generator.
* (The specs for the methods in this class describe the ongoing
* computation of this value.)
*/
private final AtomicLong seed;当多个线程同时获取随机数时,它们会竞争同一个种子,导致效率降低。
44、静态类、单例类和工厂类将其构造函数设置为私有
这是因为静态类、单例类和工厂类不需要外部new。将构造函数设置为私有可确保这些类不会生成实例对象。
良旭个人微信
本公众号的所有博文均已整理到一个目录中。请在公众号回复“m”即可获取!
