- 使用了 JMH之后,对于每一个小方法,都可以做一个非常深入的研究,对比,不一样的写法,不一样的风格,到底有没有区别,到底谁最好。
本次深入研究一下 equals
-
代码来源是 查看 阿里开源包中的工具包中的代码,看到之后,眼前一亮,然后进入了深思。
/** * check whether two components are equal<br/> * * @param src source component * @param target target component * @param <E> component type * @return <br/> * (null, null) == true * (1L,2L) == false * (1L,1L) == true * ("abc",null) == false * (null,"abc") == false */ public static <E> boolean isEquals(E src, E target) { return null == src && null == target || null != src && null != target && src.equals(target); }
-
对于 equals的使用,常规会有两种方式:
-
1) 常量.equals(obj)
这判断,没有问题,有些经验的程序员,都知道把常量放在前面,避免空指针
-
- obj1.equals(obj2)
这样使用,会有
Java.lang.NullPointerException
的风险 ,所以常规,会有一个if(null != str1){}
作为前提。
-
-
阿里将其包装了一下:
- 好处1:写成一个工具类,直接使用即可,避免新人犯错误的成本。
- 好处2:这个工具类,包裹住 非空的判断, 让使用的代码,简洁。
- 3)疑问:这样包装,是否会有性能的提升空间?
下面开始进行性能的详细分析
1)以往常规的测试方法,会写一个 main方法,进行测试:
public static void main(String[] args) {
int[] count = {10000,100000,1000000,10000000};
for(int num : count){
System.out.println("------------执行"+num+"次------------");
testEqu(num);
}
}
public static void testEqu(int count){
Affect affect = new Affect();
String a = "1";
String b = "2";
int num = count;
int temp = 0;
while (num-- > 0) {
if (a != null && a.equals(b)) {
temp++;
}
temp++;
}
System.out.println(" a.equals(b):"+affect.cost());
affect = new Affect();
num = count;
temp = 0;
while (num-- > 0) {
if (isEquals(a, b)) {
temp++;
}
temp++;
}
System.out.println("isEquals(a, b):"+affect.cost());
}
运行结果:
------------执行10000次------------
a.equals(b):1
isEquals(a, b):0
------------执行100000次------------
a.equals(b):5
isEquals(a, b):2
------------执行1000000次------------
a.equals(b):3
isEquals(a, b):18
------------执行10000000次------------
a.equals(b):29
isEquals(a, b):28
看到这个结果,小伙子们,是不是会觉得非常奇怪,为什么呢?
看到这个结果,小伙子们,是不是会觉得非常奇怪,为什么呢?
看到这个结果,小伙子们,是不是会觉得非常奇怪,为什么呢?
- 然后我们来改造一下代码:
public static void main(String[] args) throws InterruptedException {
int[] count = {10000,100000,1000000,10000000};
for(int num : count){
System.out.println("------------执行"+num+"次------------");
testEqu(num);
Thread.sleep(2000); // -------------------------加了这里
}
}
public static void testEqu(int count) throws InterruptedException {
Affect affect = new Affect();
String a = "1";
String b = "2";
int num = count;
int temp = 0;
while (num-- > 0) {
if (a != null && a.equals(b)) {
temp++;
}
temp++;
}
System.out.println(" a.equals(b):"+affect.cost());
Thread.sleep(2000);// -------------------------加了这里
affect = new Affect();
num = count;
temp = 0;
while (num-- > 0) {
if (isEquals(a, b)) {
temp++;
}
temp++;
}
System.out.println("isEquals(a, b):"+affect.cost());
}
结果是:
------------执行10000次------------
a.equals(b):0
isEquals(a, b):0
------------执行100000次------------
a.equals(b):7
isEquals(a, b):2
------------执行1000000次------------
a.equals(b):17
isEquals(a, b):3
------------执行10000000次------------
a.equals(b):45
isEquals(a, b):34
这样看,就比较明白了,但是这是为什么呢?
- 最主要的原因是GC的垃圾回收导致,这样写测试代码,是不准确的,变量都在一个类里,当对象的生命周期结束了,GC的过程,会影响其他代码的运作。
- 还有一个问题,就是 JIT , 运行次数没有到一定的程度,无法进入 JIT,但是静态方法块,先天就有优势,提前进入了JIT,所以也有可能不准确
so ,引出了 JMH , 见下面
2)使用 JMH 看一下情况
代码如下:
package org.openjdk.jmh.samples;
import org.openjdk.jmh.annotations.*;
import org.openjdk.jmh.runner.Runner;
import org.openjdk.jmh.runner.RunnerException;
import org.openjdk.jmh.runner.options.Options;
import org.openjdk.jmh.runner.options.OptionsBuilder;
import static org.openjdk.jmh.samples.ArthasCheckUtils.isEquals;
@State(Scope.Thread) // 每个测试线程一个实例
public class JMHDemo01 {
@Benchmark
public String stringConcat() {
String a = "1";
String b = "2";
int f = 0;
if (a != null) {
if (a.equals(b)) {
f++;
}
}
return "";
}
@Benchmark
public String stringConcatIsEquals() {
String a = "1";
String b = "2";
int f = 0;
if(isEquals(a, b)) {
f++;
}
return "";
}
}
测试 main 方法
public static void main(String[] args) throws RunnerException {
Options opt = new OptionsBuilder()
.include(JMHDemo01.class.getSimpleName())
.forks(1)
.build();
new Runner(opt).run();
}
看看结果:
Benchmark Mode Cnt Score Error Units
JMHDemo01.stringConcat thrpt 5 132834643.560 ± 443289.509 ops/s
JMHDemo01.stringConcatIsEquals thrpt 5 132995301.389 ± 52712.796 ops/s
-
结论:
使用了JMH 之后, 发现 结果非常的接近,包装了
equals
之后,性能还是提高了满多的,十几万次ops, 但是没有之前的测试的这么大的差距,只相差 0.12%.确定了 之前 考虑的 JIT的问题,他们其实是一样的方法,一样都到热区后,理论上的性能,应该是一致的。
-
但是对于这样优化的必要性而言,还是非常有必要的,其他的优势依旧存在:
- 1)代码整洁美观的提升
- 2)编码质量的提升
- 3)提前进入热区
- 4)可以从结果中看出,优化后的方法,更加稳定
测试代码,有写的不对的地方,请指出,转载,请标明出处。
有问题,可以给我留言。