Java十大简单性能优化

以下是Java中最容易进行的10个性能优化:
1.使用StringBuilder
这几乎是所有Java代码中的默认设置。尽量避免+操作员。当然,您可能会争辩说它StringBuilder无论如何都是语法糖,例如:
1个 String x = “a” + args.length + “b”;
…编译成
0个新的java.lang.StringBuilder [16]
3 dup
4 ldc <String“ a”> [18]
6 invokespecial java.lang.StringBuilder(java.lang.String)[20]
9 aload_0 [args]
10个数组长度
11 invokevirtual java.lang.StringBuilder.append(int):java.lang.StringBuilder [23]
14 ldc <String“ b”> [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呢?
1个
2
3
4 String x = “a” + args.length + “b”;

if (args.length == 1)
x = x + args[0];
现在StringBuilder,您将拥有第二个,它不必要地消耗了堆内存,给GC带来了压力。改写这个:
1个
2
3
4
5
6 StringBuilder x = new StringBuilder(“a”);
x.append(args.length);
x.append(“b”);

if (args.length == 1);
x.append(args[0]);
带走
在上面的示例中,如果您使用显式StringBuilder实例,或者您依赖Java编译器为您创建隐式实例,则可能完全不相关。但是请记住,我们在NOPE分支中。我们浪费的每个CPU周期都像GC这样愚蠢的事情,或者分配StringBuilder的默认容量都是浪费N x O x P时间。
根据经验,请始终使用StringBuilder而不是+运算符。如果可以的话StringBuilder,如果String构建起来比较复杂,则可以将引用保留在几种方法中。这是jOOQ在生成复杂的SQL语句时所做的。只有一个StringBuilder“遍历”您的整个SQL AST(抽象语法树)
为了大声喊叫,如果您仍然有StringBuffer参考文献,请务必将其替换为StringBuilder。您实际上几乎不需要同步正在创建的字符串。
2.避免使用正则表达式
正则表达式相对便宜且方便。但是,如果您位于NOPE分支中,那么它们将是您最糟糕的事情。如果绝对必须在计算密集型代码节中使用正则表达式,则至少要缓存该Pattern引用,而不要一直重新编译它:
1个
2 static final Pattern HEAVY_REGEX =
Pattern.compile("(((X)*Y)Z)");
但是如果你的正则表达式真的很傻
1个 String[] parts = ipAddress.split("\.");
…那么您真的最好诉诸于普通的char[]或基于索引的操作。例如,这个完全不可读的循环执行相同的操作:
1个
2
3
4
5
6
7
8
9
10
11
12 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()版本相比,这是无法维护的。
挑战:读者中的聪明人可能会找到更快的算法。
带走
正则表达式很有用,但要付出一定的代价。如果您深入了解NOPE分支,则必须不惜一切代价避免使用正则表达式。提防使用正则表达式(例如String.replaceAll()或)的各种JDK String方法String.split()。
请使用诸如Apache Commons Lang之类的流行库来进行String操作。
3.不要使用iterator()
现在,此建议实际上并不适用于一般用例,而仅适用于NOPE分支的深处。但是,您应该考虑一下。编写Java-5样式的foreach循环很方便。您可以完全忘记循环内部,然后编写:
1个
2
3 for (String value : strings) {
// Do something useful here
}
但是,每次遇到此循环时(如果strings为)Iterable,都将创建一个新Iterator实例。如果您使用ArrayList,则会ints在堆上分配3的对象:
1个
2
3
4
5 private class Itr implements Iterator {
int cursor;
int lastRet = -1;
int expectedModCount = modCount;
// …
相反,您可以编写以下等效循环,并仅int在堆栈上“浪费”一个值,这非常便宜:
1个
2
3
4
5 int size = strings.size();
for (int i = 0; i < size; i++) {
String value : strings.get(i);
// Do something useful here
}
…或者,如果您的列表没有真正改变,您甚至可以对它的数组版本进行操作:
1个
2
3 for (String value : stringArray) {
// Do something useful here
}
带走
从可写性和可读性以及从API设计的角度来看,迭代器,Iterable和foreach循环都非常有用。但是,它们为每次迭代在堆上创建一个小的新实例。如果您多次运行此迭代,则要确保避免创建此无用的实例,而应编写基于索引的迭代。
讨论区
关于上述部分的一些有趣的分歧(特别是用Iterator索引访问代替用法)已经在Reddit上进行了讨论。
4.不要调用该方法
一些方法简单昂贵。在我们的NOPE分支示例中,叶子上没有这样的方法,但是您很可能有一个。假设您的JDBC驱动程序需要经历难以置信的麻烦才能计算的值ResultSet.wasNull()。您自己的SQL框架代码可能如下所示:
1个
2
3
4
5
6
7
8
9
10 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;
}
现在,ResultSet.wasNull() 每次int从结果集中获取一个时,就会调用此逻辑。但是getInt()合同上写着:
返回:列值;如果值为SQL NULL,则返回值为0
因此,对上述内容的简单但可能极大的改进将是:
1个
2
3
4
5
6
7
8 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-至少前泛型将在爪哇10和项目瓦尔哈拉specialisable。但是您的代码中可能没有此约束,因此应采取所有措施来替换:
1个
2 // Goes to the heap
Integer i = 817598;
… 这样:
1个
2 // Stays on the stack
int i = 817598;
使用数组时,情况变得更糟:
1个
2 // Three heap objects!
Integer[] i = { 1337, 424242 };
… 这样:
1个
2 // One heap object.
int[] i = { 1337, 424242 };
带走
当您深入了解NOPE分支时,应该非常警惕使用包装器类型。可能会给GC带来很大压力,必须时刻加油清理垃圾。
一种特别有用的优化可能是使用某种原始类型并为其创建大型的一维数组,以及几个定界符变量以指示编码对象在数组上的确切位置。
LG随附的trove4jint[]是一个出色的原始集合库,比您的平均库要复杂一些。
例外
有一个例外:boolean和byte几乎没有足够的值由JDK完全缓存。你可以写:
1个
2
3
4
5 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。
但是仅当您将它们自动装箱或调用时TheType.valueOf(),才调用构造函数!
除非确实需要新实例,否则切勿在包装器类型上调用构造函数。
这个事实还可以帮助您为同事写一个复杂的,愚蠢的愚人节玩笑
堆外
当然,您可能还想尝试堆外库,尽管它们更多是一个战略决策,而不是本地优化。
彼得·劳里(Peter Lawrey)和本·科顿(Ben Cotton)撰写的有关该主题的有趣文章是:OpenJDK和HashMap…安全地教老狗新技巧(超堆!)技巧
6.避免递归
像Scala这样的现代函数式编程语言鼓励使用递归,因为它们提供了将尾递归算法优化回到迭代算法的方法。如果您的语言支持这种优化,则可能会很好。但是即使那样,算法的最细微更改都可能会产生一个分支,从而阻止您的递归成为尾递归。希望编译器能够检测到这一点!否则,您可能会浪费大量的堆栈框架,而这些内容可能仅使用几个局部变量来实现。
带走
除了以下内容外,没有什么要说的:当您深入NOPE分支时,始终喜欢迭代而不是递归。
7.使用entrySet()
当您要遍历Map,并且同时需要键和值时,必须有充分的理由编写以下内容:
1个
2
3 for (K key : map.keySet()) {
V value : map.get(key);
}
…而不是以下内容:
1个
2
3
4 for (Entry<K, V> entry : map.entrySet()) {
K key = entry.getKey();
V value = entry.getValue();
}
当您在NOPE分支中时,无论如何,您都应该警惕地图,因为很多O(1)地图访问操作仍然是很多操作。而且访问也不是免费的。但是至少,如果您不能没有地图,请使用entrySet()它们进行迭代!Map.Entry无论如何,都存在该实例,您只需要访问它即可。
带走
entrySet()在地图迭代期间同时需要键和值时,请始终使用。
8.使用EnumSet或EnumMap
在某些情况下,例如,当使用配置映射时,会预先知道映射中可能的键数。如果该数字相对较小,则应真正考虑使用EnumSet或EnumMap,而不是常规HashSet或HashMap。可以通过查看以下内容轻松解释EnumMap.put():
1个
2
3
4
5
6
7
8 private transient Object[] vals;

public V put(K key, V value) {
// …
int index = key.ordinal();
vals[index] = maskNull(value);
// …
}
此实现的本质是这样一个事实,即我们拥有一个索引值数组,而不是哈希表。插入新值时,我们要做的就是查找映射项,只是要求枚举的常量序号,该序数由Java编译器在每种枚举类型上生成。如果这是一个全局配置图(即只有一个实例),增加的访问速度将帮助EnumMap大量强于大盘HashMap,可少用一些堆内存,而这将有运行hashCode(),并equals()在每个键。
带走
Enum并且EnumMap是非常亲密的朋友。每当您将类似枚举的结构用作键时,请考虑实际上使这些结构成为枚举并将其用作in中的键EnumMap。
9.优化您的hashCode()和equals()方法
如果您不能使用EnumMap,请至少优化hashCode()和equals()方法。一个好的hashCode()方法是必不可少的,因为它将阻止对更昂贵的调用,因为它将equals()为每个实例集生成更多不同的哈希存储桶。
在每个类层次结构中,您可能都有流行和简单的对象。让我们看一下jOOQ的org.jooq.Table实现。
最简单,最快的实现方法hashCode()是:
1个
2
3
4
5
6
7
8
9
10 // AbstractTable, a common Table base implementation:

@Override
public int hashCode() {

// [#1938] This is a much more efficient hashCode()
// implementation compared to that of standard
// QueryParts
return name.hashCode();

}
… name表名称在那里。我们甚至不考虑表的架构或任何其他属性,因为表名通常在数据库中足够不同。另外,name是一个字符串,因此它内部已经有一个缓存的hashCode()值。
该注释很重要,因为AbstractTableextends AbstractQueryPart是任何AST(抽象语法树)元素的常见基础实现。通用AST元素没有任何属性,因此它不能做任何假设来优化hashCode()实现。因此,重写的方法如下所示:
1个
2
3
4
5
6
7
8
9
10 // AbstractQueryPart, a common AST element
// base implementation:

@Override
public 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()
1个
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18岁
19
20
21
22 // AbstractTable, a common Table base implementation:

@Override
public 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;

}
第一件事:始终(不仅在NOPE分支中)equals()提前终止所有方法,如果:
this == argument
this “incompatible type” argument
请注意argument == null,如果您要instanceof检查兼容类型,则后一个条件包括。之前我们在“编码Java的10个微妙的最佳实践”中已经对此进行过博客撰写。
现在,在明显的情况下尽早中止比较之后,您可能还想在做出部分决策时就中止比较。例如,jOOQ的契约是要使Table.equals()两个表相等,无论具体的实现类型如何,它们都必须具有相同的名称。例如,这两个项目不可能相等:
com.example.generated.Tables.MY_TABLE
DSL.tableByName(“MY_OTHER_TABLE”)
如果argument 不能等于this,并且我们可以轻松地进行检查,那么我们可以进行检查,如果检查失败,则中止检查。如果检查成功,我们仍然可以从开始进行更昂贵的实现super。鉴于Universe中的大多数对象不相等,我们将通过简化此方法来节省大量CPU时间。
有些对象比其他对象更平等
对于jOOQ,大多数实例实际上是由jOOQ源代码生成器生成的表,equals()甚至进一步优化了其实现。其他数十种表类型(派生表,表值函数,数组表,联接表,数据透视表,公用表表达式等)可以保持其“简单”实现。
10.集合思考,而不是个别思考
最后但并非最不重要的一点是,有一种东西与Java无关,但适用于任何语言。此外,我们将离开NOPE分支,因为此建议可能只是帮助您从转到或类似的东西。O(N3)O(n log n)
不幸的是,许多程序员以简单的本地算法来思考。他们一步一步地解决问题,逐分支,逐循环,逐方法。这就是命令式和/或函数式编程风格。从纯粹的命令式到面向对象(仍然是命令式)再到函数式编程时,为“更大的画面”建模变得越来越容易,但是所有这些样式都缺少只有SQL和R和类似语言才能做到的:
声明式编程。
在SQL中(并且我们很喜欢,因为它是jOOQ博客),您可以声明要从数据库中获取的结果,而不会产生任何算法含义。然后,数据库可以考虑所有可用的元数据(例如约束,键,索引等),以找出可能的最佳算法。
从理论上讲,从一开始,这就是SQL和关系演算背后的主要思想。实际上,SQL供应商仅在最近十年才实施了高效的CBO(基于成本的优化工具),因此请与我们保持在一起,直到2010年SQL最终释放其全部潜能(大约是时间!)。
但是,您不必执行SQL即可进行集合思考。集合/收藏/袋/清单可提供所有语言和库。使用集合的主要优点是您的算法将变得更加简洁。编写起来非常容易:
SomeSet INTERSECT SomeOtherSet
而不是:
1个
2
3
4
5
6
7
8
9
10 // Pre-Java 8
Set result = new HashSet();
for (Object candidate : someSet)
if (someOtherSet.contains(candidate))
result.add(candidate);

