美国上市公司,专注Java培训22年

10个简单易学的Java性能优化技巧


1. 使用 StringBuilder

几乎所有 Java 代码中你都应该考虑这个问题。避免使用 + 号。你可能会认为 StringBuilder 只是个语法糖,比如:

String x = "a" + args.length + "b";

… 会编译成

0 new java.lang.StringBuilder [16]

3 dup

4 ldc [18]

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 [27]16 invokevirtual java.lang.StringBuilder.append(java.lang.String) : java.lang.StringBuilder [29]19 invokevirtual java.lang.StringBuilder.toString() : java.lang.String [32]22 astore_1 [x]

但是之后你需要根据条件来修改字符串,会发生什么事情呢?

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 T wasNull(ResultSet rs, T value) throws SQLException {

return rs.wasNull() ? null : value;

}

此处逻辑每次都会在你从结果集中获得一个 int 之后立即调用 ResultSet.wasNull()。但getInt() 的约定是:

返回: 列的数目;如果这个值是 SQL NULL,这个值将返回 0。

因此,对上述问题的简单但可能有效的改进将是:

static final T wasNull(

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 entry : map.entrySet()) {

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答疑解惑
  • 行业动态分析
  • 大神学习路径图

相关推荐

更多
  • java语言中,char 类型变量是否能保存一个汉字?
    java语言中,char 类型变量是否能保存一个汉字?
    在 Java 语言中,可以使用 char 类型的变量来存储单个的字符,请问是否能用 char 类型的变量来存储一个汉字呢? 详情>>

    2015-10-15

  • 有史以来最牛的一张程序员职业路线图!
    有史以来最牛的一张程序员职业路线图!
    最近在琢磨程序员到底路在何方,经过不断的自虐和代入,终于在迷雾森林中得图一张,看之豁然开朗。独乐乐不如众乐乐,share了: 详情>>

    2018-05-22

  • java中变量和常量有什么区别?
    java中变量和常量有什么区别?
    在使用 Java 语言进行程序设计时,经常需要用到常量和变量来存储信息。请简单叙述变量和常量有什么区别? 详情>>

    2015-10-15

  • short 和 char 类型的取值范围各是多少?
    short 和 char 类型的取值范围各是多少?
    在使用 Java 语言进行程序设计时,经常需要使用 short 型和 char 型存储数值,请简述short 型和 char 型的取值范围各是多少? 详情>>

    2015-10-15

  • Java开班时间

    收起