偶然拜讀IT界知名大佬王垠老師的博客,發現一個有意思的題目:
1 // 這段代碼里面到底哪一行錯了?為什么? 2 // 原文:http://www.yinwang.org/blog-cn/2020/02/13/java-type-system 3 public static void f() { 4 String[] a = new String[2]; 5 Object[] b = a; 6 a[0] = "hi"; 7 b[1] = Integer.valueOf(42); 8 }
雖然小菜才疏學淺,但本着學習交流的態度,寫下此篇文章來分析一下這個問題。
首先我們要讀懂每一行代碼在做什么:
String[] a = new String[2]; 定義一個字符串類型的數組a,並初始化。
Object[] b = a; 定義一個對象類型的數組b,並將字符串類型數組a賦值給b。
a[0] = "hi"; 使用變量a訪問數組中的第一個元素,賦值。
b[1] = Integer.valueOf(42); 使用變量b訪問數組中的第二個元素,賦值。
只有簡單的四行代碼,相信讀者都可以看的懂。
先不考慮太多,直接執行一下代碼,編譯通過,運行報錯:java.lang.ArrayStoreException: java.lang.Integer。
錯誤提示我們第四行代碼有問題,不可以將整型數據存儲到數組b中,而b是一個Object類型的數組,編譯通過,卻無法賦值。
分析一下原因,數組b的引用指向數組a,我們操作數組b,實際在內存中,訪問的應該是數組a,而數組a是一個字符串類型數組,整個過程中,並不存在Object類型的數組,僅有一個字符串類型的數組在內存中被創建,如圖:
變量a和變量b只不過是門面,通過這兩道門,到達的是同一個房間。只不過a門只允許String類型通過,而b門沒有任何限制。
因此,假如我們寫下a[0] = Integer.valueOf(42);,編譯器立刻會發現錯誤,提示類型錯誤,而b[1] = Integer.valueOf(42);的寫法是符合規則的,但由於實際數據結構是String數組,所以運行肯定無法通過。
為什么會這樣?出現這種問題的根本原因,在於Object[] b = a;,嚴格來說,這種語法是錯誤的,但是在JDK規范中卻被認可。
為什么說是錯誤的?面向對象中的繼承我們再熟悉不過了,子類完全具有父類的能力,子類可以退化成為父類。
單說String的確是Object的子類,完全符合規則,但數組是另一回事,本例中String數組僅僅能容納String類型的元素,而Object數組可以容納任意類型的元素,String數組並非完全具有Object數組的能力。
從另一個角度看,無論是String[] a還是Object[] b,這兩種寫法中的變量a和變量b,僅僅能決定指針的指向(引用哪個具體的數組),而無法控制數組內的元素,只能整體操作,而數組必然要涉及某個元素的部分操作,這就造成數組內部數據結構的“逸出”,必然會出現問題。
綜上,數組之間的抽象是錯誤的,數組之間沒有直接的繼承的能力,不屬於面向對象繼承的討論范疇。
實際編寫代碼時,不必過分糾結這個問題,盡量不使用這種危險的操作,而是用更加優雅的方式去實現:
1 // 這樣就能很好的發現錯誤,避免給自己挖坑 2 public static void f() { 3 String[] a = new String[2]; 4 Object b = a; //數組本身也是對象 5 a[0] = "hi"; 6 ((String[]) b)[1] = Integer.valueOf(42); 7 }