一個BUG的思考:Java中使用final修飾變量真的不能修改它的“值”嗎?


前言
在Java中,當我們希望告知編譯器某個變量在初始化之后它的值不再需要改變時,我們常用final修飾該變量。而什么情況下我們會有這種需求呢?例如,當我們在B線程使用到在A線程定義的變量時,我們就必須要使用final來修飾該變量,原理是在並發情況下禁止CPU的指令重排,防止對象引用被其他線程在對象被完全構造完成前拿來使用。所以在Java中,final用來修飾變量時,我們常常人為該變量的值是不能被修改的。那么真的是這樣嗎?下面以一個開發中遇到的case作為一個思考切入點來對該問題進行分析。
case
先來看下面一段代碼,就是使用攝像頭進行預覽畫面,在每一幀被渲染出來的時候就會走onPreviewFrame這個回調,bytes數組里面保存了這一幀畫面的信息,我們可以截取進行人臉檢測、截屏等,都是可以的。但是為了避免垃圾回收器重復回收,這里定義了同步的byte數組的緩存池,每次從緩存池里面拿一個byte數組,回調結束的時候又把它返回到緩存池里面,保證重復使用到的都是同一個byte數組,減少內存的消耗。
mCamera.startPreview();
mCamera.setPreviewCallbackWithBuffer(new Camera.PreviewCallback() {

    Pools.SynchronizedPool<byte[]> mByteArrayPool = new Pools.SynchronizedPool<byte[]>(1) {
        private final byte[][] mPool = new byte[1][mPreviewSize.getWidth() * mPreviewSize.getHeight() * 3 / 2];
        private int mPoolSize = mPool.length;

        @Override
        public byte[] acquire() {
            if (mPoolSize > 0) {
                final int lastPooledIndex = mPoolSize - 1;
                byte[] instance = mPool[lastPooledIndex];
                mPool[lastPooledIndex] = null;
                mPoolSize--;
                return instance;
            }
            return new byte[mPreviewSize.getWidth() * mPreviewSize.getHeight() * 3 / 2];
        }

        @Override
        public boolean release(@NonNull byte[] element) {
            if (mPoolSize < mPool.length) {
                mPool[mPoolSize] = element;
                mPoolSize++;
                return true;
            }
            return false;
        }
    };

    @Override
    public void onPreviewFrame(byte[] bytes, Camera camera) {
        final byte[] dst = mByteArrayPool.acquire();
        
        // 截屏(另外一個線程操作)
        sreenShot(dst);
        
        if (dst != null) {
            mByteArrayPool.release(dst);
        }

        camera.addCallbackBuffer(bytes);
    }
});
mCamera.addCallbackBuffer(new byte[mPreviewSize.getWidth() * mPreviewSize.getHeight() * 3 / 2]);
mCamera.addCallbackBuffer(new byte[mPreviewSize.getWidth() * mPreviewSize.getHeight() * 3 / 2]);
復制代碼細心的朋友可以看到,這里的dst是定義成final類型的,按道理來說它的值應該是不會被改變的,但是,最近測試在給我反饋下面圖片這個問題的時候,對代碼思前想后之后我確認了這個BUG發生的原因,就是因為上面的final數組值的變化引起了截圖內容的錯誤。

在上面的代碼中可以看到,在使用dst進行截屏(byte數組轉換成RGB)時,這個dst也被回收到緩存池里面,當還沒有截屏完成時,下一幀已經被存到dst數組里面,最后導致截屏的時候使用的數組既包含了上一幀的畫面,也包含了下一幀的畫面。由此可見這個dst數組即便是被定義成final類型,但是它的“值”還是會被改變的。
那么到底為什么會是這樣的呢?通過翻閱《Java編程思想》,里面是這么描述的:

對於基本類型,final使數值恆定不變;而對於對象引用,final使引用恆定不變。一旦引用被初始化指向一個對象,就無法再把它改為指向另一個對象。然而,對象其自身確實可以被修改的,Java並未提供使任何對象恆定不變的途徑。這一限制同樣適用數組,它也是對象。

總結
由此在Java中,對於基本類型的變量使用final修飾,它的值是恆定不變的;而對於對象(包括數組)引用的變量使用final來修飾,它引用的對象是恆定不變的,而對象的值是可以變的。
這個知識點可能不會很多人知道,但是了解之后對於我們日常開發遇到極端的Bug時可以少走彎路,減少很多Bug Fix的時間。

作者:特雷西多士
鏈接:https://juejin.im/post/5d831d7d6fb9a06ade1148b0
來源:掘金

更多免費技術資料可關注:annalin1203


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM