41.说说你平时是怎么处理 Java 异常的
1. 捕获和处理异常(try-catch)
使用 try-catch 块来捕获可能抛出的异常。为了提高代码的可维护性,通常会捕获特定的异常类型,而不是使用通用的 Exception。
捕获异常时要根据实际情况决定是否处理异常,或者将其重新抛出。
try {
// 可能抛出异常的代码
} catch (SpecificException e) {
// 处理特定的异常
} catch (AnotherSpecificException e) {
// 处理其他特定异常
}
2. 使用多个 catch 块
根据不同的异常类型,使用多个 catch 块来分别处理不同的异常。这样做有助于清晰地了解每个异常的处理方式。
try {
// 可能抛出异常的代码
} catch (IOException e) {
// 处理文件读取异常
} catch (SQLException e) {
// 处理数据库连接异常
} catch (Exception e) {
// 处理其他异常
}
3. 不要过度捕获异常
过度捕获异常(例如只使用 catch (Exception e))会掩盖潜在的问题,因此要尽量捕获特定的异常类型。
如果不打算处理异常,可以直接抛出或记录日志,但不要只是空处理。
try {
// 可能抛出异常的代码
} catch (SpecificException e) {
// 记录异常或重新抛出
throw e; // 如果不打算处理,可以将异常重新抛出
}
4. 使用 finally 释放资源
在 finally 块中释放资源,如关闭文件、数据库连接等。finally 块无论是否发生异常都会执行。
FileReader fr = null;
try {
fr = new FileReader("somefile.txt");
// 处理文件
} catch (IOException e) {
// 处理文件读取异常
} finally {
// 释放资源
if (fr != null) {
try {
fr.close();
} catch (IOException e) {
// 处理关闭文件时可能发生的异常
}
}
}
5. 异常链
在某些情况下,捕获到的异常可能需要被包装成另一个异常再抛出,形成一个异常链。这样做有助于保留原始异常的堆栈信息。
try {
// 可能抛出异常的代码
} catch (IOException e) {
throw new CustomException("Custom error message", e); // 将原始异常包装成新的异常
}
6. 自定义异常
有时需要创建自定义异常,以便更好地表达特定的错误场景。自定义异常通常是继承 Exception 或 RuntimeException 类。
class MyCustomException extends Exception {
public MyCustomException(String message) {
super(message);
}
}
try {
// 业务逻辑
throw new MyCustomException("Something went wrong");
} catch (MyCustomException e) {
// 处理自定义异常
System.out.println(e.getMessage());
}
7. 日志记录
异常应该记录到日志中,以便后期排查和分析。日志记录通常使用日志框架,如 SLF4J、Log4j 或 java.util.logging。
try {
// 可能抛出异常的代码
} catch (Exception e) {
logger.error("An error occurred: ", e);
}
8. 避免异常控制流
异常不应当被用作正常的控制流。比如,不应该使用异常来控制程序流程或跳过某些操作。异常应该用于捕获异常的情况而非普通流程。
if (someCondition) {
// 做某事
} else {
// 处理错误逻辑,避免使用异常控制流
}
9. 异常的语义和层次结构
在不同的层次结构中,应该有明确的异常分类。例如,数据库层应该抛出 SQLException,服务层抛出 ServiceException,而 UI 层则抛出 UIException。
42. HashMap 的长度为什么是 2 的 N 次方呢?
为了能让 HashMap 存数据和取数据的效率高,尽可能地减少 hash 值的碰撞,也就是说尽量把数据能均匀的分配,每个链表或者红黑树长度尽量相等。
我们首先可能会想到 % 取模的操作来实现。
下面是回答的重点哟:
取余(%)操作中如果除数是 2 的幂次,则等价于与其除数减一的与(&)操作(也就是说 hash % length == hash
&(length - 1) 的前提是 length 是 2 的 n 次方)。并且,采用二进 制位操作 & ,相对于 %
能够提高运算效率。
这就是为什么 HashMap 的长度需要 2 的 N 次方了。
43. HashMap 与 ConcurrentHashMap 的异同
- 都是 key-value 形式的存储数据;
- HashMap 是线程不安全的,ConcurrentHashMap 是 JUC 下的线程安全的;
- HashMap 底层数据结构是数组 + 链表(JDK 1.8 之前)。JDK 1.8 之后是数组 + 链表 + 红黑
树。当链表中元素个数达到 8 的时候,链表的查询速度不如红黑树快,链表会转为红黑树,红黑树查询速度快; - HashMap 初始数组大小为 16(默认),当出现扩容的时候,以 0.75 *数组大小的方式进行扩容;
- ConcurrentHashMap 在 JDK 1.8 之前是采用分段锁来现实的 Segment + HashEntry ,Segment 数组大小默认是 16 ,2 的 n 次方;JDK 1.8 之后,采用 Node + CAS + Synchronized 来保证并发安全进行实现。
44.说说深拷贝和浅拷贝?
- 浅拷贝(shallowCopy)只是增加了一个指针指向已存在的内存地址。
- 深拷贝(deepCopy)是增加了一个指针并且申请了一个新的内存,使这个增加的指针指向这个新的内存。
- 使用深拷贝的情况下,释放内存的时候不会因为出现浅拷贝时释放同一个内存的错误。
- 最好是结合克隆已经原型模式联系在一起哈,记得复习的时候,把这几个联系起来的。
45.红黑树有哪几个特征?
红黑树的特征如下:
1. 节点的颜色
每个节点要么是红色,要么是黑色。
2. 根节点是黑色
红黑树的根节点必须是黑色。
3. 红色节点的子节点是黑色(无两个红色节点相邻)
红黑树不能有两个连续的红色节点,也就是说,任何红色节点的两个子节点必须是黑色。这保证了树的平衡。
4. 从任何节点到其叶子节点的路径上的黑色节点数相同(黑色高度相同)
从任意节点到其所有叶子节点的路径上,经过的黑色节点的数量必须相同。这个属性称为黑色高度一致性。
5. 叶子节点是黑色的(虚拟的叶子节点)
红黑树中的叶子节点是黑色的,并且叶子节点并不存储数据,通常被视作虚拟节点(有时也称作空节点或NIL节点)。
6. 每个节点的黑色高度相同
从任意节点到其所有叶子节点的路径上,经过的黑色节点数是相同的,这也意味着树的所有路径从根到叶子的黑色节点数是一样的。
总结:
红黑树的这些特征(特别是红色节点不能连续和黑色高度一致性)保证了树的平衡性,从而能够在最坏情况下保持 (O(log n)) 的操作时间复杂度。
Java篇结束,接下来开始的是JVM篇,敬请期待…