歡迎光臨
每天分享高質量文章

ArrayList 其實也有雙胞胎,但區別還是挺大的!

點擊上方“芋道原始碼”,選擇“置頂公眾號”

技術文章第一時間送達!

原始碼精品專欄

 

來源:http://t.cn/RrhXJNL

一、問題產生

今天在學習ArrayList原始碼的時候發現了這麼一句註釋,即:

c.toArray might (incorrectly) not return Object[] (see 6260652)

https://bugs.java.com/view_bug.do?bug_id=6260652

這句話的意思是Collection集合型別的toArray()方法雖然宣告傳回值型別是Object[],但是具體呼叫時還真不一定就傳回Onject[]型別,也有可能是其他的型別,這還要取決於你c的實際型別,使用不當還會丟擲異常。這樣講可能會很懵比,下麵我將會詳細講解到底為什麼,現在我們先來看看Collection中的toArray()宣告,讓你對這個方法先有個大概的印象。

public Object[] toArray()// 宣告傳回值型別為Object[]

那麼什麼情況會出現上面的bug呢?我們先來看看下麵兩個例子:

1、沒有拋異常的情況

// 宣告一個ArrayList集合,泛型為String型別
List list = new ArrayList<>();
// 添加一個元素
list.add("list");
// 將上面的集合轉換為物件陣列
Object[] listArray = list.toArray(); ................ 1
// 輸出listArray的型別,輸出class [Ljava.lang.Object;
System.out.println(listArray.getClass());
// 往listArray賦值一個Onject型別的物件
listArray[0] = new Object();

2、拋異常的情況

// 同一創建一個串列,但是現在是通過Arrays工具類來創建,創建的串列型別為Arrays的內部類ArrayList型別
List asList = Arrays.asList("string");
// 轉換為物件陣列
Object[] asListArray = asList.toArray();.............. 2
// 輸出轉換後元素型別,將輸出class [Ljava.lang.String;
System.out.println(asListArray.getClass());
// 往物件陣列中添加Object型別物件,會報錯java.lang.ArrayStoreException
asListArray[0] = new Object();

上面第一種情況是通過new ArrayList()方式創建的java.util.ArrayList型別,第二種方式是使用Arrays.asList()方式創建的java.util.Arrays$ArrayList的型別,兩個型別名都是ArrayList,但實現方式確實不同的。那為什麼會報錯呢?歸根到底就是toArray()這個方法的實現方式不同導致的。我們分別先看下java.util.ArrayList類的toArray()java.util.Arrays$ArrayListtoArray()的實現方式:

java.util.ArrayList
public Object[] toArray() {
    // 呼叫Arrays工具類進行陣列拷貝
    return Arrays.copyOf(elementData, size);...............1
}

Arrays.copyOf()
public static  T[] copyOf(T[] original, int newLength) {
    return (T[]) copyOf(original, newLength, original.getClass());.................2
}
public static  T[] copyOf(U[] original, int newLength, Class extends

 T[]> newType) {
    // 在創建新陣列物件之前會先對傳入的資料型別進行判定
    @SuppressWarnings(“unchecked”)
    T[] copy = ((Object)newType == (Object)Object[].class)
        ? (T[]) new Object[newLength]
        : (T[]) Array.newInstance(newType.getComponentType(), newLength);
    System.arraycopy(original, 0, copy, 0,
                     Math.min(original.length, newLength));
    return copy;
}

下麵是java.util.Arrays$ArrayList的實現

private final E[] a;
@Override
public Object[] toArray() {
    return a.clone();
}

從上面可以看出,在java.util.ArrayList中將會呼叫ArrayscopyOf()方法,傳入一個newType進行型別判斷,newType的值為original.getClass(),由於original.getClass()傳回的型別為Object[](具體看ArrayList的原始碼可知),所以呼叫toArray()之後將傳回一個Object[]型別陣列,所以往listArray變數裡邊丟一個Object型別的物件當然不會報錯。

