# 資料結構
###### 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 的訊息
```