• 选学校
  • 高中毕后选择什么学校好
  • 启蒙星
  • 株洲北大青鸟校区升级

您现在的位置:株洲北大青鸟 >> >> 师资力量>> 技术天地

师资力量
开班信息
输入姓名手机号码预约试听课程
姓  名:*
手机号:*
创业摇篮班
开班日期:本月
招生人数:已满
就业直通班
开班日期:本月
招生人数:招生中
就业精英班
开班日期:本月
招生人数:25
技术天地
  • 10个简单易学的Java性能优化技巧
  • 〖不上名牌大学,就读株洲北大青鸟〗 发布人:株洲北大青鸟
  • 发表日期:2017/9/8 10:55:26 阅读数:2  
  •  
  •   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 object.int[] 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()。

上一篇:35+ 个 Java 代码性能优化总结
分享到:

版权所有 ©株洲健坤科技职业培训学校    学校地址:株洲市天元区黄山路205号健坤大厦(天元区消防中队对面)

咨询报名热线:400-8812-866    邮箱地址:4008812866@b.qq.com   备案号: 湘ICP备10202015号  

北大青鸟学费是多少 湖南北大青鸟怎么样
株洲北大青鸟好不好 株洲北大青鸟学费多少 株洲北大青鸟学校这么样