1. 程式人生 > >Java 基礎回顧:幾個比較重要的預定義類

Java 基礎回顧:幾個比較重要的預定義類

這篇文章中梳理了 Java 中幾個常見的預定義類:字串型別 String 類、Object 類、列舉型別 Enum 類以及陣列。

1、字串 String 類

1.1 字串相關問題總結

String 的每一個看起來會修改 String 的方法實際都是建立一個全新的 String 物件,以包含修改後的字串的內容,而最初的 String 物件則絲毫未動;

String 使用 char value[] 儲存字元,所以 String 物件在建立之後就不能修改物件儲存的字串內容,正因如此才說 String 型別是不可變的;

String 使用正則表示式的方式,應該注意一下下面的這些方法引數是正則表示式而不是普通的字串:

1. 匹配驗證操作:`"asd".matches("[0-9]")`
2. 分割操作:`"0as0d".split("[0-9]")`
3. 替換操作:`"0a0sd".replaceAll("[0-9]", "09")`

除了呼叫 String 物件的方法,還可以使用 PatternMatcher 兩個類來使用正則表示式:

private static Pattern imagePattern;

public static Uri getPreviewImage(String noteContent) {
    if (imagePattern == null) {
        imagePattern = Pattern.compile(Constants.REGEX_NOTE_PREVIEW_IMAGE);
    }
    Matcher matcher = imagePattern.matcher(noteContent);
    if (matcher.find()) {
        String str = matcher.group();
        if (!TextUtils.isEmpty(str)) {
            // do something...
        }
    }
    return null;
}

String 類有一個特殊的建立方法,就是使用 "" 建立。比如 new String("str") 實際上建立了兩個物件:一個是通過 "" 建立的,另一個是使用 new 建立。只是建立的時期不同:一個是在編譯器,一個是在執行期。

執行期間呼叫 String 的 intern 方法可以向 String Pool 中動態新增物件。如

String s1 = "strs";
String s2 = s1.intern();

這個時候,如果我們使用`s1 == s2`進行判斷的話會得到什麼結果呢?    
答案是 true. 參考intern方法的註釋:如果 Pool 中存在一個與當前 String 相等(所謂的相等是指使用 equals 方法判斷時相等)的物件的時候,就返回 Pool 中的那個物件。否則,就將當前的 String 新增到 Pool 中。

注意字串拼接操作和 == 操作的優先順序:

String s = "str";
System.out.println("s == s " + s == s);
System.out.println("s == s " + (s == s));

輸出的結果是:
    false
    s == s true

這是因為不管 + 號是用作加法還是用作連線字串,它的優先順序都要比 == 號要高。

使用字串拼接操作符 + 來拼接字串,不適合運用在大規模的場景中,這是因為當兩個字串拼接到一起時,它們的內容都要被拷貝。

char 型別是採用 UTF-16 編碼的 Unicode 程式碼點,大多數 Unicode 字元用一個程式碼單元,輔助字元需要兩個程式碼單元,使用 charAt 的時候獲取的是指定位置的程式碼單元,所以當字串中存在需要兩個程式碼單元的字元時,就容易出現錯誤。一般可以使用下面的形式獲取每個程式碼點

int cp = sentence.codePointAt(n);
If(Character.isSupplementaryCodePoint(cp)) i += 2;
else i++;

1.2 其他

  1. String 型別為 0 或多個雙位元組 Unicode 字元組成的序列,預設為 null,這與 ”” 不同。所以,要判斷一個字串是否為空,可以使用下面的形式 if (str != null && str.length != 0),即說明字串不是 null 也不是 ””
  2. 字串不是字元陣列,不能按照陣列的方式訪問;
  3. 比較字串是否相等要用 equals 方法(C++可以用 ==,C語言使用 strcmp);
  4. 要由許多小段的字串構建字串,使用 StringBuilder 效率更高;
  5. 如果要修改字串指定位置的字元,使用 subString() 擷取然後再使用 + 或者 StringBuilder 進行拼接即可;
  6. 檔案路徑中的反斜號前要增加一個額外的反斜號,如 ”c:\\myDir\\myFile.txt”
  7. Java 中字串的長度是不可變的,這樣設計是為了使字串共享;
  8. 使用 String.format() 靜態方法可以建立一個格式化的字串,而不輸出,比如:String msg = Sting.format(“Age is %d”,age);

