Array,List,Set及Map遍历内容的方法探究

简介: Array,List,Set及Map遍历内容的方法探究

日常开发中我们经常遇到需要遍历集合内容的情况。有些是按照业务需求取得数据,存在特定的格式和要求。有些是为了调试的时候临时输出数据,对格式没有特定的要求。按照场合的不同可以选择恰当的遍历方法。

Array

数组是最基础的数据结构,最常见的打印方法就是遍历数组取得对应下标的元素,简单直接。简称它为"传统遍历法"

for (int i = 0; i < array.length; i++) { 
    System.out.println("array[" + i + "]:" + array[i]);
}

但是这种方法需要判断下标,存在下标越界的风险。JAVA为我们提供了更安全的方法,即"foreach法"。JVM将自行检查数组的边界,避免越界的可能。

for (int element : array) {
    System.out.println(element);
}

上述两种方法在打印元素的时候可以加入其它内容,或更改格式,或针对元素下标或者元素内容做业务处理,比如下标为2的时候改变元素值,元素内容为某值得时候不输出等等。


但是有时候我们既不想改格式,也没有针对某下标或某元素修改的需要,只想打印内容而已。这时候我们可以选择JDK提供的默认打印方法,简称为"工具类打印法"。这种打印方法简单快捷,将会以[x, x, ...]的固定格式输出。

System.out.println(Arrays.toString(array));

除了这三种方法还有一种间接的方法,将数组转为List后遍历,简称为"List转换法"

for (E e : Arrays.asList(array)) {
    System.out.println(e);
}
// 或直接将List作为参数输出
System.out.println(Arrays.asList(array));

将数组构建成List的时候内部将遍历一次数组,foreach或toString的时候又将遍历一次List,效率较为低下。


总结下这四种方法,如何选择。


如果只是为了打印数组内容,没有其他特别的需求,建议采用"工具类打印法"。


如果打印数组内容的时候有特定格式的需求,建议采用"foreach法"。


如果还需要针对下标做些判断,建议采用"传统遍历法"。


如果存在数组转为List的现有处理,可以采用采用"List转换法"。但要注意基本类型的数组不可以采用asList转为List,其他类型的数组可以转换,但转换后的List不可执行add和remove操作。具体原因在此不作展开。

List

List是集合框架中使用最为频繁的数据结构。打印List又有哪些方法?

有类似数组的"传统遍历法"

for (int i = 0; i < list.size(); i++) {
    System.out.println(list.get(i));
}

但是当List采用LinkedList实现的话,需要意识到该遍历方法的效率较低。因为其每次查询都需要在链表里遍历,时间复杂度为O(n),加上外部循环,总时间复杂度为O(n²)。即便LinkedList#get()针对遍历进行了优化,会先判断index处于size前半段还是后半段,前半段则从头部开始遍历,后半段则从尾部开始遍历。随着n的无限大,get()的时间复杂度仍趋近于O(n)。


当然也有效率较高的"foreach法"。

for (E e: list) {
    System.out.println(e);
}

Arraylist和LinkedList的父类AbstractCollection复写了toString()迭代元素以固定的"[x, x, ...]"格式输出集合内容。所以我们可以直接将其作为参数输出,简称为"直接打印法"

System.out.println(list);

我们可能会遇到在遍历List的时候需要删除某下标对应元素或某元素的需求。如果采用上面的"传统遍历法"遍历的时候删除元素的时候会导致size突变,极易发生IndexOutOfBoundsException。采用"foreach法"遍历的时候删除元素也不安全,因为取下个值的时候可能发生ConcurrentModificationException。原因在于迭代器取值的时候会判断内部暂存的modCount和List自己的modCount比较,两者不一致则抛出该异常。


而迭代器自己提供的remove函数在调用List的removeNode移除元素之后会将暂存的modCount同步,避免了ConcurrentModificationException的抛出。简称它为"迭代法"。

Iterator<E> elementIterator = list.iterator();
while (elementIterator.hasNext()) {
    E e = elementIterator.next();
    if (...) {
        elementIterator.remove();
    }
}

前面讲述数组的时候谈及了将Array转为List当做List打印的"List转换法",那么打印List自然也有转为Array当做数组打印的思路。简称为"Array转换法"。toArray()执行的时候将迭代一遍List,遍历数组的时候又将遍历一次,比较浪费性能。

E[] array = list.toArray();
// 传统遍历数组
for (int i = 0; i < array.length; i++) {
    System.out.println(array[i]);
}
// foreach法遍历数组
for (E e : array) {
    System.out.println(e);
}

JAVA8推出了Lamba表达式和方法引用,利用该方法可以更简便地打印内容。简称为"Lamba遍历法"。Lamba表达式也是语法糖,用来起来简单方便,但是需要了解其原理。

// ① Lamba方式
// Lamba方式
list.forEach(item->System.out.println(item));
// ② 方法引用方式
list.forEach(System.out::println);
// 或
list.stream().forEach(System.out::println);

