日常开发中我们经常遇到需要遍历集合内容的情况。有些是按照业务需求取得数据,存在特定的格式和要求。有些是为了调试的时候临时输出数据,对格式没有特定的要求。按照场合的不同可以选择恰当的遍历方法。
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迭代法"。
大家可以根据打印的具体需要选择恰当的打印方法,同时考虑一些注意点,避免打印的时候发生意想不到的错误。