引子
测不准原理中有一个现象:人们对光子的观测行为本身会影响观测的结果。近期在排查问题中也遇到了类似的“诡异”问题,初期百思不得其解,真相大白之后摇头苦笑,记在这里贻笑大方。
现象
先看一段经过简化的代码。
// 对象结构体
public class Foo {
private int id;
public Foo(int i) {
this.id = i;
}
public Foo() {
}
public void setId(int id) {
this.id = id;
}
public int getId() {
return id;
}
}
// 处理逻辑
List<Foo> fooList = new ArrayList<>();
fooList.add(new Foo(1));
List<Foo> newFooList=Lists.transform(fooList,new Function<Foo, Foo>(){
@Override
public Foo apply(Foo input){
Foo output=new Foo();
// 一些处理逻辑
output.setId(input.getId()*10);
return output;
}
});
for(Foo foo:newFooList){
// 后处理逻辑
int id=foo.getId()*10;
foo.setId(id);
}
问题:最后newFooList里面元素的id是多少?
上面的代码不复杂,JAVA又有强大趁手的调试工具,连上debugger单步走一遭就是。可是结果却令人惊讶:虽然每一行代码都走到了,最后的结果却是明白无误的10——中间的那次乘10操作被吃了?更诡异的事情在后面:如果在for循环里加一个断点,明白无误的看到新的id被赋值进去,但是把鼠标移到newFooList上,里面的指向的object对象里的值还是10,且每次看这个对象的地址都在不断递增,好像后台在不断的new对象,有内存泄露?
分析
对象的地址递增可以解释为何后处理逻辑的值设不进list里:看到的对象已经不是当初的保存对象了,当然看不到设置的值了。apply函数中有new更是高度怀疑对象。
为何list里保存对象会变呢?搂了一遍Guava list的源码,里面自己实现了一个list以及相应的listIterator,只要有对数组元素的查询遍历操作,就会调用apply函数,执行apply函数里的逻辑——也就是每次new一个新对象,且设id的值为10。
为何用idea查看时,断点没动,对象的地址在递增?这里估计是idea做了特殊处理:它会调用list的get方法,重新new对象并设初值,但是不会触发里面的断点。证据就是如果在里面加上print函数,虽然没有触发断点,但是有日志打印出来。
总结
这个问题的产生的根源就是对list.transform的行为逻辑想当然了,以为apply函数的用处是一次性的。虽然java8中已经提供了替代的stream,但是在一些老系统中仍然存在着guava的代码,如果对实现原理理解不清楚就会踩坑。在上述例子中,要注意List.transform后不要再对元素进行处理,如果一定要处理,需要将结果固定到一个数组中(使用Lists.newArrayList()或者ImmutableList.copyOf)。