总结以上六种方法,如何选择。


如果只是单纯地打印内容采用"直接打印法"即可。


如果需要下标信息或者做些特殊处理,建议采用"传统遍历法",但注意LinkedList的时候避免采用该方案。


如果不需要下标信息但不希望以固定格式输出,建议采用"foreach法"。


如果在遍历的时候需要移除元素,必须采用"迭代法"。


如果现有代码里已经存在List转换为数组的处理的话,可以采用数组的打印方法,即"Array转换法"。


如果JAVA的版本是1.8及以上,可以试试"Lamba遍历法"。

Set

Set和List的区别在于Set内的元素是无序的,不重复的。不像List内元素的顺序是按照写入的顺序排放的,它的元素内容即使重复也不会覆盖而是继续在后面追加。结构特点都是类似的,所以遍历方法也差不多。

最常见的是"foreach法"打印其内容。

for (E e : set) {
    System.out.println(e);
}

和List一样为了避免ConcurrentModificationException的发生,还有"迭代器法"

Iterator<E> elementIterator = set.iterator();
while (elementIterator.hasNext()) {
    E e = elementIterator.next();
    if (...) {
        elementIterator.remove();
    }
}

抽象实现类也是AbstractCollection,所以也能当做参数直接打印。"直接打印法"

System.out.println(set);

并且Set也提供了转换为数组的方法,即可采用"Array转换法"

for (E e : set.toArray()) {
    System.out.println(e);
}

Set的遍历方法就不作总结了,选择的基本策略等同于List。

Map

Map并非继承自Collection接口,而是键值对的形式。其数据内容和List以及Set存在差异。在遍历方法上有其特有的方法,也有些类似的方法。


类似的地方在于其抽象实现类AbstractMap也复写了toString(),将以固定的{xx=yy, xx=yy, ...}格式打印Map内容。简称为"直接打印法"。

System.out.println(map);

因为是键值对的数据形式,所以Map不会提供默认的迭代器。进而无法直接采用foreach的方法去打印Map。但是Map针对key,value及键值对entry这三种输出内容分别提供了对应的集合实例供外部打印。


通过keySet()可以得到针对key的集合实例。进而采用foreach法或迭代法,简称为"key迭代法"。

Set<K> keySet = map.keySet(); 
for (K k : keySet) {
    System.out.println(k);
}

采用迭代写法可以像List和Set一样避免ConcurrentModificationException。

Iterator<K> keyIterator = map.keySet().iterator();
while (keyIterator.hasNext()) {
    K k = keyIterator.next();
    if (...) {
        keyIterator.remove();
    }
}

当利用keySet遍历Map的时候需要注意不要在拿到Key之后通过Map#get(Key)再取得Value。


一:因为HashMap等实现类的hash算法散列性不够的时候hash碰撞较为严重,导致get操作不再是数组结构的直接取值而是链表的遍历。效率会下降。


二:如果Map为LinkedHashMap实现的话,并且采用的是按访问顺序组织节点的话,在get的时候会导致节点顺序发生改变,进而导致下次取节点的时候发生ConcurrentModificationException。


keySet()可以拿到key列表以外还可以通过values()拿到value列表。简称为"value迭代法"。

Collection<V> valueCollection = map.values();
for (V v : valueCollection) {
    System.out.println(v);
}

还有避免抛出ConcurrentModificationException的迭代器法。

Iterator<V> valueIterator = map.values().iterator();
while (valueIterator.hasNext()) {
    V v = valueIterator.next();
    if (...) {
        valueIterator.remove();
    }
}

单单拿到key或value还不够,我们还需要能同时拿到它们。Map内部接口Entry就是声明了如何拿到key和value属性的接口。而通过entrySet()我们可以拿到各实现类实现的entry列表。简称为"entry迭代法"

Set<Map.Entry<K, V>> entrySet = map.entrySet();
for (Map.Entry<K, V> entry : entrySet) {
    System.out.println(entry.getKey() + entry.getValue());
}

同样的在得到各节点的时候便于删除节点的迭代器法。

Iterator<Map.Entry<K, V>> entryIterator = map.entrySet().iterator(); 
while (entryIterator.hasNext()) { 
    Map.Entry<K, V> entry = entryIterator.next(); 
    if (...) {
        entryIterator.remove();
    }
}

总结下如何选择上述打印方法。

单纯地查看内容,当然是"直接打印法"

如果只需要key列表,采用"key迭代法"。

同样的只需要value列表的化,采用"value迭代法"

两者都需要的话,采用"entry迭代法"

大家可以根据打印的具体需要选择恰当的打印方法,同时考虑一些注意点,避免打印的时候发生意想不到的错误。