2、Object 類

所有型別都隱式的派生於 java.lang.Object 類,其主要用於兩個目的:

  1. 使用 Object 引用繫結任何資料型別的物件;
  2. Object 型別執行許多基本的一般用途的方法,包括 equals(), finalize(), hashCode(), getClass(), toString(), notify(), notifyAll()wait() 等.

2.1 finilize() 方法

  1. 它是不可預測的,也是很危險的,一般情況下是不必要的;
  2. 它的出現只是對 C++ 中的解構函式的一個妥協;
  3. 如果想要在類結束的時候釋放佔用的資源,可以使用 try-catch 結構來完成。

2.2 equals() 方法

equals() 方法需要遵循的規範:

  1. 自反性:當 x!=null, 有 x.equals(x) 為 true;
  2. 對稱性:當 x!=null 且 y!=null, 有 x.equals(y) 與 y.equals(x) 結果相同;
  3. 傳遞性:當 x!=null, y!=null, z!=null, x.equals(y) 且 y.equals(z), 那麼 x.equals(z);
  4. 一致性:當 x!=null,如果 x 和 y 沒有修改過,那麼 x.equals(y) 結果不變;
  5. 對任何 x!=null,有 x.equals(nyll) 為 false.

覆寫的訣竅:

  1. 使用 == 操作檢查 “引數是否為這個物件的引用”;
  2. 使用 instanceOf 檢查 “引數是否為正確的型別”;
  3. 把引數轉換成正確的型別;
  4. 對於該類中的每個“關鍵”域,檢查引數中的域是否與該物件中的域相匹配;
    1. 對於非 floatdouble 的基本型別域,使用 == 判斷兩個值是否相等;
    2. 對於引用型別的域,可以使用 equals 方法判斷兩者是否相等;
    3. 對於 float 域,可以使用 Float.compare 方法;
    4. 對於 double 域,可以使用 Double.compare 方法;
    5. 對於陣列域,使用上述原則到每個元素,如果陣列每個元素都很重要,可用 Arrays.equals 方法;
  5. 為了獲得最佳效能,應該先比較最可能不一致的域,或者開銷最低的域;
  6. 編寫完 equals 方法之後,檢查它們是否是:對稱的、傳遞的、一致的;
  7. 覆蓋 equals 方法時總要覆寫 hashCode 方法;
  8. 不要將 equals 方法中的 Object 替換成其他型別(那就不是覆寫了)。

下面是一個示例程式,其中也包含了 hashCode 方法

private static class Person {
    private long number;
    private int age;
    private String name;
    private float wage;
    private int[] id;

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof Person)) return false;

        Person person = (Person) o;

        if (number != person.number) return false;
        if (age != person.age) return false;
        if (Float.compare(person.wage, wage) != 0) return false;
        if (name != null ? !name.equals(person.name) : person.name != null) return false;
        return Arrays.equals(id, person.id);
    }

    @Override
    public int hashCode() {
        int result = (int) (number ^ (number >>> 32));
        result = 31 * result + age;
        result = 31 * result + (name != null ? name.hashCode() : 0);
        result = 31 * result + (wage != +0.0f ? Float.floatToIntBits(wage) : 0);
        result = 31 * result + Arrays.hashCode(id);
        return result;
    }
}

2.3 hashCode() 方法

hashCode() 方法需要遵循的規範:

  1. 只要用於 equals() 方法比較的資訊沒有修改,hashCode() 多次呼叫應返回同樣的值;
  2. 兩個物件的 equals() 方法返回結果相同,那麼它們的 hashCode() 方法的返回結果也應該相同;
  3. 兩個物件的 equals() 方法返回結果不同,它們的 hashCode() 不一定要不同,但是如果不同的話,可以提高散列表的效能。

計算 hashCode() 的簡單方法:

  1. 把某個非零的常數值,比如 17,儲存在名為 resultint 型變數中;
  2. 對物件中的每個關鍵域 f(用在 equals() 方法中的域),完成以下步驟:
    1. 計算該域 f 的雜湊碼 c
      1. fboolean 型,計算 f ? 1 : 0
      2. fbyte, char, shortint 型,計算 (int) f
      3. flong 型,計算 (int) (f >>> 32)
      4. ffloat 型,計算 Float.floatToIntBits(f)
      5. fdouble 型,計算 Double.doubleToLongBits(f),然後按 long 型處理;
      6. f 為物件引用,呼叫物件的 hashCode() 方法,如果物件為 null 返回 0 ;
      7. f 為陣列,對陣列每個元素當作單獨的域處理,如果陣列所有元素都有意義,則用Arrays.hashCode() 方法;
    2. 按照下面公式將 c 合併到 resultresult = 31 * result + c
  3. 返回 result

