關於複雜數據存儲的問題--基礎篇:數組以及淺拷貝與深拷貝的問題


  記得我在寫javascript筆記時候說過:程序就是由數據和運算組成。所以對數據存儲以及讀取方式的研究是熟練掌握語言精髓的重要途徑。我在上篇文章裏說道我想重新回顧一些知識,這些知識就是數據存儲的問題,而且是複雜數據存儲的問題。我個人認爲一名優秀的程序員應該有四個主要指標:一是項目經驗,二是程序優化的能力,三是良好的設計理念,四是快速準確定位程序bug的能力。項目經驗不說,這個需要積累,而其他的能力都是可以通過學習而不斷強化的。而語言中數據存儲能力掌握的優劣是你優化程序的水平的高低的重要指標,你想讓自己的程序越來越快,按什麼數據模型快速存儲數據,並且能很快的檢索被存儲的數據纔是程序優化的本質,因此學習數據的存儲方式特別是複雜數據的存儲方式就十分的重要了。

  (學習和提高是永無止盡的,我不是技術狂熱分子,但我是一個想把事業做精做透的人,更想自己有一天能做到真正意義的創新,所以我要不斷的自我激勵,時刻準備抓住機會和靈感的那天,因爲我相信創新的靈感和機會源自於你不斷準備靈感和機會到來的這個過程之中。)

  呵呵,不說大話了,開始幹實事了。不想創新,我只想寫程序時候思路開闊,不會只要是數組就寫ArrayList,碰到鍵值對就是HashMap,也許我們會有更好的選擇,但你一定要知道更好的選擇是什麼。

  java的第一個真理:在java裏除了基本類型就是對象。java比C好用的一個重要指標:java裏提供了大量對對象操作的集合類,這些集合類大部分都在java.util包裏

  但是第一個用來存儲複雜數據的數據結構不是List、Set或者是Map,而是數組(Array)。我就先從數組說起。

  如何定義數組

  如果有位面試官讓你說出這個問題的答案,你能很全面的說出來嗎?或許很多童鞋可以,但是我好幾次都回答的不全面,大家先看下面的代碼:

複製代碼
package cn.com.sxia;

import java.util.ArrayList;
import java.util.List;

public class ArrayDefine {

    public static void main(String[] args) {
        // 註釋一
        int[] a1;
        int a2[];
        // 註釋二
        int[] a3 = { 1, 2, 3, 4, 5 };
        int a4[] = { 6, 7, 8, 9, 0 };
        // 註釋三
        int[] a5 = new int[3];
        int a6[] = new int[3];
        // 註釋四
        Integer[] b1;
        Integer b2[];
        Integer[] b3 = new Integer[3];
        Integer b4[] = new Integer[3];
        // 註釋五
        Integer[] b5 = { 1, 2, 3 };
        Integer[] b6 = { new Integer(4), new Integer(5), new Integer(6) };
        System.out.println("b5.length:" + b5.length);
        System.out.println("b6.length:" + b6.length);
        // 註釋六
        Integer[] b7 = new Integer[] { new Integer(4), new Integer(5),
                new Integer(6) };
        System.out.println("b7.length:" + b7.length);
    }
}
複製代碼

  結果如下:

b5.length:3
b6.length:3
b7.length:3

  下面我就對註釋做一一解釋:

  註釋一java裏提供了兩組定義數組的方式,二者是等價的,而int a2[]是C和C++語言的風格。在java裏定義一個數組只是表明某個變量獲得了數組的引用,但此時數組是沒有被分配任何空間的,要讓數組獲得相應的內存空間就得對數組進行初始化。

  註釋二:假如我們知道我們那些數據要放到數組中,這些數據是確定無疑的,那麼註釋二的做法是一個簡便的 數組初始化方式:直接使用花括號初始化數組

  註釋三:這個實例給了我們另外一種選擇:當我們只知道數組的個數而不知道數組內容時候數組該如何定義了?這個時候數組的length屬性裏存儲了數組的長度,length或許是我們在使用數組這個數據結構中最常用的一個屬性,適當了用局部變量記下它的值,而不是每次通過數組重新計算length的值是一種提高程序效率的有效方式。

  註釋四:這裏我將數據類型換成了對象,用法和基本類型一樣。

  註釋五:用花括號初始化對象,大家看到對於Integer類型我們可以直接使用int類型初始化,java會自動把int轉化這個Integer對象。

  註釋六:這裏給出了對象數組定義的另一種做法,效果和註釋五下面的代碼類似,不過註釋六下面的代碼會靈活點,例如我們把Integer換成Object,那麼這個數組就變成了通用數組了,這個小技巧太小兒科了,這裏就不深入了。

  雖然數組存儲基本類型和對象從代碼表象上看用法差不多,但是它們在本質上還是有區別的:對象的數組保存的是引用而基本類型的數組是直接保存基本類型的值

  在java裏我們會常常忽視數組,這是一個極其不好的習慣,數組不管在那個編程語言裏它有時都會是優秀的複雜數據存儲結構,我們把數組和java裏的ArrayList類作比較,數組有如下的優勢:

  1. 效率很高:數組是java中一種效率最高的存儲和隨機訪問對象引用的方式,數組是使用一個簡單的線性結構,這就讓線程的訪問速度非常的快。另外線程的長度是固定的,這就免去了動態長度所帶來的性能開銷,這也是數組比較快的重要原因。最後數組存儲的數據都是統一類型的,因此使用數組時候就少了對數據類型的校驗和轉化工作,這樣數組的效率相比集合類的那種可以存儲任何類型的的特點比較起來,效率又會提升很多。
  2. 數組有存儲基本類型的能力:java中的集合類是針對對象的存儲設計的,而數組是什麼數據類型都可以作爲它的存儲的內容。
  不過java的util包還是提供對數組操作的工具類Arrays,該類的方式都是靜態使用起來很方便,但是我個人覺得這個類使用價值不大,很多功能我們自己去寫可能會更好些。
  Arrays雖然不討我喜歡,但是System類裏的arraycopy倒是很討我喜歡,在看大夥看代碼之前我們先看看arrayCopy方法在jdk文檔裏的解釋吧:
複製代碼
arraycopy
public static void arraycopy(Object src,
                             int srcPos,
                             Object dest,
                             int destPos,
                             int length)從指定源數組中複製一個數組,複製從指定的位置開始,到目標數組的指定位置結束。從 src 引用的源數組到 dest 引用的目標數組,數組組件的一個子序列被複製下來。被複制的組件的編號等於 length 參數。源數組中位置在 srcPos 到 srcPos+length-1 之間的組件被分別複製到目標數組中的 destPos 到 destPos+length-1 位置。 
如果參數 src 和 dest 引用相同的數組對象,則複製的執行過程就好像首先將 srcPos 到 srcPos+length-1 位置的組件複製到一個帶有 length 組件的臨時數組,然後再將此臨時數組的內容複製到目標數組的 destPos 到 destPos+length-1 位置一樣。 

If 如果 dest 爲 null,則拋出 NullPointerException 異常。 

如果 src 爲 null, 則拋出 NullPointerException 異常,並且不會修改目標數組。 

否則,只要下列任何情況爲真,則拋出 ArrayStoreException 異常並且不會修改目標數組: 

src 參數指的是非數組對象。 
dest 參數指的是非數組對象。 
src 參數和 dest 參數指的是那些其組件類型爲不同基本類型的數組。 
src 參數指的是具有基本組件類型的數組且 dest 參數指的是具有引用組件類型的數組。 
src 參數指的是具有引用組件類型的數組且 dest 參數指的是具有基本組件類型的數組。 
否則,只要下列任何情況爲真,則拋出 IndexOutOfBoundsException 異常,並且不會修改目標數組: 

srcPos 參數爲負。 
destPos 參數爲負。 
length 參數爲負。 
srcPos+length 大於 src.length,即源數組的長度。 
destPos+length 大於 dest.length,即目標數組的長度。 
否則,如果源數組中 srcPos 到 srcPos+length-1 位置上的實際組件通過分配轉換並不能轉換成目標數組的組件類型,則拋出 ArrayStoreException 異常。在這種情況下,將 k 設置爲比長度小的最小非負整數,這樣就無法將 src[srcPos+k] 轉換爲目標數組的組件類型;當拋出異常時,從 srcPos 到 srcPos+k-1 位置上的源數組組件已經被複制到目標數組中的 destPos 到 destPos+k-1 位置,而目標數組中的其他位置不會被修改。(因爲已經詳細說明過的那些限制,只能將此段落有效地應用於兩個數組都有引用類型的組件類型的情況。) 


參數:
src - 源數組。
srcPos - 源數組中的起始位置。
dest - 目標數組。
destPos - 目標數據中的起始位置。
length - 要複製的數組元素的數量。 
拋出: 
IndexOutOfBoundsException - 如果複製會導致對數組範圍以外的數據的訪問。 
ArrayStoreException - 如果因爲類型不匹配而使得無法將 src 數組中的元素存儲到 dest 數組中。 
NullPointerException - 如果 src 或 dest 爲 null
複製代碼
  我們的代碼如下:
複製代碼
package cn.com.sxia;

import java.util.Arrays;

