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 物件的方法,還可以使用 Pattern
和 Matcher
兩個類來使用正則表示式:
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 其他
- String 型別為 0 或多個雙位元組 Unicode 字元組成的序列,預設為 null,這與
””
不同。所以,要判斷一個字串是否為空,可以使用下面的形式if (str != null && str.length != 0)
,即說明字串不是null
也不是””
; - 字串不是字元陣列,不能按照陣列的方式訪問;
- 比較字串是否相等要用
equals
方法(C++可以用 ==,C語言使用 strcmp); - 要由許多小段的字串構建字串,使用
StringBuilder
效率更高; - 如果要修改字串指定位置的字元,使用
subString()
擷取然後再使用+
或者StringBuilder
進行拼接即可; - 檔案路徑中的反斜號前要增加一個額外的反斜號,如
”c:\\myDir\\myFile.txt”
; - Java 中字串的長度是不可變的,這樣設計是為了使字串共享;
- 使用
String.format()
靜態方法可以建立一個格式化的字串,而不輸出,比如:String msg = Sting.format(“Age is %d”,age);
。
2、Object 類
所有型別都隱式的派生於 java.lang.Object
類,其主要用於兩個目的:
- 使用
Object
引用繫結任何資料型別的物件; Object
型別執行許多基本的一般用途的方法,包括equals()
,finalize()
,hashCode()
,getClass()
,toString()
,notify()
,notifyAll()
和wait()
等.
2.1 finilize() 方法
- 它是不可預測的,也是很危險的,一般情況下是不必要的;
- 它的出現只是對 C++ 中的解構函式的一個妥協;
- 如果想要在類結束的時候釋放佔用的資源,可以使用 try-catch 結構來完成。
2.2 equals() 方法
equals() 方法需要遵循的規範:
自反性
:當 x!=null, 有 x.equals(x) 為 true;對稱性
:當 x!=null 且 y!=null, 有 x.equals(y) 與 y.equals(x) 結果相同;傳遞性
:當 x!=null, y!=null, z!=null, x.equals(y) 且 y.equals(z), 那麼 x.equals(z);一致性
:當 x!=null,如果 x 和 y 沒有修改過,那麼 x.equals(y) 結果不變;- 對任何 x!=null,有 x.equals(nyll) 為 false.
覆寫的訣竅:
- 使用
==
操作檢查 “引數是否為這個物件的引用”; - 使用
instanceOf
檢查 “引數是否為正確的型別”; - 把引數轉換成正確的型別;
- 對於該類中的每個“關鍵”域,檢查引數中的域是否與該物件中的域相匹配;
- 對於非
float
和double
的基本型別域,使用==
判斷兩個值是否相等; - 對於引用型別的域,可以使用
equals
方法判斷兩者是否相等; - 對於
float
域,可以使用Float.compare
方法; - 對於
double
域,可以使用Double.compare
方法; - 對於陣列域,使用上述原則到每個元素,如果陣列每個元素都很重要,可用
Arrays.equals
方法;
- 對於非
- 為了獲得最佳效能,應該先比較最可能不一致的域,或者開銷最低的域;
- 編寫完
equals
方法之後,檢查它們是否是:對稱的、傳遞的、一致的; - 覆蓋
equals
方法時總要覆寫hashCode
方法; - 不要將
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()
方法需要遵循的規範:
- 只要用於
equals()
方法比較的資訊沒有修改,hashCode()
多次呼叫應返回同樣的值; - 兩個物件的
equals()
方法返回結果相同,那麼它們的hashCode()
方法的返回結果也應該相同; - 兩個物件的
equals()
方法返回結果不同,它們的hashCode()
不一定要不同,但是如果不同的話,可以提高散列表的效能。
計算 hashCode()
的簡單方法:
- 把某個非零的常數值,比如
17
,儲存在名為result
的int
型變數中; - 對物件中的每個關鍵域
f
(用在equals()
方法中的域),完成以下步驟:- 計算該域
f
的雜湊碼c
;- 若
f
為boolean
型,計算f ? 1 : 0
; - 若
f
為byte
,char
,short
或int
型,計算(int) f
; - 若
f
為long
型,計算(int) (f >>> 32)
; - 若
f
為float
型,計算Float.floatToIntBits(f)
; - 若
f
為double
型,計算Double.doubleToLongBits(f)
,然後按 long 型處理; - 若
f
為物件引用,呼叫物件的hashCode()
方法,如果物件為null
返回 0 ; - 若
f
為陣列,對陣列每個元素當作單獨的域處理,如果陣列所有元素都有意義,則用Arrays.hashCode()
方法;
- 若
- 按照下面公式將
c
合併到result
,result = 31 * result + c
;
- 計算該域
- 返回
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");
}
}
上面是一個列舉的示例,注意這裡面的一些細節:
- 首先,定義列舉的時候要用
enum
關鍵字,這只是替代了定義類的時候的class
關鍵字; - 實際上列舉是隱式繼承
Enum.class
的,所以,列舉本身也是一個類,並且上面用到的values()
方法就來自於Enum
; - 列舉通常用來表示那些不會進行修改的物件,所以我們可以根據需要向列舉中新增一些欄位;
- 列舉的欄位可以是
public
的,因為它同時也是final
的,所以,不用擔心暴露得太多而無法控制的問題; - 因為所有的列舉型別都預設繼承了
Enum
類,所以自定義列舉型別就不能再繼承其他類了; - 列舉有一個
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 };
注意:
-
如果通過
{}
初始化陣列元素,則用 new 關鍵字建立陣列不需要也不能指定陣列的元素個數,編譯器會自動推斷元素個數. 即int []arr = new int[5]{1,2,3,4,5};
是錯誤的. -
就是如果指定了陣列的內容就不能指定陣列的大小。指定陣列大小隻能在僅僅定義陣列的時候使用。
-
建立一個數組而沒有給其元素賦值時,數字陣列所有元素初始化為
0
,boolean
陣列的所有元素初始化為false
,物件陣列的所有元素初始化為null
; -
可以將
型別[]
當作一個整體,不要往[]
中新增數字; -
常見的定義陣列的錯誤形式:
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
提供了一些提供了一系列的方法,可以用來對陣列進行操作:
sort()
:對陣列進行排序;binarySearch()
:使用二分法查詢指定的鍵值(注意查詢之前需要先排序);copyOf()
和copyOfRange()
:複製陣列,擷取或使用預設值填充;toString()
:返回指定陣列內容的字串表示形式;fill()
: 使用指定的值型別填充陣列;hashCode()
:產生陣列的雜湊碼;asList()
:將指定陣列轉換成List
型別,注意asList()
方法返回的ArrayList
是Arrays
的私有靜態內部類,它不允許我們向返回的容器中加入或者移除元素。
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();
常用的複製陣列方法總結:
- 使用迴圈;
- 使用陣列變數的
clone()
方法,簡單但是不靈活; - 使用
System.arraycopy()
方法. 簡單靈活; 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]; // 合法,但是會在編譯期得到“不受檢查”的警告資訊
}