Java程序性能优化—十年码农总结的编程小技巧

程序的性能受代码质量的直接影响。在本文中,主要介绍一些代码编写的小技巧和惯例,这些技巧有助于在代码级别上提升系统性能。

1、慎用异常

在Java软件开发中,经常使用 try-catch 进行错误捕获,但是,try-catch 语句对系统性能而言是非常糟糕的。虽然在一次 try-catch中,无法察觉到它对性能带来的损失,但是,一旦try-catch被应用于循环之中,就会给系统性能带来极大的伤害。

以下是一段将try-catch应用于for循环内的示例


这段代码我运行时间是 27211 ms。如果将try-catch移到循环体外,那么就能提升系统性能,如下代码


运行耗时 15647 ms。可见tyr-catch对系统性能的影响。

2、使用局部环境

调用方法时传递的参数以及在调用中创建的临时变量都保存在栈(Stack)中,速度较快。其他变量,如静态变量、实例变量等,都在堆(Heap)中创建,速度较慢。

下面是一段测试用例

// private static int a = 0;

public static void main(String[] args) { int a = 0; long start = System.currentTimeMillis(); for (int i = 0; i < 1000000; i++) {

a = a + 1;

System.out.println(i);

}

System.out.println(System.currentTimeMillis() - start);

}

运行结果很明显,使用静态变量耗时15677ms,使用局部变量耗时13509ms。由此可见,局部变量的访问速度高于类的成员变量。

3、位运算代替乘除法

在所有的运算中,位运算是最为高效的。因此,可以尝试使用位运算代替部分算术运算,来提高系统的运行速度。

比如在HashMap的源码中使用了位运算

static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

static final int MAXIMUM_CAPACITY = 1 << 30;

对于整数的乘除运算优化

a*=2

a/=2

用位运算可以写为

a<<=1a>>=1

4、替换switch

关键字 switch 语句用于多条件判断, switch 语句的功能类似于 if-else 语句,两者性能也差不多。因此,不能说 switch 语句会降低系统的性能。但是,在绝大部分情况下,switch 语句还是有性能提升空间的。

来看下面的例子:

public static void main(String[] args) { long start = System.currentTimeMillis(); int re = 0; for (int i = 0;i<1000000;i++){

re = switchInt(i);

System.out.println(re);

}

System.out.println(System.currentTimeMillis() - start+"毫秒");//17860

} public static int switchInt(int z){ int i = z%10+1; switch (i){ case 1:return 3; case 2:return 6; case 3:return 7; case 4:return 8; case 5:return 10; case 6:return 16; case 7:return 18; case 8:return 44; default:return -1;

}

}

就分支逻辑而言,这种 switch 模式的性能并不差。但是如果换一种新的思路替代switch,实现相同的程序功能,性能就能有很大的提升空间。

public static void main(String[] args) { long start = System.currentTimeMillis(); int re = 0; int[] sw = new int[]{0,3,6,7,8,10,16,18,44}; for (int i = 0;i<1000000;i++){

re = arrayInt(sw,i);

System.out.println(re);

}

System.out.println(System.currentTimeMillis() - start+"毫秒");//12590

}

public static int arrayInt( int[] sw,int z){ int i = z%10+1; if (i>7 || i<1){ return -1;

}else { return sw[i];

}

}

以上代码使用全新的思路,使用一个连续的数组代替了 switch 语句。因为对数据的随机访问是非常快的,至少好于 switch 的分支判断。通过实验,使用switch的语句耗时17860ms,使用数组的实现只耗时12590ms,提升了5s多。在软件开发中,换一种思路可能会取得更好的效果,比如使用数组替代switch语句就是就是一个很好的例子。在此我向大家推荐一个架构学习交流裙。交流学习裙号:821169538,里面会分享一些资深架构师录制的视频录像

5、一维数组代替二维数组

由于数组的随机访问的性能非常好,许多JDK类库,如ArrayList、Vector等都是使用了数组作为其数组实现。但是,作为软件开发人员也必须知道,一位数组和二维数组的访问速度是不一样的。一位数组的访问速度要优于二维数组。因此,在性能敏感的系统中要使用二维数组的,可以尝试通过可靠地算法,将二维数组转为一维数组再进行处理,以提高系统的响应速度。

6、提取表达式

在软件开发过程中,程序员很容易有意无意让代码做一些“重复劳动”,在大部分情况下,由于计算机的告诉运行,这些“重复劳动”并不会对性能构成太大的威胁,但若将系统性能发挥到极致,提取这些“重复劳动”相当有意义。

来看下面的测试用例:

@Test public void test(){ long start = System.currentTimeMillis();

ArrayList list = new ArrayList(); for (int i = 0;i<100000;i++){

System.out.println(list.add(i));

} //以上是为了做准备

for (int i = 0;i

System.out.println(list.get(i));

}

System.out.println(System.currentTimeMillis() - start);//5444

}

如果我们把list.size()方法提取出来,优化后的代码如下:

@Test public void test(){ long start = System.currentTimeMillis();

ArrayList list = new ArrayList(); for (int i = 0;i<100000;i++){

System.out.println(list.add(i));

} //以上是为了做准备

int n = list.size(); for (int i = 0;i

System.out.println(list.get(i));

}

System.out.println(System.currentTimeMillis() - start);//3514

}

在我的机器上,前者耗时5444ms,后者耗时3514ms,相差2s左右,可见,提取重复的操作是相当有意义的。

7、展开循环