示例程式碼可以參考 equals 方法,它是由 IDEA 自動生成的。

2.4 clone() 方法:物件克隆

當我們使用 = 將一個引用型別賦值給另一個引用型別的時候,會使得兩個引用型別共享一份資源的拷貝。
這時候修改一個引用會使的兩個引用的內容都發生變化,而使用 clone() 就可以解決這個問題。Clone 之後的兩個引用型別各自有自己的一份內容,不會相互影響。

但是,有一點值得注意的是,下面的程式碼中的 CloneableClass 中不存在引用型別,如果其中仍然存在引用型別的話,我們需要在 clone() 方法中也將該引用型別 clone 一份。

public static void main(String[] args) throws CloneNotSupportedException {
    // 測試1
    CloneableClass test1 = new CloneableClass("Test Class-01");
    CloneableClass cloned = test1.clone();
    // 修改了克隆物件的欄位會反映到被克隆物件上
    cloned.inner.name = "clone2.inner.name"; 
    System.out.println(test1.inner.name);
    // 測試2
    AnotherCloneableClass test2 = new AnotherCloneableClass("Test Class-02");
    AnotherCloneableClass anotherCloned = test2.clone();
    anotherCloned.inner.name = "anotherClone2.inner.name";
    System.out.println(test2.inner.name);
}

private static class InnerClass implements Cloneable{
    public String name;

    public InnerClass clone() throws CloneNotSupportedException{
        return (InnerClass)super.clone();
    }
}

private static class CloneableClass implements Cloneable{
    private String name;
    public InnerClass inner;

    public CloneableClass(String name){
        this.name = name;
        inner = new InnerClass();
    }

    public CloneableClass clone() throws CloneNotSupportedException{
        return (CloneableClass)super.clone();
    }
}

private static class AnotherCloneableClass extends CloneableClass{
    public AnotherCloneableClass(String name) {
        super(name);
    }

    public AnotherCloneableClass clone() throws CloneNotSupportedException{
        AnotherCloneableClass cloned = (AnotherCloneableClass) super.clone();
        cloned.inner = inner.clone();
        return cloned;
    }
}

輸出結果:

clone2.inner.name
null

3、列舉類 Enum

3.1 列舉的示例

3.1.1 基本使用示例

下面是列舉的一個使用示例:

public enum ProductType {
    NORMAL(0, "普通品"),
    SPECIAL(1, "特殊品"); // 這裡的分號是必不可少的

    public final String name;
    public final int no;

    ProductType(int id, String name) {
	    this.id = id;
        this.name = name;
    }

    public static ProductType getPortraitById(String name) {
        for (ProductType type : values()){
            if (type.name.equals(name)){
                return type;
            }
        }
        throw new IllegalArgumentException("illegal argument");
    }
}

上面是一個列舉的示例,注意這裡面的一些細節:

  1. 首先,定義列舉的時候要用 enum 關鍵字,這只是替代了定義類的時候的 class 關鍵字;
  2. 實際上列舉是隱式繼承 Enum.class 的,所以,列舉本身也是一個類,並且上面用到的 values() 方法就來自於 Enum
  3. 列舉通常用來表示那些不會進行修改的物件,所以我們可以根據需要向列舉中新增一些欄位;
  4. 列舉的欄位可以是 public 的,因為它同時也是 final 的,所以,不用擔心暴露得太多而無法控制的問題;
  5. 因為所有的列舉型別都預設繼承了 Enum 類,所以自定義列舉型別就不能再繼承其他類了;
  6. 列舉有一個 ordinal 欄位,它表示的是指定的列舉值在所有列舉值中的位置(從 0 開始)。不過,我們通常傾向於自己實現自己的 id 來給列舉值標序,因為這樣更有利於維護。

3.1.2 使用介面組織列舉

雖然,列舉沒有辦法繼承新的類,但是卻可以實現介面。這裡我們在介面內部定義一組列舉,用來表示同一個大類中的一些分組,然後每個列舉內部再定義一些具體的列舉值:

public interface City {
    enum ChineseCity implements City {
        BEIJING, SHANGHAI, GUANGZHOU;
    }

    enum AmericanCity implements City {
        NEW_YORK, HAWAII, IOWA, WASHINGTON;
    }

    enum EnglishCity implements City {
        BRISTOL, CAMBRIDGE, CHESTER, LIVERPOOL;
    }
}

這裡我們定義的是一個城市的介面,藉口內部定義了三個列舉,分別枚舉了中國、美國和英國的城市。可以看出在這裡我們使用介面定義了“城市”的抽象概念,然後在介面的內部定義了三個列舉,來對應三個不同的國家。這樣就相當於在 City 到具體的列舉之間又增加了一個新的層次。定義完畢之後,我們可以這麼使用:

public static void main(String ...args) {
    City city = City.AmericanCity.IOWA;
    System.out.println(city);
    System.out.println(City.ChineseCity.BEIJING);
    System.out.println(City.AmericanCity.NEW_YORK);
    System.out.println(City.EnglishCity.LIVERPOOL);
}

3.1.3 使用列舉增強列舉的擴充套件性

如下面的程式碼所示,我們定義了一個介面型別 Operation 來表示一些操作,其內部定義了一個執行的方法(需要注意的是,我們在使用 enum 定義列舉的時候,實現介面的方法的操作是在各個列舉值上面實現的)。我們可以先定義一些基本的列舉型別,如果我們要在原來的基礎之上進行拓展的話,那麼我們只需要實現 Operation 並新增新的列舉即可:

public interface Operation {
    double apply(int num1, int num2);
}

public enum BasicOperation implements Operation {
    ADD() {
        public double apply(int num1, int num2) {
            return num1 + num2;
        }
    },
    MINUS() {
        public double apply(int num1, int num2) {
            return num1 - num2;
        }
    },
    TIMES() {
        public double apply(int num1, int num2) {
            return num1 / num2;
        }
    },
    DIVIDE() {
        public double apply(int num1, int num2) {
            return num1 * num2;
        }
    };
}

public enum ExtendedOperation implements Operation {
    EXP() {
        public double apply(int num1, int num2) {
            return Math.exp(num1);
        }
    };
}

對以上定義的方法的一個呼叫:

Operation operation = BasicOperation.ADD;
System.out.println(operation.apply(7, 8));

使用上面的兩行程式碼,我們可以輕易地得出結果為 15. 這是沒有問題的,而且我們可以看出這裡藉助於列舉實現了策略模式。

3.2 EnumSet 和 EnumMap

這是兩個適用於列舉型別的容器型別,略。

4、陣列

4.1 一維陣列

4.1.1 一維陣列的宣告

型別[] 陣列名; 或 型別 陣列名[];

宣告和建立分別進行

型別[] 陣列名;
陣列名 = new 型別[元素個數];

宣告和建立同時進行

型別[] 陣列名 = new 型別[元素個數];

4.1.2 一維陣列的例項化

陣列名 = new 型別[]{ 元素0,元素1,......,元素n };
型別[] 陣列名 = new 型別[]{ 元素0,元素1,......,元素n };
型別[] 陣列名 = { 元素0,元素1,......,元素n };

注意:

  1. 如果通過 {} 初始化陣列元素,則用 new 關鍵字建立陣列不需要也不能指定陣列的元素個數,編譯器會自動推斷元素個數. 即 int []arr = new int[5]{1,2,3,4,5}; 是錯誤的.

  2. 就是如果指定了陣列的內容就不能指定陣列的大小。指定陣列大小隻能在僅僅定義陣列的時候使用。

  3. 建立一個數組而沒有給其元素賦值時,數字陣列所有元素初始化為 0boolean 陣列的所有元素初始化為 false,物件陣列的所有元素初始化為 null

  4. 可以將 型別[] 當作一個整體,不要往 [] 中新增數字;

  5. 常見的定義陣列的錯誤形式:

     int[4][2] arr = new int[][];
     int[][] arr; arr = new int[4][2]{........};
     int[][] arr = new int[4][2]{........};
    

4.1.3 一維陣列的訪問

陣列名[下標];

可以通過陣列的 length 屬性獲得陣列的長度,即

陣列名.length;

4.2 二維陣列

4.2.1 二維陣列宣告

