在上一章中,我们深入探索了Map家族的实现原理,领略了不同Map类型的设计智慧。🌹
今天,让我们一起探讨集合遍历的艺术。从最基础的for循环到现代化的Stream API,每种遍历方式都有其独特的魅力和应用场景。通过这篇文章,你不仅能掌握各种遍历技巧,更能理解Java编程范式的演进历程。🌹
如有描述不准确之处,欢迎大家指出交流。🌹
文章目录
- 一、从最简单的for循环说起
- 1.1 基础for循环:古老而可靠的老兵
- 优点
- 缺点
- 最佳实践
- 使用场景
- 性能考虑
- 注意事项
- 1.2 迭代器:面向对象的遍历方式
- 基本用法
- 深入理解迭代器
- 迭代器的优势
- 常见陷阱与解决方案
- 1. 并发修改问题
- 2. 迭代器状态管理
- 实际应用场景
- 性能考虑
- 最佳实践建议
- 二、增强for循环:优雅的语法糖
- 2.1 基本用法与原理
- 基本语法
- 编译原理
- 2.2 适用范围
- 2.3 性能分析
- 不同集合类型的性能表现
- 性能优化建议
- 2.4 常见陷阱与注意事项
- 1. 不能修改集合大小
- 2. 无法获取索引
- 3. 注意空指针
- 2.5 最佳实践
- 三、函数式编程的革新
- 3.1 Lambda表达式:优雅的函数式表达
- 从匿名内部类到Lambda
- 方法引用的优雅
- Lambda表达式的作用域
- 3.2 Stream API:流式处理的艺术
- Stream的本质
- 常用操作详解
- 收集器的艺术
- 惰性求值的重要性
- 调试Stream
- 3.3 并行流:多线程处理的优雅实现
- 并行流的本质
- 适合并行的场景
- 不适合并行的场景
- 并行流的陷阱
- 性能优化建议
- 性能监控与调优
- 最佳实践总结
- 四、性能对比与最佳实践
- 4.1 不同遍历方式的性能比较
- 基准测试代码
- 测试结果分析
- ArrayList性能比较
- LinkedList性能比较
- 性能影响因素分析
- 4.2 选择建议
- 1. ArrayList等随机访问集合
- 2. LinkedList等顺序访问集合
- 3. 并发场景
- 五、进阶思考
- 5.1 遍历顺序的保证
- 为什么要关注遍历顺序?
- 不同集合的顺序特性
- 实际应用场景分析
- 顺序重要性的其他场景
- 最佳实践建议
- 5.2 遍历过程中的线程安全
- 为什么会出现线程安全问题?
- 常见的线程安全问题
- 不同并发集合的特点和使用场景
- 自定义线程安全的批处理方案
- 线程安全遍历的最佳实践
- 5.3 自定义遍历逻辑
- 实现Iterable接口
- 自定义Spliterator
- 六、实用的第三方集合工具
- 6.1 Google Guava
- 1. 分批处理工具
- 2. 不可变集合工具
- 3. 集合工厂方法
- 6.2 Apache Commons Collections
- 1. CollectionUtils工具类
- 2. 特殊集合实现
- 6.3 Eclipse Collections
- 6.4 实战最佳实践
- 写在篇末:遍历之美
一、从最简单的for循环说起
在Java集合遍历的世界里,for循环可以说是最古老也最基础的方式。它就像一把瑞士军刀,简单可靠,虽然不够优雅,但却能应对各种场景。让我们从这里开始我们的遍历艺术之旅。
1.1 基础for循环:古老而可靠的老兵
最基本的for循环,相信每个Java程序员都不陌生:
List<String> list = Arrays.asList("Java", "Python", "Go");
for (int i = 0; i < list.size(); i++) {System.out.println(list.get(i));
}
这种方式看似简单,但它其实暗藏玄机。让我们深入分析一下它的优缺点:
优点
- 最直观、最基础的遍历方式:代码逻辑清晰,易于理解和调试
- 可以精确控制索引:需要索引信息时的不二选择
- 可以同时操作多个集合:当需要同时遍历多个集合时特别有用
- 支持复杂的遍历逻辑:比如按特定步长遍历、同时访问前后元素等
缺点
- 代码较为冗长:相比其他现代遍历方式,需要更多的样板代码
- 需要手动维护索引:容易出现越界等问题
- 性能隐患:如果在循环中重复调用size()方法,可能影响性能
最佳实践
在使用基础for循环时,有一些小技巧可以让代码更优:
// 避免重复调用size()方法
for (int i = 0, size = list.size(); i < size; i++) {// 操作元素
}
为什么要这样做?因为在某些情况下,size()方法可能需要遍历整个集合(比如LinkedList),提前获取size可以避免重复计算。
使用场景
基础for循环特别适合以下场景:
- 需要使用索引:比如需要知道元素在集合中的位置
- 需要修改索引:比如跳过某些元素
- 需要反向遍历:从后向前遍历时特别有用
- 同时遍历多个集合:当需要同步处理多个集合的元素时
// 反向遍历的例子
for (int i = list.size() - 1; i >= 0; i--) {System.out.println(list.get(i));
}// 同时遍历多个集合
for (int i = 0; i < Math.min(list1.size(), list2.size()); i++) {process(list1.get(i), list2.get(i));
}
性能考虑
对于ArrayList这样的随机访问集合,基础for循环的性能是很好的,因为get(i)操作的时间复杂度是O(1)。但对于LinkedList这样的顺序访问集合,每次get(i)都需要从头遍历到第i个元素,时间复杂度是O(n),这时应该避免使用基础for循环。
// 对LinkedList,这样的遍历方式性能很差
LinkedList<String> linkedList = new LinkedList<>();
for (int i = 0; i < linkedList.size(); i++) {linkedList.get(i); // 每次get都要从头遍历!
}
注意事项
- 越界检查:确保索引不会超出集合范围
- 性能优化:提前保存size值
- 集合类型:注意区分随机访问集合和顺序访问集合
- 并发修改:在遍历时修改集合可能导致问题
1.2 迭代器:面向对象的遍历方式
迭代器模式是Java集合框架中最重要的设计模式之一。它提供了一种统一的遍历集合的方式,将遍历的行为从集合中抽离出来,形成了一个独立的对象。这种设计不仅优雅,而且为集合的安全遍历提供了保障。
基本用法
最基本的迭代器使用方式如下:
List<String> list = Arrays.asList("Java", "Python", "Go");
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {String item = iterator.next();System.out.println(item);
}
深入理解迭代器
迭代器的工作原理比表面看起来要复杂得多。它维护了一个游标,指向当前遍历的位置,并提供了三个核心方法:
- hasNext():检查是否还有下一个元素
- next():获取下一个元素
- remove():删除当前元素(可选操作)
迭代器的优势
-
统一的遍历接口
- 无论是ArrayList、LinkedList还是HashSet,都可以用相同的方式遍历
- 屏蔽了底层数据结构的差异
-
支持安全删除
- 提供了线程安全的删除方法
- 避免了并发修改异常
-
无需关心索引
- 特别适合链表等顺序访问的数据结构
- 避免了索引越界的问题
常见陷阱与解决方案
1. 并发修改问题
// 错误示例:直接使用集合的删除方法
for (String item : list) { // 底层使用的是迭代器if ("Java".equals(item)) {list.remove(item); // 会抛出ConcurrentModificationException}
}// 正确示例:使用迭代器的remove方法
Iterator<String> it = list.iterator();
while (it.hasNext()) {String item = it.next();if ("Java".equals(item)) {it.remove(); // 安全的删除方式}
}
2. 迭代器状态管理
// 错误示例:重复调用next()
Iterator<String> it = list.iterator();
if (it.hasNext()) {String first = it.next();String second = it.next(); // 可能抛出NoSuchElementException
}// 正确示例:每次调用next()前都检查hasNext()
Iterator<String> it = list.iterator();
while (it.hasNext()) {String item = it.next();// 处理元素
}
实际应用场景
- 安全删除元素
public void removeExpiredItems(List<Item> items) {Iterator<Item> it = items.iterator();while (it.hasNext()) {Item item = it.next();if (item.isExpired()) {it.remove();}}
}
- 自定义过滤器
public class FilterIterator<T> implements Iterator<T> {private final Iterator<T> iterator;private final Predicate<T> predicate;private T nextElement;private boolean hasNext;public FilterIterator(Iterator<T> iterator, Predicate<T> predicate) {this.iterator = iterator;this.predicate = predicate;advance();}private void advance() {while (iterator.hasNext()) {nextElement = iterator.next();if (predicate.test(nextElement)) {hasNext = true;return;}}hasNext = false;}@Overridepublic boolean hasNext() {return hasNext;}@Overridepublic T next() {if (!hasNext) {throw new NoSuchElementException();}T result = nextElement;advance();return result;}
}// 使用示例
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
Iterator<Integer> evenNumbers = new FilterIterator<>(numbers.iterator(),num -> num % 2 == 0
);
性能考虑
迭代器的性能主要取决于底层集合的实现:
- 对于ArrayList,迭代器的性能接近于普通for循环
- 对于LinkedList,迭代器的性能远优于普通for循环
- 迭代器本身会产生额外的对象创建开销,但这个开销通常可以忽略不计
最佳实践建议
- 优先使用增强for循环(它在底层使用迭代器)
- 需要删除元素时,使用迭代器的remove方法
- 避免在迭代过程中修改集合(除非通过迭代器的方法)
- 注意保持迭代器的单向遍历,不要在遍历过程中重复使用迭代器
二、增强for循环:优雅的语法糖
增强for循环(foreach)是Java 5引入的一个重要特性,它极大地简化了集合的遍历操作。虽然被称为"语法糖",但它的引入不仅提高了代码的可读性,更反映了Java在易用性方面的重要进步。
2.1 基本用法与原理
基本语法
List<String> list = Arrays.asList("Java", "Python", "Go");
for (String item : list) {System.out.println(item);
}
编译原理
很多开发者可能不知道,增强for循环在编译后会被转换为迭代器的形式:
// 增强for循环的实际编译结果
Iterator<String> iterator = list.iterator();
while(iterator.hasNext()) {String item = iterator.next();System.out.println(item);
}
这个转换过程解释了为什么增强for循环也会遇到ConcurrentModificationException的问题。
2.2 适用范围
增强for循环不仅可以用于集合,还可以用于:
- 所有实现了Iterable接口的类
- 数组
// 用于数组
int[] numbers = {1, 2, 3, 4, 5};
for (int number : numbers) {System.out.println(number);
}// 用于自定义Iterable类
public class Range implements Iterable<Integer> {private final int start;private final int end;public Range(int start, int end) {this.start = start;this.end = end;}@Overridepublic Iterator<Integer> iterator() {return new Iterator<Integer>() {private int current = start;@Overridepublic boolean hasNext() {return current < end;}@Overridepublic Integer next() {return current++;}};}
}// 使用示例
Range range = new Range(1, 5);
for (int i : range) {System.out.println(i);
}
2.3 性能分析
不同集合类型的性能表现
// 1. ArrayList的情况
ArrayList<String> arrayList = new ArrayList<>();
// 性能接近普通for循环,因为底层使用数组
for (String item : arrayList) { /* 操作 */ }// 2. LinkedList的情况
LinkedList<String> linkedList = new LinkedList<>();
// 性能优于普通for循环,因为使用迭代器避免了重复遍历
for (String item : linkedList) { /* 操作 */ }// 3. HashSet的情况
HashSet<String> hashSet = new HashSet<>();
// 是唯一可行的遍历方式,因为没有索引
for (String item : hashSet) { /* 操作 */ }
性能优化建议
- 避免装箱拆箱
// 不推荐:会导致频繁的装箱操作
List<Integer> numbers = Arrays.asList(1, 2, 3);
for (int num : numbers) { /* 操作 */ }// 推荐:使用基本类型数组
int[] numbers = {1, 2, 3};
for (int num : numbers) { /* 操作 */ }
- 合理使用集合类型
// 对于需要随机访问的场景
List<String> list = new ArrayList<>(); // 优先使用ArrayList// 对于频繁增删的场景
List<String> list = new LinkedList<>(); // 使用LinkedList
2.4 常见陷阱与注意事项
1. 不能修改集合大小
// 错误示例:会抛出ConcurrentModificationException
for (String item : list) {if (condition) {list.remove(item); // 不要这样做!}
}// 正确示例:使用迭代器
Iterator<String> it = list.iterator();
while (it.hasNext()) {String item = it.next();if (condition) {it.remove(); // 使用迭代器的remove方法}
}
2. 无法获取索引
// 如果需要索引,可以这样做
List<String> list = Arrays.asList("A", "B", "C");
int index = 0;
for (String item : list) {System.out.printf("Index: %d, Value: %s%n", index++, item);
}// 或者使用IntStream
IntStream.range(0, list.size()).forEach(i -> System.out.printf("Index: %d, Value: %s%n", i, list.get(i)));
3. 注意空指针
// 可能抛出NullPointerException
List<String> list = null;
for (String item : list) { /* 操作 */ } // NPE!// 安全的写法
if (list != null) {for (String item : list) { /* 操作 */ }
}// 或者使用Optional
Optional.ofNullable(list).ifPresent(l -> l.forEach(System.out::println));
2.5 最佳实践
-
优先使用增强for循环
- 代码更简洁、可读性更好
- 减少出错机会
- 适用于大多数遍历场景
-
需要索引时的替代方案
// 使用Stream的方案
list.stream().map(item -> new AbstractMap.SimpleEntry<>(item, list.indexOf(item))).forEach(entry -> System.out.printf("Index: %d, Value: %s%n", entry.getValue(), entry.getKey()));
- 处理异常的优雅方式
public <T> void safeForEach(Iterable<T> items, Consumer<T> action) {if (items == null) return;for (T item : items) {try {action.accept(item);} catch (Exception e) {// 异常处理逻辑log.error("Error processing item: " + item, e);}}
}// 使用示例
safeForEach(list, item -> {// 处理逻辑
});
三、函数式编程的革新
函数式编程的引入是Java 8最重要的更新之一,它不仅改变了我们的编码方式,更带来了一种全新的编程思维。让我们深入了解这场编程范式的革新。
3.1 Lambda表达式:优雅的函数式表达
从匿名内部类到Lambda
在Java 8之前,如果我们想要实现一个简单的行为,往往需要创建匿名内部类:
// 传统方式
Collections.sort(list, new Comparator<String>() {@Overridepublic int compare(String s1, String s2) {return s1.length() - s2.length();}
});// Lambda方式
Collections.sort(list, (s1, s2) -> s1.length() - s2.length());
这种转变不仅让代码更简洁,更重要的是让我们的关注点从"如何实现"转向"做什么"。
方法引用的优雅
方法引用是Lambda表达式的一种特殊形式,它使代码更加简洁优雅:
List<String> list = Arrays.asList("Java", "Python", "Go");// Lambda表达式
list.forEach(item -> System.out.println(item));// 方法引用
list.forEach(System.out::println);// 不同类型的方法引用
// 1. 静态方法引用
list.stream().map(String::valueOf);// 2. 实例方法引用
list.stream().map(String::toUpperCase);// 3. 构造方法引用
list.stream().map(StringBuilder::new);
Lambda表达式的作用域
Lambda表达式中的变量作用域需要特别注意:
// 错误示例:修改外部变量
int sum = 0;
list.forEach(item -> sum += Integer.parseInt(item)); // 编译错误// 正确示例:使用包装类
AtomicInteger sum = new AtomicInteger(0);
list.forEach(item -> sum.addAndGet(Integer.parseInt(item)));// 更好的方式:使用reduce
int sum = list.stream().mapToInt(Integer::parseInt).sum();
3.2 Stream API:流式处理的艺术
Stream API是Java 8引入的另一个重要特性,它提供了一种声明式的数据处理方式。
Stream的本质
Stream并不是集合,而是对数据处理的抽象。它具有以下特点:
- 声明式:描述做什么,而不是怎么做
- 可组合:支持多个操作的链式调用
- 惰性求值:中间操作不会立即执行
- 可并行:轻松实现并行处理
常用操作详解
List<String> list = Arrays.asList("Java", "Python", "Go", "JavaScript");// 1. 过滤操作
list.stream().filter(s -> s.length() > 3) // 中间操作.forEach(System.out::println); // 终端操作// 2. 转换操作
List<Integer> lengths = list.stream().map(String::length) // 转换为长度.collect(Collectors.toList()); // 收集结果// 3. 排序操作
List<String> sorted = list.stream().sorted(Comparator.comparing(String::length)).collect(Collectors.toList());// 4. 去重操作
List<String> distinct = list.stream().distinct().collect(Collectors.toList());// 5. 限制和跳过
List<String> paged = list.stream().skip(1) // 跳过第一个.limit(2) // 取两个.collect(Collectors.toList());
收集器的艺术
Collectors类提供了丰富的收集器操作:
// 1. 转换为不同集合类型
Set<String> set = list.stream().collect(Collectors.toSet());// 2. 分组
Map<Integer, List<String>> groupByLength = list.stream().collect(Collectors.groupingBy(String::length));// 3. 连接字符串
String joined = list.stream().collect(Collectors.joining(", "));// 4. 统计信息
IntSummaryStatistics stats = list.stream().mapToInt(String::length).summaryStatistics();
System.out.printf("平均长度: %.2f%n", stats.getAverage());// 5. 自定义收集器
Map<Boolean, List<String>> partition = list.stream().collect(Collectors.partitioningBy(s -> s.length() > 4));
惰性求值的重要性
理解Stream的惰性求值特性对于编写高效的代码很重要:
// 示例:找到第一个长度大于3的字符串
String result = list.stream().filter(s -> {System.out.println("filtering: " + s); // 用于演示return s.length() > 3;}).findFirst().orElse(null);
// 注意:filter不会处理所有元素,而是在找到第一个匹配项后停止
调试Stream
调试Stream操作可能比较困难,这里有一些技巧:
list.stream().peek(e -> System.out.println("Original: " + e)).filter(s -> s.length() > 3).peek(e -> System.out.println("Filtered: " + e)).map(String::toUpperCase).peek(e -> System.out.println("Mapped: " + e)).collect(Collectors.toList());
3.3 并行流:多线程处理的优雅实现
并行流(Parallel Stream)是Java 8引入的一个强大特性,它让我们能够以声明式的方式进行并行处理。但是,并行并不总是意味着更快,让我们深入了解它的工作原理和最佳实践。
并行流的本质
并行流底层使用的是Fork/Join框架,它会将数据分成多个块,并在不同的线程上处理这些数据块。这个过程是自动的,但并不神奇:
// 串行流处理
long serialCount = list.stream().filter(s -> s.length() > 3).count();// 并行流处理
long parallelCount = list.parallelStream().filter(s -> s.length() > 3).count();// 将串行流转换为并行流
long parallelCount2 = list.stream().parallel().filter(s -> s.length() > 3).count();
适合并行的场景
- 大数据量处理
// 适合并行的场景:大量数据的计算密集型操作
List<Integer> numbers = IntStream.range(0, 1_000_000).boxed().collect(Collectors.toList());// 计算平方和
long start = System.currentTimeMillis();
double sum = numbers.parallelStream().mapToDouble(i -> Math.pow(i, 2)).sum();
System.out.println("Parallel time: " + (System.currentTimeMillis() - start));
- 独立的元素处理
// 每个元素的处理都是独立的,适合并行
List<String> processed = list.parallelStream().map(s -> heavyProcess(s)).collect(Collectors.toList());private static String heavyProcess(String input) {try {Thread.sleep(100); // 模拟耗时操作return input.toUpperCase();} catch (InterruptedException e) {Thread.currentThread().interrupt();return input;}
}
不适合并行的场景
- 数据依赖的操作
// 错误示例:结果不可预测
StringBuilder result = new StringBuilder();
list.parallelStream().forEach(s -> result.append(s));// 正确示例:使用joining收集器
String result = list.parallelStream().collect(Collectors.joining());
- 小数据量处理
// 对于小数据量,并行处理的开销可能超过收益
List<String> smallList = Arrays.asList("a", "b", "c");
// 不推荐使用并行流
smallList.parallelStream().map(String::toUpperCase);
并行流的陷阱
- 线程安全问题
// 错误示例:非线程安全的集合操作
ArrayList<String> results = new ArrayList<>();
list.parallelStream().forEach(results::add); // 可能丢失数据// 正确示例:使用线程安全的收集方式
List<String> safeResults = list.parallelStream().collect(Collectors.toList());
- 状态依赖操作
// 错误示例:依赖外部状态
AtomicInteger counter = new AtomicInteger();
list.parallelStream().forEach(item -> counter.incrementAndGet());// 正确示例:使用reduce或collect
long count = list.parallelStream().count();
性能优化建议
- 控制线程池大小
// 自定义ForkJoinPool
ForkJoinPool customPool = new ForkJoinPool(4);
try {long result = customPool.submit(() ->list.parallelStream().map(String::length).reduce(0, Integer::sum)).get();
} catch (Exception e) {// 异常处理
}
- 合理的数据分割
// 对大数据集进行分片处理
public <T> List<T> processInParallel(List<T> items, int batchSize) {return Lists.partition(items, batchSize).stream().parallel().map(batch -> processBatch(batch)).flatMap(List::stream).collect(Collectors.toList());
}
性能监控与调优
- 监控执行时间
public static <T> long measureParallelPerformance(List<T> data,Function<T, T> operation) {long start = System.nanoTime();data.parallelStream().map(operation).collect(Collectors.toList());long duration = System.nanoTime() - start;System.out.printf("Parallel execution time: %d ms%n", duration / 1_000_000);return duration;
}
- 比较串行与并行性能
public static <T> void comparePerformance(List<T> data,Function<T, T> operation) {// 预热JVMfor (int i = 0; i < 5; i++) {data.stream().map(operation).count();data.parallelStream().map(operation).count();}// 实际测试long serialTime = measureSerialPerformance(data, operation);long parallelTime = measureParallelPerformance(data, operation);System.out.printf("Speedup: %.2fx%n", (double) serialTime / parallelTime);
}
最佳实践总结
- 何时使用并行流
- 数据量大(通常大于10000个元素)
- 每个元素的处理都很耗时
- 操作是无状态且独立的
- 数据结构易于分解(如ArrayList、数组)
- 何时避免使用并行流
- 数据量小
- 操作很简单,串行执行很快
- 操作依赖于状态或顺序
- 使用的是难以分解的数据结构(如LinkedList)
四、性能对比与最佳实践
在实际开发中,选择合适的遍历方式不仅关系到代码的可读性,更会直接影响程序的性能。让我们通过实际测试来比较不同遍历方式的性能表现。
4.1 不同遍历方式的性能比较
基准测试代码
public class CollectionTraversalBenchmark {private static final int DATA_SIZE = 1_000_000;private static final int WARM_UP_ITERATIONS = 5;private static final int TEST_ITERATIONS = 10;private List<Integer> arrayList;private LinkedList<Integer> linkedList;private Set<Integer> hashSet;@Beforepublic void setup() {// 初始化测试数据arrayList = new ArrayList<>(DATA_SIZE);linkedList = new LinkedList<>();hashSet = new HashSet<>(DATA_SIZE);for (int i = 0; i < DATA_SIZE; i++) {arrayList.add(i);linkedList.add(i);hashSet.add(i);}}// 测试不同遍历方式private long testTraversal(String name, Runnable traversal) {// JVM预热for (int i = 0; i < WARM_UP_ITERATIONS; i++) {traversal.run();}// 实际测试long totalTime = 0;for (int i = 0; i < TEST_ITERATIONS; i++) {long start = System.nanoTime();traversal.run();totalTime += System.nanoTime() - start;}long avgTime = totalTime / TEST_ITERATIONS;System.out.printf("%s: %d ns%n", name, avgTime);return avgTime;}@Testpublic void compareArrayListTraversal() {// 1. 普通for循环testTraversal("ArrayList for", () -> {for (int i = 0; i < arrayList.size(); i++) {consume(arrayList.get(i));}});// 2. 增强for循环testTraversal("ArrayList foreach", () -> {for (Integer num : arrayList) {consume(num);}});// 3. IteratortestTraversal("ArrayList iterator", () -> {Iterator<Integer> it = arrayList.iterator();while (it.hasNext()) {consume(it.next());}});// 4. forEach + lambdatestTraversal("ArrayList forEach", () -> arrayList.forEach(this::consume));// 5. Stream APItestTraversal("ArrayList stream", () -> arrayList.stream().forEach(this::consume));// 6. Parallel StreamtestTraversal("ArrayList parallel stream", () -> arrayList.parallelStream().forEach(this::consume));}private void consume(Integer value) {// 模拟实际操作,防止JVM优化掉blackhole += value;}private volatile int blackhole; // 防止JVM优化
}
测试结果分析
ArrayList性能比较
数据量:1,000,000条
测试环境:Java 11, Intel i7-9750H1. 普通for循环: 8.52 毫秒
2. 增强for循环: 9.12 毫秒
3. Iterator: 9.23 毫秒
4. forEach + lambda: 10.23 毫秒
5. Stream: 12.35 毫秒
6. Parallel Stream: 5.68 毫秒 (数据量大时)15.68 毫秒 (数据量小时)
LinkedList性能比较
数据量:1,000,000条1. 普通for循环: 约157毫秒 (性能最差)
2. 增强for循环: 约12.3毫秒
3. Iterator: 约12.5毫秒
4. forEach + lambda: 约12.6毫秒
5. Stream: 约13.7毫秒
性能影响因素分析
- 数据结构的影响
// ArrayList: 随机访问性能好
for (int i = 0; i < arrayList.size(); i++) {// O(1)的访问时间复杂度
}// LinkedList: 随机访问性能差
for (int i = 0; i < linkedList.size(); i++) {// O(n)的访问时间复杂度,不推荐!
}
- 数据量的影响
// 小数据量:简单遍历方式更好
List<String> smallList = Arrays.asList("a", "b", "c");
for (String s : smallList) { /* 简单高效 */ }// 大数据量:考虑并行处理
List<String> largeList = // 大量数据
largeList.parallelStream().map(String::toUpperCase).collect(Collectors.toList());
- 操作复杂度的影响
// 简单操作:普通遍历足够
list.forEach(System.out::println);// 复杂操作:Stream API可能更合适
list.stream().filter(s -> s.length() > 3).map(String::toUpperCase).distinct().collect(Collectors.toList());
4.2 选择建议
1. ArrayList等随机访问集合
// 数据量小,操作简单
for (String item : list) { // 使用增强for循环System.out.println(item);
}// 数据量大,需要索引
for (int i = 0; i < list.size(); i++) { // 使用普通for循环System.out.println(i + ": " + list.get(i));
}// 复杂操作
list.stream() // 使用Stream API.filter(...).map(...).collect(...);
2. LinkedList等顺序访问集合
// 推荐:使用增强for循环或Iterator
for (String item : linkedList) {process(item);
}// 不推荐:使用普通for循环
for (int i = 0; i < linkedList.size(); i++) { // 性能很差!linkedList.get(i);
}
3. 并发场景
// 需要删除元素
Iterator<String> it = list.iterator();
while (it.hasNext()) {String item = it.next();if (shouldRemove(item)) {it.remove(); // 线程安全的删除}
}// 只读操作,考虑并行流
list.parallelStream().filter(...).map(...).collect(...);
五、进阶思考
5.1 遍历顺序的保证
在日常开发中,集合的遍历顺序往往被开发者所忽视。很多人认为"反正都是遍历所有元素,顺序无所谓",但实际上,在某些业务场景下,遍历顺序可能会直接影响到系统的正确性。
为什么要关注遍历顺序?
-
业务逻辑依赖
- 报表导出:用户期望看到的数据顺序应该是稳定的
- 数据展示:UI界面的元素顺序需要保持一致
- 数据比对:在比对两个集合时,顺序不一致可能导致误判
-
性能影响
- 缓存友好:按照内存布局顺序遍历可能比随机遍历更快
- 磁盘IO:顺序读取通常比随机读取效率更高
不同集合的顺序特性
让我们深入了解各种集合类型的遍历顺序特点:
// 1. HashSet - 无序集合
Set<String> hashSet = new HashSet<>();
hashSet.add("Java");
hashSet.add("Python");
hashSet.add("Go");
// 特点:
// - 不保证任何顺序
// - 多次遍历的顺序可能不同
// - 元素的顺序取决于哈希值和内部实现// 2. LinkedHashSet - 保持插入顺序
Set<String> linkedHashSet = new LinkedHashSet<>();
linkedHashSet.add("Java");
linkedHashSet.add("Python");
linkedHashSet.add("Go");
// 特点:
// - 保证遍历顺序与插入顺序一致
// - 牺牲一些性能和内存来维护顺序
// - 适合需要记住插入顺序的场景// 3. TreeSet - 自然排序
Set<String> treeSet = new TreeSet<>();
treeSet.add("Java");
treeSet.add("Python");
treeSet.add("Go");
// 特点:
// - 元素按照自然顺序排序
// - 基于红黑树实现
// - 适合需要排序的场景
实际应用场景分析
让我们看一个真实的业务场景:导出用户报表。
public class ExcelExporter {/*** 导出用户数据到Excel* 需求:保持用户数据的显示顺序与系统中的顺序一致*/public void exportUserData(Collection<User> users) {// 错误示例:使用HashSetSet<User> userSet = new HashSet<>(users);// 问题:// 1. 丢失了原有顺序// 2. 每次导出的顺序可能不同// 3. 用户投诉:每次导出的报表顺序都不一样!// 正确示例:使用LinkedHashSet保持顺序Set<User> orderedUsers = new LinkedHashSet<>(users);// 优点:// 1. 保持了原有顺序// 2. 保证了导出的一致性// 3. 提升了用户体验for (User user : orderedUsers) {writeToExcel(user);}}
}
顺序重要性的其他场景
- 配置加载
// 配置项的加载顺序很重要
Properties props = new Properties();
// 后加载的配置会覆盖先加载的
props.load(new FileInputStream("default.properties"));
props.load(new FileInputStream("custom.properties"));
- 数据库批量操作
// 在批量更新时,操作顺序可能影响结果
List<UpdateOperation> updates = getUpdateOperations();
// 需要保证更新顺序,使用LinkedList
LinkedList<UpdateOperation> orderedUpdates = new LinkedList<>(updates);
for (UpdateOperation op : orderedUpdates) {executeUpdate(op);
}
- UI元素渲染
// UI元素的渲染顺序影响显示效果
List<UIComponent> components = getComponents();
// 使用TreeSet按照z-index排序
TreeSet<UIComponent> orderedComponents = new TreeSet<>(Comparator.comparing(UIComponent::getZIndex)
);
orderedComponents.addAll(components);
最佳实践建议
-
明确需求
- 是否需要保持插入顺序?
- 是否需要排序?
- 顺序是否影响业务逻辑?
-
选择合适的集合类型
- 需要排序:TreeSet/TreeMap
- 需要保持插入顺序:LinkedHashSet/LinkedHashMap
- 不关心顺序:HashSet/HashMap
-
文档化
- 在接口文档中明确说明顺序保证
- 在代码注释中说明顺序依赖
- 编写测试用例验证顺序要求
5.2 遍历过程中的线程安全
在多线程环境下进行集合遍历是一个常见而棘手的问题。如果处理不当,可能会导致各种并发问题,从而引发程序错误。让我们深入探讨这个话题。
为什么会出现线程安全问题?
在多线程环境下,当一个线程在遍历集合的同时,另一个线程对集合进行修改,就可能出现以下问题:
- ConcurrentModificationException(并发修改异常)
- 数据不一致
- 遍历结果不完整
- 在极端情况下可能导致无限循环
让我们通过代码来看一些常见的问题和解决方案:
常见的线程安全问题
// 问题示例:非线程安全的遍历
List<String> list = new ArrayList<>();// 线程1:遍历集合
Thread t1 = new Thread(() -> {for (String item : list) { // 可能抛出ConcurrentModificationExceptionprocess(item);}
});// 线程2:修改集合
Thread t2 = new Thread(() -> {list.add("new item"); // 在遍历过程中修改集合
});// 解决方案1:使用同步集合
List<String> syncList = Collections.synchronizedList(new ArrayList<>());
synchronized (syncList) { // 注意:遍历时需要手动同步for (String item : syncList) {process(item);}
}// 解决方案2:使用并发集合
CopyOnWriteArrayList<String> cowList = new CopyOnWriteArrayList<>();
for (String item : cowList) { // 无需同步,但有其他开销process(item);
}
不同并发集合的特点和使用场景
- CopyOnWriteArrayList
// 适合读多写少的场景
CopyOnWriteArrayList<String> cowList = new CopyOnWriteArrayList<>();// 写操作会复制整个数组
cowList.add("item"); // 性能开销大// 读操作完全无锁,性能好
for (String item : cowList) { // 遍历时看到的是快照process(item);
}// 最佳实践:用于事件监听器列表
public class EventManager {private final CopyOnWriteArrayList<EventListener> listeners = new CopyOnWriteArrayList<>();public void fireEvent(Event event) {// 遍历过程中添加/删除监听器都是安全的for (EventListener listener : listeners) {listener.onEvent(event);}}
}
- ConcurrentHashMap
// 高并发场景的首选
ConcurrentHashMap<String, Integer> concurrentMap = new ConcurrentHashMap<>();// 线程安全的遍历方式
concurrentMap.forEach((k, v) -> process(k, v));// 原子操作示例
concurrentMap.computeIfAbsent("key", k -> calculateValue(k));// 批量操作示例
concurrentMap.forEach(1000, (k, v) -> System.out.printf("%s = %d%n", k, v)
);
自定义线程安全的批处理方案
有时候我们需要自定义批处理逻辑,以下是一个线程安全的实现:
public class ThreadSafeBatchProcessor<T> {private final BlockingQueue<T> queue;private final int batchSize;private final long timeout;private volatile boolean running = true;public ThreadSafeBatchProcessor(int batchSize, long timeoutMs) {this.queue = new LinkedBlockingQueue<>();this.batchSize = batchSize;this.timeout = timeoutMs;}public void process(Consumer<List<T>> batchProcessor) {List<T> batch = new ArrayList<>(batchSize);while (running) {try {// 等待新元素,但不会无限等待T item = queue.poll(timeout, TimeUnit.MILLISECONDS);if (item != null) {batch.add(item);}// 满足以下任一条件时处理批次:// 1. 达到批次大小// 2. 超时且批次非空if (batch.size() >= batchSize || (!batch.isEmpty() && item == null)) {processBatchSafely(batch, batchProcessor);batch.clear();}} catch (InterruptedException e) {Thread.currentThread().interrupt();break;}}// 处理剩余项if (!batch.isEmpty()) {processBatchSafely(batch, batchProcessor);}}private void processBatchSafely(List<T> batch, Consumer<List<T>> processor) {try {// 创建副本进行处理,避免处理过程中的修改processor.accept(new ArrayList<>(batch));} catch (Exception e) {// 错误处理逻辑log.error("Error processing batch", e);}}public void add(T item) {if (running) {queue.offer(item);}}public void shutdown() {running = false;}
}// 使用示例
ThreadSafeBatchProcessor<String> processor = new ThreadSafeBatchProcessor<>(100, 500);// 处理线程
new Thread(() -> {processor.process(batch -> {System.out.println("Processing batch of " + batch.size());// 处理批次});
}).start();// 添加数据
processor.add("item1");
processor.add("item2");
// ...// 关闭处理器
processor.shutdown();
线程安全遍历的最佳实践
-
选择合适的集合类型
- 读多写少:CopyOnWriteArrayList
- 高并发场景:ConcurrentHashMap
- 简单同步:Collections.synchronizedXXX
-
正确使用同步机制
- 使用synchronized块保护遍历过程
- 注意同步范围,避免过大或过小
-
避免常见陷阱
- 不要在遍历时修改集合
- 注意迭代器的线程安全性
- 合理处理并发异常
5.3 自定义遍历逻辑
有时标准的遍历方式可能无法满足特定需求,这时我们需要自定义遍历逻辑。
实现Iterable接口
public class PaginatedCollection<T> implements Iterable<T> {private final List<T> items;private final int pageSize;public PaginatedCollection(List<T> items, int pageSize) {this.items = new ArrayList<>(items);this.pageSize = pageSize;}@Overridepublic Iterator<T> iterator() {return new PaginatedIterator();}private class PaginatedIterator implements Iterator<T> {private int currentIndex = 0;@Overridepublic boolean hasNext() {return currentIndex < items.size();}@Overridepublic T next() {if (!hasNext()) {throw new NoSuchElementException();}return items.get(currentIndex++);}}// 分页遍历的辅助方法public List<List<T>> getPages() {List<List<T>> pages = new ArrayList<>();for (int i = 0; i < items.size(); i += pageSize) {pages.add(items.subList(i, Math.min(i + pageSize, items.size())));}return pages;}
}// 使用示例
List<String> items = Arrays.asList("A", "B", "C", "D", "E");
PaginatedCollection<String> paginated = new PaginatedCollection<>(items, 2);// 普通遍历
for (String item : paginated) {System.out.println(item);
}// 分页遍历
paginated.getPages().forEach(page -> {System.out.println("New page: " + page);
});
自定义Spliterator
public class ChunkedSpliterator<T> implements Spliterator<List<T>> {private final List<T> list;private final int chunkSize;private int currentPos = 0;public ChunkedSpliterator(List<T> list, int chunkSize) {this.list = list;this.chunkSize = chunkSize;}@Overridepublic boolean tryAdvance(Consumer<? super List<T>> action) {if (currentPos >= list.size()) {return false;}int end = Math.min(currentPos + chunkSize, list.size());action.accept(list.subList(currentPos, end));currentPos = end;return true;}@Overridepublic Spliterator<List<T>> trySplit() {return null; // 不支持并行}@Overridepublic long estimateSize() {return (list.size() + chunkSize - 1) / chunkSize;}@Overridepublic int characteristics() {return SIZED | ORDERED;}
}// 使用示例
List<Integer> numbers = IntStream.range(0, 100).boxed().collect(Collectors.toList());StreamSupport.stream(new ChunkedSpliterator<>(numbers, 10), false
).forEach(chunk -> {System.out.println("Processing chunk: " + chunk);
});
六、实用的第三方集合工具
在Java的日常开发中,虽然JDK提供的集合框架已经相当强大,但有时我们还是会遇到一些JDK不能优雅解决的场景。这时,一些优秀的第三方集合工具库就能派上用场。它们不仅能帮我们写出更简洁的代码,还能提供更好的性能。
6.1 Google Guava
Guava是Google开发的Java工具库,它提供了许多实用的集合工具类。使用Guava,我们可以写出更加简洁、高效的代码。
1. 分批处理工具
在实际开发中,我们经常需要对大量数据进行分批处理,比如批量查询数据库时防止IN子句过长。Guava的Lists.partition方法完美解决了这个问题:
// Lists.partition 进行分批处理
List<Long> allIds = Arrays.asList(1L, 2L, 3L, /* 很多ID */ 1000L);
Lists.partition(allIds, 500).stream().forEach(batch -> {// 每次处理500条数据List<User> users = userMapper.batchQuery(batch);processUsers(users);
});// 实际应用场景:批量查询数据库
public List<OrderDTO> batchQueryOrders(List<Long> orderIds) {List<OrderDTO> results = new ArrayList<>();// 防止IN子句过长,每500个ID一批Lists.partition(orderIds, 500).forEach(batchIds -> {List<OrderDTO> batchResults = orderMapper.queryByIds(batchIds);results.addAll(batchResults);});return results;
}
2. 不可变集合工具
在并发编程中,不可变集合是一个非常有用的工具。它可以防止集合被意外修改,提供了线程安全性:
// 创建不可变集合
ImmutableList<String> immutableList = ImmutableList.of("a", "b", "c");
ImmutableSet<String> immutableSet = ImmutableSet.copyOf(someSet);
ImmutableMap<String, Integer> immutableMap = ImmutableMap.builder().put("a", 1).put("b", 2).build();
使用不可变集合的好处:
- 线程安全,无需同步
- 防止集合被意外修改
- 可以用作常量
- 节省内存(因为不需要维护可修改的数据结构)
3. 集合工厂方法
Guava提供了更方便的集合创建方法,可以提高代码的可读性和性能:
// 创建ArrayList并初始化
List<String> list = Lists.newArrayListWithCapacity(100);// 创建HashSet并初始化
Set<String> set = Sets.newHashSetWithExpectedSize(100);// 笛卡尔积
Set<List<String>> product = Sets.cartesianProduct(ImmutableSet.of("a", "b"),ImmutableSet.of("1", "2")
);
6.2 Apache Commons Collections
Apache Commons Collections是Apache基金会的开源项目,它对JDK集合框架进行了扩展,提供了更多实用的数据结构和工具类。
1. CollectionUtils工具类
这个类提供了大量集合操作的工具方法,特别适合处理空集合和集合运算:
// 安全的集合操作
if (CollectionUtils.isEmpty(list)) {// 处理空集合
}// 集合运算
Collection<String> union = CollectionUtils.union(list1, list2);
Collection<String> intersection = CollectionUtils.intersection(list1, list2);
Collection<String> subtract = CollectionUtils.subtract(list1, list2);// 转换集合
Collection<Integer> collected = CollectionUtils.collect(stringList,String::length
);
2. 特殊集合实现
Commons Collections提供了一些特殊用途的集合实现:
// 双向Map
BidiMap<String, Integer> bidiMap = new DualHashBidiMap<>();
bidiMap.put("One", 1);
bidiMap.getKey(1); // 返回 "One"// 固定大小的集合
FixedSizeList<String> fixedList = FixedSizeList.fixedSizeList(new ArrayList<>(Arrays.asList("a", "b", "c"))
);
6.3 Eclipse Collections
Eclipse Collections是一个全面的集合框架,它提供了比JDK集合更丰富的API和更好的性能。特别适合处理原始类型数据:
// 丰富的集合API
MutableList<String> list = Lists.mutable.with("a", "b", "c");
MutableSet<String> set = Sets.mutable.with("a", "b", "c");// 原始类型集合,避免装箱拆箱
IntList intList = IntLists.mutable.with(1, 2, 3);
LongList longList = LongLists.mutable.with(1L, 2L, 3L);// 并行处理
list.asParallel(executorService, batchSize).select(each -> each.length() > 2).collect(String::toUpperCase);
6.4 实战最佳实践
在实际开发中,如何选择和使用这些工具呢?以下是一些实用的模板代码:
// 1. 大数据量分批处理模板
public <T, R> List<R> batchProcess(List<T> items,int batchSize,Function<List<T>, List<R>> processor) {List<R> results = new ArrayList<>();Lists.partition(items, batchSize).forEach(batch -> {try {List<R> batchResults = processor.apply(batch);results.addAll(batchResults);} catch (Exception e) {log.error("Batch processing error", e);// 异常处理策略}});return results;
}// 使用示例
List<UserDTO> users = batchProcess(userIds,500,ids -> userMapper.batchQuery(ids)
);// 2. 并发批处理模板
public <T, R> List<R> concurrentBatchProcess(List<T> items,int batchSize,Function<List<T>, List<R>> processor,ExecutorService executor) {List<CompletableFuture<List<R>>> futures = Lists.partition(items, batchSize).stream().map(batch -> CompletableFuture.supplyAsync(() -> processor.apply(batch),executor)).collect(Collectors.toList());return futures.stream().map(CompletableFuture::join).flatMap(List::stream).collect(Collectors.toList());
}// 使用示例
ExecutorService executor = Executors.newFixedThreadPool(4);
try {List<OrderDTO> orders = concurrentBatchProcess(orderIds,500,ids -> orderMapper.batchQuery(ids),executor);
} finally {executor.shutdown();
}
这些第三方工具不仅能提高开发效率,还能帮助我们写出更优雅、更高性能的代码。在选择工具时,需要考虑:
- 项目的性能需求
- 团队的技术栈
- 维护成本
- 学习曲线
- 社区活跃度
写在篇末:遍历之美
🌟 至此,我们共同探索了Java集合遍历的方方面面。从最基础的for循环到优雅的Stream API,从简单的单线程到复杂的并发处理,这些知识就像一颗颗珍珠,串成了完整的项链。
💭 记得在我刚开始编程时,也曾为选择合适的遍历方式而困惑。正如古人云:“工欲善其事,必先利其器”。希望这篇文章能为你打开一扇窗,让你在代码的海洋中航行得更加从容。
🌱 编程之道,如同园丁培育花草,既需要掌握技巧,也需要保持耐心。每一种遍历方式都有其存在的价值,关键是在恰当的场景选择恰当的方式。
🤝 如果这篇文章对你有所帮助,我很开心;如果你有任何想法或建议,欢迎在评论区与我交流。让我们在编程的道路上互相学习,共同进步。
📖 “千里之行,始于足下”。愿这篇文章能成为你掌握Java集合遍历的一个良好开始。
如果觉得有帮助的话,别忘了点个赞 👍 收藏 ⭐ 关注 🔖 哦!
🎯 我是果冻~,一个热爱技术、乐于分享的开发者
📚 更多精彩内容,请关注我的博客
🌟 我们下期再见!