資料結構

tags: java

目錄

陣列

陣列(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 的例子,例如:

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:

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來取得每個元素:

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範例:

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)匹配的資料,例如:

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 中所有的物件,這會很有用,例如:

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 物件,其內含物件之順序即為當初加入物件之順序,例如:

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 的訊息