匿名类

匿名类踩坑

背景

在需求开发的时候,涉及的业务需要详细记录更新或者插入操作影响字段的新旧值,这其实就是所谓的业务log,方便以后查证以及扯皮的时候能用上。最简单的办法肯定是在db字段上做触发器,但是这个需要DAB去搞,为了不麻烦DBA,我们通常的业务log都是自己写表的,而且对于日志的格式我们可以有很大的控制权。

实现方式

记录db记录对象变更log最简单直接的方式就是搞一个大json,不管三七二十一,直接把对象序列化。这样的话,对于有很多字段的大对象如果仅仅修改一个字段而记录整个对象序列化的值,其实很浪费磁盘,而且也不利于快速的发现变更的字段。其实我们关注的往往是一些重要值的变更,而忽略一些不重要的值,so我们的目标应该只记录有变化的字段的新值和旧值。

如果我们想写一个通用的对象比较工具类,肯定是要脱离具体的类,而用Object代替,并通过反射的方式获取字段的值进行比对。形如:

1
2
public static List<String> diffCompareOfSameClass(Object oldInstance, Object newInstance) {
}

我们都知道,在用java对象进行比较时,我们都要重写equals方法和hashCode方法,其中重写equal方法时最开始的两个判断肯定是判断“是否是同一个对象”,“是否是同一个class”。在写对象比较工具类时,这两个判断也必须写在代码的开头。如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public static List<String> diffCompareOfSameClass(Object oldInstance, Object newInstance) {

List<String> diffLog = Lists.newArrayList();

if (oldInstance == newInstance) {
return Lists.newArrayList();
}

if (oldInstance.getClass().getClassLoader() != newInstance.getClass().getClassLoader()) {
return Lists.newArrayList();
}

if (oldInstance.getClass() != newInstance.getClass()) {
return Lists.newArrayList();
}

......
......
......
......
}

下面开始正式进入本文的重点:匿名构造函数与匿名类的坑。提前注明上面代码的坑在: oldInstance.getClass() != newInstance.getClass() 这个判断上。

构造代码块与匿名类的坑

写完上面的对象比较工具后,写了一个简单的测试类,测试一下效果。测试类如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@Data
static class Test {
private Integer a;

private Integer b;

private String c;
}


public static void main(String[] args) {

Test tc = new Test() {{
setA(10);
setA(9);
setC("8");
}};

Test td = new Test() {{
setA(10);
setA(9);
setC("9");
}};

List<String> diffLogs = diffCompareLogOfSameClass(tc, td);
}

可以预测一下结果,diffLogs会是多少?

上面的测试代码执行完后,结果集会永远为空。为啥呢? 因为这行代码“oldInstance.getClass() != newInstance.getClass()”恒等于true。为啥呢? 难道oldInstance.getClass()不等于newInstance.getClass()? 他们不都是同一个类new出来的吗?打开debug看看他们的class对象,发现一个很奇怪的现象,class的name真的不一样,而且后面还带有$1和$2。$我们都知道表示内部类,但是$1和$2这种表示是应该是匿名内部类,那么为啥new对象会出现匿名内部类呢?

image-20200406185202946

image-20200406185224520

继续看下面的代码,通过new对象,并直接赋值,与在构造代码块中直接赋值,我们比较一下class对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
@Data
static class Test {
private Integer a;

private Integer b;

private String c;
}


public static void main(String[] args) {

Test ta = new Test();
ta.setA(10);
ta.setB(9);
ta.setC("8");

Test tb = new Test();
tb.setA(10);
tb.setB(9);
tb.setC("8");

Test tc = new Test() {{
setA(10);
setA(9);
setC("8");
}};

Test td = new Test() {{
setA(10);
setA(9);
setC("8");
}};

System.out.println("ta: " + ta.getClass());
System.out.println("tb: " + tb.getClass());
System.out.println("tc: " + tc.getClass());
System.out.println("td: " + td.getClass());
}
1
2
3
4
ta: class com.hplegend.util.ObjectValueComparator$Test
tb: class com.hplegend.util.ObjectValueComparator$Test
tc: class com.hplegend.util.ObjectValueComparator$1
td: class com.hplegend.util.ObjectValueComparator$2

what? ta和tb是同一个class,但是tc和td却不是?why?难道new对象加构造代码块有什么隐藏的功能?

查阅资料,发现new对象加构造代码块(初始化代码块)还确实有隐藏功能。如下创建对象同时set值的形式,并不是我们常见的new Test对象。然后再赋值。

1
2
3
4
5
Test td = new Test() {{
setA(10);
setA(9);
setC("8");
}};

这是一种叫做“匿名类构造”的写法,实际上,下面的代码并非创建一个Test对象,而是创建了一个Test对象的子类,这个子类是匿名类,而代码:

1
2
3
4
5
{
setA(10);
setA(9);
setC("8");
}

叫做匿名内部类的初始化代码块。我们把:

1
2
3
4
5
Test td = new Test() {{
setA(10);
setA(9);
setC("8");
}};

用类张开更加明显:

1
2
3
4
5
6
7
8
9
10
11
class Sub extends Test {
{
setA(10);
setA(9);
setC("8");
}

}


Test td = new Sub();

此时是不是明白了,为啥代码块:

1
2
3
4
5
Test td = new Test() {{
setA(10);
setA(9);
setC("8");
}};

的class对象是带有$n的了?因为确实是一个匿名内部类。那么形如:

1
Test td = new Test() {};

正式匿名内部类的定义,实际上可以看成:

1
2
3
4
5
class Sub extends Test {

}

Test td = new Sub();

总结

本次踩坑的原因:只了解了构造代码块的用法,能让代码更加美观和紧凑,但是却没有详细的了解底层的实现。