项目经验总结-twice

时间:2023-03-09 15:36:30
项目经验总结-twice

1、尽量指定类、方法的final修饰符

  带有final修饰符的类是不可派生的。在Java核心API中,有许多应用final的例子,例如java.lang.String,整个类都是final的。为类指定final修饰符可以让类不可以被继承,为方法指定final修饰符可以让方法不可以被重写。如果指定了一个类为final,则该类所有的方法都是final的。Java编译器会寻找机会内联所有的final方法,内联对于提升Java运行效率作用重大,具体参见Java运行期优化。此举能够使性能平均提高50%。

2、尽量重用对象

  特别是String对象的使用,出现字符串连接时应该使用StringBuilder/StringBuffer代替。由于Java虚拟机不仅要花时间生成对象,以后可能还需要花时间对这些对象进行垃圾回收和处理,因此,生成过多的对象将会给程序的性能带来很大的影响。

3、尽可能使用局部变量

  调用方法时传递的参数以及在调用中创建的临时变量都保存在栈中速度较快,其他变量,如静态变量、实例变量等,都在堆中创建,速度较慢。另外,栈中创建的变量,随着方法的运行结束,这些内容就没了,不需要额外的垃圾回收。

4、尽量减少对变量的重复计算

  明确一个概念,对方法的调用,即使方法中只有一句语句,也是有消耗的,包括创建栈帧、调用方法时保护现场、调用方法完毕时恢复现场等。所以例如下面的操作:

for (int i = 0; i < list.size(); i++)
{...}

  建议替换为:

for (int i = 0, int length = list.size(); i < length; i++)
{...}

  这样,在list.size()很大的时候,就减少了很多的消耗。

5、尽量采用懒加载的策略,即在需要的时候才创建

  例如:

String str = "aaa";
if (i == 1) {
list.add(str);
}

  建议替换为:

if (i == 1) {
String str = "aaa";
list.add(str);
}

6、不要在循环中使用try…catch…,应该把其放在最外层

  除非不得已。如果毫无理由地这么写了,只要你的领导资深一点、有强迫症一点,八成就要骂你为什么写出这种垃圾代码来了

7、如果能估计到待添加的内容长度,为底层以数组方式实现的集合、工具类指定初始长度

  比如ArrayList、LinkedLlist、StringBuilder、StringBuffer、HashMap、HashSet等等,以StringBuilder为例:

  (1)StringBuilder()      // 默认分配16个字符的空间

  (2)StringBuilder(int size)   // 默认分配size个字符的空间

  (3)StringBuilder(String str) // 默认分配16个字符+str.length()个字符空间

  可以通过类(这里指的不仅仅是上面的StringBuilder)的来设定它的初始化容量,这样可以明显地提升性能。比如StringBuilder吧,length表示当前的StringBuilder能保持的字符数量。因为当StringBuilder达到最大容量的时候,它会将自身容量增加到当前的2倍再加2,无论何时只要StringBuilder达到它的最大容量,它就不得不创建一个新的字符数组然后将旧的字符数组内容拷贝到新字符数组中—-这是十分耗费性能的一个操作。试想,如果能预估到字符数组中大概要存放5000个字符而不指定长度,最接近5000的2次幂是4096,每次扩容加的2不管,那么:

  (1)在4096的基础上,再申请8194个大小的字符数组,加起来相当于一次申请了12290个大小的字符数组,如果一开始能指定5000个大小的字符数组,就节省了一倍以上的空间

  (2)把原来的4096个字符拷贝到新的的字符数组中去

  这样,既浪费内存空间又降低代码运行效率。所以,给底层以数组实现的集合、工具类设置一个合理的初始化容量是错不了的,这会带来立竿见影的效果。但是,注意,像HashMap这种是以数组+链表实现的集合,别把初始大小和你估计的大小设置得一样,因为一个table上只连接一个对象的可能性几乎为0。初始大小建议设置为2的N次幂,如果能估计到有2000个元素,设置成new HashMap(128)、new HashMap(256)都可以。

8、当复制大量数据时,使用System.arraycopy()命令

  用1w、10w、100w、1000w次数测试数组复制效率,单位是纳秒
  1w:
       for:       147630
       clone:      30789
     sys copy:   7894
  10w:
       for:       1895112
       clone:      220261
     sys copy:   72236
  100w:
       for:       7529924
       clone:      2160373
     sys copy:   1111962
  1000w:
        for:       18103632
       clone:       21056234
    sys copy:   11426726
结论:System.arraycopy明显快于其余2中方法,并且clone要快于for。
  但这只是在size很大的情况下,接下来我用10、100、1000又测了一下,又发现了有趣的现象:
  10:
       for:       395
     clone:      4737
     sys copy:   2763
  100:
       for:       1579
       clone:      8684
     sys copy:   5526
  1000:
       for:       14211
       clone:       10658
     sys copy:  5527
