guava之Lists.transform陷阱
0x00
guava提供了Lists.transform这个方法来方便地对List进行转换,原型如下:
public static <F, T> List<T> transform(List<F> fromList, Function<? super F, ? extends T> function);
这个需求在实际开发中非常常见。
最常见的就是请求和响应的转换了。用户请求的Req和后台使用的Bean一定会有差异,同时给用户返回的Resp和后台使用的Bean也一定会有差异。造成这种差异的原因很多,比如请求和响应中有敏感数据,或者数据复杂等。
使用guava的Lists.transform可以方便的处理,比如:
List<Integer> originList = Lists.newArrayList(1, 10);
System.out.printf("originList is %s.\n", originList);
List<Point> pointList = Lists.transform(originList, p -> {
Point point = new Point(p, p);
return point;
});
System.out.printf("pointList list is %s.\n", pointList);
输出:
originList is [1, 10].
pointList list is [java.awt.Point[x=1,y=1], java.awt.Point[x=10,y=10]].
上面这个代码将一个整数列表,转换成了一个X坐标和Y坐标相等的Point列表。
0x01
但是Lists.transform返回的这个List是有猫腻的!
如果你想对这个返回的List进行修改,如下:
for (Point point : pointList) {
point.x = point.x + 1;
point.y = point.y + 1;
}
System.out.printf("after change, pointList list is %s.\n", pointList);
输出:
after change, pointList list is [java.awt.Point[x=1,y=1], java.awt.Point[x=10,y=10]].
发现pointList里的值并没有发生改变!
如果你是用List.get()进行访问元素,将元素的hashcode打印出来。会发现,每次访问同一个元素获得的hashcode是不一样的,他们是不同的元素,所以修改的不是最初的对象!
Point point = pointList.get(0);
System.out.printf("pointList[0] is %s, hashcode is %d.\n", point, System.identityHashCode(point));
Point point2 = pointList.get(0);
System.out.printf("pointList[0] is %s, hashcode is %d.\n", point2, System.identityHashCode(point2));
输出:
pointList[0] is java.awt.Point[x=1,y=1], hashcode is 2101440631.
pointList[0] is java.awt.Point[x=1,y=1], hashcode is 2109957412.
0x10
[柯南BGM响起]
真相只有一个!
就是Lists.transform返回的List有问题!
仔细看下该方法的实现:
public static <F, T> List<T> transform(List<F> fromList, Function<? super F, ? extends T> function) {
return (List)(fromList instanceof RandomAccess ? new Lists.TransformingRandomAccessList(fromList, function) : new Lists.TransformingSequentialList(fromList, function));
}
在这个例子中,传入的fromList出一个RandomAccess的List,所以返回一个Lists.TransformingRandomAccessList,这个类声明如下:
private static class TransformingRandomAccessList<F, T> extends AbstractList<T> implements RandomAccess, Serializable
这个类的override了多个方法,其中就有get方法,看下get方法的实现:
public T get(int index) {
return this.function.apply(this.fromList.get(index));
}
即每次get的时候,都会调用一次传进来的Function。如果你的Function每次都new一个对象返回,那么每次get获取到的必然是不同的对象!
同理,迭代器的两个方法如下:
public Iterator<T> iterator() {
return this.listIterator();
}
public ListIterator<T> listIterator(int index) {
return new TransformedListIterator<F, T>(this.fromList.listIterator(index)) {
T transform(F from) {
return TransformingRandomAccessList.this.function.apply(from);
}
};
}
每次迭代的时候也是调用了Function方法!
咿?如果每次都调用了Function方法,那么如果源List修改了,那么岂不是获取到数据也变了?试下:
Point point = pointList.get(0);
System.out.printf("pointList[0] is %s, hashcode is %d.\n", point, System.identityHashCode(point));
originList.set(0, 100);
Point point2 = pointList.get(0);
System.out.printf("pointList[0] is %s, hashcode is %d.\n", point2, System.identityHashCode(point2));
输出:
pointList[0] is java.awt.Point[x=1,y=1], hashcode is 2101440631.
pointList[0] is java.awt.Point[x=100,y=100], hashcode is 2109957412.
果然!
凶手原来是你!
不,凶手其实是我们!
0x11
说到这里,java8提供了类似方便的方法进行这样的处理:
List<Point> pointList = originList.stream().map(p -> {
Point point = new Point(p, p);
return point;
}).collect(Collectors.toList());
因为这个方法返回的是ArrayList的实例。