型別[][] 陣列名; 或 型別 陣列名[][];

宣告和建立分別進行:

型別[][] 陣列名;
陣列名 = new 型別[元素個數1][元素個數2];

宣告和建立同時進行:

型別[][] 陣列名 = new 型別[元素個數1][元素個數2];

4.2.2 二維陣列的例項化

陣列名 = new 型別[] { 元素0, 元素1, ......, 元素n };
型別[][] 陣列名 = new 型別[][] { 元素0, 元素1, ......, 元素n };
型別[][] 陣列名 = { 元素0, 元素1, ......, 元素n };

4.2.3 二維陣列訪問

陣列名[下標1][下標2];

譬如 arr[2][3]; 可以理解為包含兩個陣列的陣列. 故:

陣列名.length         // 返回 陣列名 的元素個數
陣列名[下標].length   // 返回 陣列[下標] 的元素個數

4.3 不規則陣列

4.3.1 宣告並初始化

int[][] jaggedArray = { {1,3,5,7,9}, {0,2,4,6,8}, {11,22} };
int[][] jaggedArray = { new int[]{1,3,5,7,9}, 
    new int[]{0,2,4,6,8}, new int[]{11,22} };
int[][] jaggedArray = new int[][]{ new int[]{1,3,5,7,9}, new int[]{0,2,4,6,8}, new int[]{11,22} };
int[][] jaggedArray = new int[3][ ];
jaggedArray[0] = new int[]{1,3,5,7,9}; jaggedArray[1] = new int[]{0,2,4,6,8};; jaggedArray[2] = new int[]{11,22};

4.4 陣列的工具類

4.4.1 Java.util.Arrays

Java.util.Arrays 提供了一些提供了一系列的方法,可以用來對陣列進行操作:

  1. sort():對陣列進行排序;
  2. binarySearch():使用二分法查詢指定的鍵值(注意查詢之前需要先排序);
  3. copyOf()copyOfRange():複製陣列,擷取或使用預設值填充;
  4. toString():返回指定陣列內容的字串表示形式;
  5. fill(): 使用指定的值型別填充陣列;
  6. hashCode():產生陣列的雜湊碼;
  7. asList():將指定陣列轉換成 List 型別,注意 asList() 方法返回的 ArrayListArrays 的私有靜態內部類,它不允許我們向返回的容器中加入或者移除元素。

4.4.2 System.arraycopy()

System.arraycopy() 提供了拷貝陣列的高效方法,它的定義是:System.arraycopy(src, scrPos, dest, destPos, length)。注意,該方法不會自動包裝和自動拆包,所以 Integer[]int[] 是不能相互複製的。

使用示例:

int[] arr = new int[]{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
int[] dest = new int[5];
System.arraycopy(arr, 4, dest, 2, 3);
System.out.println(Arrays.toString(dest));

輸出結果是:

[0, 0, 4, 5, 6]

可以看出它的效果是:將 arr 中從索引 4 開始的連續 3 個元素拷貝到 dest 中從 2 開始向後的所有元素中。並且這裡要求複製到 dest 中的元素個數必須小於 dest 定義的個數。

4.4.3 陣列克隆

clone() 方法不能直接用於多維陣列,要用的話也要在每一維上使用 clone() 方法:

int[] arr = {1,2,3,4,5}; int[] arr2 = arr.clone();

常用的複製陣列方法總結:

  1. 使用迴圈;
  2. 使用陣列變數的 clone() 方法,簡單但是不靈活;
  3. 使用 System.arraycopy() 方法. 簡單靈活;
  4. Java,util.Arrays.copyOf / copyOfRange() 方法. 簡單靈活。

4.4.4 陣列與泛型

不能例項化具有引數化型別的陣列,如下面的兩種例項化方式中第一種是合法的,而第二種是不合法的:

Bag<Integer>[] bags = new Bag[5]; // 合法
// Bag<Integer>[] bags = new Bag<Integer>[5]; // 不合法
for (int i=0;i<5;i++) {
    bags[i] = new Bag();
    bags[i].value = i;
}

不能建立泛型陣列,但是可以建立 Object[] 陣列,然後將其強制轉換成泛型陣列:

private static class Bag<T> {
    // T[] ts = new T[5]; // 非法
    T[] ts = (T[]) new Object[5]; // 合法,但是會在編譯期得到“不受檢查”的警告資訊
}