# 資料結構 ###### tags: `java` ## 目錄 - [陣列](#陣列) - [一維陣列](#一維陣列) - [二維陣列](#二維陣列) - [字串](#字串) - [容器](#容器) - [List](#List) - [Set](#Set) - [Map](#Map) ## 陣列 陣列(Array)是一組資料的集合,集合裡的每筆資料都會有一個對應的索引值(Index),只要指定索引值就可以取出對應的資料,在程式中經常會使用陣列進行整批資料的存放;在 Java 中,陣列不僅是一組資料群組,也是一個物件,可以透過一些好用的物件方法來做操作,所以比傳統上的一些程式語言的陣列只是一群資料方便許多。 ### 一維陣列 現在要整理全班的 Java 小考成績,您希望寫個小程式,全班共有 10 名學生,每個學生有「姓名」、「學號」、「國文」、「英文」、「數學」等成績。假使用宣告變數的方式,將會需要10 x 50 = 500個變數來儲存成績資料,假使今天班上學生是20名甚至50名呢?光宣告變數就非常麻煩。 Java 提供「陣列」(Array)可以宣告一個以「索引」(Index)作為識別的資料結構,在 Java 中,可以這麼宣告一個陣列並初始陣列內容: ``` int[] array = {1, 2, 3, 4, 5}; ``` > **補充** > > 此為陣列簡易宣告方式。 這個程式片段宣告了一個 score 陣列,它的內容包括 1、2、3、4 與 5 這五個元素,要存取陣列時,必須透過索引值來指定存取陣列中的哪個元素,在Java 中陣列的索引是由0開始,也就是說索引 0 的位置儲存 1、索引 1 的位置儲存 2、索引 3 的位置儲存 4,依此類推,如果陣列中的每個值取出並顯示出來,可以使用 for 迴圈,如範例: ``` public class App { public static void main(String[] args) { int[] score = {1, 2, 3, 4, 5}; for(int i = 0; i < score.length; i++) System.out.printf("array[%d] = %d\n", i, array[i]); } } ``` 執行的結果如下: ``` array[0] = 1 array[1] = 2 array[2] = 3 array[3] = 4 array[4] = 5 ``` 在存取陣列元素時,必須注意指定的索引值不可超出陣列範圍,例如上面範例中,陣列最多可以索引到 4,所以不可以存取超過 4 的索引值,否則會發生 ArrayIndexOutOfBoundsException 例外,如果不處理這個例外,程式將會終止。 使用 length 這個陣列物件的屬性成員,在 Java 中陣列是一個物件,而不是單純的資料集合,陣列物件的 length 屬性成員可以取回陣列的長度,也就是陣列中的元素個數。 當宣告一個陣列時,其實就是產生一個陣列物件,在 Java 中物件都是以 new 來配置記憶體空間,陣列一樣,完整的陣列宣告方式如下: ``` int[] array = new int[10]; ``` 在上面的宣告中,arr 是個 int[] 型態的參考名稱,程式會為 array 配置可以儲存 10 個 int 整數的一維陣列物件,索引為 0 到 9,初始值預設為 0,在 Java 中配置陣列之後,若還沒有指定初值,則依資料型態的不同,會預設有不同的初值,如下表所示: ##### **陣列元素初始值** | 資料型態 | 初始值 | | -------- | ------ | | byte | 0 | | short | 0 | | int | 0 | | long | 0L | | float | 0.0f | | double | 0.0d | | char | \u0000 | | boolean | false | **範例:**使用配置的語法來宣告陣列,並使用 for 迴圈來設定每個元素的值然後顯示出來: ``` public class App { public static void main(String[] args) { int[] arr = new int[10]; System.out.print("arr 初始值: "); for(int i = 0; i < arr.length; i++) { System.out.print(arr[i] + " "); arr[i] = i; } System.out.print("\narr 設定值: "); for(int i = 0; i < arr.length; i++) System.out.print(arr[i] + " "); System.out.println(); } } ``` 執行結果: ``` arr 初始值: 0 0 0 0 0 0 0 0 0 0 arr 設定值: 0 1 2 3 4 5 6 7 8 9 ``` 如果您想要在使用 new 新增陣列時一併指定初始值,則可以如下撰寫: ``` int[] score = new int[] {33, 85, 21, 94, 67}; ``` > **補充** > > 這個方式不必指定陣列長度,其長度為後面提供的初始值數量。 完整範例: ``` public class App { public static void main(String[] args) { int[] score = new int[] {34, 86, 41, 95, 27}; for(int i = 0; i < score.length; i++) System.out.printf("score[%d] = %d\n", i, score[i]); } } ``` 由於陣列的記憶體空間是使用 new 配置而來,表示也可以使用動態的方式來宣告陣列長度,而不用在程式中事先決定陣列大小,例如: ``` import java.io.*; public class App { public static void main(String[] args) throws Exception { BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); System.out.print("請輸入學生人數: "); int length = Integer.parseInt(br.readLine()); float[] score = new float[length]; // 動態配置長度 for(int i = 0; i < score.length; i++) { System.out.print("輸入分數:"); float input = Float.parseFloat(br.readLine()); score[i] = input; } System.out.print("\n分數:"); float total = 0; for(int i = 0; i < score.length; i++) { total = total + score[i]; System.out.print(score[i] + " "); } System.out.printf("\n平均:%.2f\n", total / score.length); } } ``` 執行結果: ``` 請輸入學生人數: 3 輸入分數:38.8 輸入分數:66.4 輸入分數:93.1 分數:38.8 66.4 93.1 平均:66.10 ``` **解說:** 先宣告一個陣列參考名稱 score,使用 float[] score 表示 score 名稱將參考至一個元素為 float 的一維陣列物件,在使用者輸入指定長度後,再使用這個長度來配置陣列物件,並將這個陣列物件指定給 score 名稱來參考。 > **補充** > > 陣列的索引值由 0 開始表示的是:所指定的陣列元素相對於陣列第一個元素記憶體位置的位移量(Offset)。索引為 0 表示位移量為 0,所以就是指第一個元素,而索引 9 就是指相對於第一個元素的位移量為 9。 ### 二維陣列 一維陣列使用「名稱」與「一個索引」來指定存取陣列中的元素,二維陣列使用「名稱」與「兩個索引」來指定存取陣列中的元素,其宣告方式與一維陣列類似: ``` int[][] arr = { {1, 2, 3}, {4, 5, 6} }; ``` 從上面這個程式片段來看,可以看出二維陣列的索引方式,宣告了 2 列(Row)3 行(Column)的陣列,使用 { } 與適當的斷行可以指定陣列初值。 二維陣列的存取: ``` public class App { public static void main(String[] args) { int[][] arr = {{1, 2, 3}, {4, 5, 6}}; for(int i = 0; i < arr.length; i++) { for(int j = 0; j < arr[0].length; j++) System.out.print(arr[i][j] + " "); System.out.println(); } } } ``` 執行結果: ``` 1 2 3 4 5 6 ``` 陣列值 `arr[i][j]` 表示指定的是第 i 列第 j 行的值。在使用二維陣列物件時,注意 length 所代表的長度,陣列名稱後直接加上 length(如 `arr.length`),所指的是有幾列(Row);指定索引後加上 length(如 `arr[0].length`),指的是該列所擁有的元素,也就是行(Column)數目。 **範例:** 透過將陣列元素指定給變數來存取二維陣列 ``` public class App { public static void main(String[] args) { int[][] arr = {{1, 2, 3}, {4, 5, 6}}; int[] foo = arr[0]; // 將arr[0] 所參考的陣列物件指定給foo for(int i = 0; i < foo.length; i++) { System.out.print(foo[i] + " "); } System.out.println(); foo = arr[1]; // 將arr[1] 所參考的陣列物件指定給foo for(int i = 0; i < foo.length; i++) { System.out.print(foo[i] + " "); } System.out.println(); } } ``` 如果在使用 new 配置二維陣列後想要一併指定初值,則可以如下撰寫: ``` int[][] arr = new int[][] {{1, 2, 3}, {4, 5, 6}}; ``` 陣列的長度並不一定要全部長度都相同,一個二維陣列中,可以每個元素的長度都是不同的陣列,列如: ``` public class App { public static void main(String[] args) { int arr[][]; arr = new int[2][]; arr[0] = new int[3]; // arr[0] 參考至長度為3的一維陣列 arr[1] = new int[5]; // arr[1] 參考至長度為5的一維陣列 for(int i = 0; i < arr.length; i++) { for(int j = 0; j < arr[i].length; j++) arr[i][j] = j + 1; } for(int i = 0; i < arr.length; i++) { for(int j = 0; j < arr[i].length; j++) System.out.print(arr[i][j] + " "); System.out.println(); } } } ``` 這個例子中,陣列第一列的長度是3,而第二列的長度是 5,執行結果如下: ``` 1 2 3 1 2 3 4 5 ``` ### 三維陣列 宣告三維以上的陣列,如果要同時宣告初始元素值,可以使用以下的簡便語法: ``` int[][][] arr = { {{1, 2, 3}, {4, 5, 6}}, {{7, 8, 9}, {10, 11, 12}} }; ``` 如果要動態宣告三維陣列,就使用以下的語法: ``` int[][][] arr = new int[2][2][3]; ``` > **提示** > > 三維以上的更多維陣列之宣告,在Java中雖然可行,但並不建議使用,使用多維陣列會讓元素索引的指定更加困難,此時適當的將資料加以分割,或是使用其它的資料結構來解決,會比直接宣告多維陣列來得實在。 ### 練習 1. 將下列學生成績管理系統改為可以計算全部平均分數。 ``` import java.io.BufferedReader; import java.io.InputStreamReader; public class App { public static void main(String[] argc) throws Exception { BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); System.out.println("==============="); System.out.println("學生成績管理系統"); System.out.println("==============="); System.out.print("請問需要幾筆學生資料: "); // 使用者輸入陣列大小 int totalStudentNumber = Integer.parseInt(br.readLine()); int[] a = new int[totalStudentNumber]; System.out.println("陣列大小=" + a.length); for(int i = 0 ; i < a.length ; i++) { System.out.print("請輸入第" + (i + 1) + "筆資料: "); a[i] = Integer.parseInt(br.readLine()); } for(int score : a) System.out.print(score + ", "); } } ``` ## 進階陣列觀念 ### 進階的陣列操作 藉由對陣列物件的進一步探討,您可以稍微瞭解 Java 對物件處理的一些作法,首先來看看一維陣列的參考名稱之宣告: ``` int[] arr = null; ``` 在這個宣告中,arr 表示一個可以參考至 int 一維陣列物件的參考名稱,但是目前您將這個名稱參考至 null,表示這個名稱參考還沒有參考至實際的物件,在 Java 中,'=' 運算用於基本資料型態時,是將值複製給變數,但當用於物件時,則是將物件指定給參考名稱來參考,您也可以將同一個物件指定給兩個參考名稱,當物件的值藉由其中一個參考名稱進行操作而變更時,另一個參考名稱所參考到的值也會更動,例如: ``` public class App { public static void main(String[] args) { int[] arr1 = {1, 2, 3, 4, 5}; int[] tmp1 = arr1; int[] tmp2 = arr1; System.out.print("tmp1陣列值:"); for(int i = 0; i < tmp1.length; i++) System.out.print(tmp1[i] + " "); System.out.print("\ntmp2陣列值:"); for(int i = 0; i < tmp2.length; i++) System.out.print(tmp2[i] + " "); tmp1[2] = 9; System.out.print("\n\n修改後tmp1陣列值:"); for(int i = 0; i < tmp1.length; i++) System.out.print(tmp1[i] + " "); System.out.print("\n修改後tmp2取出陣列值:"); for(int i = 0; i < tmp2.length; i++) System.out.print(tmp2[i] + " "); System.out.println(); } } ``` 執行結果: ``` tmp1陣列值:1 2 3 4 5 tmp2陣列值:1 2 3 4 5 修改後tmp1陣列值:1 2 9 4 5 修改後tmp2陣列值:1 2 9 4 5 ``` 在這個範例中,藉由 tmp1 名稱改變了索引 2 的元素值,由於 tmp2 也參考至同一陣列物件,所以 tmp2 索引 2 元素值也被跟著改變;有三個參考名稱參考至同一個陣列物件,也就是 arr1、tmp1 與 tmp2。 所以,如果取出 arr1 索引 2 的元素,元素值也會是 9。 在宣告 int[] arr 之後,arr 是一個一維陣列物件的參考名稱,所以它可以參考至任何長度的一維陣列物件,例如: ``` public class App { public static void main(String[] args) { int[] arr1 = {1, 2, 3, 4, 5}; int[] arr2 = {5, 6, 7}; int[] tmp = arr1; System.out.print("使用tmp取出arr1中的元素:"); for(int i = 0; i < tmp.length; i++) System.out.print(tmp[i] + " "); tmp = arr2; System.out.print("\n使用tmp取出arr2中的元素:"); for(int i = 0; i < tmp.length; i++) System.out.print(tmp[i] + " "); System.out.println(); } } ``` tmp 可以參考至擁有5個元素的一維陣列,也可以參考至擁有 3 個元素的一維陣列,執行結果如下: ``` 使用tmp取出arr1中的元素:1 2 3 4 5 使用tmp取出arr2中的元素:5 6 7 ``` 在 Java 中陣列是一個物件,使用 '=' 指定時,是將物件==「指定給陣列名稱來參考,而不是將陣列進行複製」==,如果想將整個陣列的值複製給另一個陣列的話,可以使用迴圈,將整個陣列的元素值走訪一遍,並指定給另一個陣列相對應的索引位置,例如: ``` public class App { public static void main(String[] args) { int[] arr1 = {1, 2, 3, 4, 5}; int[] arr2 = new int[5]; for(int i = 0; i < arr1.length; i++) arr2[i] = arr1[i]; for(int i = 0; i < arr2.length; i++) System.out.print(arr2[i] + " "); System.out.println(); } } ``` 執行結果: ``` 1 2 3 4 5 ``` 另一個進行陣列複製的方法是使用 System 類別所提供的 arraycopy() 方法,其語法如下: ``` System.arraycopy(來源, 起始索引, 目的, 起始索引, 複製長度); ``` 例如: ``` public class App { public static void main(String[] args) { int[] arr1 = {1, 2, 3, 4, 5}; int[] arr2 = new int[5]; System.arraycopy(arr1, 0, arr2, 0, arr1.length); for(int i = 0; i < arr2.length; i++) System.out.print(arr2[i] + " "); System.out.println(); } } ``` > **補充** > > 或是使用 Arrays 類別的copyOf() 方法 ### Arrays 類別 對陣列的一些基本操作,像是排序、搜尋與比較等動作是很常見的,在 Java 中提供了 Arrays 類別可以協助您作這幾個動作,Arrays 類別位於 java.util 套件中,它提供了幾個方法可以直接呼叫使用。 ##### Arrays 類別常用方法 | 名稱 | 說明 | | -------------- | ------------------------------------------------------------ | | copyOf() | 複製陣列內容 | | sort() | 幫助您對指定的陣列排序,所使用的是快速排序法 | | binarySearch() | 讓您對已排序的陣列進行二元搜尋,如果找到指定的值就傳回該值所在的索引,否則就傳回負值 | | fill() | 當您配置一個陣列之後,會依資料型態來給定預設值,例如整數陣列就初始為 0,您可以使用Arrays.fill()方法來將所有的元素設定為指定的值 | | equals() | 比較兩個陣列中的元素值是否全部相等,如果是將傳回true,否則傳回 false | 使用 Arrays 來進行陣列的排序與搜尋: ``` import java.io.*; import java.util.Arrays; public class App { public static void main(String[] args) throws Exception { BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); int[] arr = {63, 7, 13, 54, 87, 47, 2 ,73, 41, 91}; int arrCopy = Arrays.copy(arr, arr.lenght); // 複製陣列 System.out.print("排序前: "); for(int i = 0; i < arr.length; i++) System.out.print(arr[i] + " "); System.out.println(); Arrays.sort(arr); System.out.print("排序後: "); for(int i = 0; i < arr.length; i++) System.out.print(arr[i] + " "); System.out.print("\n請輸入搜尋值: "); int key = Integer.parseInt(br.readLine()); int find = -1; if((find = Arrays.binarySearch(arr, key)) > -1) { System.out.println("找到值於索引 " + find + " 位置"); } else System.out.println("找不到指定值"); } } ``` 執行結果: ``` 排序前: 63 7 13 54 87 47 2 73 41 91 排序後: 2 7 13 41 47 54 63 73 87 91 請輸入搜尋值: 91 找到值於索引 9 位置 ``` 使用 Arrays 來進行陣列的填充與比較: ``` import java.util.Arrays; public class App { public static void main(String[] args) { int[] arr1 = new int[10]; int[] arr2 = new int[10]; int[] arr3 = new int[10]; Arrays.fill(arr1, 5); Arrays.fill(arr2, 5); Arrays.fill(arr3, 10); System.out.print("arr1: "); for(int i = 0; i < arr1.length; i++) System.out.print(arr1[i] + " "); System.out.println("\narr1 = arr2 ? " + Arrays.equals(arr1, arr2)); System.out.println("arr1 = arr3 ? " + Arrays.equals(arr1, arr3)); } } ``` 執行結果: ``` arr1: 5 5 5 5 5 5 5 5 5 5 arr1 = arr2 ? true arr1 = arr3 ? false ``` 請注意到,不可以用 `==` 來比較兩個陣列的元素值是否相等,`==` 使用於物件比對時,是用來測試兩個物件名稱是否參考至同一個物件,也就是測試兩個名稱是不是綁定至同一個物件,例如: ``` public class App { public static void main(String[] args) { int[] arr1 = {1, 2, 3, 4, 5}; int[] arr2 = {1, 2, 3, 4, 5}; int[] tmp = arr1; System.out.println(arr1 == tmp); System.out.println(arr2 == tmp); } } ``` 雖然 arr1 與 arr2 中的元素值是相同的,但實際上 arr1 與 arr2 是參考至不同的兩個陣列物件,將 arr1 指定給 tmp 來參考,由於 tmp 與 arr1 是參考同一陣列物件,所以進行 `arr1==tmp` 比較時會顯示 true,而 tmp 與 arr2 是參考至不同陣列物件,所以進行 `arr2==tmp` 比較時會顯示 false,執行結果如下: ``` true false ``` ##### Arrays 類別對二維以上陣列的支援 | 名稱 | 說明 | | -------------- | ------------------------------------------------------------ | | deepEquals() | 對陣列作深層比較,簡單的說,您可以對二維仍至三維以上的陣列進行比較是否相等 | | deepToString() | 將陣列值作深層輸出,簡單的說,您可以對二維仍至三維以上的陣列輸出其字串值 | 對三個二維陣列進行深層比較與深層輸出: ``` import java.util.Arrays; public class App { public static void main(String args[]) { int[][] arr1 = {{1, 2, 3}, {4, 5, 6}, {7, 8, 9}}; int[][] arr2 = {{1, 2, 3}, {4, 5, 6}, {7, 8, 9}}; int[][] arr3 = {{0, 1, 3}, {4, 6, 4}, {7, 8, 9}}; System.out.println("arr1 內容等於 arr2 ? " + Arrays.deepEquals(arr1, arr2)); System.out.println("arr1 內容等於 arr3 ? " + Arrays.deepEquals(arr1, arr3)); System.out.println("arr1 deepToString()\n\t" + Arrays.deepToString(arr1)); } } ``` 執行結果: ``` arr1 內容等於 arr2 ? true arr1 內容等於 arr3 ? false arr1 deepToString() [[1, 2, 3], [4, 5, 6], [7, 8, 9]] ``` ### foreach 與陣列 foreach也是迴圈的一種,又稱加強的 for 迴圈(Enhanced for Loop),其應用的對象之一是在陣列的循序存取上,語法如下: ``` for(資料型態 元素 : 陣列) {   System.out.println(元素)   ... } ``` 原本使用以下的方式來循序存取陣列中的元素: ``` int[] arr = {1, 2, 3, 4, 5}; for(int i = 0; i < arr.length; i++) System.out.println(arr[i]); ``` 改使foreach語法: ``` int[] arr = {1, 2, 3, 4, 5}; for(int element : arr) System.out.println(element); ``` 每一次從 arr 中取出的元素,會自動設定給 element,不需判斷是否超出了陣列的長度,注意 element 的型態必須與陣列元素的元素的型態相同。 如果是物件的話,作法也是類似,例如存取字串陣列的話,可以如下: ``` String[] names = {"Aaron Ho", "momor", "bush"}; for(String name : names) System.out.println(name); ``` 二維陣列也是相同作法: ``` int[][] arr = {{1, 2, 3}, {4, 5, 6}, {7, 8, 9}}; for(int[] row : arr) { for(int element : row) { System.out.println(element); } } ``` 三維以上的陣列使用 foreach 的方式來存取也可以依此類推。 # 字串 ### String 由字元所組成的一串文字符號被稱之為「字串」,例如 "Hello" 這個字串就是由 'H'、'e'、'l'、'l'、'o' 這五個字元所組成,在某些程式語言中,字串是以字元陣列的方式存在。 在 Java 中字串不僅僅是字元陣列,而是 String 類別的一個實體,在Java因為String使用的頻率很高,所以可以不需要使用標準物件的建立方式,而是可以像基本型態一樣來宣告一個字串,例如: ``` String text = "字串的使用"; System.out.println(text); ``` 注意字串的直接指定必須使用分號 "" 來包圍著文字,字串的每個字元是使用 Unicode 字元來建構,在建構一個字串物件之後,可以直接在標準輸出串流(out)中指定字串物件的參考名稱來輸出字串。 字串的串接在 Java 中可以直接使用 '+' 運算子,'+' 本來是加法運算子,但使用在字串上會被重新定義(Override)為字串的串接,例如: ``` String msg = "哈囉!"; msg = msg + "Aaron Ho!"; System.out.println(msg); ``` 這一段程式碼會在文字模式上顯示 "哈囉!Aaron Ho!"。 字串在 Java 中以 String 類別的一個實例存在,所以每個字串物件本身會擁有幾個可操作的方法。 **String 物件常用方法** | 方法 | 說明 | | ------------- | -------------------------------------------- | | length() | 取得字串的字元長度 | | equals() | 判斷原字串中的字元是否相等於指定字串中的字元 | | toLowerCase() | 將字串中的英文字元轉為小寫 | | toUpperCase() | 將字串中的英文字元轉為大寫 | 示範了以上的幾個字串操作方法的使用: ``` public class App { public static void main(String[] args) { String text = "hello"; System.out.println("字串內容: " + text); System.out.println("字串長度: " + text.length()); System.out.println("等於hello? " + text.equals("hello")); System.out.println("轉為大寫: " + text.toUpperCase()); System.out.println("轉為小寫: " + text.toLowerCase()); } } ``` 執行結果: ``` 字串內容: hello 字串長度: 5 等於hello? true 轉為大寫: HELLO 轉為小寫: hello ``` 如果要將輸入的字串轉換為整數、浮點數等資料型態,可以使用下表各類別所提供的各個靜態(static)解析方法: | 方法 | 說明 | | ------------------------ | ------------------------ | | Byte.parseByte(字串) | 將字串剖析為位元 | | Short.parseShort(字串) | 將字串剖析為short整數 | | Integer.parseInt(字串) | 將字串剖析為int整數 | | Long.parseLong(字串) | 將字串剖析為long整數 | | Float.parseFloat(字串) | 將字串剖析為float浮點數 | | Double.parseDouble(字串) | 將字串剖析為double浮點數 | 注意如果指定的字串無法剖析為指定的資料型態數值,則會發生 `NumberFormatException` 例外。 宣告字串時,都是以這樣的樣子來宣告: ``` String str = "Aaron Ho"; ``` 這樣的宣告方式看來像是基本資料型態宣告,但事實上 String 並不是 Java 的基本資料型態,String 是 java.lang 套件下所提供的類別,如果以正常配置物件的觀念來宣告字串,應該為: ``` String str = new String("Aaron Ho"); ``` **取得字串中的字元之方法** | 方法 | 說明 | | ---------------------------------------------- | ---------------------------------- | | char charAt(int index) | 傳回指定索引處的字元 | | int indexOf(int ch) | 傳回指定字元第一個找到的索引位置 | | int indexOf(String str) | 傳回指定字串第一個找到的索引位置 | | int lastIndexOf(int ch) | 傳回指定字元最後一個找到的索引位置 | | String substring(int beginIndex) | 取出指定索引處至字串尾端的子字串 | | String substring(int beginIndex, int endIndex) | 取出指定索引範圍子字串 | | char[] toCharArray() | 將字串轉換為字元陣列 | 範例: ``` public class App { public static void main(String[] args) { String text = "Today is not my day."; System.out.println("字串內容: "); for(int i = 0; i < text.length(); i++) System.out.print(text.charAt(i)); System.out.println("\n第一個day: " + text.indexOf("day")); System.out.println("最後一個day: " + text.lastIndexOf("day")); char[] charArr = text.toCharArray(); // 字串轉字元陣列 System.out.println("\n字元Array內容: "); for(int i = 0; i < charArr.length; i++) System.out.print(charArr[i]); } } ``` 執行結果: ``` Today is not my day. 第一個day: 2 最後一個day: 16 字元Array內容: Today is not my day. ``` 在初始化字串物件時,除了直接在 '=' 後使用 "" 來指定字串常數之外,也可以使用字元陣列來初始化,例如使用字元陣列 name,建構出一個內容為 "aaron" 的字串: ``` char[] name = {'a', 'a', 'r', 'o', 'n'}; String str = new String(name); ``` 使用 endsWith() 方法可以判斷字串是不是以指定的文字作為結束,這個常用在過濾檔案名稱,例如: ``` public class App { public static void main(String[] args) { String[] filenames = {"Aaron Ho.jpg", "cater.gif", "bush.jpg", "wuwu.jpg", "clockman.gif"}; System.out.print("過濾出jpg檔案: "); for(int i = 0; i < filenames.length; i++) { if(filenames[i].endsWith("jpg")) { System.out.print(filenames[i] + " "); } } System.out.println(""); } } ``` 執行結果: ``` 過濾出jpg檔案: Aaron Ho.jpg bush.jpg wuwu.jpg ``` > **補充** > > 在宣告字串一併指定字串值時,如果字串長度過長,可以分作兩行來寫,這樣比較容易閱讀,例如: > ``` > String text = "這是第一行.\n" > + "這是第二行.\n"; > ``` ### 不可變(immutable)字串 使用字串有一個非常重要的觀念必須知道,一個字串物件一旦被配置,它的內容就是固定不可變的(immutable),例如下面這個宣告: ``` String str = "Aaron Ho"; ``` 這個宣告會配置一個長度為 8、內容為 "Aaron Ho" 的字串物件,誰都無法改變這個字串物件的內容;下面的程式並不是改變一個字串物件的內容: ``` String str = "Hello"; str = "Java"; ``` 事實上在這個程式片段中會有兩個字串物件,一個是 "Hello" 字串物件,長度為 5,一個是 "Java" 字串物件,長度為 4,兩個是不同的字串物件。 在 Java 中,使用 '=' 將一個字串物件指定給一個參考名稱,其意義為==「改變該名稱所參考的物件」==,原來被參考的字串物件若沒有其它名稱來參考它,就會在適當的時機被 Java 的「垃圾回收」 (Garbage collection)機制回收,在 Java 中,程式設計人員通常不用關心無用物件的資源釋放問題,Java 會檢查物件是否不再被參考,如果沒有被任何名稱參考的物件將會被回收。 在 Java 執行時會維護一個String池(Pool),對於一些可以共享的字串物件,會先在 String 池中查找是否存在相同的 String 內容(字元相同),如果有就直接傳回,而不是直接創造一個新的 String 物件,以減少記憶體的耗用,在程式中使用下面的方式來宣告,則實際上是指向同一個字串物件: ``` String str1 = "Aaron Ho"; String str2 = "Aaron Ho"; System.out.println(str1 == str2); ``` 當直接在程式中使用 "" 來包括一個字串時,該字串就會在 String 池中。 如果 `==` 被使用於兩個參考名稱時,它是用於比較兩個參考名稱是否參考至同一物件,所以str1==str2 比較時,程式的執行結果會顯示 true。 `==` 在 Java 中被用來比較兩個參考名稱是否參考至同一物件,所以==「不可以用 `==` 來比較兩個字串的字元內容是否相同」==,例如: ``` String str1 = new String("aaron"); String str2 = new String("aaron"); System.out.println(str1 == str2); ``` 雖然兩個字串物件的字元值完全相同,但實際上在這個程式片段中,因為產生了兩個 String 的實例,str1 與 str2 分別參考至不同的實例,所以使用 '==' 比較時結果會顯示 false,如果要比較兩個字串物件的字元值是否相同,需要使用 equals() 方法,例如: ``` String str1 = new String("aaron"); String str2 = new String("aaron"); System.out.println(str1.equals(str2)); ``` 一個常見的問題是:上面的程式片段產生了幾個 String 的實例?很多人會回答 2 個,但答案是 3 個,因為 "aaron" 就是一個,它存在於字串池中,而您又使用 new 建構出兩個 String 物件,分別由 str1 與 str2 參考,所以總共會有 3 個 String 實例。 ### StringBuilder 類別 一個 String 物件的長度是固定的,您不能改變它的內容,或者是附加新的字元至 String 物件中,您也許會使用 '+' 來串接字串以達到附加新字元或字串的目的,但 '+' 會產生一個新的 String 實例,如果程式對這種附加字串的需求很頻繁,並不建議使用 '+' 來進行字串的串接,在物件導向程式設計中,最好是能重複運用已生成的物件,物件的生成需要記憶體空間與時間,不斷的產生 String 實例是一件沒有效率的行為。 使用 StringBuilder 會讓程式的效率大大提昇,例如: ``` public class App { public static void main(String[] args) { String text = ""; long beginTime = System.currentTimeMillis(); for(int i = 0; i < 10000; i++) text = text + i; long endTime = System.currentTimeMillis(); System.out.println("使用字串相加執行時間:" + (endTime - beginTime)); StringBuilder builder = new StringBuilder(""); beginTime = System.currentTimeMillis(); for(int i = 0; i < 10000; i++) builder.append(i); endTime = System.currentTimeMillis(); System.out.println("使用StringBuilder執行時間:" + (endTime - beginTime)); } } ``` 首先使用 '+' 來串接字串,再使用 System.currentTimeMillis() 取得 for 迴圈執行前、後的系統時間,如此就可以得知 for 迴圈執行了多久,例如: ``` 使用字串相加執行時間:209 使用StringBuilder執行時間:2 ``` 使用 StringBuilder 最後若要輸出字串結果,可以呼叫toString() 方法;可以使用 length() 方法得知目前物件中的字元長度,而 capacity() 可傳回該物件目前可容納的字元容量,另外 StringBuilder 還有像是 insert() 方法可以將字元插入指定的位置,如果該位置以後有字元,則將所有的字元往後移;deleteChar() 方法可以刪除指定位置的字元,而 reverse() 方法可以反轉字串。 > **補充** > > 還有另一個StringBuffer與StringBuilder 被設計具有相同的操作介面,在單機非「多執行緒」(Multithread)的情況下使用 StringBuilder 會有較好的效率,因為 StringBuilder 沒有處理「同步」(Synchronized)問題;StringBuffer 則會處理同步問題,如果StringBuilder 會在多執行緒下被操作,則要改用 StringBuffer,讓物件自行管理同步問題。 ## 字串進階運用 ### 命令列引數 在文字模式下執行程式時,通常可以連帶指定一些引數給程式。 在文字模式下啟動一個 Java 程式時,也可以一併指定引數,以讓程式進行相對應的功能,也就是輸入命令列引數(Command line argument)給程式,在您撰寫主程式時,會在 main() 的參數列撰寫 String[] args,目的就是用來接受一個字串陣列,只要取得 args 中的元素值,就可以取出 Java 程式運行時被指定的引數,例如: ``` public class App { public static void main(String[] args) { System.out.print("讀入的引數: "); for(int i = 0; i < args.length; i++) System.out.print(args[i] + " "); System.out.println(); } } ``` args 索引 0 的值是從程式名稱後第一個引數開始,以空白為區隔,依序地儲存在 args 陣列中,執行的方式與結果如下頁: ``` java App help me 讀入的引數: help me ``` 也可使用foreach來取得每個引數: ``` public class App { public static void main(String[] args) { System.out.print("讀入的引數: "); for(String arg : args) System.out.print(arg + " "); System.out.println(); } } ``` ### 分割字串 將字串依所設定的條件予以分離是很常見的操作,例如指令的分離、文字檔案的資料讀出等,以後者而言,在文字檔案中儲存以下的資料時,在讀入檔案後,將可以使用 String 的 split() 來協助每一個資料分離。 例如: ``` public class App { public static void main(String args[]) { String[] fakeFileData = { "aaron\t77/5/26\t0968123456\t5433343", "apple\t78/7/23\t0968654321\t5432343" }; for(String data : fakeFileData) { String[] tokens = data.split("\t"); for(String token : tokens) { System.out.print(token + "\t| "); } System.out.println(); } } } ``` 執行結果: ``` aaron | 77/5/26 | 0968123456 | 5433343 | apple | 78/7/23 | 0968654321 | 5432343 | ``` split() 依您所設定的分隔設定,將字串分為數個子字串並以 String 陣列傳回。 ``` import java.util.Arrays; public class App { public static void main(String[] argc) throws Exception { String sample = "Today is a rainy day, and Aaron have to work."; String[] picNames = {"aaron.jpg", "andy.gif", "apple.png", "abner.jpg", "amanda.png"}; sample = sample.toLowerCase(); // 全部英文轉小寫 sample = sample.toUpperCase(); // 全部英文轉大寫 char[] sampleChar = sample.toCharArray(); // 轉成字元陣列 System.out.println(Arrays.toString(sampleChar)); int indexOfDay = sample.indexOf("DAY"); System.out.println("day的位置在索引: " + indexOfDay); int lastIndexOfDay = sample.lastIndexOf("DAY"); System.out.println("day的位置最後出現在索引: " + lastIndexOfDay); sample = sample.replace(".", "").replace(",", ""); // 去掉逗點跟句點 String[] splitStr = sample.split(" "); // 字串切割 System.out.println("字串切割: " + Arrays.toString(splitStr)); sample = sample.substring(6, 8); System.out.println(sample); System.out.println(Arrays.toString(picNames)); for(String filename : picNames) if(filename.endsWith(".png")) System.out.println("檔名:" + filename); } } ``` # 物件容器(Container) 在程式運行的過程中,很多時候您需要將物件暫時儲存在一個容器中統一管理,之後需要時再將物件取出,要使用什麼樣的容器依設計需求而定: 1. 使用循序有索引的串列(List)結構來儲存物件。 2. 使用不允許重複相同物件的集合(Set)結構。 3. 使用「鍵-值」(Key-Value)存取的Map。 ------------------ ### List介面 java.util.ArrayList 類別實作了 java.util.List 介面,所以要先認識一下 List 介面,List 介面是 java.util.Collection 介面的子介面,而 Collection 介面則是 java.lang.Iterable 的子介面,Iterable 介面要求實作一個 iterator() 方法。 package java.lang; import java.util.Iterator; public interface Iterable<T> { Iterator<T> iterator(); } 從 J2SE 5.0 開始增加了泛型設計的新功能,所以像 Iterable、Collection 相關介面與其實作類別,都使用泛型的功能重新改寫了。 Iterable 介面要求實作它的類別傳回一個實作 java.util.Iterator 介面的物件,事實上您在 Java SE 的 API 中找不到任何實作 Iterator 的類別,因為 Iterator 會根據實際的容器資料結構來迭代元素,而容器的資料結構實作方式對外界是隱藏的,使用者不用知道這個結構,只需要知道 Iterator 的操作方法,就可以取出元素,Iterator 介面的定義如下: package java.util; public interface Iterator<E> { boolean hasNext(); E next(); void remove(); } Collection 介面繼承了 Iterator 介面,定義了加入元素、移除元素、元素長度等方法, package java.util; public interface Collection<E> extends Iterable<E> { int size(); boolean isEmpty(); boolean contains(Object o); Iterator<E> iterator(); <T> T[] toArray(T[] a); boolean add(E o); boolean remove(Object o); boolean containsAll(Collection<?> c); boolean addAll(Collection<? extends E> c); boolean removeAll(Collection<?> c); boolean retainAll(Collection<?> c); void clear(); boolean equals(Object o); int hashCode(); } Collection 在移除元素及取得元素上的定義是比較通用,List 介面則又增加了根據索引取得物件的方法,這說明了 List 資料結構的特性,每個加入 List 中的元素是循序加入的,並可指定索引來存取元素(以下原始碼只是節錄部份)。 package java.util; public interface List<E> extends Collection<E> { .... boolean addAll(int index, Collection<? extends E> c); E get(int index); E set(int index, E element); void add(int index, E element); E remove(int index); int indexOf(Object o); int lastIndexOf(Object o); List<E> subList(int fromIndex, int toIndex); .... } List 資料結構的特性是,每個加入 List 中的元素是循序加入的,並可指定索引來存取元素,List 可以使用陣列(Array)或是鏈結串列(Linked List)來實作這個特性,前者在 Java SE 中的實作就是 java.util.ArrayList,後者就是 java.util.LinkedList,對於循序加入與存取,使用 ArrayList 的效率比較好,對於經常變動元素排列順序的需求,使用 LinkedList 會比較好。 ### ArrayList ArrayList 實作了 List 介面,ArrayList 使用陣列結構實作 List 資料結構,陣列的特性是可以使用索引來快速指定物件的位置,所以對於快速的隨機取得物件來說,使用 ArrayList 可以得到較好的效能,但由於使用陣列實作,若要從中間作移除或插入物件的動作,會需要搬動後段的陣列元素以重新調整索引順序,所以速度上就會慢的多。 先來看看一個使用 ArrayList 的例子,例如: ```java import java.io.*; import java.util.ArrayList; import java.util.List; public class App { public static void main(String[] args) throws Exception { BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); List<String> list = new ArrayList<String>(); System.out.println("輸入名稱(使用quit結束)"); while(true) { System.out.print("# "); String input = br.readLine(); if(input.equals("quit")) break; list.add(input); } System.out.print("顯示輸入: "); for(int i = 0; i < list.size(); i++) System.out.print(list.get(i) + " "); System.out.println(); } } ``` Java的泛型(Generic)的功能,使用物件容器時可以宣告將儲存的物件型態,物件在存入容器會被限定為宣告的型態,編譯器在編譯時期會協助進行型態檢查,而取出物件時也不至於失去原來的型態資訊,這可以避免型態轉換時的問題。 使用 add() 方法可以將一個物件加入 ArrayList中,使用 size() 方法可以傳回目前的 ArrayList 的長度,使用 get() 可以傳回指定索引處的物件,使用 toArray() 可以將 ArrayList 中的物件轉換為物件陣列,執行結果如下: 輸入名稱(使用quit結束) # Aaron # Apple # Andy # quit 顯示輸入: Aaron Apple Andy 可以使用 get() 方法指定索引值取出物件,如果要循序取出容器中所有的物件,則可以使用 Iterator: ```java import java.io.*; import java.util.*; public class App { public static void main(String[] args) throws Exception { BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); List<String> list = new ArrayList<String>(); System.out.println("輸入名稱(輸入quit結束)"); while(true) { System.out.print("# "); String input = br.readLine(); if(input.equals("quit")) break; list.add(input); } // 使用 Iterator 取得元素 Iterator iterator = list.iterator(); while(iterator.hasNext()) { // 還有下一個元素嗎? // 使用 next() 取得下一個元素 System.out.print(iterator.next() + " "); } System.out.println(); } } ``` iterator() 方法會傳回一個 Iterator 物件,這個物件提供遍訪容器元素的方法,hasNext() 方法測試 Iterator 中是否還有物件,如果有的話,可以使用 next() 方法取出。 另外,也可以使用foreach來取得每個元素: ```java import java.io.*; import java.util.*; public class App { public static void main(String[] args) throws Exception { BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); List<String> list = new ArrayList<String>(); System.out.println("輸入名稱(輸入quit結束)"); while(true) { System.out.print("# "); String input = br.readLine(); if(input.equals("quit")) break; list.add(input); } // 使用foreach來遍訪List中的元素 for(String s : list) { System.out.print(s + " "); } System.out.println(); } } ``` ``` import java.util.ArrayList; public class App { public static void main(String[] argc) throws Exception { ArrayList<String> listStr = new ArrayList<>(); ArrayList<Integer> listInt = new ArrayList<>(); listStr.add("Aaron"); listInt.add(99); listInt.add(88); listInt.add(77); // 取出ArrayList內所有資料 for(int i = 0 ; i < listInt.size() ; i++) System.out.print(listInt.get(i) + " "); for(int element : listInt) System.out.print(element + " "); listStr.add(0, "Apple"); // 在索引0的地方插入資料 System.out.println("listStr第1筆資料: " + listStr.get(0)); System.out.println("listInt第1筆資料: " + listInt.get(0)); listStr.remove(0); // 移除資料 listInt.remove(0); listStr.removeAll(listStr); // 清掉全部資料 System.out.println("listStr長度:" + listStr.size()); System.out.println("listInt長度:" + listInt.size()); } } ``` ``` import java.io.*; import java.util.ArrayList; import java.util.List; public class App { public static void main(String[] argc) throws Exception { BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); System.out.println("<<< 請輸入數字,quit結束輸入 >>>"); List<Integer> data = new ArrayList<>(); while(true) { System.out.print("請輸入: "); String input = br.readLine(); if(input.equals("quit")) break; else { int inputInt = Integer.parseInt(input); data.add(inputInt); // 存起來 } } // 算總和 int sum = 0; for(int e : data) { sum += e; } System.out.println("平均為: " + sum / data.size()); } } ``` ### HashSet java.util.HashSet 實作了 java.util.Set 介面,Set 介面一樣繼承了 Collection 介面,List 容器中的物件允許重複,但==Set 容器中的物件都是唯一的==,Set 容器有自己的一套排序規則,所以無法確保元素的順序跟存入的順序一樣。 HashSet 的排序規則是利用湊雜(Hash),所以加入 HashSet 容器的物件還必須重新定義 hashCode() 方法,HashSet 根據湊雜碼來確定物件於容器中儲存的位置,也可以根據雜湊碼來快速的找到容器中的物件。 在比較兩個加入 HashSet 容器中的物件是否相同時,會先比較 hashCode() 方法傳回的值是否相同,如果相同,則再使用 equals() 方法比較,如果兩者都相同,則視為相同的物件。 > **提示** > > 在定義類別時,最好總是重新定義 equals() 與 hashCode() 方法,以符合 Java 的設計規範。 HashSet範例: ```java import java.util.*; public class App { public static void main(String[] args) { Set<String> set = new HashSet<String>(); set.add("Aaron Ho"); set.add("momor"); set.add("bush"); // 故意加入重複的物件 set.add("Aaron Ho"); // 使用 Iterator 顯示物件 Iterator iterator = set.iterator(); while(iterator.hasNext()) { System.out.print(iterator.next() + " "); } System.out.println(); set.remove("bush"); // 也可使用 enhanced for loop 顯示物件 for(String name : set) { System.out.print(name + " "); } System.out.println(); } } ``` 可以看到,即使重複加入了 "Aaron Ho" 字串,HashSet 中仍只有一個 "Aaron Ho" 字串物件,這是 Set 的特性,另一個要注意的是,迭代 HashSet 中所有的值時,其順序與加入容器的順序可能不相同,迭代所有值時的順序是 HashSet 排序過後的順序,執行結果如下: bush momor Aaron Ho momor Aaron Ho ## Map 實作 java.util.Map 介面的物件會將「鍵」(Key)映射至「值」(Value),「值」指的是要存入 Map 容器的物件。在將物件存入 Map 物件時,需要同時給定一個「鍵」,要取回物件時可以指定鍵,如此就可以取得與鍵對應的物件「值」,Map 中的每一個鍵都是唯一的,不能有重複的鍵,Map也擁有自己的排序機制。 ### HashMap Map 的特性即「鍵-值」(Key-Value)匹配,簡單的說,可以將實作 Map 介面的容器物件當作一個有很多房間的房子,每個房間的門有唯一的鑰匙(Key),將物件儲存至房間中時,要順便擁有一把鑰匙,下次要取回物件時,就是根據這把鑰匙取回物件。 java.util.HashMap 實作了 Map 介面,HashMap 在內部實作使用雜湊(Hash),用來在很快的時間內可以找到「鍵-值」(Key-Value)匹配的資料,例如: ```java import java.util.*; public class App { public static void main(String[] args) { Map<String, String> map = new HashMap<String, String>(); String key1 = "Aaron Ho"; String key2 = "Apple Chen"; map.put(key1, "Aaron Ho 的訊息"); map.put(key2, "Apple Chen 的訊息"); System.out.println(map.get(key1)); System.out.println(map.get(key2)); } } ``` 在範例中宣告 Map 型態與新增 Map 實例時,指定了「鍵-值」所要使用的型態,在範例中都是宣告為 String 型態,也就是要以 String 物件作為「鍵」,而存入的「值」也要是 String 物件,使用 Map 的 put() 方法將物件存入時,必須同時指定「鍵」和「值」,而要取回物件時,則使用 get() 方法並指定「鍵」,傳回的會是對應於鍵的「值」,程式的執行結果如下: Aaron Ho 的訊息 Apple Chen 的訊息 可以使用 values() 方法返回一個實作 Collection 的物件,當中包括所有的「值」物件,如果需要一次迭代 Map 中所有的物件,這會很有用,例如: ```java import java.util.*; public class App { public static void main(String[] args) { Map<String, String> map = new HashMap<String, String>(); map.put("Apple Chen", "Apple Chen 的訊息"); map.put("momor", "momor 的訊息"); map.put("Aaron Ho", "Aaron Ho 的訊息"); Collection collection = map.values(); Iterator iterator = collection.iterator(); while(iterator.hasNext()) { System.out.println(iterator.next()); } System.out.println(); // 事實上也可以使用foreach迴圈 for(String value : map.values()) { System.out.println(value); } } } ``` 結果如下: momor 的訊息 Apple Chen 的訊息 Aaron Ho 的訊息 momor 的訊息 Apple Chen 的訊息 Aaron Ho 的訊息 如果想要在迭代所有的物件時,依照插入的順序來排列,則可以使用 java.util.LinkedHashMap,它是 HashMap 的子類別,在使用 values() 所返回的 Collection 物件,其內含物件之順序即為當初加入物件之順序,例如: ```java import java.util.*; public class App { public static void main(String[] args) { Map<String, String> map = new LinkedHashMap<String, String>(); map.put("Apple Chen", "Apple Chen 的訊息"); map.put("momor", "momor 的訊息"); map.put("Aaron Ho", "Aaron Ho 的訊息"); for(String value : map.values()) { System.out.println(value); } } } ``` 執行結果: ``` Apple Chen 的訊息 momor 的訊息 Aaron Ho 的訊息 ```