public class ArraysCopy {

    
    public static void main(String[] args) {
        /*基本數據類型*/
        System.out.println("========================基本數據類型==========================");
        int arr1[] = new int[5];
        int arr2[] = new int[9];
        
        //Arrays工具類的部分方法使用
        Arrays.fill(arr1, 7);
        System.out.println(Arrays.toString(arr1) + "arr1的長度是:" + arr1.length);
        Arrays.fill(arr2, 9);
        System.out.println(Arrays.toString(arr2) + "arr2的長度是:" + arr2.length);
        
        //數組拷貝
        System.arraycopy(arr1, 0, arr2, 0, arr1.length);
        System.out.println("新的數組:" + Arrays.toString(arr2) + "arr2的長度是:" + arr2.length);
        
        /*對象的操作*/
        System.out.println("========================對象的操作==========================");
        Integer[] arrObj1 = new Integer[5];
        Integer[] arrObj2 = new Integer[9];
        Arrays.fill(arrObj1, new Integer(7));
        System.out.println(Arrays.toString(arrObj1) + "arrObj1的長度是:" + arrObj1.length);
        Arrays.fill(arrObj2, new Integer(9));
        System.out.println(Arrays.toString(arrObj2) + "arrObj2的長度是:" + arrObj2.length);
        
        //數組拷貝
        System.arraycopy(arrObj1, 0, arrObj2, 0, arrObj1.length);
        System.out.println("新的數組:" + Arrays.toString(arrObj2) + "arrObj2的長度是:" + arrObj2.length);
    }

}
複製代碼
  運行結果如下:
複製代碼
========================基本數據類型==========================
[7, 7, 7, 7, 7]arr1的長度是:5
[9, 9, 9, 9, 9, 9, 9, 9, 9]arr2的長度是:9
新的數組:[7, 7, 7, 7, 7, 9, 9, 9, 9]arr2的長度是:9
========================對象的操作==========================
[7, 7, 7, 7, 7]arrObj1的長度是:5
[9, 9, 9, 9, 9, 9, 9, 9, 9]arrObj2的長度是:9
新的數組:[7, 7, 7, 7, 7, 9, 9, 9, 9]arrObj2的長度是:9
複製代碼
  代碼裏我順便演示了Arrays類的部分功能,使用數組經常因爲數組大小一開始就固定好的缺點,我們不得不去重新拷貝數組,System.arraycopy方法提供了一個十分簡便的方案,不過這個方法也是有問題的,從代碼裏我們可以認爲System的arrayCopy方法什麼樣的數組都可以拷貝,但是對象數組的拷貝就有點不同,對象拷貝複製的是對象的引用而非對象的本身,這種拷貝叫做淺拷貝(shallow copy)。
  方法的問題就是這個淺拷貝所造成,下面我就談談淺拷貝以及它對應的深拷貝。
  淺拷貝和深拷貝的定義如下:

  淺拷貝:比如A對象被複制到B對象,但是B對象只是複製了A對象本身,如果A對象裏還有存在指向其他對象數組或者是引用,B對象內部不會複製這些內部的信息而是指向原來A對象引用的信息。

  深拷貝:還是列舉A對象被複制到B對象的例子,B對象拷貝到的是A對象的所有信息,包括A對象內部的對象引用。

  可能看到上面的解釋很多人還是不太清晰淺拷貝和深拷貝的區別,我下面用圖形來展示它們的區別,首先是A對象,A裏包含了兩個內部對象innerA和innerB,如下圖:

  如果把A對象拷貝到B對象,但是B對象內部並沒有拷貝innerA對象和innerB對象,B對象內部還是指向原來的innerA對象和innerB對象,如下圖:

  如果把A對象深拷貝到B對象,那麼結果如下圖:

  A的內部對象也會拷貝到B對象內部。

  上面的圖形應該可以清晰的表達出淺拷貝和深拷貝的區別了吧。爲什麼會有淺拷貝和深拷貝了,到底是什麼原因產生了淺拷貝和深拷貝了?昨天一個朋友用C語言的方式給我做了解答,哎,可惜C語言我只是在大學裏學過,現在忘記的差不多了,但是我哪位朋友指出了淺拷貝和深拷貝是因爲指針所產生的,產生的條件就是值的傳遞和返回,本質是數據在內存中存放的方式所造成

  下面我就要探求深淺拷貝的本質了,首先我又要說一個java語言裏的真理:Java中所有參數傳遞都是按引用傳遞

   引用,引用還是引用,我前面講了太多引用了啊。

  今天寫累了,我下一篇博文就從這個萬惡的引用講起。

  重新研究編程語言是件很開心的事情,特別是在你已經做過一些項目以後你再看使用的語言的語法知識,你會有種豁然開朗的感覺:你知道那些知識很有用,那些知識比較難,更重要的是你知道你重新複習這些技術你今後能把他們用到什麼樣的地方,或許有人說這些都是基礎,我們都做不少的項目了,何必再浪費時間,但你如果是對你所做的技術有更高的要求,你想進步而不是應付工作,學精一個東西一定會讓你的成長不會很快碰到瓶頸了,不斷學習和工作的人總會比懈怠的人收穫的更多,這就是我的想法。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章