Java基础-知识点03
- String类
- String类的作用及特性
- String不可以改变的原因及好处
- String、StringBuilder、StringBuffer的区别
- String中的replace和replaceAll的区别
- 字符串拼接使用+还是使用Stringbuilder
- String中的equal()与Object方法中equals()区别
- String a = new String("abc");创建了几个对象?
- 成员变量
- 实例变量
- 静态变量
- 静态变量与实例变量的内存分配有何不同?
- 静态变量在多线程环境下的安全性如何保证?
- 如何实现常量(不可变)类:
- 成员变量的默认值
- 成员变量的访问权限
- hashcode
- hashCode 方法的原理和实现
- hashcode和equals()区别
- 为什么重写 equals() 时必须重写 hashCode() 方法?
- 为什么 JDK 还要同时提供这两个方法呢?
- 为什么不同的对象可能会有相同的hashcode?什么是哈希冲突?
- 如何解决哈希冲突?
- 哈希表的负载因子
- 为什么哈希表负载因子默认为0.75?
- 异常
- 异常的层次结构
- 受检异常(非运行时异常)
- 非受检异常(运行时异常)
- Error类及其子类
- Java 中的异常处理机制
- try-catch-finally 中哪个部分可以省略
- 异常的声明
- 异常的抛出
- 抛出异常的场景
- 什么是异常链(Exception Chaining)?如何使用异常链?
- 如何自定义异常类?
String类
String
类是Java中用于表示字符串的类。在Java中,字符串是一系列字符的序列,用于表示文本数据。
String类的作用及特性
1、创建字符串: 可以通过字符串字面量或使用new关键字来创建字符串对象。
String str1 = "Hello"; // 使用字符串字面量创建
String str2 = new String("World"); // 使用new关键字创建
2、字符串长度: 可以使用length()方法获取字符串的长度。
String str = "Hello, World!";
int length = str.length(); // length = 13
3、字符串连接: 可以使用+运算符或concat()方法将字符串连接起来。
String str1 = "Hello";
String str2 = "World";
String result = str1 + " " + str2; // 使用+运算符连接
String result2 = str1.concat(" ").concat(str2); // 使用concat()方法连接
4、字符串比较: 可以使用equals()方法比较两个字符串是否相等。
String str1 = "Hello";
String str2 = "hello";
boolean isEqual = str1.equals(str2); // false,区分大小写
5、子串提取: 可以使用substring()方法从字符串中提取子串。
String str = "Hello, World!";
String sub = str.substring(7); // 提取从索引为7开始的子串:"World!"
6、字符串查找: 可以使用indexOf()方法查找指定字符或子串在字符串中的位置。
String str = "Hello, World!";
int index = str.indexOf("World"); // index = 7,子串"World"在字符串中的位置
7、字符串替换: 可以使用replace()方法替换字符串中的字符或子串。
String str = "Hello, World!";
String replaced = str.replace("World", "Java"); // 替换子串:"Hello, Java!"
8、字符串拆分: 可以使用split()方法根据指定的分隔符将字符串拆分为字符串数组。
String str = "apple,orange,banana";
String[] fruits = str.split(","); // fruits = ["apple", "orange", "banana"]
9、字符串转换: 可以使用toLowerCase()、toUpperCase()等方法将字符串转换为小写或大写形式。
String str = "Hello, World!";
String lowerCase = str.toLowerCase(); // 转换为小写:"hello, world!"
String upperCase = str.toUpperCase(); // 转换为大写:"HELLO, WORLD!"
String类还提供了许多其他方法,如去除空白字符、字符查找、字符替换等
String不可以改变的原因及好处
1、String类不该可变的原因:
- String内部使用
char数组
存储数据,该数组被声明为 final。所以数组初始化后就不能引用其他数组。 - String内部没有提供更改 char数组 的方法。 因此保证 String 不可变。
2、String对象不可变的好处:
- 安全性和可靠性: 不可变性确保了字符串对象的安全性和可靠性。
- 可以缓存 hash 值 (String 用做 HashMap 的 key)
- String Pool 的需要,如果一个 String 对象已经被创建过了,那么就会从 String Pool 中取得引用。只有 String 是不可变的,才可能使用 String Pool。
String、StringBuilder、StringBuffer的区别
String, StringBuffer, 和 StringBuilder 是 Java 中用于处理字符串的三个类,它们在功能和性能上有一些区别:
1、String:
- String 是
不可变
的类,一旦创建后就不能修改其内容,线程安全
。 - 每次对 String 进行修改(如拼接、替换等操作)都会生成一个新的 String 对象,原有的对象不变。
- 适用于那些不需要频繁修改的字符串,如存储常量、配置信息等。
2、StringBuffer:
- StringBuffer 是
可变
的类,可以动态修改其内容。 - 提供了许多方法来进行字符串的拼接、插入、删除、替换等操作。
- 是
线程安全
的,所有的公共方法都使用了synchronized
关键字进行同步。 - 适用于
多线程
环境下需要频繁修改字符串的场景。
3、StringBuilder:
- StringBuilder 是
可变
的类,和 StringBuffer 类似,但不保证线程安全
。 - 没有对方法进行同步处理,因此在单线程环境下性能更好。
- 适用于
单线程
环境下需要频繁修改字符串的场景,比如字符串拼接、动态生成文本等。
记忆表格
特点 | String | StringBuffer | StringBuilder |
---|---|---|---|
可变性 | 不可变 | 可变 | 可变 |
线程安全性 | 线程安全 | 线程安全 | 非线程安全 |
内容修改方式 | 生成新的String对象 | 直接修改原对象 | 直接修改原对象 |
同步机制 | 不需要 | 使用synchronized关键字同步 | 不需要 |
性能 | 较低(频繁修改时) | 一般 | 较高(频繁修改时) |
适用场景 | 不需要频繁修改的字符串 | 多线程环境下需要频繁修改的字符串 | 单线程环境下需要频繁修改的字符串 |
String中的replace和replaceAll的区别
String类中的replace和replaceAll方法都用于替换字符串中的字符或子串。
1、replace方法:
replace方法接受两个参数:要替换的旧字符或子串,和替换成的新字符或子串。
它只会替换字符串中所有匹配的旧字符或子串为新字符或子串,不涉及正则表达式。
2、replaceAll方法:
replaceAll方法接受两个参数:要替换的正则表达式,和替换成的新字符串。
它使用正则表达式来匹配要替换的内容,可以实现更灵活的替换规则。
记忆
:replace方法是基于字符或子串的简单替换,而replaceAll方法则更加灵活,可以基于正则表达式进行替换,因此在需要复杂替换规则或匹配模式时,更适合使用replaceAll方法。
字符串拼接使用+还是使用Stringbuilder
在进行字符串拼接时,推荐使用StringBuilder或StringBuffer而不是简单的使用+操作符,特别是在有大量字符串拼接的情况下。因为StringBuilder和StringBuffer可以提供更好的性能和内存利用率。
使用+操作符进行字符串拼接时,会调用StringBuilder 的 append() 方法实现拼接,然后通过 toString() 方法将 StringBuilder 转换为最终的字符串对象。但是在循环体中,每循环一次就会创建一个新的对象,这样会导致大量的对象创建和内存消耗。
如果是在单线程环境下进行字符串拼接,推荐使用StringBuilder,因为它的性能更高,不需要同步操作。如果在多线程环境下进行字符串拼接,应该使用StringBuffer,因为它是线程安全的,内部方法都使用了synchronized关键字进行同步。
String中的equal()与Object方法中equals()区别
1、String类中的equals()方法:
String类重写了Object类中的equals()方法,用于比较两个字符串对象的内容是否相同。String类的equals()方法比较的是字符串的内容,而不是引用地址。
String str1 = "hello";
String str2 = "hello";
boolean result = str1.equals(str2); // true,比较的是字符串内容
2、Object类中的equals()方法:
Object类中的equals()方法是用于比较两个对象的引用地址是否相同。如果一个类没有重写Object类的equals()方法,则默认使用Object类中的方法进行引用地址的比较。
Object obj1 = new Object();
Object obj2 = new Object();
boolean result = obj1.equals(obj2); // false,比较的是对象的引用地址
String a = new String(“abc”);创建了几个对象?
创建了2个对象。
1、使用new关键字,首先会在堆中创建一个实例对象,即创建了一个字符串常量 “abc” 的对象。这个对象存储在字符串常量池中,因为字符串常量是不可变的,所以它可以被多个字符串变量引用,并且在内存中只有一份拷贝。
2、创建了一个新的字符串对象 a。这个对象是通过调用 new String(“abc”) 构造方法创建的,它在堆内存中独立存在,并且包含了字符串常量 “abc” 的内容。
成员变量
Java 中的成员变量是指定义在类中的变量,包含实例变量和静态变量。用于存储对象的状态或类的共享状态。
实例变量
- 实例变量是属于对象的变量,每个对象都有自己的一份实例变量,它们在内存中存储在对象的内部。
- 实例变量在对象被创建时被初始化,并且每个对象的实例变量值是相互独立的。
- 实例变量通常用于存储对象的状态信息,比如一个人的姓名、年龄、性别等。
- 实例变量只能通过对象来访问,不能通过类名来访问。
public class MyClass {
int instanceVar; // 实例变量
}
静态变量
- 静态变量是属于类的变量,所有对象共享一份静态变量,它们在内存中只会有一份拷贝,无论该类被实例化多少次,该变量的值都是相同的。
- 静态变量通常用于存储所有对象共享的数据,比如常量、全局计数器等。
- 静态变量可以通过类名来访问,也可以通过对象来访问,但是建议使用类名来访问。
public class MyClass {
static int staticVar; // 静态变量
}
静态变量与实例变量的内存分配有何不同?
- 实例变量在每个对象创建时分配内存,每个对象都有自己的实例变量副本。
- 静态变量在类加载时分配内存,只会被分配一次,所有类的实例共享同一个静态变量。
静态变量在多线程环境下的安全性如何保证?
- 静态变量可以在多线程环境下共享,但是需要注意同步访问静态变量的操作,可以使用
synchronized
关键字或者volatile
关键字来保证线程安全性。
如何实现常量(不可变)类:
- 使用 final 关键字修饰类,防止类被继承。
- 使用 final 关键字修饰变量,防止变量被修改。
- 使用构造方法或静态代码块初始化不可变对象的值。
- 提供只读访问方法,不提供修改方法。
成员变量的默认值
- 对于基本数据类型的成员变量,默认值为0或false。
- 对于对象引用类型的成员变量,默认值为null。
成员变量的访问权限
成员变量的访问权限可以通过访问修饰符来控制,常用的访问修饰符包括public、private、protected
和默认访问修饰符。
hashcode
hashCode
是 Java 中用于获取对象哈希码的方法。哈希码是一个整数,用于快速定位对象在哈希表等数据结构中的位置,比如在集合类如 HashMap、HashSet 中的使用。hashCode 方法定义在 Object 类中,因此所有 Java 类都可以调用 hashCode 方法。
hashCode 方法的原理和实现
- hashCode 方法返回的是对象的哈希码,可以理解为对象的逻辑地址。hashCode 方法的默认实现是根据对象的地址计算哈希码,即将对象的内存地址转换成一个整数值。。
- 相同对象调用 hashCode 方法多次应该返回相同的结果。
- 对于不同的对象,其哈希码一般应该是不同的,但不保证不同对象的哈希码绝对唯一。
hashcode和equals()区别
equals() 是用来判断两个对象是否相等,即内容是否相相等。
hashCode 方法用于获取对象的哈希码,用于快速定位对象在哈希表等数据结构中的位置。hashcode 也可以用来两个对象是否相等,但是判断结果不准确,因为哈希码相等的两个对象不一定是同一个对象。
为什么重写 equals() 时必须重写 hashCode() 方法?
因为两个相等的对象的 hashCode 值必须是相等。如果 equals 方法判断两个对象是相等的,那这两个对象的 hashCode 值也要相等。如果重写 equals() 时没有重写 hashCode() 方法的话就可能会导致 equals 方法判断是相等的两个对象,但hashCode 值却不相等。
总结:
- 如果两个对象的hashCode 值相等,那这两个对象不一定相等(哈希碰撞)。
- 如果两个对象的hashCode 值相等并且equals()方法也返回 true,我们才认为这两个对象相等。
- 如果两个对象的hashCode 值不相等,我们就可以直接认为这两个对象不相等。
为什么 JDK 还要同时提供这两个方法呢?
为了提高效率。在一些容器(比如 HashMap、HashSet)中,有了 hashCode() 之后,判断元素是否在对应容器中的效率会更高。
在Java的⼀些集合类的实现中,比较两个对象是否相等时,先调用对象的 hashCode()方法得到hashCode进行比较,如果hashCode不相同,就可以直接认为这两个对象不相同,如果hashCode相同,进⼀步调用equals()方法进行比较。
而equals()⽅法,就是⽤来最终确定两个对象是不是相等的,因为equals()方法的实现会比较重,逻辑⽐较多,⽽hashCode()主要就是得到⼀个哈希值,实际上就⼀个数字,效率较快,所以在比较两个对象时,通常都会先根据 hashCode先比较⼀下。
为什么不同的对象可能会有相同的hashcode?什么是哈希冲突?
不同的对象可能会有相同的哈希码,这种情况通常称为哈希冲突。哈希冲突是指两个不同的对象经过哈希函数计算后得到相同的哈希码。
这种情况可能发生的原因包括以下几点:
- 哈希码范围有限: 哈希码是一个整数,在有限的范围内取值。如果两个不同的对象经过哈希函数计算后得到的哈希码在这个范围内是相同的,就会发生哈希冲突。
- 对象属性相似性: 如果两个对象的属性在哈希计算中具有相似性或者相等性,那么它们的哈希码可能会相同。
- 哈希码与内存地址无关: Java 中的哈希码通常与对象的内存地址无关,因此即使两个不同的对象在内存中存储位置不同,它们的哈希码也有可能相同。
如何解决哈希冲突?
哈希冲突是指两个不同的对象经过哈希函数计算后得到相同的哈希码。为了解决哈希冲突,通常采用以下几种常见的方法:
1、开放寻址法:
当发生哈希冲突时,使用一定的规则找到下一个可用的存储位置,直到找到一个空闲位置或者遍历整个表。常见的开放寻址法包括线性探测
、平方探测
、双重散列
等。
- 线性探测法:从当前占用的地址往后,逐一排查,有空的地址就占用。如果查到表尾还没有就返回到表头,直到查完。
- 平方探测法:探测的主要思想是,如果在原始哈希码的位置发生冲突,就通过计算一个增量序列来寻找下一个空闲位置。这个增量序列通常采用二次方的增量。如从发生冲突的位置d[i],按照地址+平方往后找,即d[i]+12,d[i]+22,d[i]+32直到找到空地址。
- 双重散列法:双重散列法的基本思想是,当发生哈希冲突时,通过第一个哈希函数计算原始位置,然后通过第二个哈希函数计算增量,不断尝试将冲突的位置加上增量直到找到空闲位置或者遍历整个哈希表。
2、链地址法
在链地址法中,哈希表的每个桶(存储位置)都对应一个链表,发生哈希冲突时,新元素会直接添加到对应桶的链表末尾,形成一个链式结构。
链地址法的基本思想是,将具有相同哈希码但不同的键值的元素存储在同一个桶中,通过链表将它们串联起来。这样,在发生哈希冲突时,不需要重新计算位置或者探测空闲位置,只需要在对应桶的链表中进行插入、查找或删除操作即可。
链地址法的具体过程:
- 使用哈希函数计算键值的哈希码,确定存储位置(桶)。
- 如果该桶为空,直接将元素插入到该桶中。
- 如果该桶非空,遍历链表查找是否存在相同键值的元素:
- 如果存在相同键值的元素,则更新该元素的值。
- 如果不存在相同键值的元素,则将新元素添加到链表末尾。
3、链地址法的优化
在链地址法中,当链表过长时可能会影响性能。可以采用链表长度超过阈值时,将链表转换为更高效的数据结构,如红黑树,来优化查找和操作性能。
这些方法各有优缺点,选择合适的解决方法取决于具体的应用场景和性能需求。在实际开发中,常见的哈希表实现如 HashMap 就是通过链地址法来解决哈希冲突,并对桶的数量和链表长度等进行动态调整来保持性能。
哈希表的负载因子
哈希表的负载因子是指哈希表中已存储元素个数与哈希表容量之比,通常用公式表示为负载因子 = 元素个数 / 哈希表容量。负载因子是衡量哈希表空间利用率的重要指标之一。
负载因子反映了哈希表的密集程度。当负载因子较小时,哈希表中空槽较多,可能会浪费一些存储空间;当负载因子较大时,哈希表中的槽可能会被填满,导致哈希冲突的概率增加,查找、插入、删除等操作的时间复杂度可能会变高。
在 Java 中,哈希表的默认负载因子
通常是 0.75
。这个默认值指的是当哈希表中的元素个数达到容量的 75% 时,会触发哈希表的扩容操作。
为什么哈希表负载因子默认为0.75?
这个负载因子的选择是经过实践和考量的结果,旨在保持哈希表的性能和空间利用率的平衡。
1、空间利用率: 负载因子为 0.75 表示在哈希表容量的 75% 时触发扩容,这样可以保证哈希表的空间利用率比较高,减少了内存浪费。
2、性能平衡: 通过控制负载因子,可以在空间利用率和性能之间寻找一个平衡点。负载因子过小会导致频繁的扩容操作,影响性能;负载因子过大会增加哈希冲突的概率,也会影响性能。
3、哈希冲突管理: 适度的负载因子可以有效控制哈希冲突的发生,避免在元素插入、查找和删除等操作时频繁地发生冲突。
在 Java 中,可以通过 HashMap 和 HashSet 等类的构造函数来自定义负载因子。例如,在创建一个 HashMap 实例时,可以使用指定负载因子的构造函数来设置自定义的负载因子值。
Map<String, Integer> map = new HashMap<>(16, 0.8f); // 设置负载因子为 0.8
Set<String> set = new HashSet<>(32, 0.6f); // 设置负载因子为 0.6
异常
异常在 Java 中是一种用于处理程序运行时错误的机制。
异常的层次结构
Java 的异常类主要分为两大类:Throwable
类及其子类和 Error
类及其子类。Throwable 类是所有 Java 异常类的根类,它有两个主要的子类:Exception 类和 Error 类。
Exception
类: 是表示程序可以处理的异常情况的基类,它包括了各种受检异常(非运行时异常)和非受检异常(运行时异常)。
Error
类: 表示严重的错误,通常由系统级问题引起,比如内存不足、虚拟机错误等。Error 类及其子类不需要程序员显式地处理或者抛出,通常是由虚拟机或系统级别的组件处理。
受检异常(非运行时异常)
受检异常是指在编译时强制要求程序处理的异常。Exception 的子类中,除了 RuntimeException 及其子类之外的所有异常,都属于受检异常。这些异常通常是由外部因素引起的,比如文件不存在、网络连接失败等。受检异常必须在代码中显式地处理或者通过 throws 关键字声明抛出。
常见的受检异常:
IOException
:输入输出异常,比如文件读写错误。SQLException
:数据库操作异常。
public void readFile() throws IOException {
FileInputStream fis = new FileInputStream("file.txt");
// 处理文件读取逻辑
fis.close();
}
非受检异常(运行时异常)
非受检异常也称为运行时异常,是指在运行时可能会发生但不需要强制处理的异常。RuntimeException 及其子类属于非受检异常,这些异常通常是由程序逻辑错误引起的,比如空指针异常、数组下标越界等。非受检异常不要求在代码中显式处理,但可以选择捕获并处理,也可以通过 throws 关键字声明抛出。
RuntimeException:运行时异常的基类
NullPointerException
:空指针异常。ArrayIndexOutOfBoundsException
:数组索引越界异常。ClassCastException
:类转换异常等。
public void divide(int a, int b) {
if (b == 0) {
throw new ArithmeticException("除数不能为零");
}
int result = a / b;
System.out.println("结果:" + result);
}
Error类及其子类
Error 类及其子类:程序中无法处理的错误, 此类错误一般表示代码运行时 JVM 出现问题。此类错误发生时,JVM 将终止线程。这些错误是不受检异常,非代码性错误。
通常有:
Virtual MachineError
(虚拟机运行错误)NoClassDefFoundError
(类定义错误)OutOfMemoryError
:内存不足错误StackOverflowError
:栈溢出错误
Java 中的异常处理机制
Java 中的异常处理机制通过 try-catch-finally
块来实现。
try 块: 在 try 块中编写可能会引发异常的代码。try 块是异常处理的起点,用于包裹可能出现异常的代码段。
try 用于监听,其后可接零个或多个 catch 块,如果没有 catch 块,则必须跟一个 finally 块。
try {
// 可能会引发异常的代码块
} catch (异常类型1 变量名1) {
// 处理异常的代码块
} catch (异常类型2 变量名2) {
// 处理其他类型的异常
} finally {
// 最终执行的代码块
// 无论是否发生异常,都会执行
}
catch 块: 在 catch 块中处理 try 块中可能抛出的异常。catch 块可以有多个,用于捕获不同类型的异常。
//捕获特定类型的异常:
try {
// 可能会引发异常的代码块
} catch (IOException e) {
// 处理 IOException 异常
} catch (NullPointerException e) {
// 处理 NullPointerException 异常
}
//捕获通用异常(Exception 类的子类):
try {
// 可能会引发异常的代码块
} catch (Exception e) {
// 处理所有类型的异常
}
finally 块: finally 块中的代码无论是否发生异常都会执行,用于执行一些必须要完成的操作,比如释放资源、关闭连接等。
try {
// 可能会引发异常的代码块
} catch (异常类型1 变量名1) {
// 处理异常的代码块
} finally {
// 最终执行的代码块
// 无论是否发生异常,都会执行
}
异常处理机制的工作流程
- 当程序执行到 try 块中的代码时,如果发生异常,则会立即跳转到与异常类型匹配的 catch 块中执行对应的处理代码。
- 如果发生异常但没有匹配到对应的 catch 块,则异常会沿着方法调用链向上抛出,直到找到匹配的 catch 块或者到达方法的最外层(main 方法)。
- 如果在 try 块中没有发生异常,则会跳过 catch 块,直接执行 finally 块中的代码,然后继续执行 try-catch-finally 结构之后的代码。
注意:
不要在 finally 语句块中使用 return! 当 try 语句和 finally 语句中都有 return 语句时,try 语句块中的 return 语句会被忽略。这是因为 try 语句中的 return 返回值会先被暂存在一个本地变量中,当执行到 finally 语句中的 return 之后,这个本地变量的值就变为了 finally 语句中的 return 返回值。
try-catch-finally 中哪个部分可以省略
在使用 try-catch-finally
块时,可以省略 catch
或 finally
中的任何一个部分,但不能同时省略它们。这是因为 try 块中必须至少有一个 catch 块或一个 finally 块,用于处理可能出现的异常或资源释放。
如果省略 catch 块,则表示将异常传播到调用方处理,而如果省略 finally 块,则表示不需要执行任何清理代码,例如关闭文件或数据库连接等。
需要注意的是,try-catch 块是合法的,即省略了 finally 块。这种情况下,如果出现异常,catch 块会处理异常,并且程序将继续执行 try-catch 块之后的代码。如果没有异常,则程序也将继续执行 try-catch 块之后的代码。
异常的声明
异常的声明主要涉及两个方面:方法声明可能会抛出的异常和自定义异常类的声明。
1、方法声明可能会抛出的异常: 在方法声明中可以使用 throws 关键字来声明方法可能会抛出的异常,需要在调用时进行异常处理或者继续向上抛出异常。
public void myMethod() throws IOException, SQLException {
// 可能会引发 IOException 或 SQLException 的代码块
}
在上面的例子中,myMethod() 方法声明可能会抛出 IOException 和 SQLException 两种异常,调用者在调用这个方法时需要对这些异常进行处理或者继续向上抛出。
2、自定义异常类的声明:自定义异常类的声明主要包括定义异常类的结构和功能,以及异常的使用方式。自定义异常类一般需要继承自 Exception 类或其子类,并根据实际需求添加合适的构造方法和其他方法。
public class MyCustomException extends Exception {
public MyCustomException(String message) {
super(message);
}
}
在这个例子中,声明了一个名为 MyCustomException 的自定义异常类,它继承自 Exception 类,并提供了一个带有异常信息的构造方法。
异常的抛出
抛出异常的目的是告诉调用者或者上层代码发生了异常,并且中断当前的执行流程,让异常处理机制来处理这个异常。通常通过 throw 关键字来实现。
如果代码可能会引发某种错误,可以创建一个合适的异常类实例并抛出它。
public static double method(int value) {
if(value == 0) {
throw new ArithmeticException("参数不能为0"); //抛出一个运行时异常
}
return 5.0 / value;
}
有时我们会从 catch 中抛出一个异常,目的是为了改变异常的类型。多用于在多系统集成时,当某个子系统故障,异常类型可能有多种,可以用统一的异常类型向外暴露,不需暴露太多内部异常细节。
private static void readFile(String filePath) throws MyException {
try {
// code
} catch (IOException e) {
MyException ex = new MyException("read file failed.");
ex.initCause(e);
throw ex;
}
}
习惯上,定义一个异常类应包含两个构造函数,一个无参构造函数和一个带有详细描述信息的构造函数(Throwable 的 toString 方法会打印这些详细信息,调试时很有用), 比如下面用到的自定义MyException:
public class MyException extends Exception {
public MyException(){ }
public MyException(String msg){
super(msg);
}
// ...
}
抛出异常的场景
- 检测到错误情况: 当程序检测到某些错误情况时,可以抛出异常来中断程序的执行,并提供错误信息以便后续处理。
- 业务逻辑异常: 在业务逻辑中,某些特定情况可能需要抛出异常来通知调用者或者上层代码。
- 异常转换: 在某些情况下,需要将底层异常转换成更高层次的异常抛出,以便更好地管理和处理异常。
在使用 throw 抛出异常时,需要注意以下几点:
- 抛出的异常应该是合理的,能够准确描述错误的情况,并提供有用的异常信息。
- 抛出的异常应该被上层代码捕获和处理,以避免未捕获的异常导致程序崩溃。
- 自定义异常类时,可以继承自 Java 的 Exception 或其子类,或者直接实现 Throwable 接口来定义异常类。
什么是异常链(Exception Chaining)?如何使用异常链?
异常链是指在捕获和处理异常时,将当前异常和导致当前异常的原因异常(根异常)链接起来,形成一个异常链。这种链式结构可以帮助调试和追踪异常的来源,提供更全面的异常信息,方便进行故障排查和修复。
在 Java 中,可以通过在捕获异常时使用 initCause() 方法或者在抛出新异常时将原始异常作为参数传递来创建异常链。
如何自定义异常类?
要自定义异常类,在 Java 中通常需要继承自 Exception 类或其子类,并提供合适的构造方法和其他必要的成员方法。步骤如下:
1、创建异常类: 创建一个新的 Java 类,并继承自 Exception 类或其子类。
public class MyCustomException extends Exception {
// 可以在这里添加异常类的成员变量和方法
}
2、添加构造方法:在自定义异常类中添加构造方法,通常需要至少提供一个带有异常信息的构造方法。
public class MyCustomException extends Exception {
// 构造方法,传入异常信息
public MyCustomException(String message) {
super(message);
}
}
3、添加其他方法(可选): 根据需要,可以在自定义异常类中添加其他方法或成员变量,以满足特定的异常处理需求。
public class MyCustomException extends Exception {
private int errorCode;
// 构造方法,传入异常信息和错误代码
public MyCustomException(String message, int errorCode) {
super(message);
this.errorCode = errorCode;
}
// 获取错误代码的方法
public int getErrorCode() {
return errorCode;
}
}
4、在代码中使用自定义异常类: 在需要抛出异常的地方,使用 throw 关键字抛出自定义异常对象。
public class MyClass {
public void myMethod() throws MyCustomException {
// 某些条件满足时抛出自定义异常
if (someCondition) {
throw new MyCustomException("发生了自定义异常", 500);
}
}
}
使用自定义异常类时,需要注意:
- 异常类通常需要提供带有异常信息的构造方法,以便在抛出异常时传递描述性的异常信息。
- 可以根据需要添加其他方法或成员变量,来扩展自定义异常类的功能。
- 在代码中抛出自定义异常时,需要使用 throw 关键字并创建异常对象。
- 在捕获自定义异常时,可以根据异常类型来捕获并处理异常,以及获取异常对象中的信息。