測不準原理?記一次Guava佇列問題的排查

北征發表於2017-06-05

引子

測不準原理中有一個現象:人們對光子的觀測行為本身會影響觀測的結果。近期在排查問題中也遇到了類似的“詭異”問題,初期百思不得其解,真相大白之後搖頭苦笑,記在這裡貽笑大方。

現象

先看一段經過簡化的程式碼。

// 物件結構體
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)。


相關文章