計划真的趕不上變化,時間過得真快。廢話不多說了,今天主要記錄之前有同事遇到的一些坑分享出來。
一、封裝類的應用會引起NPE異常
對於其他對象的應用,一般在使用之前會判斷它是否為空,如果不為空才會使用它以及它里面的一些屬性值。但是對於基本類型的封裝類型,就有很多人漏掉對於它的判斷。
就在前面幾天有位同事問我說這段代碼它怎么會報空指針呢? 先模擬下這個場景下的代碼:
// 第三方的實體類對象 class Count{ Integer total; String name; String leavingNum; //... } public class DemoTest { public void fun() { Count count = new Count(); // 經過一系類對應值的獲取之后 // 需要判斷total是否為0,如果為0就進行其他的業務操作。 if (count.total == 0) { // 比方說打印日志。 } } @Test public void test() { fun(); } }
他給我看的代碼就是類似fun()方法中的代碼, 我一看這個代碼就感覺不對勁,我說你這個Count類下的total屬性是int嗎?如果是int就可以這么用。他說為什么? “如果是Integer類型,那么它的初始值不是0而是null, 而你上面的這些邏輯又不能保證total 一定會獲取到數值,那么它就還有可能是null,你這樣使用的話就有可能包NPE的問題。所以針對對象的使用提前判空更有保障。” 我接着說。 在我說的過程中他反應還是很快的,立馬查看了這個實體類中total字段的類型,於是就明白了。 如果沒有養成提前使用判空的習慣(除非你能保證一定會有值),老手都容易會踩這樣的坑。比方說針對Boolean類型的使用,有很多人會直接這樣的哦(這樣肯定會有問題的)。
Boolean flag = null; // 經過一系類操作處理 // 進行判斷 if (flag) { //...... }
另外針對基本類型的封裝類型使用還有些要注意的請看這篇文章:https://www.cnblogs.com/yuanfy008/p/8321217.html
二、subList帶來的隱患
現在有很多都是基於分布式服務,那應該會存在這個域對應數據需要同步到其他域下,然后這種同步必然會產生差異,需要一種自檢的job去檢測差異。打個比方有些商家自己有官網售賣自己的產品,也還有可能會在天貓開旗艦店售賣。假如它分配在天貓的商品信息是通過它本地天貓數據庫同步過去的,那么這種難免會產生差異,特別是庫存,如果一邊多一邊少就可能會導致超賣的情況。 所以這種情況需要有個job對比兩邊的差異,下面先簡單模擬下事發情況(注意下面的用法):
1 public class SubListTest { 2 3 @Test 4 public void test1() { 5 // 初始集合(有序) 6 List<Integer> list = new ArrayList<>(); 7 list.addAll(Arrays.asList(1, 2, 3, 4)); 8 9 // 業務場景:需要將list集合與很多場景下的數據進行對比,然后取出不同的。 10 // 對比的場景就不還原了,假設每次都是前面兩個不同。 這里只列舉四次對比,為了方便查看效果不使用for循環 11 12 // 第一次截取不同的數據 13 list = list.subList(0, 2); 14 // 查看list中有多少數據 15 System.out.println(list); 16 17 // 查詢有新的數據,往list中添加 18 list.addAll(Arrays.asList(5, 6)); 19 //... 20 // 第二次截取 21 list = list.subList(0, 2); 22 System.out.println(list); 23 24 // 查詢有新的數據,往list中添加 25 list.addAll(Arrays.asList(7, 8)); 26 //... 27 // 第三次截取 28 list = list.subList(0, 2); 29 System.out.println(list); 30 31 // 查詢有新的數據,往list中添加 32 list.addAll(Arrays.asList(9, 10)); 33 //... 34 // 第四次截取 35 list = list.subList(0, 2); 36 System.out.println(list); 37 38 } 39 }
看到上面代碼其實很簡單,輸出結果大家也都知道。下面先一步一步的分析,然后再介紹在大量數據的情況下這會產生結果。
第一步:查看下list的內存分配地址,后面會有需要。
第二步:跳到第一次截取之后,看list有什么變化?
它的內存地址變了,也就說每次subList都會產生一個新對象,那么得查看下這subList的源碼,而源碼中確實是會產生一個新對象。但是請仔細SubList的構造函數,其中會存放它的父級對象。那么這會產生什么影響呢?請接着往下看。
public List<E> subList(int fromIndex, int toIndex) { subListRangeCheck(fromIndex, toIndex, size); return new SubList(this, 0, fromIndex, toIndex); } private class SubList extends AbstractList<E> implements RandomAccess { private final AbstractList<E> parent; private final int parentOffset; private final int offset; int size; SubList(AbstractList<E> parent, int offset, int fromIndex, int toIndex) { this.parent = parent; this.parentOffset = fromIndex; this.offset = offset + fromIndex; this.size = toIndex - fromIndex; this.modCount = ArrayList.this.modCount; } // 后面方法省略 }
第三步:在第三次截取之前直接在源碼中SubList構造函數中打斷點,然后跳轉進來,看看對應對象的屬性值:
咋一看,這里面怎么param_1怎么來的?通過第二步查看SubList的源碼再加上第一步的需要你留意list原生的的對象內存地址,你就知道param_1對應是這個list的根對象,它一直保留子對象新增的對象。那么大家想下,這種做法當遇到海量數據對比差異時會產生什么影響呢?
如果剛才看懂了上面所說的,那么肯定會明白這個list的根對象累積到后面肯定會變成大對象,這樣會導致平凡的fullGc而且你還回收不掉。因為它一直在使用,直至這個程序運行結束。
那么像上面這種場景怎么優化解決呢? 可以這樣考慮, 每次對比時候都弄一個新的list去獲取差異,然后再把這個有差異的list添加至總的差異結果集中。(其實我們平時也不會用一個list去反反復復的subList)。如果大家有更好的優化,請留言探討。