再看看java.util.Arrays$ArrayList的實現,可以看出資料型別定義為泛型EE的具體型別將根據你傳入的實際型別決定,由於你傳入了"string",所以實際型別就是String[],當然呼叫a.clone()之後還是一樣傳回String[]型別,只不過是這裡做了一個向上轉型,將String[]型別轉為Object[]型別傳回罷了,但是註意,雖然傳回的取用Object[],但實際的型別還是String[],當你往一個取用型別和實際型別不匹配的物件中添加元素時,就是報錯。不服?下麵舉個慄子:

// 陣列strings為String[]型別
String[] strings = { "a""b" };
// 向上轉型為Object[]型別,那麼這個objects就屬於取用型別為Object[],而實際型別為String[]
Object[] objects = strings;
// 添加一個Object型別變數,就報錯啦!
objects[0] = new Object();//! java.lang.ArrayStoreException

為了加深理解,我們來總結下java中的向上轉型和向下轉型的區別。我們都知道我們可以通過註入Father fa = new Son()的方式進行宣告,僅為Father型別為Son型別的父類,即發生向上轉型,向上轉型在java中是自動完成的,不需要進行強制轉換,不會丟擲異常。向下轉型分為兩種情況,下麵結合代碼演示:

// 向上轉型
Father fa = new Son();

Father fafa = new Father();

// 向下轉型(不會報錯)
Son son = (Son) fa;.................1

// 向下轉型,報錯了java.lang.ClassCastException
Son sonson = (Son) fafa;.......................2

可以發現1處不會報錯,2處卻報錯了,因為1fa變數的實際型別是Son,取用型別為Father,向下轉換取決於實際型別而不取決於取用型別,比如fafa這個變數的實際型別就是其本身Father,在java中,父類預設是不能強制轉換為子類的。

二、總結

首先最重要有以下幾點:

  • 1、Java中陣列集合向上轉型之後,不能往陣列集合中添加取用型別(即父型別)的物件,而應該添加實際型別的物件,比如說`Father[] father = son[],你就不能往father中添加Father型別了,而應該是Son

  • 2、Java中向上轉型是預設允許的,但是向下轉型可能會丟擲錯誤,得小心使用!

  • 3、要小心採用Arrays.asList()創建的集合型別不是java.util.ArrayList,而是java.util.Arrays$ArrayList,兩個類的很多方法實現方式也不一樣。

謝謝閱讀,歡迎評論區交流!



如果你對 Dubbo / Netty 等等原始碼與原理感興趣,歡迎加入我的知識星球一起交流。長按下方二維碼噢

目前在知識星球更新了《Dubbo 原始碼解析》目錄如下:

01. 除錯環境搭建
02. 專案結構一覽
03. 配置 Configuration
04. 核心流程一覽

05. 拓展機制 SPI

06. 執行緒池

07. 服務暴露 Export

08. 服務取用 Refer

09. 註冊中心 Registry

10. 動態編譯 Compile

11. 動態代理 Proxy

12. 服務呼叫 Invoke

13. 呼叫特性 

14. 過濾器 Filter

15. NIO 服務器

16. P2P 服務器

17. HTTP 服務器

18. 序列化 Serialization

19. 集群容錯 Cluster

20. 優雅停機

21. 日誌適配

22. 狀態檢查

23. 監控中心 Monitor

24. 管理中心 Admin

25. 運維命令 QOS

26. 鏈路追蹤 Tracing

… 一共 69+ 篇

目前在知識星球更新了《Netty 原始碼解析》目錄如下:

01. 除錯環境搭建
02. NIO 基礎
03. Netty 簡介
04. 啟動 Bootstrap

05. 事件輪詢 EventLoop

06. 通道管道 ChannelPipeline

07. 通道 Channel

08. 位元組緩衝區 ByteBuf

09. 通道處理器 ChannelHandler

10. 編解碼 Codec

11. 工具類 Util

… 一共 61+ 篇

目前在知識星球更新了《資料庫物體設計》目錄如下:


01. 商品模塊
02. 交易模塊
03. 營銷模塊
04. 公用模塊

… 一共 17+ 篇

原始碼不易↓↓↓

點贊支持老艿艿↓↓

赞(0)

分享創造快樂