相关文章
|
10月前
|
存储 JavaScript 前端开发
Set中的add()方法和数组的push()方法有什么区别?
Set中的add()方法和数组的push()方法有什么区别?
530 122
|
10月前
|
存储 安全 JavaScript
如何使用Set的delete()方法删除元素?
如何使用Set的delete()方法删除元素?
731 122
|
10月前
|
存储 安全 JavaScript
如何使用Set的add()方法添加元素?
如何使用Set的add()方法添加元素?
794 58
|
9月前
|
JavaScript 前端开发
如何使用Set的delete()方法删除Set中的所有元素?
如何使用Set的delete()方法删除Set中的所有元素?
425 1
|
11月前
|
安全 Java API
【Java性能优化】Map.merge()方法:告别繁琐判空,3行代码搞定统计累加!
在日常开发中,我们经常需要对Map中的值进行累加统计。}else{代码冗长,重复调用get()方法需要显式处理null值非原子操作,多线程下不安全今天要介绍的方法,可以让你用一行代码优雅解决所有这些问题!方法的基本用法和优势与传统写法的对比分析多线程安全版本的实现Stream API的终极优化方案底层实现原理和性能优化建议一句话总结是Java 8为我们提供的Map操作利器,能让你的统计代码更简洁、更安全、更高效!// 合并两个列表});简单累加。
1040 0
|
JavaScript 前端开发 API
JavaScript中通过array.map()实现数据转换、创建派生数组、异步数据流处理、复杂API请求、DOM操作、搜索和过滤等,array.map()的使用详解(附实际应用代码)
array.map()可以用来数据转换、创建派生数组、应用函数、链式调用、异步数据流处理、复杂API请求梳理、提供DOM操作、用来搜索和过滤等,比for好用太多了,主要是写法简单,并且非常直观,并且能提升代码的可读性,也就提升了Long Term代码的可维护性。 只有锻炼思维才能可持续地解决问题,只有思维才是真正值得学习和分享的核心要素。如果这篇博客能给您带来一点帮助,麻烦您点个赞支持一下,还可以收藏起来以备不时之需,有疑问和错误欢迎在评论区指出~
|
JavaScript 前端开发 Java
深入理解 JavaScript 中的 Array.find() 方法:原理、性能优势与实用案例详解
Array.find() 是 JavaScript 数组方法中一个非常实用和强大的工具。它不仅提供了简洁的查找操作,还具有性能上的独特优势:返回的引用能够直接影响原数组的数据内容,使得数据更新更加高效。通过各种场景的展示,我们可以看到 Array.find() 在更新、条件查找和嵌套结构查找等场景中的广泛应用。 在实际开发中,掌握 Array.find() 的特性和使用技巧,可以让代码更加简洁高效,特别是在需要直接修改原数据内容的情形。 只有锻炼思维才能可持续地解决问题,只有思维才是真正值得学习和分享的核心要素。如果这篇博客能给您带来一点帮助,麻烦您点个赞支持一
|
移动开发 运维 供应链
通过array.some()实现权限检查、表单验证、库存管理、内容审查和数据处理;js数组元素检查的方法,some()的使用详解,array.some与array.every的区别(附实际应用代码)
array.some()可以用来权限检查、表单验证、库存管理、内容审查和数据处理等数据校验工作,核心在于利用其短路机制,速度更快,节约性能。 博客不应该只有代码和解决方案,重点应该在于给出解决方案的同时分享思维模式,只有思维才能可持续地解决问题,只有思维才是真正值得学习和分享的核心要素。如果这篇博客能给您带来一点帮助,麻烦您点个赞支持一下,还可以收藏起来以备不时之需,有疑问和错误欢迎在评论区指出~
|
供应链 JavaScript 前端开发
通过array.every()实现数据验证、权限检查和一致性检查;js数组元素检查的方法,every()的使用详解,array.some与array.every的区别(附实际应用代码)
array.every()可以用来数据验证、权限检查、一致性检查等数据校验工作,核心在于利用其短路机制,速度更快,节约性能。 博客不应该只有代码和解决方案,重点应该在于给出解决方案的同时分享思维模式,只有思维才能可持续地解决问题,只有思维才是真正值得学习和分享的核心要素。如果这篇博客能给您带来一点帮助,麻烦您点个赞支持一下,还可以收藏起来以备不时之需,有疑问和错误欢迎在评论区指出~
|
JSON 前端开发 API
多维数组操作,不要再用遍历循环foreach了!来试试数组展平的小妙招!array.flat()用法与array.flatMap() 用法及二者差异详解
理论上array.flat()能做的事情,array.flatMap()都可以做,但是array.flat()更简单,占用内存更少,执行更快。 这个相对冷门一些,w3school上都没有相关教程,看到就是赚到,收藏就是财富! 博客不应该只有代码和解决方案,重点应该在于给出解决方案的同时分享思维模式,只有思维才能可持续地解决问题,只有思维才是真正值得学习和分享的核心要素。如果这篇博客能给您带来一点帮助
下一篇
开通oss服务