与前面所介绍的优化技巧略有不同,笔者认为展开循环是一种在极端情况下使用的优化手段,因为展开循环很可能会影响代码的可读性和可维护性,而这两者对软件系统来说也是极为重要的。但是,当性能问题成为系统主要矛盾时,展开循环绝对是一种值得尝试的技术。

8、布尔运算代替位运算

虽然位运算的速度远远高于算术运算,但是在条件判断时,使用位运算替代布尔运算却是非常错误的选择。

在条件判断时,Java会对布尔运算做相当充分的优化。假设有表达式 a,b,c 进行布尔运算“a&&b&&c” ,根据逻辑与的特点,只要在整个布尔表达式中有一项返回false,整个表达式就返回false,因此,当表达式a为false时,该表达式将立即返回 false ,而不会再去计算表达式b 和c。同理,当计算表达式为“a||b||c”时,也是一样。

若使用位运算(按位与”&“、按位或”|“)代替逻辑与和逻辑或,虽然位运算本身没有性能问题,但是位运算总是要将所有的子表达式全部计算完成后,再给出最终结果。因此,从这个角度来说,使用位运算替代布尔运算会使系统进行很多无效计算。

9、使用arrayCopy()

数组复制是一项使用频率很高的功能,JDK中提供了一个高效的API来实现它:

如果在应用程序需要进行数组复制,应该使用这个函数,而不是自己实现。

方法代码:

public static native void arraycopy(Object src, int srcPos,

Object dest, int destPos, int length);

它的用法是将源数组 src 从索引 srcPos 处复制到目标数组 dest 的 索引destPos处,复制的长度为 length。

System.arraycopy() 方法是 native 方法,通常 native 方法的性能要优于普通的方法。仅出于性能考虑,在软件开发中,尽可能调用 native 方法。

10、使用Buffer进行I/O流操作

除NIO外,使用 Java 进行 I/O操作有两种基本方法:

使用基于InputStream 和 OutputStream 的方式;(字节流)

使用 Writer 和 Reader。(字符流)

无论使用哪种方式进行文件 I/O,如果能合理地使用缓冲,就能有效的提高I/O的性能。

11、使用clone()代替new

在Java中新建对象实例最常用的方法是使用 new 关键字。JDK对 new 的支持非常好,使用 new 关键字创建轻量级对象时,速度非常快。但是,对于重量级对象,由于对象在构造函数中可能会进行一些复杂且耗时的操作,因此,构造函数的执行时间可能会比较长。导致系统短期内无法获得大量的实例。为了解决这个问题,可以使用Object.clone() 方法。

Object.clone() 方法可以绕过构造函数,快速复制一个对象实例。但是,在默认情况下,clone()方法生成的实例只是原对象的浅拷贝。

这里不得不提Java只有值传递了,关于这点,我的理解是基本数据类型引用的是值,普通对象引用的也是值,不过这个普通对象引用的值其实是一个对象的地址。代码示例:在此我向大家推荐一个架构学习交流裙。交流学习裙号:821169538,里面会分享一些资深架构师录制的视频录像

int i = 0; int j = i; //i的值是0

User user1 = new User();

User user2 = user1; //user1值是new User()的内存地址

如果需要深拷贝,则需要重新实现 clone() 方法。下面看一下ArrayList实现的clone()方法:

public Object clone() { try {

ArrayListv = (ArrayList) super.clone();

v.elementData = Arrays.copyOf(elementData, size);

v.modCount = 0; return v;

} catch (CloneNotSupportedException e) { // this shouldn't happen, since we are Cloneable

throw new InternalError(e);

}

}

在ArrayList的clone()方法中,首先使用 super.clone() 方法生成一份浅拷贝对象。然后拷贝一份新的elementData数组让新的ArrayList去引用。使克隆后的ArrayList对象与原对象持有不同的引用,实现了深拷贝。

12、静态方法替代实例方法

使用 static 关键字描述的方法为静态方法。在Java中,由于实例方法需要维护一张类似虚函数表的结构,以实现对多态的支持。与静态方法相比,实例方法的调用需要更多的资源。因此,对于一些常用的工具类方法,没有对其进行重载的必要,那么将它们声明为 static,便可以加速方法的调用。同时,调用 static 方法不需要生成类的实例。比调用实例方法更为方便、易用。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,293评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,604评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,958评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,729评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,719评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,630评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,000评论 3 397
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,665评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,909评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,646评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,726评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,400评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,986评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,959评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,197评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,996评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,481评论 2 342

推荐阅读更多精彩内容

  • 1. Java基础部分 基础部分的顺序:基本语法,类相关的语法,内部类的语法,继承相关的语法,异常的语法,线程的语...
    子非鱼_t_阅读 31,567评论 18 399
  • 【程序1】 题目:古典问题:有一对兔子,从出生后第3个月起每个月都生一对兔子,小兔子长到第三个月后每个月又生一对兔...
    叶总韩阅读 5,125评论 0 41
  • 工作&学习 好多临时事项,好在最重要的报告已完结。 Python 和mysql的学习本周有停滞,下周那实例上手练。...
    聂Lydia阅读 96评论 0 0
  • 一夜未眠 收拾好几个月的紧张心情 在破晓时分西装革履 一路鸣炮的迎亲车队 在欢乐感动的气氛中 我女朋友 站在舞台中...
    流浪山狼阅读 173评论 0 1
  • UIPickView和UIDatePicker 1.UIPickView什么时候用?• 通常在注册模块,当用户需要...
    SoManyDumb阅读 415评论 0 0