Java集合使用中的常见错误及避免方法
立即解锁
发布时间: 2025-08-17 02:05:37 阅读量: 12 订阅数: 19 


Java编程中常见的100个错误及预防方法
# Java 集合使用中的常见错误及避免方法
## 1. 依赖 HashMap 和 HashSet 的遍历顺序
### 1.1 问题描述
在 Java 中,有多种方式可以获取集合的完整内容,如使用 for - each 循环迭代、调用 stream() 方法、将其转换为数组或调用 toString() 方法。在许多集合中,元素的顺序是有规定的,例如 TreeSet 的顺序可以是自然顺序或由比较器指定,LinkedHashSet 的顺序可以是插入顺序或访问顺序。然而,像 HashSet 或 HashMap 的键,其顺序是故意不指定的,这使得 Java 标准库的开发者能够更高效地实现这些集合。
但有时开发者会忘记这一点,或者根本没有考虑到。例如,将 HashSet 的元素打印在用户界面上时,由于元素以完全随机的顺序显示,用户很难找到他们要找的项。如果程序只使用少量数据进行测试,且三四个元素的顺序看起来合乎逻辑,这个问题可能就不会被注意到。
有趣的是,如果 HashSet 中的元素是小整数,它们可能看起来是有序的。以下代码可以创建一个包含 0 到 99 的 HashSet 并打印:
```java
import java.util.HashSet;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
public class Main {
public static void main(String[] args) {
HashSet<Integer> set = IntStream.range(0, 100).boxed()
.collect(Collectors.toCollection(HashSet::new));
System.out.println(set);
}
}
```
使用标准的 OpenJDK 实现的 HashSet,你可能会观察到数字按顺序递增。但这只是巧合,取决于实现细节。如果稍微改变数据,比如将每个数字乘以 10,数字就不再有序了:
```java
import java.util.HashSet;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
public class Main {
public static void main(String[] args) {
HashSet<Integer> set = IntStream.range(0, 100).mapToObj(x -> x * 10)
.collect(Collectors.toCollection(HashSet::new));
System.out.println(set);
}
}
```
此外,开发者有时会在测试中依赖 HashSet 的遍历顺序。例如,假设要测试一个返回 `Set<String>` 的 `getSeasons()` 方法,为了简化比较,一些开发者可能会将方法的输出转换为字符串,运行一次并将结果记录到预期字符串中:
```java
import org.junit.jupiter.api.Test;
import java.util.Set;
import static org.junit.jupiter.api.Assertions.assertEquals;
public class TestClass {
@Test
public void testGetSeasons() {
String result = getSeasons().toString();
assertEquals("[Winter, Autumn, Summer, Spring]", result);
}
private Set<String> getSeasons() {
// 实现逻辑
return null;
}
}
```
如果 `getSeasons()` 返回的是 HashSet,这个测试就会变得很脆弱,因为它依赖于 HashSet 的特定实现。虽然迭代顺序的改变很少发生,但仍然是可能的。Java 8 就引入了这样的改变,许多开发者在升级时不得不重写他们的测试。
如果 `getSeasons()` 方法使用 `Set.of()` 不可变集合工厂方法,情况会更有趣。这个集合也不保证任何顺序,为了防止开发者依赖不可靠的顺序,每次重启虚拟机时它的顺序都会随机化。所以如果运行一次并记录结果,下次测试很可能会失败,因为顺序会不同。`Map.of()` 方法返回的映射中的条目顺序也是如此。
### 1.2 避免方法
- 迭代集合时,问问自己该集合是否有顺序,以及顺序是否重要。如果要在用户界面中显示元素,那么顺序肯定很重要,此时应该对元素进行排序。
- 在测试中断言集合或映射相等时,直接比较映射或集合,这样顺序会被忽略。例如,上面的测试可以重写为:
```java
import org.junit.jupiter.api.Test;
import java.util.Set;
import static org.junit.jupiter.api.Assertions.assertEquals;
public class TestClass {
@Test
public void testGetSeasons() {
assertEquals(Set.of("Winter", "Spring", "Summer", "Autumn"), getSeasons());
}
private Set<String> getSeasons() {
// 实现逻辑
return null;
}
}
```
## 2. 迭代期间的并发修改
### 2.1 问题描述
通常,非并发的 Java 集合禁止在迭代期间进行并发修改,除非通过用于迭代的迭代器进行修改。当检测到这种非法修改时,通常会在运行时抛出 `ConcurrentModificationException`。例如,以下代码会抛出异常:
```java
import java.util.ArrayList;
import java.util.List;
public class Main {
public static void main(String[] args) {
List<String> list = new ArrayList<>(List.of("a", "b", "c"));
for (String s : list) {
System.out.println(s);
if (s.equals("b")) {
list.add("x");
}
}
}
}
```
实际上,增强的 for - each 循环是迭代器循环的语法糖,上述代码等价于:
```java
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
public class Main {
public static void main(String[] args) {
List<String> list = new ArrayList<>(List.of("a", "b", "c"));
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
String s = iterator.next();
System.out.println(s);
if (s.equals("b")) {
list.add("x");
}
}
}
}
```
为了检测并发修改,标准 Java 集合会维护一个 `modCount` 整数字段,每次对集合进行修改时该字段都会递增。创建迭代器时,会将当前的 `modCount` 值复制到迭代器实例中。每次调用 `next()` 方法时,会检查迭代器内部的 `modCount` 副本是否仍然等于原始集合中的 `modCount` 值,如果检测到差异则抛出异常。
需要注意的是,使用索引循环遍历列表不会抛出异常,因为没有使用迭代器。例如,以下循环会成功终止,并打印 "a"、"b"、"c" 和 "x":
```java
import java.util.ArrayList;
import java.util.List;
public class Main {
public static void main(String[] args) {
List<String> list = new ArrayList<>(List.of("a", "b", "c"));
for (int i = 0; i < list.size(); i++) {
String s = list.get(i);
System.out.println(s);
if (s.equals("b")) {
list.add("x");
}
}
}
}
```
虽然通常建议尽可能使用 for - each 循环,但在这种情况下,索引循环可以成功终止,而 for - each 循环会抛出异常。在实际代码中,并发修改可能更复杂,例如集合可能在循环内调用的另一个方法中被修改。盲目地将所有索引循环替换为 for - each 循环可能会破坏程序。
在某些情况下,即使使用迭代器,也不会抛出 `ConcurrentModificationException`。例如,以下代码不会抛出异常:
```java
import java.util.ArrayList;
import java.util.List;
public class Main {
public static void main(String[] args) {
List<String> list = new ArrayList<>(List.of("a", "b", "c"));
for (String s : list) {
System.out.println(s);
if (s.equals("b")) {
list.remove("a");
}
}
}
}
```
该代码会打印 "a" 和 "b",然后结束,不会打印 "c"。这是因为 `hasNext()` 方法只是检查处理的元素数量是否达到列表大小,而只有 `next()` 方法会检查 `modCount` 相等性。当在第二次迭代后移除元素时,列表大小减为 2,等于迭代次数,所以循环会正常结束。这种行为没有明确规定,未来的 Java 版本或其他集合实现可能会抛出异常,因此不能总是依赖 `ConcurrentModificationException` 被抛出。
### 2.2 避免方法
- 将 `ConcurrentModificationException` 视为朋友,而不是敌人。如果发生这种异常,说明程序中存在 bug,应该解决问题,而不是忽略
0
0
复制全文
相关推荐










