異常 (Exception)。程序世界并非總是完美的,異常處理機制就是Java為我們提供的優雅應對錯誤的解決方案。
一、為什么需要異常處理?—— 從現實世界說起
想象一下現實生活中的場景:
- 開車上班:你計劃開車去公司(正常流程)。
- 但可能遇到異常情況:爆胎、沒油、交通事故。
- 你不會因此就停在路中間,而是有應對措施:換備胎、呼叫救援、聯系保險和交警。
- 網上購物:你提交訂單(正常流程)。
- 但可能遇到異常情況:庫存不足、網絡斷開、支付失敗。
- 網站不會直接崩潰,而是會給你友好的提示:“庫存不足”、“網絡異常,請重試”。
程序中的問題:
在程序中,同樣充滿了各種意外:
- 用戶輸入了錯誤格式的數據。
- 要打開的文件不存在。
- 網絡連接突然中斷。
- 數據庫服務宕機。
- 算術運算除以0。
沒有異常處理的傳統做法(C語言風格):
使用方法的返回值來表示錯誤狀態(如返回 -1
、null
等)。這會導致:
- 代碼臃腫:正常的業務邏輯和錯誤處理代碼混雜在一起,可讀性差。
- 容易遺漏:程序員可能忘記檢查返回值。
- 傳遞麻煩:錯誤信息需要一層層手動傳遞回調用者。
Java的解決方案:異常機制
Java使用 “拋出(Throw)-捕獲(Catch)” 模型。當錯誤發生時,方法會立即拋出(throw) 一個代表該錯誤的對象(異常對象),然后由專門的代碼塊來捕獲(catch) 并處理它。這使得正常邏輯和錯誤處理邏輯分離,代碼更加清晰和健壯。
二、Java異常體系結構
Java將所有的異常和錯誤封裝成了類,形成了一個清晰的繼承體系。Throwable
類是所有錯誤和異常的頂級父類。
text
Throwable/ \/ \Error Exception/ \/ \RuntimeException IOException, SQLException, ...(unchecked) (checked)
1. Error (錯誤)
- 描述:指程序無法處理的嚴重問題,通常是JVM內部的錯誤或系統資源耗盡。
- 特點:應用程序不應該試圖捕獲和處理這類錯誤。
- 舉例:
OutOfMemoryError
:內存溢出。StackOverflowError
:棧溢出。VirtualMachineError
:虛擬機錯誤。
2. Exception (異常)
指程序本身可以捕獲和處理的問題。它分為兩大類:
- Checked Exception (受檢異常)
- 描述:除了
RuntimeException
及其子類以外的所有Exception
子類。 - 特點:編譯器會檢查它。如果一個方法可能拋出受檢異常,必須在方法簽名上用
throws
聲明,或者方法內部用try-catch
捕獲處理。否則,代碼無法通過編譯。 - 舉例:
IOException
:IO操作異常。SQLException
:數據庫操作異常。ClassNotFoundException
:類找不到異常。FileNotFoundException
:文件找不到異常。
- 描述:除了
- Unchecked Exception (非受檢異常 / 運行時異常)
- 描述:
RuntimeException
類及其所有子類。 - 特點:編譯器不強制要求處理。程序可以選擇捕獲處理,也可以不處理。這些異常通常是由程序邏輯錯誤引起的,應該在代碼開發階段盡量避免。
- 舉例:
NullPointerException
:空指針異常。ArrayIndexOutOfBoundsException
:數組下標越界異常。ArithmeticException
:算術異常(如除以0)。ClassCastException
:類型轉換異常。IllegalArgumentException
:非法參數異常。
- 描述:
簡單記憶:
- Error:搞不定,別處理。
- Exception:能搞定,要處理。
- Checked:不處理編譯就報錯。
- Unchecked:不處理運行才報錯。
三、異常處理的關鍵字與機制
Java通過五個關鍵字來處理異常:try
, catch
, finally
, throw
, throws
。
1. 捕獲異常:try-catch-finally
這是處理異常的核心結構,用于捕獲和處理方法內部可能發生的異常。
try
塊:包裹可能會發生異常的代碼。后面必須跟一個或多個catch
塊或一個finally
塊。catch
塊:捕獲并處理特定類型的異常。可以有多個catch
塊,用于處理不同類型的異常。finally
塊:無論是否發生異常,都會執行的代碼。通常用于釋放資源(如關閉文件、數據庫連接等)。
基本語法:
java
try {// 可能會發生異常的代碼FileInputStream fis = new FileInputStream("nonexistent.txt");
} catch (FileNotFoundException e) {// 捕獲并處理FileNotFoundException異常System.out.println("文件找不到: " + e.getMessage());e.printStackTrace(); // 打印異常的堆棧跟蹤信息(非常有用 for debugging)
} catch (IOException e) { // 可以捕獲多個更具體的異常,父類異常要寫在后面System.out.println("發生IO異常");
} catch (Exception e) {// 捕獲所有其他異常(兜底)System.out.println("發生未知異常");
} finally {// 無論是否發生異常,都會執行的代碼System.out.println("finally塊始終執行");// 這里可以寫關閉資源的代碼,即使try塊中發生異常,資源也能被釋放
}
catch
的匹配順序:
- 從上到下進行匹配,一旦匹配成功,后面的
catch
塊就不會再執行。 - 必須將更具體(子類)的異常放在前面,更通用(父類)的異常放在后面。否則,子類的
catch
塊將永遠無法被執行,導致編譯錯誤。
2. 拋出異常:throw
與 throws
-
throw
:用在方法內部,主動拋出一個異常對象(new
一個異常對象)。java
public void setAge(int age) {if (age < 0 || age > 120) {// 主動拋出一個運行時異常throw new IllegalArgumentException("年齡不合法: " + age);}this.age = age; }
-
throws
:用在方法聲明處,聲明該方法可能拋出的受檢異常。調用此方法的代碼必須處理這些異常(要么繼續throws
,要么try-catch
)。java
// 該方法聲明它可能拋出FileNotFoundException和IOException public void readFile() throws FileNotFoundException, IOException {FileInputStream fis = new FileInputStream("a.txt");// ... 讀寫操作fis.close(); }
四、異常處理的最佳實踐與流程
1. 異常處理流程
- 程序執行
try
塊中的代碼。 - 如果
try
塊中發生異常,異常對象被拋出。 - JVM中止當前
try
塊的執行,開始檢查各個catch
塊。 - 找到第一個能匹配該異常類型的
catch
塊并執行。 - 執行
finally
塊中的代碼(如果有)。 - 繼續執行
try-catch-finally
結構之后的代碼。
如果異常沒有被捕獲,它會沿著調用棧向上傳遞,如果一直傳遞到 main
方法仍未被處理,JVM會終止程序并打印異常的堆棧跟蹤信息。
2. 最佳實踐
-
具體明確:盡量捕獲具體的異常,而不是簡單地用
catch (Exception e)
一網打盡。 -
勿吞異常:不要在
catch
塊中什么都不做(如只寫一個空塊或只打印)。這會將錯誤隱藏,給調試帶來巨大困難。 -
善用 finally:將釋放資源的代碼(關閉文件、連接、流等)放在
finally
塊中,確保資源總能被釋放。(JDK 7 的 try-with-resources 語句更好,后續會學)。 -
早拋出,晚捕獲:在底層方法中,遇到無法處理的異常應盡早
throw
出去;在高層(如UI層或主邏輯層)統一進行catch
和處理(如給用戶友好提示)。 -
異常轉譯:有時捕獲一個異常后,可以拋出一個對當前層更有意義的業務異常。
java
catch (SQLException e) {throw new DaoException("數據庫操作失敗", e); // 將原始異常e作為cause傳入 }
-
文檔化:使用JavaDoc的
@throws
標簽為方法聲明它可能拋出的異常。
五、自定義異常
Java允許我們創建自己的異常類,通常用于表示特定的業務邏輯錯誤。
步驟:
- 繼承自
Exception
(受檢異常)或RuntimeException
(非受檢異常)。 - 通常提供兩個構造方法:一個無參構造,一個帶有詳細描述信息(String message)的構造方法。
示例:自定義一個“余額不足”的異常。
java
// 1. 繼承RuntimeException,定義為非受檢異常(業務上通常如此)
public class InsufficientBalanceException extends RuntimeException {// 2. 提供構造方法public InsufficientBalanceException() {super();}public InsufficientBalanceException(String message) {super(message); // 調用父類構造方法}// 也可以提供帶原因(cause)的構造方法public InsufficientBalanceException(String message, Throwable cause) {super(message, cause);}
}// 使用自定義異常
public void withdraw(double amount) {if (amount > balance) {throw new InsufficientBalanceException("當前余額" + balance + ",取款金額" + amount + "不足");}balance -= amount;
}
總結
Java的異常處理機制是編寫健壯、可靠應用程序的基石。
核心概念 | 說明 |
---|---|
體系結構 | Throwable -> Error / Exception -> Checked / Unchecked |
處理機制 | try-catch-finally 用于捕獲,throw /throws 用于拋出 |
關鍵區別 | Checked異常必須處理,否則編譯不通過;Unchecked異常不強制。 |
finally作用 | 無論是否異常,都執行,常用于釋放資源。 |
自定義異常 | 繼承 Exception 或 RuntimeException ,用于表示特定業務錯誤。 |
包裝類 (Wrapper Class)。它是連接基本數據類型和對象世界的橋梁。
我將作為您的助手,帶您理解為什么需要包裝類,以及如何在實際開發中熟練地使用它們。
一、為什么需要包裝類?—— 從“對象”說起
Java是一個面向對象的語言,其核心操作都是基于對象(Object
)的。然而,我們之前學習的八種基本數據類型(byte
, short
, int
, long
, float
, double
, char
, boolean
) 卻不是對象。
這就帶來了一個矛盾和一些實際需求:
-
某些場景只能使用對象:
- 泛型:Java的泛型(如
ArrayList<>
)要求類型參數必須是類類型,不能是基本數據類型。 - 集合框架:像
ArrayList
,HashMap
這些容器,它們只能存儲對象(Object
的子類)。
java
// 錯誤!無法編譯,泛型不能使用基本類型 // ArrayList<int> list = new ArrayList<int>();// 正確!必須使用包裝類 ArrayList<Integer> list = new ArrayList<Integer>(); list.add(10); // 這里其實發生了自動裝箱
- 泛型:Java的泛型(如
-
需要對象提供的功能:
- 基本數據類型只是一塊純粹的數據,沒有方法。
- 而包裝類是類,內部提供了很多有用的靜態方法和常量,可以方便地進行數據轉換、判斷、計算等。
java
int num = Integer.parseInt("123"); // 將字符串轉換為int int max = Integer.MAX_VALUE; // 獲取int的最大值
-
需要表示“空”(null)的概念:
- 基本數據類型的變量總是有值的(即使默認值也是0或false)。
- 而包裝類對象可以為
null
,可以用來表示“數據缺失”或“尚未賦值”的狀態,這在數據庫查詢、JSON解析等場景中非常常見。
java
Integer score = null; // 可以表示“成績未知” // int score = null; // 編譯錯誤!基本類型不能為null
Java的解決方案:
為每一種基本數據類型都設計了一個對應的“包裝類”,將這些基本類型“包裝”起來,使其具有對象的形態。
二、包裝類與基本類型的對應關系
八種基本數據類型都有其對應的包裝類,這些包裝類都在 java.lang
包下,因此無需手動導入。
基本數據類型 | 包裝類 |
---|---|
byte | Byte |
short | Short |
int | Integer |
long | Long |
float | Float |
double | Double |
char | Character |
boolean | Boolean |
注意:
Integer
和Character
的類名是全稱,不是Int
和Char
。- 除了
Integer
和Character
,其他包裝類的類名基本都是基本類型首字母大寫。
三、裝箱與拆箱 (Boxing & Unboxing)
“裝箱”和“拆箱”是操作包裝類的核心概念。
- 裝箱 (Boxing):將基本數據類型轉換為對應的包裝類對象。
- 拆箱 (Unboxing):將包裝類對象轉換為對應的基本數據類型。
在JDK 1.5之前,必須手動進行裝箱和拆箱,非常繁瑣。
1. 手動裝箱與拆箱 (JDK 1.5之前)
java
// 手動裝箱:基本類型 -> 包裝類對象
int num1 = 10;
Integer integerObj1 = Integer.valueOf(num1); // 方式一:推薦,可能用到緩存
// Integer integerObj2 = new Integer(num1); // 方式二:已過時(Deprecated),不推薦// 手動拆箱:包裝類對象 -> 基本類型
Integer integerObj2 = Integer.valueOf(20);
int num2 = integerObj2.intValue(); // 調用xxxValue()方法// 其他類型類似
double d = 3.14;
Double doubleObj = Double.valueOf(d);
double d2 = doubleObj.doubleValue();char c = 'A';
Character charObj = Character.valueOf(c);
char c2 = charObj.charValue();
2. 自動裝箱與拆箱 (Autoboxing & Unboxing, JDK 1.5+)
從JDK 1.5開始,Java引入了自動裝箱和自動拆箱的特性。編譯器在背后自動為我們添加上面的手動轉換代碼,極大地方便了開發。
- 自動裝箱:可以直接將基本數據類型賦值給包裝類引用。
- 自動拆箱:可以直接將包裝類對象賦值給基本類型變量,或者參與基本類型的運算。
java
// --- 自動裝箱 --- //
Integer a = 10; // 編譯器自動改為:Integer a = Integer.valueOf(10);
Double b = 3.14;
Character c = '嗨';
Boolean d = true;// --- 自動拆箱 --- //
int e = a; // 編譯器自動改為:int e = a.intValue();
double f = b;
char g = c;
boolean h = d;// 自動拆箱的常見場景:參與運算
Integer i = 100;
Integer j = 200;
int result = i + j; // 1. i和j先自動拆箱為int// 2. 然后進行 100 + 200 的加法運算// 3. 將結果300賦值給result// 在集合中使用(最常用的場景)
ArrayList<Integer> list = new ArrayList<>();
list.add(1); // 自動裝箱:int -> Integer
list.add(2);
int first = list.get(0); // 自動拆箱:Integer -> int
注意:雖然自動裝箱拆箱很方便,但背后依然有方法調用(如 valueOf()
, intValue()
)的開銷,在性能極度敏感的場景(如超大規模循環)中需要留意。
四、包裝類的常用操作與方法
包裝類提供了很多實用的靜態方法和成員方法。
1. 類型轉換
-
字符串 -> 基本類型 / 包裝類:這是最常用的功能!
java
// String -> int String str = "123"; int num = Integer.parseInt(str); // 核心方法:parseXxx(String s)// String -> double String str2 = "3.14"; double d = Double.parseDouble(str2);// String -> boolean // ("true" -> true, 其他任何字符串 -> false) String str3 = "true"; boolean b = Boolean.parseBoolean(str3);
-
基本類型 / 包裝類 -> 字符串:
java
int num = 456; // 方式一:最常用,字符串拼接(本質是String.valueOf()) String str1 = "" + num;// 方式二:String類的valueOf()方法 String str2 = String.valueOf(num);// 方式三:包裝類的toString()靜態方法 String str3 = Integer.toString(num);// 方式四:包裝類對象的toString()方法 Integer obj = 456; String str4 = obj.toString();
2. 常用常量與方法
java
// 常用常量
System.out.println(Integer.MAX_VALUE); // int最大值: 2147483647
System.out.println(Integer.MIN_VALUE); // int最小值: -2147483648
System.out.println(Double.NaN); // 表示"非數字"
System.out.println(Double.POSITIVE_INFINITY); // 正無窮大// 比較方法
Integer x = 100;
Integer y = 100;
// 比較包裝對象的值
System.out.println(x.equals(y)); // true (推薦:比較值)
// 比較對象的引用地址(有坑!)
System.out.println(x == y); // true? false? 下文詳解// 其他工具方法
System.out.println(Integer.bitCount(7)); // 計算二進制中1的個數 (7的二進制是111,輸出3)
System.out.println(Integer.toBinaryString(10)); // 轉二進制字符串: "1010"
System.out.println(Integer.toHexString(255)); // 轉十六進制字符串: "ff"
五、重要特性:包裝類的緩存機制
這是一個面試高頻考點和易錯點!
Java為了節省內存和提高性能,對部分包裝類對象實現了緩存。即在一定的數值范圍內,相同的值會返回同一個對象。
Integer
緩存:默認緩存了 -128 到 127 之間的整數。Byte
,Short
,Long
緩存:緩存范圍也是 -128 到 127。Character
緩存:緩存了 0 到 127 之間的字符(ASCII字符)。Boolean
緩存:直接緩存了TRUE
和FALSE
兩個對象。
緩存機制帶來的影響:
java
Integer a = 100;
Integer b = 100;
System.out.println(a == b); // true (因為100在緩存范圍內,a和b是同一個對象)Integer c = 200;
Integer d = 200;
System.out.println(c == d); // false (因為200不在緩存范圍內,c和d是new的兩個不同對象)// 正確的比較方式:始終使用.equals()方法來比較包裝類的值!
System.out.println(a.equals(b)); // true
System.out.println(c.equals(d)); // true
結論:比較兩個包裝類對象的值是否相等,一定要使用 .equals()
方法,永遠不要使用 ==
!
六、總結與最佳實踐
特性 | 說明 |
---|---|
存在意義 | 讓基本類型具備對象的特性,用于泛型、集合、表示null等場景。 |
自動裝箱 | Integer i = 10; (基本類型 -> 包裝類) |
自動拆箱 | int n = i; (包裝類 -> 基本類型) |
核心方法 | Integer.parseInt("123") , i.toString() |
比較 | 必須使用 i.equals(j) ,切勿使用 == 。 |
緩存機制 | Integer 等緩存了-128到127的對象,== 在此范圍內可能為true。 |
最佳實踐:
- 優先使用基本類型:在不需要對象的場景下(如局部變量、循環計數器),使用基本類型性能更好。
- 必要時刻使用包裝類:在泛型、集合、需要表示
null
值時,必須使用包裝類。 - 比較用
.equals()
:牢記包裝類比較值要使用.equals()
方法。 - 警惕空指針異常 (NPE):包裝類對象可以為
null
,自動拆箱時如果對象是null
,會拋出NullPointerException
。
Integer possibleNull = null;
// int num = possibleNull; // 運行時拋出 NullPointerException
// 安全做法
if (possibleNull != null) {int num = possibleNull;
}
Java字符串(String)深度解析
作為您的Java開發助手,我將為您全面系統地梳理Java中最重要的類之一——字符串(String)。字符串處理是編程中最常見的任務,深入理解String類對成為優秀的Java開發者至關重要。
一、String類的重要性與特點
為什么String如此重要?
- 字符串處理占日常Java開發的30%以上工作量
- 幾乎所有應用都需要處理文本數據、用戶輸入、文件內容等
- Java為字符串優化提供了特殊機制
String類的核心特點
- 不可變性(Immutability):String對象一旦創建,其值就不能被改變
- 字符串池(String Pool):Java使用字符串池優化內存使用
- final類:String類是final的,不能被繼承
- 實現了多個接口:Serializable, Comparable, CharSequence
二、String對象的創建方式
1. 直接使用字面量(推薦)
java
String str1 = "Hello World"; // 使用字符串池
String str2 = "Hello World"; // 重用字符串池中的對象
System.out.println(str1 == str2); // true,引用相同對象
2. 使用new關鍵字
java
String str3 = new String("Hello World"); // 強制創建新對象
String str4 = new String("Hello World"); // 創建另一個新對象
System.out.println(str3 == str4); // false,不同對象
System.out.println(str3.equals(str4)); // true,內容相同
3. 從字符數組創建
java
char[] charArray = {'H', 'e', 'l', 'l', 'o'};
String str5 = new String(charArray); // "Hello"
4. 從字節數組創建
java
byte[] byteArray = {72, 101, 108, 108, 111}; // ASCII碼
String str6 = new String(byteArray); // "Hello"
三、字符串池(String Pool)機制
什么是字符串池?
- Java為了減少內存開銷而設計的特殊存儲區域
- 存儲所有字符串字面量
- 避免創建相同內容的重復字符串
字符串池工作原理
java
String s1 = "Java"; // 在池中創建
String s2 = "Java"; // 重用池中的對象
String s3 = new String("Java"); // 在堆中創建新對象System.out.println(s1 == s2); // true,相同引用
System.out.println(s1 == s3); // false,不同引用
System.out.println(s1.equals(s3)); // true,內容相同
intern()方法
java
String s4 = new String("Python").intern(); // 將字符串放入池中或返回池中的引用
String s5 = "Python";
System.out.println(s4 == s5); // true
四、字符串不可變性及其影響
不可變性的含義
java
String str = "Hello";
str.concat(" World"); // 返回新字符串"Hello World",但str仍然是"Hello"
System.out.println(str); // 輸出"Hello"// 正確的方式
String newStr = str.concat(" World");
System.out.println(newStr); // 輸出"Hello World"
不可變性的優點
- 安全性:字符串作為參數傳遞時不會被修改
- 線程安全:多個線程可以安全地共享字符串
- 哈希碼緩存:String的hashCode()方法會緩存結果,提高哈希表性能
- 字符串池實現基礎:只有不可變對象才能被安全地共享
不可變性的缺點
頻繁修改字符串時會產生大量臨時對象,影響性能:
java
// 低效的字符串拼接
String result = "";
for (int i = 0; i < 1000; i++) {result += i; // 每次循環都會創建新的StringBuilder和String對象
}
五、String類的常用方法
1. 獲取字符串信息
java
String str = "Hello Java";// 獲取長度
int len = str.length(); // 10// 獲取指定位置字符
char ch = str.charAt(1); // 'e'// 獲取字符數組
char[] chars = str.toCharArray();// 獲取子字符串
String sub1 = str.substring(6); // "Java"
String sub2 = str.substring(0, 5); // "Hello"
2. 字符串比較
java
String s1 = "Java";
String s2 = "java";
String s3 = "Java";// 區分大小寫比較
boolean b1 = s1.equals(s2); // false// 不區分大小寫比較
boolean b2 = s1.equalsIgnoreCase(s2); // true// 比較字符串順序
int result = s1.compareTo(s2); // 負數(s1 < s2)
int result2 = s1.compareToIgnoreCase(s2); // 0// 檢查前綴后綴
boolean starts = s1.startsWith("Ja"); // true
boolean ends = s1.endsWith("va"); // true
3. 字符串查找
java
String text = "Java is a programming language";// 查找字符/字符串位置
int index1 = text.indexOf('a'); // 1
int index2 = text.indexOf('a', 2); // 從位置2開始查找,返回3
int index3 = text.indexOf("programming"); // 10// 從后向前查找
int lastIndex = text.lastIndexOf('a'); // 22// 檢查是否包含
boolean contains = text.contains("Java"); // true
4. 字符串轉換
java
String str = " Hello World ";// 去除首尾空格
String trimmed = str.trim(); // "Hello World"// 大小寫轉換
String upper = str.toUpperCase(); // " HELLO WORLD "
String lower = str.toLowerCase(); // " hello world "// 替換字符/字符串
String replaced = str.replace('l', 'L'); // " HeLLo WorLd "
String replacedAll = str.replaceAll("l", "L"); // " HeLLo WorLd "// 分割字符串
String[] parts = "Java,Python,C++".split(","); // ["Java", "Python", "C++"]
六、字符串拼接性能優化
1. 使用StringBuilder(非線程安全,性能高)
java
// 高效的字符串拼接
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 1000; i++) {sb.append(i);
}
String result = sb.toString();
2. 使用StringBuffer(線程安全,性能稍低)
java
// 線程安全的字符串拼接
StringBuffer sbf = new StringBuffer();
for (int i = 0; i < 1000; i++) {sbf.append(i);
}
String result = sbf.toString();
3. Java 8+的String.join()
java
// 拼接字符串數組或集合
String[] words = {"Java", "Python", "C++"};
String result = String.join(", ", words); // "Java, Python, C++"List<String> list = Arrays.asList("Apple", "Banana", "Orange");
String result2 = String.join(" - ", list); // "Apple - Banana - Orange"
七、字符串與其它類型的轉換
1. 基本數據類型轉字符串
java
// 多種方式
String s1 = String.valueOf(123); // "123"
String s2 = String.valueOf(3.14); // "3.14"
String s3 = String.valueOf(true); // "true"
String s4 = 456 + ""; // "456" (不推薦,會產生臨時字符串)
2. 字符串轉基本數據類型
java
// 使用包裝類的parseXxx方法
int i = Integer.parseInt("123");
double d = Double.parseDouble("3.14");
boolean b = Boolean.parseBoolean("true");// 處理可能出現的異常
try {int num = Integer.parseInt("123abc");
} catch (NumberFormatException e) {System.out.println("不是有效的數字格式");
}
八、正則表達式與字符串
1. 匹配模式
java
String email = "test@example.com";
boolean isValid = email.matches("^[\\w.-]+@[\\w.-]+\\.[a-zA-Z]{2,}$");
2. 替換操作
java
String text = "My phone number is 123-456-7890";
// 隱藏電話號碼
String hidden = text.replaceAll("\\d{3}-\\d{3}-\\d{4}", "***-***-****");
3. 分割字符串
java
String csv = "Java,Python,C++,JavaScript";
String[] languages = csv.split(",\\s*"); // 按逗號分割,允許逗號后有空格
九、性能優化與最佳實踐
1. 優先使用字符串字面量
java
// 好
String s1 = "Hello";// 不好(除非確需新實例)
String s2 = new String("Hello");
2. 使用StringBuilder進行復雜拼接
java
// 好
StringBuilder sb = new StringBuilder();
sb.append("Hello").append(" ").append("World");// 不好(產生多個臨時對象)
String result = "Hello" + " " + "World"; // 編譯器會優化為StringBuilder,但復雜循環中仍需顯式使用
3. 使用equals()比較內容,==比較引用
java
String s1 = "Java";
String s2 = new String("Java");// 比較內容
boolean contentEqual = s1.equals(s2); // true// 比較引用
boolean referenceEqual = (s1 == s2); // false
4. 注意字符串編碼
java
// 指定編碼轉換
try {String str = "中文";byte[] utf8Bytes = str.getBytes("UTF-8");String newStr = new String(utf8Bytes, "UTF-8");
} catch (UnsupportedEncodingException e) {e.printStackTrace();
}
十、Java 9+的字符串優化
1. 緊湊字符串(Compact Strings)
- Java 9前:String使用char[]存儲,每個字符占2字節
- Java 9+:根據字符串內容選擇Latin-1(1字節)或UTF-16(2字節)編碼
- 減少內存占用,提高性能
2. 字符串拼接優化
Java 9+對字符串拼接進行了內部優化,但在復雜場景下仍建議使用StringBuilder。
Java集合框架(Collection Framework)全面解析
作為您的Java開發助手,我將為您系統性地梳理Java集合框架的知識。集合框架是Java中最重要、最常用的工具庫之一,幾乎在所有Java應用程序中都會用到。
一、為什么需要集合框架?
在編程中,我們經常需要存儲和操作一組對象。數組雖然可以存儲多個元素,但存在以下局限性:
- 長度固定,無法動態擴展
- 缺乏豐富的操作方法
- 只能存儲相同類型的數據
集合框架解決了這些問題,提供了:
- 動態大小:集合可以動態增長和縮小
- 豐富API:提供了添加、刪除、查找、排序等豐富操作
- 類型安全:通過泛型保證類型安全
- 高性能:針對不同場景優化了數據結構
二、集合框架體系結構
Java集合框架主要由兩大接口組成:Collection和Map。
1. Collection接口繼承體系
text
Collection (接口)
├── List (接口 - 有序、可重復)
│ ├── ArrayList (數組實現)
│ ├── LinkedList (鏈表實現)
│ └── Vector (線程安全的數組實現,已較少使用)
│ └── Stack (棧實現)
├── Set (接口 - 無序、不可重復)
│ ├── HashSet (哈希表實現)
│ │ └── LinkedHashSet (保持插入順序的HashSet)
│ ├── TreeSet (紅黑樹實現,有序)
│ └── EnumSet (枚舉專用Set)
└── Queue (接口 - 隊列)├── PriorityQueue (優先級隊列)├── LinkedList (也可作為隊列使用)└── Deque (接口 - 雙端隊列)├── ArrayDeque (數組實現的雙端隊列)└── LinkedList (鏈表實現的雙端隊列)
2. Map接口繼承體系
text
Map (接口)
├── HashMap (哈希表實現)
│ └── LinkedHashMap (保持插入順序的HashMap)
├── Hashtable (線程安全的哈希表,已較少使用)
│ └── Properties (配置專用)
├── TreeMap (紅黑樹實現,有序)
└── WeakHashMap (弱引用HashMap)
三、核心接口詳解
1. Collection接口
所有集合類的根接口,定義了基本操作:
java
boolean add(E e) // 添加元素
boolean remove(Object o) // 刪除元素
boolean contains(Object o) // 判斷是否包含
int size() // 獲取元素數量
boolean isEmpty() // 判斷是否為空
Iterator<E> iterator() // 獲取迭代器
void clear() // 清空集合
2. List接口(有序、可重復)
- 元素有順序(插入順序)
- 可以通過索引訪問元素
- 允許重復元素
3. Set接口(無序、不可重復)
- 元素無順序(除非是TreeSet或LinkedHashSet)
- 不允許重復元素
- 最多包含一個null元素
4. Map接口(鍵值對)
- 存儲鍵值對映射
- 鍵不能重復(重復的鍵會覆蓋舊值)
- 每個鍵最多映射到一個值
5. Queue接口(隊列)
- 先進先出(FIFO)的數據結構
- 支持在隊列兩端進行操作
四、主要實現類詳解
1. ArrayList
基于動態數組實現,是最常用的List實現。
java
// 創建ArrayList
List<String> list = new ArrayList<>();// 添加元素
list.add("Apple");
list.add("Banana");
list.add("Orange");// 訪問元素
String fruit = list.get(0); // "Apple"// 遍歷元素
for (String item : list) {System.out.println(item);
}// 使用索引遍歷
for (int i = 0; i < list.size(); i++) {System.out.println(list.get(i));
}// 使用迭代器遍歷
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {System.out.println(iterator.next());
}// 使用forEach方法(Java 8+)
list.forEach(System.out::println);// 刪除元素
list.remove("Banana");
list.remove(0); // 按索引刪除// 判斷是否包含
boolean hasApple = list.contains("Apple");// 轉換為數組
String[] array = list.toArray(new String[0]);
特點:
- 隨機訪問快(O(1))
- 在尾部添加元素快(O(1))
- 在中間插入/刪除元素慢(需要移動元素,O(n))
- 非線程安全
2. LinkedList
基于雙向鏈表實現,可用作List、Queue或Deque。
java
// 創建LinkedList
List<String> list = new LinkedList<>();
Queue<String> queue = new LinkedList<>();
Deque<String> deque = new LinkedList<>();// 作為List使用
list.add("A");
list.add("B");
list.get(1); // "B"// 作為Queue使用
queue.offer("A"); // 添加元素到隊列尾
queue.poll(); // 移除并返回隊列頭元素
queue.peek(); // 返回隊列頭元素但不移除// 作為Deque使用
deque.addFirst("A"); // 添加到隊列頭
deque.addLast("B"); // 添加到隊列尾
deque.removeFirst(); // 移除并返回隊列頭元素
deque.removeLast(); // 移除并返回隊列尾元素
特點:
- 在任意位置插入/刪除元素快(O(1))
- 隨機訪問慢(需要遍歷,O(n))
- 實現了List和Deque接口
- 非線程安全
3. HashSet
基于哈希表實現,是最常用的Set實現。
java
// 創建HashSet
Set<String> set = new HashSet<>();// 添加元素
set.add("Apple");
set.add("Banana");
set.add("Orange");
set.add("Apple"); // 重復元素,不會被添加// 判斷是否包含
boolean hasApple = set.contains("Apple");// 刪除元素
set.remove("Banana");// 遍歷元素
for (String item : set) {System.out.println(item);
}// 獲取元素數量
int size = set.size();
特點:
- 添加、刪除、查找操作快(平均O(1))
- 元素無序
- 允許null元素
- 非線程安全
4. LinkedHashSet
繼承自HashSet,保持元素的插入順序。
java
Set<String> set = new LinkedHashSet<>();
set.add("Apple");
set.add("Banana");
set.add("Orange");// 遍歷時會按照添加順序輸出
for (String fruit : set) {System.out.println(fruit); // Apple, Banana, Orange
}
特點:
- 保持插入順序
- 性能略低于HashSet
- 非線程安全
5. TreeSet
基于紅黑樹實現,元素有序。
java
// 創建TreeSet
Set<String> set = new TreeSet<>();// 添加元素(會自動排序)
set.add("Orange");
set.add("Apple");
set.add("Banana");// 遍歷時會按自然順序輸出
for (String fruit : set) {System.out.println(fruit); // Apple, Banana, Orange
}// 可以使用Comparator自定義排序
Set<String> customSet = new TreeSet<>(Comparator.reverseOrder());
customSet.add("Orange");
customSet.add("Apple");
customSet.add("Banana");for (String fruit : customSet) {System.out.println(fruit); // Orange, Banana, Apple
}
特點:
- 元素有序(自然順序或指定Comparator)
- 添加、刪除、查找操作時間復雜度為O(log n)
- 不允許null元素
- 非線程安全
6. HashMap
基于哈希表實現,是最常用的Map實現。
java
// 創建HashMap
Map<String, Integer> map = new HashMap<>();// 添加鍵值對
map.put("Apple", 1);
map.put("Banana", 2);
map.put("Orange", 3);// 獲取值
Integer count = map.get("Apple"); // 1// 判斷是否包含鍵
boolean hasApple = map.containsKey("Apple");// 判斷是否包含值
boolean hasValue = map.containsValue(1);// 遍歷鍵
for (String key : map.keySet()) {System.out.println(key);
}// 遍歷值
for (Integer value : map.values()) {System.out.println(value);
}// 遍歷鍵值對
for (Map.Entry<String, Integer> entry : map.entrySet()) {System.out.println(entry.getKey() + ": " + entry.getValue());
}// 使用forEach方法(Java 8+)
map.forEach((k, v) -> System.out.println(k + ": " + v));// 刪除鍵值對
map.remove("Banana");// 獲取元素數量
int size = map.size();
特點:
- 基于哈希表,操作速度快(平均O(1))
- 鍵無序
- 允許null鍵和null值
- 非線程安全
7. LinkedHashMap
繼承自HashMap,保持鍵的插入順序或訪問順序。
java
// 保持插入順序
Map<String, Integer> map = new LinkedHashMap<>();
map.put("Apple", 1);
map.put("Banana", 2);
map.put("Orange", 3);// 遍歷時會按照插入順序輸出
for (String key : map.keySet()) {System.out.println(key); // Apple, Banana, Orange
}// 創建按訪問順序排序的LinkedHashMap(最近最少使用)
Map<String, Integer> lruMap = new LinkedHashMap<>(16, 0.75f, true);
lruMap.put("Apple", 1);
lruMap.put("Banana", 2);
lruMap.put("Orange", 3);lruMap.get("Apple"); // 訪問Apple,使其成為最近訪問的// 遍歷時會按訪問順序輸出(最近訪問的在最后)
for (String key : lruMap.keySet()) {System.out.println(key); // Banana, Orange, Apple
}
特點:
- 保持鍵的插入順序或訪問順序
- 性能略低于HashMap
- 非線程安全
8. TreeMap
基于紅黑樹實現,鍵有序。
java
// 創建TreeMap
Map<String, Integer> map = new TreeMap<>();// 添加鍵值對(按鍵的自然順序排序)
map.put("Orange", 3);
map.put("Apple", 1);
map.put("Banana", 2);// 遍歷時會按鍵的自然順序輸出
for (String key : map.keySet()) {System.out.println(key + ": " + map.get(key)); // Apple:1, Banana:2, Orange:3
}// 可以使用Comparator自定義排序
Map<String, Integer> customMap = new TreeMap<>(Comparator.reverseOrder());
customMap.put("Orange", 3);
customMap.put("Apple", 1);
customMap.put("Banana", 2);for (String key : customMap.keySet()) {System.out.println(key + ": " + map.get(key)); // Orange:3, Banana:2, Apple:1
}
特點:
- 鍵有序(自然順序或指定Comparator)
- 操作時間復雜度為O(log n)
- 不允許null鍵(但允許null值)
- 非線程安全
五、集合的遍歷方式
1. 使用迭代器(Iterator)
java
List<String> list = new ArrayList<>();
// 添加元素...Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {String element = iterator.next();System.out.println(element);// 可以在遍歷時安全刪除元素if (element.equals("RemoveMe")) {iterator.remove();}
}
2. 使用for-each循環(推薦)
java
for (String element : list) {System.out.println(element);// 注意:不能在for-each循環中直接刪除元素,會拋出ConcurrentModificationException
}
3. 使用forEach方法(Java 8+)
java
list.forEach(element -> System.out.println(element));
// 或使用方法引用
list.forEach(System.out::println);
4. 遍歷Map的幾種方式
java
Map<String, Integer> map = new HashMap<>();
// 添加鍵值對...// 1. 遍歷鍵
for (String key : map.keySet()) {System.out.println(key + ": " + map.get(key));
}// 2. 遍歷值
for (Integer value : map.values()) {System.out.println(value);
}// 3. 遍歷鍵值對(推薦)
for (Map.Entry<String, Integer> entry : map.entrySet()) {System.out.println(entry.getKey() + ": " + entry.getValue());
}// 4. 使用forEach方法(Java 8+)
map.forEach((k, v) -> System.out.println(k + ": " + v));
六、集合工具類Collections
Collections類提供了許多操作集合的靜態方法:
1. 排序操作
java
List<Integer> list = Arrays.asList(3, 1, 4, 1, 5, 9);// 排序
Collections.sort(list);
System.out.println(list); // [1, 1, 3, 4, 5, 9]// 反轉排序
Collections.sort(list, Collections.reverseOrder());
System.out.println(list); // [9, 5, 4, 3, 1, 1]// 隨機打亂
Collections.shuffle(list);
System.out.println(list); // 隨機順序// 反轉列表
Collections.reverse(list);
System.out.println(list); // 反轉順序
2. 查找和替換操作
java
List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);// 二分查找(必須先排序)
Collections.sort(list);
int index = Collections.binarySearch(list, 3); // 2// 查找極值
int max = Collections.max(list); // 5
int min = Collections.min(list); // 1// 替換所有
Collections.replaceAll(list, 1, 10);
System.out.println(list); // [10, 2, 3, 4, 5]// 填充
Collections.fill(list, 0);
System.out.println(list); // [0, 0, 0, 0, 0]
3. 同步包裝
將非線程安全的集合轉換為線程安全的版本:
java
List<String> syncList = Collections.synchronizedList(new ArrayList<>());
Set<String> syncSet = Collections.synchronizedSet(new HashSet<>());
Map<String, Integer> syncMap = Collections.synchronizedMap(new HashMap<>());
七、集合的性能比較與選擇指南
集合類型 | 實現類 | 特點 | 適用場景 |
---|---|---|---|
List | ArrayList | 隨機訪問快,增刪慢 | 需要頻繁按索引訪問元素 |
LinkedList | 增刪快,隨機訪問慢 | 需要頻繁在頭部/中間插入刪除元素 | |
Set | HashSet | 查找快,無序 | 需要快速查找,不關心順序 |
LinkedHashSet | 查找快,保持插入順序 | 需要快速查找且保持插入順序 | |
TreeSet | 有序,查找較慢 | 需要有序遍歷元素 | |
Map | HashMap | 查找快,無序 | 需要快速鍵值查找,不關心順序 |
LinkedHashMap | 查找快,保持插入/訪問順序 | 需要快速查找且保持順序 | |
TreeMap | 有序,查找較慢 | 需要有序遍歷鍵 | |
Queue | ArrayDeque | 雙端隊列,高效 | 需要隊列或棧功能 |
PriorityQueue | 優先級隊列 | 需要按優先級處理元素 |
八、最佳實踐與注意事項
-
使用泛型:始終使用泛型指定集合元素類型,避免類型轉換錯誤
java
// 好 List<String> list = new ArrayList<>();// 不好(會產生警告,可能運行時出錯) List list = new ArrayList();
-
使用接口類型聲明:使用接口類型聲明集合變量,提高代碼靈活性
java
// 好 List<String> list = new ArrayList<>(); Set<String> set = new HashSet<>(); Map<String, Integer> map = new HashMap<>();// 不好(將實現綁定到具體類) ArrayList<String> list = new ArrayList<>();
-
預估初始容量:對于已知大小的集合,設置初始容量避免頻繁擴容
java
// 預估有1000個元素 List<String> list = new ArrayList<>(1000); Map<String, Integer> map = new HashMap<>(1000);
-
注意線程安全:默認集合實現都不是線程安全的,多線程環境下需要同步
java
// 方式1:使用同步包裝 List<String> syncList = Collections.synchronizedList(new ArrayList<>());// 方式2:使用并發集合(java.util.concurrent包) ConcurrentHashMap<String, Integer> concurrentMap = new ConcurrentHashMap<>(); CopyOnWriteArrayList<String> copyOnWriteList = new CopyOnWriteArrayList<>();
-
正確實現equals和hashCode:如果要將自定義對象作為HashMap的鍵或HashSet的元素,必須正確重寫equals()和hashCode()方法
java
public class Person {private String name;private int age;@Overridepublic boolean equals(Object o) {if (this == o) return true;if (o == null || getClass() != o.getClass()) return false;Person person = (Person) o;return age == person.age && Objects.equals(name, person.name);}@Overridepublic int hashCode() {return Objects.hash(name, age);} }
-
使用Java 8+新特性:利用Stream API和Lambda表達式簡化集合操作
java
List<String> list = Arrays.asList("Apple", "Banana", "Orange", "Avocado");// 過濾并轉換 List<String> result = list.stream().filter(s -> s.startsWith("A")).map(String::toUpperCase).collect(Collectors.toList());System.out.println(result); // [APPLE, AVOCADO]
九、總結
Java集合框架提供了豐富的數據結構和算法,是Java編程的核心組成部分。掌握集合框架的關鍵點包括:
- 理解體系結構:清楚Collection和Map兩大接口體系及其實現類
- 掌握常用實現類:熟練使用ArrayList、LinkedList、HashSet、HashMap等常用集合
- 正確選擇集合類型:根據需求特點選擇合適的集合實現
- 熟練操作集合:掌握集合的遍歷、排序、查找等操作
- 遵循最佳實踐:使用泛型、注意線程安全、正確實現equals/hashCode等