// Even Java 8 doesn’t really help
someSet.stream()
.filter(someOtherSet::contains)
.collect(Collectors.toSet());
有人可能会认为函数式编程和Java 8将帮助您编写更简单,更简洁的算法。不一定是真的。您可以将命令性的Java-7循环转换为功能性的Java-8 Stream集合,但是您仍在编写相同的算法。编写类似SQL的表达式是不同的。这个…
SomeSet INTERSECT SomeOtherSet
…可以由实施引擎以1000种方式实施。正如我们今天所了解的EnumSet,在运行INTERSECT操作之前将这两个集合自动转换为明智的选择也许是明智的。也许我们可以并行化这一点,INTERSECT而无需对Stream.parallel()
结论
在本文中,我们讨论了在NOPE分支上完成的优化,即深入到高复杂度算法中。在我们的案例中,作为jOOQ开发人员,我们对优化我们的SQL生成感兴趣:
每个查询仅在单个查询上生成 StringBuilder
我们的模板引擎实际上是解析字符,而不是使用正则表达式
我们会尽可能使用数组,尤其是在侦听器上进行迭代时
我们避免了不必调用的JDBC方法
等等…
jOOQ位于“食物链的底部”,因为它是(次)API,在调用离开JVM进入DBMS之前,我们的客户应用程序正在调用它。位于食物链的底部意味着在jOOQ中执行的每一行代码可能被称为N x O x P倍,因此我们必须热切地进行优化。
最后,开发这么多年我也总结了一套学习Java的资料与面试题,如果你在技术上面想提升自己的话,可以关注我,私信发送领取资料或者在评论区留下自己的联系方式,有时间记得帮我点下转发让跟多的人看到哦。在这里插入图片描述

发布了76 篇原创文章 · 获赞 11 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/zhaozihao594/article/details/104183236
今日推荐