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个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

TechMerger

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值