Java 编程细节:提升代码质量的关键注意事项
在 Java 开发中,一些看似微小的细节往往决定了代码的健壮性、可读性和性能。本文总结了日常编程中容易忽略的关键细节,涵盖方法重写、对象比较、异常处理等核心场景,帮助开发者写出更可靠的代码。
重写 equals 与 hashCode 的注意事项
equals 和 hashCode 是 Object 类的核心方法,用于对象比较和哈希表操作,重写时需严格遵循规范,否则可能导致集合框架(如 HashMap、HashSet)行为异常。
重写 equals 时必须使用 @Override 注解
错误示例:误将参数类型改为具体类(如User),导致实际是重载而非重写。
1
2
3
4// 错误:这是重载(参数类型为 User),而非重写 Object 的 equals
public boolean equals(User other) {
return this.id == other.id;
}正确做法:参数类型必须为Object,并添加@Override注解,让编译器校验正确性。
1
2
3
4
5
6
7// 强制编译器检查是否正确重写
public boolean equals(Object obj) {
if (this == obj) return true; // 自身比较
if (obj == null || getClass() != obj.getClass()) return false; // 类型不一致
User other = (User) obj; // 强制转型
return this.id == other.id; // 核心比较逻辑
}
重写 equals 必须同时重写 hashCode
规范要求:
Object类文档明确规定:若两个对象通过
equals比较相等,则它们的hashCode必须返回相同的值。
若两个对象hashCode不同,则equals一定返回false。后果:若仅重写
equals而忽略hashCode,会导致对象在哈希集合(如HashSet)中无法正确去重,或在HashMap中作为键时无法被正确查找。正确示例:
1
2
3
4
5
6
7
8
9
10
public boolean equals(Object obj) {
// 省略 equals 实现...
}
public int hashCode() {
// 确保 equals 相等的对象,hashCode 也相等
return Objects.hash(id, name); // 推荐使用 Objects.hash 组合字段
}最佳实践:
- 参与
equals比较的字段(如id、name),必须全部纳入hashCode计算。 - 避免使用可变字段作为
hashCode计算依据(对象修改后哈希值变化,导致集合操作异常)。
- 参与
字符串处理的细节
字符串是 Java 中最常用的数据类型,其不可变性和常量池特性需特别注意。
避免频繁创建字符串对象
问题:
String是不可变对象,每次拼接(+)都会创建新对象,性能低下。优化:
- 少量拼接可用
StringBuilder(非线程安全)或StringBuffer(线程安全)。 - 编译期确定的字符串常量,JVM 会自动优化(如
String s = "a" + "b"等价于"ab")。
1
2
3
4
5
6
7
8
9
10
11
12// 低效:循环中使用 + 拼接
String result = "";
for (int i = 0; i < 1000; i++) {
result += i; // 每次创建新 String 对象
}
// 高效:使用 StringBuilder
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 1000; i++) {
sb.append(i); // 仅修改内部字符数组
}
String result = sb.toString();- 少量拼接可用
字符串比较用 equals,而非 ==
==:比较对象的内存地址(是否为同一实例)。equals:比较字符串内容(需非null调用,否则抛NullPointerException)。1
2
3
4
5
6
7
8String s1 = "hello";
String s2 = new String("hello"); // 新建对象,内存地址不同
System.out.println(s1 == s2); // false(地址不同)
System.out.println(s1.equals(s2)); // true(内容相同)
// 安全比较(避免 null 问题)
System.out.println(Objects.equals(s1, s2)); // true(内部处理 null 逻辑)
异常处理的规范
异常处理不仅是错误捕获,更关乎代码的可维护性和问题排查效率。
避免捕获 Exception 或 Throwable
问题:大范围捕获异常会掩盖真正的错误(如
NullPointerException、OutOfMemoryError),导致调试困难。正确做法:捕获具体异常(如
IOException、SQLException),仅处理已知且可恢复的错误。1
2
3
4
5
6
7
8
9
10
11
12
13
14// 错误:捕获所有异常,无法区分业务异常和系统错误
try {
// 业务逻辑
} catch (Exception e) {
e.printStackTrace();
}
// 正确:捕获具体异常
try {
Files.readAllBytes(Paths.get("file.txt"));
} catch (IOException e) { // 仅处理 IO 相关错误
log.error("文件读取失败", e); // 详细日志,而非仅打印堆栈
throw new BusinessException("文件不存在", e); // 包装为业务异常
}
不要忽略异常(空 catch 块)
问题:空
catch块会导致错误被隐藏,后续排查时无法追溯原因。正确做法:至少记录日志,或根据业务逻辑抛出合理异常。
1
2
3
4
5
6
7
8
9
10
11
12
13// 错误:异常被忽略,问题无法排查
try {
// 可能抛出异常的代码
} catch (IOException e) {
// 空块
}
// 正确:记录日志
try {
// 可能抛出异常的代码
} catch (IOException e) {
log.warn("非关键操作失败,不影响主流程", e); // 记录警告日志
}
集合操作的细节
集合是 Java 编程的基础,不当使用会导致性能问题或逻辑错误。
遍历集合时避免修改结构
问题:使用
for-each或迭代器遍历集合时,若直接调用add/remove修改结构,会抛出ConcurrentModificationException。解决方案:
- 使用迭代器的
remove方法(仅Iterator支持,ListIterator还支持add)。 - 遍历副本,修改原集合。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17List<String> list = new ArrayList<>(Arrays.asList("a", "b", "c"));
// 错误:for-each 中修改集合
for (String s : list) {
if (s.equals("b")) {
list.remove(s); // 抛 ConcurrentModificationException
}
}
// 正确:使用迭代器
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
String s = iterator.next();
if (s.equals("b")) {
iterator.remove(); // 安全删除
}
}- 使用迭代器的
初始化集合时指定初始容量
问题:
ArrayList、HashMap等集合默认容量较小(如ArrayList初始容量为 10),频繁扩容会导致数组复制,影响性能。优化:根据预估大小设置初始容量,减少扩容次数。
1
2
3// 已知需存储 1000 个元素,指定初始容量
List<String> list = new ArrayList<>(1000); // 避免多次扩容
Map<String, Integer> map = new HashMap<>(2000); // HashMap 需考虑负载因子(默认 0.75)
其他关键细节
1. 包装类的自动装箱与拆箱
注意:包装类(
Integer、Long等)的==比较的是对象地址,而非值(除非是-128~127范围内的Integer,JVM 会缓存)。推荐:使用
equals比较值,或通过intValue()转为基本类型后比较。1
2
3
4
5
6
7
8Integer a = 127;
Integer b = 127;
System.out.println(a == b); // true(缓存范围内)
Integer c = 128;
Integer d = 128;
System.out.println(c == d); // false(超出缓存范围)
System.out.println(c.equals(d)); // true(比较值)
2. 静态常量与枚举
常量:使用
static final定义常量,避免魔法值(如if (type == 1)应改为if (type == OrderType.NORMAL))。枚举:多常量场景(如状态、类型)优先使用
enum,而非整数或字符串,增强代码可读性和类型安全。1
2
3
4
5
6
7
8
9// 推荐:使用枚举
public enum OrderStatus {
PENDING, PAID, SHIPPED, DELIVERED
}
// 使用
if (order.getStatus() == OrderStatus.PAID) {
// 处理已支付订单
}
3. 资源关闭
必须:文件流、数据库连接、网络连接等资源需手动关闭,推荐使用 try-with-resources 自动管理(Java 7+)。
1
2
3
4
5
6
7
8
9
10// 正确:自动关闭资源
try (FileInputStream fis = new FileInputStream("file.txt");
BufferedReader br = new BufferedReader(new InputStreamReader(fis))) {
String line;
while ((line = br.readLine()) != null) {
// 处理内容
}
} catch (IOException e) {
log.error("文件处理失败", e);
}
v1.3.10