java筆記:關於複雜資料存放區的問題–基礎篇:數組以及淺拷貝與深拷貝的問題(上)

來源:互聯網
上載者:User

  記得我在寫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) {
/*基礎資料型別 (Elementary Data Type)*/
System.out.println("========================基礎資料型別 (Elementary Data Type)==========================");
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);
}

}
  運行結果如下:
========================基礎資料型別 (Elementary Data Type)==========================
[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中所有參數傳遞都是按引用傳遞。

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

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

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

 

  

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.