结论:可以看到,在size为10、100的时候for循环快的飞起~而在size到了1000后System.arraycopy才明显快了些。
总结:在数组的size很大的时候,考虑使用System.arraycopy来提高效率,而在size比较小的时候,可以直接使用for循环。但由于nanoTime获取的是纳秒级别的,一纳秒相当于一秒的10亿分之一,所以在虽然在10、100的时候for更快,但也只快了0.0025、0.004毫秒,没错是“毫秒”!这几乎可以忽略不计了~相较之下在10w、100w、1000w下相差了1、6、7毫秒,这虽然对我们人类来说也是没啥区别,但对于计算器来说还是有些差别的。所以综上所述,建议使用System.arraycopy,并且System.arraycopy还可以选择性的copy数组

9、乘法和除法使用移位操作

  例如:

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;
}

  移位操作虽然快,但是可能会使代码不太好理解,因此最好加上相应的注释。

10、循环内不要不断创建对象引用

  例如:

for (int i = 1; i <= count; i++) {
Object obj = new Object();
}

  这种做法会导致内存中有count份Object对象引用存在,count很大的话,就耗费内存了,建议为改为:

Object obj = null;
for (int i = 0; i <= count; i++) {
obj = new Object();
}

  这样的话,内存中只有一份Object对象引用,每次new Object()的时候,Object对象引用指向不同的Object罢了,但是内存中只有一份,这样就大大节省了内存空间了。

11、尽量避免随意使用静态变量

  要知道,当某个对象被定义为static的变量所引用,那么gc通常是不会回收这个对象所占有的堆内存的,如:

public class A {
private static B b = new B();
}

  此时静态变量b的生命周期与A类相同,如果A类不被卸载,那么引用B指向的B对象会常驻内存,直到程序终止

12、实现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循环去遍历。

13、把一个基本数据类型转为字符串,基本数据类型.toString()是最快的方式、String.valueOf(数据)次之、数据+”"最慢

  把一个基本数据类型转为一般有三种方式,例如一个Integer型数据index,可以使用以下三种方式:

  1. index.toString()

  2. String.valueOf(index)

  3. index + ""

  以上三种方式的效率如何,看一个测试:

public static void main(String[] args) {
int loopTime = 50000;
Integer index = 0;
long startTime = System.currentTimeMillis();
for (int j = 0; j < loopTime; j++) {
String str = String.valueOf(index);
}
System.out.println("String.valueOf():" + (System.currentTimeMillis() - startTime) + "ms"); startTime = System.currentTimeMillis();
for (int j = 0; j < loopTime; j++) {
String str = index.toString();
}
System.out.println("Integer.toString():" + (System.currentTimeMillis() - startTime) + "ms"); startTime = System.currentTimeMillis();
for (int j = 0; j < loopTime; j++) {
String str = index + "";
}
System.out.println("index + \"\":" + (System.currentTimeMillis() - startTime) + "ms");
}

  运行结果为:

    String.valueOf():  11ms
  Integer.toString():  5ms
  index + "":      25ms

  所以以后遇到把一个基本数据类型转为String的时候,优先考虑使用toString()方法。至于为什么,很简单:

  1、String.valueOf()方法底层调用了Integer.toString()方法,但是会在调用前做空判断

  2、Integer.toString()方法就不说了,直接调用了

  3、index + "" 底层使用了StringBuilder实现,先用append方法拼接,再用toString()方法获取字符串

  三者对比下来,明显是2最快、1次之、3最慢

14、多种使用方式去遍历Map

 public static void main(String[] args) {
Map<String, String> map = new HashMap<String, String>();
map.put("1", "value1");
map.put("2", "value2");
map.put("3", "value3"); //第一种:普遍使用,二次取值
System.out.println("通过Map.keySet遍历key和value:");
for (String key : map.keySet()) {
System.out.println("key= "+ key + " and value= " + map.get(key));
} //第二种
System.out.println("通过Map.entrySet使用iterator遍历key和value:");
Iterator<Map.Entry<String, String>> it = map.entrySet().iterator();
while (it.hasNext()) {
Map.Entry<String, String> entry = it.next();
System.out.println("key= " + entry.getKey() + " and value= " + entry.getValue());
} //第三种:推荐,尤其是容量大时
System.out.println("通过Map.entrySet遍历key和value");
for (Map.Entry<String, String> entry : map.entrySet()) {
System.out.println("key= " + entry.getKey() + " and value= " + entry.getValue());
} //第四种
System.out.println("通过Map.values()遍历所有的value,但不能遍历key");
for (String v : map.values()) {
System.out.println("value= " + v);
}
}

  如果你只是想遍历一下这个Map的key值,那用 Set<String> keySet = hm.keySet(); 会比较合适一些

15、对资源的close()建议分开操作

  意思是,比如我有这么一段代码:

try {
XXX.close();
YYY.close();
}catch (Exception e) {
...
}

  建议修改为:

try{
XXX.close();
}catch(Exception e) { ... }
try{
YYY.close();
}catch(Exception e) { ... }

  虽然有些麻烦,却能避免资源泄露。我们想,如果没有修改过的代码,万一XXX.close()抛异常了,那么就进入了cath块中了,YYY.close()不会执行,YYY这块资源就不会回收了,一直占用着,这样的代码一多,是可能引起资源句柄泄露的。而改为下面的写法之后,就保证了无论如何XXX和YYY都会被close掉。