10个简单易学的Java性能优化技巧
1. 使用 StringBuilder
几乎所有 Java 代码中你都应该考虑这个问题。避免使用 + 号。你可能会认为 StringBuilder 只是个语法糖,比如:
String x = "a" + args.length + "b";
… 会编译成
0 new java.lang.StringBuilder [16]
3 dup
4 ldc
6 invokespecial java.lang.StringBuilder(java.lang.String) [20]
9 aload_0 [args]10 arraylength11 invokevirtual
java.lang.StringBuilder.append(int) : java.lang.StringBuilder [23]14 ldc
但是之后你需要根据条件来修改字符串,会发生什么事情呢?
String x = "a" + args.length + "b";
if (args.length == 1)
x = x + args[0];
你现在会有第二个 StringBuilder,这个 StringBuilder 本来没有存在的必要,它会消耗堆内存,给 GC 增加负担。你应该这样写:
StringBuilder x = new StringBuilder("a");
x.append(args.length);
x.append("b");
if (args.length == 1);
x.append(args[0]);
关键点
在上面的例子中,显式地使用 StringBuilder 实例,和 Java 编译器隐式使用 StringBuilder 实例是没有关联的。但是记住,我们在 N.O.P.E 分支。我们在每个 CPU 周期对 GC 和为 StringBuilder 分配空间所产生的浪费,都会浪费 N x O x P 倍。
总是使用 StringBuilder,不用 + 运算符是一个不错的规则。如果你的字符串构建起来很复杂,尽可能在多个方法间使用同一个 StringBuilder。这就是你在生成复杂的 SQL 时 jOOQ 所做的事情。只有一个 StringBuilder 在整个 SQL AST (Abstract Syntax Tree) 中“游走”。
如果你还有 StringBuffer 引用,大声哭吧,把它们换成 StringBuilder,因为你很少需要同步创建一个字符串。[注:StringBuffer 与 StringBuilder 的区别就在于 StringBuffer 是线程安全的,但在同步代码中没必要使用 StringBuffer,它会带来额外的性能开销。]
2. 避免正则表达式
正则表达式相对便宜和方便。但是如果你在 N.O.P.E 分支,那很糟糕了。如果你必须在计算机密集的代码段中使用正则表达式,至少把 Pattern 的引用缓存下来,避免每次都对其重新编译:
static final Pattern HEAVY_REGEX =
Pattern.compile("(((X)*Y)*Z)*");
但是如果你的正则表达式真的很简单,就像
String[] parts = ipAddress.split("\\.");
… 然后你真的最好诉诸普通的 char[] 或基于索引的操作。例如下面很信读的一段代码做了同样的事情:
int length = ipAddress.length();int offset = 0;int part = 0;for (int i = 0; i < length; i++) {
if (i == length - 1 ||
ipAddress.charAt(i + 1) == '.') {
parts[part] =
ipAddress.substring(offset, i + 1);
part++;
offset = i + 2;
}
}
… 这也说明了为什么你不应该过早进行优化。与 split() 的版本相比,这简直不可维护。
挑战:请读者们找到更快的方法。
分析
正则表达式很有用,但需要代价。如果你在 N.O.P.E 分支,就必须避免正则表达式的代价。小心使用各种 JDK String 那些使用正则表达式的方法,比如 String.replaceAll()、String.split() 等。
请使用像 Apache Commons Lang 这样的知名类库来操作字符串。
3. 不要使用 iterator()
这个建议不太适用于常规用例,只适用于 N.O.P.E. 分支,但你也可以用用看。编写 Java-5 风格的 foreach 循环很方便。 你可以完全忽略循环内部变量,并编写:
for (String value : strings) {
// Do something useful here}
然而,每当你运行到循环内部时,如果 string 是一个 Iterable,你就要创建一个新的 Iterator 实例。如果你正在使用 ArrayList,这将会在堆上分配一个含 3 个 int 的对象:
private class Itr implements Iterator
int cursor;
int lastRet = -1;
int expectedModCount = modCount;
// ...
相反,你可以编写以下代码——等价循环体,并且在栈上仅“浪费”一个 int 值,开销低:
int size = strings.size();for (int i = 0; i < size; i++) {
String value : strings.get(i);
// Do something useful here}
… 或者,你可以选择不改变链表,在数组版本上使用同样的操作:
for (String value : stringArray) {
// Do something useful here}
关键点
从可写性和可读性以及从 API 设计的角度来看,Iterators、Iterable 和 foreach 循环都是非常有用的。但它们在堆上为每次单独的迭代创建一个小的新实例。 如果你运行这个迭代许多次,又想避免创建这个无用的实例,可以使用基于索引的迭代。
4. 不要调用这些方法
一些方法简单但开销不小。在N.O.P.E.分支示例中,我们没有在叶节点上使用这样的方法,但你可能使用到了。我们假设 JDBC 驱动程序需要耗费大量资源来计算 ResultSet.wasNull() 的值。你可能会用下列代码开发 SQL 框架:
if (type == Integer.class) {
result = (T) wasNull(rs,
Integer.valueOf(rs.getInt(index)));
}
// And then...static final
return rs.wasNull() ? null : value;
}
此处逻辑每次都会在你从结果集中获得一个 int 之后立即调用 ResultSet.wasNull()。但getInt() 的约定是:
返回: 列的数目;如果这个值是 SQL NULL,这个值将返回 0。
因此,对上述问题的简单但可能有效的改进将是:
static final
ResultSet rs, T value
) throws SQLException {
return (value == null ||
(value.intValue() == 0 && rs.wasNull()))
? null : value;
}
因此,这不需要过多考虑。
关键点
不要在算法的“叶节点”中调用开销昂贵的方法,而是缓存该调用,或者如果方法规约允许则规避之。
5. 使用基本类型和栈
上面的例子来自 jOOQ,它大量使用了泛型。泛型会强制对 byte、short、int 和 long 这些类型进行装箱 —— 至少在这之前:泛型会在 Java 10 和 Valhalla 项目中实现专业化。不过现在你的代码里并没实现这种约束,所以你得采取措施:
// Goes to the heapInteger i = 817598;
… 替换为下面这个:
// Stays on the stackint i = 817598;
如果你使用数组的话,情况不太妙:
// Three heap objects!Integer[] i = { 1337, 424242 };
… 替换成这个:
// One heap #[] i = { 1337, 424242 };
关键点
当你在深入 N.O.P.E. 分支时,要小心使用装箱类型。你可能会给 GC 制造很大的压力,因为它必须一直清理你的烂摊子。
有一个特别有效的办法对此进行优化,即使用某些基本类型,并为它创建一个巨大的一维数组,以及相应的定位变量来明确指出编码后的对象放在数组的哪个位置。
LGPL 授权的 trove4j 库实现了基本数据类型的集合,它看起来比 int[] 要好些。
例外
此规则有一个例外:boolean 和 byte 值很小,足够 JDK 完全缓存。你可以这样写:
Boolean a1 = true; // ... syntax sugar for:Boolean a2 = Boolean.valueOf(true);
Byte b1 = (byte) 123; // ... syntax sugar for:Byte b2 = Byte.valueOf((byte) 123);
对于其他整型基本类型的低值也是如此,包括 char、short、int、long。
但只在对它们自动装箱,或调用 Type.valueOf() 时,而不是在你调用构造函数时!
除非你确实需要一个新实例,否则永远不要在 wrapper 类型上调用构造函数
堆外
当然,你可能还想测试堆外库,虽然它们更多地是一个战略决策,而不是局部优化。
6. 避免递归
像Scala这样的现代函数式编程语言鼓励使用递归,因为它们提供了将尾部递归算法优化为迭代算法的机制。如果你的编程语言支持这样的优化,你可能会感觉不错。但是即便如此,算法最细微的改变也可能产生一个分支,用于避免递归变成尾递归。希望编译器会检测到这种情况!否则,你可能浪费了很多堆栈帧的资源,用于保存可能仅使用一些局部变量就可以实现的功能。
关键点
除此之外没有什么可说的:当你深入 N.O.P.E 分支时,总是优先选择迭代,而不是递归。
7. 使用 entrySet()
当你需要在 Map 中迭代,并且需要访问键和值时,你可能会写如下代码:
for (K key : map.keySet()) {
V value : map.get(key);
}
…而不是下面这种代码:
for (Entry
K key = entry.getKey();
V value = entry.getValue();
}
当你处于 N.O.P.E. 分支时,你应该警惕 Map,因为很多很多 O(1) 的 Map 访问操作仍然包含很多其他操作。而且访问也不是免费的。 但如果没有 Map,你可以使用 entrySet() 来迭代他们! Map.Entry 实例仍然存在,你只需要访问它即可。
关键点
在 map 迭代过程中当你需要同时访问键值和值本身时,总是使用 entrySet()。
8. 使用 EnumSet 或者 EnumMap
有时候映射表中可能存在的键的数量是预先可以知道的 —— 比如配置表。如果这个数量相对较小,你应该考虑使用 EnumSet 或 EnumMap,而不是常用的 HashSet 或 HashMap。看下面的 EnumMap.put() 就明白了:
private transient Object[] vals;
public V put(K key, V value) {
// ... int index = key.ordinal();
vals[index] = maskNull(value);
// ...}
这个实现的本质是使用索引值的数组来代替了哈希表。插入一个新值的时候,我们要做的就是通过枚举的原始常量值来查找映射表的项,这个常量值是 Java 编译器为每个枚举类型生成的。如果这是一个全局的配置表(假如只有一个实例),EnumMap 的执行效率会比 HashMap 高。HashMap 会用稍少一些的堆类型,但它需要对每个键进行 hashCode() 和 equals() 计算。
关键点
Enum 和 EnumMap 是好朋友。只要你把像枚举的结构作为键,就应该考虑把那些结构转换为枚举,并用它们作为 EnumMap 的键。
9. 优化 hashCode() 和 equals() 方法
如果你没有使用 EnumMap,至少应该优化 hashCode() 和 equals() 方法。一个好的 hashCode() 方法很有必要,因为它会减少进一步调用 eqauls() 的次数。好的哈希算法会为每组实例产生更多不同的哈希桶 [ 注:桶指把哈希码相同的一组数据放在一起的列表结构 ]。
在每个类层次结构中都可能存在简单的对象。
这是 hashCode() 最简单最快的实现:
// AbstractTable, a common Table base implementation: @Overridepublic int hashCode() {
// [#1938] This is a much more efficient hashCode() // implementation compared to that of standard // QueryParts return name.hashCode();
}
…其中 name 只是简单的表名。我们甚至不用考虑表结构或其它表属性,因为表名在数据库中已经足以保证唯一性。而且 name 是个字符串,所以它已经在内部缓存了 hashCode() 的值。
这里的注释非常重要,因为 AbstractTable 继承自 AbstractQueryPart,它是 ATS(抽象语法树)元素的公共实现。公共的 AST 元素没有任何属性,所以它不能基于假设来优化 hashCode() 实现。因此,得像这样覆写方法:
// AbstractQueryPart, a common AST element// base implementation: @Overridepublic int hashCode() {
// This is a working default implementation. // It should be overridden by concrete subclasses, // to improve performance return create().renderInlined(this).hashCode();
}
换句话说,整个 SQL 生成的工作流必定触发对公共 AST 元素计算哈希码。
equals() 更有意思:
// AbstractTable, a common Table base implementation: @Overridepublic boolean equals(Object that) {
if (this == that) {
return true;
}
// [#2144] Non-equality can be decided early, // without executing the rather expensive // implementation of AbstractQueryPart.equals() if (that instanceof AbstractTable) {
if (StringUtils.equals(name,
(((AbstractTable) that).name))) {
return super.equals(that);
}
return false;
}
return false;
}
首要的事情:总是 (不仅是在 N.O.P.E. 分支) 以下列情况中止调用 equals():
this == 参数
this 与参数 "类型不兼容"
注意,如果你使用 instanceof 来检查兼容类型,后者其实包含了参数 == null 的情况。
在根据明显情况提前中止比较之后,你可能还想通过部分对比来早些结束比较。举例来说, jOOQ 的 Table.equals() 用来比较两个表是否相等,无论它们具体类型是什么,它们必须有相同的名称。下面这两项是不可能相等的:
com.example.generated.Tables.MY_TABLE
DSL.tableByName("MY_OTHER_TABLE")
如果参数与 this 不等,而且我们可以早一些检查出来,那就应该这样做,并在检查失败时中止比较。检查成功后再继续进行 super 中相对重量级的实现。因为大多数对象是不等的,我们会通过简化该方法来节约大量时间。
某些对象比其它对象更平等
在这个 jOOQ 的示例中,多数实例实际上是由 jOOQ 源码生成器生成的表,它们的 equals() 已经优化了。其它几十个表类型(派生表、表值函数、数组表、连接表、透视表、公共表表达式等)可以保留他们的“简单”实现。
10. 深入了解集合而不是其中单独的元素
最后同样重要的是,有一个与 Java 无关,但适用于所有语言的东西。此外,我们应该撇开 N.O.P.E. 分支,因为这个建议可能只是帮助你从 O(N3) 降低到 O(n log n,或类似的情况。
许多程序员仅了解简单的局部算法。他们一步一步地、一个分支一个分支地、一个循环一个循环地、一个方法一个方法地解决问题。 这是命令式和/或函数式编程的风格。 虽然从纯命令式到面向对象(仍然是命令式)到函数式编程,建模“更大场景”变得越来越容易,但所有这些样式都缺少 SQL 和 R 及其类似语言包含的概念:
声明式编程。
你可以在 SQL 中申明想从数据库中获得的结果,不需要使用任何算法。数据库会考虑所有可用的元数据(比如约束、键、索引等)并指出最佳算法。
理论上,这是 SQL 和相关计算的主要思想。实践中,SQL 厂商已在过去10年里实现了高性能的 CBO (基于成本的优化器),所以在 2010 年的时候 SQL 已经发挥了其全部潜力。
不过你也不需要在数据集中使用 SQL。集/集合/包/列表在所有语言和库中都有。使用集合最主要的优势在于你的算法会变得简洁很多。很容易写出来:
SomeSet INTERSECT SomeOtherSet
而不是这样:
// Pre-Java 8Set result = new HashSet();for (Object candidate : someSet)
if (someOtherSet.contains(candidate))
result.add(candidate);
// Even Java 8 doesn't really helpsomeSet.stream()
.filter(someOtherSet::contains)
.collect(Collectors.toSet());
很多人认为在 Java8 中的函数式编程会更容易,算法会更简洁。但实际并不是这样。你可以翻译指令,把 Java-7-loop 变成 函数式的 Java-8 流集合,但你还是要写同样的算法。写一个 SQL-esque 表达式是不一样的。
SomeSet INTERSECT SomeOtherSet
... 通过实现引擎以1000种方式实现。现在我们知道,在运行 INTERSECT 操作之前,或许自动转换成两个集合到 EnumSet 的做法是明智的。也许我们可以并行化这个 INTERSECT 而不让底层来调用 Stream.parallel()。
感谢大家阅读由java培训机构分享的“10个简单易学的Java性能优化技巧”希望对大家有所帮助,更多精彩内容请关注Java培训官网
免责声明:本文由小编转载自网络,旨在分享提供阅读,版权归原作者所有,如有侵权请联系我们进行删除
【免责声明】本文部分系转载,转载目的在于传递更多信息,并不代表本网赞同其观点和对其真实性负责,如涉及作品内容、版权和其它问题,请在30日内与我们联系,我们会予以重改或删除相关文章,以保证您的权益!
Java开发高端课程免费试学
大咖讲师+项目实战全面提升你的职场竞争力
- 海量实战教程
- 1V1答疑解惑
- 行业动态分析
- 大神学习路径图
相关推荐
更多2015-10-15
2015-10-15
达内就业喜报
更多>Java开班时间
-
北京 丨 11月27日
火速抢座 -
上海 丨 11月27日
火速抢座 -
广州 丨 11月27日
火速抢座 -
兰州 丨 11月27日
火速抢座 -
杭州 丨 11月27日
火速抢座 -
南京 丨 11月27日
火速抢座 -
沈阳 丨 11月27日
火速抢座 -
大连 丨 11月27日
火速抢座 -
长春 丨 11月27日
火速抢座 -
哈尔滨 丨 11月27日
火速抢座 -
济南 丨 11月27日
火速抢座 -
青岛 丨 11月27日
火速抢座 -
烟台 丨 11月27日
火速抢座 -
西安 丨 11月27日
火速抢座 -
天津 丨 11月27日
火速抢座 -
石家庄 丨 11月27日
火速抢座 -
保定 丨 11月27日
火速抢座 -
郑州 丨 11月27日
火速抢座 -
合肥 丨 11月27日
火速抢座 -
太原 丨 11月27日
火速抢座 -
苏州 丨 11月27日
火速抢座 -
武汉 丨 11月27日
火速抢座 -
成都 丨 11月27日
火速抢座 -
重庆 丨 11月27日
火速抢座 -
厦门 丨 11月27日
火速抢座 -
福州 丨 11月27日
火速抢座 -
珠海 丨 11月27日
火速抢座 -
南宁 丨 11月27日
火速抢座 -
东莞 丨 11月27日
火速抢座 -
贵阳 丨 11月27日
火速抢座 -
昆明 丨 11月27日
火速抢座 -
洛阳 丨 11月27日
火速抢座 -
临沂 丨 11月27日
火速抢座 -
潍坊 丨 11月27日
火速抢座 -
运城 丨 11月27日
火速抢座 -
呼和浩特丨11月27日
火速抢座 -
长沙 丨 11月27日
火速抢座 -
南昌 丨 11月27日
火速抢座 -
宁波 丨 11月27日
火速抢座 -
深圳 丨 11月27日
火速抢座 -
大庆 丨 11月27日
火速抢座