优雅的写代码

代码优化的目标

1.减小代码的体积。

2.提高代码运行效率

代码规范

  1. 所有文件的开头都要有Java文档的注释(/* /)
  2. 常量应该全部大写,单词之间由下划线分隔(例如,MAX_WORK_HOURS)
  3. 数组标识:要用”int[] packets”,而不是”int packets[]”,后一种永远也不要用
  4. 不要在循环中构造和释放对象
  5. 不要在循环中频繁查询数据库
  6. 方法要通盘考虑,尽量做到复用。(多想)

代码优化细节

尽量重用对象

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

string类是final类,不能被继承,并且它的成员方法都默认为final方法。在java中,被final修饰的类是不允许被继承的,并且该类中的成员方法都默认为final方法。在早期的JVM实现版本中,被final修饰的方法会被转为内嵌调用以提升执行效率。从Java SE5/6开始,就渐渐摒弃这种方式了。因此在现在版本中并不需要考虑用final去提升方法调用效率。只有在确定不想让该方法被覆盖时,才将方法设置为final.

String类其实是通过char数组来保存字符串的。所以无论是substring、concat、replace操作都不是在原有的字符串上进行的,而是重新生成了一个新的字符串对象。所以结论是:对String对象的任何改变都不影响到原对象,相关的任何change操作都会生成新的对象。

通过new关键字来生成对象是在堆区进行的,所以通过new来创建对象,创建出的一定是不同的对象,即使字符串的内容是相同的。

在class文件中有一部分来存储编译期间生成的字面常量以及符合的引用,这部分叫做class文件常量池,在运行期间对应着方法区的运行时常量池。下面代码 str1和str3都存储在常量池中,只存了一份。

public class Main {
    public static void main(String[] args) {
        String str1 = "hello world";
        String str2 = new String("hello world");
        String str3 = "hello world";
        String str4 = new String("hello world");

        System.out.println(str1==str2);
        System.out.println(str1==str3);
        System.out.println(str2==str4);
    }
}

输出结果为false true false

既然已经有了String类,那为什么还需要StringBuilder、StringBuffer类呢

public class Main {
    public static void main(String[] args) {
        String string = "";
        for(int i=0;i<10000;i++){
            string += "hello";
        }
    }
}

这句 string += “hello”;的过程相当于将原有的string变量指向的对象内容取出与”hello”作字符串相加操作再存进另一个新的String对象当中,再让string变量指向新生成的对象。结论:使用String进行字符串拼接会造成内存资源浪费。

public class Main {
    public static void main(String[] args) {
        StringBuilder stringBuilder = new StringBuilder();
        for(int i=0;i<10000;i++){
            stringBuilder.append("hello");
        }
    }
}

使用StringBuilder new操作只进行了一次,也就是说只生成了一个对象,append操作是在原有对象的基础上进行的。因此在循环了10000次之后,这段代码所占的资源要比上面小得多。

事实上,StringBuilder和StringBuffer类拥有的成员属性以及成员方法基本相同,区别是StringBuffer类的成员方法前面多了一个关键字:synchronized,不用多说,这个关键字是在多线程访问时起到安全保护作用的,也就是说StringBuffer是线程安全的。

StringBuilder的insert方法:

public StringBuilder insert(int index, char str[], int offset, int len){
  super.insert(index, str, offset, len);
  return this;
}

StringBuffer的insert方法:

public synchronized StringBuffer insert(int index, char str[], int offset, int len) {

    super.insert(index, str, offset, len);
    return this;
}

结论:

1.对于直接相加字符串,效率很高,因为在编译器便确定了它的值,也就是说形如”I”+”love”+”java”; 的字符串相加,在编译期间便被优化成了”Ilovejava”。这个可以用javap -c命令反编译生成的class文件进行验证。

对于间接相加(即包含字符串引用),形如s1+s2+s3; 效率要比直接相加低,因为在编译器不会对引用变量进行优化。

2.String、StringBuilder、StringBuffer三者的执行效率:

StringBuilder > StringBuffer > String

当然这个是相对的,不一定在所有情况下都是这样。

比如String str = “hello”+ “world”的效率就比 StringBuilder st = new StringBuilder().append(“hello”).append(“world”)要高。

因此,这三个类是各有利弊,应当根据不同的情况来进行选择使用:

当字符串相加操作或者改动较少的情况下,建议使用 String str=”hello”这种形式;

当字符串相加操作较多的情况下,建议使用StringBuilder,如果采用了多线程,则使用StringBuffer。

尽可能使用局部变量

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

及时关闭流

Java编程过程中,进行数据库连接、I/O流操作时务必小心,在使用完毕后,及时关闭以释放资源。因为对这些大对象的操作会造成系统大的开销,稍有不慎,将会导致严重的后果。尽量避免在for循环中进行数据库的操作。

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

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

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

替换为

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

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

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

例如:

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

建议替换为:

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

慎用异常

异常对性能不利。抛出异常首先要创建一个新的对象,Throwable接口的构造函数调用名为fillInStackTrace()的本地同步方法,fillInStackTrace()方法检查堆栈,收集调用跟踪信息。只要有异常被抛出,Java虚拟机就必须调整调用堆栈,因为在处理过程中创建了一个新的对象。异常只能用于错误处理,不应该用来控制程序流程。

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

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

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

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

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

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

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

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

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

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

例如:

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

基于效率和类型检查的考虑,应该尽可能使用array,无法确定数组大小时才使用ArrayList,尽量使用HashMap、ArrayList、StringBuilder,除非线程安全需要,否则不推荐使用Hashtable、Vector、StringBuffer,后三者由于使用同步机制而导致了性能开销。

实现RandomAccess接口的集合比如ArrayList,应当使用最普通的for循环而不是foreach循环来遍历

if (list instanceof RandomAccess){
    for (int i = 0; i < list.size(); i++){
    ...
    }
} else {
    Iterator<?> iterator = list.iterable();
        while (iterator.hasNext()){
        iterator.next()
    }
}

这是JDK推荐给用户的。JDK API对于RandomAccess接口的解释是:实现RandomAccess接口用来表明其支持快速随机访问,此接口的主要目的是允许一般的算法更改其行为,从而将其应用到随机或连续访问列表时能提供良好的性能。实际经验表明,实现RandomAccess接口的类实例,假如是随机访问的,使用普通for循环效率将高于使用foreach循环;反过来,如果是顺序访问的,则使用Iterator会效率更高。

使用带缓冲的输入输出流进行IO操作

带缓冲的输入输出流,即BufferedReader、BufferedWriter、BufferedInputStream、BufferedOutputStream,这可以极大地提升IO效率

字符串变量和字符串常量equals的时候将字符串常量写在前面

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

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
i + "":25ms

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

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

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

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

i + “”底层使用了StringBuilder实现,先用append方法拼接,再用toString()方法获取字符串

使用最有效率的方式去遍历Map

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的key值,那用”Set keySet = hm.keySet();”会比较合适一些