本文列举java开发中代码优化的一些方法。
尽量重用对象,不要循坏创建对象,比如:for循环字符串拼接
在这篇文章里面介绍了为什么不能循环拼接字符串对象。
容器类初始化的时候指定长度
例如map和list,如果我们知道我们的数据量有多大,我们最好一开始的时候就指定长度,因为这些list和map在扩容的时候,会做一些例如数据复制的动作或者重新hash的复杂动作,影响效率。
先说arraylist,它内部存储的结构其实是数组,扩容的时候需要复制一遍原来的数据,影响效率;而hashmap在resize的时候,也是很复杂,会影响效率。另外hashmap有一点不同的,传入值不一定会作为初始化容量值,例如传入3,最终tableSizeFor得到的初始化容量值是4,总是2的n次方,是为了hash的时候key分布得更加均匀。
ArrayList随机遍历快,LnkedLiset添加删除快
集合遍历尽量减少重复计算
例如:for(int i = 0,len=collection.size;i<len;i++){},也就是collection.size()尽量先计算出来,以后就不用再计算了。
使用Entry遍历Map
for(Map.entry<String,String> entry:map.entrySet()){
String key = entry.getKey();
String value = entry.getValue();
}
使用entry一次性把key和value取出来了,如果只取keySet,那么如果要value就要再get一遍,所以最好一起取出来。
大数组复制使用System.arraycopy
因为System.arraycopy使用的是原生的方法,所以效率比较高,速度比较快
尽量使用基本类型而不是包装类型
为什么这么说呢?先看一段代码:
public static void main(String[] args) {
int i = 100;
System.out.println(i);
Integer i2 = 100;
System.out.println(i2);
}
它编译生产的字节码是:
0: bipush 100
2: istore_1
3: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
6: iload_1
7: invokevirtual #5 // Method java/io/PrintStream.println:(I)V
10: bipush 100
12: invokestatic #2 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
15: astore_2
16: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
19: aload_2
20: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/Object;)V
我们可以看到,调用包装类型的时候,多了一步:
invokestatic #2 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
也就是这个基本类型会通过valueOf被包装成Integer类型,所以使用包装类型的时候,都会有一个类似valueOf的操作,把基本类型变成包装类型,这里就会有一点性能上的影响了。
关于valueOf,我们再看下面一段代码:
public static void main(String[] args) {
Integer i1=100;
Integer i2 = 100;
System.out.println(i1==i2);
Integer i3=1000;
Integer i4=1000;
System.out.println(i3==i4);
}
输出true和false
看了Integer的valueOf源码:
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
low默认是-128,hign默认是127,所以如果之前有缓存,小于127的都可以从缓存里面读取,所以是相等的。大于127就自己创建了,所以 就不相等。
不要手动调用System.gc()
System.gc()通知垃圾收集器进行fullGC,但是什么时候fullgc是不确定的。而且生产环境可能会加+DisableExplicitGC来禁用System.gc(),所以开发过程中最好不要有依赖System.gc()的代码。
及时消除过期对象的引用,防止内存泄露
这里有一个模拟stack的类,表面上很难看出问题。
public class Stack {
private Object[] elements;
private int size = 0;
private static final int DEFAULT_INITIAL_CAPACITY = 16;
public Stack() {
elements = new Object[DEFAULT_INITIAL_CAPACITY];
}
public void push(Object e) {
ensureCapacity();
elements[size++] = e;
}
public Object pop() {
if (size == 0)
throw new EmptyStackException();
return elements[--size];
}
/**
* Ensure space for at least one more element, roughly doubling the capacity
* each time the array needs to grow.
*/
private void ensureCapacity() {
if (elements.length == size)
elements = Arrays.copyOf(elements, 2 * size + 1);
}
}
问题出在这里:
public Object pop() {
if (size == 0)
throw new EmptyStackException();
return elements[--size];
}
我们返回了elements的最后一个元素,那么现在有elements[size-1]指向的对象有两个引用,一个是elements[size-1],一个是外面引用它的引用,如果外面引用释放了这个对象,elements[size-1]这个指针一直还在,对象也不会释放,这里一直被引用着,所以存在内存泄漏的风险,正确的做法是修改为:
public Object pop() {
if (size == 0)
throw new EmptyStackException();
Object object = elements[size-1];
elements[size-1] = null;
size--;
return object;
}
或者:
public Object pop() {
if (size == 0)
throw new EmptyStackException();
Object object = elements[--size];
elements[size] = null;
return object;
}
来自《effective java》中文版 第二版page21
尽量使用局部变量,减小变量的作用域
因为作用域越小,出了作用域,对象就可以被回收了,可以尽快地做垃圾回收。
尽量使用非同步容器
例如,优先考虑使用ArrayList而不是Vector,因为Vector的几乎每个方法都是synchronized,影响效率。
尽量减小同步作用范围,synchronized方法vs同步代码块
在非static方法上加锁,其实就相当于在this上加锁,例如下面两个方法就是效率就是一样的:
public synchronized void f1() {//在this對象上加锁
System.out.println("f1");
}
public void f2() {
synchronized(this) {
System.out.println("f2");
}
}
但是如果下面,明显f2的效率比f1的要高:
public synchronized void f1() {//在this對象上加锁
System.out.println("f");
System.out.println("f1");
System.out.println("f");
}
public void f2() {
System.out.println("f");
synchronized(this) {
System.out.println("f2");
}
System.out.println("f");
}
所以,从这个角度上来说,同步代码块比同步方法更能减小同步的范围,一般来说更加倾向使用同步代码块。
如果是在static方法上加锁,那么就相当于在类上加锁,下面这两个方法是相同的:
public static synchronized void f3() {
System.out.println("f3");
}
public static void f4() {
synchronized(SynchronizedTest.class) {
System.out.println("f4");
}
}
用ThreadLocal缓存线程不安全对象,例如SimpleDateFormat
例如:
public class SimpleDateFormatUtil {
private static ThreadLocal<SimpleDateFormat> dateFormatHolder = new ThreadLocal<SimpleDateFormat>() {
protected SimpleDateFormat initialValue() {
return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
}
};
public static void main(String[] args) {
dateFormatHolder.get().format(new Date());
}
}
这不仅保证了线程安全,也可以提高效率
尽量使用延时加载
例如:
public class Singleton {
private Singleton() {}
private static class SingletonHolder{
private static Singleton instance = new Singleton();
}
public static Singleton getInstance() {
return SingletonHolder.instance;
}
}
为什么说是延时加载呢?SingletonHolder是一个类,在不对这个类引用的时候,是不对这个类加载的;当引用SingletonHolder这个类的时候,会把里面的成员Singleton加载出来。SingletonHolder在初始化的时候,由jvm类加载机制保证,所以不会出现线程安全的问题。所以它是线程安全的,并且只有引用这个类的时候才加载,所以是延时加载的。
尽量减少使用反射,加缓存
如果真的每次都要反射,试着把反射加缓存里面,下次就不用反射了。框架里面经常会使用这种机制。
尽量使用连接池,线程池,对象池,缓存
我们知道,跟数据库建立连接是比较耗内存的,所以建立完连接,就放回去,下次可以使用
及时释放资源,I/O流、Socket、数据库连接
jdk1.7开始使用了自动关闭资源的try语句
try (
// 声明、初始化两个可关闭的资源
BufferedReader br = new BufferedReader(new FileReader(
"AutoCloseTest.java"));
PrintStream ps = new PrintStream(new FileOutputStream(
"readme.txt"))) {
// 使用两个资源
System.out.println(br.readLine());
ps.println("test");
}
慎用异常,不要用异常来表示正常业务逻辑
因为异常也是比较重的对象,发生异常的时候,会把整个堆栈记录下来,再往外抛,比较耗时间,会影响正常业务逻辑的时间
String操作尽量少用正则表达式
因为正则表达式虽然好用,却也做了复杂的操作,所以影响效率。
例如String的replaceAll使用的就是正则,replace也是但是比较轻量(只是普通字符串替换),所以能用replace的尽量不要用replaceAll。
日志输出注意使用不同的级别
有的日志在生产环境打的,有的日志在测试环境打的,生产环境打太多日志会影响效率,生产环境只打关键的日志。
日志中参数拼接使用占位符
log.info("orderId:"+orderId);不推荐
log.info("orderId:{}",orderId);推荐
因为我们都知道,字符串的拼接实际上每次都要new String Builder,效率很低,这里讲过
另外即使我们设置了日志级别,info不输出,但是还是会把(“orderId:”+orderId这种字符串拼接拼出来,一样会影响效率。