Java常用工具类
异常 (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等