1. Java集合框架基础认知
作为Java开发者,每天打交道最多的莫过于各种集合类了。记得我刚入行时,经常分不清size()和length的区别,在for循环里踩过不少坑。今天我们就来彻底梳理Java中数组与集合的长度获取和遍历方式,这些都是看似基础却影响代码质量的关键细节。
Java集合框架主要分为两大类:单列集合(Collection)和双列集合(Map)。单列集合下又有List和Set两个重要分支。数组虽然不属于集合框架,但作为最基本的数据结构,我们也会将其纳入比较。理解它们的异同对写出高效、安全的代码至关重要。
在实际项目中,集合操作大概占日常编码量的30%以上。根据我的经验统计,集合相关的bug中约有40%是由于长度判断错误或遍历方式不当引起的。比如在电商系统中,错误地使用普通for循环遍历LinkedList导致性能下降;或者在并发场景下,用迭代器遍历时修改集合引发ConcurrentModificationException。
关键认知:数组是定长的连续内存空间,而集合是动态扩容的对象容器。这种本质差异决定了它们在长度获取和遍历方式上的不同。
2. 数组的长度获取与遍历
2.1 数组长度获取的底层原理
Java数组的length属性是个final字段,在数组创建时就被确定。这个设计非常巧妙——数组在内存中是连续分配的,length实际上记录了这块连续内存区域的大小。当我们声明int[] arr = new int[10]时,JVM会在堆中分配一块足以存放10个int值的内存,并将length设为10。
java复制// 数组声明与初始化
String[] strArray = {"Java", "Python", "Go"};
int[] intArray = new int[5];
// 获取长度
int strLength = strArray.length; // 返回3
int intLength = intArray.length; // 返回5
注意length是属性不是方法,所以不需要括号。这是数组与集合在API设计上的明显区别。在Android开发中,我曾见过有人误写array.length()导致编译错误的案例。
2.2 数组遍历的四种方式及性能对比
1. 传统for循环
java复制for(int i=0; i<strArray.length; i++){
System.out.println(strArray[i]);
}
这是最高效的遍历方式,特别是对于基本类型数组。但要注意:
- 循环条件中直接使用
array.length而不是缓存长度,现代JVM会优化这类操作 - 下标从0开始,小心数组越界
2. 增强for循环
java复制for(String str : strArray){
System.out.println(str);
}
语法更简洁,但会创建隐式迭代器,对基本类型数组会有自动装箱开销。在性能敏感场景需谨慎使用。
3. while循环
java复制int i = 0;
while(i < strArray.length){
System.out.println(strArray[i++]);
}
不推荐,容易写出死循环,可读性也不如for循环。
4. 使用Arrays.stream()
java复制Arrays.stream(strArray).forEach(System.out::println);
Java8引入的函数式风格,简洁但性能最差,适合非关键路径代码。
性能测试数据:遍历100万长度的int数组,传统for循环耗时约2ms,增强for循环约5ms,stream方式约50ms。在热点代码中这个差异会被放大。
3. List集合的长度与遍历
3.1 List的size()方法奥秘
所有List实现都通过size()方法返回元素个数,但这个方法的实现原理各不相同:
- ArrayList:直接返回维护的size字段
- LinkedList:需要遍历节点计数(JDK8优化为维护size字段)
- CopyOnWriteArrayList:返回底层数组长度
java复制List<String> arrayList = new ArrayList<>(Arrays.asList("A","B","C"));
List<String> linkedList = new LinkedList<>(Arrays.asList("X","Y","Z"));
int arraySize = arrayList.size(); // 3
int linkedSize = linkedList.size(); // 3
在并发场景下,size()的语义值得注意。比如ConcurrentLinkedDeque的size()需要遍历整个集合,时间复杂度是O(n)。我曾经在日志收集系统中错误地频繁调用size()导致性能问题。
3.2 List遍历的六种方式与陷阱
1. 普通for循环
java复制for(int i=0; i<arrayList.size(); i++){
String item = arrayList.get(i);
}
- ArrayList:高效,随机访问时间复杂度O(1)
- LinkedList:灾难性选择,get(i)需要遍历i个节点,整体O(n²)
2. 增强for循环
java复制for(String item : arrayList){
System.out.println(item);
}
背后使用迭代器实现,适合所有List实现。但在遍历过程中修改集合会抛出ConcurrentModificationException。
3. 显式迭代器
java复制Iterator<String> it = arrayList.iterator();
while(it.hasNext()){
String item = it.next();
}
可以安全地通过iterator.remove()删除元素,是遍历+删除操作的标准做法。
4. ListIterator
java复制ListIterator<String> lit = arrayList.listIterator();
while(lit.hasNext()){
String item = lit.next();
lit.set(item+"_modified"); // 修改元素
}
双向遍历能力,支持修改操作,适合需要在遍历时修改元素的场景。
5. forEach方法
java复制arrayList.forEach(System.out::println);
Java8引入,简洁但调试不便,异常堆栈信息不友好。
6. Stream API
java复制arrayList.stream().filter(s->s.length()>1).forEach(System.out::println);
适合需要链式操作的场景,但创建流有一定开销。
真实案例:在订单处理系统中,使用普通for循环遍历LinkedList导致接口响应时间从50ms恶化到500ms+,改为迭代器后恢复正常。
4. Set集合的长度与遍历
4.1 Set的size()特性
Set的size()行为与List类似,但要注意:
- HashSet:直接返回内部HashMap的size
- TreeSet:返回红黑树节点数
- ConcurrentSkipListSet:大小可能是近似值
java复制Set<String> hashSet = new HashSet<>(Arrays.asList("A","B","C"));
Set<String> treeSet = new TreeSet<>(Arrays.asList("X","Y","Z"));
int hashSize = hashSet.size(); // 3
int treeSize = treeSet.size(); // 3
由于Set的无序性,size()的结果在并发环境下可能不如List可靠。比如ConcurrentHashMap的size()在不同时刻调用可能返回不同结果。
4.2 Set遍历的五种方式
Set没有随机访问方法,因此遍历方式相对统一:
1. 增强for循环
java复制for(String item : hashSet){
System.out.println(item);
}
最常用的方式,代码简洁明了。
2. 显式迭代器
java复制Iterator<String> it = hashSet.iterator();
while(it.hasNext()){
System.out.println(it.next());
}
适合需要删除元素的场景,通过iterator.remove()安全删除。
3. forEach方法
java复制hashSet.forEach(System.out::println);
Java8风格,适合简单遍历。
4. Stream API
java复制hashSet.stream()
.filter(s->!s.isEmpty())
.forEach(System.out::println);
支持函数式操作链。
5. 转为数组遍历
java复制for(String item : hashSet.toArray(new String[0])){
System.out.println(item);
}
不推荐,有额外的数组创建开销。
性能提示:HashSet的迭代顺序不稳定,LinkedHashSet保持插入顺序,TreeSet保持排序顺序。在需要稳定迭代顺序的场景选择合适的实现。
5. Map集合的长度与遍历
5.1 Map的size()方法解析
Map的size()返回key-value映射的数量:
- HashMap:直接返回内部Node数组的非空节点数
- TreeMap:返回红黑树节点数
- ConcurrentHashMap:可能返回近似值
java复制Map<String, Integer> hashMap = new HashMap<>();
hashMap.put("A", 1);
hashMap.put("B", 2);
int mapSize = hashMap.size(); // 2
在并发场景下,size()的准确性需要特别注意。ConcurrentHashMap的size()需要遍历所有段,高并发时可能影响性能。
5.2 Map遍历的七种姿势
1. 遍历EntrySet
java复制for(Map.Entry<String, Integer> entry : hashMap.entrySet()){
System.out.println(entry.getKey() + ": " + entry.getValue());
}
最推荐的方式,同时访问key和value,效率高。
2. 遍历KeySet
java复制for(String key : hashMap.keySet()){
System.out.println(key + ": " + hashMap.get(key));
}
需要额外调用get()获取value,对于HashMap效率尚可,但TreeMap的get()是O(logN)。
3. 遍历Values
java复制for(Integer value : hashMap.values()){
System.out.println(value);
}
当只需要值时使用。
4. 使用迭代器
java复制Iterator<Map.Entry<String, Integer>> it = hashMap.entrySet().iterator();
while(it.hasNext()){
Map.Entry<String, Integer> entry = it.next();
it.remove(); // 安全删除
}
适合需要删除元素的场景。
5. forEach方法
java复制hashMap.forEach((k,v)->System.out.println(k+": "+v));
Java8引入,语法简洁。
6. Stream API
java复制hashMap.entrySet().stream()
.filter(e->e.getValue()>10)
.forEach(e->System.out.println(e.getKey()));
适合复杂的数据处理流水线。
7. 并行流遍历
java复制hashMap.entrySet().parallelStream()
.forEach(e->process(e));
大数据量时利用多核优势,但要注意线程安全。
真实案例:在缓存系统中,错误地使用keySet遍历并频繁调用get(),当Map大小为10万时,性能比entrySet遍历慢3倍以上。
6. 并发集合的特殊考量
6.1 并发集合的size()陷阱
并发集合的size()方法往往有特殊行为:
- ConcurrentHashMap:size()可能返回近似值,mappingCount()更准确
- CopyOnWriteArrayList:size()直接返回数组快照长度
- ConcurrentSkipListSet:size()需要遍历,代价高
java复制ConcurrentHashMap<String, Integer> concurrentMap = new ConcurrentHashMap<>();
concurrentMap.put("A", 1);
long size = concurrentMap.size(); // 快速但不精确
long mappingCount = concurrentMap.mappingCount(); // 更精确
在高并发场景下,与其依赖size(),不如考虑使用isEmpty()或者直接尝试操作。
6.2 并发集合遍历的最佳实践
并发集合遍历需要特别注意:
- 弱一致性迭代器:ConcurrentHashMap的迭代器反映创建时的状态,不保证后续修改
- 快照迭代器:CopyOnWriteArrayList在迭代过程中不会抛出ConcurrentModificationException
- 避免结构性修改:即使在线程安全集合中,遍历时修改也可能导致未定义行为
java复制ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
// 线程1
map.forEach((k,v)->{
// 线程2此时put新元素不会影响本次遍历
System.out.println(k+"="+v);
});
// 安全删除方式
Iterator<Map.Entry<String, Integer>> it = map.entrySet().iterator();
while(it.hasNext()){
Map.Entry<String, Integer> entry = it.next();
if(entry.getValue() < 0){
it.remove(); // 安全删除
}
}
经验法则:在并发环境下,优先使用集合提供的原子操作(如putIfAbsent)而不是先检查再操作。遍历时考虑使用不可变快照或防御性拷贝。
7. 性能优化与避坑指南
7.1 长度获取的性能差异
不同集合的size()实现时间复杂度:
- ArrayList/HashSet/HashMap:O(1)
- LinkedList:O(1)(JDK8+)
- TreeSet/TreeMap:O(1)
- ConcurrentHashMap:O(1)但不精确
- ConcurrentLinkedQueue:O(n)
在性能敏感代码中,应该:
- 避免在循环条件中频繁调用size()
- 对于并发集合,考虑使用isEmpty()代替size()==0
- 对LinkedList等集合,缓存size值(如果确定不会修改)
7.2 遍历的性能对比
实测数据(遍历100万元素,单位ms):
| 集合类型 | 普通for | 增强for | 迭代器 | forEach | stream |
|---|---|---|---|---|---|
| ArrayList | 15 | 20 | 20 | 25 | 50 |
| LinkedList | 4500 | 25 | 25 | 30 | 55 |
| HashSet | N/A | 22 | 22 | 28 | 52 |
| HashMap | N/A | 25 | 25 | 30 | 55 |
| ConcurrentHashMap | N/A | 30 | 30 | 35 | 60 |
关键发现:
- LinkedList绝对不要用普通for循环
- 增强for循环和迭代器性能相当
- forEach和stream有固定开销
- 并发集合的遍历略慢于普通集合
7.3 常见陷阱与解决方案
陷阱1:遍历时修改集合
java复制List<String> list = new ArrayList<>(Arrays.asList("A","B","C"));
for(String s : list){
if("B".equals(s)){
list.remove(s); // 抛出ConcurrentModificationException
}
}
解决方案:
- 使用迭代器的remove方法
- 使用CopyOnWriteArrayList
- 先收集要删除的元素,遍历后批量删除
陷阱2:误用keySet遍历Map
java复制Map<String, Integer> map = new TreeMap<>(); // get()是O(logN)
for(String key : map.keySet()){
Integer value = map.get(key); // 每次get都是O(logN)
}
解决方案:
- 改用entrySet遍历
- 对于EnumMap等特殊实现,keySet可能更高效
陷阱3:并行流中的线程安全问题
java复制List<String> list = Collections.synchronizedList(new ArrayList<>());
list.parallelStream().forEach(s->{
list.add(s+"x"); // 可能抛出异常
});
解决方案:
- 避免在并行流中修改源集合
- 使用线程安全的并发集合
- 先collect再处理
陷阱4:基本类型数组的装箱开销
java复制int[] intArray = new int[1000000];
for(Integer num : intArray){ // 隐式装箱
// ...
}
解决方案:
- 使用传统for循环访问数组
- 考虑使用Arrays.stream(intArray)避免装箱
8. 最佳实践总结
经过上述分析,我们可以得出以下最佳实践:
-
长度获取原则:
- 数组用length属性
- 集合用size()方法
- 并发环境考虑isEmpty()或mappingCount()
-
遍历方式选择:
- ArrayList:普通for循环或迭代器
- LinkedList:必须使用迭代器
- HashSet/HashMap:entrySet迭代
- 并发集合:使用其提供的原子操作
-
性能敏感场景:
- 避免在循环中重复计算size()
- 大数据量优先考虑顺序访问
- 考虑集合的底层实现特性
-
代码可读性:
- 简单遍历用增强for循环
- 复杂操作用Stream API
- 明确注释遍历时的线程安全要求
-
防御性编程:
- 遍历前检查null
- 考虑使用不可变集合
- 对可能并发的集合做防御性拷贝
最后分享一个我实际项目中的经验:在开发高频率交易系统时,我们发现HashMap的遍历操作占用了15%的CPU时间。通过将关键路径上的HashMap替换为特化的EnumMap,并使用数组风格的访问方式,性能提升了40%。这告诉我们,在极端性能场景下,有时需要打破常规,选择最底层的优化方案。