代码优化是一个非常重要的课题。有些人可能觉得没用。一些小地方有什么好修改的?对代码的运行效率有什么影响?下面是我对这个问题的思考。就像海里的鲸鱼,它吃一只小虾有用吗?没什么用,但是虾吃多了,鲸鱼就喂了。代码优化也是如此。如果项目的目标是尽快上线,没有bug,那么这个时候,代码的细节可以忽略不计。但是,如果有足够的时间来开发和维护代码,那么每一个可以优化的细节都必须考虑。小优化点的积累一定会提高代码的运行效率。
代码优化的目标是:
,减少代码的大小
,提高代码运行的效率
代码优化的细节
在Java核心API中,有很多应用final的例子,比如java.lang.String,整个类都是final。为类指定最终修饰符可以防止类被继承,为方法指定最终修饰符可以防止方法被重写。如果某个类被指定为final,则该类的所有方法都是final。Java编译器会寻找机会内联所有的final方法,内联对提高Java运行效率有重要作用。有关详细信息,请参见Java运行时优化。这可以将性能平均提高10% .
,尽量重用对象
,尤其是字符串对象的使用。发生字符串连接时,应改为使用StringBuilder/StringBuffer。由于Java虚拟机不仅要花时间生成对象,将来还可能要花时间对这些对象进行垃圾收集和处理,所以生成过多的对象会对程序的性能产生很大的影响。
,调用方法时传递的参数尽可能使用局部变量
,调用中创建的临时变量都保存在堆栈中,速度更快。其他变量,比如静态变量,实例变量,都是在堆里创建的,比较慢。此外,当方法运行时,在堆栈中创建的变量将消失,并且不需要额外的垃圾收集。
,及时关闭流
在Java编程的过程中,连接数据库和操作I/O流时一定要小心。用完后及时关闭释放资源。因为对这些大型对象的操作会造成系统的较大开销,稍有不慎就会导致严重的后果。
,尽量减少变量的重复计算
明确一个概念。即使一个方法中只有一条语句,也是有消耗的,包括创建堆栈帧、调用方法时保护场景、调用方法时还原场景等。例如,下面的操作:
for(int I =;我& ltlist . size();i++)
{}
建议替换为:
for (int i =,int length = list . size();我& lt长度;i++)
{}
这样在list.size()比较大的时候,会减少很多消耗。
尽量采用懒加载的策略,也就是只在需要的时候才创建。
例如:
string str = & #;AAA & #;;如果(i ==)
{
list . add(str);
}
建议替换为:
如果(i ==)
{
string str = & #;AAA & #;;
list . add(str);
}
小心使用异常。
对异常表现不利。当抛出异常时,应该首先创建一个新对象。Throwable接口的构造函数调用名为fillInStackTrace()的本地同步方法,fillInStackTrace()方法检查堆栈并收集调用跟踪信息。每当抛出异常时,Java虚拟机必须调整调用堆栈,因为在处理过程中创建了一个新对象。异常只能用于错误处理,不应该用于控制程序流。
不要在循环中使用try…catch…应该放在外层。
除非万不得已。如果你无缘无故写这个,只要你的领导是学长,强迫症,巴成都会骂你写这个垃圾代码。
如果可以估计要添加的内容的长度,请在底部指定数组中实现的集合和工具类的初始长度。
如ArrayList、LinkedLlist、StringBuilder、StringBuffer、HashMap、HashSet等。以StringBuilder为例:
()StringBuilder() //默认分配字符的空间。
()StringBuilder(int size) //默认情况下分配大小为字符的空间。
()StringBuilder(String str)//characters+str . length()默认情况下分配字符空格。
可以设置类的初始化容量(不仅仅是上面的StringBuilder),这样可以明显提高性能。例如,StringBuilder,length表示当前StringBuilder可以容纳的字符数。因为当StringBuilder达到一个大容量的时候,它会把容量增加到现在的两倍。每当StringBuilder达到其最大容量时,它都必须创建一个新的字符数组,并将旧字符数组的内容复制到新字符数组中——这是一个非常消耗性能的操作。想象一下,如果可以估计字符数组中的一个大轮廓存储字符而不指定长度,并且幂越接近,不考虑每次扩展的加法,那么:
()在的基础上,申请一个size的字符数组相当于一次申请一个size的字符数组。如果您可以在开始时指定一个字符数组的大小,它将节省一倍以上的空间。
()将原始字符复制到新的字符数组中。
这样就浪费了内存空间,降低了代码运行效率。所以在底层为数组实现的集合和工具类设置一个合理的初始化容量并没有错,会带来立竿见影的效果。但是注意像HashMap这样的集合是作为数组+链表实现的。不要将初始大小设置为与您的估计大小相同,因为只有一个对象连接到一个表的可能性几乎为零。建议将初始大小设置为的n次方。如果有一个估计元素,可以将其设置为new HashMap()或new HashMap()。
复制大量数据时,请使用System.arraycopy()命令。
乘法和除法使用移位运算。
例如:
for(val =;val & lt;val +=)
{
a = val *;
b = val/;
}
使用移位操作可以大大提高性能,因为在计算机的底层,对齐的操作更加方便快捷,所以建议修改为:
for(val =;val & lt;val +=)
{
a = val & lt& gt;
}
虽然shift运算速度很快,但可能会使代码难以理解,所以最好添加相应的注释。
,不要一直在循环中创建对象引用
例如:
for(int I =;我& lt=计数;i++)
{
Object obj = new Object();
}
这种做法会导致内存中存在count对象引用。如果计数非常大,它将消耗内存。建议改为:
Object obj = nullfor(int I =;我& lt=计数;i++){ obj = new Object();}
这样内存中只有一个对象引用,每次创建新的Object()时,对象引用都指向不同的对象,但内存中只有一个,这样就大大节省了内存空间。
出于效率和类型检查的考虑,尽量使用array,只有在无法确定数组大小的情况下才使用ArrayList。
尽量用HashMap,ArrayList,StringBuilder。除非线程安全需要,否则不建议使用Hashtable、Vector和StringBuffer。由于使用了同步机制,后三种方法会导致性能开销。
不要将数组声明为public static final。
因为这是没有意义的,它只是将引用定义为static final,数组的内容仍然可以随意更改。将数组声明为public是一个安全漏洞,这意味着数组可以被外部类更改。
尝试在正确的地方使用单个案例。
使用单例可以减轻加载负担,缩短加载时间,提高加载效率,但并不是所有的地方都适合单例。简单来说,单身族主要适合以下三个方面:
()控制资源的使用,通过线程同步控制资源的并发访问。
()控制实例的生成,达到节约资源的目的。
()控制数据的共享,使多个不相关的进程或线程能够在不建立直接关联的情况下进行通信。
尽量避免随意使用静态变量。
要知道,当一个对象被定义为static的变量引用时,gc通常不会回收这个对象占用的堆内存,比如:
公共A类
{
私有静态B B = new B();
}
此时静态变量B的生命周期与类A相同,如果不卸载类A,引用B指向的B对象将常驻内存,直到程序终止。
及时清除不必要的会话。
为了清除不活动的会话,许多应用服务器都有默认的会话超时,通常是几分钟。当应用服务器需要保存更多的会话时,如果内存不足,操作系统会将一些数据转移到磁盘,应用服务器可能会根据MRU(最近使用比较频繁)算法将一些不活动的会话转储到磁盘,甚至会抛出内存不足的异常。如果要将会话转储到磁盘,则必须首先对其进行序列化。在大规模集群中,序列化对象的成本很高。因此,当不再需要会话时,应该及时调用HttpSession的invalid()方法来清除会话。
实现RandomAccess接口的集合(如ArrayList)应该由一个通用的for循环而不是foreach循环来遍历。
这是JDK向用户推荐的。JDK API对RandomAccess接口的解释是:实现RandomAccess接口是用来表明它支持快速随机访问。该接口的主要目的是允许通用算法改变它们的行为,以便在应用于随机或连续访问列表时能够提供良好的性能。实际体验表明,如果随机访问实现RandomAccess接口的类实例,使用普通for循环的效率会高于使用foreach循环的效率;反之,如果是顺序访问,使用迭代器会更高效。您可以使用类似于下面的代码来做出判断:
if(列出随机访问的实例)
{ for(int I =;我& ltlist . size();i++){}
}否则{
iterator iterator = list . iterable();while(iterator . has next()){ iterator . next()}
}
foreach循环的底层实现原理是迭代器。参见Java语法sugar:变长参数和foreach循环原理。所以后半句“反过来,如果是顺序访问,用迭代器会更有效率”的意思就是用foreach循环遍历那些顺序访问的类实例。
,用同步代码块代替同步方法
这个在《多线程模块中的同步锁方法块》一文中已经讲得很清楚了。除非可以确定需要同步整个方法,否则尽量使用同步代码块,避免同步那些不需要同步的代码,影响代码执行的效率。
,将常量声明为static final,并用大写
命名,这样可以在编译时将这些内容放入常量池,运行时可以避免生成常量的值。另外,用大写字母命名常量的名字也可以很容易区分常量和变量
,不创建一些不用的对象,不导入一些不用的类
,没有意义。如果代码中出现“不使用局部变量I的值”和“从不使用导入java.util”,请删除这些无用的内容
,避免在程序运行过程中使用反射
。有关信息,请参考反射。正是反射Java为用户提供了非常强大的功能。功能强大往往意味着效率低下。不建议在程序运行过程中使用反射机制,尤其是方法的invoke方法。如果真的有必要,建议的做法是通过反射实例化一个对象,在项目启动时放入内存——用户只关心在与对等体交互时获得更快的响应速度,而不关心对等体启动项目需要多长时间。
,数据库连接池和线程池
都是用来重用对象的。前者可以避免频繁地打开和关闭连接,后者可以避免频繁地创建和销毁线程
,使用缓冲的iostream进行IO操作
[即BufferedReader、BufferedWriter、BufferedInputStream和BufferedOutputStream,可以大大提高IO效率
。ArrayList用于更多顺序插入和随机访问的场景,使用LinkedList
用于中间删除和插入更多元素的场景。如果你了解ArrayList和LinkedList的原理,你就知道
。不要让公共方法有太多的形参
,也就是公共方法是外部提供的方法。如果给这些方法的参数太多,主要有两个缺点:
,违背了面向对象的编程思想。Java强调一切都是对象,参数太多不符合面向对象的编程思想
,参数太多必然增加方法调用的错误概率
[/h]比如我们用JDBC写一个insertStudentInfo方法,有一个学生信息字段要插入到学生表中,这个参数可以封装在一个实体类中。作为insert方法的参数
,字符串变量和字符串常量等于,把字符串常量写在前面
是常用的伎俩。如果有以下代码& #;;
if(str . equals(& #;)){
& #;
}
建议修改为:
string str = & #;;
if(& #;。equals(str))
{
& #;
}
这主要是为了避免空指针异常
。要知道java里的if (i ==)和if (== i)是没有区别的,但是从阅读习惯上讲,建议用前者。
在C/C++中,“if (i ==)”判断条件成立,而且是基于NAND的,表示假,否定表示真。如果有这样的代码:
int I =;
if(I = =)
{
& #;
} else {
& #;
}
C/C++判断" I = = " = "不成立,所以用,即false表示。但是如果:
int I =;if(I =){ & #;} else { & #;}
万一程序员不小心把“if (i ==)”写成了“if (i =)”,那就有问题了。如果I赋给If,if确定里面的内容不为真,返回值为真,但是当I为清时,比较值为假,应该返回。这种情况在C/C++开发中很可能发生,会导致一些无法理解的错误。所以为了避免开发者在if语句中不正确的赋值操作,建议将if语句写成:
int I =;if(= = I){ & #;} else { & #;}
这样即使开发者不小心写了“= i”,C/C++编译器也能第一时间查出来,因为我们可以把I赋给变量,而不是常量。
但是在Java中,C/C++的“if (i =)”语法是不可能的,因为一旦写了这个语法,Java就会编译出错误“类型不匹配:无法从int转换成boolean”。不过,虽然Java中的“if (i ==)”和“if (== i)”在语义上没有区别,但从阅读习惯上来说,还是建议使用前者比较好。
,不要在数组上使用toString()方法
看看在数组上使用toString()打印出来的是什么:
public static void main(string[]args)[/br/
system . out . println(is . toString());
}
结果是:
[I@af
意在打印出数组的内容,但也有可能因为数组引用为空而导致空指针异常。虽然对数组toString()没有意义,但是可以打印出set toString()的内容,因为set AbstractCollections的父类覆盖了对象的toString()方法。
,不要强制向下转换超出范围的基本数据类型
这样永远得不到想要的结果:
public static void main(String[]args)
{
long L = L;
int I =(int)l;
system . out . println(I);
}
我们可能期望得到其中的一些,但结果是:
解释。Java long是一个字节位,所以在计算机中的表示应该是:
一个int类型的数据是一个字节位,从低位取的上述二进制数据串的第一位是:
[/]从这个例子中,我们可以顺便得到两个结论:
。integer的默认数据类型是int,long l = L,超出了int的范围,所以末尾有一个L,表示是一个long数。对了,浮点型的默认类型是double,所以定义float的时候应该写成" " float f =。f "
,然后再写一句“int ii = l+I;”将会报告一个错误,因为long+int是一个long,不能赋给int
,公共集合类中未使用的数据必须及时移除
如果一个集合类是公共的(也就是说,它不是方法中的一个属性),那么这个集合中的元素不会被自动释放,因为总是有引用指向它。因此,如果公共集合中的一些数据没有被使用或删除,就会导致公共集合不断增长,从而使系统存在内存泄漏的隐患。
,将基本数据类型转换为字符串,基本数据类型。toString()是比较快的方式,String.valueOf (data)次之,data+" "比较慢
把基本数据类型转换成通用数据类型有三种方式,我有一个整型数据I。
public static void main(String[]args)
{
int loop time =;
Integer I =;long start time = system . current time millis();for(int j =;j & lt循环时间;j++)
{
string str = string . value of(I);
}
system . out . println(& #;string . value of():& #;+(system . current time millis()& #;start time)+& #;ms & #;);
start time = system . current time millis();for(int j =;j & lt循环时间;j++)
{
string str = I . tostring();
}
system . out . println(& #;integer . tostring():& #;+(system . current time millis()& #;start time)+& #;ms & #;);
start time = system . current time millis();for(int j =;j & lt循环时间;j++)
{
string str = I+& #;;
}
system . out . println(& #;I+/& #;/:+(system . current time millis()& #;start time)+& #;ms & #;);
}
运行结果是:
string . value of():ms integer . tostring():ms I+& #;:毫秒
因此,将来将基本数据类型转换为字符串时,首选toString()方法。至于为什么,很简单:
在String.valueOf()方法的底部,调用Integer.toString()方法,但是在调用之前判断会被短路。
,Integer.toString()方法就不说了,直接调用。
i+" "的底层由StringBuilder实现,通过append方法拼接,然后通过toString()方法获取字符串。
三者相比,明显是快、二、慢。
使用更高效的方式遍历地图。
有许多方法可以遍历地图。通常,我们在场景中需要的是遍历地图中的键和值,所以推荐且高效的方式是:
公共静态void main(String[] args)
{
HashMap hm = new HashMap();
hm . put(& #;, );
Set & lt;地图。Entry & gtentry set = hm . entry set();
迭代器& lt地图。Entry & gtITER = entry set . iterator();while(ITER . has next())
{
Map。entry entry = ITER . next();
system . out . println(entry . getkey()+& #;/t & #;+entry . getvalue());
}
}
如果您只想遍历此映射的键值,请使用" Set keySet = hm . keySet();"会比较合适
,资源的close()建议单独操作
。意思是比如我有这个代码:
try {
XXX . close .
yyy . close();
}catch(异常e)
{
& #;
}
建议修改为:
try { XXX . close();}catch(异常e){ & #;}试试{ yyy . close();}catch(异常e){ & #;}
虽然有些麻烦,但是可以避免资源泄露。我们认为,如果没有修改代码,万一XXX.close()抛出异常,那么它就在cath块中,YYY.close()不会被执行,所以YYY的这个资源不会被回收,一直被占用。如果这样的代码比较多,可能会造成资源句柄的泄露。改成以下写法后,保证无论如何XXX和YYY都会很亲近。
作者:徐州百都网络 | 来源: | 发布于:2022-04-03 10:59:33